Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Apply suggestions from code review
  • Loading branch information
dolgachio authored Sep 29, 2023
commit c17d32d6cbf426a946c44576596727ff3b354353
55 changes: 28 additions & 27 deletions 8-web-components/7-shadow-dom-events/article.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Тіньовий DOM та події

Головна мета створення тіньового дерева це інкапсуляція внутрішньої реалізації компоненту.
Головна мета створення тіньового дерева -- це інкапсуляція внутрішньої реалізації компоненту.

Скажімо, було виконано подію click всередині тіньового DOM компоненту `<user-card>`. Але ж скріпти в головному документі і гадки не мають про внутрішню будову тіньового DOM, особливо, якщо компонент походить зі сторонньої бібліотеки. Отже, для збереження інкапсуляції вмісту, браузер *змінює у цієї події цільовий елемент*.
Уявімо, користувач клікнув на якийсь елемент всередені тіньового DOM компоненту `<user-card>`, і відбулася подія click. Але ж скріпти в головному документі і гадки не мають про внутрішню будову тіньового DOM, особливо, якщо компонент походить зі сторонньої бібліотеки.

**Події, що відбуваються у тіньовому DOM, впливають на батьківський елемент, навіть якщо відбулися за межами компоненту.**
Отже, для збереження інкапсуляції вмісту, браузер *змінює у цієї події цільовий(target) елемент*.

**Події, що відбуваються у тіньовому DOM, мають його "host" у властивості `target` об'єкту події, якщо подія обробляється за межами компоненту.**

Розглянемо простий приклад:

Expand All @@ -19,26 +21,25 @@ customElements.define('user-card', class extends HTMLElement {
<button>Click me</button>
</p>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Внутрішній цільовий елемент: " + e.target.tagName);
e => alert("target зсередини: " + e.target.tagName);
}
});

document.onclick =
e => alert("Зовнішній цільовий елемент: " + e.target.tagName);
e => alert("target ззовні: " + e.target.tagName);
</script>
```

Клікнувши на кнопку, отримаємо наступні повідомлення:

1. Внутрішній цільовий елемент: `BUTTON` – внутрішній обробник подій отримує правильну ціль – елемент всередині тіньового DOM
2. Зовнішній цільовий елемент: `USER-CARD` – обробник подій документу отримує тіньовий хост в якості цільового елементу

1. target зсередини: `BUTTON` -- внутрішній обробник подій отримує правильний target -- елемент всередині тіньового DOM.
2. target ззовні: `USER-CARD` -- обробник подій документу отримує тіньовий хост в якості target події.

Зміна цільового елементу – чудова річ, тому що зовнішній документ не повинен знати про внутрішній вміст компоненту. З цієї точки зору, подія відбулась в `<user-card>`.
Зміна target події -- чудова річ, бо зовнішній документ не повинен знати про внутрішній вміст компоненту. З цієї точки зору, подія відбулась в `<user-card>`.

**Зміна цільового елементу не відбувається, якщо подія починається з елементу зі слота, що фактично знаходиться в звичайному світлому DOM.**
**Зміна target не відбувається, якщо подія починається з елементу зі слоту, що фактично знаходиться в звичайному світлому DOM.**

Наприклад, якщо користувач клікає на `<span slot="username">` у прикладі, наведеному нижче, цільовим елементом є саме цей `span` елемент, для обох обробників звичайного (світлого) та тіньового:
Наприклад, якщо користувач клікає на `<span slot="username">` у прикладі, наведеному нижче, цільовим елементом є саме цей елемент `span`, для обох обробників -- звичайного (світлого) та тіньового:

```html run autorun="no-epub" untrusted height=60
<user-card id="userCard">
Expand All @@ -56,25 +57,25 @@ customElements.define('user-card', class extends HTMLElement {
</div>`;

this.shadowRoot.firstElementChild.onclick =
e => alert("Внутрішній цільовий елемент: " + e.target.tagName);
e => alert("target зсередини: " + e.target.tagName);
}
});

userCard.onclick = e => alert(`Зовнішній цільовий елемент: ${e.target.tagName}`);
userCard.onclick = e => alert(`target ззовні: ${e.target.tagName}`);
</script>
```

Якщо клік відбувся на `"Іван Коваль"`, для обох внутрішнього та зовнішнього обробників цільовим елементом є `<span slot="username">`. Так як це елемент зі світлого DOM, то зміни цільового елементу не відбувається.
Якщо клік відбувся на `"Іван Коваль"`, для обох -- внутрішнього та зовнішнього -- обробників у target буде елемент `<span slot="username">`. Це елемент зі світлого DOM, тому зміна target не відбувається.

З іншого боку, якщо клік відбувся на елементі з тіньового DOM, т.я.`<b>Name</b>`, тоді він вспливає з тіньового DOM, a його цільовим елементом `event.target` стає `<user-card>`.
З іншого боку, якщо клік відбувся на елементі з тіньового DOM, напр. на `<b>Name</b>`, то коли він вспливає з тіньового DOM, його `event.target` стає `<user-card>`.

## Спливання, event.composedPath()

Для цілей спливання подій (бульбашковий механізм) використовується розгорнутий DOM.
Для реалізації спливання подій (бульбашковий механізм) використовується підхід розгорнутого DOM.

Отже, якщо у нас є елемент у слоті, і подія відбувається десь усередині цього елементу, тоді вона підіймається до `<slot>` і вище.
Отже, якщо у нас є елемент у слоті, і подія відбувається десь всередині цього елементу, тоді вона підіймається до `<slot>` і вище.

Повний шлях до початкового цільового елементу з усіма тіньовими елементами можна отримати за допомогою `event.composedPath()`. Як видно з назви методу, він повертає шлях після композиції.
Повний шлях до справжнього target елементу цієї події, включаючи всі тіньові елементи, можна отримати за допомогою `event.composedPath()`. Як видно з назви методу, він повертає шлях після складання всіх його елементів.

У наведеному вище прикладі зведений DOM виглядає так:

Expand All @@ -91,7 +92,7 @@ userCard.onclick = e => alert(`Зовнішній цільовий елемен
```


Отже, для кліку по `<span slot="username">` виклик `event.composedPath()` повертає масив: [`span`, `slot`, `div`, `shadow-root`, `user-card`, `body`, `html`, `document`, `window`], що цілковито відображає батьківський ланцюжок, починаючи з цільового елемента у зведеному DOM після композиції.
Отже, для кліку по `<span slot="username">` виклик `event.composedPath()` повертає масив: [`span`, `slot`, `div`, `shadow-root`, `user-card`, `body`, `html`, `document`, `window`], що цілковито відображає батьківський ланцюжок, починаючи з target елемента у зведеному DOM після складання.

```warn header="Деталі тіньового дерева надаються лише для дерев з `{mode:'open'}`"
Якщо тіньове дерево було створено з `{mode: 'closed'}`, то тоді складений (composed) шлях починається від хоста: `user-card` і вище.
Expand All @@ -102,9 +103,9 @@ userCard.onclick = e => alert(`Зовнішній цільовий елемен

## Властивість event.composed

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

Це регулюється властивістю об’єкта події `composed`. Якщо це `true`, то подія дійсно перетинає межу. В іншому випадку його можна буде перехопити лише зсередини тіньового DOM.
Це регулюється властивістю об’єкта події `composed`. Якщо вона `true`, то подія дійсно може перетнути межу. В іншому випадку її можна буде перехопити лише зсередини тіньового DOM.

Якщо ви подивитесь на [UI Events specification](https://www.w3.org/TR/uievents), більшість подій мають `composed: true`:

Expand All @@ -118,16 +119,16 @@ userCard.onclick = e => alert(`Зовнішній цільовий елемен

Та існують деякі події, що мають `composed: false`:

- `mouseenter`, `mouseleave` (ці події взагалі не спливають вгору),
- `mouseenter`, `mouseleave` (ці події взагалі не вспливають),
- `load`, `unload`, `abort`, `error`,
- `select`,
- `slotchange`.

Ці події можна перехопити лише на елементах у межах того ж самого DOM, де знаходиться цільовий елемент події.
Ці події можна перехопити лише на елементах у межах того ж самого DOM, де знаходиться target елемент події.

## Генерація подій (сustom events)
## Генерація подій (Custom events)

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

Наприклад, тут ми створюємо `div#inner` у тіньовому DOM `div#outer` і запускаємо дві події для нього. Лише та, що має `composed: true`, виходить за межі документа:

Expand Down Expand Up @@ -168,13 +169,13 @@ inner.dispatchEvent(new CustomEvent('test', {

## Підсумки

Лише ті події перетинають тіньові межі DOM, для прапорця `composed` яких встановлено значення `true`.
Лише ті події перетинають тіньові межі DOM, у прапорці `composed` яких задано значення `true`.

Вбудовані події здебільшого мають `composed: true`, як описано у відповідних специфікаціях:

- Події інтерфейсу користувача (UI Events) <https://www.w3.org/TR/uievents>.
- Сенсорні події (Touch Events) <https://w3c.github.io/touch-events>.
- Події курсору (Pointer Events) <https://www.w3.org/TR/pointerevents>.
- Події вказівника (Pointer Events) <https://www.w3.org/TR/pointerevents>.
- ...тощо.

Деякі вбудовані події, що мають `composed: false`:
Expand Down