DOM Events
DOM Events делегирует события от общего корневого элемента и связывает их с областями и действиями, заданными через HTML-атрибуты.Поддерживает
- делегирование событий от одного корневого элемента
- обработку динамически добавленных элементов
- разделение интерфейса на области и действия
- несколько обработчиков одного действия
- глобальные обработчики событий
- удаление отдельных или всех обработчиков
- автоматическое удаление неиспользуемых DOM-слушателей
- собственные названия HTML-атрибутов
- полное уничтожение экземпляра
Для работы используется класс DOMEvents.
Подключение
В JavaScript-модуле темы импортируйте DOMEvents из ядра:
const { DOMEvents } = await import(
`${window.wsSite.coreUrl}/assets/js/dom-events.js`
);Создайте экземпляр:
const events = new DOMEvents(document);window.wsSite.coreUrl создаётся при выводе CurrentTheme::getJS().
Быстрый старт
Добавьте область и действие в HTML:
<div data-ws-scope="catalog">
<button type="button" data-ws-action="open">
Открыть каталог
</button>
</div>Зарегистрируйте обработчик:
events.on('click', 'catalog', 'open', (event, button) => {
console.log(button);
});Обработчик сработает при клике по кнопке или по вложенному в неё элементу.
Области и действия
По умолчанию используются два атрибута:
| Атрибут | Назначение |
|---|---|
data-ws-scope |
Область интерфейса. |
data-ws-action |
Действие внутри области. |
Пример:
<section data-ws-scope="profile">
<button type="button" data-ws-action="edit">
Редактировать
</button>
<button type="button" data-ws-action="delete">
Удалить
</button>
</section>Регистрация разных действий одной области:
events.on('click', 'profile', 'edit', (event, button) => {
console.log('Редактирование', button);
});
events.on('click', 'profile', 'delete', (event, button) => {
console.log('Удаление', button);
});Названия областей и действий сравниваются как обычные строки и должны совпадать с HTML-атрибутами.
Делегирование событий
Для каждого типа события экземпляр добавляет один слушатель к корневому элементу.
Когда событие достигает корня, модуль:
- Находит ближайший элемент с атрибутом области.
- Получает значение области.
- Находит ближайший элемент с атрибутом действия.
- Получает название действия.
- Вызывает подходящие обработчики.
- Вызывает глобальные обработчики этого типа события.
Поиск выполняется через closest() от event.target.
Благодаря делегированию обработчики работают и для элементов, добавленных после создания экземпляра:
events.on('click', 'notifications', 'close', (event, button) => {
button.closest('.notification')?.remove();
});
document.body.insertAdjacentHTML(
'beforeend',
`
<div class="notification" data-ws-scope="notifications">
Сообщение
<button type="button" data-ws-action="close">Закрыть</button>
</div>
`
);Повторно регистрировать обработчик не нужно.
Создание экземпляра
constructor(root = document, options = {})
Параметры:
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
root |
Document | Element | Window |
document |
Объект, на котором размещаются DOM-слушатели. |
options |
object |
{} |
Настройки атрибутов. |
Пример для отдельного контейнера:
const modal = document.querySelector('[data-modal]');
const modalEvents = new DOMEvents(modal);События должны доходить до переданного корневого объекта.
Собственные атрибуты
Названия атрибутов можно изменить:
const events = new DOMEvents(document, {
scopeAttribute: 'data-app-scope',
actionAttribute: 'data-app-action'
});HTML:
<div data-app-scope="dialog">
<button type="button" data-app-action="close">
Закрыть
</button>
</div>Обработчик:
events.on('click', 'dialog', 'close', () => {
console.log('Диалог закрыт');
});Параметры options
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
scopeAttribute |
string |
data-ws-scope |
Атрибут области. |
actionAttribute |
string |
data-ws-action |
Атрибут действия. |
Пустая строка не заменяет стандартное значение.
Обработчик действия
on(type, scope, action, handler)
Регистрирует обработчик действия:
events.on('click', 'catalog', 'open', (event, actionTarget) => {
actionTarget.classList.add('active');
});Параметры:
| Параметр | Тип | Описание |
|---|---|---|
type |
string |
Тип DOM-события. |
scope |
string |
Значение атрибута области. |
action |
string |
Значение атрибута действия. |
handler |
function |
Функция-обработчик. |
Обработчик получает:
| Аргумент | Тип | Описание |
|---|---|---|
event |
Event |
Исходное DOM-событие. |
actionTarget |
Element | null |
Ближайший элемент с атрибутом действия. |
Метод возвращает текущий экземпляр DOMEvents, поэтому вызовы можно объединять:
events
.on('click', 'dialog', 'open', openDialog)
.on('click', 'dialog', 'close', closeDialog);Несколько обработчиков
Для одного сочетания события, области и действия можно зарегистрировать несколько функций:
events.on('click', 'cart', 'add', updateCart);
events.on('click', 'cart', 'add', showNotification);Они вызываются в порядке регистрации.
Одинаковая функция также может быть зарегистрирована несколько раз. Каждый зарегистрированный обработчик будет вызван отдельно.
Действие без атрибута
Пустая строка действия позволяет обработать событие внутри области, если ближайший элемент действия не найден:
events.on('click', 'site-header', '', (event) => {
if (event.target.closest('nav a')) {
console.log('Переход по ссылке меню');
}
});Если найден элемент с непустым data-ws-action, обработчик пустого действия не вызывается.
Удаление обработчика
off(type, scope, action, handler = null)
Удаляет зарегистрированный обработчик:
function handleOpen(event, button) {
button.classList.add('active');
}
events.on('click', 'catalog', 'open', handleOpen);
events.off('click', 'catalog', 'open', handleOpen);Для удаления конкретного обработчика необходимо передать ту же функцию, которая использовалась при регистрации.
Анонимную функцию нельзя удалить через другую анонимную функцию:
events.on('click', 'catalog', 'open', () => {
console.log('Открытие');
});Если четвёртый аргумент не передан, удаляются все обработчики выбранного события, области и действия:
events.off('click', 'catalog', 'open');Метод возвращает текущий экземпляр.
Когда для типа события больше не остаётся обработчиков, соответствующий DOM-слушатель удаляется с корневого объекта.
Глобальный обработчик
onGlobal(type, handler)
Глобальный обработчик получает каждое событие указанного типа, дошедшее до корневого объекта:
events.onGlobal('click', (event, meta) => {
console.log(meta);
});Он вызывается после обработчиков области и действия.
Обработчик получает исходное событие и объект meta.
Объект meta
| Свойство | Тип | Описание |
|---|---|---|
scope |
string |
Найденная область или пустая строка. |
action |
string |
Найденное действие или пустая строка. |
matched |
boolean |
Был ли найден хотя бы один обработчик действия. |
actionTarget |
Element | null |
Элемент с атрибутом действия. |
Пример закрытия меню по клику снаружи:
events.onGlobal('click', (event, meta) => {
const menu = document.querySelector('[data-menu]');
if (!menu?.classList.contains('open')) {
return;
}
if (meta.matched || menu.contains(event.target)) {
return;
}
menu.classList.remove('open');
});matched сообщает о наличии подходящих обработчиков, но не означает, что обработчик изменил интерфейс или отменил событие.
Удаление глобального обработчика
offGlobal(type, handler = null)
Удаляет глобальный обработчик:
function handleKeydown(event) {
if (event.key === 'Escape') {
console.log('Закрытие');
}
}
events.onGlobal('keydown', handleKeydown);
events.offGlobal('keydown', handleKeydown);Если функция не передана, удаляются все глобальные обработчики этого типа:
events.offGlobal('keydown');Метод возвращает текущий экземпляр.
DOM-слушатель удаляется только тогда, когда для типа события не осталось ни глобальных обработчиков, ни обработчиков действий.
События клавиатуры
Для интерактивных элементов можно зарегистрировать несколько типов событий:
<div
data-ws-scope="site-header"
data-ws-action="menu-toggle"
role="button"
tabindex="0"
aria-expanded="false"
>
Меню
</div>function toggleMenu() {
console.log('Переключение меню');
}
events.on('click', 'site-header', 'menu-toggle', () => {
toggleMenu();
});
events.on('keydown', 'site-header', 'menu-toggle', (event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
event.preventDefault();
toggleMenu();
});Модуль не вызывает preventDefault() автоматически.
Глобальные события Window
Экземпляр можно создать для window:
const windowEvents = new DOMEvents(window);
windowEvents.onGlobal('scroll', () => {
console.log(window.scrollY);
});
windowEvents.onGlobal('resize', () => {
console.log(window.innerWidth);
});Для событий scroll и resize обычно используются глобальные обработчики, поскольку их event.target не обязан быть DOM-элементом.
Уничтожение экземпляра
destroy()
Удаляет все DOM-слушатели, зарегистрированные экземпляром, и очищает обработчики:
events.destroy();После уничтожения экземпляр больше не обрабатывает ранее зарегистрированные события.
Если экземпляр больше не нужен, вызывайте destroy() при удалении связанного интерфейса:
class Dialog {
constructor(root) {
this.events = new DOMEvents(root);
}
init() {
this.events.on('click', 'dialog', 'close', () => {
this.close();
});
}
destroy() {
this.events.destroy();
}
close() {
console.log('Закрытие');
}
}Порядок выполнения
Для одного события обработчики вызываются в следующем порядке:
- Обработчики подходящего действия в порядке регистрации.
- Глобальные обработчики в порядке регистрации.
Возвращаемое значение обработчика не влияет на дальнейший вызов функций.
Асинхронные обработчики запускаются как обычные функции и не ожидаются модулем.
Поддерживаемые события
DOMEvents использует стандартный addEventListener() и не ограничивает список типов событий.
Для делегирования подходят события, которые всплывают до корневого элемента:
clickinputchangekeydownkeyuppointerdownpointermovepointerupmouseovermouseoutfocusinfocusout
События focus, blur, mouseenter и mouseleave обычно не всплывают. Для делегирования используйте их всплывающие аналоги.
Важные замечания
- один экземпляр добавляет не более одного DOM-слушателя каждого типа
- модуль не изменяет DOM самостоятельно
- модуль не вызывает
preventDefault()илиstopPropagation() - обработчик должен быть функцией
- для удаления конкретного обработчика сохраняйте ссылку на функцию
- события должны доходить до корневого объекта
- обработчик, вызвавший
stopPropagation()ниже корня, может не дать событию попасть в модуль - параметры
capture,passive,onceиsignalне поддерживаются - вложенные элементы действий разрешаются через ближайший
data-ws-action - область и действие определяются независимо через
closest()