【问题标题】:Javascript Contenteditable - set Cursor / Caret to indexJavascript Contenteditable - 将光标/插入符号设置为索引
【发布时间】:2013-04-12 06:54:58
【问题描述】:

我将如何修改它(How to set caret(cursor) position in contenteditable element (div)?) 使其接受数字索引和元素并将光标位置设置为该索引?

例如: 如果我有这个段落:

<p contenteditable="true">This is a paragraph.</p>

我打电话给:

setCaret($(this).get(0), 3)

光标会像这样移动到索引 3:

Thi|s is a paragraph.

我有这个但没有运气:

function setCaret(contentEditableElement, index)
{
    var range,selection;
    if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
    {
        range = document.createRange();//Create a range (a range is a like the selection but invisible)
        range.setStart(contentEditableElement,index);
        range.collapse(true);
        selection = window.getSelection();//get the selection object (allows you to change selection)
        selection.removeAllRanges();//remove any selections already made
        selection.addRange(range);//make the range you have just created the visible selection
    }
    else if(document.selection)//IE 8 and lower
    { 
        range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
        range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
        range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
        range.select();//Select the range (make it the visible selection
    }
}

http://jsfiddle.net/BanQU/4/

【问题讨论】:

    标签: javascript jquery contenteditable


    【解决方案1】:

    这是我对蒂姆答案的改进。它消除了关于隐藏字符的警告,但其他警告仍然存在:

    • 仅考虑文本节点(
      隐含的换行符和块元素不包含在索引中)
    • 考虑所有文本节点,即使是那些被 CSS 隐藏的内部元素或内部元素
    • IE

    代码:

    var setSelectionByCharacterOffsets = null;
    
    if (window.getSelection && document.createRange) {
        setSelectionByCharacterOffsets = function(containerEl, start, end) {
            var charIndex = 0, range = document.createRange();
            range.setStart(containerEl, 0);
            range.collapse(true);
            var nodeStack = [containerEl], node, foundStart = false, stop = false;
    
            while (!stop && (node = nodeStack.pop())) {
                if (node.nodeType == 3) {
                    var hiddenCharacters = findHiddenCharacters(node, node.length)
                    var nextCharIndex = charIndex + node.length - hiddenCharacters;
    
                    if (!foundStart && start >= charIndex && start <= nextCharIndex) {
                        var nodeIndex = start-charIndex
                        var hiddenCharactersBeforeStart = findHiddenCharacters(node, nodeIndex)
                        range.setStart(node, nodeIndex + hiddenCharactersBeforeStart);
                        foundStart = true;
                    }
                    if (foundStart && end >= charIndex && end <= nextCharIndex) {
                        var nodeIndex = end-charIndex
                        var hiddenCharactersBeforeEnd = findHiddenCharacters(node, nodeIndex)
                        range.setEnd(node, nodeIndex + hiddenCharactersBeforeEnd);
                        stop = true;
                    }
                    charIndex = nextCharIndex;
                } else {
                    var i = node.childNodes.length;
                    while (i--) {
                        nodeStack.push(node.childNodes[i]);
                    }
                }
            }
    
            var sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
        }
    } else if (document.selection) {
        setSelectionByCharacterOffsets = function(containerEl, start, end) {
            var textRange = document.body.createTextRange();
            textRange.moveToElementText(containerEl);
            textRange.collapse(true);
            textRange.moveEnd("character", end);
            textRange.moveStart("character", start);
            textRange.select();
        };
    }
    
    var x = document.getElementById('a')
    x.focus()
    setSelectionByCharacterOffsets(x, 1, 13)
    
    function findHiddenCharacters(node, beforeCaretIndex) {
        var hiddenCharacters = 0
        var lastCharWasWhiteSpace=true
        for(var n=0; n-hiddenCharacters<beforeCaretIndex &&n<node.length; n++) {
            if([' ','\n','\t','\r'].indexOf(node.textContent[n]) !== -1) {
                if(lastCharWasWhiteSpace)
                    hiddenCharacters++
                else
                    lastCharWasWhiteSpace = true
            } else {
                lastCharWasWhiteSpace = false   
            }
        }
    
        return hiddenCharacters
    }
    

    【讨论】:

      【解决方案2】:

      这是改编自Persisting the changes of range objects after selection in HTML 的答案。请记住,这在几个方面并不完美(MaxArt 也是如此,它使用相同的方法):首先,只考虑文本节点,这意味着 &lt;br&gt; 隐含的换行符和块元素不包含在索引;其次,考虑所有文本节点,即使是那些被 CSS 隐藏的内部元素或&lt;script&gt; 元素内部的那些;第三,页面上折叠的连续空白字符都包含在索引中;最后,IE

      var setSelectionByCharacterOffsets = null;
      
      if (window.getSelection && document.createRange) {
          setSelectionByCharacterOffsets = function(containerEl, start, end) {
              var charIndex = 0, range = document.createRange();
              range.setStart(containerEl, 0);
              range.collapse(true);
              var nodeStack = [containerEl], node, foundStart = false, stop = false;
      
              while (!stop && (node = nodeStack.pop())) {
                  if (node.nodeType == 3) {
                      var nextCharIndex = charIndex + node.length;
                      if (!foundStart && start >= charIndex && start <= nextCharIndex) {
                          range.setStart(node, start - charIndex);
                          foundStart = true;
                      }
                      if (foundStart && end >= charIndex && end <= nextCharIndex) {
                          range.setEnd(node, end - charIndex);
                          stop = true;
                      }
                      charIndex = nextCharIndex;
                  } else {
                      var i = node.childNodes.length;
                      while (i--) {
                          nodeStack.push(node.childNodes[i]);
                      }
                  }
              }
      
              var sel = window.getSelection();
              sel.removeAllRanges();
              sel.addRange(range);
          }
      } else if (document.selection) {
          setSelectionByCharacterOffsets = function(containerEl, start, end) {
              var textRange = document.body.createTextRange();
              textRange.moveToElementText(containerEl);
              textRange.collapse(true);
              textRange.moveEnd("character", end);
              textRange.moveStart("character", start);
              textRange.select();
          };
      }
      

      【讨论】:

      • 我看到你已经实现了一个迭代树遍历例程。但是AFAIK那些支持getSelection的浏览器也支持document.createTreeWalkerwhich is faster。所以我们应该去尝试一下。
      • @MaxArt:是的,我从来没有遇到过支持 Range 但不支持 TreeWalker 的浏览器(两者都来自 DOM Level 2,这是有道理的)。在大多数浏览器中,我改进了这些测试并制作了一个 jsPerf,它表明您对速度的看法是正确的。 jsperf.com/text-node-traversal
      • 我很惊讶 TreeWalker 在 Chrome 中是 slower :|但无论如何它节省了一堆代码痛苦......
      • 当用您的代码 (jsfiddle.net/zQUhV/21) 替换上面的代码 (jsfiddle.net/zQUhV/20) 时,它似乎不起作用。注意:jsfiddle 代码被构建为使用箭头键在最后 2 段之间进行遍历。它在第一个链接中有效,但在第二个链接中无效,但是当索引和文本长度相等时,第一个链接会中断,setCaret(prev.get(0), prev.text().length)
      • @RyanKing:您在 jsFiddle 中有语法错误(? 而不是 {)。 jsfiddle.net/zQUhV/22
      【解决方案3】:

      range.setStartrange.setEnd 可以用于 text 节点,而不是元素节点。否则他们会引发 DOM 异常。所以你要做的是

      range.setStart(contentEditableElement.firstChild, index);
      

      我不明白你为 IE8 和更低版本做了什么。你的意思是在哪里使用index

      总的来说,如果节点的内容不止一个文本节点,您的代码就会失败。 isContentEditable === true 的节点可能会发生这种情况,因为用户可以从 Word 或其他地方粘贴文本,或者创建新行等等。

      这是我在框架中所做的改编:

      var setSelectionRange = function(element, start, end) {
          var rng = document.createRange(),
              sel = getSelection(),
              n, o = 0,
              tw = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, null);
          while (n = tw.nextNode()) {
              o += n.nodeValue.length;
              if (o > start) {
                  rng.setStart(n, n.nodeValue.length + start - o);
                  start = Infinity;
              }
              if (o >= end) {
                  rng.setEnd(n, n.nodeValue.length + end - o);
                  break;
              }
          }
          sel.removeAllRanges();
          sel.addRange(rng);
      };
      
      var setCaret = function(element, index) {
          setSelectionRange(element, index, index);
      };
      

      这里的技巧是使用setSelectionRange 函数 - 选择内部文本范围和元素 - 和start === end。在contentEditable 元素中,这会将插入符号置于所需位置。

      这应该适用于所有现代浏览器,并且适用于不仅仅是一个文本节点作为后代的元素。我会让你添加检查 startend 是否在适当的范围内。

      对于 IE8 及更低版本,事情有点困难。事情看起来有点像这样:

      var setSelectionRange = function(element, start, end) {
          var rng = document.body.createTextRange();
          rng.moveToElementText(element);
          rng.moveStart("character", start);
          rng.moveEnd("character", end - element.innerText.length - 1);
          rng.select();
      };
      

      这里的问题是innerText 不适合这种事情,因为一些空白被折叠了。如果只有一个文本节点,一切都很好,但是对于更复杂的东西,比如你在 contentEditable 元素中得到的东西,事情就搞砸了。

      IE8 不支持textContent,因此您必须使用TreeWalker 来计算字符数。但是IE8也不支持TreeWalker,所以你必须自己走DOM树......

      我仍然必须解决这个问题,但不知何故我怀疑我永远不会。即使我确实在 IE8 及更低版本中为 TreeWalker 编写了 polyfill...

      【讨论】:

      • 谢谢,我应该提到我从来没有使用过 IE8 和更低的代码。而且我从未考虑过人们将文本粘贴到元素中 - 我将不得不对此进行调查。
      • setStart()setEnd() range 方法肯定可以与元素一起使用,但偏移量表示元素在边界之前的子节点数,而不是字符索引。
      • @TimDown 是的,但在 Ryan 的情况下它会引发异常,因为第二个参数是 3(小提琴中的 5)。感谢您指出这一点,但不清楚。而且我没有使用collapse,因为该函数是setSeletionRange,然后由setCaret调用,但它通常会创建非折叠选择。
      • 在IE moveEnd()方法呢? rng.moveEnd("character", end); rng.moveStart("character", start);
      • @keligijus 啊,讨厌的小虫子... 看起来检查o &gt;= start 可以解决问题,但如果光标在新行的开头,它会被带回到末尾的上一行。那是因为,按文本计算,它是“相同”的位置......玩一些边缘情况。 :|
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-12
      • 2013-06-25
      • 1970-01-01
      • 1970-01-01
      • 2012-10-15
      相关资源
      最近更新 更多