16 липня 2023 р.

Запуск користувацьких подій

Ми можемо не тільки призначати обробники, але й генерувати події з JavaScript.

Користувацькі події можна використовувати для створення “графічних компонентів”. Наприклад, кореневий елемент нашого власного меню на основі JS може викликати події, які повідомлять, що відбувається з меню: open (меню відкрито), select (вибрано елемент) тощо. Інший код може прослуховувати ці події та дізнаватись що відбувається з меню.

Ми можемо генерувати не тільки абсолютно нові події, які ми вигадуємо для власних цілей, але й вбудовані, такі як click, mousedown тощо. Це може бути корисно для автоматизованого тестування.

Конструктор подій

Вбудовані класи подій утворюють ієрархію, подібну до класів елементів DOM. Корінь – це вбудований клас Event.

Ми можемо створювати об’єкти Event на зразок цього:

let event = new Event(type[, options]);

Аргументи:

  • type – тип події, рядок, як-от "click" або наш власний, наприклад, "my-event".

  • options – об’єкт з двома необов’язковими властивостями:

    • bubbles: true/false – якщо true, то подія спливає.
    • cancelable: true/false – якщо true, то “типова дія” може бути попереджена. Пізніше ми побачимо, що це означає для користувацьких подій.

    Типово обидва параметри є хибними: {bubbles: false, cancelable: false}.

dispatchEvent

Після створення об’єкта події ми повинні “запустити” її на елементі за допомогою виклику elem.dispatchEvent(event).

Потім обробники реагують на неї, як на звичайну подію браузера. Якщо подія була створена з прапором bubbles, вона спливає.

У наведеному нижче прикладі подія click ініціюється в JavaScript. Обробник працює так само, як якщо б клікнули кнопку:

<button id="elem" onclick="alert('Клік!');">Автоклік</button>

<script>
  let event = new Event("click");
  elem.dispatchEvent(event);
</script>
event.isTrusted

Існує спосіб відрізнити “справжню” користувацьку подію від такої, яку згенеровано скриптом.

Властивість event.isTrusted має значення true для подій, які відбуваються в результаті реальних дій користувача, і false для подій, згенерованих скриптом.

Приклад спливання

Ми можемо створити спливаючу подію з назвою "hello" і зловити її на document.

Все, що нам потрібно, це встановити bubbles на true:

<h1 id="elem">Привіт від скрипта!</h1>

<script>
  // ловимо на document...
  document.addEventListener("hello", function(event) { // (1)
    alert("Привіт від " + event.target.tagName); // Привіт від H1
  });

  // ...запуск події на елементі!
  let event = new Event("hello", {bubbles: true}); // (2)
  elem.dispatchEvent(event);

  // обробник документа активується та відобразить повідомлення.

</script>

Примітки:

  1. Нам слід використовувати addEventListener для наших користувацьких подій, оскільки on<event> існує лише для вбудованих подій, document.onhello не працює.
  2. Потрібно встановити bubbles:true, інакше подія не спливе.

Механіка спливання однакова для вбудованих (click) і користувацьких (hello) подій. Також є етапи перехоплення та спливання.

MouseEvent, KeyboardEvent та інші

Ось короткий список класів для подій інтерфейсу користувача з UI Event specification:

  • UIEvent
  • FocusEvent
  • MouseEvent
  • WheelEvent
  • KeyboardEvent

Ми повинні використовувати їх замість new Event, якщо ми хочемо створити такі події. Наприклад, new MouseEvent("click")

Правильний конструктор дозволяє вказати стандартні властивості для цього типу події.

Такі як clientX/clientY для події миші:

let event = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // 100

Зверніть увагу: цього не можна було б зробити за допомогою базового конструктора Event.

Давайте спробуємо:

let event = new Event("click", {
  bubbles: true, // тільки властивості bubbles та cancelable
  cancelable: true, // працюють в Event конструкторі
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined, невідома властивість ігнорується!

Втім, ми можемо обійти це, призначивши event.clientX=100 безпосередньо після створення об’єкта. Тож це питання зручності та дотримання правил. Події, створені браузером, завжди мають правильний тип.

Повний опис властивостей для різних події інтерфейсу користувача є в специфікації, наприклад, MouseEvent.

Користувацькі події

Для наших власних, абсолютно нових типів подій, таких як "hello", ми повинні використовувати new CustomEvent. Технічно CustomEvent – це те ж саме, що й Event, за одним винятком.

У другий аргумент (об’єкт) ми можемо додати додаткову властивість detail для будь-якої спеціальної інформації, яку ми хочемо передати разом із подією.

Наприклад:

<h1 id="elem">Привіт від Івана!</h1>

<script>
  // додаткові відомості надходять разом із подією до обробника
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

  elem.dispatchEvent(new CustomEvent("hello", {
    detail: { name: "Іван" }
  }));
</script>

Властивість detail може містити будь-які дані. Технічно ми могли б жити і без них, оскільки ми можемо призначити будь-які властивості звичайному об’єкту new Event після його створення. Але CustomEvent забезпечує спеціальне поле detail, щоб уникнути конфліктів з іншими властивостями події.

Крім того, клас події описує яка саме це подія, і якщо вона користувацька, то ми повинні використовувати CustomEvent, щоб явно вказати на це.

event.preventDefault()

Багато браузерів, які мають “типові дії”, як-от перехід за посиланням, виділення тощо.

Для нових користувацьких подій, безумовно, немає типових дій браузера, але код, який надсилає таку подію, може мати власні плани, що робити після ініціювання події.

Викликаючи event.preventDefault(), обробник події може надіслати сигнал про те, що ці дії слід скасувати.

У цьому випадку виклик elem.dispatchEvent(event) повертає false. І код, який його надіслав, знає, що продовжувати не потрібно.

Давайте подивимося на практичний приклад – кролик, що ховається (могло б бути меню, що закривається, або щось подібне).

Нижче ви можете побачити функції #rabbit і hide(), які запускають подію "hide", щоб усі зацікавлені сторони знали, що кролик збирається сховатися.

Будь-який обробник може прослухати цю подію за допомогою rabbit.addEventListener('hide',...) і, якщо потрібно, скасувати дію за допомогою event.preventDefault(). Тоді кролик не зникне:

<pre id="rabbit">
  |\   /|
   \|_|/
   /. .\
  =\_Y_/=
   {>o<}
</pre>
<button onclick="hide()">Hide()</button>

<script>
  function hide() {
    let event = new CustomEvent("hide", {
      cancelable: true // без цього прапорця preventDefault не спрацює
    });
    if (!rabbit.dispatchEvent(event)) {
      alert('Обробник запобіг дії');
    } else {
      rabbit.hidden = true;
    }
  }

  rabbit.addEventListener('hide', function(event) {
    if (confirm("Викликати preventDefault?")) {
      event.preventDefault();
    }
  });
</script>

Зверніть увагу: подія повинна мати прапор cancelable: true, інакше виклик event.preventDefault() ігнорується.

Вкладені події є синхронними

Як правило, події обробляються в черзі. Тобто: якщо браузер обробляє onclick і відбувається нова подія, напр. курсор було переміщено, тоді її обробка ставиться в чергу, відповідні обробники mousemove будуть викликані після завершення обробки onclick.

Винятком є випадки, коли одна подія починається з іншої, напр. використовуючи dispatchEvent. Такі події обробляються негайно: викликаються нові обробники подій, а потім відновлюється обробка поточної події.

Наприклад, у коді нижче подія menu-open ініціюється під час onclick.

Вона обробляється негайно, не чекаючи закінчення обробки onclick:

<button id="menu">Меню (клікни мене)</button>

<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // спрацьовує між 1 та 2
  document.addEventListener('menu-open', () => alert('вкладена подія'));
</script>

Порядок виведення такий: 1 → вкладена подія → 2.

Зауважте, що вкладена подія menu-open ловиться на document. Поширення та обробка вкладеної події закінчується до того, як опрацювання повернеться до зовнішнього коду (onclick).

Це стосується не тільки dispatchEvent, є й інші випадки. Якщо обробник подій викликає методи, які викликають інші події, вони також обробляються синхронно, вкладеним способом.

Скажімо, нам це не подобається. Ми б хотіли спочатку повністю обробити onclick, незалежно від menu-open або будь-яких інших вкладених подій.

Тоді ми можемо або помістити dispatchEvent (або інший виклик, що ініціює подію) в кінець onclick, або, можливо, краще, загорнути його в setTimeout з нульовою затримкою:

<button id="menu">Меню (клікни мене)</button>

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  document.addEventListener('menu-open', () => alert('вкладена подія'));
</script>

Тепер dispatchEvent запускається асинхронно після завершення поточного виконання коду, включаючи menu.onclick, тому обробники подій повністю відокремлені.

Порядок виведення стає: 1 → 2 → вкладена подія.

Підсумки

Щоб створити подію з коду, нам спочатку потрібно створити об’єкт події.

Базовий конструктор Event(name, options) приймає довільне ім’я події та об’єкт параметрів із двома властивостями:

  • bubbles: true якщо подія має спливати.
  • cancelable: true якщо event.preventDefault() повинен працювати.

Інші конструктори вбудованих подій, як-от MouseEvent, KeyboardEvent тощо, приймають властивості, характерні для цього типу події. Наприклад, clientX для подій миші.

Для користувацьких подій ми повинні використовувати конструктор CustomEvent. Він має додаткову опцію з назвою detail, ми повинні призначити їй дані, що стосуються події. Тоді всі обробники зможуть отримати до них доступ як event.detail.

Незважаючи на технічну можливість генерування подій браузера, таких як click або keydown, ми повинні користуватись цим з великою обережністю.

Ми не повинні генерувати події браузера, оскільки це хакерський спосіб запуску обробників. Найчастіше це ознака поганої архітектури.

Вбудовані події можуть бути створені:

  • Як явний хак, щоб змусити сторонні бібліотеки працювати належним чином, якщо вони не забезпечують інших засобів взаємодії.
  • Для автоматизованого тестування, щоб “клікнути кнопку” в скрипті та перевірити, чи правильно реагує інтерфейс.

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

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