React实现受限输入框:30字符换行、不可编辑段及光标跳转修复
问题描述
需要开发支持以下特性的文本输入域:
- 展示后端返回的默认文本,默认文本中的指定片段不可编辑
- 单行输入字符数达到30时自动按词拆分换行
现有实现存在明确缺陷:触发自动换行逻辑时,输入光标会直接跳转到全部文本末尾,无法在当前输入位置继续正常编辑。
现有实现代码:
import React, { useEffect } from "react"; import styles from "./TextEditor.module.css"; const TextEditor = (props) => { var limit = 80000; //height limit let defaultvalueArr = props.data.variables.map((vari, index) => { return vari.texts.map((p, index) => { return p; }); }); useEffect(() => { const box = document.getElementById(props.title); var charlimit = 30; // char limit per line var space; box.onkeyup = function () { var lines = box.value.split("\n"); console.log(box.value, "box.value"); for (var i = 0; i < lines.length; i++) { if (lines[i].length <= charlimit) continue; var j = 0; space = charlimit; while (j++ <= charlimit) { if (lines[i].charAt(j) === " ") space = j; } lines[i + 1] = lines[i].substring(space + 1) + (lines[i + 1] || ""); lines[i] = lines[i].substring(0, space); } box.value = lines.slice(0, 500).join("\n"); }; box.oninput = function() { box.style.height = ""; box.style.height = Math.min(box.scrollHeight, limit) + "px"; }; box.value = defaultvalueArr.join("\n"); }, []); return ( <div className={styles.TextEditor}> <textarea id={props.title} className={styles.TextArea} // rows={defaultvalueArr[0].length} ></textarea> </div> ); }; export default TextEditor;
实现效果参考:
问题根因
直接修改textarea的value属性时,浏览器默认行为是将输入光标重置到文本末尾,原有代码在重新赋值后没有主动恢复光标位置,因此出现跳转问题。此外原有实现直接操作DOM、没有做不可编辑片段的逻辑校验,不符合React开发规范也没覆盖全部需求。
修复方案
- 改用React受控组件模式管理输入值,通过ref获取textarea实例操作光标
- 执行自动换行逻辑前先记录当前光标位置,处理完文本后计算换行带来的偏移量,重新赋值后主动将光标设置到正确位置
- 对不可编辑片段增加标识(可和后端约定用
{{变量名}}格式标记受保护内容),输入事件中校验光标位置,禁止修改受保护片段
修复后的核心代码参考:
import React, { useState, useRef, useEffect } from "react"; import styles from "./TextEditor.module.css"; const CHAR_LIMIT_PER_LINE = 30; const MAX_HEIGHT = 80000; // 匹配不可编辑片段的正则,可根据后端返回规则调整 const LOCKED_PATTERN = /\{\{.*?\}\}/g; const TextEditor = (props) => { const textareaRef = useRef(null); // 初始化默认值 const initValue = props.data.variables .map(vari => vari.texts.join("\n")) .join("\n"); const [value, setValue] = useState(initValue); // 自动换行处理逻辑 const handleAutoWrap = (text, cursorPos) => { const lines = text.split("\n"); let newCursorPos = cursorPos; for (let i = 0; i < lines.length; i++) { if (lines[i].length <= CHAR_LIMIT_PER_LINE) continue; // 找最近的空格拆分,没有空格就硬拆30字符 let splitPos = CHAR_LIMIT_PER_LINE; for (let j = CHAR_LIMIT_PER_LINE; j >=0; j--) { if (lines[i][j] === " ") { splitPos = j; break; } } // 计算光标偏移:如果光标在拆分位置之后,要加1(因为多了一个换行符) const lineStart = lines.slice(0, i).join("\n").length + (i > 0 ? 1 : 0); if (cursorPos > lineStart + splitPos) { newCursorPos += 1; } lines[i + 1] = lines[i].substring(splitPos + 1) + (lines[i + 1] || ""); lines[i] = lines[i].substring(0, splitPos); } return { newText: lines.slice(0, 500).join("\n"), newCursorPos }; }; // 校验不可编辑片段是否被修改 const checkLockedContent = (oldText, newText) => { const oldLocked = oldText.match(LOCKED_PATTERN) || []; const newLocked = newText.match(LOCKED_PATTERN) || []; // 基础校验:不可编辑片段数量和内容不能变,复杂场景可以逐段校验位置 return JSON.stringify(oldLocked) === JSON.stringify(newLocked); }; const handleInput = (e) => { const textarea = textareaRef.current; const oldValue = value; const cursorPos = textarea.selectionStart; let newValue = e.target.value; // 先校验不可编辑内容 if (!checkLockedContent(oldValue, newValue)) { setValue(oldValue); // 光标放回原位 textarea.setSelectionRange(cursorPos - 1, cursorPos - 1); return; } // 处理自动换行 const { newText, newCursorPos } = handleAutoWrap(newValue, cursorPos); setValue(newText); // 处理高度自适应 textarea.style.height = ""; textarea.style.height = Math.min(textarea.scrollHeight, MAX_HEIGHT) + "px"; // 下一帧恢复光标位置 requestAnimationFrame(() => { textarea.setSelectionRange(newCursorPos, newCursorPos); }); }; useEffect(() => { // 初始化高度 const textarea = textareaRef.current; textarea.style.height = Math.min(textarea.scrollHeight, MAX_HEIGHT) + "px"; }, []); return ( <div className={styles.TextEditor}> <textarea ref={textareaRef} id={props.title} className={styles.TextArea} value={value} onInput={handleInput} ></textarea> </div> ); }; export default TextEditor;
注意:不可编辑片段的校验逻辑可以根据后端实际返回的标记规则调整,如果后端是通过位置标记不可编辑区域,只需要把
checkLockedContent方法替换为对应位置校验逻辑即可。
内容的提问来源于stack exchange,提问作者RonCode
相关产品推荐
相关产品推荐

