获取输入元素中光标或文本的像素位置

20

在IE中,我可以在输入元素中创建文本范围,在此范围内可以调用getBoundingClientRect()来获取特定字符或光标/插入符号的像素位置。是否有办法在其他浏览器中以像素为单位获取特定字符的位置?

var input = $("#myInput")[0];
var pixelPosition = null;
if (input.createTextRange)
{
    var range = input.createTextRange();
    range.moveStart("character", 6);
    pixelPosition = range.getBoundingClientRect();
}
else
{
    // Is there any way to create a range on an input's value?
}

我正在使用jQuery,但我怀疑它能否解决我的问题。如果有的话,我期望是纯JavaScript的解决方案,但欢迎jQuery的答案。


可能与您正在寻找的相关:https://dev59.com/7W855IYBdhLWcg3w6IvV - Jesse
我很确定简短的答案是“不行”,但我现在没有时间详细阐述或研究。 - Tim Down
@TimDown,我已经实现了所需的行为。由于您在范围方面有丰富的经验(因为您的Rangy项目),您能否检查一下? - Rob W
@RobW:好的,我稍后今天会看一下。 - Tim Down
@RobW:代码中涉及范围的内容很少,更多的是关于定位和样式方面的问题,这方面我了解一些但并非我的专业领域。它看起来很合理,但我会在不同的浏览器和操作系统中进行严格测试:我认为某些浏览器和操作系统中的文本输入框可能具有不同数量的无法删除甚至无法测量的填充,这可能会导致一些问题。可能还会涉及到完全模拟输入框样式的其他问题。 - Tim Down
请参考此问题以获取文本输入字段中光标位置的字符数。 - Dan Dascalescu
4个回答

17

演示
我写了一个表现符合预期的函数。您可以在此找到非常详细的演示面板:Fiddle: http://jsfiddle.net/56Rep/5/
演示中的界面是自解释的。

按照问题所要求的功能将被实现在我的函数中,如下所示:
    var pixelPosition = getTextBoundingRect(input, 6)

函数依赖
更新: 该函数是纯JavaScript编写的,不依赖于任何插件或框架!
该函数假定getBoundingClientRect方法存在。当支持时使用文本范围,否则使用我的函数逻辑来实现功能。

函数逻辑
代码本身包含多个注释。这部分内容更加详细。

  1. 创建一个临时<div>容器。
  2. 创建1-3个<span>元素,每个包含输入值的一部分(偏移量为0到selectionStartselectionStartselectionEndselectionEnd到字符串末尾,只有第二个是有意义的)。
  3. 从输入元素中复制几个重要的样式属性到这些<div><span>标签中。只复制重要的样式属性。例如,color不会被复制,因为它不以任何方式影响字符的偏移量。#1
  4. <div>文本节点(输入值)的确切位置上定位。边框和填充被考虑在内,以确保临时<div>正确地定位。
  5. 创建一个变量,它保存div.getBoundingClientRect()的返回值。
  6. 如果参数debug设置为true,则删除临时<div>
  7. 函数返回ClientRect对象。有关此对象的更多信息,请参见此页面演示还显示了属性列表:topleftrightbottomheightwidth

#1: 使用getBoundingClientRect()(以及一些次要属性)来确定输入元素的位置。然后,添加填充和边框宽度,以获取文本节点的实际位置。

已知问题
唯一一种不一致的情况是在getComputedStyle返回错误的font-family值时遇到的:当页面没有定义font-family属性时,计算样式返回一个不正确的值(即使Firebug也遇到了这个问题;环境:Linux,Firefox 3.6.23,字体“Sans Serif”)。

如演示中所示,定位有时会略微偏移(几乎为零,始终小于1像素)。

技术限制防止脚本在内容被移动时获取文本片段的精确偏移量,例如,当输入字段中的第一个可见字符不等于第一个值的字符时。

代码

// @author Rob W       http://stackoverflow.com/users/938089/rob-w
// @name               getTextBoundingRect
// @param input          Required HTMLElement with `value` attribute
// @param selectionStart Optional number: Start offset. Default 0
// @param selectionEnd   Optional number: End offset. Default selectionStart
// @param debug          Optional boolean. If true, the created test layer
//                         will not be removed.
function getTextBoundingRect(input, selectionStart, selectionEnd, debug) {
    // Basic parameter validation
    if(!input || !('value' in input)) return input;
    if(typeof selectionStart == "string") selectionStart = parseFloat(selectionStart);
    if(typeof selectionStart != "number" || isNaN(selectionStart)) {
        selectionStart = 0;
    }
    if(selectionStart < 0) selectionStart = 0;
    else selectionStart = Math.min(input.value.length, selectionStart);
    if(typeof selectionEnd == "string") selectionEnd = parseFloat(selectionEnd);
    if(typeof selectionEnd != "number" || isNaN(selectionEnd) || selectionEnd < selectionStart) {
        selectionEnd = selectionStart;
    }
    if (selectionEnd < 0) selectionEnd = 0;
    else selectionEnd = Math.min(input.value.length, selectionEnd);

    // If available (thus IE), use the createTextRange method
    if (typeof input.createTextRange == "function") {
        var range = input.createTextRange();
        range.collapse(true);
        range.moveStart('character', selectionStart);
        range.moveEnd('character', selectionEnd - selectionStart);
        return range.getBoundingClientRect();
    }
    // createTextRange is not supported, create a fake text range
    var offset = getInputOffset(),
        topPos = offset.top,
        leftPos = offset.left,
        width = getInputCSS('width', true),
        height = getInputCSS('height', true);

        // Styles to simulate a node in an input field
    var cssDefaultStyles = "white-space:pre;padding:0;margin:0;",
        listOfModifiers = ['direction', 'font-family', 'font-size', 'font-size-adjust', 'font-variant', 'font-weight', 'font-style', 'letter-spacing', 'line-height', 'text-align', 'text-indent', 'text-transform', 'word-wrap', 'word-spacing'];

    topPos += getInputCSS('padding-top', true);
    topPos += getInputCSS('border-top-width', true);
    leftPos += getInputCSS('padding-left', true);
    leftPos += getInputCSS('border-left-width', true);
    leftPos += 1; //Seems to be necessary

    for (var i=0; i<listOfModifiers.length; i++) {
        var property = listOfModifiers[i];
        cssDefaultStyles += property + ':' + getInputCSS(property) +';';
    }
    // End of CSS variable checks

    var text = input.value,
        textLen = text.length,
        fakeClone = document.createElement("div");
    if(selectionStart > 0) appendPart(0, selectionStart);
    var fakeRange = appendPart(selectionStart, selectionEnd);
    if(textLen > selectionEnd) appendPart(selectionEnd, textLen);

    // Styles to inherit the font styles of the element
    fakeClone.style.cssText = cssDefaultStyles;

    // Styles to position the text node at the desired position
    fakeClone.style.position = "absolute";
    fakeClone.style.top = topPos + "px";
    fakeClone.style.left = leftPos + "px";
    fakeClone.style.width = width + "px";
    fakeClone.style.height = height + "px";
    document.body.appendChild(fakeClone);
    var returnValue = fakeRange.getBoundingClientRect(); //Get rect

    if (!debug) fakeClone.parentNode.removeChild(fakeClone); //Remove temp
    return returnValue;

    // Local functions for readability of the previous code
    function appendPart(start, end){
        var span = document.createElement("span");
        span.style.cssText = cssDefaultStyles; //Force styles to prevent unexpected results
        span.textContent = text.substring(start, end);
        fakeClone.appendChild(span);
        return span;
    }
    // Computing offset position
    function getInputOffset(){
        var body = document.body,
            win = document.defaultView,
            docElem = document.documentElement,
            box = document.createElement('div');
        box.style.paddingLeft = box.style.width = "1px";
        body.appendChild(box);
        var isBoxModel = box.offsetWidth == 2;
        body.removeChild(box);
        box = input.getBoundingClientRect();
        var clientTop  = docElem.clientTop  || body.clientTop  || 0,
            clientLeft = docElem.clientLeft || body.clientLeft || 0,
            scrollTop  = win.pageYOffset || isBoxModel && docElem.scrollTop  || body.scrollTop,
            scrollLeft = win.pageXOffset || isBoxModel && docElem.scrollLeft || body.scrollLeft;
        return {
            top : box.top  + scrollTop  - clientTop,
            left: box.left + scrollLeft - clientLeft};
    }
    function getInputCSS(prop, isnumber){
        var val = document.defaultView.getComputedStyle(input, null).getPropertyValue(prop);
        return isnumber ? parseFloat(val) : val;
    }
}

+1. 这是一个非常强大的解决方案,适合打包成插件甚至包含在分布式库中。但是,远远超出了我的需求。个人而言,我更喜欢在CSS中完成你正在做的大部分工作。这大大降低了复杂性。此外,我很乐意假设调用者会传递良好的参数,而不是进行所有验证。让调用者承受传入垃圾的后果。还有一个想法:与其将获取起始和结束位置的能力嵌入函数中,不如简化函数并调用两次以获取两个位置。 - gilly3
关于我所提到的简化方法,请参考我发布的答案 - 这就是我最终使用的方法。顺便说一句,你的代码在IE浏览器中无法运行。要修复它,你需要首先折叠你的范围,然后将结束位置移动到结束位置和开始位置之差。请查看这个更新:http://jsfiddle.net/gilly3/56Rep/4/ - gilly3
@Rob-w,获取滚动位置在不同浏览器中存在问题...我不知道如何在FF和Opera中实现它;同时,在换行符之前/之后的插入符存在问题。 - 4esn0k
非常好!我添加了一些代码来解决textarea元素换行和包装问题。这是gist。更改的行是40-41(文本换行)和81-87(换行问题 - 它给出位置有一段空格,但如果必要,可以稍后取消)。 - lakenen
1
我刚刚更新了非常轻量级且强大的textarea-caret-position组件库,以支持<input type="text">。演示请见http://jsfiddle.net/dandv/aFPA7/。 - Dan Dascalescu
显示剩余5条评论

5

2016更新:更现代的基于HTML5的解决方案是使用contenteditable属性。

<div contenteditable="true">  <!-- behaves as input -->
   Block of regular text, and <span id='interest'>text of interest</span>
</div>

我们现在可以使用jQuery的offset()方法来找到标签的位置。当然,标签可以事先插入或动态插入。

4
我最终创建了一个隐藏的模拟输入框,用一个绝对定位的span来实现,并将其样式设置为与输入框类似。我将该span的文本设置为输入框中要查找位置的字符之前的值。然后在输入框之前插入该span并获取其偏移量:
function getInputTextPosition(input, charOffset)
{
    var pixelPosition = null;
    if (input.createTextRange)
    {
        var range = input.createTextRange();
        range.moveStart("character", charOffset);
        pixelPosition = range.getBoundingClientRect();
    }
    else
    {
        var text = input.value.substr(0, charOffset).replace(/ $/, "\xa0");
        var sizer = $("#sizer").insertBefore(input).text(text);
        pixelPosition = sizer.offset();
        pixelPosition.left += sizer.width();
        if (!text) sizer.text("."); // for computing height. An empty span returns 0
        pixelPosition.bottom = pixelPosition.top + sizer.height();
    }
    return pixelPosition
}

我 sizer span 的 CSS:

#sizer
{
    position: absolute;
    display: inline-block;
    visibility: hidden;
    margin: 3px; /* simulate padding and border without affecting height and width */
    font-family: "segoe ui", Verdana, Arial, Sans-Serif;
    font-size: 12px;
}

1
我按照这个实现创建了一个测试用例:http://jsfiddle.net/WYzfm/。无论给定的起始偏移量如何,返回的偏移量始终相等。此外,这个函数需要定义一个元素 #sizer。如果你想继续开发这个函数,我建议使用 $('<div id="sizer"></div>'),这样它就不会依赖于嵌入的文档。 - Rob W
1
@RobW - 为什么在相同的输入中不同的字符偏移会导致 topbottom 发生变化?我只会期望 left 会变,而它确实变了:http://jsfiddle.net/gilly3/WYzfm/2/. - gilly3

3

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