Как установить позицию курсора (курсора) в contenteditable элементе (div)? - PullRequest
155 голосов
/ 06 июня 2011

В качестве примера у меня есть простой HTML:

<div id="editable" contenteditable="true">
  text text text<br>
  text text text<br>
  text text text<br>
</div>
<button id="button">focus</button>

Я хочу простую вещь - когда я нажимаю кнопку, я хочу поместить курсор (курсор) в определенное место в редактируемом div.При поиске в Интернете этот JS привязан к нажатию кнопки, но он не работает (FF, Chrome):

var range = document.createRange();
var myDiv = document.getElementById("editable");
range.setStart(myDiv, 5);
range.setEnd(myDiv, 5);

Можно ли вручную установить позицию каретки, как эта?

Ответы [ 7 ]

219 голосов
/ 06 июня 2011

В большинстве браузеров вам нужны объекты Range и Selection.Вы указываете каждую из границ выбора как узел и смещение в этом узле.Например, чтобы установить каретку на пятый символ второй строки текста, вы должны сделать следующее:

var el = document.getElementById("editable");
var range = document.createRange();
var sel = window.getSelection();
range.setStart(el.childNodes[2], 5);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);

IE <9 работает совершенно иначе.Если вам необходимо поддерживать эти браузеры, вам потребуется другой код. </p>

Пример jsFiddle: http://jsfiddle.net/timdown/vXnCM/

47 голосов
/ 08 декабря 2016

Большинство ответов, которые вы найдете о приемлемом позиционировании курсора, довольно просты в том, что они обслуживают только ввод с простым ванильным текстом.Как только вы используете элементы html в контейнере, введенный текст разбивается на узлы и свободно распределяется по древовидной структуре.

Для установки позиции курсора у меня есть эта функция, которая зацикливает все дочерние текстовые узлы в предоставленном узле.и устанавливает диапазон от начала начального узла до символа chars.count :

function createRange(node, chars, range) {
    if (!range) {
        range = document.createRange()
        range.selectNode(node);
        range.setStart(node, 0);
    }

    if (chars.count === 0) {
        range.setEnd(node, chars.count);
    } else if (node && chars.count >0) {
        if (node.nodeType === Node.TEXT_NODE) {
            if (node.textContent.length < chars.count) {
                chars.count -= node.textContent.length;
            } else {
                range.setEnd(node, chars.count);
                chars.count = 0;
            }
        } else {
           for (var lp = 0; lp < node.childNodes.length; lp++) {
                range = createRange(node.childNodes[lp], chars, range);

                if (chars.count === 0) {
                    break;
                }
            }
        }
    } 

    return range;
};

Затем я вызываю подпрограмму с этой функцией:

function setCurrentCursorPosition(chars) {
    if (chars >= 0) {
        var selection = window.getSelection();

        range = createRange(document.getElementById("test").parentNode, { count: chars });

        if (range) {
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
        }
    }
};

Range.collapse (false) устанавливает курсор в конец диапазона.Я протестировал его с последними версиями Chrome, IE, Mozilla и Opera, и все они отлично работают.

PS.Если кому-то интересно, я получаю текущую позицию курсора, используя этот код:

function isChildOf(node, parentId) {
    while (node !== null) {
        if (node.id === parentId) {
            return true;
        }
        node = node.parentNode;
    }

    return false;
};

function getCurrentCursorPosition(parentId) {
    var selection = window.getSelection(),
        charCount = -1,
        node;

    if (selection.focusNode) {
        if (isChildOf(selection.focusNode, parentId)) {
            node = selection.focusNode; 
            charCount = selection.focusOffset;

            while (node) {
                if (node.id === parentId) {
                    break;
                }

                if (node.previousSibling) {
                    node = node.previousSibling;
                    charCount += node.textContent.length;
                } else {
                     node = node.parentNode;
                     if (node === null) {
                         break
                     }
                }
           }
      }
   }

    return charCount;
};

Код работает противоположно функции set - он получает текущие window.getSelection (). FocusNode и focusOffset и подсчитывает все назад.встречаются текстовые символы, пока он не достигнет родительского узла с идентификатором containerId.Перед запуском функция isChildOf просто проверяет, является ли поставляемый узел дочерним по отношению к parentId .

. Код должен работать без изменений, но я только что взял его из плагина jQuery.Я разработал, поэтому взломал пару это - дайте мне знать, если что-то не работает!

3 голосов
/ 26 августа 2016

Очень трудно установить каретку в правильное положение, когда у вас есть такой элемент продвижения, как (p) (span) и т. Д. Цель состоит в том, чтобы получить (текст объекта):

<div id="editable" contenteditable="true">dddddddddddddddddddddddddddd<p>dd</p>psss<p>dd</p>
    <p>dd</p>
    <p>text text text</p>
</div>
<p id='we'></p>
<button onclick="set_mouse()">focus</button>
<script>

    function set_mouse() {
        var as = document.getElementById("editable");
        el = as.childNodes[1].childNodes[0];//goal is to get ('we') id to write (object Text) because it work only in object text
        var range = document.createRange();
        var sel = window.getSelection();
        range.setStart(el, 1);
        range.collapse(true);
        sel.removeAllRanges();
        sel.addRange(range);

        document.getElementById("we").innerHTML = el;// see out put of we id
    }
</script>
2 голосов
/ 27 марта 2019
  const el = document.getElementById("editable");
  el.focus()
  let char = 1, sel; // character at which to place caret

  if (document.selection) {
    sel = document.selection.createRange();
    sel.moveStart('character', char);
    sel.select();
  }
  else {
    sel = window.getSelection();
    sel.collapse(el.lastChild, char);
  }
2 голосов
/ 09 марта 2018

Если вы не хотите использовать jQuery, попробуйте этот подход:

public setCaretPosition() {
    const editableDiv = document.getElementById('contenteditablediv');
    const lastLine = this.input.nativeElement.innerHTML.replace(/.*?(<br>)/g, '');
    const selection = window.getSelection();
    selection.collapse(editableDiv.childNodes[editableDiv.childNodes.length - 1], lastLine.length);
}

editableDiv ваш редактируемый элемент, не забудьте установить для него id.Затем вам нужно вытащить innerHTML из элемента и обрезать все тормозные магистрали.И просто установить коллапс с помощью следующих аргументов.

1 голос
/ 09 октября 2018

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

Вот фрагмент моего решения, благодаря большой помощи из этой ветки, документам MDN и большому количеству просмотра консоли moz ..

//onKeyPress event

if (evt.key === "\"") {
    let sel = window.getSelection();
    let offset = sel.focusOffset;
    let focus = sel.focusNode;

    focus.textContent += "\""; //setting div's innerText directly creates new
    //nodes, which invalidate our selections, so we modify the focusNode directly

    let range = document.createRange();
    range.selectNode(focus);
    range.setStart(focus, offset);

    range.collapse(true);
    sel.removeAllRanges();
    sel.addRange(range);
}

//end onKeyPress event

Это элемент contenteditable div

Я оставляю это здесь как благодарность, понимая, что уже есть принятый ответ.

0 голосов
/ 02 мая 2019

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

//Set offset in current contenteditable field (for start by default or for with forEnd=true)
function setCurSelectionOffset(offset, forEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return;

    const firstRange = sel.getRangeAt(0);

    if (offset > 0) {
        bypassChildNodes(document.activeElement, offset);
    }else{
        if (forEnd)
            firstRange.setEnd(document.activeElement, 0);
        else
            firstRange.setStart(document.activeElement, 0);
    }



    //Bypass in depth
    function bypassChildNodes(el, leftOffset) {
        const childNodes = el.childNodes;

        for (let i = 0; i < childNodes.length && leftOffset; i++) {
            const childNode = childNodes[i];

            if (childNode.nodeType === 3) {
                const curLen = childNode.textContent.length;

                if (curLen >= leftOffset) {
                    if (forEnd)
                        firstRange.setEnd(childNode, leftOffset);
                    else
                        firstRange.setStart(childNode, leftOffset);
                    return 0;
                }else{
                    leftOffset -= curLen;
                }
            }else
            if (childNode.nodeType === 1) {
                leftOffset = bypassChildNodes(childNode, leftOffset);
            }
        }

        return leftOffset;
    }
}

Я также написал код для определения текущей позиции каретки (не тестировал):

//Get offset in current contenteditable field (start offset by default or end offset with calcEnd=true)
function getCurSelectionOffset(calcEnd = false) {
    const sel = window.getSelection();
    if (sel.rangeCount !== 1 || !document.activeElement) return 0;

    const firstRange     = sel.getRangeAt(0),
          startContainer = calcEnd ? firstRange.endContainer : firstRange.startContainer,
          startOffset    = calcEnd ? firstRange.endOffset    : firstRange.startOffset;
    let needStop = false;

    return bypassChildNodes(document.activeElement);



    //Bypass in depth
    function bypassChildNodes(el) {
        const childNodes = el.childNodes;
        let ans = 0;

        if (el === startContainer) {
            if (startContainer.nodeType === 3) {
                ans = startOffset;
            }else
            if (startContainer.nodeType === 1) {
                for (let i = 0; i < startOffset; i++) {
                    const childNode = childNodes[i];

                    ans += childNode.nodeType === 3 ? childNode.textContent.length :
                           childNode.nodeType === 1 ? childNode.innerText.length :
                           0;
                }
            }

            needStop = true;
        }else{
            for (let i = 0; i < childNodes.length && !needStop; i++) {
                const childNode = childNodes[i];
                ans += bypassChildNodes(childNode);
            }
        }

        return ans;
    }
}

Вам также нужно знатьrange.startOffset и range.endOffset содержат смещение символов для текстовых узлов (nodeType === 3) и смещение дочерних узлов для узлов элементов (nodeType === 1).range.startContainer и range.endContainer могут ссылаться на любой элементный узел любого уровня в дереве (конечно, они также могут ссылаться на текстовые узлы).

...