У меня есть 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, но это не отвечает на мой вопрос). Есть идеи ?
Другой способ сделать это – добавить временный маркер в 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, поскольку это приводит к потере текущего выделения:
Rangy используется для сохранения выбора и восстановления его впоследствии. Обратите внимание, что сохранение и восстановление могут выполняться с чем-то более легким, чем Rangy, но я не хотел загружать ответ с помощью minutia. Если вы решили использовать Rangy для этой задачи, прочитайте документацию, потому что можно оптимизировать сериализацию и десериализацию.
Чтобы Rangy работал, DOM должен находиться в точно таком же состоянии до и после сохранения. Вот почему normalize()
вызывается перед добавлением маркера и после его удаления. То, что это делает, – это объединить сразу соседние текстовые узлы в один текстовый узел. Проблема в том, что добавление маркера в DOM может привести к тому, что текстовый узел будет разбит на два новых текстовых узла. Это приводит к утере выбора и, если он не отменен с нормализацией, приведет к тому, что Rangy не сможет восстановить выбор. Опять же, что-то легче, чем вызов normalize
может сделать трюк, но я не хотел загружать ответ с помощью minutia.
EDIT: Это старый ответ, который не работает для требования OP иметь узлы с одним и тем же текстом. Но это чище и легче, если у вас нет этого требования.
Вот один из вариантов, который вы можете использовать и который работает во всех основных браузерах:
document.getSelection().anchorOffset
) document.getSelection().anchorNode.data
) #mydiv
с помощью indexOf()
Код будет выглядеть так для вашего конкретного случая:
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; }
Об этом ответе
ПРИМЕЧАНИЕ. Это решение работает даже в узлах с повторным текстом, но обнаруживает html-объекты (например:
) как только один символ.
Я придумал совершенно другое решение, основанное на обработке узлов. Это не так чисто, как старый ответ ( см. Другой ответ ), но он отлично работает, даже если есть узлы с одним и тем же текстом (требование OP).
Это описание того, как это работает:
Код выглядит так:
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 .
Об этом ответе:
считаются только одним символом.