contenteditable div二次编辑光标跳首、双向绑定值重复拼接问题
问题现象
使用设置了contenteditable="true"的div作为文本输入组件时,存在两个异常:
- 第二次进入编辑状态时,光标默认定位在字符串起始位置,首次编辑可正常从左到右输入
- 自定义双向绑定逻辑异常,div内文本出现多次重复拼接的问题
现有代码
页面模板代码
<div class="css-462"> <div class="-hr-454"> <div class="-r9-455"> {{question.no}}.</div> <div class="-hv-467 transparent"> <div class="-hv-483"> <div aria-label="Question title 2 Input your question title here" [classList]="generateClassName()" role="textbox" tabindex="0" contenteditable="true" style="user-select: text;" >{{printText()}}</div> <input aria-hidden="true" tabindex="-1"> <div></div> </div> </div> </div> </div>
模板中printText()方法用于返回Map结构中存储的文本内容,通过JS轮询实现双向绑定,将输入值存入Map,预期Map的set()方法会覆盖已有键对应的值,但实际出现值重复拼接问题,轮询逻辑代码如下:
setInterval(() => { if(!this.IsQuestionnaireTitle) { if(getTextLabelOnTOTenWidget(".questionToQNo" + this.dataId)!=null && getTextLabelOnTOTenWidget(".questionToQNo" + this.dataId).length != 0) { console.log(getTextLabelOnTOTenWidget(".questionToQNo" + this.dataId)); //this.question.texts.delete("en"); this.question.texts.set("en", getTextLabelOnTOTenWidget(".questionToQNo" + this.dataId)); } } if(this.IsQuestionnaireTitle) { if(getTextLabelOnTOTenWidget(".questionTitleField")!=null && getTextLabelOnTOTenWidget(".questionTitleField").length != 0) { this.questionnaireTitle = getTextLabelOnTOTenWidget(".questionTitleField"); } if(getTextLabelOnTOTenWidget(".questionnaireDesc")!=null && getTextLabelOnTOTenWidget(".questionnaireDesc").length != 0) { this.questionnaireDescription = getTextLabelOnTOTenWidget(".questionnaireDesc"); } } }, 100)
问题根因
两个问题由同一逻辑缺陷触发:
模板插值{{printText()}}直接渲染contenteditable div的内部内容,同时100ms间隔的轮询会持续把div实时内容写回Map,Map值更新后触发模板重渲染,再次将printText()返回的内容写入div,形成异常循环:
- 光标错位:每次轮询触发重渲染时,div的DOM内容被整体替换,浏览器丢失原有光标位置记录,默认将光标重置到内容起始位置,二次进入编辑时就会出现光标在开头闪烁的现象。
- 内容重复拼接:轮询读取div内容的时机与模板重写div内容的时机存在冲突,会触发「读取到旧内容+新输入内容的拼接值→写回Map→模板将拼接值写入div→下一轮轮询读取到更长的拼接值」的死循环,最终内容持续重复拼接。
注意:setInterval轮询实现双向绑定的方案性能极差,100ms一次的高频DOM读写会频繁触发页面重排,无任何性能优势。
修复方案
1. 废弃轮询逻辑,改用原生事件监听输入
直接给contenteditable div绑定input事件,仅在用户实际输入时同步值,避免无意义的DOM读写:
// 页面初始化时为可编辑div绑定input事件,移除原有setInterval轮询逻辑 const editDiv = document.querySelector('[role="textbox"]'); editDiv.addEventListener('input', (e) => { const currentText = e.target.innerText.trim(); if(!this.IsQuestionnaireTitle) { if(currentText.length) { this.question.texts.set("en", currentText); } return; } if(e.target.classList.contains('questionTitleField') && currentText.length) { this.questionnaireTitle = currentText; } if(e.target.classList.contains('questionnaireDesc') && currentText.length) { this.questionnaireDescription = currentText; } })
2. 修复重渲染导致的光标错位问题
禁止直接用插值把内容塞到contenteditable div内部,避免每次数据更新都全量替换DOM内容:
- 仅在页面/组件首次初始化时,把Map中存储的文本赋值给div的
innerText - 后续数据更新时,先判断当前div是否处于聚焦激活状态,如果用户正在编辑,就不要主动修改div的内容,避免打断编辑状态、重置光标。
初始化赋值、非编辑态同步值的逻辑参考:
const editDiv = document.querySelector('[role="textbox"]'); // 仅当div未处于编辑状态时,才同步外部值到DOM if(document.activeElement !== editDiv) { editDiv.innerText = this.question.texts.get("en") || ''; }
如果存在强制更新内容的场景(比如表单初始化、重置),更新完DOM内容后手动将光标设置到内容末尾即可:
function setCursorToEnd(ele) { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(ele); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } // 赋值完成后调用 editDiv.innerText = '新的文本内容'; setCursorToEnd(editDiv); editDiv.focus();
内容的提问来源于stack exchange,提问作者Nesan Mano
相关产品推荐
相关产品推荐

