Отримайте позицію індексу каретки contentEditable


119

Я знаходжу багато хороших перехресних анонсів про те, як встановити позицію індексу курсора чи каретки в contentEditableелементі, але жоден про те, як отримати або знайти його індекс ...

Що я хочу зробити, це знати індекс карети в цьому розділі, на keyup.

Отже, коли користувач набирає текст, я в будь-який момент можу знати індекс його курсору в межах contentEditableелемента.

EDIT: Я шукаю INDEX в вмісті div (тексту), а не в координатах курсору.

<div id="contentBox" contentEditable="true"></div>

$('#contentbox').keyup(function() { 
    // ... ? 
});

Подивіться на його положення в тексті. Потім знайдіть останнє значення "@" перед цим положенням. Отже, лише деяка логіка тексту.
Бертван

Крім того, я не планую допускати інші теги в <diV>, лише тексті
Bertvan

ок, так я збираюся потрібні інші теги в <DIV>. Будуть теги <a>, але вкладення не буде ...
Бертван

@ Bertvan: якщо карета міститься в <a>елементі всередині <div>, яке зміщення ви хочете тоді? Зсув у тексті всередині <a>?
Тім Даун

Він ніколи не повинен знаходитися всередині елемента <a>. Елемент <a> повинен бути виведений html, тому користувач не може фактично розмістити туди карету.
Бертван

Відповіді:


121

Наступний код передбачає:

  • Завжди є один текстовий вузол в межах редагованого <div>і інших вузлів
  • Для редагуваного div не встановлено white-spaceвластивість CSSpre

Якщо вам потрібен більш загальний підхід, який буде працювати з вмістом з вкладеними елементами, спробуйте відповісти:

https://stackoverflow.com/a/4812022/96100

Код:

function getCaretPosition(editableDiv) {
  var caretPos = 0,
    sel, range;
  if (window.getSelection) {
    sel = window.getSelection();
    if (sel.rangeCount) {
      range = sel.getRangeAt(0);
      if (range.commonAncestorContainer.parentNode == editableDiv) {
        caretPos = range.endOffset;
      }
    }
  } else if (document.selection && document.selection.createRange) {
    range = document.selection.createRange();
    if (range.parentElement() == editableDiv) {
      var tempEl = document.createElement("span");
      editableDiv.insertBefore(tempEl, editableDiv.firstChild);
      var tempRange = range.duplicate();
      tempRange.moveToElementText(tempEl);
      tempRange.setEndPoint("EndToEnd", range);
      caretPos = tempRange.text.length;
    }
  }
  return caretPos;
}
#caretposition {
  font-weight: bold;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="contentbox" contenteditable="true">Click me and move cursor with keys or mouse</div>
<div id="caretposition">0</div>
<script>
  var update = function() {
    $('#caretposition').html(getCaretPosition(this));
  };
  $('#contentbox').on("mousedown mouseup keydown keyup", update);
</script>


9
Це не спрацює, якщо там є якісь інші теги. Питання: якщо каретка знаходиться в <a>елементі всередині <div>, яке зміщення ви тоді хочете? Зсув у тексті всередині <a>?
Тім Даун

3
@ Richard: Ну, keyupмабуть, це буде неправильна подія для цього, але це те, що було використано в оригінальному питанні. getCaretPosition()сама по собі прекрасна в межах власних обмежень.
Тім Даун

3
Ця демонстрація JSFIDDLE виходить з ладу, якщо натиснути клавішу Enter і перейти на новий рядок. Позиція покаже 0.
giorgio79

5
@ giorgio79: Так, оскільки розрив рядка породжує елемент <br>або <div>елемент, що порушує перше припущення, зазначене у відповіді. Якщо вам потрібно трохи більш загальне рішення, ви можете спробувати stackoverflow.com/a/4812022/96100
Tim Down

2
Чи потрібно це зробити, щоб він включав номер рядка?
Аджит

28

Кілька зморшок, на які я не бачу вирішення в інших відповідях:

  1. елемент може містити кілька рівнів дочірніх вузлів (наприклад, дочірні вузли, які мають дочірні вузли, які мають дочірні вузли ...)
  2. вибір може складатися з різних початкових і кінцевих позицій (наприклад, вибрано кілька символів)
  3. вузол, що містить початок / кінець Caret, може бути не елементом, ані його прямими дочірніми елементами

Ось спосіб отримати початкові та кінцеві позиції як зміщення до значення textContent елемента:

// node_walk: walk the element tree, stop when func(node) returns false
function node_walk(node, func) {
  var result = func(node);
  for(node = node.firstChild; result !== false && node; node = node.nextSibling)
    result = node_walk(node, func);
  return result;
};

// getCaretPosition: return [start, end] as offsets to elem.textContent that
//   correspond to the selected portion of text
//   (if start == end, caret is at given position and no text is selected)
function getCaretPosition(elem) {
  var sel = window.getSelection();
  var cum_length = [0, 0];

  if(sel.anchorNode == elem)
    cum_length = [sel.anchorOffset, sel.extentOffset];
  else {
    var nodes_to_find = [sel.anchorNode, sel.extentNode];
    if(!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
      return undefined;
    else {
      var found = [0,0];
      var i;
      node_walk(elem, function(node) {
        for(i = 0; i < 2; i++) {
          if(node == nodes_to_find[i]) {
            found[i] = true;
            if(found[i == 0 ? 1 : 0])
              return false; // all done
          }
        }

        if(node.textContent && !node.firstChild) {
          for(i = 0; i < 2; i++) {
            if(!found[i])
              cum_length[i] += node.textContent.length;
          }
        }
      });
      cum_length[0] += sel.anchorOffset;
      cum_length[1] += sel.extentOffset;
    }
  }
  if(cum_length[0] <= cum_length[1])
    return cum_length;
  return [cum_length[1], cum_length[0]];
}

3
Це потрібно вибрати як правильну відповідь. Він працює з тегами всередині тексту (прийнята відповідь не відповідає)
hamboy75

17

$("#editable").on('keydown keyup mousedown mouseup',function(e){
		   
       if($(window.getSelection().anchorNode).is($(this))){
    	  $('#position').html('0')
       }else{
         $('#position').html(window.getSelection().anchorOffset);
       }
 });
body{
  padding:40px;
}
#editable{
  height:50px;
  width:400px;
  border:1px solid #000;
}
#editable p{
  margin:0;
  padding:0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.1/jquery.min.js"></script>
<div contenteditable="true" id="editable">move the cursor to see position</div>
<div>
position : <span id="position"></span>
</div>


3
Це, на жаль, припиняє роботу, як тільки ви натискаєте клавішу Enter та починаєте інший рядок (він починається з 0 знову - ймовірно, рахуючи з CR / LF).
Ян

Він не працює належним чином, якщо у вас є кілька жирних та / або курсивних слів.
користувач2824371

14

Спробуйте це:

Caret.js Отримайте позицію карети та зміщення з текстового поля

https://github.com/ichord/Caret.js

демо: http://ichord.github.com/Caret.js


Це солодке. Мені потрібна така поведінка, щоб встановити піклування до кінця, contenteditable liколи натиснути кнопку, щоб перейменувати liвміст.
акінурі

@AndroidDev Я не автор Caret.js, але ви вважали, що отримати положення карети для всіх основних браузерів складніше, ніж кілька рядків? Чи знаєте ви чи створили альтернативу, яка не роздута, якою ви можете поділитися з нами?
adelriosantiago

8

Ніби не пізно на вечірку, але на випадок, коли хтось бореться. Жоден із пошукових запитів Google, які я знайшов за останні два дні, не створив нічого, що працює, але я придумав стисле і елегантне рішення, яке завжди працюватиме незалежно від того, скільки вкладених тегів у вас є:

function cursor_position() {
    var sel = document.getSelection();
    sel.modify("extend", "backward", "paragraphboundary");
    var pos = sel.toString().length;
    if(sel.anchorNode != undefined) sel.collapseToEnd();

    return pos;
}

// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( cursor_position(), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

Він вибирає весь шлях до початку абзацу, а потім підраховує довжину рядка для отримання поточного положення, а потім скасовує вибір, щоб повернути курсор у поточне положення. Якщо ви хочете зробити це для цілого документа (більш ніж один абзац), то змініть paragraphboundaryна documentboundaryабо будь-яку деталізацію для вашого випадку. Перегляньте API для отримання більш детальної інформації . Ура! :)


1
Якщо у мене є кожен <div contenteditable> some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text </div> раз, коли я розміщую курсор перед iтегом або будь-яким дочірнім html-елементом всередині div, позиція курсора починається з 0. Чи є спосіб уникнути цього рахунку перезавантаження?
вам

Незвичайно. Я не сприймаю такої поведінки в Chrome. Який браузер ви використовуєте?
Субрікет

2
Схоже, що selection.modify може підтримуватися або не підтримуватися у всіх браузерах. developer.mozilla.org/en-US/docs/Web/API/Selection
Кріс Салліван

7
function getCaretPosition() {
    var x = 0;
    var y = 0;
    var sel = window.getSelection();
    if(sel.rangeCount) {
        var range = sel.getRangeAt(0).cloneRange();
        if(range.getClientRects()) {
        range.collapse(true);
        var rect = range.getClientRects()[0];
        if(rect) {
            y = rect.top;
            x = rect.left;
        }
        }
    }
    return {
        x: x,
        y: y
    };
}

цей насправді працював на мене, я спробував все вище, але не вдалося.
iStudLion

дякую, але також повертається {x: 0, y: 0} у новому рядку.
hichamkazan

це повертає піксельну позицію, а не зміщення символів
4esn0k

дякую, я шукав повернення піксельної позиції з карети, і це працює добре.
Самеш

6

window.getSelection - vs - document.selection

Цей для мене працює:

function getCaretCharOffset(element) {
  var caretOffset = 0;

  if (window.getSelection) {
    var range = window.getSelection().getRangeAt(0);
    var preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(element);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    caretOffset = preCaretRange.toString().length;
  } 

  else if (document.selection && document.selection.type != "Control") {
    var textRange = document.selection.createRange();
    var preCaretTextRange = document.body.createTextRange();
    preCaretTextRange.moveToElementText(element);
    preCaretTextRange.setEndPoint("EndToEnd", textRange);
    caretOffset = preCaretTextRange.text.length;
  }

  return caretOffset;
}


// Demo:
var elm = document.querySelector('[contenteditable]');
elm.addEventListener('click', printCaretPosition)
elm.addEventListener('keydown', printCaretPosition)

function printCaretPosition(){
  console.log( getCaretCharOffset(elm), 'length:', this.textContent.trim().length )
}
<div contenteditable>some text here <i>italic text here</i> some other text here <b>bold text here</b> end of text</div>

Рядок виклику залежить від типу події, для ключових подій використовуйте це:

getCaretCharOffsetInDiv(e.target) + ($(window.getSelection().getRangeAt(0).startContainer.parentNode).index());

для події миші використовуйте це:

getCaretCharOffsetInDiv(e.target.parentElement) + ($(e.target).index())

у цих двох випадках я дбаю про перерви, додаючи цільовий індекс


4
//global savedrange variable to store text range in
var savedrange = null;

function getSelection()
{
    var savedRange;
    if(window.getSelection && window.getSelection().rangeCount > 0) //FF,Chrome,Opera,Safari,IE9+
    {
        savedRange = window.getSelection().getRangeAt(0).cloneRange();
    }
    else if(document.selection)//IE 8 and lower
    { 
        savedRange = document.selection.createRange();
    }
    return savedRange;
}

$('#contentbox').keyup(function() { 
    var currentRange = getSelection();
    if(window.getSelection)
    {
        //do stuff with standards based object
    }
    else if(document.selection)
    { 
        //do stuff with microsoft object (ie8 and lower)
    }
});

Примітка. Об'єкт діапазону "Я" може бути збережений у змінній і може бути повторно обраний у будь-який час, якщо зміна змістового розділу не зміниться.

Посилання на IE 8 і новіші версії : http://msdn.microsoft.com/en-us/library/ms535872(VS.85).aspx

Довідка щодо стандартних (усіх інших) браузерів: https://developer.mozilla.org/en/DOM/range (це документи Мозілла, але код працює в chrome, safari, opera та ie9)


1
Дякую, але як саме я отримую "індекс" позиції карет у вмісті div?
Бертван

Гаразд, це виглядає як виклик .baseOffset у .getSelection () робить трюк. Отже, це разом з вашою відповіддю відповідає на моє запитання. Дякую!
Бертван

2
На жаль .baseOffset працює лише у веб-програмі (я думаю). Це також дає лише зсув від невмілого батька карети (якщо у вас є тег <b> усередині <div>, він дасть зміщення з початку <b>, а не з початку <div> Діапазони на основі стандартів можуть використовувати range.endOffset range.startOffset range.endContainer і range.startContainer, щоб отримати зсув від батьківського вузла вибору, і сам вузол (це включає текстові вузли). IE надає range.offsetLeft, який є зсув зліва в пікселях , і так марно
Ніко Бернс

Найкраще просто зберегти об’єкт діапазону його самості та використовувати window.getSelection (). Addrange (range); <- стандарти і range.select (); <- IE для перестановки курсору в тому ж місці. range.insertNode (nodetoinsert); <- стандарти та range.pasteHTML (htmlcode); <- IE, щоб вставити текст або html на курсор.
Ніко Бернс

RangeОб'єкт , що повертається в більшості браузерів і TextRangeоб'єкт , що повертається IE дуже різні речі, так що я не впевнений , що ця відповідь вирішує багато.
Тім Даун

3

Оскільки це зайняло мене назавжди, щоб зрозуміти, використовуючи новий API window.getSelection, я буду ділитися для нащадків. Зауважте, що MDN припускає, що існує більш широка підтримка window.getSelection, проте ваш пробіг може відрізнятися.

const getSelectionCaretAndLine = () => {
    // our editable div
    const editable = document.getElementById('editable');

    // collapse selection to end
    window.getSelection().collapseToEnd();

    const sel = window.getSelection();
    const range = sel.getRangeAt(0);

    // get anchor node if startContainer parent is editable
    let selectedNode = editable === range.startContainer.parentNode
      ? sel.anchorNode 
      : range.startContainer.parentNode;

    if (!selectedNode) {
        return {
            caret: -1,
            line: -1,
        };
    }

    // select to top of editable
    range.setStart(editable.firstChild, 0);

    // do not use 'this' sel anymore since the selection has changed
    const content = window.getSelection().toString();
    const text = JSON.stringify(content);
    const lines = (text.match(/\\n/g) || []).length + 1;

    // clear selection
    window.getSelection().collapseToEnd();

    // minus 2 because of strange text formatting
    return {
        caret: text.length - 2, 
        line: lines,
    }
} 

Ось jsfiddle, яка запускається на клавіатурі. Зауважте, що швидке натискання клавіш, а також швидке видалення, здається, є пропуском подій.


Для мене працює! Дуже дякую.
dmodo

За допомогою цього вибору тексту більше неможливо, оскільки він згортається. Можливий сценарій: потрібно оцінювати кожну подію
keyUp

0

Прямий шлях вперед, який проходить через всіх дітей змістовного диву, поки він не потрапить у кінцевий контейнер. Потім я додаю зміщення кінця контейнера і у нас є індекс символів. Слід працювати з будь-якою кількістю гнізд. використовує рекурсію.

Примітка: потрібна полів для полірування, тобто для підтримкиElement.closest('div[contenteditable]')

https://codepen.io/alockwood05/pen/vMpdmZ

function caretPositionIndex() {
    const range = window.getSelection().getRangeAt(0);
    const { endContainer, endOffset } = range;

    // get contenteditableDiv from our endContainer node
    let contenteditableDiv;
    const contenteditableSelector = "div[contenteditable]";
    switch (endContainer.nodeType) {
      case Node.TEXT_NODE:
        contenteditableDiv = endContainer.parentElement.closest(contenteditableSelector);
        break;
      case Node.ELEMENT_NODE:
        contenteditableDiv = endContainer.closest(contenteditableSelector);
        break;
    }
    if (!contenteditableDiv) return '';


    const countBeforeEnd = countUntilEndContainer(contenteditableDiv, endContainer);
    if (countBeforeEnd.error ) return null;
    return countBeforeEnd.count + endOffset;

    function countUntilEndContainer(parent, endNode, countingState = {count: 0}) {
      for (let node of parent.childNodes) {
        if (countingState.done) break;
        if (node === endNode) {
          countingState.done = true;
          return countingState;
        }
        if (node.nodeType === Node.TEXT_NODE) {
          countingState.count += node.length;
        } else if (node.nodeType === Node.ELEMENT_NODE) {
          countUntilEndContainer(node, endNode, countingState);
        } else {
          countingState.error = true;
        }
      }
      return countingState;
    }
  }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.