web-dev-qa-db-fra.com

Obtenir la position du curseur dans contentEditable div

Je suis en train de trouver des tonnes de réponses intéressantes sur la définition du curseur ou de la position du curseur dans une DIV contentEditable, mais pas sur la façon d'obtenir ou de trouver sa position ...

Ce que je veux faire, c'est connaître la position du curseur dans cette division, sur keyup.

Ainsi, lorsque l'utilisateur tape du texte, je peux à tout moment connaître la position de son curseur dans la div.

EDIT: Je cherche l'INDEX dans le contenu div (texte), pas les coordonnées du curseur.

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

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

Le code suivant suppose:

  • Il y a toujours un seul nœud de texte dans le <div> Éditable et aucun autre nœud
  • Le div éditable n'a pas la propriété CSS white-space Définie sur pre

Code:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}
#caretposition {
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div>
<div id="caretposition">0</div>
<script>
  var update = function() {
    $('#caretposition').html(getCaretPosition(this));
  };
  $('#contentbox').on("mousedown mouseup keydown keyup", update);
</script>
106
Tim Down
$("#editable").on('keydown keyup mousedown mouseup',function(e){
                   
       if($(window.getSelection().anchorNode).is($(this))){
          $('#position').html('0')
       }else{
         $('#position').html(window.getSelection().anchorOffset);
       }
 });
body{
  padding:40px;
}
#editable{
  height:50px;
  width:400px;
  border:1px solid #000;
}
#editable p{
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>
<div contenteditable="true" id="editable">move the cursor to see position</div>
<div>
position : <span id="position"></span>
</div>
15
Eisa Qasemi

Essaye ça:

Caret.js Obtenir la position du caret et son décalage par rapport au champ de texte

https://github.com/ichord/Caret.js

démo: http://ichord.github.com/Caret.js

13
J.Y Han

Quelques rides que je ne vois pas être abordées dans d'autres réponses:

  1. l'élément peut contenir plusieurs niveaux de noeuds enfant (par exemple, des noeuds enfant qui ont des noeuds enfant qui ont des noeuds enfant ...).
  2. une sélection peut comprendre différentes positions de début et de fin (par exemple, plusieurs caractères sont sélectionnés)
  3. le nœud contenant un début/une fin de curseur ne peut être ni l'élément ni ses enfants directs

Voici un moyen d'obtenir les positions de début et de fin en tant que décalages par rapport à la valeur textContent de l'élément:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}
13
mwag

Un peu tard pour la fête, mais au cas où quelqu'un d'autre se débat. Aucune des recherches Google que j'ai trouvées ces deux derniers jours n'a abouti à un résultat positif, mais j'ai proposé une solution concise et élégante qui fonctionnera toujours, quel que soit le nombre de balises imbriquées que vous possédez:

cursor_position() {
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    console.log('pos: '+pos);
    if(sel.anchorNode != undefined) sel.collapseToEnd();

    return pos;
}

Il sélectionne jusqu'au début du paragraphe, puis compte la longueur de la chaîne pour obtenir la position actuelle, puis annule la sélection pour ramener le curseur à la position actuelle. Si vous souhaitez effectuer cette opération pour un document entier (plusieurs paragraphes), remplacez paragraphboundary par documentboundary ou toute autre précision relative à votre cas. Découvrez l'API pour plus de détails . À votre santé! :)

7
Soubriquet
//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with Microsoft object (ie8 and lower)
    }
});

Remarque: l'objet plage lui-même peut être stocké dans une variable et peut être sélectionné à tout moment, sauf si le contenu de la div contenteditable est modifié.

Référence pour IE 8 et versions antérieures: http://msdn.Microsoft.com/en-us/library/ms535872 (VS.85) .aspx

Référence pour les navigateurs standards (tous les autres): https://developer.mozilla.org/en/DOM/range (ce sont les documents mozilla, mais le code fonctionne en chrome, safari, opera et ie9 aussi)

4
Nico Burns
function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}
4
Nishad Up

Celui-ci fonctionne pour moi:

function getCaretCharOffsetInDiv(element) {
    var caretOffset = 0;
    if (typeof window.getSelection != "undefined") {
        var range = window.getSelection().getRangeAt(0);
        var preCaretRange = range.cloneRange();
        preCaretRange.selectNodeContents(element);
        preCaretRange.setEnd(range.endContainer, range.endOffset);
        caretOffset = preCaretRange.toString().length;
    }
    else if (typeof document.selection != "undefined" && document.selection.type != "Control")
    {
        var textRange = document.selection.createRange();
        var preCaretTextRange = document.body.createTextRange();
        preCaretTextRange.moveToElementText(element);
        preCaretTextRange.setEndPoint("EndToEnd", textRange);
        caretOffset = preCaretTextRange.text.length;
    }
    return caretOffset;
} 

la ligne appelante dépend du type d'événement. Pour un événement clé, utilisez ceci:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

pour un événement de souris, utilisez ceci:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

sur ces deux cas, je prends soin des lignes de rupture en ajoutant l'indice cible

3
Jonathan R.

Comme cela m’a pris une éternité à comprendre en utilisant la nouvelle API window.getSelection que je vais partager pour la postérité. Notez que MDN suggère une prise en charge plus large de window.getSelection. Toutefois, votre kilométrage peut varier.

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // in case there is nested doms inside editable
    while(selectedNode.parentNode !== editable) {
        selectedNode = selectedNode.parentNode;
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

Voici un jsfiddle qui se déclenche sur keyup. Notez cependant que les appuis rapides sur les touches directionnelles, ainsi que les suppressions rapides, semblent être des événements ignorés.

2
Chris Sullivan

Un moyen simple, qui parcourt tous les enfants de la div contente jusqu'à ce qu'il atteigne le conteneur final. Ensuite, j'ajoute le décalage du conteneur final et nous avons l'index de caractères. Devrait fonctionner avec n'importe quel nombre de nidifications. utilise la récursivité.

Remarque: nécessite un remplissage multiple pour, par exemple, prendre en charge 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;
    }
  }
0
alockwood05