如何获取 ContentEditable 区域中的行数和当前插入符号行位置?

Posted

技术标签:

【中文标题】如何获取 ContentEditable 区域中的行数和当前插入符号行位置?【英文标题】:How to get number of rows in ContentEditable area and current caret line position? 【发布时间】:2011-07-28 12:47:08 【问题描述】:

我的问题有两个部分,但它们是相关的。

首先 - 我有包含一些文本的 Contenteditable DIV,我需要获取此 DIV 的总行数(行数)。有可能吗?

其次 - 我需要获得插入符号行的位置,如果它在第 1、2、3 等行上......

有人可以帮忙吗?

【问题讨论】:

【参考方案1】:

直接的答案是没有任何方法可以真正得到这些数字。但是,您可以将许多不同的变通方法应用于 estimate(我使用 estimate,因为我认为它们不能 100% 准确)那些价值观。

回答您的第一个问题,如何获取元素中的行数。假设元素使用一致的line-height,您可以通过将元素height 除以line-height 来找到行数。如果您的元素带有margins、paddings 或在元素内区分line-heights,这当然会变得更加复杂。如果这还不够,line-height 属性不一定必须是数值,即它可以是 normal 或 % 等。关于line-height 属性和如何在 SO 上获得它的数字表示,所以我不会对此进行太多详细介绍。为了我的回答,我确实在元素中专门分配了一个公共line-height

您的问题中更成问题的部分是在元素中找到插入符号的位置。同样,没有一种方法可以只为您返回正确的答案。相反,估计插入符号行位置的一种方法是在当前插入符号位置 (selection) 创建一个 range,在此处插入一个虚拟节点,获取节点 offset相对于容器offset,根据它的top offset计算行数,然后去掉dummy元素。

当您不能hide() 或将其留空时,虚拟元素会出现其他问题。

这似乎是一种非常糟糕的做法,而且确实如此。尝试使用箭头导航时,它当然不能完美地工作。那么为什么不将getClientRects() 用于selection 呢?当它只是没有任何空格等的文本时,它可以正常工作,但是当你在那里扔几个<br /><br /> 时,它不会再返回你那里的行位置。此外,当您位于一行的第一个或最后一个字符时,它有时会给出不正确的行行,因为它会根据可用空间量向上或向下抛出元素。

如果您正在寻找一种万无一失的方法来查找插入符号的位置和行数,那么没有。如果粗略估计就足够了,那么我的解决方案就是一个好的开始(它当然可以做更多的工作,并且对 IE 的支持在这种情况下应该容易得多,因为 TextRange 对象实际上以像素为单位提供偏移量)。

我对此的看法:

var lineHeight = parseInt($('#editable').css('line-height'));
//var ce = $('#editable')[0].getClientRects();
var ce = $('#editable').position();
console.log("Lines: "+$('#editable').height()/lineHeight);
$('#editable').bind('click keyup keydown',function()
   if(window.getSelection)
        range = window.getSelection().getRangeAt(0);
      range.insertNode($('<canvas />').attr('id','tempCaretFinder')[0]);
       var p = $('canvas#tempCaretFinder').position();
       $('canvas#tempCaretFinder').remove();
       console.log("Caret line: "+(Math.ceil((p.top-ce.top)/lineHeight)+1));

    else if(document.selection)  
        // the IE way, which should be relatively easier. as TextRange objects return offsets directly.
        range = document.selection.createRange();  
    

);

示例:http://jsfiddle.net/niklasvh/mKQUH/

编辑:尝试 2 http://jsfiddle.net/niklasvh/mKQUH/129/

如前所述,创建虚拟元素有时会使插入符号到处乱跳,因此我再次尝试使用 getClientRects() 获取范围。为了使它们更可行,我使选择扩展了一个字符,然后创建范围,然后检查 Rect 位置,然后将插入符号移回一个字符。

为什么?

因为在一行的第一个字符上的插入符号,它的顶部参数与前一行处于同一级别,因此通过对其应用额外的字符,它可以检查它是否实际上是一个新行。 getClientRects() 的另一个问题是它不适用于空换行符。为了适应这一点,我在这些情况下使用了之前创建的虚拟元素作为后备。

最终结果:

var lineHeight = parseInt($('#editable').css('line-height'));
//var ce = $('#editable')[0].getClientRects();
var ce = $('#editable').offset();
console.log("Lines: "+$('#editable').height()/lineHeight);
$('#editable').bind('click keyup keydown',function(e)
    //alert($(window).scrollTop());
   if(window.getSelection)
       var save = window.getSelection();
       var s = window.getSelection();
       s.modify('extend','forward','character');
     // s.modify('extend','forward','line');
       range = s.getRangeAt(0);
       var p = range.getClientRects();
       var top;
       //console.log(p);
       if (typeof p[1] != "undefined")
           top = p[1].top+$(window).scrollTop();
               else if (typeof p[0] != "undefined")
                top = p[0].top+$(window).scrollTop();    
               
       else
          // sigh... let's make a real work around then
           range.insertNode($('<canvas />').attr('id','tempCaretFinder')[0]);
       var p = $('canvas#tempCaretFinder').offset();
       $('canvas#tempCaretFinder').remove();
           top = p.top;

       

     // console.log(top-ce.top);
       console.log("Caret line: "+(Math.ceil((top-ce.top)/lineHeight)));
       save.modify('move','backward','character');
       /*
       range = s.getRangeAt(0);
      range.insertNode($('<canvas />').attr('id','tempCaretFinder')[0]);
       var p = $('canvas#tempCaretFinder').position();
       $('canvas#tempCaretFinder').remove();
       console.log("Caret line: "+(Math.ceil((p.top-ce.top)/lineHeight)+1));
       console.log(e.which);

       switch(e.which)
           case 40:
             s.modify("move", "forward","line");

             break;  
                    case 38:
             s.modify("move", "backward","lineboundary");

             break;  
       
       */


    else if(document.selection) 
        // the IE way, which should be relatively easier. as TextRange objects return offsets directly.
        range = document.selection.createRange();  
    

);

【讨论】:

@Niklas 谢谢你的回答,我可以接受你在这里写的“相当不错的估计”。但是,您的示例中有一个小错误,我真的找不到问题所在。当您单击某行中间的某个位置时,似乎 contenteditable 元素会记住此水平位置,而当您使用箭头上下移动时,插入符号会改变每行的水平位置,这有点令人沮丧。使用向上和向下箭头键时的正常行为是,插入符号只是向上或向下跳到当前行,但不会改变其水平位置。 @Frodik 我也注意到了这一点(尽管仅在 firefox 上),我相信这是创建一个元素并将其注入其中,然后将其删除的结果。我尝试将解决方案更改为使用 getClientRects(),但它的问题比这个解决方案还要多。我确切地知道您遇到的问题,但很遗憾我没有找到解决方案。 @Niklas 我正在使用 Chrome,它的行为相同。如果有可能克服这个问题,您能否再次考虑一下您的解决方案。因为否则这个解决方案将毫无用处,普通用户肯定无法接受。 @Frodik 好的,我们开始吧。尝试 2:jsfiddle.net/niklasvh/mKQUH/129 用我的不同做法更新我的答案 @Niklas 感谢您的更新!虽然,我看到一些新的小错误。抱歉,如果我很挑剔,但如果您也可以摆脱这些,它将使您的解决方案完全可用。问题 #1 是当您在任何一行上点击“End”键时,插入符号不会跳到行尾,而是跳到下一行的开头。问题 #2 是插入符号水平放置的任何位置,当您使用向上或向下箭头时,它总是在行首跳转并且不保持水平位置。问题 #3 - 无法导航到最后一个字母...

以上是关于如何获取 ContentEditable 区域中的行数和当前插入符号行位置?的主要内容,如果未能解决你的问题,请参考以下文章

区域可编辑contenteditable的问题总结

如何在 IE 的 contentEditable div 中获取选定的文本节点?

如何在 IE 的 contentEditable div 中获取选定的文本节点?

js之向div contenteditable光标位置添加字符

如何从 contenteditable 中获取“插入符号位置”?

如何在contenteditable中获取光标的当前位置? [复制]