获取输入元素的光标或文本位置(以像素为单位)

Posted

技术标签:

【中文标题】获取输入元素的光标或文本位置(以像素为单位)【英文标题】:Get cursor or text position in pixels for input element 【发布时间】:2011-10-19 07:45:56 【问题描述】:

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 答案。

【问题讨论】:

可能与您要查找的内容有关:***.com/questions/4085312/… 我很确定简短的回答是“不”,但我现在没有时间详细说明或研究。 @TimDown 我已经实现了请求的行为。既然你在范围方面有很多经验(由于你的 Rangy 项目),你能检查一下吗? @RobW:好的,我今天晚些时候再看看。 @RobW:与范围相关的代码非常少:更多​​的是关于定位和样式,我对此有所了解,但并不是我的专业领域。这看起来很合理,但我会在不同的浏览器和操作系统中对其进行艰苦的测试:我认为某些浏览器和操作系统中的文本输入可能有不同数量的不可移动且可能无法测量的填充,这可能会导致问题发生。可能还有其他与精确模拟输入样式有关的问题。 【参考方案1】:

演示 我编写了一个按预期运行的函数。可以在这里找到一个非常详细的演示面板:Fiddle:http://jsfiddle.net/56Rep/5/ 演示中的界面一目了然。

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

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

函数逻辑代码本身包含几个cmets。这部分更详细。

    创建了一个临时 <div> 容器。 1 - 3 <span> 元素被创建。每个 span 包含输入值的一部分(偏移 0 到 selectionStartselectionStartselectionEndselectionEnd 到字符串结尾,只有第二个 span 有意义)。 输入元素中的几个重要样式属性被复制到这些<div><span> 标记中。仅复制重要的样式属性。例如,color 不会被复制,因为它不会以任何方式影响字符的偏移量。#1 <div> 位于文本节点(输入值)的确切位置。考虑到边框和填充,以确保临时 <div> 的位置正确。 创建了一个变量,它保存了div.getBoundingClientRect()的返回值。 临时的<div>被移除,除非参数debug被设置为true。 该函数返回ClientRect 对象。有关此对象的更多信息,请参阅this page。 demo 还显示属性列表:topleftrightbottomheightwidth

#1getBoundingClientRect()(和一些次要属性)用于确定输入元素的位置。然后加上padding和border width,得到一个文本节点的真实位置。

已知问题getComputedStylefont-family返回错误值时遇到了唯一不一致的情况:当页面未定义font-family属性时,computedStyle返回错误值(即使Firebug也遇到此问题;环境: Linux,Firefox 3.6.23,字体“无衬线”)。

从演示中可以看出,定位有时会稍微偏离(几乎为零,始终小于 1 像素)。

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

代码

// @author Rob W       http://***.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 为了完整起见,我已经包含了结束偏移量。左边偏移可以通过.left得到,右边偏移可以通过.left + .width计算。出于性能原因,最好调用该函数并使用.left + .width,而不是调用该函数两次。支持selectionStartselectionEnd 没什么大不了的。 关于 CSS:CSS 主要用于复制影响偏移的属性。如果我的Function Logic 部分有什么不清楚的地方,请指出,以便我改进。 对于我所指的简化,请参阅我发布的答案 - 这就是我最终使用的。顺便说一句,您的代码在 IE 中不起作用。要修复它,您需要先折叠您的范围,然后将 end 移动 end - start 的差值。查看此更新:jsfiddle.net/gilly3/56Rep/4 whatabout 我刚刚更新了令人难以置信的轻量级和健壮的textarea-caret-position Component 库,以支持&lt;input type="text"&gt;。演示jsfiddle.net/dandv/aFPA7【参考方案2】:

我最终在绝对定位且样式与输入类似的跨度中创建了一个隐藏的模拟输入。我将该范围的文本设置为输入的值,直到我要查找其位置的字符。我在输入之前插入跨度并得到它的偏移量:

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;

【讨论】:

我在这个实现之后创建了一个测试用例:jsfiddle.net/WYzfm。无论给定的起始偏移量如何,返回的偏移量总是相等的。此外,此函数需要定义一个元素 #sizer。如果你想继续开发这个功能,我推荐使用$('&lt;div id="sizer"&gt;&lt;/div&gt;'),这样它就不会依赖于它嵌入的文档。 @RobW - 为什么topbottom 会因同一输入中的不同字符偏移而不同?我希望只有left 会改变,它确实会改变:jsfiddle.net/gilly3/WYzfm/2。【参考方案3】:

2014 年 5 月更新:令人难以置信的轻量级和强大的 textarea-caret-positionComponent 库现在也支持 &lt;input type="text"&gt;,从而使所有其他答案都过时了。

可通过http://jsfiddle.net/dandv/aFPA7/获得演示

感谢 Rob W 为 RTL 支持提供灵感。

【讨论】:

【参考方案4】:

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

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

我们现在可以使用 jquery offset() 找到 span 的位置。当然,&lt;span&gt; 标签可以预先或动态插入。

【讨论】:

以上是关于获取输入元素的光标或文本位置(以像素为单位)的主要内容,如果未能解决你的问题,请参考以下文章

获取文本输入字段中的光标位置(以字符为单位)

获取文本输入字段中的光标位置(以字符为单位)

获取光标书写位置

如何获取可编辑div或body里光标的像素位置?

JavaScript:通过鼠标单击获取输入文本位置(以字符为单位)

以像素为单位在textarea中查找插入符号位置[重复]