Размеры и прокрутка элементов

Для того, чтобы показывать элементы на произвольных местах страницы, необходимо во-первых, знать 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 изображают пустыми, но так как текста много, то он заполняет нижнее поле padding-bottom в примере выше.

Во избежание путаницы заметим, что padding там, всё же, есть. Поля padding по CSS в элементе выше одинаковы со всех сторон. А такое заполнение – нормальное поведение браузера.

Метрики

У элементов существует ряд свойств, содержащих их внешние и внутренние размеры. Мы будем называть их «метриками».

Метрики, в отличие от свойств 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, которые в подробностях обсуждаются в главе Стили, getComputedStyle.

Получение ширины элемента может быть таким:

var elem = document.body;

alert( getComputedStyle(elem).width ); // вывести CSS-ширину для elem

Не лучше ли получать ширину так, вместо метрик? Вовсе нет!

  1. Во-первых, CSS-свойства width/height зависят от другого свойства – box-sizing, которое определяет, что такое, собственно, эти ширина и высота. Получается, что изменение box-sizing, к примеру, для более удобной вёрстки, сломает такой JavaScript.

  2. Во-вторых, в 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, в остальном всё так же. Конечно, по возможности, стоит использовать только режим соответствия стандарту.

Задачи

важность: 5

Свойство elem.scrollTop содержит размер прокрученной области при отсчете сверху. А как подсчитать размер прокрутки снизу?

Напишите соответствующее выражение для произвольного элемента elem.

Проверьте: если прокрутки нет вообще или элемент полностью прокручен – оно должно давать ноль.

Решение: elem.scrollHeight - elem.scrollTop - elem.clientHeight.

важность: 3

Напишите код, который возвращает ширину стандартной полосы прокрутки. Именно самой полосы, где ползунок. Обычно она равна 16px, в редких и мобильных браузерах может колебаться от 14px до 18px, а кое-где даже равна 0px.

P.S. Ваш код должен работать на любом HTML-документе, независимо от его содержимого.

Создадим элемент с прокруткой, но без border и padding. Тогда разница между его полной шириной offsetWidth и внутренней clientWidth будет равна как раз прокрутке:

// создадим элемент с прокруткой
var div = document.createElement('div');

div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';

// при display:none размеры нельзя узнать
// нужно, чтобы элемент был видим,
// visibility:hidden - можно, т.к. сохраняет геометрию
div.style.visibility = 'hidden';

document.body.appendChild(div);
var scrollWidth = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);

alert( scrollWidth );
важность: 3

Посмотрим следующий случай из жизни. Был текст, который, в частности, содержал div с зелеными границами:

<style>
  #moving-div {
    border: 5px groove green;
    padding: 5px;
    margin: 10px;
    background-color: yellow;
  }
</style>

Before Before Before

<div id="moving-div">
Text Text Text<br>
Text Text Text<br>
</div>

After After After

Программист Валера из вашей команды написал код, который позиционирует его абсолютно и смещает в правый верхний угол. Вот этот код:

var div = document.getElementById('moving-div');
div.style.position = 'absolute';
div.style.right = div.style.top = 0;

Побочным результатом явилось смещение текста, который раньше шел после DIV. Теперь он поднялся вверх:

Допишите код Валеры, сделав так, чтобы текст оставался на своем месте после того, как DIV будет смещен.

Сделайте это путем создания вспомогательного DIV с теми же width, height, border, margin, padding, что и у желтого DIV.

Используйте только JavaScript, без CSS.

Должно быть так (новому блоку задан фоновый цвет для демонстрации):

Открыть песочницу для задачи.

Нам нужно создать div с такими же размерами и вставить его на место «переезжающего».

Один из вариантов – это просто клонировать элемент.

Если делать это при помощи div.cloneNode(true), то склонируется все содержимое, которого может быть много. Обычно нам это не нужно, поэтому можно использовать div.cloneNode(false) для клонирования элемента со стилями, и потом поправить его width/height.

Можно и просто создать новый div и поставить ему нужные размеры.

Всё, кроме margin, можно получить из свойств DOM-элемента, а margin – только через getComputedStyle.

Причём margin мы обязаны поставить, так как иначе наш элемент при вставке будет вести себя иначе, чем исходный.

Код:

var div = document.getElementById('moving-div');

var placeHolder = document.createElement('div');
placeHolder.style.height = div.offsetHeight + 'px';
// можно и width, но в этом примере это не обязательно

// IE || другой браузер
var computedStyle = div.currentStyle || getComputedStyle(div, '');

placeHolder.style.marginTop = computedStyle.marginTop; // (1)
placeHolder.style.marginBottom = computedStyle.marginBottom;

В строке (1) использование полного название свойства "marginTop" гарантирует, что полученное значение будет корректным.

Открыть решение в песочнице.

важность: 5

Поместите мяч в центр поля.

Исходный документ выглядит так:

Используйте JavaScript, чтобы поместить мяч в центр:

  • Менять CSS нельзя, мяч должен переносить в центр ваш скрипт, через установку нужных стилей элемента.
  • JavaScript-код должен работать при разных размерах мяча (10, 20, 30 пикселей) без изменений.
  • JavaScript-код должен работать при различных размерах и местоположениях поля на странице без изменений. Также он не должен зависеть от ширины рамки поля border.

P.S. Да, центрирование можно сделать при помощи чистого CSS, но задача именно на JavaScript. Далее будут другие темы и более сложные ситуации, когда JavaScript будет уже точно необходим, это – своего рода «разминка».

Открыть песочницу для задачи.

При абсолютном позиционировании мяча внутри поля его координаты left/top отсчитываются от внутреннего угла поля, например верхнего-левого:

Метрики для внутренней зоны поля – это clientWidth/Height.

Центр – это (clientWidth/2, clientHeight/2).

Но если мы установим мячу такие значения ball.style.left/top, то в центре будет не сам мяч, а его левый верхний угол:

var ball = document.getElementById('ball');
var field = document.getElementById('field');

ball.style.left = Math.round(field.clientWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2) + 'px';

Для того, чтобы центр мяча находился в центре поля, нам нужно сместить мяч на половину его ширины влево и на половину его высоты вверх.

var ball = document.getElementById('ball');
var field = document.getElementById('field');

ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px';

Внимание, подводный камень!

Код выше стабильно работать не будет, потому что IMG идет без ширины/высоты:

<img src="ball.png" id="ball">

Высота и ширина изображения неизвестны браузеру до тех пор, пока оно не загрузится, если размер не указан явно.

После первой загрузки изображение уже будет в кеше браузера, и его размеры будут известны. Но когда браузер впервые видит документ – он ничего не знает о картинке, поэтому значение ball.offsetWidth равно 0. Вычислить координаты невозможно.

Чтобы это исправить, добавим width/height к картинке:

<img src="ball.png" width="40" height="40" id="ball">

Теперь браузер всегда знает ширину и высоту, так что все работает. Тот же эффект дало бы указание размеров в CSS.

Открыть решение в песочнице.

важность: 4

В <body> есть элемент <div> с заданной шириной width.

Задача – написать код, который «распахнет» <div> по ширине на всю страницу.

Исходный документ (<div> содержит текст и прокрутку):

P.S. Пользоваться следует исключительно средствами JS, CSS в этой задаче менять нельзя. Также ваш код должен быть универсален и не ломаться, если цифры в CSS станут другими.

P.P.S. При расширении элемент <div> не должен вылезти за границу <body>.

Открыть песочницу для задачи.

Решение через width: auto

Вначале рассмотрим решение через «умную» установку CSS-свойства.

Они могут быть разными. Самое простое выглядит так:

elem.style.width = 'auto';

Такой способ работает, так как <div> по умолчанию распахивается на всю ширину.

Конечно, такое решение не будет работать для элементов, которые сами по себе не растягиваются, например в случае со <span> или при наличии position: absolute.

Обратим внимание, такой вариант был бы неверен:

elem.style.width = '100%';

По умолчанию в CSS ширина width – это то, что внутри padding, а проценты отсчитываются от ширины родителя. То есть, ставя ширину в 100%, мы говорим: «внутренняя область должна занимать 100% ширины родителя». А в элементе есть ещё padding, которые в итоге вылезут наружу.

Можно бы поменять блочную модель, указав box-sizing через свойство elem.style.boxSizing, но такое изменение потенциально может затронуть много других свойств, поэтому нежелательно.

Точное вычисление

Альтернатива – вычислить ширину родителя через clientWidth.

Доступную внутреннюю ширину родителя можно получить, вычитая из clientWidth размеры paddingLeft/paddingRight, и затем присвоить её элементу:

var bodyClientWidth = document.body.clientWidth;

var style = getComputedStyle(elem);

var bodyInnerWidth = bodyClientWidth - parseInt(style.paddingLeft) - parseInt(style.paddingRight);

elem.style.width = bodyInnerWidth + 'px';

Такое решение будет работать всегда, вне зависимости от типа элемента. Конечно, при изменении размеров окна браузера ширина не адаптируется к новому размеру автоматически, как с width:auto. Это недостаток. Его, конечно, тоже можно обойти при помощи событий (изучим далее), но как общий рецепт – если CSS может решить задачу – лучше использовать CSS.

Открыть решение в песочнице.

важность: 5

В чём отличия между getComputedStyle(elem).width и elem.clientWidth?

Укажите хотя бы три отличия, лучше – больше.

Отличия:

  1. getComputedStyle не работает в IE8-.

  2. clientWidth возвращает число, а getComputedStyle(...).width – строку, на конце px.

  3. getComputedStyle не всегда даст ширину, он может вернуть, к примеру, "auto" для инлайнового элемента.

  4. clientWidth соответствует внутренней видимой области элемента, *включая padding, а CSS-ширина width при стандартном значении box-sizing соответствует зоне внутри padding.

  5. Если есть полоса прокрутки, то некоторые браузеры включают её ширину в width, а некоторые – нет.

    Свойство clientWidth, с другой стороны, полностью кросс-браузерно. Оно всегда обозначает размер за вычетом прокрутки, т.е. реально доступный для содержимого.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.