如何在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
相关产品推荐
相关产品推荐

