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-тегами?, но это не отвечает на мой вопрос).
Есть идеи?
Ответы
Ответ 1
Другой способ сделать это - добавить временный маркер в DOM и рассчитать смещение от этого маркера. Алгоритм ищет сериализацию HTML маркера (его outerHTML
) в рамках внутренней сериализации (innerHTML
) интересующего div
. Повторный текст не является проблемой с этим решением.
Чтобы это сработало, сериализация маркера должна быть уникальной в пределах своего div.. Вы не можете управлять тем, что пользователи вводят в поле, но вы можете управлять тем, что вы положили в DOM, поэтому это не должно быть сложно достигать. В моем примере маркер сделан уникальным статически: выбирая имя класса, которое вряд ли вызовет конфликт раньше времени. Также было бы возможно сделать это динамически, проверив DOM и изменив класс до тех пор, пока он не станет уникальным.
У меня есть fiddle для него (получена из скрипки Альваро Монторо). Основная часть:
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 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()
вызывается перед добавлением маркера и после его удаления. То, что это делает, - это объединить сразу соседние текстовые узлы в один текст node. Проблема в том, что добавление маркера в DOM может привести к тому, что текст node будет разбит на два новых текстовых узла. Это приводит к утере выбора и, если он не отменен с нормализацией, приведет к тому, что Rangy не сможет восстановить выбор. Опять же, что-то легче, чем вызов normalize
, может сделать трюк, но я не хочу загружать ответ с помощью minutia.
Ответ 2
РЕДАКТИРОВАТЬ: Это старый ответ, который не работает для требования OP иметь узлы с одним и тем же текстом. Но он чище и легче, если у вас нет этого требования.
Вот один из вариантов, который вы можете использовать и который работает во всех основных браузерах:
- Получить смещение каретки в пределах node (
document.getSelection().anchorOffset
)
- Получить текст node, в котором находится карет (
document.getSelection().anchorNode.data
)
- Получить смещение этого текста в
#mydiv
с помощью indexOf()
- Добавьте значения, полученные в 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)
- Проблема. Если у вас разные узлы с одним и тем же текстом, он может вернуть смещение первого вхождения вместо реального положения каретки.
Ответ 3
ПРИМЕЧАНИЕ. Это решение работает даже в узлах с повторным текстом, но обнаруживает html-объекты (например.
) как только один символ.
Я придумал совершенно другое решение, основанное на обработке узлов. Это не так чисто, как старый ответ (см. Другой ответ), но он отлично работает, даже если есть узлы с одним и тем же текстом (требование OP).
Это описание того, как это работает:
- Создайте стек со всеми родительскими элементами node, в котором находится карет.
- Пока стек не пуст, пересекайте узлы содержащего элемента (изначально содержимое редактируемого div).
- Если node не тот, что находится в верхней части стека, добавьте его размер в смещение.
- Если node совпадает с тем, что находится в верхней части стека: вытащите его из стека, перейдите к шагу 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 a text node, add it size to the offset
offset += children[x].length;
} else {
// if it a tag node, add it 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, такие как
, считаются только одним символом.