搜尋

首頁  >  問答  >  主體

當內部 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粉668804228382 天前797

全部回覆(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
  • 取消回覆