Holen Sie sich contentEditable Caret Index Position

119

Ich finde unzählige gute Crossbrowser-Antworten zum Einstellen der Cursor- oder Caret-Indexposition in einem contentEditableElement, aber keine zum Abrufen oder Finden des Index ...

Was ich tun möchte, ist den Index des Carets innerhalb dieser Division zu kennen keyup.

Wenn der Benutzer Text eingibt, kann ich jederzeit den Index des Cursors innerhalb des contentEditableElements erkennen.

EDIT: Ich suche den INDEX innerhalb des div-Inhalts (Text), nicht die Cursorkoordinaten.

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

$('#contentbox').keyup(function() { 
    // ... ? 
});
Bertvan
quelle
Schauen Sie sich die Position im Text an. Suchen Sie dann das letzte Vorkommen von '@' vor dieser Position. Also nur eine Textlogik.
Bertvan
Außerdem habe ich nicht vor, andere Tags innerhalb der <diV> zuzulassen, nur Text
Bertvan
ok, ja , ich bin gehen andere Tags innerhalb des <div> müssen. Es wird <a> Tags geben, aber es wird keine Verschachtelung geben ...
Bertvan
@ Bertvan: Wenn sich das Caret in einem <a>Element innerhalb des befindet <div>, welchen Offset möchten Sie dann? Der Versatz innerhalb des Textes innerhalb des <a>?
Tim Down
Es sollte sich niemals in einem <a> -Element befinden. Das <a> -Element sollte als HTML gerendert werden, damit der Benutzer das Caret dort nicht platzieren kann.
Bertvan

Antworten:

121

Der folgende Code setzt voraus:

  • Es gibt immer einen einzelnen Textknoten innerhalb der bearbeitbaren <div>und keine anderen Knoten
  • Für das bearbeitbare div ist die CSS- white-spaceEigenschaft nicht festgelegtpre

Wenn Sie einen allgemeineren Ansatz benötigen, der Inhalte mit verschachtelten Elementen bearbeitet, versuchen Sie diese Antwort:

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
quelle
9
Dies funktioniert nicht, wenn andere Tags vorhanden sind. Frage: Wenn sich das Caret in einem <a>Element innerhalb des befindet <div>, welchen Versatz möchten Sie dann? Der Versatz innerhalb des Textes innerhalb des <a>?
Tim Down
3
@Richard: Nun, es keyupist wahrscheinlich das falsche Ereignis dafür, aber es wurde in der ursprünglichen Frage verwendet. getCaretPosition()selbst ist in seinen eigenen Grenzen in Ordnung.
Tim Down
3
Diese JSFIDDLE-Demo schlägt fehl, wenn ich die Eingabetaste drücke und in eine neue Zeile gehe. Die Position zeigt 0.
giorgio79
5
@ giorgio79: Ja, da der Zeilenumbruch ein <br>oder <div>-Element erzeugt, das gegen die erste in der Antwort erwähnte Annahme verstößt. Wenn Sie eine etwas allgemeinere Lösung benötigen, können Sie stackoverflow.com/a/4812022/96100
Tim Down
2
Gibt es sowieso etwas zu tun, damit es die Zeilennummer enthält?
Adjit
28

Ein paar Falten, die ich in anderen Antworten nicht sehe:

  1. Das Element kann mehrere Ebenen von untergeordneten Knoten enthalten (z. B. untergeordnete Knoten mit untergeordneten Knoten mit untergeordneten Knoten ...).
  2. Eine Auswahl kann aus verschiedenen Start- und Endpositionen bestehen (z. B. werden mehrere Zeichen ausgewählt).
  3. Der Knoten, der einen Caret-Start / Ende enthält, ist möglicherweise weder das Element noch seine direkten untergeordneten Elemente

Hier ist eine Möglichkeit, Start- und Endpositionen als Offsets zum textContent-Wert des Elements abzurufen:

// 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
quelle
3
Dies muss als die richtige Antwort ausgewählt werden. Es funktioniert mit Tags im Text (die akzeptierte Antwort nicht)
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
quelle
3
Dies funktioniert leider nicht mehr, sobald Sie die Eingabetaste drücken und in einer anderen Zeile beginnen (es beginnt wieder bei 0 - wahrscheinlich vom CR / LF aus gezählt).
Ian
Es funktioniert nicht richtig, wenn Sie einige fette und / oder kursive Wörter haben.
user2824371
14

Versuche dies:

Caret.js Holen Sie sich die Caret-Position und den Versatz aus dem Textfeld

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

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

JY Han
quelle
Das ist süß. Ich brauchte dieses Verhalten, um Caret auf das Ende eines zu setzen, contenteditable liwenn ich auf eine Schaltfläche klickte, um liden Inhalt umzubenennen .
Akinuri
@AndroidDev Ich bin nicht der Autor von Caret.js, aber haben Sie gedacht, dass das Abrufen der Caret-Position für alle gängigen Browser komplexer ist als ein paar Zeilen? Kennen oder haben Sie eine nicht aufgeblähte Alternative geschaffen, die Sie mit uns teilen können?
Adelriosantiago
8

Ein bisschen zu spät zur Party, aber für den Fall, dass jemand anderes Probleme hat. Keine der Google-Suchanfragen, die ich in den letzten zwei Tagen gefunden habe, hat etwas ergeben, das funktioniert, aber ich habe eine präzise und elegante Lösung gefunden, die immer funktioniert, unabhängig davon, wie viele verschachtelte Tags Sie haben:

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>

Es wählt bis zum Anfang des Absatzes aus und zählt dann die Länge der Zeichenfolge, um die aktuelle Position zu erhalten, und macht dann die Auswahl rückgängig, um den Cursor an die aktuelle Position zurückzusetzen. Wenn Sie dies für ein gesamtes Dokument (mehr als einen Absatz) tun möchten, ändern Sie paragraphboundarydie documentboundaryGranularität für Ihren Fall oder eine andere . Weitere Informationen finden Sie in der API . Prost! :) :)

Spitzname
quelle
1
Wenn ich jedes Mal, wenn ich den <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Cursor vor dem iTag oder einem untergeordneten HTML-Element platziere div, die Cursorposition bei 0 beginne. Gibt es eine Möglichkeit, dieser Neustartzählung zu entgehen?
VAM
Seltsam. Ich bekomme dieses Verhalten in Chrome nicht. Welchen Browser verwenden Sie?
Soubriquet
2
Es sieht so aus, als ob selection.modify in allen Browsern unterstützt wird oder nicht. 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
quelle
Dieser hat tatsächlich für mich funktioniert, ich habe alle oben versucht, sie haben es nicht getan.
iStudLion
Danke, aber es gibt auch {x: 0, y: 0} in der neuen Zeile zurück.
Hichamkazan
Dies gibt die
Pixelposition
danke, ich habe nach der Pixelposition von caret gesucht und es funktioniert gut.
Sameesh
6

window.getSelection - vs - document.selection

Dieser funktioniert für mich:

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>

Die aufrufende Leitung hängt vom Ereignistyp ab. Verwenden Sie für Schlüsselereignisse Folgendes:

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

Verwenden Sie für Mausereignisse Folgendes:

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

In diesen beiden Fällen kümmere ich mich um Bruchlinien, indem ich den Zielindex hinzufüge

Jonathan R.
quelle
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)
    }
});

Hinweis: Das Bereichsobjekt selbst kann in einer Variablen gespeichert und jederzeit erneut ausgewählt werden, es sei denn, der Inhalt des inhaltsbearbeitbaren Div ändert sich.

Referenz für IE 8 und niedriger: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Referenz für Standardbrowser (alle anderen): https://developer.mozilla.org/en/DOM/range (es sind die Mozilla-Dokumente, aber Code funktioniert auch in Chrome, Safari, Opera und IE9).

Nico Burns
quelle
1
Danke, aber wie genau bekomme ich den 'Index' der Caret-Position im div-Inhalt?
Bertvan
OK, es sieht so aus, als würde der Aufruf von .baseOffset unter .getSelection () den Trick machen. Zusammen mit Ihrer Antwort beantwortet dies meine Frage. Vielen Dank!
Bertvan
2
Leider funktioniert .baseOffset nur im Webkit (glaube ich). Außerdem erhalten Sie nur den Versatz vom unmittelbaren übergeordneten Element des Carets (wenn Sie ein <b> -Tag im <div> haben, wird der Versatz vom Beginn des <b> und nicht vom Anfang des <div> angegeben Auf Standards basierende Bereiche können range.endOffset range.startOffset range.endContainer und range.startContainer verwenden, um den Offset vom übergeordneten Knoten der Auswahl und vom Knoten selbst (einschließlich Textknoten) abzurufen. IE stellt range.offsetLeft bereit Versatz von links in Pixel und so nutzlos.
Nico Burns
Es ist am besten, das Bereichsobjekt selbst zu speichern und window.getSelection (). Addrange (Bereich) zu verwenden. <- Standards und range.select (); <- IE zum Neupositionieren des Cursors an derselben Stelle. range.insertNode (nodetoinsert); <- Standards und range.pasteHTML (HTML-Code); <- IE zum Einfügen von Text oder HTML am Cursor.
Nico Burns
Das Rangevon den meisten Browsern zurückgegebene TextRangeObjekt und das vom IE zurückgegebene Objekt sind sehr unterschiedliche Dinge, daher bin ich mir nicht sicher, ob diese Antwort viel löst.
Tim Down
3

Da ich ewig gebraucht habe , um mithilfe der neuen window.getSelection- API herauszufinden, werde ich sie für die Nachwelt freigeben . Beachten Sie, dass MDN eine umfassendere Unterstützung für window.getSelection vorschlägt. Ihr Kilometerstand kann jedoch variieren.

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

Hier ist eine jsfiddle , die beim Keyup ausgelöst wird. Beachten Sie jedoch, dass schnelles Drücken von Richtungstasten sowie schnelles Löschen Ereignisse zu überspringen scheinen.

Chris Sullivan
quelle
Funktioniert bei mir! Ich danke dir sehr.
Dmodo
Mit diesem Text ist eine Auswahl nicht mehr möglich, da er reduziert ist. Mögliches Szenario: muss bei jedem keyUp-Ereignis ausgewertet werden
hschmieder
0

Ein direkter Weg, der alle Kinder des inhaltsbearbeitbaren Div durchläuft, bis er den endContainer erreicht. Dann füge ich den Endcontainer-Offset hinzu und wir haben den Zeichenindex. Sollte mit einer beliebigen Anzahl von Verschachtelungen funktionieren. verwendet Rekursion.

Hinweis: Für die Unterstützung ist eine Polyfüllung erforderlichElement.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
quelle