Запазване и възстановяване на позицията на каретката за contentEditable div

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


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


Отговори (3)


обратно към 2016 г. :)
След като попаднах на решения тук и те не ми паснаха, защото моят DOM се сменяше напълно след всяко писане. Направих повече проучвания и предлагам просто решение, което запазва курсора според позицията на символа, което работи перфектно за мен.

Идеята е много проста.

  1. намерете дължината на знаците преди каретката и я запазете.
  2. променете DOM.
  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 възможно ли е enter да работи по същия начин като shift + enter? - 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
Възможно ли е да се коригира този код за множество divs с възможност за редактиране на съдържание? Така че мога да избера, да кажем, 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). Основното предимство на 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 поддържа ли множество divs за редактиране на съдържание? Например запазване на позицията на каретката над три различни 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: за вмъкване на невидими span елементи в началото и края на текущия избор (или един в каретката). и restore_range: това ще промени обхватите обратно в селекция. нищо повече от това. - person Norman; 10.03.2019
comment
@Norman: Пренесох кода на Rangy към самостоятелен Gist: gist.github.com/timdown/244ae2ea7302e26ba932a43cb0ca3 908. Очевидно можете да изрежете нещата за избор, ако желаете, но го оставих, в случай че е полезно за някой друг. - person Tim Down; 11.03.2019