192 votes

Obtenir la position du caret contentEditable

Je trouve des tonnes de bonnes réponses, multi-navigateur, sur comment set la position du caret dans un contentEditable mais aucune sur la façon de obtenir la position du caret en premier lieu.

Ce que je veux faire, c'est connaître la position du caret dans un div sur keyup . Ainsi, lorsque l'utilisateur tape du texte, je peux, à tout moment, connaître la position du signe d'insertion dans l'image. contentEditable élément.

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});

0 votes

Regardez sa position dans le texte. Ensuite, recherchez la dernière occurrence de '@' avant cette position. Donc, juste un peu de logique de texte.

0 votes

De plus, je ne prévois pas d'autoriser d'autres balises à l'intérieur du <diV>, seulement du texte.

0 votes

Ok, oui je Je suis going to need other tags within the <div>. Il y aura des balises <a>, mais il n'y aura pas d'imbrication...

2voto

will Points 85

Si vous définissez le style de la division modifiable comme étant "display:inline-block ; white-space : pre-wrap", vous n'obtenez pas de nouvelles divisions enfant lorsque vous saisissez une nouvelle ligne, vous obtenez simplement le caractère LF (c'est-à-dire le signe ) ;.

function showCursPos(){
    selection = document.getSelection();
    childOffset = selection.focusOffset;
    const range = document.createRange();
    eDiv = document.getElementById("eDiv");
    range.setStart(eDiv, 0);
    range.setEnd(selection.focusNode, childOffset);
    var sHtml = range.toString();
    p = sHtml.length; 
    sHtml=sHtml.replace(/(\r)/gm, "\\r");
    sHtml=sHtml.replace(/(\n)/gm, "\\n");
    document.getElementById("caretPosHtml").value=p;
    document.getElementById("exHtml").value=sHtml;   
  }

click/type in div below:
<br>
<div contenteditable name="eDiv" id="eDiv"  
     onkeyup="showCursPos()" onclick="showCursPos()" 
     style="width: 10em; border: 1px solid; display:inline-block; white-space: pre-wrap; "
     >123&#13;&#10;456&#10;789</div>
<p>
html caret position:<br> <input type="text" id="caretPosHtml">
<p>  
html from start of div:<br> <input type="text" id="exHtml">

Ce que j'ai remarqué, c'est que lorsque vous appuyez sur "enter" dans la division modifiable, cela crée un nouveau nœud, et le focusOffset est donc remis à zéro. C'est pourquoi j'ai dû ajouter une variable d'intervalle, et l'étendre depuis le focusOffset des nœuds enfants jusqu'au début de eDiv (et donc capturer tout le texte entre les deux).

2voto

John Ernest Points 300

Celle-ci s'appuie sur la réponse de @alockwood05 et fournit à la fois une fonctionnalité get et set pour un caret avec des balises imbriquées à l'intérieur de la div contenteditable ainsi que les décalages dans les nœuds de sorte que vous avez une solution qui est à la fois sérialisable et dé-sérialisable par décalages ainsi.

J'utilise cette solution dans un éditeur de code multiplateforme qui doit obtenir la position de début/fin du signe d'insertion avant la coloration syntaxique via un lexer/analyseur, puis la rétablir immédiatement après.

function countUntilEndContainer(parent, endNode, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
            countingState.done = true;
            countingState.offsetInNode = offset;
            return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
            countingState.offsetInNode = offset;
            countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilEndContainer(node, endNode, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function countUntilOffset(parent, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node.nodeType === Node.TEXT_NODE) {
            if (countingState.count <= offset && offset < countingState.count + node.length)
            {
                countingState.offsetInNode = offset - countingState.count;
                countingState.node = node; 
                countingState.done = true; 
                return countingState; 
            }
            else { 
                countingState.count += node.length; 
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilOffset(node, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function getCaretPosition()
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);    
    let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
    let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
    let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
    let offsets = { start: start, end: end, offsets: offsetsCounts };
    return offsets;
}

function setCaretPosition(start, end)
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);
    let startNode = countUntilOffset(editor, start);
    let endNode = countUntilOffset(editor, end);
    let newRange = new Range();
    newRange.setStart(startNode.node, startNode.offsetInNode);
    newRange.setEnd(endNode.node, endNode.offsetInNode);
    sel.removeAllRanges();
    sel.addRange(newRange);
    return true;
}

2voto

PhiLho Points 23458

J'ai utilisé John Ernest Je me suis servi de l'excellent code de l'auteur, et je l'ai un peu retravaillé pour mes besoins :

  • Utilisation de TypeScript (dans une application Angular) ;
  • En utilisant une structure de données légèrement différente.

Et en travaillant dessus, je suis tombé sur le peu connu (ou peu utilisé) TreeWalker, et j'ai encore simplifié le code, car il permet de se débarrasser de la récursivité.

Une optimisation possible pourrait consister à parcourir l'arbre une fois pour trouver le nœud de départ et le nœud d'arrivée, mais.. :

  • Je doute que le gain de vitesse soit perceptible par l'utilisateur, même à la fin d'une page énorme et complexe ;
  • Cela rendrait l'algorithme plus complexe et moins lisible.

Au lieu de cela, j'ai traité le cas où le début est le même que la fin (juste un signe d'insertion, pas de réelle sélection).

[EDIT] Il semble que les noeuds de la gamme sont toujours de type Text, donc j'ai simplifié le code un peu plus, et cela permet d'obtenir la longueur du noeud sans le couler.

Voici le code :

export type CountingState = {
    countBeforeNode: number;
    offsetInNode: number;
    node?: Text; // Always of Text type
};

export type RangeOffsets = {
    start: CountingState;
    end: CountingState;
    offsets: { start: number; end: number; }
};

export function isTextNode(node: Node): node is Text {
    return node.nodeType === Node.TEXT_NODE;
}

export function getCaretPosition(container: Node): RangeOffsets | undefined {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) { return undefined; }
    const range = selection.getRangeAt(0);
    const start = countUntilEndNode(container, range.startContainer as Text, range.startOffset);
    const end = range.collapsed ? start : countUntilEndNode(container, range.endContainer as Text, range.endOffset);
    const offsets = { start: start.countBeforeNode + start.offsetInNode, end: end.countBeforeNode + end.offsetInNode };
    const rangeOffsets: RangeOffsets = { start, end, offsets };
    return rangeOffsets;
}

export function setCaretPosition(container: Node, start: number, end: number): boolean {
    const selection = window.getSelection();
    if (!selection) { return false; }
    const startState = countUntilOffset(container, start);
    const endState = start === end ? startState : countUntilOffset(container, end);
    const range = document.createRange(); // new Range() doesn't work for me!
    range.setStart(startState.node!, startState.offsetInNode);
    range.setEnd(endState.node!, endState.offsetInNode);
    selection.removeAllRanges();
    selection.addRange(range);
    return true;
}

function countUntilEndNode(
    parent: Node,
    endNode: Text,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (node === endNode) {
            // We found the target node, memorize it.
            countingState.node = node;
            countingState.offsetInNode = offset;
            break;
        }
        // Add length of text nodes found in the way, until we find the target node.
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}

function countUntilOffset(
    parent: Node,
    offset: number,
    countingState: CountingState = { countBeforeNode: 0, offsetInNode: 0 },
): CountingState {
    const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT);
    while (treeWalker.nextNode()) {
        const node = treeWalker.currentNode as Text;
        if (countingState.countBeforeNode <= offset && offset < countingState.countBeforeNode + node.length) {
            countingState.offsetInNode = offset - countingState.countBeforeNode;
            countingState.node = node;
            break;
        }
        countingState.countBeforeNode += node.length;
    }
    return countingState;
}

1voto

alockwood05 Points 90

Un moyen direct, qui itère à travers tous les enfants de la div contenteditable jusqu'à ce qu'il atteigne le endContainer. Ensuite, j'ajoute le décalage du endContainer et nous avons l'index des caractères. Cela devrait fonctionner avec n'importe quel nombre d'imbrications, en utilisant la récursion.

Remarque : nécessite un remplissage en poly pour ie de soutenir Element.closest('div[contenteditable]')

https://codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';

    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }

1voto

barhatsor Points 1347

Cette réponse fonctionne avec des éléments de texte imbriqués, en utilisant des fonctions récursives.

Bono : définit la position du curseur à la position sauvegardée.

function getCaretData(elem) {
  var sel = window.getSelection();
  return [sel.anchorNode, sel.anchorOffset];
}

function setCaret(el, pos) {
  var range = document.createRange();
  var sel = window.getSelection();
  range.setStart(el,pos);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
}

let indexStack = [];

function checkParent(elem) {

  let parent = elem.parentNode;
  let parentChildren = Array.from(parent.childNodes);

  let elemIndex = parentChildren.indexOf(elem);

  indexStack.unshift(elemIndex);

  if (parent !== cd) {

    checkParent(parent);

  } else {

    return;

  }

}

let stackPos = 0;
let elemToSelect;

function getChild(parent, index) {

  let child = parent.childNodes[index];

  if (stackPos < indexStack.length-1) {

    stackPos++;

    getChild(child, indexStack[stackPos]);

  } else {

    elemToSelect = child;

    return;

  }

}

let cd = document.querySelector('.cd'),
    caretpos = document.querySelector('.caretpos');

cd.addEventListener('keyup', () => {

  let caretData = getCaretData(cd);

  let selectedElem = caretData[0];
  let caretPos = caretData[1];

  indexStack = [];
  checkParent(selectedElem);

  cd.innerHTML = 'Hello world! <span>Inline! <span>In inline!</span></span>';

  stackPos = 0;
  getChild(cd, indexStack[stackPos]);

  setCaret(elemToSelect, caretPos);

  caretpos.innerText = 'indexStack: ' + indexStack + '. Got child: ' + elemToSelect.data + '. Moved caret to child at pos: ' + caretPos;

})

.cd, .caretpos {
  font-family: system-ui, Segoe UI, sans-serif;
  padding: 10px;
}

.cd span {
  display: inline-block;
  color: purple;
  padding: 5px;
}

.cd span span {
  color: chocolate;
  padding: 3px;
}

:is(.cd, .cd span):hover {
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px #005ecc;
}

<div class="cd" contenteditable="true">Hello world! <span>Inline! <span>In inline!</span></span></div>
<div class="caretpos">Move your caret inside the elements above </div>

Codepen

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X