You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Web、iOS、Android端为TextInput选中文本应用粗斜体等样式?

实现自定义文本编辑器的粗体/斜体/下划线样式应用

要给选中内容应用样式,核心是用富文本片段数组存储带样式的文本块,而非纯字符串,再结合选中范围修改对应片段的样式,最后渲染成带样式的Text组件。以下是具体实现步骤:

1. 重构数据存储结构

放弃纯文本状态,改用包含样式标记的富文本片段数组,每个片段记录文本内容和样式开关:

const [richText, setRichText] = useState([
  { text: '', styles: { bold: false, italic: false, underline: false } }
]);
// 统一维护跨平台选中范围(格式为 { start, end })
const [selection, setSelection] = useState({ start: 0, end: 0 });

2. 统一跨平台选中范围处理

把Web和Native的选中结果转换成一致的索引范围格式:

// 更新Native端的选中处理逻辑
onSelectionChange={(e) => {
  const nativeSelection = e.nativeEvent.selection;
  const unifiedSelection = {
    start: nativeSelection.start,
    end: nativeSelection.end
  };
  setSelection(unifiedSelection);
  if (setCursorPosition) setCursorPosition(unifiedSelection);
}}

// 封装统一的选中范围获取函数
const getCurrentSelection = () => {
  if (Platform.OS === 'web') {
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return { start: 0, end: 0 };
    const range = sel.getRangeAt(0);
    const inputEl = textInput.current;
    if (!inputEl) return { start: 0, end: 0 };
    
    // 计算选中内容在文本中的索引位置
    const preRange = document.createRange();
    preRange.selectNodeContents(inputEl);
    preRange.setEnd(range.startContainer, range.startOffset);
    const start = preRange.toString().length;
    preRange.setEnd(range.endContainer, range.endOffset);
    const end = preRange.toString().length;
    return { start, end };
  } else {
    return selection;
  }
};

3. 实现样式切换逻辑(以粗体为例,斜体/下划线同理)

核心逻辑是拆分选中范围覆盖的富文本片段,切换对应片段的目标样式:

const toggleStyle = (styleType) => {
  const { start, end } = getCurrentSelection();
  if (start === end) return; // 无选中内容直接返回

  let newRichText = [...richText];
  let processedStart = 0;

  // 遍历片段,定位选中范围对应的部分
  for (let i = 0; i < newRichText.length; i++) {
    const fragment = newRichText[i];
    const fragmentEnd = processedStart + fragment.text.length;

    // 片段完全在选中范围外,跳过
    if (fragmentEnd <= start || processedStart >= end) {
      processedStart = fragmentEnd;
      continue;
    }

    // 计算当前片段与选中范围的交集
    const fragSelStart = Math.max(start - processedStart, 0);
    const fragSelEnd = Math.min(end - processedStart, fragment.text.length);

    // 拆分出选中前的部分(如果存在)
    if (fragSelStart > 0) {
      newRichText.splice(i, 1, {
        text: fragment.text.slice(0, fragSelStart),
        styles: { ...fragment.styles }
      });
      i++;
      processedStart += fragSelStart;
    }

    // 处理选中部分:切换目标样式
    const selectedText = fragment.text.slice(fragSelStart, fragSelEnd);
    newRichText.splice(i, 1, {
      text: selectedText,
      styles: { ...fragment.styles, [styleType]: !fragment.styles[styleType] }
    });
    i++;
    processedStart += selectedText.length;

    // 拆分出选中后的部分(如果存在)
    if (fragSelEnd < fragment.text.length) {
      newRichText.splice(i, 0, {
        text: fragment.text.slice(fragSelEnd),
        styles: { ...fragment.styles }
      });
    }

    break;
  }

  setRichText(newRichText);
};

// 绑定按钮事件
const onBold = () => toggleStyle('bold');
const onItalic = () => toggleStyle('italic');
const onUnderline = () => toggleStyle('underline');

4. 渲染富文本到TextInput

把富文本片段数组转换成嵌套的Text组件,每个组件应用对应样式:

const highlightedText = richText.map((fragment, index) => {
  const textStyles = [];
  if (fragment.styles.bold) textStyles.push({ fontWeight: 'bold' });
  if (fragment.styles.italic) textStyles.push({ fontStyle: 'italic' });
  if (fragment.styles.underline) textStyles.push({ textDecorationLine: 'underline' });

  return (
    <Text key={index} style={textStyles}>
      {fragment.text}
    </Text>
  );
});

// 渲染TextInput(替换原highlightedText并处理输入逻辑)
<TextInput
  onChangeText={(text) => {
    // 输入新内容时,继承光标位置的样式
    const { start } = selection;
    let newRichText = [...richText];
    let processedLength = 0;

    for (let i = 0; i < newRichText.length; i++) {
      processedLength += newRichText[i].text.length;
      if (processedLength >= start) {
        const prevLen = processedLength - newRichText[i].text.length;
        const prevText = newRichText[i].text.slice(0, start - prevLen);
        const nextText = newRichText[i].text.slice(start - prevLen);
        
        newRichText.splice(i, 1);
        if (prevText) {
          newRichText.splice(i, 0, { text: prevText, styles: { ...newRichText[i]?.styles || { bold: false, italic: false, underline: false } } });
          i++;
        }
        newRichText.splice(i, 0, { text, styles: { ...newRichText[i-1]?.styles || { bold: false, italic: false, underline: false } } });
        if (nextText) {
          newRichText.splice(i+1, 0, { text: nextText, styles: { ...newRichText[i-1]?.styles || { bold: false, italic: false, underline: false } } });
        }
        break;
      }
    }
    setRichText(newRichText);
  }}
  style={[S.textInputStyle, textInputStyle]}
  ref={textInput}
  multiline
  onSelectionChange={(e) => {
    const nativeSelection = e.nativeEvent.selection;
    const unifiedSelection = {
      start: nativeSelection.start,
      end: nativeSelection.end
    };
    setSelection(unifiedSelection);
    if (setCursorPosition) setCursorPosition(unifiedSelection);
  }}
  selectTextOnFocus
  autoCapitalize='none'
  autoCorrect={false}
  onFocus={() => setFocused(true)}
  {...props}
>
  {highlightedText}
</TextInput>

关键说明

  • 富文本片段拆分是核心:确保选中区域被单独分离,样式修改仅影响目标部分,不破坏其他文本的样式。
  • Web端需要通过Range API将DOM选中转换为文本索引,保证与Native端逻辑一致。
  • 输入新内容时继承光标位置的样式,维持编辑体验的连贯性。

内容的提问来源于stack exchange,提问作者JAVEDPATHAN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 04:15:27