14 серпня 2023 р.

Розмір і прокрутка елемента

Є багато властивостей JavaScript, які дозволяють нам читати інформацію про ширину, висоту елемента та інші геометричні характеристики.

Ми часто потребуємо їх під час переміщення або позиціювання елементів в JavaScript.

Приклад елемента

Як приклад елемента для демонстрації властивостей ми використаємо наведений нижче:

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

Він має рамки, відступи та прокручування. Повний набір функцій. Тут немає зовнішніх відступів (margins), оскільки вони не є частиною самого елемента, і для них немає спеціальних властивостей.

Елемент виглядає ось так:

Ви можете відкрити документ у пісочниці.

Зверніть увагу на смугу прокрутки

На малюнку вище показаний найскладніший випадок, коли елемент має смугу прокрутки. Деякі браузери (не всі) резервують місце для нього, беручи його з вмісту (позначеного вище як “content width”).

Таким чином, без смуги прокрутки ширина вмісту становила б 300px, але якщо ширина смуги прокрутки 16px (ширина може відрізнятися залежно від пристрою та браузера), то залишається лише 300 - 16 = 284px, і ми повинні це враховувати. Ось чому приклади з цього розділу надані зі смугою прокрутки. Без неї деякі розрахунки будуть простіші.

Область 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. Для елементів, що не відображаються (display:none або коли його немає у документі).
  2. Для елементів <body> та <html>.
  3. Для елементів з position:fixed.

offsetWidth/Height

Тепер перейдемо до самого елемента.

Ці дві властивості є найпростішими. Вони забезпечують “зовнішню” ширину/висоту елемента. Або, іншими словами, його повний розмір, включаючи рамки(border).

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

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

Геометричні властивості, такі як координати та розміри, обчислюються лише для відображених елементів.

Якщо елемент (або будь-який із його предків) має display:none або його немає в документі, тоді всі геометричні властивості дорівнюють нулю (або null для offsetParent).

Наприклад, offsetParent дорівнює null, а offsetWidth, offsetHeight дорівнюють 0, якщо ми створили елемент, але ще не вставили його в документ, або він (або його предок) має display:none.

Ми можемо використати це, щоб перевірити, чи приховано елемент, наприклад:

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

Зауважимо, що функція isHidden також поверне true для елементів, які в принципі показуються, але їх розміри дорівнюють нулю (наприклад, порожні <div>).

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 змусить елемент прокрутитися до самого верху/низу відповідно.

Не варто брати ширину/висоту з 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 – ширина/висота вмісту, включаючи відступи, але без смуги прокрутки.
  • scrollWidth/scrollHeight – ширина/висота вмісту, як і clientWidth/clientHeight, але також включає прокручену невидиму частину елемента.
  • 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 буде саме ширина смуги прокрутки:

// створюємо div з прокруткою
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. Тут ми робимо “розминку”.

Відкрити пісочницю для завдання.

М’яч має position:absolute. Це означає, що його left/top координати вимірюються від найближчого розташованого елемента(батька), тобто #field (тому що він має 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';

Тепер м’яч нарешті відцентрований.

Attention: the pitfall!

Код не працюватиме надійно, поки <img> не має ширини/висоти:

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

Коли браузер не знає ширини/висоти зображення (з атрибутів тегів або CSS), він вважає, що вони дорівнюють 0 поки не закінчиться завантаження зображення.

Отже, значення ball.offsetWidth буде 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 це внутрішня область вмісту елемента плюс відступи, тоді як ширина CSS (зі стандартним box-sizing) це внутрішня область вмісту без відступів.
  4. Якщо є смуга прокрутки і браузер резервує для неї простір, деякі браузери віднімають цей простір із ширини CSS (тому що він більше не доступний для вмісту), а деякі ні. Властивість clientWidth завжди однакова: розмір смуги прокрутки віднімається при її наявності.
Навчальна карта