Багато подій автоматично призводять до певних дій, які виконує браузер.
Наприклад:
- Клік на посилання ініціює навігацію до його URL-адреси.
- Клік на кнопку відправки форми ініціює її відправку на сервер.
- Натискання кнопки миші на тексті і переміщення курсору – виділяє текст.
Якщо ми обробляємо подію в JavaScript, ми можемо не захотіти, щоб відбулась типова дія браузера, і замість цього захочемо реалізувати іншу поведінку.
Запобігання дії браузера
Є два способи запобігти діям браузера:
- Основний спосіб – це використовувати об’єкт
event
. Існує методevent.preventDefault()
. - Якщо обробник призначено за допомогою
on<event>
(а неaddEventListener
), поверненняfalse
спрацює так само.
У цьому HTML після кліку на посилання навігація не відбувається, браузер нічого не робить:
<a href="/" onclick="return false">Клікніть тут</a>
чи
<a href="/" onclick="event.preventDefault()">тут</a>
У наступному прикладі ми використаємо цю техніку для створення меню на основі JavaScript.
false
з обробника є виняткомЗначення, яке повертає обробник події, зазвичай ігнорується.
Єдиним винятком є return false
з обробника, призначеного за допомогою on<event>
.
У всіх інших випадках значення return
ігнорується. Зокрема, немає сенсу повертати true
.
Приклад: меню
Розглянемо таке меню сайту:
<ul id="menu" class="menu">
<li><a href="/html">HTML</a></li>
<li><a href="/javascript">JavaScript</a></li>
<li><a href="/css">CSS</a></li>
</ul>
Ось так це може виглядати в певними CSS-правилами:
Пункти меню реалізовані як HTML-посилання <a>
, а не кнопки <button>
. Для цього є кілька причин, наприклад:
- Багато людей люблять використовувати “клік правою кнопкою миші” як “відкрити в новому вікні”. Якщо ми використаємо
<button>
або<span>
, це не спрацює. - Під час індексації пошукові системи переходять за посиланнями
<a href="...">
.
Тому ми використовуємо <a>
у розмітці. Але зазвичай ми маємо намір обробляти кліки в JavaScript. Тому ми повинні запобігати типовим діям браузера.
Як тут:
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
let href = event.target.getAttribute('href');
alert( href ); // ...може бути завантаження з сервера, генерація інтерфейсу користувача тощо
return false; // запобігання діям браузера (перехід за URL-адресою не відбудеться)
};
Якщо ми опустимо return false
, то після виконання нашого коду браузер виконає свою “типову дію” – перехід до URL-адреси в href
. Тут нам це не потрібно, оскільки ми самі обробляємо клік.
До речі, використання тут делегування подій робить наше меню дуже гнучким. Ми можемо додавати вкладені списки та стилізувати їх за допомогою CSS.
Певні події перетікають одна в іншу. Якщо запобігти першій події, то не буде і другої.
Наприклад, mousedown
на полі <input>
призводить до фокусування в ньому та події focus
. Якщо ми запобіжимо події mousedown
, фокус не відбудеться.
Спробуйте клікнути на першому <input>
нижче – відбувається подія focus
. Але якщо натиснути другий, фокуса не буде.
<input value="Фокус працює" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Клікни на мене">
Це тому, що дія браузера скасовується під час mousedown
. Фокусування все ще можливе, якщо ми використаємо інший спосіб введення вхідних даних. Наприклад, клавіша Tab для перемикання з 1-го входу на 2-й. Але вже не за допомогою кліку.
Опція «пасивного» обробника
Необов’язковий параметр passive: true
для addEventListener
сигналізує браузеру, що обробник не збирається викликати preventDefault()
.
Чому це може знадобитися?
На мобільних пристроях є деякі події, наприклад touchmove
(коли користувач переміщує палець по екрану), які типово викликають прокручування, але цьому прокручуванню можна запобігти за допомогою preventDefault()
в обробнику.
Тому, коли браузер виявляє таку подію, він повинен спочатку опрацювати всі обробники, а потім, якщо preventDefault
ніде не викликано, він може продовжити прокручування. Це може призвести до непотрібних затримок та миготіння в інтерфейсі користувача.
Параметр passive: true
повідомляє браузеру, що обробник не збирається скасовувати прокручування. Тоді браузер відразу прокручує його, забезпечуючи максимально плавний користувацький досвід, попутно обробляючи подію.
Для деяких браузерів (Firefox, Chrome) параметр passive
має типове значення true
для таких подій як touchstart
або touchmove
.
event.defaultPrevented
Властивість event.defaultPrevented
має значення true
, якщо типову дію було скасовано, і false
в іншому випадку.
Є цікавий варіант використання.
Пам’ятаєте, в розділі Бульбашковий механізм (спливання та занурення) ми говорили про event.stopPropagation()
і чому припинення спливання – це погано?
Іноді замість цього ми можемо використовувати event.defaultPrevented
, щоб повідомити іншим обробникам, що подія була оброблена.
Розглянемо практичний приклад.
Типово браузер під час події contextmenu
(клік правою кнопкою миші) показує контекстне меню зі стандартними параметрами. Ми можемо запобігти цьому і показати своє, ось так:
<button>Правий клік показує контекстне меню браузера</button>
<button oncontextmenu="alert('Малюємо наше меню'); return false">
Правий клік показує наше власне контекстне меню
</button>
Тепер, на додаток до цього контекстного меню, ми хотіли б реалізувати контекстне меню всього документа.
Після кліку правою кнопкою миші має з’явитися найближче контекстне меню.
<p>Правий клік, щоб відкрити контекстне меню документа</p>
<button id="elem">Правий клік, щоб відкрити контекстне меню кнопки</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстне меню кнопки");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстне меню документа");
};
</script>
Проблема полягає в тому, що коли ми клікаємо на elem
, ми отримуємо два меню: на рівні кнопки та (подія спливає) на рівні документа.
Як це виправити? Одне з рішень полягає в тому, щоб подумати так: «Коли ми обробляємо правий клік в обробнику кнопки, давайте зупинимо спливання події» та використовуємо event.stopPropagation()
:
<p>Правий клік, щоб відкрити меню документа</p>
<button id="elem">Правий клік, щоб відкрити меню кнопки (виправлено за допомогою event.stopPropagation)</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
event.stopPropagation();
alert("Контекстне меню кнопки");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстне меню документа");
};
</script>
Тепер меню на рівні кнопки працює, як задумано. Але якою ціною? Ми назавжди забороняємо доступ до інформації про клік правою кнопкою миші для будь-якого зовнішнього коду, включаючи лічильники, які збирають статистику тощо. Це зовсім нерозумно.
Альтернативним рішенням було б перевірити в обробнику document
, чи запобігли типовим дії? Якщо це так, значить, подія була оброблена, і нам не потрібно на це реагувати.
<p>Правий клік, щоб відкрити контекстне меню документа (додано перевірку для event.defaultPrevented)</p>
<button id="elem">Правий клік, щоб відкрити меню кнопки</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Контекстне меню кнопки");
};
document.oncontextmenu = function(event) {
if (event.defaultPrevented) return;
event.preventDefault();
alert("Контекстне меню документа");
};
</script>
Тепер теж все працює коректно. Якщо ми маємо вкладені елементи, і кожен з них має власне контекстне меню, це також спрацює. Просто не забудьте перевірити наявність event.defaultPrevented
у кожному обробнику contextmenu
.
Як ми чітко бачимо, event.stopPropagation()
та event.preventDefault()
(також відомий як return false
) – це два різні методи. Вони не пов’язані один з одним.
Існують також альтернативні способи реалізації вкладених контекстних меню. Один з них – це мати єдиний глобальний об’єкт з обробником для document.oncontextmenu
, а також методами, які дозволяють нам зберігати в ньому інші обробники.
Об’єкт буде ловити будь-який клік правою кнопкою миші, переглядати збережені обробники та запускати відповідний.
Але тоді кожен фрагмент коду, якому буде потрібно контекстне меню, повинен знати про цей об’єкт і використовувати його замість власного обробника contextmenu
.
Підсумки
Існує багато типових дій браузера:
mousedown
– розпочинає виділення (якщо перемістити курсор).click
на<input type="checkbox">
– додає/знімає прапорець зinput
.submit
– браузер надсилає форму на сервер після кліку на<input type="submit">
або натискання Enter всередині поля форми.keydown
– натискання клавіші може призвести до додавання символу в поле або інших дій.contextmenu
– подія відбувається при правому кліку та полягає в тому, щоб показати контекстне меню браузера.- …та багато інших…
Усім типовим діям можна запобігти, якщо ми хочемо обробляти подію виключно за допомогою JavaScript.
Щоб запобігти типовій дії, використовуйте event.preventDefault()
або return false
. Другий метод працює лише для обробників, призначених за допомогою on<event>
.
Параметр passive: true
для addEventListener
повідомляє браузеру, що дію не буде скасовано. Це корисно для деяких мобільних подій, таких як touchstart
і touchmove
, щоб повідомити браузеру, що він не повинен чекати закінчення роботи обробників, перш ніж разпочати прокрутку.
Якщо типову дію було скасовано, значення event.defaultPrevented
стає true
, інакше false
.
Технічно, запобігаючи типовій дії і додаючи JavaScript, ми можемо налаштувати поведінку будь-яких елементів. Наприклад, ми можемо зробити так, щоб посилання <a>
працювало як кнопка, а кнопка <button>
вела себе як посилання (переспрямовувала на іншу URL-адресу або щось подібне).
Але загалом ми повинні зберігати семантичне значення елементів HTML. Наприклад, виконувати навігацію повинна не кнопка, а тег <a>
.
Окрім того, що це “просто хороша річ”, яка робить ваш HTML кращим з точки зору доступності.
Також якщо ми розглянемо приклад із <a>
, то зверніть увагу: браузер дозволяє нам відкривати такі посилання в новому вікні (клікаючи на них правою кнопкою миші або іншими способами). І людям таке подобається. Але якщо ми робимо кнопку, яка веде себе як посилання за допомогою JavaScript, і навіть виглядає як посилання за допомогою CSS, тоді <a>
-специфічні функції браузера все одно не працюватимуть для неї.