suchen

Heim  >  Fragen und Antworten  >  Hauptteil

Behalten Sie die Caret-Position im HTML-Inhalt bei, wenn sich der innere HTML-Code ändert

Ich habe ein Div, das als WYSIWYG-Editor fungiert. Es fungiert als Textfeld, rendert jedoch die Markdown-Syntax darin, um Live-Änderungen anzuzeigen.

Problem: Beim Eingeben von Buchstaben wird die Einfügemarke auf den Anfang des Div zurückgesetzt.


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

Markdown-Editoren wie QuillJS scheinen in der Lage zu sein, untergeordnete Elemente zu bearbeiten, ohne das übergeordnete Element zu bearbeiten. Dadurch wird das Problem vermieden, aber ich bin mir jetzt sicher, wie ich diese Logik mit diesem Setup wiederherstellen kann.

Frage: Wie kann verhindert werden, dass die Einfügemarke beim Tippen zurückgesetzt wird?

Update: Ich habe es geschafft, die Caret-Position bei jeder Eingabe an das Ende des Div zu senden. Dies führt jedoch immer noch zu einer grundsätzlichen Neuausrichtung der Position. https://codepen.io/ADAMJR/pen/KKvGNbY


P粉668804228P粉668804228382 Tage vor798

Antworte allen(2)Ich werde antworten

  • 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;}
    }

    Antwort
    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, "**$1**") // bold
          .replace(/(?$1*") // 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;
    }

    Antwort
    0
  • StornierenAntwort