javascript: focusOffset с тегами html

У меня есть contenteditable div, как следует (| = позиция курсора):

<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div> 

Я хотел бы получить текущую позицию курсора, включая теги html. Мой код:

 var offset = document.getSelection().focusOffset; 

Смещение возвращается 5 (полный текст из последнего тега), но мне нужно, чтобы он обрабатывал теги html. Ожидаемое значение возврата – 40. Код должен работать со всеми браузерами-повторителями. (Я также проверил это: window.getSelection () смещение с тегами HTML, но это не отвечает на мой вопрос). Есть идеи ?

Solutions Collecting From Web of "javascript: focusOffset с тегами html"

Другой способ сделать это – добавить временный маркер в DOM и рассчитать смещение от этого маркера. Алгоритм ищет сериализацию HTML маркера (его outerHTML ) внутри внутренней сериализации ( innerHTML ) интересующего div . Повторный текст не является проблемой с этим решением.

Чтобы это сработало, сериализация маркера должна быть уникальной в пределах своего div. Вы не можете управлять тем, что пользователь вводит в поле, но вы можете контролировать, что вы вложили в DOM, поэтому этого не должно быть трудно. В моем примере маркер сделан уникальным статически: выбирая имя класса, которое вряд ли вызовет конфликт раньше времени. Также было бы возможно сделать это динамически, проверив DOM и изменив класс до тех пор, пока он не станет уникальным.

У меня есть скрипка для него (получена из собственной скрипки Альваро Монторо). Основная часть:

 function getOffset() { if ($("." + unique).length) throw new Error("marker present in document; or the unique class is not unique"); // We could also use rangy.getSelection() but there's no reason here to do this. var sel = document.getSelection(); if (!sel.rangeCount) return; // No ranges. if (!sel.isCollapsed) return; // We work only with collapsed selections. if (sel.rangeCount > 1) throw new Error("can't handle multiple ranges"); var range = sel.getRangeAt(0); var saved = rangy.serializeSelection(); // See comment below. $mydiv[0].normalize(); range.insertNode($marker[0]); var offset = $mydiv.html().indexOf($marker[0].outerHTML); $marker.remove(); // Normalizing before and after ensures that the DOM is in the same shape before // and after the insertion and removal of the marker. $mydiv[0].normalize(); rangy.deserializeSelection(saved); return offset; } 

Как вы можете видеть, код должен компенсировать добавление и удаление маркера в DOM, поскольку это приводит к потере текущего выделения:

  1. Rangy используется для сохранения выбора и восстановления его впоследствии. Обратите внимание, что сохранение и восстановление могут выполняться с чем-то более легким, чем Rangy, но я не хотел загружать ответ с помощью minutia. Если вы решили использовать Rangy для этой задачи, прочитайте документацию, потому что можно оптимизировать сериализацию и десериализацию.

  2. Чтобы Rangy работал, DOM должен находиться в точно таком же состоянии до и после сохранения. Вот почему normalize() вызывается перед добавлением маркера и после его удаления. То, что это делает, – это объединить сразу соседние текстовые узлы в один текстовый узел. Проблема в том, что добавление маркера в DOM может привести к тому, что текстовый узел будет разбит на два новых текстовых узла. Это приводит к утере выбора и, если он не отменен с нормализацией, приведет к тому, что Rangy не сможет восстановить выбор. Опять же, что-то легче, чем вызов normalize может сделать трюк, но я не хотел загружать ответ с помощью minutia.

EDIT: Это старый ответ, который не работает для требования OP иметь узлы с одним и тем же текстом. Но это чище и легче, если у вас нет этого требования.

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

  1. Получите смещение каретки внутри своего узла ( document.getSelection().anchorOffset )
  2. Получить текст узла, в котором находится каретка ( document.getSelection().anchorNode.data )
  3. Получите смещение этого текста в #mydiv с помощью indexOf()
  4. Добавьте значения, полученные в 1 и 3, чтобы получить смещение каретки внутри div.

Код будет выглядеть так для вашего конкретного случая:

 var offset = document.getSelection().anchorOffset; var text = document.getSelection().anchorNode.data; var textOffset = $("#mydiv").html().indexOf( text ); offsetCaret = textOffset + offset; 

Вы можете увидеть рабочую демонстрацию на этом JSFiddle (просмотреть консоль, чтобы увидеть результаты).

И более общая версия функции (которая позволяет передавать div как параметр, поэтому его можно использовать с другим contenteditable ) на этом другом JSFiddle :

 function getCaretHTMLOffset(obj) { var offset = document.getSelection().anchorOffset; var text = document.getSelection().anchorNode.data; var textOffset = obj.innerHTML.indexOf( text ); return textOffset + offset; } 

Об этом ответе

  • Он будет работать во всех последних браузерах по запросу (проверен на Chrome 42, Firefox 37 и Explorer 11).
  • Он короткий и легкий и не требует никакой внешней библиотеки (даже не jQuery)
  • Проблема . Если у вас разные узлы с одним и тем же текстом, он может вернуть смещение первого вхождения вместо реального положения каретки.

ПРИМЕЧАНИЕ. Это решение работает даже в узлах с повторным текстом, но обнаруживает html-объекты (например: &nbsp; ) как только один символ.

Я придумал совершенно другое решение, основанное на обработке узлов. Это не так чисто, как старый ответ ( см. Другой ответ ), но он отлично работает, даже если есть узлы с одним и тем же текстом (требование OP).

Это описание того, как это работает:

  1. Создайте стек со всеми родительскими элементами узла, в котором находится каретка.
  2. В то время как стек не пуст, перейдите к узлам содержащего элемента (изначально содержимое редактируемого div).
  3. Если узел не совпадает с узлом в верхней части стека, добавьте его размер в смещение.
  4. Если узел такой же, как тот, который находится в верхней части стека: вытащите его из стека, перейдите к шагу 2.

Код выглядит так:

 function getCaretOffset(contentEditableDiv) { // read the node in which the caret is and store it in a stack var aux = document.getSelection().anchorNode; var stack = [ aux ]; // add the parents to the stack until we get to the content editable div while ($(aux).parent()[0] != contentEditableDiv) { aux = $(aux).parent()[0]; stack.push(aux); } // traverse the contents of the editable div until we reach the one with the caret var offset = 0; var currObj = contentEditableDiv; var children = $(currObj).contents(); while (stack.length) { // add the lengths of the previous "siblings" to the offset for (var x = 0; x < children.length; x++) { if (children[x] == stack[stack.length-1]) { // if the node is not a text node, then add the size of the opening tag if (children[x].nodeType != 3) { offset += $(children[x])[0].outerHTML.indexOf(">") + 1; } break; } else { if (children[x].nodeType == 3) { // if it's a text node, add it's size to the offset offset += children[x].length; } else { // if it's a tag node, add it's size + the size of the tags offset += $(children[x])[0].outerHTML.length; } } } // move to a more inner container currObj = stack.pop(); children = $(currObj).contents(); } // finally add the offset within the last node offset += document.getSelection().anchorOffset; return offset; } 

Вы можете увидеть рабочую демонстрацию на этом JSFiddle .


Об этом ответе:

  • Он работает во всех основных браузерах.
  • Он светлый и не требует внешних библиотек (кроме jQuery)
  • У этого есть проблема : html сущности как &nbsp; считаются только одним символом.