17 лютого 2022 р.

Типові дії браузера

Багато подій автоматично призводять до певних дій, які виконує браузер.

Наприклад:

  • Клік на посилання ініціює навігацію до його 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()

Як ми чітко бачимо, 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>-специфічні функції браузера все одно не працюватимуть для неї.

Завдання

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

Чому в коді нижче return false взагалі не працює?

<script>
  function handler() {
    alert( "..." );
    return false;
  }
</script>

<a href="https://w3.org" onclick="handler()">браузер перейде на w3.org</a>

Браузер переходить за URL-адресою після кліку, але нам це не потрібно.

Як це виправити?

Коли браузер зчитує атрибут on*, як onclick, він створює обробник із його вмісту.

Для onclick="handler()" функція буде:

function(event) {
  handler() // вміст onclick
}

Тепер ми бачимо, що значення, яке повертає handler(), не використовується і не впливає на результат.

Виправлення просте:

<script>
  function handler() {
    alert("...");
    return false;
  }
</script>

<a href="https://w3.org" onclick="return handler()">w3.org</a>

Також ми можемо використовувати event.preventDefault(), наприклад:

<script>
  function handler(event) {
    alert("...");
    event.preventDefault();
  }
</script>

<a href="https://w3.org" onclick="handler(event)">w3.org</a>
важливість: 5

Зробіть так, щоб усі посилання всередині елемента з id="contents" запитали у користувача, чи дійсно він хоче вийти. І якщо ні, то не переходьте за посиланням.

Ось таким чином:

Детальніше:

  • HTML всередині елемента може бути завантажений або динамічно відновлений в будь-який час, тому ми не можемо знайти всі посилання та розмістити на них обробники. Використовуйте делегування подій.
  • Вміст може мати вкладені теги. Внутрішні посилання також, як-от <a href=".."><i>...</i></a>.

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

Це чудове використання шаблону делегування подій.

У реальному житті замість того, щоб запитувати, ми можемо надіслати запит на «реєстрацію» на сервер, який зберігає інформацію про те, куди пішов відвідувач. Або ми можемо завантажити вміст і показати його прямо на сторінці (якщо це дозволено).

Все, що нам потрібно, це зловити contents.onclick і використати confirm, щоб запитати користувача. Хорошою ідеєю було б використовувати link.getAttribute('href') замість link.href для URL-адреси. Подробиці дивіться у рішенні.

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

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

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

Ось таким чином:

P.S. Використовуйте делегування подій.

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

Рішення полягає в тому, щоб призначити обробник контейнеру та відстежувати кліки. Якщо клік відбувається на посилання <a>, змініть src для #largeImg на href мініатюри.

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

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

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)