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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 15:31:15