演示
我写了一个表现符合预期的函数。您可以在此找到非常详细的演示面板:Fiddle: http://jsfiddle.net/56Rep/5/
演示中的界面是自解释的。
按照问题所要求的功能将被实现在我的函数中,如下所示:
var pixelPosition = getTextBoundingRect(input, 6)
函数依赖
更新: 该函数是纯JavaScript编写的,不依赖于任何插件或框架!
该函数假定getBoundingClientRect
方法存在。当支持时使用文本范围,否则使用我的函数逻辑来实现功能。
函数逻辑
代码本身包含多个注释。这部分内容更加详细。
- 创建一个临时
<div>
容器。
- 创建1-3个
<span>
元素,每个包含输入值的一部分(偏移量为0到selectionStart
、selectionStart
到selectionEnd
、selectionEnd
到字符串末尾,只有第二个是有意义的)。
- 从输入元素中复制几个重要的样式属性到这些
<div>
和<span>
标签中。只复制重要的样式属性。例如,color
不会被复制,因为它不以任何方式影响字符的偏移量。#1
<div>
在文本节点(输入值)的确切位置上定位。边框和填充被考虑在内,以确保临时<div>
被正确地定位。
- 创建一个变量,它保存
div.getBoundingClientRect()
的返回值。
- 如果参数
debug
设置为true,则删除临时<div>
。
- 函数返回
ClientRect
对象。有关此对象的更多信息,请参见此页面。 演示还显示了属性列表:top
、left
、right
、bottom
、height
和width
。
#1: 使用getBoundingClientRect()
(以及一些次要属性)来确定输入元素的位置。然后,添加填充和边框宽度,以获取文本节点的实际位置。
已知问题
唯一一种不一致的情况是在getComputedStyle
返回错误的font-family
值时遇到的:当页面没有定义font-family
属性时,计算样式返回一个不正确的值(即使Firebug也遇到了这个问题;环境:Linux,Firefox 3.6.23,字体“Sans Serif”)。
如演示中所示,定位有时会略微偏移(几乎为零,始终小于1像素)。
技术限制防止脚本在内容被移动时获取文本片段的精确偏移量,例如,当输入字段中的第一个可见字符不等于第一个值的字符时。
代码
function getTextBoundingRect(input, selectionStart, selectionEnd, debug) {
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 (typeof input.createTextRange == "function") {
var range = input.createTextRange();
range.collapse(true);
range.moveStart('character', selectionStart);
range.moveEnd('character', selectionEnd - selectionStart);
return range.getBoundingClientRect();
}
var offset = getInputOffset(),
topPos = offset.top,
leftPos = offset.left,
width = getInputCSS('width', true),
height = getInputCSS('height', true);
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;
for (var i=0; i<listOfModifiers.length; i++) {
var property = listOfModifiers[i];
cssDefaultStyles += property + ':' + getInputCSS(property) +';';
}
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);
fakeClone.style.cssText = cssDefaultStyles;
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();
if (!debug) fakeClone.parentNode.removeChild(fakeClone);
return returnValue;
function appendPart(start, end){
var span = document.createElement("span");
span.style.cssText = cssDefaultStyles;
span.textContent = text.substring(start, end);
fakeClone.appendChild(span);
return span;
}
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;
}
}