获取contentEditable插入符号索引位置


119

我发现了很多关于如何在浏览器中设置光标或插入符号索引位置的好方法。 contentEditable元素中,但是却没有关于如何获取或找到其索引的 ...

我想做的是知道这个div内插入符号的索引 keyup

因此,当用户键入文本时,我随时可以知道其在 contentEditable元素中的。

编辑:我正在寻找div内容(文本),而不是光标坐标内的索引

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

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

查看其在文本中的位置。然后,在该位置之前查找“ @”的最后一次出现。所以只是一些文本逻辑。
Bertvan

另外,我不打算在<diV>中仅允许文本使用其他标签
Bertvan 2010年

好吧,是我会需要在<DIV>等标签。将有<a>标签,但不会有嵌套...
Bertvan

@Bertvan:如果插入符号位于内的<a>元素中<div>,那么您想要什么偏移量?文本内的偏移量<a>
Tim Down

它绝不能在<a>元素内。<a>元素应呈现为html,因此用户实际上无法将插入号放置在其中。
Bertvan

Answers:


121

以下代码假定:

  • 可编辑内容中始终只有一个文本节点<div>,没有其他节点
  • 可编辑的div没有将CSS white-space属性设置为pre

如果您需要更通用的方法来处理带有嵌套元素的内容,请尝试以下答案:

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>
Tim Down

3
@Richard:好吧,keyup这可能是错误的事件,但这是原始问题中使用的方法。getCaretPosition()本身在自己的限制内是可以的。
蒂姆·唐

3
如果按Enter并换新行,则该JSFIDDLE演示将失败。该位置会显示0
giorgio79

5
@ giorgio79:是的,因为换行符生成一个<br><div>元素,这违反了答案中提到的第一个假设。如果您需要更通用的解决方案,则可以尝试stackoverflow.com/a/4812022/96100
Tim Down

2
无论如何有做,所以它包括行号?
Adjit

28

在其他答案中,我看不到一些皱纹:

  1. 元素可以包含多个级别的子节点(例如,具有具有子节点的子节点的子节点...)
  2. 一个选择可以包含不同的开始和结束位置(例如,选择了多个字符)
  3. 包含插入符号开始/结束的节点可能不是元素或其直接子元素

这是一种获取起点和终点位置作为元素的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 '19

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
不幸的是,一旦您按回车键并在另一行上开始,它就会停止工作(它再次从0开始-可能从CR / LF开始计数)。
伊恩(Ian)

如果您有一些粗体和/或斜体字,则无法正常使用。
user2824371 '18

14

试试这个:

Caret.js从文本字段获取插入符号位置和偏移量

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

演示:http : //ichord.github.com/Caret.js


真好吃 contenteditable li点击按钮重命名li内容时,我需要将此行为设置为插入符号的末尾。
akinuri

@AndroidDev我不是Caret.js的作者,但您是否考虑过为所有主要浏览器设置插入符的位置比几行更为复杂?您是否知道或创建了可以与我们共享的非blo肿替代方案?
adelriosantiago19年

8

Kinda迟到了聚会,但万一其他人都在挣扎。在过去的两天中,我没有发现任何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>

它一直选择到段落的开头,然后计算字符串的长度以获取当前位置,然后撤消选择以将光标返回到当前位置。如果要对整个文档(一个以上的段落)执行此操作,请根据您的情况更改paragraphboundarydocumentboundary或任意粒度。查看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> 每次都将游标放置在itag或其中的任何子html元素之前div,则游标位置将从0开始。是否有办法逃避此重新启动计数?
vam

奇。我在Chrome中没有得到这种行为。你使用的是什么浏览器?
Soubriquet

2
看起来或并非所有浏览器都支持selection.modify。developer.mozilla.org/en-US/docs/Web/API/Selection
Chris Sullivan

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

谢谢,我正在寻找从插入符号检索像素位置,它工作正常。
Sameesh

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)
    }
});

注意:范围对象本身可以存储在变量中,并且可以随时重新选择,除非contenteditable div的内容更改。

IE 8及更低版本的参考:http : //msdn.microsoft.com/zh-cn/library/ms535872(VS.85).aspx

标准(所有其他)浏览器的参考:https : //developer.mozilla.org/en/DOM/range(使用mozilla文档,但代码也适用于chrome,safari,opera和ie9)


1
谢谢,但是我怎样才能准确地获得div内容中插入符号位置的“索引”呢?
Bertvan

好的,好像在.getSelection()上调用.baseOffset可以解决问题。因此,这与您的答案一起回答了我的问题。谢谢!
Bertvan

2
不幸的是.baseOffset仅在webkit中起作用(我认为)。它还仅给您与插入符号的上级父级的偏移量(如果您在<div>内有<b>标记,它将提供距<b>开头而不是<div>开头的偏移量基于标准的范围可以使用range.endOffset range.startOffset range.endContainer和range.startContainer来获取与选择的父节点及其节点本身(包括文本节点)之间的偏移量。IE提供range.offsetLeft –以像素为单位从左侧偏移,因此毫无用处
Nico Burns 2010年

最好只是将范围对象自身存储起来,并使用window.getSelection()。addrange(range); <-标准和range.select(); <-IE,用于将光标重新放置在同一位置。range.insertNode(nodetoinsert); <-standards和range.pasteHTML(htmlcode); <-IE在光标处插入文本或html。
Nico Burns 2010年

Range大多数浏览器和返回的对象TextRange通过IE浏览器返回的对象是非常不同的东西,所以我不知道这个答案解决了多少。
Tim Down

3

由于这使我永远无法使用新的window.getSelection API,因此我将分享给后代。请注意,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,
    }
} 

这是在keyup上触发的jsfiddle。但是请注意,快速方向按键以及快速删除似乎是跳过事件。


为我工作!非常感谢。
dmodo

使用此文本时,已折叠,因此不再可能进行选择。可能的情况:需要评估每个keyUp事件
hschmieder

0

一种简单的方法,它遍历contenteditable div的所有子项,直到到达endContainer。然后,我添加结束容器偏移量,然后得到字符索引。应该与任何数量的嵌套一起使用。使用递归。

注意:需要多边形填充以支持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;
    }
  }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.