Obter a posição do índice circunflexo contentEditable

119

Estou encontrando toneladas de boas respostas de crossbrowser sobre como AJUSTAR o cursor ou a posição do índice circunflexo em um contentEditableelemento, mas nenhuma sobre como GET ou encontrar seu índice ...

O que eu quero fazer é saber o índice do circunflexo dentro desta div, em keyup.

Portanto, quando o usuário está digitando um texto, posso saber a qualquer momento o índice do cursor dentro do contentEditableelemento.

EDIT: Estou procurando o INDEX dentro do conteúdo div (texto), não as coordenadas do cursor.

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

$('#contentbox').keyup(function() { 
    // ... ? 
});
Bertvan
fonte
Observe sua posição no texto. Em seguida, procure a última ocorrência de '@' antes dessa posição. Então, apenas alguma lógica de texto.
Bertvan
Além disso, não pretendo permitir outras tags no <diV>, apenas texto
Bertvan
ok, sim, eu estou indo precisar outras tags dentro do <div>. Haverá <a> tags, mas não haverá aninhamento ...
Bertvan
@Bertvan: se o cursor estiver dentro de um <a>elemento dentro de <div>, qual deslocamento você deseja então? O deslocamento dentro do texto dentro do <a>?
Tim Down,
Nunca deve estar dentro de um elemento <a>. O elemento <a> deve ser renderizado em html, para que o usuário não possa realmente colocar o cursor lá.
Bertvan,

Respostas:

121

O código a seguir pressupõe:

  • Sempre há um único nó de texto dentro do editável <div>e nenhum outro nó
  • O div editável não tem a white-spacepropriedade CSS definida parapre

Se você precisar de uma abordagem mais geral que funcionará com o conteúdo com elementos aninhados, tente esta resposta:

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>

Tim Down
fonte
9
Isso não funcionará se houver outras tags lá. Pergunta: se o cursor estiver dentro de um <a>elemento dentro de <div>, qual deslocamento você deseja então? O deslocamento dentro do texto dentro do <a>?
Tim Down de
3
@Richard: Bem, keyupprovavelmente é o evento errado para isso, mas é o que foi usado na pergunta original. getCaretPosition()em si está bem dentro de suas próprias limitações.
Tim Down de
3
Essa demonstração JSFIDDLE falha se eu pressiono enter e vou para uma nova linha. A posição mostrará 0.
giorgio79
5
@ giorgio79: Sim, porque a quebra de linha gera um elemento <br>ou <div>, o que viola a primeira suposição mencionada na resposta. Se você precisar de uma solução um pouco mais geral, pode tentar stackoverflow.com/a/4812022/96100
Tim Down
2
Existe alguma maneira de fazer isso para incluir o número da linha?
Adjit
28

Algumas rugas que não vejo sendo abordadas em outras respostas:

  1. o elemento pode conter vários níveis de nós filhos (por exemplo, nós filhos que têm nós filhos que têm nós filhos ...)
  2. uma seleção pode consistir em diferentes posições inicial e final (por exemplo, vários caracteres são selecionados)
  3. o nó que contém um início / fim de cursor não pode ser o elemento ou seus filhos diretos

Esta é uma maneira de obter as posições inicial e final como deslocamentos para o valor textContent do 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]];
}
mwag
fonte
3
Essa deve ser selecionada como a resposta certa. Funciona com tags dentro do texto (a resposta aceita não)
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
fonte
3
Infelizmente, isso para de funcionar assim que você pressiona enter e começa em outra linha (começa em 0 novamente - provavelmente contando a partir do CR / LF).
Ian
Não funciona corretamente se você tiver algumas palavras em negrito e / ou itálico.
user2824371
14

Experimente isto:

Caret.js Obtenha a posição do cursor e o deslocamento do campo de texto

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

demonstração: http://ichord.github.com/Caret.js

JY Han
fonte
Isto é doce. Eu precisava desse comportamento para definir o acento circunflexo para o fim de a contenteditable liquando clicava em um botão para renomear lio conteúdo.
akinuri
@AndroidDev Não sou o autor de Caret.js, mas você considerou que obter a posição do cursor para todos os principais navegadores é mais complexo do que algumas linhas? Você conhece ou criou uma alternativa não inchada que pode compartilhar conosco?
adelriosantiago
8

Meio atrasado para a festa, mas no caso de mais alguém estar com dificuldades. Nenhuma das pesquisas do Google que encontrei nos últimos dois dias resultou em algo que funcionasse, mas eu encontrei uma solução concisa e elegante que sempre funcionará, não importa quantas tags aninhadas você tenha:

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>

Ele seleciona todo o caminho de volta ao início do parágrafo e então conta o comprimento da string para obter a posição atual e, em seguida, desfaz a seleção para retornar o cursor à posição atual. Se você quiser fazer isso para um documento inteiro (mais de um parágrafo), mude paragraphboundarypara documentboundaryou qualquer granularidade para o seu caso. Confira a API para mais detalhes . Felicidades! :)

Soubriquet
fonte
1
Se eu tiver <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Everytime I posicionar o cursor antes da itag ou qualquer elemento html filho dentro div, a posição do cursor está começando em 0. Existe uma maneira de escapar dessa contagem de reinicialização?
vam
Ímpar. Não estou obtendo esse comportamento no Chrome. Qual navegador você está usando?
Soubriquet de
2
Parece que selection.modify pode ou não ser compatível com todos os 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
    };
}
Nishad Up
fonte
este realmente funcionou para mim, eu tentei todos os acima, não funcionaram.
iStudLion
obrigado, mas também retorna {x: 0, y: 0} na nova linha.
hichamkazan
isso retorna a posição do pixel, não o deslocamento do caractere
4esn0k
obrigado, eu estava procurando recuperar a posição do pixel do cursor e está funcionando bem.
Sameesh
6

window.getSelection - vs - document.selection

Este funciona para mim:

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>

A linha de chamada depende do tipo de evento, para evento-chave use isto:

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

para eventos de mouse, use isto:

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

nesses dois casos, cuido das linhas de quebra adicionando o índice de destino

Jonathan R.
fonte
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: o próprio objeto de intervalo pode ser armazenado em uma variável e pode ser selecionado novamente a qualquer momento, a menos que o conteúdo do div editável pelo conteúdo seja alterado.

Referência para IE 8 e inferior: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Referência para navegadores padrão (todos os outros): https://developer.mozilla.org/en/DOM/range (são os documentos do mozilla, mas o código funciona em chrome, safari, opera e ie9 também)

Nico Burns
fonte
1
Obrigado, mas como exatamente obtenho o 'índice' da posição do cursor no conteúdo div?
Bertvan
OK, parece que chamar .baseOffset em .getSelection () resolve. Então isso, junto com sua resposta, responde à minha pergunta. Obrigado!
Bertvan
2
Infelizmente .baseOffset só funciona no webkit (eu acho). Também fornece apenas o deslocamento do pai imediato do circunflexo (se você tiver uma <b> tag dentro do <div>, ele fornecerá o deslocamento do início do <b>, não do início do <div> . Os intervalos baseados em padrões podem usar range.endOffset range.startOffset range.endContainer e range.startContainer para obter o deslocamento do pai da seleção e o próprio nó (isso inclui nós de texto). O IE fornece range.offsetLeft, que é o deslocado da esquerda em pixels , e tão inútil.
Nico Burns,
É melhor apenas armazenar o próprio objeto range e usar window.getSelection (). Addrange (range); <- padrões e range.select (); <- IE para reposicionar o cursor no mesmo lugar. range.insertNode (nodetoinsert); <- padrões e intervalo.pasteHTML (código html); <- IE para inserir texto ou html no cursor.
Nico Burns
O Rangeobjeto retornado pela maioria dos navegadores e o TextRangeobjeto retornado pelo IE são coisas extremamente diferentes, então não tenho certeza se esta resposta resolve muito.
Tim Down
3

Como isso me levou uma eternidade para descobrir como usar a nova API window.getSelection que irei compartilhar para a posteridade. Observe que o MDN sugere que haja um suporte mais amplo para window.getSelection; no entanto, sua milhagem pode 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,
    }
} 

Aqui está um jsfiddle que dispara no keyup. Observe, entretanto, que os pressionamentos rápidos das teclas direcionais, bem como a exclusão rápida, parecem ser eventos ignorados.

Chris Sullivan
fonte
Funciona para mim! Muito obrigado.
dmodo
Com esta seleção de texto não é mais possível, pois está recolhido. Cenário possível: necessidade de avaliar em cada evento keyUp
hschmieder
0

Uma maneira direta, que itera por todos os filhos do div contenteditable até atingir o endContainer. Em seguida, adiciono o deslocamento final do contêiner e temos o índice de caracteres. Deve funcionar com qualquer número de assentamentos. usa recursão.

Nota: requer um poli preenchimento para, por exemplo, apoiarElement.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
fonte