Подтвердить что ты не робот

Contenteditable DIV - как определить, находится ли курсор в начале или конце содержимого

У меня есть contenteditable div, который содержит типичный редактор wysiwyg html (жирный шрифт, привязки, списки).

Мне нужно определить, находится ли текущий курсор, onKeyDown, в начале и в конце div. Причина этого заключается в том, что на основе позиции курсора и нажатой клавиши я могу захотеть объединить этот div с предыдущим div в обратном пространстве или создать новый следующий div для ввода.

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

Я надеюсь, что я должен упустить какое-то простое решение.

Есть ли относительно простой способ определить это - я открыт для использования библиотеки, такой как Rangy.

Спасибо!

Изменить: я что-то думаю в этих строках:

$('.mycontenteditable').bind('keydown', handle_keydown)

handle_keydown = function(e) {

  range = window.getSelection().getRangeAt(0)
  start_range = document.createRange()
  start_range.selectNodeContents(this.firstChild)
  start_range.collapse(true) // collapse to start
  is_start = start_range.compareBoundaryPoints(Range.START_TO_START,range)
  end_range = document.createRange()
  end_range.selectNodeContents(this.lastChild)
  end_range.collapse(false)
  is_end = end_range.compareBoundaryPoints(Range.END_TO_END,range)
}

Я собираюсь столкнуться с любыми нечетными проблемами с чем-то вроде этого?

4b9b3361

Ответ 1

Вот как я решил это решить. Предлагаемый мною выше раствор работал, но иногда были способом много случаев краев, так что я в конечном итоге, учитывая, сколько текста был до или после курсора, и если это 0 символов, то я был в начале или в конце:

handle_keydown = function(e) {
  // Get the current cusor position
  range = window.getSelection().getRangeAt(0)
  // Create a new range to deal with text before the cursor
  pre_range = document.createRange();
  // Have this range select the entire contents of the editable div
  pre_range.selectNodeContents(this);
  // Set the end point of this range to the start point of the cursor
  pre_range.setEnd(range.startContainer, range.startOffset);
  // Fetch the contents of this range (text before the cursor)
  this_text = pre_range.cloneContents();
  // If the text length is 0, we're at the start of the div.
  at_start = this_text.textContent.length === 0;
  // Rinse and repeat for text after the cursor to determine if we're at the end.
  post_range = document.createRange();
  post_range.selectNodeContents(this);
  post_range.setStart(range.endContainer, range.endOffset);
  next_text = post_range.cloneContents();
  at_end = next_text.textContent.length === 0;
}

Еще не совсем уверен, что есть какие-либо другие случаи края, так как я не совсем уверен, как unit test это, так как он требует взаимодействия с мышью. - Там, вероятно, библиотеку, чтобы иметь дело с этим где-то

Ответ 2

Я бы использовал подобный подход к вашему, за исключением использования метода toString() объектов Range, а не cloneContents(), чтобы избежать ненужного клонирования. Кроме того, в IE < 9 (который не поддерживает диапазоны), вы можете использовать аналогичный подход с свойством text TextRange.

Обратите внимание, что это будет иметь проблемы, когда в содержимом есть ведущие и/или завершающие разрывы строк, потому что метод toString() диапазона работает так же, как свойство textContent для node и рассматривает только текстовые узлы, поэтому не учитывайте разрывы строк, подразумеваемые <br> или элементами блока. Также CSS не учитывается: например, текст внутри элементов, скрытых через display: none, включен.

Вот пример:

Live demo: http://jsfiddle.net/YA3Pu/1/

код:

function getSelectionTextInfo(el) {
    var atStart = false, atEnd = false;
    var selRange, testRange;
    if (window.getSelection) {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            selRange = sel.getRangeAt(0);
            testRange = selRange.cloneRange();

            testRange.selectNodeContents(el);
            testRange.setEnd(selRange.startContainer, selRange.startOffset);
            atStart = (testRange.toString() == "");

            testRange.selectNodeContents(el);
            testRange.setStart(selRange.endContainer, selRange.endOffset);
            atEnd = (testRange.toString() == "");
        }
    } else if (document.selection && document.selection.type != "Control") {
        selRange = document.selection.createRange();
        testRange = selRange.duplicate();

        testRange.moveToElementText(el);
        testRange.setEndPoint("EndToStart", selRange);
        atStart = (testRange.text == "");

        testRange.moveToElementText(el);
        testRange.setEndPoint("StartToEnd", selRange);
        atEnd = (testRange.text == "");
    }

    return { atStart: atStart, atEnd: atEnd };
}

Ответ 3

Я понял этот довольно последовательный и короткий метод:

function isAtTextEnd() {
    var sel = window.getSelection(),
      offset = sel.focusOffset;
  sel.modify ("move","forward","character");
  if (offset == sel.focusOffset) return true;
  else {
    sel.modify ("move","backward","character");
    return false;
  }
}

Ключ: попытаться принудительно переместить его на один символ вперед - если он действительно двигался: не в конце (переместить его на один символ назад), если нет - в конце (нет необходимости возвращаться, он не переехать).
Реализация для начала текста противоположна и "оставлена как упражнение для читателя"...

Кариес:

  • Метки MDN modify как "Нестандартные", хотя таблица совместимости показывает довольно широкую поддержку (протестирована на работу с последними версиями Chrome и Firefox, согласно таблице - не поддерживается в Edge).
    Я попытался использовать для него более поддерживаемый extend() - однако, как ни странно, расширение работает даже в конце текста.

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

Ответ 4

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

Это работает в текущих Chrome, Firefox, Safari и Opera. Microsoft Edge снова является выбросом, поскольку само выделение текста частично нарушается в contenteditable div когда в начале или в конце содержимого появляются новые строки. К сожалению, я еще не нашел решение этой проблемы.

Стоит также отметить, что логика отличается не только между браузерами, но также и между режимами white-space (normal и pre*), потому что браузер будет генерировать разные узлы для каждого при наборе текста.

document.addEventListener("selectionchange", function() {
  updateCaretInfo(document.getElementById('input-normal'))
  updateCaretInfo(document.getElementById('input-pre'))  
});



function updateCaretInfo(input) {

  function isAcceptableNode(node, side) {
    if (node === input) { return true }

    const childProperty = side === 'start' ? 'firstChild' : 'lastChild'
    while (node && node.parentNode && node.parentNode[childProperty] === node) {
      if (node.parentNode === input) {
        return true
      }

      node = node.parentNode
    }

    return false
  }

  function isAcceptableOffset(offset, node, side) {
    if (side === 'start') {
      return offset === 0
    }

    if (node.nodeType === Node.TEXT_NODE) {
      return offset >= node.textContent.replace(/\n$/, '').length
    }
    else {
      return offset >= node.childNodes.length - 1
    }
  }

  function isAcceptableSelection(selection, side) {
    return selection &&
      selection.isCollapsed &&
      isAcceptableNode(selection.anchorNode, side) &&
      isAcceptableOffset(selection.anchorOffset, selection.anchorNode, side)
  }


  const selection = document.getSelection()
  const isAtStart = isAcceptableSelection(selection, 'start')
  const isAtEnd = isAcceptableSelection(selection, 'end')

  document.getElementById('start-' + input.id).innerText = isAtStart ? 'YES' : 'no'
  document.getElementById('end-' + input.id).innerText = isAtEnd ? 'YES' : 'no'
}
body {
  padding: 10px;
}

[id^="input-"] {
  border:        1px solid black;
  display:       inline-block;
  margin-bottom: 10px;
  padding:       5px;
}
<div contenteditable id="input-normal">Move the caret inside here!</div>
(<code>white-space: normal</code>)

<p>
  Caret at start: <span id="start-input-normal">no</span><br>
  Caret at end: <span id="end-input-normal">no</span>
</p>

<hr>

<div contenteditable id="input-pre" style="white-space: pre-wrap">Move the caret inside here!</div>
(<code>white-space: pre-wrap</code>)

<p>
  Caret at start: <span id="start-input-pre">no</span><br>
  Caret at end: <span id="end-input-pre">no</span>
</p>

Ответ 5

Для проверки, находится ли курсор/каретка в конце ввода:

this.$('input').addEventListener('keydown', (e) => {
    if (e.target.selectionEnd == e.target.value.length) {
        // DO SOMETHING
    }
})