Сохранение и восстановление позиции курсора для contentEditable div

У меня есть div contentEditable, innerHTML которого можно обновлять через AJAX при редактировании. Проблема в том, что когда вы меняете содержимое div, курсор перемещается в конец div (или теряет фокус в зависимости от браузера). Какое хорошее кросс-браузерное решение для сохранения положения курсора перед изменением innerHTML, а затем для его восстановления?


person Arty    schedule 02.01.2011    source источник


Ответы (3)


назад в 2016 :)
После я наткнулся на решения здесь и они меня не устроили, т.к. мой DOM менялся полностью после каждого набора текста. Я провел дополнительные исследования и нашел простое решение, которое сохраняет курсор по положению символа, и оно работает идеально для меня.

Идея очень проста.

  1. найдите длину символов перед кареткой и сохраните ее.
  2. изменить ДОМ.
  3. используя TreeWalker, чтобы пройти только text nodes из context node и считать символы, пока мы не получим правильный text node и позицию внутри него

Два крайних случая:

  1. содержимое удалено полностью, поэтому нет text node:
    итак: переместите курсор в начало узла контекста

  2. содержимого меньше, чем указанный index :
    поэтому: переместите курсор в конец последнего узла

function saveCaretPosition(context){
    var selection = window.getSelection();
    var range = selection.getRangeAt(0);
    range.setStart(  context, 0 );
    var len = range.toString().length;

    return function restore(){
        var pos = getTextNodeAtPosition(context, len);
        selection.removeAllRanges();
        var range = new Range();
        range.setStart(pos.node ,pos.position);
        selection.addRange(range);

    }
}

function getTextNodeAtPosition(root, index){
    const NODE_TYPE = NodeFilter.SHOW_TEXT;
    var treeWalker = document.createTreeWalker(root, NODE_TYPE, function next(elem) {
        if(index > elem.textContent.length){
            index -= elem.textContent.length;
            return NodeFilter.FILTER_REJECT
        }
        return NodeFilter.FILTER_ACCEPT;
    });
    var c = treeWalker.nextNode();
    return {
        node: c? c: root,
        position: index
    };
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.5.1/prism.min.js"></script>
<link href="https://rawgit.com/PrismJS/prism/gh-pages/themes/prism.css" rel="stylesheet"/>
<style>
  *{
    outline: none
    }
</style>  
<h3>Edit the CSS Snippet </H3>
<pre>
    <code class="language-css" contenteditable=true >p { color: red }</code>
</pre>

<script >
  var code = document.getElementsByTagName('code')[0];
  
  code.addEventListener('input',function () {
        var restore = saveCaretPosition(this);
        Prism.highlightElement(this);
        restore();
    })
</script>

person pery mimon    schedule 20.07.2016
comment
Что делать, если у вас есть разрывы строк и другие элементы форматирования в редактируемом элементе содержимого? - person pelican_george; 01.11.2016
comment
вы имеете в виду как ‹br› из ‹span›‹/span›? все равно должно работать. эта сборка кода для редактора форматированного текста, который меняет элементы вокруг курсора при написании пользователем - person pery mimon; 13.12.2016
comment
У меня есть редактируемый контент с перерывами и т. д., и он работает. Если вы используете его для функции отмены, сохраните последнее нажатие клавиши в обработчике onKeyDown и используйте range.setStart(pos.node ,pos.position-(lastKeypress == 13 ? 0:1));, чтобы курсор не прогуливался :-) - person Tschallacka; 14.09.2017
comment
На самом деле у меня тот же вопрос, что и у @pelican_george - ваш подход работает очень хорошо, но он не работает с разрывами строк. Как только вы вставите разрыв строки, курсор останется на первой строке (даже если новая строка была создана). Взгляните на jsfiddle с вашим примером: jsfiddle.net/80ovoxr9 Я не мог работать с разрывами строк К сожалению :( - person Lucas Motta; 09.02.2018
comment
Спасибо за скрипку. Я проверяю его и нахожу небольшую ошибку в цикле. но есть еще одна ошибка в Prism. Я исправил ошибку цикла (jsfiddle.net/80ovoxr9/10/]. ( Shift + Enter помогите разобраться) - person pery mimon; 19.02.2018
comment
Как насчет восстановления выделения, а не только позиции каретки? - person Nathan B; 02.06.2018
comment
пользователь выбирает какой-то текст, а затем продолжает писать материал, и он ожидает, что область выбора останется? в любом случае, я думаю, вы можете создать два объекта диапазона, один для начальной позиции, а другой для конечной позиции, и использовать их для восстановления диапазона выбора. - person pery mimon; 08.06.2018
comment
@perymimon Это спасло мой день. Спасибо. - person kirill.buga; 22.01.2019
comment
Я получаю ошибку с этим скриптом, из-за которой курсор перескакивает в начало, когда я помещаю его в конец и ввожу символ. Решение так же просто, как изменить index > elem.textContent.length и position: index, но я не могу изменить ответ, потому что очередь заполнена. - person tvanc; 25.04.2020
comment
'lastNode' присваивается значение, но никогда не используется - person Motla; 29.06.2020
comment
@Motla Спасибо :), это немного почистило, возможно, останется там с предыдущей версии. - person pery mimon; 18.07.2020
comment
Этот ответ так близок к совершенству, но все еще имеет некоторые проблемы с вводом ключа/разрыва строки. Shift+Enter не работает в firefox. Также добавление разрывов строк в конец строки не работает в Firefox. Кроме того, в Chrome можно ли заставить работать ввод так же, как сдвиг + ввод? - person Connor; 20.08.2020
comment
Благодарю. Вы можете знать, как причина этого и как ее решить? Я обновлю свою причину - person pery mimon; 22.08.2020

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

http://jsfiddle.net/6jbwet9q/9/

Протестировано в Chrome, FF и IE10+. Позволяет изменять, удалять и восстанавливать html, сохраняя позицию/выбор курсора.

HTML

<div id=bE contenteditable=true></div>

JS

function saveRangePosition()
  {
  var range=window.getSelection().getRangeAt(0);
  var sC=range.startContainer,eC=range.endContainer;

  A=[];while(sC!==bE){A.push(getNodeIndex(sC));sC=sC.parentNode}
  B=[];while(eC!==bE){B.push(getNodeIndex(eC));eC=eC.parentNode}

  return {"sC":A,"sO":range.startOffset,"eC":B,"eO":range.endOffset};
  }

function restoreRangePosition(rp)
  {
  bE.focus();
  var sel=window.getSelection(),range=sel.getRangeAt(0);
  var x,C,sC=bE,eC=bE;

  C=rp.sC;x=C.length;while(x--)sC=sC.childNodes[C[x]];
  C=rp.eC;x=C.length;while(x--)eC=eC.childNodes[C[x]];

  range.setStart(sC,rp.sO);
  range.setEnd(eC,rp.eO);
  sel.removeAllRanges();
  sel.addRange(range)
  }

function getNodeIndex(n){var i=0;while(n=n.previousSibling)i++;return i}
person poby    schedule 21.10.2014
comment
Это выглядит так, как будто он преобразует каждую границу диапазона выбора в путь и обратно. Это отличный подход, если структура DOM одинакова до и после изменений innerHTML, что не гарантируется. - person Tim Down; 31.03.2015
comment
Можно ли исправить этот код для нескольких редактируемых элементов div? Так что я могу выбрать, скажем, 1 из 3 div contenteditable, а затем получить позицию, в которую я хочу вставить. - person Gjert; 06.01.2016
comment
Uncaught ReferenceError: bE не определен - person Nathan B; 02.06.2018

Обновление: я перенес код Rangy в отдельный Gist:

https://gist.github.com/timdown/244ae2ea7302e26ba932a43cb0ca3908

Исходный ответ

Вы можете использовать Rangy, мою кросс-браузерную библиотеку диапазона и выбора. У него есть модуль сохранения и восстановления выбора, который кажется хорошо- подходит для ваших нужд.

Этот подход не сложен: он вставляет элементы-маркеры в начале и конце каждого выбранного диапазона и использует эти элементы-маркеры для последующего восстановления границ диапазона, что можно было бы реализовать без Rangy в небольшом количестве кода (и вы могли бы даже адаптировать собственный код Ранги). Основным преимуществом Rangy является поддержка IE ‹= 8.

person Tim Down    schedule 07.02.2011
comment
Фантастический. У меня были некоторые опасения по поводу использования случайной библиотеки от какого-то парня на SO, но она сделала то, что я хотел, в 2 строки кода. Спасибо! - person thedayturns; 08.08.2011
comment
@thedayturns: Это правильное отношение, поэтому я не виню тебя :) Я рад, что это помогло. - person Tim Down; 08.08.2011
comment
@TimDown Поддерживает ли Rangy несколько редактируемых элементов div? Например, сохранение положения каретки над тремя разными элементами div. Причина в том, что я хочу использовать 1 редактор для 3 разных полей. - person Gjert; 06.01.2016
comment
Я не уверен, как этот подход может работать, если я полностью заменю весь контент div - person pery mimon; 20.07.2016
comment
@perymimon: Нет, не будет. В этом случае я бы использовал подход, основанный на смещении символов. - person Tim Down; 20.07.2016
comment
Привет, Тим, извини за вопрос, но есть ли отдельная версия модуля сохранения/восстановления, для которой не требуется ядро ​​Rangy? - person Norman; 10.03.2019
comment
По сути, я ищу две функции: save_range: для вставки невидимых элементов диапазона в начале и конце текущего выделения (или в точке вставки). и restore_range: это изменит диапазоны обратно на выбор. не более того. - person Norman; 10.03.2019
comment
@Norman: я перенес код Рэнджи в отдельный Gist: gist.github.com/timdown/244ae2ea7302e26ba932a43cb0ca3908. Очевидно, вы можете вырезать материал выбора, если хотите, но я оставил его на случай, если он будет полезен кому-то еще. - person Tim Down; 11.03.2019