Obtener contenido Posición de índice de intercalación editable


119

Estoy encontrando toneladas de buenas respuestas de navegadores cruzados sobre cómo CONFIGURAR el cursor o la posición del índice de intercalación en un contentEditableelemento, pero ninguna sobre cómo OBTENER o encontrar su índice ...

Lo que quiero hacer es conocer el índice del signo de intercalación dentro de este div, en keyup.

Entonces, cuando el usuario está escribiendo texto, puedo saber en cualquier momento el índice de su cursor dentro del contentEditableelemento.

EDITAR: Estoy buscando el ÍNDICE dentro del contenido div (texto), no las coordenadas del cursor.

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

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

Mira su posición en el texto. Luego, busque la última aparición de '@' antes de esa posición. Así que solo un poco de lógica de texto.
Bertvan

Además, no planeo permitir otras etiquetas dentro de <diV>, solo texto
Bertvan

ok, sí lo estoy iba a necesitar otras etiquetas dentro de la etiqueta <div>. Habrá etiquetas <a>, pero no habrá anidación ...
Bertvan

@Bertvan: si el signo de intercalación está dentro de un <a>elemento dentro de <div>, ¿qué compensación desea entonces? ¿El desplazamiento dentro del texto dentro de <a>?
Tim Down

Nunca debería estar dentro de un elemento <a>. El elemento <a> debe renderizarse en html, por lo que el usuario no puede colocar el signo de intercalación allí.
Bertvan

Respuestas:


121

El siguiente código asume:

  • Siempre hay un solo nodo de texto dentro del editable <div>y ningún otro nodo
  • El div editable no tiene la white-spacepropiedad CSS establecida enpre

Si necesita un enfoque más general que funcione con contenido con elementos anidados, pruebe esta respuesta:

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

Código:

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
Esto no funcionará si hay otras etiquetas allí. Pregunta: si el símbolo de intercalación está dentro de un <a>elemento dentro de <div>, ¿qué compensación desea entonces? ¿El desplazamiento dentro del texto dentro de <a>?
Tim Down

3
@Richard: Bueno, keyupes probable que sea el evento incorrecto para esto, pero es lo que se usó en la pregunta original. getCaretPosition()en sí mismo está bien dentro de sus propias limitaciones.
Tim Down

3
Esa demostración de JSFIDDLE falla si presiono enter y paso a una nueva línea. La posición mostrará 0.
giorgio79

5
@ giorgio79: Sí, porque el salto de línea genera un elemento <br>o <div>, que viola el primer supuesto mencionado en la respuesta. Si necesita una solución un poco más general, puede probar stackoverflow.com/a/4812022/96100
Tim Down

2
¿Hay alguna forma de hacer esto para que incluya el número de línea?
Ajuste el

28

Algunas arrugas que no veo que se aborden en otras respuestas:

  1. el elemento puede contener múltiples niveles de nodos secundarios (por ejemplo, nodos secundarios que tienen nodos secundarios que tienen nodos secundarios ...)
  2. una selección puede constar de diferentes posiciones inicial y final (por ejemplo, se seleccionan varios caracteres)
  3. el nodo que contiene un inicio / final de Caret puede no ser ni el elemento ni sus hijos directos

Esta es una forma de obtener las posiciones inicial y final como compensaciones del valor textContent del 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
Esto debe seleccionarse como la respuesta correcta. Funciona con etiquetas dentro del texto (la respuesta aceptada no lo hace)
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
Desafortunadamente, esto deja de funcionar tan pronto como presiona Enter y comienza en otra línea (comienza en 0 nuevamente, probablemente contando desde el CR / LF).
Ian

No funciona correctamente si tiene algunas palabras en negrita y / o cursiva.
user2824371

14

Prueba esto:

Caret.js Obtener la posición de intercalación y el desplazamiento del campo de texto

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

demostración: http://ichord.github.com/Caret.js


Esto es dulce. Necesitaba este comportamiento para establecer el cursor al final de un contenteditable licuando se hacía clic en un botón para cambiar liel nombre del contenido.
akinuri

@AndroidDev No soy el autor de Caret.js, pero ¿ha considerado que obtener la posición del cursor para todos los principales navegadores es más complejo que unas pocas líneas? ¿Conoces o has creado una alternativa no hinchada que puedas compartir con nosotros?
adelriosantiago

8

Un poco tarde para la fiesta, pero en caso de que alguien más tenga problemas. Ninguna de las búsquedas de Google que he encontrado durante los últimos dos días ha dado con algo que funcione, pero se me ocurrió una solución concisa y elegante que siempre funcionará sin importar cuántas etiquetas anidadas tenga:

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>

Selecciona todo el camino hasta el principio del párrafo y luego cuenta la longitud de la cadena para obtener la posición actual y luego deshace la selección para devolver el cursor a la posición actual. Si desea hacer esto para un documento completo (más de un párrafo), cambie paragraphboundarya documentboundaryo cualquier granularidad para su caso. Consulte la API para obtener más detalles . ¡Salud! :)


1
Si tengo <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Cada vez que coloco el cursor antes de la ietiqueta o cualquier elemento html secundario dentro div, la posición del cursor comienza en 0. ¿Hay alguna manera de escapar de este recuento de reinicios?
vam

Impar. No obtengo ese comportamiento en Chrome. ¿Qué navegador estás usando?
Soubriquet

2
Parece que selection.modify puede o no ser compatible con todos los navegadores. 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
    };
}

este realmente funcionó para mí, probé todos los anteriores, no lo hicieron.
iStudLion

gracias pero también devuelve {x: 0, y: 0} en la nueva línea.
hichamkazan

esto devuelve la posición del píxel, no el desplazamiento del carácter
4esn0k

gracias, estaba buscando recuperar la posición de los píxeles del símbolo de intercalación y está funcionando bien.
Sameesh

6

window.getSelection - vs - document.selection

Esta funciona para mí:

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 línea de llamada depende del tipo de evento, para evento clave use esto:

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

para el evento del mouse use esto:

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

en estos dos casos me ocupo de las líneas de ruptura agregando el índice de destino


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: el objeto de rango en sí mismo se puede almacenar en una variable y se puede volver a seleccionar en cualquier momento a menos que cambie el contenido del div contenteditable.

Referencia para IE 8 y versiones anteriores: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Referencia para navegadores estándares (todos los demás): https://developer.mozilla.org/en/DOM/range (son los documentos de mozilla, pero el código funciona en Chrome, Safari, Opera e ie9 también)


1
Gracias, pero ¿cómo obtengo exactamente el 'índice' de la posición de intercalación en el contenido div?
Bertvan

Bien, parece que llamar a .baseOffset en .getSelection () funciona. Así que esto, junto con su respuesta, responde a mi pregunta. ¡Gracias!
Bertvan

2
Desafortunadamente .baseOffset solo funciona en webkit (creo). También solo le da el desplazamiento desde el padre inmediato del signo de intercalación (si tiene una etiqueta <b> dentro del <div>, le dará el desplazamiento desde el inicio del <b>, no el inicio del <div> .Los rangos basados ​​en estándares pueden usar range.endOffset range.startOffset range.endContainer y range.startContainer para obtener el desplazamiento del nodo principal de la selección y el nodo en sí (esto incluye nodos de texto). IE proporciona range.offsetLeft, que es el desplazado desde la izquierda en píxeles , y por lo tanto inútil.
Nico Burns

Es mejor almacenar el objeto de rango en sí mismo y usar window.getSelection (). Addrange (range); <- estándares y rango.select (); <- IE para reposicionar el cursor en el mismo lugar. range.insertNode (nodo a insertar); <- estándares y rango.pasteHTML (código html); <- IE para insertar texto o html en el cursor.
Nico Burns

El Rangeobjeto devuelto por la mayoría de los navegadores y el TextRangeobjeto devuelto por IE son cosas extremadamente diferentes, por lo que no estoy seguro de que esta respuesta resuelva mucho.
Tim Down

3

Como esto me tomó una eternidad averiguar el uso de la nueva API window.getSelection que voy a compartir para la posteridad. Tenga en cuenta que MDN sugiere que hay un soporte más amplio para window.getSelection, sin embargo, su millaje puede variar.

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

Aquí hay un jsfiddle que se dispara al activar el teclado. Sin embargo, tenga en cuenta que las pulsaciones rápidas de las teclas de dirección, así como la eliminación rápida, parecen ser eventos de omisión.


¡Funciona para mi! Muchas gracias.
dmodo

Con este texto, la selección ya no es posible porque está contraído. Escenario posible: necesidad de evaluar cada evento
keyUp

0

Una forma sencilla, que itera a través de todos los niños del div contenteditable hasta que llega al endContainer. Luego agrego el desplazamiento del contenedor final y tenemos el índice de caracteres. Debería funcionar con cualquier número de nidos. utiliza la recursividad.

Nota: requiere un relleno de polietileno para que, por ejemplo, soporteElement.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;
    }
  }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.