获取文本区域中插入符号的像素偏移位置

32
在我的项目中,我试图以像素为单位获取 textarea 中插入符的偏移位置。这个可以做到吗?
在来这里之前,我已经查看了许多 links,特别是 Tim Down 的,但我找不到适用于 IE8+、Chrome 和 Firefox 的解决方案。似乎 Tim Down 正在研究这个问题
我发现的一些 other links 存在许多问题,比如无法找到插入符位置的顶部偏移量。
我试图获取插入符的偏移位置,因为我想通过基于插入符的偏移位置定位它,在 textarea 内显示自动完成建议框。 PS:我不能使用 contenteditable div,因为我已经编写了许多与 textarea 相关的代码。

1
你试过这个吗:https://dev59.com/sHI-5IYBdhLWcg3wYXH8#1909997作者说它可以在IE上运行。 - MythThrazz
“偏移位置”和“索引位置”有什么区别? - Aaron Digulla
@AaronDigulla 使用索引位置,我只能获取左侧值(基于字母宽度,大约),但无法获取顶部值。 - Mr_Green
你需要 X 和 Y 坐标吗? - Tivie
2
@DuplicateVoters,这个问题确实之前已经被问过了,而且现在它已经有一个可能很好地解决了OP的需求的答案(我没有测试过),但是我想指出,在这个问题被提出和回答的时候,“之前已经被问过并且已经有答案”的那个问题还没有一个能够满足OP需要获取textarea中插入符偏移位置的像素的答案。 - Mathijs Flietstra
显示剩余3条评论
5个回答

18
你可以创建一个单独的(不可见)元素,并将其从开头到光标位置填充textarea内容。Textarea和“克隆”应具有匹配的CSS(字体属性,内边距/外边距/边框和宽度)。然后将这些元素堆叠在一起。
让我先用一个工作示例开始,然后逐步浏览代码:http://jsfiddle.net/g7rBk/

更新的Fiddle带IE8修复

HTML:
<textarea id="input"></textarea>
<div id="output"><span></span></div>
<div id="xy"></div>

CSS:

/* identical styling to match the dimensions and position of textarea and its "clone"
*/
#input, #output {
    position:absolute;
    top:0;
    left:0;
    font:14px/1 monospace;
    padding:5px;
    border:1px solid #999;
    white-space:pre;
    margin:0;
    background:transparent;
    width:300px;
    max-width:300px;
}
/* make sure the textarea isn't obscured by clone */
#input { 
    z-index:2;
    min-height:200px;
}

#output { 
    border-color:transparent; 
}

/* hide the span visually using opacity (not display:none), so it's still measurable; make it break long words inside like textarea does. */
#output span {
    opacity:0;
    word-wrap: break-word;
    overflow-wrap: break-word;
}
/* the cursor position indicator */
#xy { 
    position:absolute; 
    width:4px;
    height:4px;
    background:#f00;
}

JavaScript:

/* get references to DOM nodes we'll use */
var input = document.getElementById('input'),
    output = document.getElementById('output').firstChild,
    position = document.getElementById('position'),

/* And finally, here it goes: */
    update = function(){
         /* Fill the clone with textarea content from start to the position of the caret. You may need to expand here to support older IE [1]. The replace /\n$/ is necessary to get position when cursor is at the beginning of empty new line.
          */
         output.innerHTML = input.value.substr( 0, input.selectionStart ).replace(/\n$/,"\n\001");

        /* the fun part! 
           We use an inline element, so getClientRects[2] will return a collection of rectangles wrapping each line of text.
           We only need the position of the last rectangle.
         */
        var rects = output.getClientRects(),
            lastRect = rects[ rects.length - 1 ],
            top = lastRect.top - input.scrollTop,
            left = lastRect.left+lastRect.width;
        /* position the little div and see if it matches caret position :) */
        xy.style.cssText = "top: "+top+"px;left: "+left+"px";
    }

[1] 如何获取textarea中光标的位置,以字符为单位

[2] https://developer.mozilla.org/en/docs/DOM/element.getClientRects

编辑:此示例仅适用于固定宽度的文本区域。要使其与用户可调整大小的文本区域配合使用,您需要添加一个事件监听器来监听调整大小事件,并将#output尺寸设置为匹配新的#input尺寸。


16

这里有一种用 rangyinputsrangyjQuery 的方法。

这个方法基本上是将textarea中的整个文本复制到相同大小的div中。我设置了一些 CSS,以确保在每个浏览器中,textareadiv以完全相同的方式包裹它们的内容。

当单击 textarea 时,我会读取光标所在的字符索引,然后在 div 内部插入一个与该索引相同的光标 span。只做这个操作时,如果用户单击行首,光标 span 会跳回到上一行,因此我检查前一个字符是否是一个空格(这将允许换行),如果是,则将其包装在一个 span 中,并将下一个单词(紧接着光标位置的单词)也包装在一个 span 中。现在,我比较这两个 span 的顶部值,如果它们不同,则发生了一些换行,因此我假设 #nextword spantopleft 值等同于光标位置。
这种方法仍有改进的空间,我相信我没有考虑到所有可能出现的问题,即使我已经考虑到了,我也没有为所有问题实施修复,因为我目前没有时间这样做,以下是您需要考虑的一些事情:
  • 它还不能处理使用Enter插入的硬回车(已解决)
  • 输入连续多个空格时位置会断开(已解决)
  • 我认为连字符也可以允许内容换行。

目前在Windows 8上,使用Chrome、Firefox、IE和Safari的最新版本,它在所有浏览器中的工作方式完全相同。我的测试还不够严格。

这里有一个jsFiddle

我希望它能对您有所帮助,至少它可能会给您一些构建的思路。

一些特点:

  • 我已经为您添加了一个ul,它位于正确的位置,并修复了Firefox中的一个问题,在DOM操作后,textarea选择未重新设置回其原始位置。

  • 我已添加了IE7 - IE9支持,并修复了评论中指出的多个单词选择问题。

  • 我已添加了对使用Enter插入硬回车和多个连续空格的支持。

  • 我已修复了ctrl+shift+left arrow文本选择方法的默认行为问题。

JavaScript

function getTextAreaXandY() {

    // Don't do anything if key pressed is left arrow
    if (e.which == 37) return;     

    // Save selection start
    var selection = $(this).getSelection();
    var index = selection.start;

    // Copy text to div
    $(this).blur();
    $("div").text($(this).val());

    // Get current character
    $(this).setSelection(index, index + 1);
    currentcharacter = $(this).getSelection().text;

    // Get previous character
    $(this).setSelection(index - 1, index)
    previouscharacter = $(this).getSelection().text;

    var start, endchar;
    var end = 0;
    var range = rangy.createRange();

    // If current or previous character is a space or a line break, find the next word and wrap it in a span
    var linebreak = previouscharacter.match(/(\r\n|\n|\r)/gm) == undefined ? false : true;
    
    if (previouscharacter == ' ' || currentcharacter == ' ' || linebreak) {
        i = index + 1; // Start at the end of the current space        
        while (endchar != ' ' && end < $(this).val().length) {
            i++;
            $(this).setSelection(i, i + 1)
            var sel = $(this).getSelection();
            endchar = sel.text;
            end = sel.start;
        }

        range.setStart($("div")[0].childNodes[0], index);
        range.setEnd($("div")[0].childNodes[0], end);
        var nextword = range.toHtml();
        range.deleteContents();
        var position = $("<span id='nextword'>" + nextword + "</span>")[0];
        range.insertNode(position);
        var nextwordtop = $("#nextword").position().top;
    }

    // Insert `#caret` at the position of the caret
    range.setStart($("div")[0].childNodes[0], index);
    var caret = $("<span id='caret'></span>")[0];
    range.insertNode(caret);
    var carettop = $("#caret").position().top;

    // If preceding character is a space, wrap it in a span
    if (previouscharacter == ' ') {
        range.setStart($("div")[0].childNodes[0], index - 1);
        range.setEnd($("div")[0].childNodes[0], index);
        var prevchar = $("<span id='prevchar'></span>")[0];
        range.insertNode(prevchar);
        var prevchartop = $("#prevchar").position().top;
    }

    // Set textarea selection back to selection start
    $(this).focus();
    $(this).setSelection(index, selection.end);

    // If the top value of the previous character span is not equal to the top value of the next word,
    // there must have been some wrapping going on, the previous character was a space, so the wrapping
    // would have occured after this space, its safe to assume that the left and top value of `#nextword`
    // indicate the caret position
    if (prevchartop != undefined && prevchartop != nextwordtop) {
        $("label").text('X: ' + $("#nextword").position().left + 'px, Y: ' + $("#nextword").position().top);
        $('ul').css('left', ($("#nextword").position().left) + 'px');
        $('ul').css('top', ($("#nextword").position().top + 13) + 'px');
    }
    // if not, then there was no wrapping, we can take the left and the top value from `#caret`    
    else {
        $("label").text('X: ' + $("#caret").position().left + 'px, Y: ' + $("#caret").position().top);
        $('ul').css('left', ($("#caret").position().left) + 'px');
        $('ul').css('top', ($("#caret").position().top + 14) + 'px');
    }

    $('ul').css('display', 'block');
}

$("textarea").click(getTextAreaXandY);
$("textarea").keyup(getTextAreaXandY);

HTML

<div></div>
<textarea>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</textarea>
<label></label>
<ul>
    <li>Why don't you type this..</li>
</ul>

CSS

body {
    font-family: Verdana;
    font-size: 12px;
    line-height: 14px;
}
textarea, div {
    font-family: Verdana;
    font-size: 12px;
    line-height: 14px;
    width: 300px;
    display: block;
    overflow: hidden;
    border: 1px solid black;
    padding: 0;
    margin: 0;
    resize: none;
    min-height: 300px;
    position: absolute;
    -moz-box-sizing: border-box;
    white-space: pre-wrap;
}
span {
    display: inline-block;
    height: 14px;
    position: relative;
}
span#caret {
    display: inline;
}
label {
    display: block;
    margin-left: 320px;
}
ul {
    padding: 0px;
    margin: 9px;
    position: absolute;
    z-index: 999;
    border: 1px solid #000;
    background-color: #FFF;
    list-style-type:none;
    display: none;
}
@media screen and (-webkit-min-device-pixel-ratio:0) {
    span {
        white-space: pre-wrap;
    }
}
div {
    /* Firefox wrapping fix */
    -moz-padding-end: 1.5px;
    -moz-padding-start: 1.5px;
    /* IE8/IE9 wrapping fix */
    padding-right: 5px\0/;
    width: 295px\0/;
}
span#caret
{
    display: inline-block\0/;
}

7

获取光标位置的解决方案比其他答案中提供的要简单得多。

请注意,这个问题是2008年的重复问题,我在这里已经回答了它。我只会维护那个链接上的答案,因为这个问题应该在很多年前就被关闭了。

答案副本

我正在寻找一个meteor-autocomplete的textarea光标坐标插件,所以我评估了GitHub上的所有8个插件。远远胜出的是来自Componenttextarea-caret-position

特点

  • 像素级精度
  • 没有任何依赖关系
  • 浏览器兼容性: Chrome,Safari,Firefox(尽管它有两个 错误),IE9+; 可能在Opera,IE8或更早版本中工作,但未经过测试
  • 支持任何字体系列和大小,以及文本转换
  • 文本区域可以具有任意填充或边框
  • 不会因文本区域中的水平或垂直滚动条而混淆
  • 支持文本中的硬回车、制表符(除IE外)和连续空格
  • 在文本区域中长于列的行上正确位置
  • 当包装长单词时,在行末没有“空白”位置

这是一个演示 - http://jsfiddle.net/dandv/aFPA7/

enter image description here

工作原理

创建一个离屏幕的镜像 <div>,并将其样式设置与 <textarea> 相同。然后将文本框中光标之前的文本复制到 div 中,并在其后插入一个 <span>。接着,将 span 的文本内容设置为文本框中余下的文本,以便在伪造的 div 中忠实地复制换行。

这是唯一保证处理所有关于长行换行的边缘情况的方法。GitHub 也使用它来确定其@用户下拉菜单的位置。


1
非常详细的答案 - alessioalex

2

工作示例的JsFiddle链接:http://jsfiddle.net/42zHC/2/

基本上,我们需要找出可以适应宽度的列数(因为它将是等宽字体)。我们必须始终强制显示滚动条,否则计算会出错。然后,我们将适合宽度的列数除以宽度,得到每个字符的x偏移量。然后我们在文本区域上设置行高。由于我们知道一行中有多少字符,我们可以将其与字符数量相除,然后得到行号。通过行高,我们现在有了y偏移量。然后获取文本区域的scrollTop,并将其减去,以便一旦开始使用滚动条,它仍然显示在正确的位置。

Javascript:

$(document).ready(function () {
  var cols = document.getElementById('t').cols;
  var width = document.getElementById('t').clientWidth;
  var height = $('textarea').css('line-height');
  var pos = $('textarea').position();
  $('#t').on('keyup', function () {
    el = document.getElementById("t");
    if (el.selectionStart) { 
        selection = el.selectionStart; 
      } else if (document.selection) { 
        el.focus(); 
        var r = document.selection.createRange(); 
        if (r == null) { 
           selection = 0; 
        } 
        var re = el.createTextRange(), 
        rc = re.duplicate(); 
        re.moveToBookmark(r.getBookmark()); 
        rc.setEndPoint('EndToStart', re); 
        selection = rc.text.length; 
      } else { selection = 0 }
    var row = Math.floor((selection-1) / cols);
    var col = (selection - (row * cols));
    var x = Math.floor((col*(width/cols)));
    var y = (parseInt(height)*row);
    $('span').html("row: " + row + "<br>columns" + col + "<br>width: " + width + "<br>x: " + x +"px<br>y: " + y +"px<br>Scrolltop: "+$(this).scrollTop()).css('top',pos.top+y-$(this).scrollTop()).css('left',pos.left+x+10);
  });
});

HTML:

<textarea id="t"></textarea>
<br>
<span id="tooltip" style="background:yellow"></span>

CSS:

textarea {
  height: 80px;
  line-height: 12px;
  overflow-y:scroll;
}
span {
  position: absolute;
}

谢谢您的回复。这很酷,但有一些限制,比如当我按下回车键并输入内容时它不起作用。 :) - Mr_Green
根据字体是否等宽,这是一个相当大且不必要的限制。@Mr_Green,我查看了GitHub上所有textarea插件光标位置,并改进了最具有能力、稳定性和简单性的插件。它没有依赖项,在Firefox、Chrome和IE9上工作(可能在旧版浏览器中也可以),而且只有80行代码。 - Dan Dascalescu
@DanDascalescu 谢谢。不知何故,我没有收到您的提醒。该项目已经完成,所以我不再测试它了。希望对其他人有所帮助 :)。另外,您可以通过添加“show”来在旧版浏览器中使用jsfiddle进行检查,例如jsfiddle.net/abcd/show - Mr_Green

-1

我无法找到类似的解决方案,所以我的解决方法是在文本区域中定位插入符号的字符位置,剪切当前段落并将其显示在文本区域旁边。

使用偏移量,在可编辑文本的视图中放置一个虚拟光标(divdisplay:inline,1像素宽,border-left: 1px solid black)。

这样,您可以创建一个视觉反馈区域,在其中显示效果的结果(就像stackoverflow在您编写答案时所做的那样)。


1
抱歉,我无法理解您的解释。 - Mr_Green
这是一个解决方法。我不再尝试寻找光标位置,而是展示给用户文本的外观。但是是否有用取决于你需要知道插入符位置的原因。 - Aaron Digulla
因为我正在尝试展示一个由ul元素制作的建议框,并根据那些偏移位置值进行定位。 - Mr_Green

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