Get cursor or text position in pixels for input element

后端 未结 4 1429
礼貌的吻别
礼貌的吻别 2020-12-01 11:06

IE allows me to create a text range in an input element, upon which I can call getBoundingClientRect() and get the position in pixels of a certain character or

相关标签:
4条回答
  • 2020-12-01 11:28

    2016 update: A more modern HTML5 based solution would be to use the contenteditable property.

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

    We can now find the position of the span using jquery offset(). And of course, the <span> tags can be inserted upfront or dynamically.

    0 讨论(0)
  • 2020-12-01 11:29

    May 2014 update: The incredibly lightweight and robust textarea-caret-position Component library now supports <input type="text"> as well, rendering all other answers obsolete.

    A demo is available at http://jsfiddle.net/dandv/aFPA7/

    Thanks to Rob W for inspiration towards RTL support.

    0 讨论(0)
  • 2020-12-01 11:34

    Demo
    I have written a function which behaves as expected. A very detailed demonstration panel can be found here: Fiddle: http://jsfiddle.net/56Rep/5/
    The interface in the demo is self-explanatory.

    The functionality as requested in the question would be implemented in my function as follows:
        var pixelPosition = getTextBoundingRect(input, 6)

    Function dependencies
    Updated: The function is pure JavaScript, and not dependent on any plugin or framework!
    The function assumes that the getBoundingClientRect method exist. Text ranges are used when they're supported. Otherwise, the functionality is achieved using my function logic.

    Function logic
    The code itself contains several comments. This part goes in a deeper detail.

    1. One temporary <div> container is created.
    2. 1 - 3 <span> elements are created. Each span holds a part of the input's value (offsets 0 to selectionStart, selectionStart to selectionEnd, selectionEnd to end of string, only the second span is meaninngful).
    3. Several significant style properties from the input element are copied to these <div> and <span> tags. Only significant style properties are copied. For example, color is not copied, because it does not affect the offsets of a character in any way.#1
    4. The <div> is positioned at the exact position of the text node (input's value). Borders and paddings are taken into account, to make sure that the temporary <div> is correctly positioned.
    5. A variable is created, which holds the return value of div.getBoundingClientRect().
    6. The temporary <div> is removed, unless parameter debug is set to true.
    7. The function returns the ClientRect object. For more information about this object, see this page. The demo also shows a list of properties: top, left, right, bottom, height and width.

    #1: getBoundingClientRect() (and some minor properties) is used to determine the position of the input element. Then, the padding and border width are added, to get the real position of a text node.

    Known issues
    The only case of an inconsistency was encountered when getComputedStyle returned a wrong value for font-family: When a page hasn't defined a font-family property, the computedStyle returns an incorrect value (even Firebug is experiencing this issue; environment: Linux, Firefox 3.6.23, font "Sans Serif").

    As visible in the demo, the positioning is sometimes slightly off (almost zero, always smaller than 1 pixel).

    Technical restrictions prevents the script from getting the exact offset of a text fragment when the contents has been moved, e.g. when the first visible character in an input field does not equal the first value's character.

    Code

    // @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;
        }
    }
    
    0 讨论(0)
  • 2020-12-01 11:43

    I ended up creating a hidden mock input out of a span positioned absolutely and styled similarly to the input. I set the text of that span to the value of the input up to the character whose position I want to find. I insert the span before the input and get it's offset:

    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
    }
    

    The css for my sizer span:

    #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;
    }
    
    0 讨论(0)
提交回复
热议问题