Для кросс-браузерного вычисления координат элемента давно используется суммирование offsetLeft/offsetTop. Список глюков этого подхода такой же длинный, как история его существования.
Эта статья - о том, как вычислять координаты не только кросс-браузерно, но и правильно. Да, и еще - быстро.
Логика этого подхода довольно проста.
Она заключается в том, что браузер позиционирует элементы относительно друг друга, и у каждого элемента есть свой "родитель по позиционированию": offsetParent.
В качестве offsetParent обычно выступает родитель parentNode. Но это не всегда так.
Например, для элемента с position='absolute' родителем по позиционированию является ближайший позиционированный родитель, то есть первый элемент в цепочке вложенности, у которого свойство position - одно из: absolute, relative или fixed(не поддерживается IE<7).
Наиболее подробно это описано в стандарте CSS: containing block details. Этот самый "containing block" - как раз и определяет offsetParent, от которого отсчитывается позиция элемента.
elem.offsetParentДвигаемся вверх по цепочке родителей elem, останавливаясь на следующих элементах, которые являются offsetParent:
<body>position - не static(значение по умолчанию)elem.position='static'У элемента <body> никогда нет offsetParent.
В IE 7+/Opera у элементов с elem.position='fixed' нет offsetParent.
Сдвиг относительно offsetParent'а задается свойствами offsetTop/offsetLeft:

Остается пройтись по всем offsetParent и просуммировать сдвиги. Последним offsetParent обычно является body:
function getOffsetSum(elem) {
var top=0, left=0
while(elem) {
top = top + parseFloat(elem.offsetTop)
left = left + parseFloat(elem.offsetLeft)
elem = elem.offsetParent
}
return {top: Math.round(top), left: Math.round(left)}
}
Основных проблем с этим кодом две.
offsetParent'ов.Вместо того, чтобы писать длинный кроссбраузерный код с разбором багов, который уж точно везде работает корректно, рассмотрим альтернативное решение, которое мало того что соответствует стандарту - его отлично поддерживают Internet Explorer 6+, Firefox 3+ и Opera 9.62+.
getBoundingClientRectЭтот малоизвестный метод по стандарту должен быть у каждого элемента DOM.
На момент написания статьи он реализован в SVN у Webkit. Для нас это значит, что Chrome и Safari с его поддержкой будут готовы уже скоро.
Он возвращает прямоугольник, ограничивающий элемент.
Важно, что координаты прямоугольника заданы относительно окна, а не документа, то есть не учитывают прокрутку страницы. Кроме того, координаты прямоугольника могут быть дробными в Firefox 3.
Координаты элемента на странице - это левый-верхний угол прямоугольника + прокрутка страницы.
Код обработчика onclick:
var br=this.getBoundingClientRect()
alert("Top:"+br.top+", Left:"+br.left+", Right:"+br.right+", Bottom:"+br.bottom)
getBoundingClientRect() ?По стандарту CSS любое содержимое находится внутри некоторого прямоугольника: css box.
В случае с блочными элементами, например DIV - этим прямоугольником яляется сам элемент. Такой прямоугольник называют block box.
Если элемент строчный, например, длинный текст - он уже может быть не такой простой формы, а требует для отображения нескольких прямоугольников anonymous box. Обо всем этом подробно об этом написано в стандарте: http://www.w3.org/TR/CSS21/visuren.html#anonymous-block-level".
Так что содержание элемента DOM может находится как в одном, так и в нескольких прямоугольниках css box.
Можно получить список всех прямоугольников, соответствующих elem, вызовом elem.getClientRects(). Здесь IE<8 может сделать свои прямоугольники, не соответствующие стандарту, но они для нас не играют роли, так как метод getClientRects() нам не нужен.
Метод elem.getBoundingClientRect() возвращает один (минимальный) прямоугольник, который включает в себя все прямоугольники getClientRects() с содержимым элемента.
На основе метода elem.getBoundingClientRect() мы можем сделать новый вариант функции:
function getOffsetRect(elem) {
// (1)
var box = elem.getBoundingClientRect()
// (2)
var body = document.body
var docElem = document.documentElement
// (3)
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft
// (4)
var clientTop = docElem.clientTop || body.clientTop || 0
var clientLeft = docElem.clientLeft || body.clientLeft || 0
// (5)
var top = box.top + scrollTop - clientTop
var left = box.left + scrollLeft - clientLeft
return { top: Math.round(top), left: Math.round(left) }
}
pageXOffset/pageYOffset, а в IE, при наличии DOCTYPE прокрутка вычисляется либо на documentElement(<html>), иначе на body - что есть то и беремhtml или body) бывает сдвинут относительно окна (IE). Получаем этот сдвиг.html/body, чтобы получить координаты относительно документаДля Firefox дополнительно мы округляем координаты вызовом Math.round().
На демо находятся 3 вложенных DIV'а. Все они с border, некоторые с position/margin/padding.
Клик на внутреннем отображает значения getOffsetSum/getOffsetRect, а также показывает координаты курсора на момент клика: event.pageX/pageY.
Координаты выводятся сразу под DIV'ами.
На момент написания статьи демо работает в IE6+,Firefox 3+ и Opera 9.62+.
Обратите внимание: результаты getOffsetSum(elem) и getOffsetRect(elem) не во всех браузерах совпадают.
Чтобы увидеть, какой вариант правильный - кликните на самой верхней-левой точке элемента (на самом верхнем-левом уголке черной рамки).
Тогда в pageX/pageY события появятся реальные значения угла элемента, и вы сможете их сравнить с getOffsetSum/getOffsetRect. Именно для такого сравнения в демо и добавлен вывод event.
Что делать, если метод getBoundingClientRect не поддерживается?
С одной стороны, мы могли бы рассмотреть корректную кросс-браузерную реализацию для FF2/Safari/Chrome/Konqueror. Она включает в себя много кода для обхода браузерных багов при подсчетах, которые нам совсем не интересны.
С другой - FF2 давно умер, а движок Safari/Chrome содержит поддержку getBoundingClientRect в SVN, и значит она скоро будет в релизе.
Думаю, и такие явные аутсайдеры как Konqueror подсуетятся, т.к движок Konqueror - по сути такой же, как и Safari.
Поэтому предлагаю использовать:
function getOffset(elem) {
if (elem.getBoundingClientRect) {
// "правильный" вариант
return getOffsetRect(elem)
} else {
// пусть работает хоть как-то
return getOffsetSum(elem)
}
}
Скачать полный финальный вариант.
Вы узнали, как вычислять координаты элемента на странице: быстро и точно в IE6+/FF3+/Opera 9.62+, и как-то чтоб работало - в Safari/Chrome/Konqueror.
Скоро и в Safari/Chrome/Konqueror будет поддержка правильного метода, поэтому аутсайдерами останутся лишь совсем редкие и багливые браузеры, и все будет работать ок
.