javascript 以像素为单位获取 textarea 中插入符号的偏移位置

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16212871/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-27 03:44:17  来源:igfitidea点击:

Get the offset position of the caret in a textarea in pixels

javascriptjquerycross-browsertextarea

提问by Mr_Green

In my project I'm trying to get the offset position of the caret in a textareain pixels. Can this be done?

在我的项目中,我试图以textarea像素为单位获取插入符号的偏移位置。这能做到吗?

Before asking here, I have gone through many links, especially Tim Down's, but I couldn't find a solution which works in IE8+, Chrome and Firefox. It seems Tim Down is working on this.

在问这里之前,我浏览了很多链接,尤其是 Tim Down 的,但我找不到适用于 IE8+、Chrome 和 Firefox 的解决方案。似乎Tim Down 正在为此努力

Some otherlinkswhich I have found have many issues like not finding the top offset of the caret position.

我发现的其他一些链接有很多问题,比如找不到插入符号位置的顶部偏移量。

I am trying to get the offset position of the caret because I want to show an auto-complete suggestion box inside the textareaby positioning it based on the offset position of the caret.

我正在尝试获取插入符号的偏移位置,因为我想根据插入符号的偏移位置对其进行定位,从而在其中显示一个自动完成建议框textarea

PS:I can't use a contenteditable divbecause I have written lots of code related to a textarea.

PS:我不能使用 acontenteditable div因为我写了很多与 a 相关的代码textarea

采纳答案by Mathijs Flietstra

Here's an approach using rangyinputs, rangyand jQuery.

这是使用rangyinputsrangyjQuery 的方法

It basically copies the whole text from inside the textareainto a divof the same size. I have set some CSS to ensure that in every browser, the textareaand the divwrap their content in exactly the same way.

它基本上将整个文本从内部复制textareadiv相同大小的a中。我设置了一些 CSS 以确保在每个浏览器中,textareadiv以完全相同的方式包装它们的内容。

When the textareais clicked, I read out at which character index the caret is positioned, then I insert a caret spanat the same index inside the div. By only doing that I ended up having an issue with the caret spanjumping back to the previous line if the user clicked at the start of a line. To fix that I check if the previous character is a space(which would allow a wrap to occur), if that is true, I wrap it in a span, and I wrap the next word (the one directly after the caret position) in a span. Now I compare the top values between these two span's, if they differ, there was some wrapping going on, so I assume that the topand the leftvalue of the #nextwordspanare equivalent to the caret position.

textarea被点击时,我读出在该字符索引插入符号被定位,然后我插入一个插入符span内的同一索引处divspan如果用户单击行的开头,我最终会遇到插入符号跳回上一行的问题。为了解决这个问题,我检查前一个字符是否是 a space(这将允许发生换行),如果是true,我将它包装在 a 中span,然后将下一个单词(插入符号位置直接后面的那个)包装在 a 中span。现在我比较这两个span's之间的最高值,如果它们不同,则有一些包装正在进行,所以我假设top和 的left#nextwordspan等同于插入符号位置。

This approach can still be improved upon, I'm sure I haven't thought of everything that could possibly go wrong, and even if I have, then I haven't bothered implementing a fix for all of them as I don't have the time to do so at the moment, a number of things that you would need to look at:

这种方法仍然可以改进,我敢肯定我还没有想到可能会出错的一切,即使我有,我也没有费心为所有这些问题实施修复,因为我没有现在是时候这样做了,您需要考虑以下几点:

  • it doesn't yet handle hard returns inserted with Enter(fixed)
  • positioning breaks when entering multiple spaces in a row(fixed)
  • I think hyphens would allow a content wrap to occur as well..
  • 它尚未处理插入Enter(固定)的硬返回
  • 连续输入多个空格时定位中断(固定)
  • 我认为连字符也会允许内容换行发生..

Currently it works exactly the same way across browsers here on Windows 8 with the latest versions of Chrome, Firefox, IE and Safari. My testing has not been very rigorous though.

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

Here's a jsFiddle.

这是一个jsFiddle.

I hope it will help you, at the very least it might give you some ideas to build on.

我希望它会帮助你,至少它可以给你一些想法。

Some Features:

一些特点:

  • I have included a ulfor you which is positioned in the right spot, and fixed a Firefox issue where the textareaselection was not re-set back to its original spot after the DOM manipulations.

  • I have added IE7 - IE9 support and fixed the multiple word selection issue pointed out in the comments.

  • I have added support for hard returns inserted with Enterand multiple spaces in a row.

  • I have fixed an issue with the default behaviour for the ctrl+shift+left arrowtext selection method.

  • 我已经包含了一个ul位于正确位置的for you,并修复了一个 Firefox 问题,即textarea在 DOM 操作后选择没有重新设置回原来的位置。

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

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

  • 我有固定的默认行为问题ctrl+ shift+left arrow的文本选择方法。

JavaScript

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

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

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
<textarea id="input"></textarea>
<div id="output"><span></span></div>
<div id="xy"></div>
/; width: 295px
/* 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;
}
/; } span#caret { display: inline-block
/* 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
$(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);
  });
});
1"); /* 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"; }
/; }

回答by pawel

You can create a separate (invisible) element and fill it with textarea content from start to the cursor position. Textarea and the "clone" should have matching CSS (font properties, padding/margin/border and width). Then stack these elements on top of each other.

您可以创建一个单独的(不可见的)元素,并从开始到光标位置用 textarea 内容填充它。Textarea 和“克隆”应该具有匹配的 CSS(字体属性、内边距/边距/边框和宽度)。然后将这些元素堆叠在一起。

Let me start with a working example, then walk through the code: http://jsfiddle.net/g7rBk/

让我从一个工作示例开始,然后遍历代码:http: //jsfiddle.net/g7rBk/

Updated Fiddle(with IE8 fix)

更新的小提琴使用 IE8 修复

HTML:

HTML:

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

Textarea is self-explanatory. Output is a hidden element to which we'll pass text content and make measures. What's important is that we'll use an inline element. the "xy" div is just an indicator for testing purposes.

Textarea 是不言自明的。输出是一个隐藏元素,我们将向其传递文本内容并进行度量。重要的是我们将使用内联元素。“xy” div 只是用于测试目的的指标。

CSS:

CSS:

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

JavaScript:

JavaScript:

##代码##

[1] Caret position in textarea, in characters from the start

[1]在 textarea 中插入符号的位置,从开始的字符数

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

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

Edit: This example only works for fixed-width textarea. To make it work with user-resizable textarea you'd need to add an event listener to the resize event and set the #output dimensions to match new #input dimensions.

编辑:此示例仅适用于固定宽度的 textarea。要使其与用户可调整大小的 textarea 一起使用,您需要将事件侦听器添加到调整大小事件并设置 #output 尺寸以匹配新的 #input 尺寸。

回答by Dan Dascalescu

There's a much simpler solution for getting the caret position in pixels, than what's been presented in the other answers.

与其他答案中提供的内容相比,以像素为单位获取插入符号位置的解决方案要简单得多。

Note that this question is a duplicate of a 2008 one, and I've answered it here. I'll only maintain the answer at that link, since this question should have been closed as duplicate years ago.

请注意,此问题与 2008 年的问题重复,我已在此处回答。我只会在那个链接上保留答案,因为这个问题应该在几年前重复关闭。

Copy of the answer

答案副本

I've looked for a textarea caret coordinates plugin for meteor-autocomplete, so I've evaluated all the 8 plugins on GitHub. The winner is, by far, textarea-caret-positionfrom Component.

我已经寻找了用于meteor-autocomplete的textarea caret 坐标插件,所以我已经评估了GitHub 上的所有8 个插件。到目前为止,获胜者是来自Component 的textarea-caret-position

Features

特征

  • pixel precision
  • no dependencies whatsoever
  • browser compatibility: Chrome, Safari, Firefox (despite twobugsit has), IE9+; may work but not tested in Opera, IE8 or older
  • supports any font family and size, as well as text-transforms
  • the text area can have arbitrary padding or borders
  • not confused by horizontal or vertical scrollbars in the textarea
  • supports hard returns, tabs (except on IE) and consecutive spaces in the text
  • correct position on lines longer than the columns in the text area
  • no "ghost" position in the empty spaceat the end of a line when wrapping long words
  • 像素精度
  • 没有任何依赖
  • 浏览器兼容性:Chrome、Safari、Firefox(尽管有两个错误)、IE9+;可能工作但未在 Opera、IE8 或更早版本中测试
  • 支持任何字体系列和大小,以及文本转换
  • 文本区域可以有任意填充或边框
  • 不会被 textarea 中的水平或垂直滚动​​条混淆
  • 支持硬回车、制表符(IE 除外)和文本中的连续空格
  • 比文本区域中的列长的行上的正确位置
  • 换行长单词时,行尾空白处没有“幽灵”位置

Here's a demo - http://jsfiddle.net/dandv/aFPA7/

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

enter image description here

在此处输入图片说明

How it works

怎么运行的

A mirror <div>is created off-screen and styled exactly like the <textarea>. Then, the text of the textarea up to the caret is copied into the div and a <span>is inserted right after it. Then, the text content of the span is set to the remainder of the text in the textarea, in order to faithfully reproduce the wrapping in the faux div.

镜子<div>是在屏幕外创建的,其样式与<textarea>. 然后,直到插入符号的 textarea 的文本被复制到 div 中,并<span>在它之后插入a 。然后,将span的文本内容设置为textarea中文本的剩余部分,以忠实地再现仿div中的换行。

This is the only method guaranteed to handle all the edge cases pertaining to wrapping long lines. It's also used by GitHub to determine the position of its @user dropdown.

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

回答by dave

JsFiddle of working example: http://jsfiddle.net/42zHC/2/

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

Basically, we figure out how many columns fit in the width (since it will be monospace). We have to force scrollbars to always be there otherwise the calculation is off. Then we divide the number of columns that fit with the width, and we get the x offset per character. Then we set the line height on the textarea. Since we know how many characters are in a row, we can divide that with the number of characters and we get the row number. With the line height, we now have the y offset. Then we get the scrollTop of the textarea and subtract that, so that once it starts using the scrollbar, it still shows up in the right position.

基本上,我们计算出多少列适合宽度(因为它将是等宽的)。我们必须强制滚动条始终在那里,否则计算就会关闭。然后我们将适合宽度的列数除以,我们得到每个字符的 x 偏移量。然后我们在textarea上设置行高。由于我们知道一行中有多少个字符,我们可以将其除以字符数,我们得到行号。有了行高,我们现在有了 y 偏移量。然后我们得到 textarea 的 scrollTop 并减去它,这样一旦它开始使用滚动条,它仍然显示在正确的位置。

Javascript:

Javascript:

##代码##

HTML:

HTML:

##代码##

CSS:

CSS:

##代码##

回答by Aaron Digulla

I couldn't get something similar to work, so my solution was to locate the character position of the caret in the textarea, cut out the current paragraph and display this next to the textarea.

我找不到类似的东西,所以我的解决方案是在 textarea 中找到插入符号的字符位置,剪切当前段落并将其显示在 textarea 旁边。

Using the offset, I placed a fake cursor (div, display:inline, 1px wide, border-left: 1px solid black) in this view of the editable text.

使用偏移量,我在此可编辑文本视图中放置了一个假光标(div, display:inline, 1px 宽, border-left: 1px solid black)。

This way, you can create a visual feedback area where you can show the result of effects (quite like stackoverflow does when you write an answer).

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