Подтвердить что ты не робот

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-тегами?, но это не отвечает на мой вопрос). Есть идеи?

4b9b3361

Ответ 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-объекты (например. &nbsp;) как только один символ.

Я придумал совершенно другое решение, основанное на обработке узлов. Это не так чисто, как старый ответ (см. Другой ответ), но он отлично работает, даже если есть узлы с одним и тем же текстом (требование 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, такие как &nbsp;, считаются только одним символом.