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

时间:2011-08-03 17:27:26

标签: javascript jquery firefox google-chrome

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

4 个答案:

答案 0 :(得分:16)

<强>演示
我写了一个行为符合预期的函数。可在此处找到非常详细的演示面板:小提琴:http://jsfiddle.net/56Rep/5/
演示中的界面不言自明。

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

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

功能逻辑
代码本身包含几条注释。这部分详细介绍。

  1. 创建了一个临时 <div>容器。
  2. 创建了1 - 3个<span>个元素。每个范围都包含输入值的一部分(偏移0到selectionStartselectionStartselectionEndselectionEnd到字符串结尾,只有第二个跨度是有意义的。“ / LI>
  3. 输入元素中的几个重要样式属性将复制到这些<div><span>标记。仅复制重要的样式属性。例如,color不会被复制,因为它不会以任何方式影响角色的偏移。#1
  4. <div>位于文本节点(输入值)的确切位置。考虑边框和填充,以确保临时<div> 正确定位。
  5. 创建一个变量,该变量包含 div.getBoundingClientRect() 的返回值。
  6. 除非参数<div>设置为true,否则将删除临时debug
  7. 该函数返回ClientRect个对象。有关此对象的更多信息,请参阅this page demo 还会显示一系列属性:topleftrightbottomheightwidth
  8. #1 getBoundingClientRect()(以及一些次要属性)用于确定输入元素的位置。然后,添加填充和边框宽度,以获得文本节点的实际位置。

    已知问题
    getComputedStylefont-family返回错误的值时遇到了唯一的不一致情况:当页面未定义font-family属性时,computedStyle返回不正确的值(即使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 :(得分:3)

2014年5月更新:令人难以置信的轻量级且功能强大的textarea-caret-position 组件库现在也支持<input type="text">,所有其他答案都已过时。< / p>

http://jsfiddle.net/dandv/aFPA7/

提供演示

感谢Rob W对RTL支持的启发。

答案 2 :(得分:2)

我最终创建了一个隐藏的模拟输入,该输入位于绝对位置并且与输入类似的样式。我将该范围的文本设置为输入的值,直到我想要找到的位置的字符。我在输入之前插入了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范围的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;
}

答案 3 :(得分:1)

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>标签可以预先插入或动态插入。