Obtenir le contenu Position de l'index caret modifiable

119

Je trouve des tonnes de bonnes réponses crossbrowser sur la façon de définir la position de l'index du curseur ou du curseur dans un contentEditableélément, mais aucune sur la façon d'obtenir ou de trouver son index ...

Ce que je veux faire, c'est connaître l'index du curseur dans cette div, sur keyup.

Ainsi, lorsque l'utilisateur tape du texte, je peux à tout moment connaître l'index de son curseur dans l' contentEditableélément.

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

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

$('#contentbox').keyup(function() { 
    // ... ? 
});
Bertvan
la source
Regardez sa position dans le texte. Ensuite, recherchez la dernière occurrence de «@» avant cette position. Donc juste un peu de logique textuelle.
Bertvan
De plus, je ne prévois pas d'autoriser d'autres balises dans le <diV>, seulement du texte
Bertvan
ok, oui je vais avoir besoin d'autres balises dans le <div>. Il y aura des balises <a>, mais il n'y aura pas d'imbrication ...
Bertvan
@Bertvan: si le curseur est à l'intérieur d'un <a>élément à l'intérieur de <div>, quel décalage voulez-vous alors? Le décalage dans le texte à l'intérieur du <a>?
Tim Down
Il ne devrait jamais être dans un élément <a>. L'élément <a> doit être rendu html, de sorte que l'utilisateur ne peut pas réellement y placer le curseur.
Bertvan

Réponses:

121

Le code suivant suppose:

  • Il y a toujours un seul nœud de texte dans le modifiable <div>et aucun autre nœud
  • La div modifiable n'a pas la white-spacepropriété CSS définie surpre

Si vous avez besoin d'une approche plus générale qui fonctionnera avec du contenu avec des éléments imbriqués, essayez cette réponse:

https://stackoverflow.com/a/4812022/96100

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>

Tim Down
la source
9
Cela ne fonctionnera pas s'il y a d'autres balises là-dedans. Question: si le curseur est à l'intérieur d'un <a>élément à l'intérieur de <div>, quel décalage voulez-vous alors? Le décalage dans le texte à l'intérieur du <a>?
Tim Down
3
@Richard: Eh bien, keyupc'est probablement le mauvais événement pour cela, mais c'est ce qui a été utilisé dans la question d'origine. getCaretPosition()lui-même est bien dans ses propres limites.
Tim Down
3
Cette démo JSFIDDLE échoue si j'appuie sur Entrée et que je passe sur une nouvelle ligne. La position affichera 0.
giorgio79
5
@ giorgio79: Oui, car le saut de ligne génère un élément <br>ou <div>, qui viole la première hypothèse mentionnée dans la réponse. Si vous avez besoin d'une solution un peu plus générale, vous pouvez essayer stackoverflow.com/a/4812022/96100
Tim Down
2
Y a-t-il un moyen de le faire pour qu'il inclue le numéro de ligne?
Ajuster le
28

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

  1. l'élément peut contenir plusieurs niveaux de nœuds enfants (par exemple, des nœuds enfants qui ont des nœuds enfants qui ont des nœuds enfants ...)
  2. une sélection peut se composer de 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 / fin Caret ne peut être ni l'élément ni ses enfants directs

Voici un moyen d'obtenir les positions de début et de fin sous forme de 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]];
}
mwag
la source
3
Cela doit être sélectionné comme la bonne réponse. Cela fonctionne avec des balises à l'intérieur du texte (la réponse acceptée ne le fait pas)
hamboy75
17

$("#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>

Eisa Qasemi
la source
3
Cela cesse malheureusement de fonctionner dès que vous appuyez sur Entrée et commencez sur une autre ligne (cela recommence à 0 - probablement à partir du CR / LF).
Ian
Cela ne fonctionne pas correctement si vous avez des mots en gras et / ou en italique.
user2824371
14

Essaye ça:

Caret.js Obtenir la position du curseur et le décalage à partir du champ de texte

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

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

JY Han
la source
C'est mignon. J'avais besoin de ce comportement pour définir le signe caret à la fin d'un contenteditable lilorsque je cliquais sur un bouton pour renommer lile contenu de.
akinuri le
@AndroidDev Je ne suis pas l'auteur de Caret.js mais avez-vous considéré que la position du curseur pour tous les principaux navigateurs est plus complexe que quelques lignes? Connaissez-vous ou avez-vous créé une alternative non gonflée que vous pouvez partager avec nous?
adelriosantiago
8

Un peu tard à la fête, mais au cas où quelqu'un d'autre aurait du mal. Aucune des recherches Google que j'ai trouvées au cours des deux derniers jours n'a abouti à quoi que ce soit qui fonctionne, mais j'ai trouvé une solution concise et élégante qui fonctionnera toujours quel que soit le nombre de balises imbriquées que vous avez:

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

    return pos;
}

// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( cursor_position(), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

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 voulez faire cela pour un document entier (plus d'un paragraphe), changez paragraphboundaryen documentboundaryou en toute granularité pour votre cas. Consultez l'API pour plus de détails . À votre santé! :)

Sobriquet
la source
1
Si j'ai <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Chaque fois que je place le curseur avant la ibalise ou tout élément html enfant à l'intérieur div, la position du curseur commence à 0. Existe-t-il un moyen d'échapper à ce compte de redémarrage?
vam
Impair. Je n'obtiens pas ce comportement dans Chrome. Quel navigateur utilisez-vous?
Soubriquet
2
On dirait que selection.modify peut ou non être pris en charge sur tous les navigateurs. developer.mozilla.org/en-US/docs/Web/API/Selection
Chris Sullivan
7
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
    };
}
Nishad Up
la source
celui-ci a vraiment fonctionné pour moi, j'ai essayé tous ceux ci-dessus, ils n'ont pas.
iStudLion
merci mais il renvoie aussi {x: 0, y: 0} sur une nouvelle ligne.
hichamkazan
cela renvoie la position du pixel, pas le décalage du caractère
4esn0k
merci, je cherchais à récupérer la position des pixels du caret et cela fonctionne bien.
Sameesh le
6

window.getSelection - vs - document.selection

Celui-ci fonctionne pour moi:

function getCaretCharOffset(element) {
  var caretOffset = 0;

  if (window.getSelection) {
    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 (document.selection && 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;
}


// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

La ligne appelante dépend du type d'événement, pour l'événement clé, utilisez ceci:

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

pour l'é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'index cible

Jonathan R.
la source
4
//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 range lui-même peut être stocké dans une variable et peut être re-sélectionné à tout moment à moins que le contenu de la div contenteditable change.

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 (c'est la documentation mozilla, mais le code fonctionne aussi dans Chrome, Safari, Opera et ie9)

Nico Burns
la source
1
Merci, mais comment obtenir exactement l '«index» de la position du curseur dans le contenu div?
Bertvan
OK, il semble que l'appel de .baseOffset sur .getSelection () fasse l'affaire. Donc ceci, avec votre réponse, répond à ma question. Merci!
Bertvan
2
Malheureusement .baseOffset ne fonctionne que dans webkit (je pense). Il ne vous donne également que le décalage par rapport au parent immédiat du curseur (si vous avez une balise <b> à l'intérieur du <div>, il donnera le décalage depuis le début du <b>, pas le début du <div> . Les plages basées sur des normes peuvent utiliser range.endOffset range.startOffset range.endContainer et range.startContainer pour obtenir le décalage à partir du nœud parent de la sélection et du nœud lui-même (cela inclut les nœuds de texte). IE fournit range.offsetLeft qui est le décalage par rapport à la gauche en pixels , et donc inutile.
Nico Burns
Il est préférable de stocker l'objet range lui-même et d'utiliser window.getSelection (). Addrange (range); <- standards et range.select (); <- IE pour repositionner le curseur au même endroit. range.insertNode (nodetoinsert); <- standards et range.pasteHTML (htmlcode); <- IE pour insérer du texte ou du html au niveau du curseur.
Nico Burns
L' Rangeobjet renvoyé par la plupart des navigateurs et l' TextRangeobjet renvoyé par IE sont des choses extrêmement différentes, donc je ne suis pas sûr que cette réponse résout beaucoup.
Tim Down
3

Comme cela m'a pris une éternité à comprendre l'utilisation de la nouvelle API window.getSelection, je vais partager pour la postérité. Notez que MDN suggère qu'il existe une prise en charge plus large de window.getSelection, mais 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,
        };
    }

    // 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 pressions rapides sur les touches directionnelles, ainsi que la suppression rapide semblent être des événements de saut.

Chris Sullivan
la source
Travaille pour moi! Merci beaucoup.
dmodo le
Avec ce texte, la sélection n'est plus possible car il est réduit. Scénario possible: besoin d'évaluer à chaque événement
keyUp
0

Une manière simple, qui itère à travers tous les chidren de la div contenteditable jusqu'à ce qu'elle atteigne le endContainer. Ensuite, j'ajoute le décalage du conteneur de fin et nous avons l'index des caractères. Devrait fonctionner avec n'importe quel nombre d'emboîtements. utilise la récursivité.

Remarque: nécessite un remplissage poly pour ie pour soutenirElement.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;
    }
  }
alockwood05
la source