首页 > 解决方案 > HTML contenteditable:当内部 HTML 更改时保持插入符号位置

问题描述

我有一个充当所见即所得编辑器的 div。这充当文本框,但在其中呈现 markdown 语法,以显示实时更改。

问题:当输入一个字母时,插入符号的位置被重置为 div 的开头。

const editor = document.querySelector('div');
editor.innerHTML = parse('**dlob**  *cilati*');

editor.addEventListener('input', () => {
  editor.innerHTML = parse(editor.innerText);
});

function parse(text) {
  return text
    .replace(/\*\*(.*)\*\*/gm, '**<strong>$1</strong>**')     // bold
    .replace(/\*(.*)\*/gm, '*<em>$1</em>*');                  // italic
}
div {
  height: 100vh;
  width: 100vw;
}
<div contenteditable />

代码笔:https : //codepen.io/ADAMJR/pen/MWvPebK

像 QuillJS 这样的 Markdown 编辑器似乎在不编辑父元素的情况下编辑子元素。这避免了问题,但我现在确定如何使用此设置重新创建该逻辑。

问题:如何让插入符号位置在打字时不重置?

更新:我已经设法在每个输入上将插入符号位置发送到 div 的末尾。然而,这仍然基本上重置了位置。https://codepen.io/ADAMJR/pen/KKvGNbY

标签: javascripthtmlformsdomtext

解决方案


您需要先获取光标的位置,然后处理并设置内容。然后恢复光标位置。

当存在嵌套元素时,恢复光标位置是一个棘手的部分。此外,您每次都在创建新元素<strong><em>旧元素被丢弃。

const editor = document.querySelector('.editor');
editor.innerHTML = parse('For **bold** two stars. For *italic* one star. Some more **bold**.');

editor.addEventListener('input', () => {
  //get current cursor position
  let sel = window.getSelection();
  let node = sel.focusNode;
  let offset = sel.focusOffset;
  let pos = getCursorPosition(editor, node, offset, {pos: 0, done: false});
  //console.log('Position: ', pos);

  editor.innerHTML = parse(editor.innerText);

  // restore the position
  sel.removeAllRanges();
  let range = setCursorPosition(editor, document.createRange(), {  pos: pos.pos, done: false});
  range.collapse(true);
  sel.addRange(range);
  
});

function parse(text) {
  //use (.*?) lazy quantifiers to match content inside
  return text
    .replace(/\*{2}(.*?)\*{2}/gm, '**<strong>$1</strong>**') // bold
    .replace(/(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)/gm, '*<em>$1</em>*'); // italic
}

// get the cursor position from .editor start
function getCursorPosition(parent, node, offset, stat) {
  if (stat.done) return stat;

  let currentNode = null;
  if (parent.childNodes.length == 0) {
    stat.pos += parent.textContent.length;
  } else {
    for (var i = 0; i < parent.childNodes.length && !stat.done; i++) {
      currentNode = parent.childNodes[i];
      if (currentNode === node) {
        stat.pos += offset;
        stat.done = true;
        return stat;
      } else
        getCursorPosition(currentNode, node, offset, stat);
    }
  }
  return stat;
}

//find the child node and relative position and set it on range
function setCursorPosition(parent, range, stat) {
  if (stat.done) return range;

  if (parent.childNodes.length == 0) {
    if (parent.textContent.length >= stat.pos) {
      range.setStart(parent, stat.pos);
      stat.done = true;
    } else {
      stat.pos = stat.pos - parent.textContent.length;
    }
  } else {
    for (var i = 0; i < parent.childNodes.length && !stat.done; i++) {
      currentNode = parent.childNodes[i];
      setCursorPosition(currentNode, range, stat);
    }
  }
  return range;
}
.editor {
  height: 100px;
  width: 300px;
  border: 1px solid #888;
  padding: 0.5rem;
}

em,strong{
font-size: 1.3rem;
}
<div class='editor' contenteditable />

API window.getSelection返回节点和相对于它的位置。每次创建全新元素时,我们都无法使用旧节点对象恢复位置。因此,为了保持简单并拥有更多控制权,我们将获得相对于.editorusinggetCursorPosition函数的位置。并且,在我们设置了 innerHTML 内容之后,我们使用setCursorPosition.
这两个函数都适用于嵌套元素。

此外,改进了正则表达式:使用 (.*?) 惰性量词和前瞻和后视,以实现更好的匹配。你可以找到更好的表达方式。

笔记:

  • 我已经在 Windows 10 上的 Chrome 97 上测试了代码。
  • 在演示中使用递归解决方案getCursorPositionsetCursorPosition使其保持简单。

推荐阅读