Получить контент Редактируемое положение курсора

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

Я хочу знать позицию курсора в div на keyup. Итак, когда пользователь вводит текст, я могу в любой момент узнать положение курсора внутри элемента contentEditable.

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

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

person Bertvan    schedule 19.10.2010    source источник
comment
Посмотрите на его положение в тексте. Затем найдите последнее появление символа «@» перед этой позицией. Так что немного текстовой логики.   -  person Bertvan    schedule 20.10.2010
comment
Кроме того, я не планирую разрешать использование других тегов в ‹diV›, только текст   -  person Bertvan    schedule 20.10.2010
comment
хорошо, да, мне мне понадобятся другие теги в ‹div›. Теги ‹a› будут, а вложенности не будет ...   -  person Bertvan    schedule 20.10.2010
comment
@Bertvan: если курсор находится внутри элемента <a> внутри <div>, какое смещение вы хотите тогда? Смещение внутри текста внутри <a>?   -  person Tim Down    schedule 21.10.2010
comment
Он никогда не должен находиться внутри элемента ‹a›. Элемент ‹a› должен быть отрисован в формате html, чтобы пользователь не мог поместить туда курсор.   -  person Bertvan    schedule 21.10.2010
comment
Вы нашли способ получить положение курсора вокруг дочерних элементов html? Я пытаюсь решить ту же проблему :(   -  person wilsonpage    schedule 10.05.2011
comment
Этот ответ может помочь. Код лучше, чем все ответы здесь. Я не знаю, работает ли он на все 100%, но он работает для всего, что мне нужно. stackoverflow.com/a/64823701/3245937   -  person user875234    schedule 13.11.2020


Ответы (14)


Следующий код предполагает:

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

Если вам нужен более общий подход, который будет работать с вложенными элементами, попробуйте этот ответ:

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

Код:

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>

person Tim Down    schedule 20.10.2010
comment
Извините, пришлось отменить ответ: мне мне понадобятся другие теги. Внутри ‹div› будут теги ‹a›, но без вложенности. Проверим ваше решение, возможно, это даже сработает для того, что мне нужно ... - person Bertvan; 20.10.2010
comment
О, и я знаю, что сначала сказал, что там не будет никаких других тегов. Извините моя ошибка. - person Bertvan; 20.10.2010
comment
Это не сработает, если там есть другие теги. Вопрос: если курсор находится внутри элемента <a> внутри <div>, какое смещение тогда вам нужно? Смещение внутри текста внутри <a>? - person Tim Down; 20.10.2010
comment
Возможно, вы захотите удалить console.log, так как это приведет к сбою в браузерах, в которых не установлен firebug. - person Nico Burns; 20.10.2010
comment
Вы нашли способ получить положение курсора вокруг дочерних элементов html? Я пытаюсь решить ту же проблему :( - person wilsonpage; 10.05.2011
comment
@pagewil: Я ответил на новый вопрос, который вы создали (stackoverflow.com/questions/5951886/) - person Tim Down; 10.05.2011
comment
Я нашел ваш фрагмент, когда искал эту проблему, но в своем решении я использую свойство white-space: pre. Не могли бы вы дать некоторое представление о том, как отредактировать фрагмент кода, чтобы справиться с этим (или некоторую информацию о том, как по-другому обрабатывается курсор)? Я ценю вашу помощь. Спасибо! - person jedd.ahyoung; 31.12.2012
comment
Я использую этот код для получения позиции курсора для браузеров, отличных от IE, но моя проблема в том, что для каждой новой строки позиция каретки начинается с 0. Я бы хотел, чтобы он также включал количество символов в предыдущих строках. Является ли это возможным? Я также задавал этот вопрос здесь (stackoverflow.com/questions/14565572/) - person Crista23; 30.01.2013
comment
Это не работает в Firefox (а также в IE). возвращает 0 или 1 вместо 4, когда я набираю when в своем Contenteditable div. Chrome возвращает 4, как и ожидалось. - person Mr_Green; 26.04.2013
comment
@TimDown Я понял. Введите when, затем нажмите ENTER, а затем нажмите SPACE. вывод в firefox - 0. Это происходит из-за функции placeCaretAtEnd(), которую вы упомянули здесь. Вот скрипка. Пожалуйста помоги. - person Mr_Green; 26.04.2013
comment
Как можно изменить этот код для работы с дочерними узлами? Чтобы позиция курсора учитывала html-теги и т. Д. - person Rafael; 28.05.2014
comment
Остерегайтесь этого кода, если вы набираете очень быстро, одно и то же число будет отображаться несколько раз (по крайней мере, в хроме). Вот скрипка - person Richard; 20.08.2014
comment
@Richard: Ну, keyup, скорее всего, не подходит для этого, но это то, что было использовано в исходном вопросе. getCaretPosition() сам по себе хорош в пределах своих ограничений. - person Tim Down; 20.08.2014
comment
Эта демонстрация JSFIDDLE терпит неудачу, если я нажимаю Enter и перехожу на новую строку. Позиция покажет 0. - person giorgio79; 21.08.2014
comment
@ giorgio79: Да, потому что разрыв строки создает элемент <br> или <div>, что нарушает первое предположение, упомянутое в ответе. Если вам нужно более общее решение, вы можете попробовать stackoverflow.com/a/4812022/96100 - person Tim Down; 21.08.2014
comment
Да просто тестирую. Пока круто :) - person giorgio79; 21.08.2014
comment
Есть ли способ сделать это, чтобы включить номер строки? - person Adjit; 18.07.2016
comment
Почему для этого решения важно правило CSS? - person sarkiroka; 30.04.2020
comment
@sarkiroka: Правило CSS не важно. Это просто для того, чтобы показать некоторую визуальную разницу между редактируемыми и нередактируемыми частями демонстрации. - person Tim Down; 04.05.2020

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

  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]];
}
person mwag    schedule 03.11.2018
comment
Это должно быть выбрано как правильный ответ. Он работает с тегами внутри текста (в принятом ответе нет) - person hamboy75; 11.04.2019
comment
Есть ли способ включить разрывы строк? Нажатие Enter не меняет результат этой функции. Также я знаю, что это не упоминается в вопросе, но эквивалентный setCaretPosition был бы очень полезен, чтобы увидеть - person Connor; 21.08.2020
comment
Повторные символы новой строки: да, но это несколько более запутанное решение. новые строки представлены в текстовых узлах как безтекстовые узлы BR, вставленные в дерево узлов, которые не отражаются должным образом в textContent. Итак, для их обработки любая ссылка на textContent должна быть заменена функцией, например. getNodeInnerText (), который будет обходить дерево узлов и создавать правильную текстовую строку, и, в частности, вставляет \ n для любых узлов BR (в большинстве случаев - это более тонко, чем это) - person mwag; 21.08.2020
comment
setCaretPosition задается / отвечает здесь: stackoverflow.com/questions/512528/ (хотя я использую модифицированную версию решения, не помню почему) - person mwag; 21.08.2020
comment
Привет. Опоздал на вечеринку на несколько лет. Вопрос, как использовать ваш код? Если я хочу, например, отобразить позицию курсора? Как вы вручную сохраняете позицию, а затем загружаете ее? Спасибо за ответ. Я искал что-то подобное, что работает уже несколько дней. - person WeAreDoomed; 30.03.2021
comment
Этот ответ не работает, если не установлены window.getSelection (). AnchorNode и window.getSelection (). ExtensionNode - person Patlatus; 06.04.2021
comment
@WeAreDoomed см. Вышеупомянутый комментарий re setCaretPosition - person mwag; 08.04.2021

Попробуй это:

Caret.js Получить позицию и смещение курсора из текстового поля

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

демонстрация: http://ichord.github.com/Caret.js

person J.Y Han    schedule 02.06.2014
comment
Это мило. Мне нужно было такое поведение, чтобы установить курсор в конец contenteditable li при нажатии кнопки для переименования содержимого li. - person akinuri; 26.02.2018
comment
@AndroidDev Я не являюсь автором Caret.js, но считали ли вы, что получение позиции курсора для всех основных браузеров сложнее, чем несколько строк? Знаете ли вы или создали не раздутую альтернативу, которой вы можете поделиться с нами? - person adelriosantiago; 28.11.2019

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

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>

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

person Soubriquet    schedule 23.01.2019
comment
Если у меня <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> Каждый раз, когда я помещаю курсор перед тегом i или любым дочерним элементом html внутри div, позиция курсора начинается с 0. Есть ли способ избежать этого счетчика перезапуска? - person vam; 05.02.2019
comment
Странный. Я не получаю такого поведения в Chrome. Какой браузер вы используете? - person Soubriquet; 08.02.2019
comment
Похоже, что selection.modify может поддерживаться, а может и не поддерживаться во всех браузерах. developer.mozilla.org/en-US/docs/Web/API/ Выбор - person Chris Sullivan; 07.04.2019
comment
Очень хорошо. Молодец. - person Len White; 20.12.2020
comment
Не работает в Firefox: / NS_ERROR_NOT_IMPLEMENTED selection.modify похоже, что он действительно не поддерживается в этом браузере: developer.mozilla.org/en-US/docs/Web/API/Selection/modify - person fguillen; 07.07.2021

window.getSelection - vs - document.selection

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

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>

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

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

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

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

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

person Jonathan R.    schedule 22.05.2015
comment
единственное решение, которое работает и с новыми строками, спасибо - person Hannes Schneidermayer; 02.02.2021

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

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

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

Справочник по стандартным (всем остальным) браузерам: https://developer.mozilla.org/en/DOM/range (это документы Mozilla, но код также работает в Chrome, Safari, Opera и IE9)

person Nico Burns    schedule 19.10.2010
comment
Спасибо, но как мне получить «индекс» позиции курсора в содержимом div? - person Bertvan; 20.10.2010
comment
Хорошо, похоже, что вызов .baseOffset на .getSelection () делает свое дело. Итак, это вместе с вашим ответом отвечает на мой вопрос. Спасибо! - person Bertvan; 20.10.2010
comment
К сожалению .baseOffset работает только в webkit (я думаю). Он также дает только смещение от промежуточного родителя каретки (если у вас есть тег ‹b› внутри ‹div›, он даст смещение от начала ‹b›, а не начала ‹div› Для диапазонов на основе стандартов можно использовать range.endOffset range.startOffset range.endContainer и range.startContainer для получения смещения от родительского node выделения и самого узла (включая текстовые узлы). IE предоставляет range.offsetLeft, который является смещением слева в пикселях, и поэтому бесполезен. - person Nico Burns; 20.10.2010
comment
Лучше всего сохранить сам объект диапазона и использовать window.getSelection (). Addrange (range); ‹- стандарты и range.select (); ‹- IE для перемещения курсора на то же место. range.insertNode (nodetoinsert); ‹- стандарты и range.pasteHTML (htmlcode); ‹- IE для вставки текста или HTML в курсор. - person Nico Burns; 20.10.2010
comment
Объект Range, возвращаемый большинством браузеров, и объект TextRange, возвращаемый IE, - это совершенно разные вещи, поэтому я не уверен, что этот ответ многое решает. - person Tim Down; 20.10.2010
comment
@Nico (непосредственный родитель): я не планирую разрешать использование других тегов в ‹diV›, только текст - person Bertvan; 20.10.2010
comment
@Tim: На данный момент он работает в Webkit, поэтому я счастлив, но если появится лучшее (кроссбраузерное) решение, я изменю ответ. - person Bertvan; 20.10.2010

Поскольку это заняло у меня целую вечность, чтобы понять, используя новый window.getSelection API Я поделюсь для потомков. Обратите внимание, что 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,
        };
    }

    // 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, который срабатывает при нажатии клавиши. Однако обратите внимание, что быстрое нажатие клавиш со стрелками, а также быстрое удаление, похоже, являются пропущенными событиями.

person Chris Sullivan    schedule 07.04.2019
comment
Работает для меня! Большое спасибо. - person dmodo; 11.09.2019
comment
С этим текстом выделение больше невозможно, так как оно свернуто. Возможный сценарий: необходимо оценивать каждое событие keyUp - person hschmieder; 28.09.2019

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

Примечание: требуется poly fill для ie поддерживать 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;
    }
  }
person alockwood05    schedule 16.04.2019

Если вы установите редактируемый стиль div на display: inline-block; white-space: pre-wrap вы не получаете новые дочерние div при вводе новой строки, вы просто получаете символ LF (т.е. & # 10) ;.

function showCursPos(){
    selection = document.getSelection();
    childOffset = selection.focusOffset;
    const range = document.createRange();
    eDiv = document.getElementById("eDiv");
    range.setStart(eDiv, 0);
    range.setEnd(selection.focusNode, childOffset);
    var sHtml = range.toString();
    p = sHtml.length; 
    sHtml=sHtml.replace(/(\r)/gm, "\\r");
    sHtml=sHtml.replace(/(\n)/gm, "\\n");
    document.getElementById("caretPosHtml").value=p;
    document.getElementById("exHtml").value=sHtml;   
  }
click/type in div below:
<br>
<div contenteditable name="eDiv" id="eDiv"  
     onkeyup="showCursPos()" onclick="showCursPos()" 
     style="width: 10em; border: 1px solid; display:inline-block; white-space: pre-wrap; "
     >123&#13;&#10;456&#10;789</div>
<p>
html caret position:<br> <input type="text" id="caretPosHtml">
<p>  
html from start of div:<br> <input type="text" id="exHtml">

Я заметил, что когда вы нажимаете Enter в редактируемом div, он создает новый узел, поэтому focusOffset сбрасывается до нуля. Вот почему мне пришлось добавить переменную диапазона и расширить ее от focusOffset дочерних узлов до начала eDiv (и, таким образом, захватить весь текст между ними).

person will    schedule 08.01.2021
comment
У меня это отлично работает в Chrome и Firefox - person fguillen; 07.07.2021

Этот основан на ответе @ alockwood05 и предоставляет функции получения и установки для каретки с вложенными тегами внутри contenteditable div, а также смещения внутри узлов, так что у вас есть решение, которое может быть как сериализуемым, так и десериализуемым с помощью смещений.

Я использую это решение в кросс-платформенном редакторе кода, которому необходимо получить начальную / конечную позицию курсора до выделения синтаксиса с помощью лексера / парсера, а затем сразу же вернуть его обратно.

function countUntilEndContainer(parent, endNode, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
            countingState.done = true;
            countingState.offsetInNode = offset;
            return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
            countingState.offsetInNode = offset;
            countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilEndContainer(node, endNode, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function countUntilOffset(parent, offset, countingState = {count: 0}) {
    for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node.nodeType === Node.TEXT_NODE) {
            if (countingState.count <= offset && offset < countingState.count + node.length)
            {
                countingState.offsetInNode = offset - countingState.count;
                countingState.node = node; 
                countingState.done = true; 
                return countingState; 
            }
            else { 
                countingState.count += node.length; 
            }
        } else if (node.nodeType === Node.ELEMENT_NODE) {
            countUntilOffset(node, offset, countingState);
        } else {
            countingState.error = true;
        }
    }
    return countingState;
}

function getCaretPosition()
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);    
    let start = countUntilEndContainer(editor, range.startContainer, range.startOffset);
    let end = countUntilEndContainer(editor, range.endContainer, range.endOffset);
    let offsetsCounts = { start: start.count + start.offsetInNode, end: end.count + end.offsetInNode };
    let offsets = { start: start, end: end, offsets: offsetsCounts };
    return offsets;
}

function setCaretPosition(start, end)
{
    let editor = document.getElementById('editor');
    let sel = window.getSelection();
    if (sel.rangeCount === 0) { return null; }
    let range = sel.getRangeAt(0);
    let startNode = countUntilOffset(editor, start);
    let endNode = countUntilOffset(editor, end);
    let newRange = new Range();
    newRange.setStart(startNode.node, startNode.offsetInNode);
    newRange.setEnd(endNode.node, endNode.offsetInNode);
    sel.removeAllRanges();
    sel.addRange(newRange);
    return true;
}
person John Ernest    schedule 06.02.2021

Этот работает для угловых

private getCaretPosition() {
   let caretRevCount = 0;
   if (window.getSelection) {
      const selection = window.getSelection();
      const currentNode = selection.focusNode.parentNode;
      caretRevCount = selection.focusOffset;
      let previousNode = currentNode.previousSibling;
      while(previousNode && previousNode.nodeName === 'SPAN') { 
      // you can check specific element
      caretRevCount += previousNode.textContent.length;
      previousNode = previousNode.previousSibling;
      }
    }
    return caretRevCount;
}
person Parthybaraja V    schedule 02.04.2021
comment
Добро пожаловать в Stack Overflow. Уважаемый @Parthybaraja V, пожалуйста, ответьте на вопросы более подробно. - person CyberEternal; 03.04.2021

Этот ответ работает с вложенными текстовыми элементами с использованием рекурсивных функций. ????

Бонус: устанавливает положение курсора в сохраненное положение.

function getCaretData(elem) {
  var sel = window.getSelection();
  return [sel.anchorNode, sel.anchorOffset];
}

function setCaret(el, pos) {
  var range = document.createRange();
  var sel = window.getSelection();
  range.setStart(el,pos);
  range.collapse(true);
  sel.removeAllRanges();
  sel.addRange(range);
}


let indexStack = [];

function checkParent(elem) {
  
  let parent = elem.parentNode;
  let parentChildren = Array.from(parent.childNodes);
  
  let elemIndex = parentChildren.indexOf(elem);
  
  indexStack.unshift(elemIndex);
  
  if (parent !== cd) {
    
    checkParent(parent);
    
  } else {
    
    return;
    
  }
  
}

let stackPos = 0;
let elemToSelect;

function getChild(parent, index) {
  
  let child = parent.childNodes[index];
  
  if (stackPos < indexStack.length-1) {
    
    stackPos++;
        
    getChild(child, indexStack[stackPos]);
    
  } else {
    
    elemToSelect = child;
    
    return;
    
  }
  
}


let cd = document.querySelector('.cd'),
    caretpos = document.querySelector('.caretpos');

cd.addEventListener('keyup', () => {
  
  let caretData = getCaretData(cd);
  
  let selectedElem = caretData[0];
  let caretPos = caretData[1];
  
  
  indexStack = [];
  checkParent(selectedElem);
    
  
  cd.innerHTML = 'Hello world! <span>Inline! <span>In inline!</span></span>';
  
  
  stackPos = 0;
  getChild(cd, indexStack[stackPos]);
  
  
  setCaret(elemToSelect, caretPos);
  
  
  caretpos.innerText = 'indexStack: ' + indexStack + '. Got child: ' + elemToSelect.data + '. Moved caret to child at pos: ' + caretPos;
  
})
.cd, .caretpos {
  font-family: system-ui, Segoe UI, sans-serif;
  padding: 10px;
}

.cd span {
  display: inline-block;
  color: purple;
  padding: 5px;
}

.cd span span {
  color: chocolate;
  padding: 3px;
}

:is(.cd, .cd span):hover {
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px #005ecc;
}
<div class="cd" contenteditable="true">Hello world! <span>Inline! <span>In inline!</span></span></div>
<div class="caretpos">Move your caret inside the elements above ⤴</div>

Codepen

person barhatsor    schedule 30.06.2021

person    schedule
comment
К сожалению, это перестает работать, как только вы нажимаете Enter и начинаете с другой строки (она снова начинается с 0 - вероятно, считая от CR / LF). - person Ian; 13.09.2016
comment
Он не будет работать должным образом, если у вас есть слова, выделенные жирным шрифтом и / или курсивом. - person user2824371; 30.01.2018

person    schedule
comment
это действительно сработало для меня, я пробовал все вышеперечисленные, они не работали. - person Mxlvin; 28.12.2018
comment
спасибо, но он также возвращает {x: 0, y: 0} в новой строке. - person hichamkazan; 07.03.2019
comment
это возвращает позицию пикселя, а не смещение символа - person 4esn0k; 28.03.2020
comment
спасибо, я искал извлечение пиксельной позиции из каретки, и она работает нормально. - person Sameesh; 14.06.2020
comment
Спасибо! Это то, что я искал, координаты, а не смещение символа, похоже, работает! - person Ariel Frischer; 20.11.2020