Get cursor or text position in pixels for input element
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 the cursor/caret. Is there any way of getting the position of a certain character in pixels in other browsers?
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?
}
I'm using jQuery, but I doubt it will be able to address my situation. I expect a pure JavaScript solution, if any, but jQuery answers are welcome.
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.
- One temporary
<div>
container is created. - 1 - 3
<span>
elements are created. Each span holds a part of the input's value (offsets 0 toselectionStart
,selectionStart
toselectionEnd
,selectionEnd
to end of string, only the second span is meaninngful). - 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 - 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. - A variable is created, which holds the return value of
div.getBoundingClientRect()
. - The temporary
<div>
is removed, unless parameterdebug
is set to true. - 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
andwidth
.
#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;
}
}
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;
}
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.
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.
精彩评论