搜索

首页  >  问答  >  正文

当内部 HTML 更改时保留 HTML contenteditable 中的插入符位置

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

问题:键入字母时,插入符号位置会重置为 div 的开头。


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

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

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


Codepen:https://codepen.io/ADAMJR/pen/MWvPebK

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

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

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


P粉668804228P粉668804228432 天前833

全部回复(2)我来回复

  • P粉393030917

    P粉3930309172023-11-09 18:34:46

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

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

    回复
    0
  • P粉060112396

    P粉0601123962023-11-09 14:42:18

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

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

    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, "****") // bold
          .replace(/(?*") // italic
          // handle special characters
          .replace(/\n/gm, "
    ") .replace(/\t/gm, " ") ); } // 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; }
    .editor {
      height: 100px;
      width: 400px;
      border: 1px solid #888;
      padding: 0.5rem;
      white-space: pre;
    }
    
    em, strong{
      font-size: 1.3rem;
    }

    回复
    0
  • 取消回复