Для того, чтобы показывать элементы на произвольных местах страницы, необходимо во-первых, знать CSS-позиционирование, а во-вторых – уметь работать с «геометрией элементов» из JavaScript.
В этой главе мы поговорим о размерах элементов DOM, способах их вычисления и метриках – различных свойствах, которые содержат эту информацию.
Образец документа
Мы будем использовать для примера вот такой элемент, у которого есть рамка (border), поля (padding), и прокрутка:
<div id="example">
...Текст...
</div>
<style>
#example {
width: 300px;
height: 200px;
border: 25px solid #E8C48F; /* рамка 25px */
padding: 20px; /* поля 20px */
overflow: auto; /* прокрутка */
}
</style>
У него нет отступов margin, в этой главе они не важны, так как метрики касаются именно размеров самого элемента, отступы в них не учитываются.
Результат выглядит так:
Вы можете открыть этот документ в песочнице.
В иллюстрации выше намеренно продемонстрирован самый сложный и полный случай, когда у элемента есть ещё и полоса прокрутки.
В этом случае полоса прокрутки «отодвигает» содержимое вместе с padding влево, отбирая у него место.
Именно поэтому ширина содержимого обозначена как content width и равна 284px, а не 300px, как в CSS.
Точное значение получено в предположении, что ширина полосы прокрутки равна 16px, то есть после её вычитания на содержимое остаётся 300 - 16 = 284px. Конечно, она сильно зависит от браузера, устройства, ОС.
Мы должны в точности понимать, что происходит с размерами элемента при наличии полосы прокрутки, поэтому на картинке выше это отражено.
padding могут быть заполнены текстомНа рисунке выше поля padding изображены пустыми, но текст там вполне может быть, к примеру, при наличии вертикальной прокрутки.
Метрики
У элементов существует ряд свойств, содержащих их внешние и внутренние размеры. Мы будем называть их «метриками».
Метрики, в отличие от свойств CSS, содержат числа, всегда в пикселях и без единиц измерения на конце.
Вот общая картина:
На картинке все они с трудом помещаются, но, как мы увидим далее, их значения просты и понятны.
Будем исследовать их снаружи элемента и вовнутрь.
offsetParent, offsetLeft/Top
Ситуации, когда эти свойства нужны, можно перечислить по пальцам. Они возникают действительно редко. Как правило, эти свойства используют, потому что не знают средств правильной работы с координатами, о которых мы поговорим позже.
Несмотря на то, что эти свойства нужны реже всего, они – самые «внешние», поэтому начнём с них.
В offsetParent находится ссылка на родительский элемент в смысле отображения на странице.
Уточним, что имеется в виду.
Когда браузер рисует страницу, то он высчитывает дерево расположения элементов, иначе говоря «дерево геометрии» или «дерево рендеринга», которое содержит всю информацию о размерах.
При этом одни элементы естественным образом рисуются внутри других. Но, к примеру, если у элемента стоит position:absolute, то его расположение вычисляется уже не относительно непосредственного родителя parentNode, а относительно ближайшего позиционированного элемента (т.е. свойство position которого не равно static), или BODY, если такой отсутствует.
Получается, что элемент имеет в дополнение к обычному родителю в DOM – ещё одного «родителя по позиционированию», то есть относительно которого он рисуется. Этот элемент и будет в свойстве offsetParent.
Свойства offsetLeft/Top задают смещение относительно offsetParent.
В примере ниже внутренний <div> имеет DOM-родителя <form>, но offsetParent у него <main>, и сдвиги относительно его верхнего-левого угла будут в offsetLeft/Top:
<main style="position: relative">
<form>
<div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
</form>
</main>
offsetWidth/Height
Теперь переходим к самому элементу.
Эти два свойства – самые простые. Они содержат «внешнюю» ширину/высоту элемента, то есть его полный размер, включая рамки border.
Для нашего элемента:
offsetWidth = 390– внешняя ширина блока, её можно получить сложением CSS-ширины (300px, но её часть на рисунке выше отнимает прокрутка, поэтому284 + 16), полей(2*20px) и рамок (2*25px).offsetHeight = 290– внешняя высота блока.
Координаты и размеры в JavaScript устанавливаются только для видимых элементов.
Для элементов с display:none или находящихся вне документа дерево рендеринга не строится. Для них метрики равны нулю. Кстати, и offsetParent для таких элементов тоже null.
Это даёт нам замечательный способ для проверки, виден ли элемент:
function isHidden(elem) {
return !elem.offsetWidth && !elem.offsetHeight;
}
- Работает, даже если родителю элемента установлено свойство
display:none. - Работает для всех элементов, кроме
TR, с которым возникают некоторые проблемы в разных браузерах. Обычно, проверяются неTR, поэтому всё ок. - Считает элемент видимым, даже если позиционирован за пределами экрана или имеет свойство
visibility:hidden. - «Схлопнутый» элемент, например пустой
divбез высоты и ширины, будет считаться невидимым.
clientTop/Left
Далее внутри элемента у нас рамки border.
Для них есть свойства-метрики clientTop и clientLeft.
В нашем примере:
clientLeft = 25– ширина левой рамкиclientTop = 25– ширина верхней рамки
…Но на самом деле они – вовсе не рамки, а отступ внутренней части элемента от внешней.
В чём же разница?
Она возникает тогда, когда документ располагается справа налево (операционная система на арабском языке или иврите). Полоса прокрутки в этом случае находится слева, и тогда свойство clientLeft включает в себя ещё и ширину полосы прокрутки.
Получится так:
clientWidth/Height
Эти свойства – размер элемента внутри рамок border.
Они включают в себя ширину содержимого width вместе с полями padding, но без прокрутки.
На рисунке выше посмотрим вначале на clientHeight, её посчитать проще всего. Прокрутки нет, так что это в точности то, что внутри рамок: CSS-высота 200px плюс верхнее и нижнее поля padding (по 20px), итого 240px.
На рисунке нижний padding заполнен текстом, но это неважно: по правилам он всегда входит в clientHeight.
Теперь clientWidth – ширина содержимого здесь не равна CSS-ширине, её часть «съедает» полоса прокрутки.
Поэтому в clientWidth входит не CSS-ширина, а реальная ширина содержимого 284px плюс левое и правое поля padding (по 20px), итого 324px.
Если padding нет, то clientWidth/Height в точности равны размеру области содержимого, внутри рамок и полосы прокрутки.
Поэтому в тех случаях, когда мы точно знаем, что padding нет, их используют для определения внутренних размеров элемента.
scrollWidth/Height
Эти свойства – аналоги clientWidth/clientHeight, но с учётом прокрутки.
Свойства clientWidth/clientHeight относятся только к видимой области элемента, а scrollWidth/scrollHeight добавляют к ней прокрученную (которую не видно) по горизонтали/вертикали.
На рисунке выше:
scrollHeight = 723– полная внутренняя высота, включая прокрученную область.scrollWidth = 324– полная внутренняя ширина, в данном случае прокрутки нет, поэтому она равнаclientWidth.
Эти свойства можно использовать, чтобы «распахнуть» элемент на всю ширину/высоту, таким кодом:
element.style.height = element.scrollHeight + 'px';
Нажмите на кнопку, чтобы распахнуть элемент:
scrollLeft/scrollTop
Свойства scrollLeft/scrollTop – ширина/высота невидимой, прокрученной в данный момент, части элемента слева и сверху.
Следующая иллюстрация показывает значения scrollHeight и scrollTop для блока с вертикальной прокруткой.
scrollLeft/scrollTop можно изменятьВ отличие от большинства свойств, которые доступны только для чтения, значения scrollLeft/scrollTop можно изменить, и браузер выполнит прокрутку элемента.
При клике на следующий элемент будет выполняться код elem.scrollTop += 10. Поэтому он будет прокручиваться на 10px вниз:
Меня
1
2
3
4
5
6
7
8
9
Не стоит брать width/height из CSS
Мы рассмотрели метрики – свойства, которые есть у DOM-элементов. Их обычно используют для получения их различных высот, ширин и прочих расстояний.
Теперь несколько слов о том, как не надо делать.
Как мы знаем, CSS-высоту и ширину можно установить с помощью elem.style и извлечь, используя getComputedStyle, которые в подробностях обсуждаются в главе Стили и классы.
Получение ширины элемента может быть таким:
var elem = document.body;
alert( getComputedStyle(elem).width ); // вывести CSS-ширину для elem
Не лучше ли получать ширину так, вместо метрик? Вовсе нет!
-
Во-первых, CSS-свойства
width/heightзависят от другого свойства –box-sizing, которое определяет, что такое, собственно, эти ширина и высота. Получается, что изменениеbox-sizing, к примеру, для более удобной вёрстки, сломает такой JavaScript. -
Во-вторых, в CSS свойства
width/heightмогут быть равныauto, например, для инлайн-элемента:<span id="elem">Привет!</span> <script> alert( getComputedStyle(elem).width ); // auto </script>Конечно, с точки зрения CSS размер
auto– совершенно нормально, но нам-то в JavaScript нужен конкретный размер в пикселях, который мы могли бы использовать для вычислений. Получается, что в данном случае ширинаwidthиз CSS вообще бесполезна.
Есть и ещё одна причина.
Полоса прокрутки – причина многих проблем и недопониманий. Как говорится, «дьявол кроется в деталях». Недопустимо, чтобы наш код работал на элементах без прокрутки и начинал «глючить» с ней.
Как мы говорили ранее, при наличии вертикальной полосы прокрутки, в зависимости от браузера, устройства и операционной системы, она может сдвинуть содержимое.
Получается, что реальная ширина содержимого меньше CSS-ширины. И это учитывают свойства clientWidth/clientHeight.
…Но при этом некоторые браузеры также учитывают это в результате getComputedStyle(elem).width, то есть возвращают реальную внутреннюю ширину, а некоторые – именно CSS-свойство. Эти кросс-браузерные отличия – ещё один повод не использовать такой подход, а использовать свойства-метрики.
Если ваш браузер показывает полосу прокрутки (например, под Windows почти все браузеры так делают), то вы можете протестировать это сами, нажав на кнопку в ифрейме ниже.
У элемента с текстом в стилях указано width:300px.
На момент написания этой главы при тестировании в Chrome под Windows alert выводил 283px, а в Firefox – 300px. При этом оба браузера показывали прокрутку. Это из-за того, что Firefox возвращал именно CSS-ширину, а Chrome – реальную ширину, за вычетом прокрутки.
Описанные разночтения касаются только чтения свойства getComputedStyle(...).width из JavaScript, визуальное отображение корректно в обоих случаях.
Итого
У элементов есть следующие метрики:
offsetParent– «родитель по дереву рендеринга» – ближайшая ячейка таблицы, body для статического позиционирования или ближайший позиционированный элемент для других типов позиционирования.offsetLeft/offsetTop– позиция в пикселях левого верхнего угла блока, относительно егоoffsetParent.offsetWidth/offsetHeight– «внешняя» ширина/высота блока, включая рамки.clientLeft/clientTop– отступ области содержимого от левого-верхнего угла элемента. Если операционная система располагает вертикальную прокрутку справа, то равны ширинам левой/верхней рамки, если же слева (ОС на иврите, арабском), тоclientLeftвключает в себя прокрутку.clientWidth/clientHeight– ширина/высота содержимого вместе с полямиpadding, но без полосы прокрутки.scrollWidth/scrollHeight– ширина/высота содержимого, включая прокручиваемую область. Включает в себяpaddingи не включает полосы прокрутки.scrollLeft/scrollTop– ширина/высота прокрученной части документа, считается от верхнего левого угла.
Все свойства, доступны только для чтения, кроме scrollLeft/scrollTop. Изменение этих свойств заставляет браузер прокручивать элемент.
В этой главе мы считали, что страница находится в режиме соответствия стандартам. В режиме совместимости – некоторые старые браузеры требуют document.body вместо documentElement, в остальном всё так же. Конечно, по возможности, стоит использовать только режим соответствия стандарту.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)