Далее привожу код реализации для решения своей задачи. Она немного отличается от сформулированной в самом начале топика. У меня есть поле редактирования в котором строится формула с определенным синтаксисом, при вводе в это поле каких-либо символов появляется список лексем разрешенных в этом месте (наподобии IntelliSense в Microsoft Visual Studio или в своем роде Google Suggest), при щелчке по одной из выведенных лексем введенные символы заменяются на эту лексему и курсор позиционируется после этой лексемы, а сама лексема выделяется на пол секунды, что бы привлечь внимание пользователя (Выделение реализовано наложением полупрозрачного div'а поверх поля редактирования на ту область, которую занимает лексема).
Соответственно отличия от исходной задачи состоят в том, что мне
- требуется только горизонтальная координата, т.к. поле однострочное (элемент input)
- кроме положения курсора необходимо перевести длину лексемы в символах поверх которой будет накладываться div в ширину этого div'а в пикселях
Все вычисления делаются в одном методе. Комментарии на английском, но думаю будут ясны.
// Returns as object {xCoordinate, lengthInPixels} x-coordinate (with document.body acts as offset paret) of the cursor in an input element
// (single line text field) and the length in pixels the area that ends on the character after which the cursor is positioned
function GetCursorXCoordinateAndLength(elementOrItsId, lengthInCharacters)
{
var element = elementOrItsId;
if (typeof(elementOrItsId) == "string")
{
element = document.getElementById(elementOrItsId);
}
if (!/input/i.test(element.nodeName))
{
throw "Method GetCursorXCoordinate supports only INPUT elements.";
}
// IE
if (document.selection)
{
var r = document.selection.createRange();
r.collapse(false);
r.moveStart("character", -lengthInCharacters);
// A trick to make method GetOffsetLeft work for IE selection range object
r.offsetParent = r.parentElement();
var offsetLeft = GetOffsetLeft(r, document.body);
var result = { xCoordinate: offsetLeft + r.boundingWidth, lengthInPixels: r.boundingWidth};
r.collapse(false);
return result;
}
// Fire Fox (not tested for other Gecko browsers so may not work for them)
else if (typeof(element.setSelectionRange) != 'undefined')
{
// Create text area with the same content and set cursor to the same position to get scrollLeft attribute
// we have to do it to properly handle the case when value don't fit the input so user can
// scroll it so we need find out what scrollLeft is but it is always = 0 for input element
var textarea = document.createElement("textarea");
textarea.style.visibility = "hidden";
document.body.appendChild(textarea);
textarea.setAttribute("wrap", "off");
textarea.style.width = element.offsetWidth + "px";
textarea.style.overflow = "auto";
var styles = document.defaultView.getComputedStyle(element, "");
var s;
for (var i = 0; i < styles.length; i++)
{
s = styles[i];
if (s.indexOf("border") > -1 ||
s.indexOf("padding") > -1 ||
s.indexOf("font") > -1)
{
setStyle(textarea, s, getStyle(element, s));
}
}
textarea.style.position = "absolute";
textarea.style.zIndex = -1;
textarea.style.left = GetOffsetLeft(element, document.body) + "px";
textarea.style.top = GetOffsetTop(element, document.body) + "px";
var textNode = document.createTextNode(element.value);
textarea.appendChild(textNode);
textarea.setSelectionRange(element.selectionStart, element.selectionStart);
// Here is Fire Fox hack to scroll cursor into view (enter space and then remove it)
// thus this method will return proper x coordinate only if the cursor in the element
// was positioned with the same hack. It means that it may work wrong sometimes if cursor
// was positioned by user by hand.
var e = document.createEvent("KeyboardEvent");
e.initKeyEvent("keypress", true, true, null, false, false, false, false, 0, 32);
textarea.dispatchEvent(e);
e = document.createEvent("KeyboardEvent");
e.initKeyEvent("keypress", true, true, null, false, false, false, false, 8, 0);
textarea.dispatchEvent(e);
var scrollLeft = textarea.scrollLeft;
document.body.removeChild(textarea);
// Now create DIV with the same content and set cursor to the same position
// to get position of the cursor in pixels
var div = document.createElement("div");
div.style.visibility = "hidden";
document.body.appendChild(div);
styles = document.defaultView.getComputedStyle(element, "");
for (i = 0; i < styles.length; i++)
{
s = styles[i];
if (s.indexOf("border") > -1 ||
s.indexOf("padding") > -1 ||
s.indexOf("font") > -1)
{
setStyle(div, s, getStyle(element, s));
}
}
div.style.overflow = "visible";
div.style.position = "absolute";
div.style.zIndex = -1;
div.style.left = GetOffsetLeft(element, document.body) + "px";
div.style.top = GetOffsetTop(element, document.body) + "px";
textNode = document.createTextNode(element.value);
div.appendChild(textNode);
var r = document.createRange();
var elementRight = document.createElement("span");
r.setStart(textNode, element.selectionStart);
r.setEnd(textNode, element.selectionStart);
r.surroundContents(elementRight);
var xCoordinate = GetOffsetLeft(elementRight, document.body) - scrollLeft;
// Calculate length in pixels
var startPosition = element.selectionStart - lengthInCharacters;
if (startPosition < 0)
{
startPosition = 0;
}
var elementLeft = document.createElement("span");
r.setStart(textNode, startPosition);
r.setEnd(textNode, startPosition);
r.surroundContents(elementLeft);
var lengthInPixels = elementRight.offsetLeft - elementLeft.offsetLeft;
document.body.removeChild(div);
return {xCoordinate: xCoordinate, lengthInPixels: lengthInPixels};
}
return null;
}
// Cross-browser method of calculation offsetLeft value for specified offsetParent
// (it returns the number of pixels from the left edge of element supplied as 'offsetParent' parameter
// till the left edge of the element supplied as 'element' parameter)
function GetOffsetLeft(element, offsetParent)
{
var possiblyOffsetParent = offsetParent;
var result = 0;
do
{
result += element.offsetLeft;
element = element.offsetParent;
}
while (element && element != possiblyOffsetParent);
// If offsetParent is not one of real offset parents of the element calculate offset left considering the
// document as mutual parent for both elements - so the result will be the differentce of offsetLeft values
if (!element || !offsetParent)
{
var parentOffsetLeft = 0;
while (offsetParent && offsetParent.offsetParent)
{
parentOffsetLeft += offsetParent.offsetLeft;
offsetParent = offsetParent.offsetParent;
}
return result - parentOffsetLeft;
}
return result;
}
// Cross-browser method of calculation offsetTop value for specified offsetParent
// (it returns the number of pixels from the top of element supplied as 'offsetParent' parameter
// till the top of the element supplied as 'element' parameter)
function GetOffsetTop(element, offsetParent)
{
var possiblyOffsetParent = offsetParent;
var result = 0;
do
{
result += element.offsetTop;
element = element.offsetParent;
}
while (element && element != possiblyOffsetParent);
// If offsetParent is not one of real offset parents of the element calculate offset top considering the
// document as mutual parent for both elements - so the result will be the differentce of offsetTop values
if (!element || !offsetParent)
{
var parentOffsetTop = 0;
while (offsetParent && offsetParent.offsetParent)
{
parentOffsetTop += offsetParent.offsetTop;
offsetParent = offsetParent.offsetParent;
}
return result - parentOffsetTop;
}
return result;
}
Некоторые пояснения по коду:
1) Метод поддерживает только элемент input, поэтому в нем не реализована логика обработки поля textarea, реализованная в vsk_frm_cursor_offset
2) Метод возвращает смещение по оси x относительно тела документа а не самого поля редактирования
3) GetOffsetTop, GetOffsetLeft - вспомогательные методы