16 липня 2023 р.

Вступ до подій браузера

Подія – це сигнал від браузера, що щось сталося. Всі DOM-вузли подають такі сигнали (хоча події бувають не тільки в DOM).

Ось список найпоширеніших DOM-подій, поки що просто для ознайомлення:

Події миші:

  • click – відбувається, коли клацнули на елемент лівою кнопкою миші (на пристроях із сенсорними екранами воно відбувається при торканні).
  • contextmenu – відбувається, коли клацнули на елемент правою кнопкою миші.
  • mouseover / mouseout – коли миша наводиться на / залишає елемент.
  • mousedown / mouseup – коли натиснули / відпустили кнопку миші на елементі.
  • mousemove – під час руху миші.

Події клавіатури:

  • keydown та keyup – коли користувач натискає / відпускає клавішу.

Події елементів форми:

  • submit – користувач надіслав форму <form>.
  • focus – користувач фокусується на елементі, наприклад, натискає на <input>.

Події документа:

  • DOMContentLoaded – коли HTML завантажено й оброблено, DOM документа повністю побудований і доступний.

CSS події:

  • transitionend – коли CSS-анімацію завершено.

Існує багато інших подій. Ми докладно розберемо їх у наступних розділах.

Обробники подій

Події можна призначити обробник, тобто функцію, яка спрацює, щойно подія сталася.

Саме завдяки обробникам JavaScript код може реагувати на дії користувача.

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

Використання атрибута HTML

Обробник може бути призначений прямо в розмітці, атрибуті, який називається on<event>.

Наприклад, щоб призначити обробник події click на елементі input, можна використовувати атрибут onclick, ось так:

<input value="Натисни мене" onclick="alert('Клік!')" type="button">

При натисканні мишкою на кнопці виконається код, вказаний в атрибуті onclick.

Зверніть увагу, що для вмісту атрибуту onclick використовуються одинарні лапки, а сам атрибут знаходиться в подвійних. Якщо ми забудемо про це і поставимо подвійні лапки всередині атрибуту, ось так: onclick="alert("Click!")", код не буде працювати.

Атрибут HTML-тега – не найзручніше місце для написання великої кількості коду, тому краще створити окрему JavaScript-функцію та викликати її там.

Наступний приклад по кліку запускає функцію countRabbits():

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Кролик номер " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Рахувати кроликів!">

Як ми пам’ятаємо, атрибут HTML-тега не чутливий до регістру, тому ONCLICK буде працювати так само як onClick і onCLICK… Але, як правило, атрибути пишуть у нижньому регістрі onclick.

Використання властивостей DOM-об’єкта

Можемо призначати обробник, використовуючи властивість DOM-елемента on<event>.

Наприклад, elem.onclick:

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Дякую');
  };
</script>

Якщо обробник заданий через атрибут, то браузер читає HTML-розмітку, створює нову функцію із вмісту атрибута та записує у властивість.

Цей спосіб, по суті, аналогічний до попереднього.

Ці два приклади коду працюють однаково:

  1. Тільки HTML:

    <input type="button" onclick="alert('Клік!')" value="Кнопка">
  2. HTML + JS:

    <input type="button" id="button" value="Кнопка">
    <script>
      button.onclick = function() {
        alert('Клік!');
      };
    </script>

У першому прикладі використовувався атрибут HTML для ініціалізації button.onclick, тоді як у другому прикладі – сценарій, ось і вся різниця.

Оскільки в елемента DOM може бути тільки одна властивість з ім’ям onclick, то призначити більше одного обробника таким чином не можна.

У прикладі нижче призначення обробника додатково через JavaScript перезапише обробник з атрибуту:

<input type="button" id="elem" onclick="alert('Було')" value="Click me">
<script>
  elem.onclick = function() { // Перезапише існуючий обробник
    alert('Стало'); // виведеться лише це повідомлення
  };
</script>

Щоб видалити обробник – установіть elem.onclick = null.

Доступ до елемента через this

Усередині обробника події this посилається на поточний елемент, тобто на той, на якому, як кажуть, «висить» (тобто призначений) обробник.

У коді нижче button виводить свій вміст, використовуючи this.innerHTML:

<button onclick="alert(this.innerHTML)">Натисни на мене</button>

Поширені помилки

Якщо ви починаєте працювати з подіями, зверніть увагу на наступні моменти.

Ви можете призначити функцію що вже визначена, як обробника:

function sayThanks() {
  alert('Thanks!');
}

elem.onclick = sayThanks;

Будьте обережні! Функція повинна буди присвоєна в такому вигляді – sayThanks, а не sayThanks().

// правильно
button.onclick = sayThanks;

// неправильно
button.onclick = sayThanks();

Якщо додати дужки, то sayThanks() – це вже виклик функції, результат якого (рівний undefined, тому що функція нічого не повертає) буде присвоєно onclick. Тож це не буде працювати.

…А ось у розмітці, на відміну від властивості, дужки потрібні:

<input type="button" id="button" onclick="sayThanks()">

Цю різницю просто пояснити. При створенні обробника браузером з атрибута він автоматично створює функцію з тілом зі значення атрибута:

Таким чином розмітка генерує таку властивість:

button.onclick = function() {
  sayThanks(); // <-- вміст атрибуту
};

Не використовуйте setAttribute для обробників.

Такий виклик не буде працювати:

// При натисканні на <body> виникнуть помилки,
// атрибути завжди рядки, і функція стане рядком
document.body.setAttribute('onclick', function() { alert(1) });

Регістр DOM-властивості має значення…

Використовуйте elem.onclick, а не elem.ONCLICK, тому що DOM-властивості чутливі до регістру.

addEventListener

Фундаментальний недолік описаних вище способів присвоєння обробника – неможливість повісити кілька обробників для однієї події.

Наприклад, одна частина коду хоче при натисканні на кнопку підсвітити її, а інша – показати повідомлення.

Ми хочемо призначити два обробники для цього. Але новий обробник перезапише попередній:

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // замінить попередній обробник

Розробники стандартів досить давно це зрозуміли і запропонували альтернативний спосіб призначення обробників за допомогою спеціальних методів addEventListener та removeEventListener. Вони вільні від цього недоліку.

Синтаксис додавання обробника:

element.addEventListener(event, handler, [options]);
event
Назва події, наприклад "click".
handler
Посилання на функцію-обробник.
options
Додатковий об’єкт із властивостями:
  • once: якщо true, тоді обробник буде автоматично вилучений після виконання.
  • capture: фаза, на якій повинен спрацювати обробник, докладніше про це буде розказано у розділі Бульбашковий механізм (спливання та занурення). Так історично склалося, що options може бути false/true, це те саме, що {capture: false/true}.
  • passive: якщо true, тоді обробник ніколи не викличе preventDefault(), докладніше про це буде розказано у розділі Типові дії браузера.

Для видалення обробника слід використовувати removeEventListener:

element.removeEventListener(event, handler, [options]);
Видалення підписника на подію вимагає саме ту ж функцію

Для видалення потрібно передати саме ту функцію-обробник, яка була присвоєна.

Отак не спрацює:

elem.addEventListener( "click" , () => alert('Дякую!'));
// ....
elem.removeEventListener( "click", () => alert('Дякую!'));

Обробник не буде видалено, так як removeEventListener передано не таку ж функцію, а іншу, з однаковим кодом.

Ось так вірно:

function handler() {
  alert( 'Дякую!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

Зверніть увагу – якщо функцію обробник не зберегти будь-де, ми не зможемо її видалити. Немає методу, який дозволяє отримати з елемента обробники подій, присвоєні через addEventListener.

Метод addEventListener дозволяє додавати кілька обробників на одну подію одного елемента, наприклад:

<input id="elem" type="button" value="Натисни мене"/>

<script>
  function handler1() {
    alert('Дякую!');
  };

  function handler2() {
    alert('Ще раз дякую!');
  }

  elem.onclick = () => alert("Привіт");
  elem.addEventListener("click", handler1); // Дякую!
  elem.addEventListener("click", handler2); // Ще раз дякую!
</script>

Як видно з прикладу вище, можна одночасно призначати обробники через DOM-властивість і через addEventListener. Однак, щоб уникнути плутанини, рекомендується вибрати один спосіб.

Обробники деяких подій можна присвоїти лише через addEventListener

Існують події, які не можна призначити через DOM-властивість, але можна через addEventListener.

Наприклад, така подія DOMContentLoaded, яке спрацьовує, коли завершено завантаження та побудову DOM документа.

// не буде працювати
document.onDOMContentLoaded = function() {
  alert("DOM побудований");
};
// буде працювати
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM побудований");
});

Таким чином addEventListener більш універсальний. Хоча зауважимо, що таких подій меншість, це скоріше виняток, ніж правило.

Об’єкт події

Щоб правильно обробити подію, можуть знадобитися деталі того, що сталося. Не просто “клік” або “натискання клавіші”, але й координати вказівника миші, яка саме клавіша натиснута і так далі.

Коли відбувається подія, браузер створює об’єкт події, записує в нього деталі та передає його як аргумент функції-обробнику.

Приклад нижче демонструє отримання координат миші з події:

<input type="button" value="Натисни мене" id="elem">

<script>
  elem.onclick = function(event) {
    // вивести тип події, елемент та координати кліка
    alert(event.type + " на " + event.currentTarget);
    alert("Координати: " + event.clientX + ":" + event.clientY);
  };
</script>

Деякі властивості об’єкту event:

event.type
Тип події, у цьому випадку "click".
event.currentTarget
Елемент, у якому спрацював обробник. Це значення зазвичай таке саме, як і в this, але якщо обробник є функцією-стрілкою чи за допомогою bind прив’язаний інший об’єкт this, то ми можемо отримати елемент з event.currentTarget.
event.clientX / event.clientY
Координати курсору в момент кліку у площині вікна для подій миші.

Існує багато властивостей подій. Більшість з них залежать від типу події: події клавіатури мають один набір подій, а події вказівника – інший. Ми розглянемо детальніше деякі з них пізніше.

Об’єкт події доступний і в HTML

При призначенні обробника в HTML теж можна використовувати об’єкт event, ось так:

<input type="button" onclick="alert(event.type)" value="Тип події">

Це можливо тому, що коли браузер з атрибуту створює функцію-обробник, вона виглядає так: function(event) { alert(event.type) }. Тобто, її перший аргумент називається "event", а тіло взяте з атрибуту.

Об’єкт-обробник: handleEvent

Ми можемо призначити обробником не лише функцію, а й об’єкт за допомогою addEventListener. У такому разі, коли відбувається подія, викликається метод об’єкта handleEvent.

Наприклад:

<button id="elem">Натисни мене</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>

Як бачимо, якщо addEventListener отримує об’єкт як обробник, він викликає obj.handleEvent(event), коли відбувається подія.

Ми також можемо використати клас для цього, ось так:

<button id="elem">Натисни мене</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "Натиснута кнопка миші";
          break;
        case 'mouseup':
          elem.innerHTML += "...і відпущена.";
          break;
      }
    }
  }

  let menu = new Menu();

  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

Тут той самий об’єкт обробляє обидві події. Зверніть увагу, ми повинні явно призначити обидва обробники через addEventListener. Тоді об’єкт menu отримуватиме події mousedown та mouseup, а не інші (не призначені) типи подій.

Метод handleEvent не обов’язково має виконувати всю роботу сам. Він може викликати інші методи, які створені під обробку конкретних типів подій, ось так:

<button id="elem">Натисни мене</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Натиснута кнопка миші";
    }

    onMouseup() {
      elem.innerHTML += "...і відпущена.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

Тепер обробка подій розділена методами, що спрощує підтримку коду.

Підсумки

Є три способи призначення обробників подій:

  1. Атрибут HTML: onclick="...".
  2. Властивість DOM: elem.onclick = function.
  3. Спеціальні методи: elem.addEventListener(event, handler[, phase]) для додавання, removeEventListener для видалення.

HTML-атрибути використовуються рідко тому, що JavaScript у HTML-тегу виглядає трохи дивно. До того ж багато коду там не напишеш.

DOM-властивості можна використовувати, але ми не можемо призначити більше одного обробника на один тип події. У багатьох випадках із цим обмеженням можна миритися.

Останній спосіб найбільш гнучкий, проте потрібно писати більше коду. Є кілька типів подій, які працюють лише через нього, наприклад transitionend та DOMContentLoaded. Також addEventListener підтримує об’єкти-обробники подій. В цьому випадку викликається метод об’єкту handleEvent.

Не важливо, як ви призначаєте обробник, він отримує об’єкт події першим аргументом. Цей об’єкт містить подробиці про те, що сталося.

Ми вивчимо більше про події та їх типи у наступних розділах.

Завдання

важливість: 5

Напишіть такий JavaScript, щоб після натискання на кнопку button, елемент <div id="text"> зникав.

Демонстрація роботи:

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

важливість: 5

Напишіть такий код, щоб після натискання на кнопку, вона зникала.

Наприклад:

Можемо використати this в обробнику для доступу до самого елемента:

<input type="button" onclick="this.hidden=true" value="Сховати">

У змінній button знаходиться кнопка. Спочатку на ній немає обробників.

Який з обробників запуститься? Що буде виведено під час кліку після виконання коду?

button.addEventListener("click", () => alert("1"));

button.removeEventListener("click", () => alert("1"));

button.onclick = () => alert(2);

Відповідь: 1 і 2.

Перший обробник спрацює, тому що він не був вилучений методом removeEventListener. Щоб видалити обробник, необхідно передати саме ту функцію, яка була призначена як обробник. Попри те, що код ідентичний, в removeEventListener передається нова, інша функція.

Щоб видалити функцію-обробник, потрібно десь зберегти посилання на неї, наприклад:

function handler() {
  alert(1);
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

Обробник button.onclick спрацює все одно. Разом з addEventListener.

важливість: 5

Нехай м’яч переміщається при натисканні на поле, туди, де був клік, ось так:

Вимоги:

  • Центр м’яча повинен збігатися з курсором миші (якщо це можливо без перетину країв поля);
  • CSS-анімація бажана, але не є обов’язковою;
  • М’яч у жодному разі не повинен перетинати межі поля;
  • При прокручуванні сторінки нічого не повинно ламатися;

Нотатки:

  • Код повинен уміти працювати з різними розмірами м’яча та поля, не прив’язуватися до будь-яких фіксованих значень.
  • Використовуйте властивості event.clientX/event.clientY, щоб вирахувати координати миші при кліці.

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

Спочатку ми маємо вибрати метод позиціювання м’яча.

Ми не можемо використати position:fixed для цього, оскільки прокручування сторінки переміщатиме м’яч поля.

Правильніше використовувати position:absolute і, щоб зробити позиціювання справді надійним, зробимо саме поле field позиціонованим.

Тоді м’яч буде позиціонований щодо поля:

#field {
  width: 200px;
  height: 150px;
  position: relative;
}

#ball {
  position: absolute;
  left: 0; /* по відношенню до найближчого розташованого предка (field) */
  top: 0;
  transition: 1s all; /* CSS-анімація для значень left/top робить пересування м’яча плавним */
}

Далі ми маємо призначити коректні значення ball.style.left/top. Зараз вони містять координати щодо поля.

Як на зображенні:

Ми маємо event.clientX/clientY – координати натискання мишки щодо вікна браузера.

Щоб отримати значення left для м’яча після натискання мишки щодо поля, ми повинні від координати натискання мишки відняти координату лівого краю поля та ширину межі:

let left = event.clientX - fieldCoords.left - field.clientLeft;

Значення ball.style.left означає «лівий край елемента» (м’яча). І якщо ми призначимо такий left для м’яча, то його ліва межа, а не центр, буде під курсором миші.

Нам потрібно зрушити м’яч на половину його висоти вгору та половину його ширини вліво, щоб центр м’яча точно збігався з точкою натискання мишки.

У результаті значення для left буде таким:

let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;

Вертикальна координата обчислюватиметься за тією ж логікою.

Слід пам’ятати, що ширина та висота м’яча має бути відома в той момент, коли ми отримуємо значення ball.offsetWidth. Це значення може бути задано в HTML або CSS.

Відкрити рішення в пісочниці.

важливість: 5

Створіть меню, яке відкривається/згортається після кліку:

P.S. HTML/CSS вихідного документа можна і треба змінювати.

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

HTML/CSS

Для початку створимо розмітку HTML/CSS нашого меню.

Меню – це окремий графічний компонент на сторінці, тому його краще винести в окремий DOM-елемент.

Список пунктів меню може бути представлений у вигляді списку ul/li.

Приклад HTML структури:

<div class="menu">
  <span class="title">Солодощі (тисни на мене)!</span>
  <ul>
    <li>Тістечко</li>
    <li>Пончик</li>
    <li>Мед</li>
  </ul>
</div>

Для заголовка ми використовуємо тег <span>, тому що <div>, як і будь-який блоковий елемент, має приховану властивість display:block, це означає, що він має 100% ширину.

Наприклад:

<div style="border: solid red 1px" onclick="alert(1)">Солодощі (тисни мене)!</div>

Таким чином, якщо ми додамо обробник події в onclick, то він буде спрацьовувати на клік на всій ширині меню.

Оскільки <span> має неявну властивість display: inline, він займає тільки стільки місця, щоб умістити весь текст:

<span style="border: solid red 1px" onclick="alert(1)">Солодощі (тисни мене)!</span>

Перемикання меню

Функціонал перемикання меню повиннен змінювати стрілку та приховувати або показувати список елементів меню.

Всі ці зміни можна реалізувати засобами CSS. За допомогою JavaScript ми будемо змінювати вигляд меню, додаючи або видаляючи клас .open.

Без класу .open меню буде закритим:

.menu ul {
  margin: 0;
  list-style: none;
  padding-left: 20px;
  display: none;
}

.menu .title::before {
  content: '▶ ';
  font-size: 80%;
  color: green;
}

…А з класом .open стрілка зміниться і список буде показано:

.menu.open .title::before {
  content: '▼ ';
}

.menu.open ul {
  display: block;
}

Відкрити рішення в пісочниці.

Є список повідомлень.

За допомогою JavaScript для кожного повідомлення додайте у верхній правий кут кнопку закриття.

Результат має виглядати, як показано тут:

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

Щоб додати кнопку закриття, ми можемо використовувати або position:absolute (і зробити плитку (pane) position:relative) або float:right. Перевага варіанта з float:right у тому, що кнопка закриття ніколи не перекриє текст, але варіант position:absolute дає більше свободи для дій. Загалом вибір за вами.

Тоді для кожного повідомлення(pane) код може бути таким:

pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');

Елемент <button> стає pane.firstChild, таким чином ми можемо додати до нього обробник події:

pane.firstChild.onclick = () => pane.remove();

Відкрити рішення в пісочниці.

Створити “Карусель” – стрічку зображень, яку можна прокручувати, натискаючи на стрілки.

Надалі до неї можна буде додати анімацію, динамічне підвантаження та інші можливості.

P.S. У цьому завданні розробка структури HTML/CSS становить 90% рішення.

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

Стрічка зображень може бути представлена як список ul/li з картинками <img>.

Потрібно розташувати стрічку всередині <div> фіксованого розміру, так щоб одночасно була видна тільки потрібна частина списку:

Щоб список зробити горизонтальним, нам потрібно застосувати CSS-властивість display: inline-block для <li>.

Для тегу <img> ми також повинні налаштувати display, оскільки за умовчанням він є inline. У всіх елементах типу inline резервується додаткове місце під “хвости” символів. І щоб його забрати, нам потрібно прописати display:block.

Для прокручування будемо переміщати <ul>. Це можна робити по-різному, наприклад, призначенням CSS-властивості transform: translateX() (краще для продуктивності) або margin-left:

У зовнішнього <div> фіксована ширина, тому зайві зображення обрізаються.

Вся карусель – це самостійний «графічний компонент» на сторінці, таким чином нам краще його «обернути» в окремий <div class="carousel"> і вже модифікувати стилі всередині нього.

Відкрити рішення в пісочниці.

Навчальна карта