12 сентября 2024 г.

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

Существует множество JavaScript-свойств, которые позволяют считывать информацию об элементе: ширину, высоту и другие геометрические характеристики. В этой главе мы будем называть их «метрики».

Они часто требуются, когда нам нужно передвигать или позиционировать элементы с помощью JavaScript.

Простой пример

В качестве простого примера демонстрации свойств мы будем использовать следующий элемент:

<div id="example">
  ...Текст...
</div>
<style>
  #example {
    width: 300px;
    height: 200px;
    border: 25px solid #E8C48F;
    padding: 20px;
    overflow: auto;
  }
</style>

У элемента есть рамка (border), внутренний отступ (padding) и прокрутка. Полный набор характеристик. Обратите внимание, тут нет внешних отступов (margin), потому что они не являются частью элемента, для них нет особых JavaScript-свойств.

Результат выглядит так:

Вы можете открыть этот пример в песочнице.

Внимание, полоса прокрутки

В иллюстрации выше намеренно продемонстрирован самый сложный и полный случай, когда у элемента есть ещё и полоса прокрутки. Некоторые браузеры (не все) отбирают место для неё, забирая его у области, отведённой для содержимого (помечена как «content width» выше).

Таким образом, без учёта полосы прокрутки ширина области содержимого (content width) будет 300px, но если предположить, что ширина полосы прокрутки равна 16px (её точное значение зависит от устройства и браузера), тогда остаётся только 300 - 16 = 284px, и мы должны это учитывать. Вот почему примеры в этой главе даны с полосой прокрутки. Без неё некоторые вычисления будут проще.

Область padding-bottom (нижний внутренний отступ) может быть заполнена текстом

Нижние внутренние отступы padding-bottom изображены пустыми на наших иллюстрациях, но если элемент содержит много текста, то он будет перекрывать padding-bottom, это нормально.

Метрики

Вот общая картина с геометрическими свойствами:

Значениями свойств являются числа, подразумевается, что они в пикселях.

Давайте начнём исследовать, начиная снаружи элемента.

offsetParent, offsetLeft/Top

Эти свойства редко используются, но так как они являются «самыми внешними» метриками, мы начнём с них.

В свойстве offsetParent находится предок элемента, который используется внутри браузера для вычисления координат при рендеринге.

То есть, ближайший предок, который удовлетворяет следующим условиям:

  1. Является CSS-позиционированным (CSS-свойство position равно absolute, relative, fixed или sticky),
  2. или <td>, <th>, <table>,
  3. или <body>.

Свойства offsetLeft/offsetTop содержат координаты x/y относительно верхнего левого угла offsetParent.

В примере ниже внутренний <div> имеет элемент <main> в качестве offsetParent, а свойства offsetLeft/offsetTop являются сдвигами относительно верхнего левого угла (180):

<main style="position: relative" id="main">
  <article>
    <div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
  </article>
</main>
<script>
  alert(example.offsetParent.id); // main
  alert(example.offsetLeft); // 180 (обратите внимание: число, а не строка "180px")
  alert(example.offsetTop); // 180
</script>

Существует несколько ситуаций, когда offsetParent равно null:

  1. Для скрытых элементов (с CSS-свойством display:none или когда его нет в документе).
  2. Для элементов <body> и <html>.
  3. Для элементов с position:fixed.

offsetWidth/Height

Теперь переходим к самому элементу.

Эти два свойства – самые простые. Они содержат «внешнюю» ширину/высоту элемента, то есть его полный размер, включая рамки.

Для нашего элемента:

  • offsetWidth = 390 – внешняя ширина блока, её можно получить сложением CSS-ширины (300px), внутренних отступов (2 * 20px) и рамок (2 * 25px).
  • offsetHeight = 290 – внешняя высота блока.
Метрики для не показываемых элементов равны нулю.

Координаты и размеры в JavaScript устанавливаются только для видимых элементов.

Если элемент (или любой его родитель) имеет display:none или отсутствует в документе, то все его метрики равны нулю (или null, если это offsetParent).

Например, свойство offsetParent равно null, а offsetWidth и offsetHeight равны 0, когда мы создали элемент, но ещё не вставили его в документ, или если у элемента (или у его родителя) display:none.

Мы можем использовать это, чтобы делать проверку на видимость:

function isHidden(elem) {
  return !elem.offsetWidth && !elem.offsetHeight;
}

Заметим, что функция isHidden также вернёт true для элементов, которые в принципе показываются, но их размеры равны нулю.

clientTop/Left

Пойдём дальше. Внутри элемента у нас рамки (border).

Для них есть свойства-метрики clientTop и clientLeft.

В нашем примере:

  • clientLeft = 25 – ширина левой рамки
  • clientTop = 25 – ширина верхней рамки

…Но на самом деле эти свойства – вовсе не ширины рамок, а отступы внутренней части элемента от внешней.

В чём же разница?

Она возникает, когда документ располагается справа налево (операционная система на арабском языке или иврите). Полоса прокрутки в этом случае находится слева, и тогда свойство clientLeft включает в себя ещё и ширину полосы прокрутки.

В этом случае clientLeft будет равно 25, но с прокруткой – 25 + 16 = 41.

Вот соответствующий пример на иврите:

clientWidth/Height

Эти свойства – размер области внутри рамок элемента.

Они включают в себя ширину области содержимого вместе с внутренними отступами padding, но без прокрутки:

На рисунке выше посмотрим вначале на высоту clientHeight.

Горизонтальной прокрутки нет, так что это в точности то, что внутри рамок: CSS-высота 200px плюс верхние и нижние внутренние отступы (2 * 20px), итого 240px.

Теперь clientWidth – ширина содержимого здесь равна не 300px, а 284px, т.к. 16px отведено для полосы прокрутки. Таким образом: 284px плюс левый и правый отступы – всего 324px.

Если нет внутренних отступов padding, то clientWidth/Height в точности равны размеру области содержимого внутри рамок за вычетом полосы прокрутки (если она есть).

Поэтому в тех случаях, когда мы точно знаем, что отступов нет, можно использовать clientWidth/clientHeight для получения размеров внутренней области содержимого.

scrollWidth/Height

Эти свойства – как clientWidth/clientHeight, но также включают в себя прокрученную (которую не видно) часть элемента.

На рисунке выше:

  • scrollHeight = 723 – полная внутренняя высота, включая прокрученную область.
  • scrollWidth = 324 – полная внутренняя ширина, в данном случае прокрутки нет, поэтому она равна clientWidth.

Эти свойства можно использовать, чтобы «распахнуть» элемент на всю ширину/высоту.

Таким кодом:

// распахнуть элемент на всю высоту
element.style.height = `${element.scrollHeight}px`;

Нажмите на кнопку, чтобы распахнуть элемент:

текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст текст

scrollLeft/scrollTop

Свойства scrollLeft/scrollTop – ширина/высота невидимой, прокрученной в данный момент, части элемента слева и сверху.

Следующая иллюстрация показывает значения scrollHeight и scrollTop для блока с вертикальной прокруткой.

Другими словами, свойство scrollTop – это «сколько уже прокручено вверх».

Свойства scrollLeft/scrollTop можно изменять

В отличие от большинства свойств, которые доступны только для чтения, значения scrollLeft/scrollTop можно изменять, и браузер выполнит прокрутку элемента.

При клике на следующий элемент будет выполняться код elem.scrollTop += 10. Поэтому он будет прокручиваться на 10px вниз.

Кликни
Меня
1
2
3
4
5
6
7
8
9

Установка значения scrollTop на 0 или на большое значение, такое как 1e9, прокрутит элемент в самый верх/низ соответственно.

Не стоит брать width/height из CSS

Мы рассмотрели метрики, которые есть у DOM-элементов, и которые можно использовать для получения различных высот, ширин и прочих расстояний.

Но как мы знаем из главы Стили и классы, CSS-высоту и ширину можно извлечь, используя getComputedStyle.

Так почему бы не получать, к примеру, ширину элемента при помощи getComputedStyle, вот так?

let elem = document.body;

alert( getComputedStyle(elem).width ); // показывает CSS-ширину elem

Почему мы должны использовать свойства-метрики вместо этого? На то есть две причины:

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

  2. Во-вторых, CSS свойства width/height могут быть равны auto, например, для инлайнового элемента:

    <span id="elem">Привет!</span>
    
    <script>
      alert( getComputedStyle(elem).width ); // auto
    </script>

    Конечно, с точки зрения CSS width:auto – совершенно нормально, но нам-то в JavaScript нужен конкретный размер в px, который мы могли бы использовать для вычислений. Получается, что в данном случае ширина из CSS вообще бесполезна.

Есть и ещё одна причина: полоса прокрутки. Бывает, без полосы прокрутки код работает прекрасно, но стоит ей появиться, как начинают проявляться баги. Так происходит потому, что полоса прокрутки «отъедает» место от области внутреннего содержимого в некоторых браузерах. Таким образом, реальная ширина содержимого меньше CSS-ширины. Как раз это и учитывают свойства clientWidth/clientHeight.

…Но с getComputedStyle(elem).width ситуация иная. Некоторые браузеры (например, Chrome) возвращают реальную внутреннюю ширину с вычетом ширины полосы прокрутки, а некоторые (например, Firefox) – именно CSS-свойство (игнорируя полосу прокрутки). Эти кроссбраузерные отличия – ещё один повод не использовать getComputedStyle, а использовать свойства-метрики.

Если ваш браузер показывает полосу прокрутки (например, под Windows почти все браузеры так делают), то вы можете протестировать это сами, нажав на кнопку в ифрейме ниже.

У элемента с текстом в стилях указано CSS-свойство width:300px.

На ОС Windows браузеры Firefox, Chrome и Edge резервируют место для полосы прокрутки. Но Firefox отображает 300px, в то время как Chrome и Edge – меньше. Это из-за того, что Firefox возвращает именно CSS-ширину, а остальные браузеры – «реальную» ширину за вычетом прокрутки.

Обратите внимание: описанные различия касаются только чтения свойства getComputedStyle(...).width из JavaScript, визуальное отображение корректно в обоих случаях.

Итого

У элементов есть следующие геометрические свойства (метрики):

  • offsetParent – ближайший CSS-позиционированный родитель или ближайший td, th, table, body.
  • offsetLeft/offsetTop – позиция в пикселях верхнего левого угла относительно offsetParent.
  • offsetWidth/offsetHeight – «внешняя» ширина/высота элемента, включая рамки.
  • clientLeft/clientTop – расстояние от верхнего левого внешнего угла до внутренного. Для операционных систем с ориентацией слева-направо эти свойства равны ширинам левой/верхней рамки. Если язык ОС таков, что ориентация справа налево, так что вертикальная полоса прокрутки находится не справа, а слева, то clientLeft включает в своё значение её ширину.
  • clientWidth/clientHeight – ширина/высота содержимого вместе с внутренними отступами padding, но без полосы прокрутки.
  • scrollWidth/scrollHeight – ширина/высота содержимого, аналогично clientWidth/Height, но учитывают прокрученную, невидимую область элемента.
  • scrollLeft/scrollTop – ширина/высота прокрученной сверху части элемента, считается от верхнего левого угла.

Все свойства доступны только для чтения, кроме scrollLeft/scrollTop, изменение которых заставляет браузер прокручивать элемент.

Задачи

важность: 5

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

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

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

Решение:

let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight;

Другими словами: (вся высота) минус (часть, прокрученная сверху) минус (видимая часть) – результат в точности соответствует размеру прокрутки снизу.

важность: 3

Напишите код, который возвращает ширину стандартной полосы прокрутки.

Для Windows она обычно колеблется от 12px до 20px. Если браузер не выделяет место под полосу прокрутки (так тоже бывает, она может быть прозрачной над текстом), тогда значение может быть 0px.

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

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

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

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

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

// мы должны вставить элемент в документ, иначе размеры будут равны 0
document.body.append(div);
let scrollWidth = div.offsetWidth - div.clientWidth;

div.remove();

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

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

Каковы координаты центра поля?

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

  • Элемент должен позиционироваться за счёт JavaScript, а не CSS.
  • Код должен работать с любым размером мяча (10, 20, 30 пикселей) и любым размером поля без привязки к исходным значениям.

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

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

Мяч имеет CSS-свойство position:absolute. Это означает, что координаты left/top измеряются относительно ближайшего спозиционированного элемента, которым является #field (т.к. у него есть CSS-свойство position:relative).

Координаты отсчитываются от внутреннего верхнего левого угла поля:

Ширина и высота внутреннего поля – это clientWidth/clientHeight. Таким образом, его центр имеет координаты (clientWidth/2, clientHeight/2).

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

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

Вот как это выглядит:

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

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">

Если браузеру неизвестны ширина и высота изображения (из атрибута HTML-тега или CSS-свойств), он считает их равными 0 до тех пор, пока изображение не загрузится.

При первой загрузке браузер обычно кеширует изображения, так что при последующей загрузке оно будет доступно тут же, вместе с размерами. Но при первой загрузке значение ширины мяча ball.offsetWidth равно 0. Это приводит к вычислению неверных координат.

Мы можем исправить это, добавив атрибуты width/height тегу <img>:

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

…Или задав размеры в CSS:

#ball {
  width: 40px;
  height: 40px;
}

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

важность: 5

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

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

Отличия:

  1. clientWidth возвращает число, а getComputedStyle(elem).width – строку с px на конце.
  2. getComputedStyle не всегда даст ширину, он может вернуть, к примеру, "auto" для строчного элемента.
  3. clientWidth соответствует внутренней области элемента, включая внутренние отступы padding, а CSS-ширина (при стандартном значении box-sizing) соответствует внутренней области без внутренних отступов padding.
  4. Если есть полоса прокрутки, и для неё зарезервировано место, то некоторые браузеры вычитают его из CSS-ширины (т.к. оно больше недоступно для содержимого), а некоторые – нет. Свойство clientWidth всегда ведёт себя одинаково: оно всегда обозначает размер за вычетом прокрутки, т.е. реально доступный для содержимого.
Карта учебника