Contenteditable DIV - 如何确定光标是否在内容的开头或结尾

21
我有一个contenteditable的div,其中包含典型的所见即所得编辑器HTML代码(粗体、锚点和列表)。
我需要在按下键时确定当前光标是否位于div的开头和结尾。这是因为根据光标位置和按下的键,我可能想在回退时将此div与前一个div合并,或在按Enter键后创建一个新的div。
我一直在尝试使用范围(range)进行调整,但当你在元素内部处理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)
}

我会遇到任何奇怪的问题吗?

5个回答

29

我会使用类似于您的方法,但是使用Range对象的toString()方法而不是cloneContents()来避免不必要的克隆。另外,在IE<9(不支持范围)中,您可以使用类似的方法来处理TextRangetext属性。

请注意,当内容中存在前导和/或尾随换行符时,这将产生问题,因为范围的toString()方法就像节点的textContent属性一样工作,并且仅考虑文本节点,因此不考虑由<br>或块元素暗示的换行符。还未考虑CSS:例如,隐藏在display:none通过元素内的文本被包含。

以下是一个例子:

实时演示: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 };
}

1
完美运作。处理了 div 内的换行。 - mythicalcoder

16

这是我最终解决问题的方法。我之前提出的解决方案有时可以工作,但存在太多边缘情况,所以最终我考虑了光标前后有多少文本。如果前后都没有任何字符,则意味着光标在开头或结尾:

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's 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;
}

我仍然不确定是否还有其他边缘情况,因为我不知道如何对此进行单元测试,因为这需要鼠标交互 - 可能有一个库可以处理这个问题。


这应该是相当健壮的,除了IE < 9之外,它不支持范围或选择对象。 - Tim Down
2
请注意,此解决方案不考虑换行符。如果光标和结尾之间的唯一文本是一系列换行符,则会错误地说光标在div的末尾。空行始终用<br>表示,但您不能仅在post_range.cloneContents()中查找<br>,因为它包括当前行的<br>,如果当前行是最后一行。此外,在Chrome中,如果您在最后一行上,或者如果您在倒数第二行且最后一行为空,则cloneContents()正好是<div><br/></div>。因此,仅使用范围无法解决此问题。 - twhb
这不应该是答案,因为它没有解决@twhb所概述的完整问题。应该考虑更全面的解决方案。 - nox7

10
我想出了这个相当一致且简短的方法:
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() - 但是,奇怪的是,即使在文本末尾,扩展也可以工作

  • 如果在用户启动插入符号的移动之后检查(例如在键盘或鼠标事件处理程序中) - 您应该处理将插入符号强制移动到意外位置的情况。


3
到目前为止,这对我很有效,非常简单而聪明的想法。 - simion314
你太棒了。我卡在这里有一段时间了。现在运行得很好。 - Steve Carey

0
今天我遇到了同样的问题,没有找到解决方案,所以我开发了以下方法。它只使用了Selection,没有使用Range或特定于供应商的功能。它还考虑了内容开头和结尾的换行符。
它适用于当前版本的Chrome、Firefox、Safari和Opera浏览器。 Microsoft Edge是个例外,因为当内容开头或结尾有换行符时,在contenteditable div中文本选择本身就部分失效了。不幸的是,我还没有找到解决这个问题的方法。
值得注意的是,逻辑不仅在不同的浏览器之间不同,而且在white-space模式(normal vs. 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>


-1

检查光标/插入符号是否位于输入的末尾的简单解决方案:

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

不适用于输入,仅供内容编辑使用。 - Anton Medvedev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接