Stínový DOM a události

Myšlenkou stínového stromu je zapouzdřit interní implementační detaily komponenty.

Řekněme, že k události kliknutí dojde uvnitř stínového DOM <user-card> komponent. Ale skripty v hlavním dokumentu nemají ponětí o vnitřních částech stínového DOM, zvláště pokud komponenta pochází z knihovny třetí strany.

Aby tedy zůstaly podrobnosti zapouzdřené, prohlížeč retargetuje událost.

Události, ke kterým dochází ve stínovém DOM, mají jako cíl hostitelský prvek, pokud jsou zachyceny mimo komponentu.

Zde je jednoduchý příklad:

<user-card></user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<p>
      <button>Click me</button>
    </p>`;
    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

document.onclick =
  e => alert("Outer target: " + e.target.tagName);
</script>

Pokud kliknete na tlačítko, zprávy jsou:

  1. Vnitřní cíl:BUTTON – Interní obsluha události získá správný cíl, prvek uvnitř stínového DOM.
  2. Vnější cíl:USER-CARD – obsluha události dokumentu získá jako cíl stínového hostitele.

Retargeting událostí je skvělá věc, protože vnější dokument nemusí vědět o vnitřnostech součástí. Z jeho pohledu se událost stala <user-card> .

Retargeting nenastane, pokud k události dojde na štěrbinovém prvku, který fyzicky žije ve světlém DOM.

Pokud například uživatel klikne na <span slot="username"> v příkladu níže je cílem události přesně toto span prvek pro obsluhu stínu i světla:

<user-card id="userCard">
  <span slot="username">John Smith</span>
</user-card>

<script>
customElements.define('user-card', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `<div>
      <b>Name:</b> <slot name="username"></slot>
    </div>`;

    this.shadowRoot.firstElementChild.onclick =
      e => alert("Inner target: " + e.target.tagName);
  }
});

userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`);
</script>

Pokud dojde ke kliknutí na "John Smith" , pro vnitřní i vnější ovladače je cílem <span slot="username"> . To je prvek z lehkého DOM, takže žádné přesměrování.

Na druhou stranu, pokud ke kliknutí dojde na prvek pocházející ze stínového DOM, např. na <b>Name</b> , pak, jak vybublá ze stínového DOM, jeho event.target je resetováno na <user-card> .

Bublání, event.composedPath()

Pro účely bublání událostí se používá zploštělý DOM.

Pokud tedy máme štěrbinový prvek a někde v něm dojde k události, pak to probublává až do <slot> a nahoru.

Úplnou cestu k původnímu cíli události se všemi stínovými prvky lze získat pomocí event.composedPath() . Jak vidíme z názvu metody, tato cesta je vedena po složení.

Ve výše uvedeném příkladu je zploštělý DOM:

<user-card id="userCard">
  #shadow-root
    <div>
      <b>Name:</b>
      <slot name="username">
        <span slot="username">John Smith</span>
      </slot>
    </div>
</user-card>

Takže pro kliknutí na <span slot="username"> , volání na číslo event.composedPath() vrátí pole:[span , slot , div , shadow-root , user-card , body , html , document , window ]. To je přesně nadřazený řetězec z cílového prvku ve zploštělém DOM po složení.

Podrobnosti stínového stromu jsou poskytovány pouze pro {mode:'open'} stromy

Pokud byl stínový strom vytvořen s {mode: 'closed'} , pak složená cesta začíná od hostitele:user-card a nahoru.

Je to podobný princip jako u jiných metod, které pracují se stínovým DOM. Vnitřnosti uzavřených stromů jsou zcela skryté.

event.composed

Většina událostí úspěšně probublává stínovou hranicí DOM. Existuje jen málo událostí, které ne.

Toto se řídí composed vlastnost objektu události. Pokud je to true , pak událost překročí hranici. V opačném případě jej lze zachytit pouze zevnitř stínového DOM.

Pokud se podíváte na specifikaci událostí uživatelského rozhraní, většina událostí má composed: true :

  • blur , focus , focusin , focusout ,
  • click , dblclick ,
  • mousedown , mouseup mousemove , mouseout , mouseover ,
  • wheel ,
  • beforeinput , input , keydown , keyup .

Všechny události dotyku a události ukazatele mají také composed: true .

Některé události mají composed: false ačkoli:

  • mouseenter , mouseleave (vůbec nebublají),
  • load , unload , abort , error ,
  • select ,
  • slotchange .

Tyto události lze zachytit pouze u prvků v rámci stejného modelu DOM, kde se nachází cíl události.

Vlastní události

Když odesíláme vlastní události, musíme nastavit obě bubbles a composed vlastnosti na true aby probublával a vycházel ze součásti.

Například zde vytvoříme div#inner ve stínovém DOM div#outer a spustit na něm dvě události. Pouze ten s composed: true přejde mimo dokument:

<div id="outer"></div>

<script>
outer.attachShadow({mode: 'open'});

let inner = document.createElement('div');
outer.shadowRoot.append(inner);

/*
div(id=outer)
  #shadow-dom
    div(id=inner)
*/

document.addEventListener('test', event => alert(event.detail));

inner.dispatchEvent(new CustomEvent('test', {
  bubbles: true,
  composed: true,
  detail: "composed"
}));

inner.dispatchEvent(new CustomEvent('test', {
  bubbles: true,
  composed: false,
  detail: "not composed"
}));
</script>

Shrnutí

Události překračují hranice stínu DOM pouze v případě, že mají hodnotu composed příznak je nastaven na true .

Vestavěné události mají většinou composed: true , jak je popsáno v příslušných specifikacích:

  • Události uživatelského rozhraní https://www.w3.org/TR/uievents.
  • Dotkněte se Událostí https://w3c.github.io/touch-events.
  • Ukazatelské události https://www.w3.org/TR/pointerevents.
  • …A tak dále.

Některé vestavěné události, které mají composed: false :

  • mouseenter , mouseleave (také nebublá),
  • load , unload , abort , error ,
  • select ,
  • slotchange .

Tyto události lze zachytit pouze u prvků v rámci stejného modelu DOM.

Pokud odešleme CustomEvent , pak bychom měli explicitně nastavit composed: true .

Upozorňujeme, že v případě vnořených komponent může být jeden stínový DOM vnořen do druhého. V takovém případě složené události probublávají všemi stínovými hranicemi DOM. Pokud je tedy událost určena pouze pro bezprostředně obklopující komponentu, můžeme ji také odeslat na stínovém hostiteli a nastavit composed: false . Pak je mimo stínovou komponentu DOM, ale nepronikne do DOM vyšší úrovně.