Тень ДОМ 301

Расширенные концепции и API DOM

В этой статье рассказывается о других удивительных вещах, которые можно сделать с помощью Shadow DOM! Он основан на концепциях, обсуждавшихся в Shadow DOM 101 и Shadow DOM 201 .

Использование нескольких теневых корней

Если вы устраиваете вечеринку, становится душно, если все толпятся в одной комнате. Вам нужна возможность распределения групп людей по нескольким комнатам. Элементы, на которых размещен Shadow DOM, тоже могут это делать, то есть они могут одновременно размещать более одного теневого корня.

Давайте посмотрим, что произойдет, если мы попытаемся прикрепить к хосту несколько теневых корней:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

Отрисовывается «Root 2 FTW», несмотря на то, что мы уже прикрепили теневое дерево. Это связано с тем, что побеждает последнее теневое дерево, добавленное к хосту. Что касается рендеринга, это стек LIFO. Проверка DevTools подтверждает это поведение.

Так какой же смысл использовать несколько теней, если на вечеринку рендеринга приглашается только последняя? Введите точки вставки тени.

Точки вставки тени

«Теневые точки вставки» ( <shadow> ) похожи на обычные точки вставки ( <content> ) тем, что они являются заполнителями. Однако вместо того, чтобы быть заполнителями для содержимого хоста, они являются хостами для других теневых деревьев . Это Shadow DOM: Начало!

Как вы, вероятно, можете себе представить, чем дальше вы бурите кроличью нору, все становится сложнее. По этой причине в спецификации очень четко указано, что происходит, когда задействовано несколько элементов <shadow> :

Возвращаясь к нашему исходному примеру, первый теневой root1 был исключен из списка приглашенных. Добавление точки вставки <shadow> возвращает его:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

В этом примере есть пара интересных вещей:

  1. «Root 2 FTW» по-прежнему отображается выше «Root 1 FTW». Это связано с тем, где мы разместили точку вставки <shadow> . Если вы хотите обратного, переместите точку вставки: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>'; .
  2. Обратите внимание, что теперь в корневом каталоге root1 есть точка вставки <content> . Это заставляет текстовый узел «Light DOM» участвовать в рендеринге.

Что отображается в <shadow> ?

Иногда полезно знать, что старое теневое дерево отображается в <shadow> . Вы можете получить ссылку на это дерево через .olderShadowRoot :

**root2.olderShadowRoot** === root1 //true

Получение теневого корня хоста

Если элемент содержит Shadow DOM, вы можете получить доступ к его самому молодому теневому корню, используя .shadowRoot :

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

Если вы беспокоитесь о том, что люди перейдут в вашу тень, переопределите .shadowRoot как нулевое:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

Немного взлома, но это работает. В конце концов, важно помнить, что, хотя Shadow DOM и удивительно фантастичен, он не был разработан как функция безопасности . Не полагайтесь на него для полной изоляции контента.

Создание Shadow DOM в JS

Если вы предпочитаете создавать DOM в JS, HTMLContentElement и HTMLShadowElement есть для этого интерфейсы.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

Этот пример почти идентичен примеру в предыдущем разделе . Единственная разница в том, что теперь я использую select для извлечения недавно добавленного <span> .

Работа с точками вставки

Узлы, которые выбираются из основного элемента и «распределяются» в теневое дерево, называются… барабанная дробь… распределенными узлами! Им разрешено пересекать границу тени, когда точки вставки приглашают их войти.

Что концептуально странно в точках вставки, так это то, что они физически не перемещают DOM. Узлы хоста остаются нетронутыми. Точки вставки просто перепроецируют узлы с хоста в теневое дерево. Это презентация/рендеринг: «Переместите эти узлы сюда» «Отобразите эти узлы в этом месте».

Например:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

Вуаля! h2 не является дочерним элементом теневого DOM. Это приводит к еще одному моменту:

Элемент.getDistributedNodes()

Мы не можем перейти в <content> , но API .getDistributedNodes() позволяет нам запрашивать распределенные узлы в точке вставки:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

Подобно .getDistributedNodes() , вы можете проверить, в какие точки вставки распределен узел, вызвав его .getDestinationInsertionPoints() :

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

Инструмент: Визуализатор Shadow DOM

Понять черную магию Shadow DOM сложно. Я помню, как впервые пытался осмыслить это.

Чтобы наглядно представить, как работает рендеринг Shadow DOM, я создал инструмент с использованием d3.js. Оба поля разметки слева доступны для редактирования. Не стесняйтесь вставлять свою собственную разметку и экспериментировать, чтобы увидеть, как все работает, а точки вставки перемещают узлы хоста в теневое дерево.

Визуализатор теневого DOM
Запустить визуализатор Shadow DOM

Попробуйте и дайте мне знать, что вы думаете!

Модель событий

Некоторые события пересекают теневую границу, а некоторые нет. В тех случаях, когда события пересекают границу, цель события корректируется, чтобы поддерживать инкапсуляцию, которую обеспечивает верхняя граница теневого корня. То есть события перенаправляются так, чтобы они выглядели так, как будто они пришли из главного элемента, а не из внутренних элементов в Shadow DOM .

Воспроизвести действие 1

  • Это интересно. Вы должны увидеть mouseout от элемента хоста ( <div data-host> ) к синему узлу. Несмотря на то, что это распределенный узел, он все равно находится на хосте, а не в ShadowDOM. При дальнейшем перемещении мыши вниз по желтому узлу снова происходит наведение mouseout на синий узел.

Воспроизвести действие 2

  • На хосте появляется один mouseout (в самом конце). Обычно вы видите триггер событий mouseout для всех желтых блоков. Однако в данном случае эти элементы являются внутренними для Shadow DOM, и событие не выходит за его верхнюю границу.

Воспроизвести Действие 3

  • Обратите внимание, что когда вы щелкаете по входу, focusin появляется не на входе, а на самом хост-узле. Он был перенацелен!

События, которые всегда останавливаются

Следующие события никогда не пересекают границу тени:

  • прерывать
  • ошибка
  • выбирать
  • изменять
  • нагрузка
  • перезагрузить
  • изменить размер
  • прокрутка
  • выбрать начало

Заключение

Надеюсь, вы согласитесь, что Shadow DOM невероятно мощный инструмент . Впервые у нас есть правильная инкапсуляция без лишнего багажа <iframe> или других старых методов.

Shadow DOM, безусловно, сложный зверь, но его стоит добавить в веб-платформу. Потратьте на это некоторое время. Изучите это. Задавайте вопросы.

Если вы хотите узнать больше, прочтите вводную статью Доминика Shadow DOM 101 и мою статью Shadow DOM 201: CSS и стили .