Ottieni contenuto Posizione dell'indice del cursore modificabile


119

Sto trovando tonnellate di buone risposte crossbrowser su come IMPOSTARE il cursore o la posizione dell'indice del cursore in un contentEditableelemento, ma nessuna su come OTTENERE o trovare il suo indice ...

Quello che voglio fare è conoscere l'indice del cursore all'interno di questo div, su keyup.

Quindi, quando l'utente sta digitando del testo, posso in qualsiasi momento conoscere l'indice del suo cursore all'interno contentEditabledell'elemento.

EDIT: Sto cercando l' INDICE all'interno del contenuto div (testo), non le coordinate del cursore.

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

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

Guarda la sua posizione nel testo. Quindi, cerca l'ultima occorrenza di "@" prima di quella posizione. Quindi solo un po 'di logica del testo.
Bertvan

Inoltre, non ho intenzione di consentire altri tag all'interno di <diV>, solo testo
Bertvan

ok, sì, io sto andando ad avere bisogno di altri tag all'interno del <div>. Ci saranno tag <a>, ma non ci sarà nidificazione ...
Bertvan

@Bertvan: se il cursore è all'interno di un <a>elemento all'interno di <div>, quale offset vuoi allora? L'offset all'interno del testo all'interno del <a>?
Tim Down

Non dovrebbe mai essere all'interno di un elemento <a>. L'elemento <a> dovrebbe essere reso html, quindi l'utente non può effettivamente inserire il cursore lì dentro.
Bertvan

Risposte:


121

Il codice seguente presuppone:

  • C'è sempre un singolo nodo di testo all'interno del modificabile <div>e nessun altro nodo
  • Il div modificabile non ha la white-spaceproprietà CSS impostata supre

Se hai bisogno di un approccio più generale che funzioni contenuto con elementi nidificati, prova questa risposta:

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

Codice:

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>


9
Questo non funzionerà se ci sono altri tag lì dentro. Domanda: se il cursore si trova all'interno di un <a>elemento all'interno di <div>, quale offset vuoi allora? L'offset all'interno del testo all'interno del <a>?
Tim Down

3
@Richard: Beh, keyupè probabile che sia l'evento sbagliato per questo, ma è quello che è stato utilizzato nella domanda originale. getCaretPosition()di per sé va bene entro i propri limiti.
Tim Down

3
Quella demo JSFIDDLE fallisce se premo Invio e vado su una nuova riga. La posizione mostrerà 0.
giorgio79

5
@ giorgio79: Sì, perché l'interruzione di riga genera un elemento <br>or <div>, che viola il primo presupposto menzionato nella risposta. Se hai bisogno di una soluzione leggermente più generale, potresti provare stackoverflow.com/a/4812022/96100
Tim Down

2
C'è comunque da fare in modo che includa il numero di riga?
Adjit

28

Alcune rughe che non vedo essere affrontate in altre risposte:

  1. l'elemento può contenere più livelli di nodi figlio (ad es. nodi figlio che hanno nodi figlio che hanno nodi figlio ...)
  2. una selezione può essere composta da diverse posizioni di inizio e fine (ad es. vengono selezionati più caratteri)
  3. il nodo contenente un segno di saluto iniziale / finale potrebbe non essere l'elemento o i suoi figli diretti

Ecco un modo per ottenere le posizioni di inizio e fine come offset rispetto al valore textContent dell'elemento:

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

3
Questa deve essere selezionata come risposta giusta. Funziona con i tag all'interno del testo (la risposta accettata no)
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>


3
Questo purtroppo smette di funzionare non appena si preme invio e si inizia su un'altra riga (ricomincia da 0, probabilmente contando dal CR / LF).
Ian

Non funziona correttamente se hai alcune parole in grassetto e / o corsivo.
user2824371

14

Prova questo:

Caret.js Ottieni la posizione del cursore e l'offset dal campo di testo

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

demo: http://ichord.github.com/Caret.js


Questo è dolce. Avevo bisogno di questo comportamento per impostare il cursore alla fine di a contenteditable liquando si fa clic su un pulsante per rinominare liil contenuto.
akinuri

@AndroidDev Non sono l'autore di Caret.js ma hai considerato che ottenere la posizione del cursore per tutti i principali browser è più complesso di poche righe? Conosci o hai creato un'alternativa non gonfiata che puoi condividere con noi?
adelriosantiago

8

Un po 'tardi per la festa, ma nel caso qualcun altro stia lottando. Nessuna delle ricerche su Google che ho trovato negli ultimi due giorni ha prodotto qualcosa che funziona, ma ho trovato una soluzione concisa ed elegante che funzionerà sempre indipendentemente dal numero di tag nidificati che hai:

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>

Seleziona fino all'inizio del paragrafo e quindi conta la lunghezza della stringa per ottenere la posizione corrente, quindi annulla la selezione per riportare il cursore nella posizione corrente. Se desideri eseguire questa operazione per un intero documento (più di un paragrafo), cambia paragraphboundaryin documentboundaryo qualsiasi granularità per il tuo caso. Controlla l'API per maggiori dettagli . Saluti! :)


1
Se ho <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Ogni volta che posiziono il cursore prima del itag o di qualsiasi elemento html figlio all'interno div, la posizione del cursore inizia da 0. C'è un modo per sfuggire a questo conteggio di riavvio?
vam

Dispari. Non ottengo quel comportamento in Chrome. Quale browser utilizzate?
Soubriquet

2
Sembra che selection.modify possa o meno essere supportato su tutti i browser. 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
    };
}

questo in realtà ha funzionato per me, ho provato tutti quelli sopra, non l'hanno fatto.
iStudLion

grazie ma restituisce anche {x: 0, y: 0} sulla nuova riga.
hichamkazan

questo restituisce la posizione dei pixel, non l'offset del carattere
4esn0k

grazie, stavo cercando di recuperare la posizione dei pixel dal cursore e funziona bene.
Sameesh

6

window.getSelection - vs - document.selection

Questo funziona per me:

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 linea chiamante dipende dal tipo di evento, per l'evento chiave utilizzare questo:

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

per l'evento del mouse usa questo:

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

in questi due casi mi occupo delle linee di interruzione aggiungendo l'indice di destinazione


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

Nota: l'oggetto intervallo può essere memorizzato in una variabile e può essere riselezionato in qualsiasi momento a meno che il contenuto del div contenteditable non cambi.

Riferimento per IE 8 e versioni precedenti: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Riferimento per i browser standard (tutti gli altri): https://developer.mozilla.org/en/DOM/range (è la documentazione di mozilla, ma il codice funziona anche in chrome, safari, opera e ie9)


1
Grazie, ma come ottengo esattamente l '"indice" della posizione del cursore nei contenuti div?
Bertvan

OK, sembra che chiamare .baseOffset su .getSelection () faccia il trucco. Quindi questo, insieme alla tua risposta, risponde alla mia domanda. Grazie!
Bertvan

2
Sfortunatamente .baseOffset funziona solo in webkit (credo). Inoltre ti dà solo l'offset dal genitore immediato del cursore (se hai un tag <b> all'interno del <div> darà l'offset dall'inizio del <b>, non l'inizio del <div> . Gli intervalli basati su standard possono utilizzare range.endOffset range.startOffset range.endContainer e range.startContainer per ottenere l'offset dal nodo padre della selezione e dal nodo stesso (inclusi i nodi di testo). IE fornisce range.offsetLeft che è il offset da sinistra in pixel , e così inutile.
Nico Burns

È meglio memorizzare l'oggetto range da solo e usare window.getSelection (). Addrange (range); <- standard e range.select (); <- IE per riposizionare il cursore nella stessa posizione. range.insertNode (nodetoinsert); <- standard e range.pasteHTML (htmlcode); <- IE per inserire testo o html nel cursore.
Nico Burns

L' Rangeoggetto restituito dalla maggior parte dei browser e l' TextRangeoggetto restituito da IE sono cose estremamente diverse, quindi non sono sicuro che questa risposta risolva molto.
Tim Down

3

Dato che ho impiegato un'eternità a capire usando la nuova API window.getSelection, condividerò per i posteri. Nota che MDN suggerisce che esiste un supporto più ampio per window.getSelection, tuttavia, il tuo chilometraggio potrebbe variare.

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

Ecco un jsfiddle che si attiva su keyup. Si noti, tuttavia, che le pressioni rapide dei tasti direzionali e l'eliminazione rapida sembrano essere eventi di salto.


Per me va bene! Grazie mille.
dmodo

Con questo testo la selezione non è più possibile in quanto è compresso. Possibile scenario: necessità di valutare ogni evento
keyUp

0

Un modo diretto, che itera attraverso tutti i bambini del div contenteditable fino a raggiungere endContainer. Quindi aggiungo l'offset del contenitore finale e abbiamo l'indice dei caratteri. Dovrebbe funzionare con qualsiasi numero di annidamenti. usa la ricorsione.

Nota: richiede un riempimento poli per ie per supportareElement.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;
    }
  }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.