HTML contenteditable:当内部 HTML 更改时保持插入位置

ADA*_*MJR 7 html javascript forms text dom

我有一个充当所见即所得编辑器的 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
}
Run Code Online (Sandbox Code Playgroud)
div {
  height: 100vh;
  width: 100vw;
}
Run Code Online (Sandbox Code Playgroud)
<div contenteditable />
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

onk*_*kar 15

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

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

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

editor.addEventListener("input", () => {
  //get current cursor position
  const sel = window.getSelection();
  const node = sel.focusNode;
  const offset = sel.focusOffset;
  const pos = getCursorPosition(editor, node, offset, { pos: 0, done: false });
  if (offset === 0) pos.pos += 0.5;

  editor.innerHTML = parse(editor.innerText);

  // restore the position
  sel.removeAllRanges();
  const 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
      // handle special characters
      .replace(/\n/gm, "<br>")
      .replace(/\t/gm, "&#9;")
  );
}

// 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 (let 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 (let i = 0; i < parent.childNodes.length && !stat.done; i++) {
      currentNode = parent.childNodes[i];
      setCursorPosition(currentNode, range, stat);
    }
  }
  return range;
}
Run Code Online (Sandbox Code Playgroud)
.editor {
  height: 100px;
  width: 400px;
  border: 1px solid #888;
  padding: 0.5rem;
  white-space: pre;
}

em, strong{
  font-size: 1.3rem;
}
Run Code Online (Sandbox Code Playgroud)
<div class="editor" contenteditable ></div>
Run Code Online (Sandbox Code Playgroud)

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

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

笔记:

  • 我已经在 Windows 10 上的 Chrome 97 上测试了代码。
  • 在演示中使用了递归解决方案getCursorPositionsetCursorPosition保持简单。
  • 特殊字符(如换行符)需要转换为其等效的 HTML 形式,例如<br>. 需要white-space: pre在可编辑元素上设置制表符。我尝试在演示中处理\n, 。\t

  • 这比 Olian04 的答案(该答案似乎并不总是接受输入)更顺利。在 Mac、Chrome 96 和 Firefox 95 上进行了测试。 (2认同)

Oli*_*n04 5

大多数富文本编辑器的做法是保持自己的内部状态,在按键事件上更新它并渲染自定义可视层。例如这样:

\n

\r\n
\r\n
const $editor = document.querySelector(\'.editor\');\nconst state = {\n cursorPosition: 0,\n contents: \'hello world\'.split(\'\'),\n isFocused: false,\n};\n\n\nconst $cursor = document.createElement(\'span\');\n$cursor.classList.add(\'cursor\');\n$cursor.innerText = \'\xe1\xa0\x8e\'; // Mongolian vowel separator\n\nconst renderEditor = () => {\n  const $contents = state.contents\n    .map(char => {\n      const $span = document.createElement(\'span\');\n      $span.innerText = char;\n      return $span;\n    });\n  \n  $contents.splice(state.cursorPosition, 0, $cursor);\n  \n  $editor.innerHTML = \'\';\n  $contents.forEach(el => $editor.append(el));\n}\n\ndocument.addEventListener(\'click\', (ev) => {\n  if (ev.target === $editor) {\n    $editor.classList.add(\'focus\');\n    state.isFocused = true;\n  } else {\n    $editor.classList.remove(\'focus\');\n    state.isFocused = false;\n  }\n});\n\ndocument.addEventListener(\'keydown\', (ev) => {\n  if (!state.isFocused) return;\n  \n  switch(ev.key) {\n    case \'ArrowRight\':\n      state.cursorPosition = Math.min(\n        state.contents.length, \n        state.cursorPosition + 1\n      );\n      renderEditor();\n      return;\n    case \'ArrowLeft\':\n      state.cursorPosition = Math.max(\n        0, \n        state.cursorPosition - 1\n      );\n      renderEditor();\n      return;\n    case \'Backspace\':\n      if (state.cursorPosition === 0) return;\n      delete state.contents[state.cursorPosition-1];\n      state.contents = state.contents.filter(Boolean);\n      state.cursorPosition = Math.max(\n        0, \n        state.cursorPosition - 1\n      );\n      renderEditor();\n      return;\n    default:\n      // This is very naive\n      if (ev.key.length > 1) return;\n      state.contents.splice(state.cursorPosition, 0, ev.key);\n      state.cursorPosition += 1;\n      renderEditor();\n      return;\n  }  \n});\n\nrenderEditor();
Run Code Online (Sandbox Code Playgroud)\r\n
.editor {\n  position: relative;\n  min-height: 100px;\n  max-height: max-content;\n  width: 100%;\n  border: black 1px solid;\n}\n\n.editor.focus {\n  border-color: blue;\n}\n\n.editor.focus .cursor {\n  position: absolute;\n  border: black solid 1px;\n  border-top: 0;\n  border-bottom: 0;\n  animation-name: blink;\n  animation-duration: 1s;\n  animation-iteration-count: infinite;\n}\n\n@keyframes blink {\n  from {opacity: 0;}\n  50% {opacity: 1;}\n  to {opacity: 0;}\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<div class="editor"></div>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n