Получить позицию каретки в contentEditable div

Я нахожу массу хороших, кроссбраузерных альянсов о том, как установить позицию курсора или каретки в contentEditable DIV, но не на том, как получить или найти свою позицию...

То, что я хочу сделать, это знать позицию каретки внутри этого div, на клавиатуре.

Итак, когда пользователь вводит текст, я могу в любой момент узнать его позицию курсора в div.

EDIT: Я ищу ИНДЕКС в содержимом div (текст), а не координаты курсора.

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

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

Ответ 1

В следующем коде предполагается:

  • В пределах редактируемого <div> и других узлов всегда есть один текст node
  • Измененный div не имеет свойства CSS white-space, установленного в pre

код:

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>

Ответ 2

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

Ответ 4

Несколько морщин, которые я не вижу в других ответах:

  1. элемент может содержать несколько уровней дочерних узлов (например, дочерние узлы, у которых есть дочерние узлы, у которых есть дочерние узлы...)
  2. выбор может состоять из разных начальных и конечных позиций (например, выбирается несколько символов)
  3. узел, содержащий начало/конец каретки, не может быть ни элементом, ни его прямым потомком

Вот способ получить начальную и конечную позиции в виде смещений для значения элемента textContent:

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

Ответ 5

Вроде поздно на вечеринку, но в случае, если кто-то еще борется. Ни один из поисковых запросов Google, которые я обнаружил за последние два дня, не нашел ничего, что работает, но я нашел краткое и элегантное решение, которое всегда будет работать независимо от того, сколько у вас вложенных тегов:

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

    return pos;
}

Он выбирает весь путь обратно к началу абзаца, а затем считает длину строки, чтобы получить текущую позицию, а затем отменяет выбор, чтобы вернуть курсор в текущую позицию. Если вы хотите сделать это для всего документа (более одного абзаца), то измените paragraphboundary на paragraphboundary documentboundary или любую гранулярность для вашего случая. Проверьте API для более подробной информации. Ура! :)

Ответ 6

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

Примечание: объект диапазона его само может храниться в переменной и может быть повторно выбран в любое время, если содержимое содержимого, доступного для контента, не изменится.

Ссылка для IE 8 и ниже: http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Ссылка на стандарты (все остальные): https://developer.mozilla.org/en/DOM/range (его документы mozilla, но код работает в хроме, сафари, опера и т.д.)

Ответ 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
    };
}

Ответ 8

Это работает для меня:

function getCaretCharOffsetInDiv(element) {
    var caretOffset = 0;
    if (typeof window.getSelection != "undefined") {
        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 (typeof document.selection != "undefined" && 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;
} 

вызывающая строка зависит от типа события, для ключевого события это:

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

для события мыши используйте это:

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

в этих двух случаях я занимаюсь линиями разрыва, добавляя целевой индекс

Ответ 9

Поскольку это заняло у меня целую вечность, чтобы понять, используя новый API window.getSelection, я собираюсь поделиться для потомков. Обратите внимание, что MDN предполагает более широкую поддержку window.getSelection, однако ваш пробег может отличаться.

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

    // in case there is nested doms inside editable
    while(selectedNode.parentNode !== editable) {
        selectedNode = selectedNode.parentNode;
    }

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

Вот jsfiddle, который запускается по ключу. Однако обратите внимание, что быстрое нажатие клавиш направления, а также быстрое удаление, кажется, пропускают события.

Ответ 10

Прямой путь, который перебирает всех детей contenteditable div, пока не достигнет endContainer. Затем я добавляю смещение конечного контейнера и у нас есть индекс символа. Должно работать с любым количеством вложений. использует рекурсию.

Примечание: требуется поли заливка для т.е. для поддержки Element.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;
    }
  }