Javascript Contenteditable - установить курсор/карет для индексации

Как я могу изменить это (Как установить позицию курсора (курсора) в контентном элементе (div)?), чтобы он принимал числовой индекс и элемент и устанавливает позицию курсора в этот индекс?

Например: Если бы у меня был параграф:

<p contenteditable="true">This is a paragraph.</p>

И я позвонил:

setCaret($(this).get(0), 3)

Курсор переместится на индекс 3 следующим образом:

Thi|s is a paragraph.

У меня есть это, но не повезло:

function setCaret(contentEditableElement, index)
{
    var range,selection;
    if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
    {
        range = document.createRange();//Create a range (a range is a like the selection but invisible)
        range.setStart(contentEditableElement,index);
        range.collapse(true);
        selection = window.getSelection();//get the selection object (allows you to change selection)
        selection.removeAllRanges();//remove any selections already made
        selection.addRange(range);//make the range you have just created the visible selection
    }
    else if(document.selection)//IE 8 and lower
    { 
        range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
        range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        range.select();//Select the range (make it the visible selection
    }
}

http://jsfiddle.net/BanQU/4/

Ответ 1

Вот ответ, адаптированный из Сохранение изменений объектов диапазона после выбора в HTML. Имейте в виду, что это не так идеально по нескольким причинам (как и MaxArt, который использует тот же подход): во-первых, учитываются только текстовые узлы, что означает, что разрывы строк подразумеваются <br>, а элементы блока не включены в индекс; во-вторых, рассматриваются все текстовые узлы, даже те элементы, которые скрыты CSS или внутри элементов <script>; в-третьих, последовательные символы пробела, которые рухнули на странице, все включены в индекс; наконец, правила IE <= 8 отличаются друг от друга, поскольку он использует другой механизм.

var setSelectionByCharacterOffsets = null;

if (window.getSelection && document.createRange) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var charIndex = 0, range = document.createRange();
        range.setStart(containerEl, 0);
        range.collapse(true);
        var nodeStack = [containerEl], node, foundStart = false, stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var nextCharIndex = charIndex + node.length;
                if (!foundStart && start >= charIndex && start <= nextCharIndex) {
                    range.setStart(node, start - charIndex);
                    foundStart = true;
                }
                if (foundStart && end >= charIndex && end <= nextCharIndex) {
                    range.setEnd(node, end - charIndex);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
} else if (document.selection) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(containerEl);
        textRange.collapse(true);
        textRange.moveEnd("character", end);
        textRange.moveStart("character", start);
        textRange.select();
    };
}

Ответ 2

range.setStart и range.setEnd могут использоваться в текстовых узлах, а не в узлах элементов. Или же они вызовут исключение DOM. Итак, что вам нужно сделать, это

range.setStart(contentEditableElement.firstChild, index);

Я не понимаю, что вы сделали для IE8 и ниже. Где вы хотели использовать index?

В целом, ваш код выходит из строя, если содержимое узлов больше, чем один текст node. Это может произойти для узлов с isContentEditable === true, поскольку пользователь может вставлять текст из Word или других мест или создавать новую строку и т.д.

Здесь адаптация того, что я сделал в моей структуре:

var setSelectionRange = function(element, start, end) {
    var rng = document.createRange(),
        sel = getSelection(),
        n, o = 0,
        tw = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, null);
    while (n = tw.nextNode()) {
        o += n.nodeValue.length;
        if (o > start) {
            rng.setStart(n, n.nodeValue.length + start - o);
            start = Infinity;
        }
        if (o >= end) {
            rng.setEnd(n, n.nodeValue.length + end - o);
            break;
        }
    }
    sel.removeAllRanges();
    sel.addRange(rng);
};

var setCaret = function(element, index) {
    setSelectionRange(element, index, index);
};

Трюк здесь заключается в использовании функции setSelectionRange, которая выбирает диапазон текста внутри и элемента - с помощью start === end. В элементах contentEditable это помещает каретку в нужное положение.

Это должно работать во всех современных браузерах и для элементов, которые имеют не только текст node как потомок. Я позволю вам добавить проверки для start и end в подходящий диапазон.

Для IE8 и ниже все немного сложнее. Все будет выглядеть примерно так:

var setSelectionRange = function(element, start, end) {
    var rng = document.body.createTextRange();
    rng.moveToElementText(element);
    rng.moveStart("character", start);
    rng.moveEnd("character", end - element.innerText.length - 1);
    rng.select();
};

Проблема здесь в том, что innerText не подходит для такого рода вещей, поскольку некоторые белые пробелы рушится. Все прекрасно, если есть только текст node, но прикручены к чему-то более сложному, как те, что вы получаете в contentEditable.

IE8 не поддерживает textContent, поэтому вам нужно подсчитать символы, используя TreeWalker. Но чем снова IE8 не поддерживает TreeWalker, так что вам нужно пройти дерево DOM самостоятельно...

Мне все еще нужно это исправить, но почему-то я сомневаюсь, что когда-нибудь буду. Даже если бы я сделал код polyfill для TreeWalker в IE8 и ниже...

Ответ 3

Вот мое улучшение по поводу ответа Тима. Он удаляет оговорку о скрытых символах, но остальные оговорки остаются:

  • учитываются только текстовые узлы (разрывы строк, подразумеваемые <br> и блочные элементы не включены в индекс)
  • рассматриваются все текстовые узлы, даже те элементы, которые скрыты CSS или внутри элементов
  • Правила IE <= 8 отличаются друг от друга, поскольку он использует другой механизм.

Код:

var setSelectionByCharacterOffsets = null;

if (window.getSelection && document.createRange) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var charIndex = 0, range = document.createRange();
        range.setStart(containerEl, 0);
        range.collapse(true);
        var nodeStack = [containerEl], node, foundStart = false, stop = false;

        while (!stop && (node = nodeStack.pop())) {
            if (node.nodeType == 3) {
                var hiddenCharacters = findHiddenCharacters(node, node.length)
                var nextCharIndex = charIndex + node.length - hiddenCharacters;

                if (!foundStart && start >= charIndex && start <= nextCharIndex) {
                    var nodeIndex = start-charIndex
                    var hiddenCharactersBeforeStart = findHiddenCharacters(node, nodeIndex)
                    range.setStart(node, nodeIndex + hiddenCharactersBeforeStart);
                    foundStart = true;
                }
                if (foundStart && end >= charIndex && end <= nextCharIndex) {
                    var nodeIndex = end-charIndex
                    var hiddenCharactersBeforeEnd = findHiddenCharacters(node, nodeIndex)
                    range.setEnd(node, nodeIndex + hiddenCharactersBeforeEnd);
                    stop = true;
                }
                charIndex = nextCharIndex;
            } else {
                var i = node.childNodes.length;
                while (i--) {
                    nodeStack.push(node.childNodes[i]);
                }
            }
        }

        var sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
    }
} else if (document.selection) {
    setSelectionByCharacterOffsets = function(containerEl, start, end) {
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(containerEl);
        textRange.collapse(true);
        textRange.moveEnd("character", end);
        textRange.moveStart("character", start);
        textRange.select();
    };
}

var x = document.getElementById('a')
x.focus()
setSelectionByCharacterOffsets(x, 1, 13)

function findHiddenCharacters(node, beforeCaretIndex) {
    var hiddenCharacters = 0
    var lastCharWasWhiteSpace=true
    for(var n=0; n-hiddenCharacters<beforeCaretIndex &&n<node.length; n++) {
        if([' ','\n','\t','\r'].indexOf(node.textContent[n]) !== -1) {
            if(lastCharWasWhiteSpace)
                hiddenCharacters++
            else
                lastCharWasWhiteSpace = true
        } else {
            lastCharWasWhiteSpace = false   
        }
    }

    return hiddenCharacters
}