Дополнительные концепции и DOM API
В этой статье рассказывается о других удивительных вещах, которые можно делать с помощью 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 Inception!
Как вы, вероятно, можете себе представить, по мере того, как вы углубляетесь в кроличью нору, все усложняется. Поэтому в спецификации четко указано, что происходит, когда используется несколько элементов <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>
В этом примере есть несколько интересных моментов:
- "Root 2 FTW" по-прежнему отображается над "Root 1 FTW". Это связано с тем, где мы разместили
точку вставки
<shadow>. Если вы хотите, чтобы было наоборот, переместите точку вставки:root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';. - Обратите внимание, что в root1 теперь есть точка вставки
<content>. Это позволяет текстовому узлу Light DOM участвовать в отрисовке.
Что отрисовывается в <shadow>?
Иногда полезно знать, какое старое теневое дерево отображается в <shadow>. Получить ссылку на это дерево можно с помощью .olderShadowRoot:
**root2.olderShadowRoot** === root1 //true
Получение теневого корня хоста
Если элемент содержит Shadow DOM, вы можете получить доступ к его самому новому корню Shadow DOM с помощью .shadowRoot:
var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null
Если вы не хотите, чтобы пользователи могли заходить в тень, задайте для .shadowRoot значение null:
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 не является дочерним по отношению к Shadow DOM. Это приводит к ещё одному моменту:
Element.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 Visualizer
Понять, как работает Shadow DOM, непросто. Помню, как я впервые пытался в этом разобраться.
Чтобы наглядно показать, как работает рендеринг Shadow DOM, я создал инструмент с помощью d3.js. Оба поля наценки слева можно редактировать. Вы можете вставить собственную разметку и посмотреть, как она работает и как точки вставки перемещают узлы хоста в теневое дерево.
Попробуйте и поделитесь с нами своим мнением.
Модель событий
Некоторые события пересекают границу тени, а некоторые – нет. В случаях, когда события пересекают границу, цель события корректируется, чтобы сохранить инкапсуляцию, которую обеспечивает верхняя граница теневого корня. Это означает, что события перенаправляются так, чтобы они выглядели как исходящие от элемента хоста, а не от внутренних элементов Shadow DOM.
Play Action 1
- Это интересный вопрос. Вы должны увидеть
mouseoutот главного элемента (<div data-host>) к синему узлу. Несмотря на то, что это распределенный узел, он все еще находится в хосте, а не в ShadowDOM. Если переместить указатель мыши ниже, в желтую область, то для синего узла снова будет вызвано событиеmouseout.
Play Action 2
- На хосте (в самом конце) появляется один
mouseout. Обычно для всех желтых блоков активируется событиеmouseout. Однако в данном случае эти элементы являются внутренними для Shadow DOM, и событие не выходит за его верхнюю границу.
Play Action 3
- Обратите внимание, что при нажатии на поле ввода
focusinпоявляется не на нем, а на самом хост-узле. Цель изменена.
События, которые всегда останавливаются
Следующие события никогда не пересекают границу теневого дерева:
- прервать
- ошибка
- выбрать
- Изменять
- партия
- reset
- изменить размер
- scroll
- selectstart
Заключение
Надеюсь, вы согласитесь, что Shadow DOM – это невероятно мощный инструмент. Впервые у нас есть надлежащая инкапсуляция без дополнительного багажа <iframe> или других старых методов.
Shadow DOM – это, конечно, сложная штука, но она стоит того, чтобы добавить ее в веб-платформу. Попробуйте поработать с ним. Полезные сведения Задавайте вопросы.
Дополнительную информацию можно найти в статье Доминика Shadow DOM 101 и моей статье Shadow DOM 201: CSS & Styling.