Maison  >  Questions et réponses  >  le corps du texte

Préserver la position du curseur dans le contenu HTML modifiable lorsque le HTML interne change

J'ai un div qui fait office d'éditeur WYSIWYG. Il agit comme une zone de texte mais y restitue la syntaxe Markdown pour afficher les modifications en direct.

Problème : Lors de la saisie de lettres, la position du curseur est réinitialisée au début du 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

Les éditeurs Markdown comme QuillJS semblent être capables de modifier les éléments enfants sans modifier l'élément parent. Cela évite le problème, mais je sais maintenant comment recréer cette logique avec cette configuration.

Question : Comment empêcher la réinitialisation de la position du curseur lors de la frappe ?

Mise à jour : J'ai réussi à envoyer la position du curseur à la fin du div sur chaque entrée. Cependant, cela réinitialise essentiellement la position. https://codepen.io/ADAMJR/pen/KKvGNbY


P粉668804228P粉668804228315 Il y a quelques jours737

répondre à tous(2)je répondrai

  • P粉393030917

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

    La plupart des éditeurs de texte enrichi conservent leur propre état interne, le mettent à jour en fonction des événements clés et restituent une couche visuelle personnalisée. Par exemple :

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

    répondre
    0
  • P粉060112396

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

    Vous devez d'abord obtenir la position du curseur, puis traiter et définir le contenu. Restaurez ensuite la position du curseur.

    Restaurer la position du curseur est la partie la plus délicate lorsqu'il y a des éléments imbriqués. De plus, vous créerez de nouveaux éléments à chaque fois et les anciens seront supprimés.

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

    répondre
    0
  • Annulerrépondre