Для кросс-браузерного вычисления координат элемента давно используется суммирование 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(значение по умолчанию)
элементы table,th,td - если 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)}
}
Основных проблем с этим кодом две.
Он слегка глючит, в разных браузерах - по-разному. Есть проблемы с border'ами элементов, ошибки при прокрутке внутри элементов и некоторые другие.
Он медленный. Каждый раз приходится пройти всю цепочку offsetParent'ов.
Вместо того, чтобы писать длинный кроссбраузерный код с разбором багов, который уж точно везде работает корректно, рассмотрим альтернативное решение, которое мало того что соответствует стандарту - его отлично поддерживают Internet Explorer 6+, Firefox 3+ и Opera 9.62+.
Этот малоизвестный метод по стандарту должен быть у каждого элемента 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) }
}
Получить ограничивающий прямоугольник для элемента.
Задать две переменных для удобства
Вычислить прокрутку документа. Все браузеры, кроме IE, поддерживают 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 и getOffsetRect
getOffsetSum:значение getOffsetSum()
getOffsetRect:значение getOffsetRect()
event:координаты клика мышью относительно документа
Обратите внимание: результаты 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 будет поддержка правильного метода, поэтому аутсайдерами останутся лишь совсем редкие и багливые браузеры, и все будет работать ок .
Интересная статья. У меня ворос для разработчиков, которые смотрели в код популярных библиотек (jQuery, Mootools, Prototype, extJS) как там реализованно вычисление координат?
Так, как здесь описано, только хуже.
За исключением jQuery/dojo toolkit, где тоже реализовано так, как здесь описано, но в dojo вариант offsetParent лучше проработан для Firefox 2 и Safari/Chrome, а jQuery вообще использует parentNode и делает ряд дополнительных измерений для обнаружения багов браузера.
в MooTools 2.2.3, как на меня сама удачная реализация определения координаторов с поддержкой getBoundingClientRect и бордеров элементов. Позволю себе привести часть исходного кода:
getOffsets: function() {
if (this.getBoundingClientRect){
var bound = this.getBoundingClientRect(),
html = document.id(this.getDocument().documentElement),
scroll = html.getScroll(),
isFixed = (styleString(this, 'position') == 'fixed');
return {
x: parseInt(bound.left, 10) + ((isFixed) ? 0 : scroll.x) - html.clientLeft,
y: parseInt(bound.top, 10) + ((isFixed) ? 0 : scroll.y) - html.clientTop
};
}
var element = this, position = {x: 0, y: 0};
if (isBody(this)) return position;
while (element && !isBody(element)){
position.x += element.offsetLeft;
position.y += element.offsetTop;
if (Browser.Engine.gecko){
if (!borderBox(element)){
position.x += leftBorder(element);
position.y += topBorder(element);
}
var parent = element.parentNode;
if (parent && styleString(parent, 'overflow') != 'visible'){
position.x += leftBorder(parent);
position.y += topBorder(parent);
}
} else if (element != this && Browser.Engine.webkit){
position.x += leftBorder(element);
position.y += topBorder(element);
}
element = element.offsetParent;
}
if (Browser.Engine.gecko && !borderBox(this)){
position.x -= leftBorder(this);
position.y -= topBorder(this);
}
return position;
}
function styleNumber(element, style){
return styleString(element, style).toInt() || 0;
};
function borderBox(element){
return styleString(element, '-moz-box-sizing') == 'border-box';
};
function topBorder(element){
return styleNumber(element, 'border-top-width');
};
function leftBorder(element){
return styleNumber(element, 'border-left-width');
};
function isBody(element){
return (/^(?:body|html)$/i).test(element.tagName);
};
У меня пока не работает как надо getBoundingClientRect(), так как у элементов (body и div) выставлено position:relative без смещений и есть margin у внутреннего div относительно body. Правильно отображает только Хром (Сафари 4 не проверял), остальные браузеры не учитывают margin (IE, Opera, FF), приходится его вручную дописывать к координатам.
В своё время много перечитал документации по этому вопросу. Но делалось всё под ужасно бажные IE5.5-6, старую Оперу < 9.1 и Фаерфокс. Наилучшим решение вроде казалось тогда применение getBoundingClientRect(). Но баг с тем, что эксплорер и опера под одни и те же числовые значения рисовали элементы не много, но различающиеся по размеру, иной раз сводил усилия по оформлению страницы к оценке "пойдёт". На тот момент было ужасно интересно заглянуть в код эксплорера и оперы. Кто-нибудь знает как там вычисляется размер элементов и как он соотносится с замерами окон?
Спасибо за отличную статью. Помогла мне очень, реализовал для эффекта PhotoLine, что бы определить место положение самой ленты, для прокрутки ее роликом мыши.
Точно так же реализован offset в jQuery. Но недавно столкнулся с неожиданным багом - есть у меня
с заданной шириной, всередине его есть спаны и в случаи перехода спана на новую строку - в 1 случае getBoundingClientRect дает некорректный координаты - он видит слово на предыдущей строке, а оно уже в следующей находится.
Вот рабочая функция. Может кому-то пригодиться. Статья понравилась. И это мой к ней комментарий. Чтобы уж совсем не был пустым коммент, выложил сию функцию.
function ElemCoords(obj)
{
var curleft = 0;
var curtop = 0;
if (obj.offsetParent)
{
while (1)
{
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
if (!obj.offsetParent)
break;
obj=obj.offsetParent;
}
}
else if (obj.x || obj.y)
{
curleft += obj.x;
curtop += obj.y;
}
return {"x":curleft, "y":curtop};
}
Получить координаты элемента можно вызовом функции, типа "ElemCoords(object).x".
function left (elem)
{
var left=0;
alert(elem.offsetLeft)
while(elem)
{
left += parseFloat(elem.offsetLeft);
elem= elem.offsetParent;
}
return Math.round(left);
}
когда я писал этот (Автор: Гость (не зарегистрирован), дата: 29 ноября, 2011 - 12:56) пример, я имел ввиду что, левый верхний угол body необязательно начинается с верхнего левого угла вьюпорта.
Посмотрите на пример и протестите его на всех IE скроля и не скроля страницу.
Сделать метод, умеющий нормально определять координаты на экране для всех браузеров, включая IE6+, включая режим совместимости, можно.
Для этого нужно всего лишь учесть поправку смещения начала координат относительно вьюпорта:
var div = document.createElement('div');
div.style.position = 'absolute';
div.style.left = '0px';
div.style.top = '0px';
div.style.display = 'block';
document.body.appendChild(div);
var horizontalFix = div.getBoundingClientRect().left;
var verticalFix = div.getBoundingClientRect().top;
что делать, если имеется пустой div или br на странице, как вычислить его координаты, getBoundingClientRect возвращает прямоугольник с нулевыми координатами?
Подскажите, пожалуйста. А можно решить такую задачу - имеется определенное слово, необходимо проверить есть ли оно на экране, если есть - выдать его координаты.
Добрый день. Ситуация следующая. Есть множество елементов на странице, создаются они юезром, ограничения на создание по количеству нет. Можно удалять старые и создавать новые. Их можно ресайзить и перетаскивать. Все они имеют братски-сестринские связи. Как мне сравнить координаты одного елемента, выбранного юзером, с другими, для запрета пересечения элементов друг-другом?
В статье не учтено, что родитель элемента (или любой вверх по цепочке) может иметь position: sticky, и вся схема просто рухнет, хоть с offsetParent, хоть с getBoundingClientRect. При нулевой прокрутке всё сработает верно, а потом.. Впрочем предлагаю просмотреть пример с демонстрацией проблемы: jsfiddle.net/rafaylik/sf5Lcrjp
Как можно поступить, если родитель имеет position: sticky:
Интересная статья. У меня ворос для разработчиков, которые смотрели в код популярных библиотек (jQuery, Mootools, Prototype, extJS) как там реализованно вычисление координат?
Так, как здесь описано, только хуже.
За исключением jQuery/dojo toolkit, где тоже реализовано так, как здесь описано, но в dojo вариант offsetParent лучше проработан для Firefox 2 и Safari/Chrome, а jQuery вообще использует parentNode и делает ряд дополнительных измерений для обнаружения багов браузера.
Давно искал урок как найти абсолютную позицию, "Комбинированный вариант" подходит отлично.
Хорошая статья. Добавлю только что стоит проверять body что он есть, те body&&body.clientTop
Статья супер!МОЛОДЕЦ!
В Opere 9.20 не работает.Видимо в версии 9.62 уже поддерживается.
Большое спасибо за ёмкую статью по существу. А то копать по всем стандартам довольно затруднительно %^>
в MooTools 2.2.3, как на меня сама удачная реализация определения координаторов с поддержкой getBoundingClientRect и бордеров элементов. Позволю себе привести часть исходного кода:
В JQuery глюки с clientY функции offset() и подобных в Опере и Хроме вот один из тикетов:
http://dev.jquery.com/ticket/4583
Очен помогло, спасибо!
У меня пока не работает как надо getBoundingClientRect(), так как у элементов (body и div) выставлено position:relative без смещений и есть margin у внутреннего div относительно body. Правильно отображает только Хром (Сафари 4 не проверял), остальные браузеры не учитывают margin (IE, Opera, FF), приходится его вручную дописывать к координатам.
В своё время много перечитал документации по этому вопросу. Но делалось всё под ужасно бажные IE5.5-6, старую Оперу < 9.1 и Фаерфокс. Наилучшим решение вроде казалось тогда применение getBoundingClientRect(). Но баг с тем, что эксплорер и опера под одни и те же числовые значения рисовали элементы не много, но различающиеся по размеру, иной раз сводил усилия по оформлению страницы к оценке "пойдёт". На тот момент было ужасно интересно заглянуть в код эксплорера и оперы. Кто-нибудь знает как там вычисляется размер элементов и как он соотносится с замерами окон?
Спасибо за отличную статью. Помогла мне очень, реализовал для эффекта PhotoLine, что бы определить место положение самой ленты, для прокрутки ее роликом мыши.
Спасибо. Очень помогла.
Просто хочу сказать Большое Спасибо!
Много-много здоровья и долгих лет жизни автору!
Это то, что я искал!
Точно так же реализован offset в jQuery. Но недавно столкнулся с неожиданным багом - есть у меня
с заданной шириной, всередине его есть спаны и в случаи перехода спана на новую строку - в 1 случае getBoundingClientRect дает некорректный координаты - он видит слово на предыдущей строке, а оно уже в следующей находится.
Браузер safari, платформа ipod.
Молоток! Биг сенкс фор ю
очень полезная статья
С использованием JQuery
$("#block")
.click(function(e){
X = e.pageX-$(this).offset().left;
Y = e.pageY-$(this).offset().top;
})
Вот рабочая функция. Может кому-то пригодиться. Статья понравилась. И это мой к ней комментарий. Чтобы уж совсем не был пустым коммент, выложил сию функцию.
function ElemCoords(obj)
{
var curleft = 0;
var curtop = 0;
if (obj.offsetParent)
{
while (1)
{
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
if (!obj.offsetParent)
break;
obj=obj.offsetParent;
}
}
else if (obj.x || obj.y)
{
curleft += obj.x;
curtop += obj.y;
}
return {"x":curleft, "y":curtop};
}
Получить координаты элемента можно вызовом функции, типа "ElemCoords(object).x".
Отличный код. так лень собирать было самому, просто взял этот. Работает. Спс )
автору искренняя благодарность
На лето 2011 getBoundingClientRect поддерживается всеми актуальными браузерами.
определите пожалуйста координаты элемента/клика на такой странице:
окошко браузера должно быть открыть шире чем на max-width
Превосходно! Браво! Прекрасная статья. Помогла однозначно разрулить проблемы с обработкой ситуаций сложного позиционирования (fixed->absolute->static->absolute) вложенных элементов. Достойный материал.
как насчет такого варианта
Гениально !!!
когда я писал этот (Автор: Гость (не зарегистрирован), дата: 29 ноября, 2011 - 12:56) пример, я имел ввиду что, левый верхний угол body необязательно начинается с верхнего левого угла вьюпорта.
Посмотрите на пример и протестите его на всех IE скроля и не скроля страницу.
Сделать метод, умеющий нормально определять координаты на экране для всех браузеров, включая IE6+, включая режим совместимости, можно.
Для этого нужно всего лишь учесть поправку смещения начала координат относительно вьюпорта:
У меня IE 7. Функция getBoundingClientRect не учитывает свойство таблиц cellSpacing при подсчете.
Супер!
Спасибо, отличная статья - помогла!
что делать, если имеется пустой div или br на странице, как вычислить его координаты, getBoundingClientRect возвращает прямоугольник с нулевыми координатами?
Подскажите, пожалуйста. А можно решить такую задачу - имеется определенное слово, необходимо проверить есть ли оно на экране, если есть - выдать его координаты.
Адреса страниц
Добрый день. Ситуация следующая. Есть множество елементов на странице, создаются они юезром, ограничения на создание по количеству нет. Можно удалять старые и создавать новые. Их можно ресайзить и перетаскивать. Все они имеют братски-сестринские связи. Как мне сравнить координаты одного елемента, выбранного юзером, с другими, для запрета пересечения элементов друг-другом?
В статье не учтено, что родитель элемента (или любой вверх по цепочке) может иметь position: sticky, и вся схема просто рухнет, хоть с offsetParent, хоть с getBoundingClientRect. При нулевой прокрутке всё сработает верно, а потом.. Впрочем предлагаю просмотреть пример с демонстрацией проблемы:
jsfiddle.net/rafaylik/sf5Lcrjp
Как можно поступить, если родитель имеет position: sticky:
И рабочий пример (спасибо Malleys за совет):
jsfiddle.net/Lk74do8u