renovate webcomponents

This commit is contained in:
Ilya Kantor 2015-02-21 16:37:10 +03:00
parent 35081a779a
commit 0e62abbff8
21 changed files with 421 additions and 476 deletions

View file

@ -1,4 +1,4 @@
# Shadow DOM, шаблоны и стили
# Shadow DOM
Спецификация [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) является отдельным стандартом. Частично он уже используется для обычных DOM-элементов, но также применяется для создания веб-компонентов.
@ -44,7 +44,7 @@ Shadow DOM можно создать внутри любого элемента
Например:
```html
<!--+ run autorun -->
<!--+ run autorun="no-epub" -->
<p id="elem">Доброе утро, страна!</p>
<script>
@ -60,7 +60,7 @@ Shadow DOM можно создать внутри любого элемента
Внутрь этого Shadow DOM, при желании, можно поместить обычное содержимое. Для этого нужно указать, куда. В Shadow DOM это делается через "точку вставки" (insertion point). Она объявляется при помощи тега `<content>`, например:
```html
<!--+ run autorun -->
<!--+ run autorun="no-epub" -->
<p id="elem">Доброе утро, страна!</p>
<script>
@ -92,7 +92,7 @@ Shadow DOM примера выше в инструментах разработ
Например:
```html
<!--+ run autorun -->
<!--+ run autorun="no-epub" -->
<section id="elem">
<h1>Новости</h1>
@ -129,7 +129,7 @@ Shadow DOM примера выше в инструментах разработ
Если нужно работать с содержимым в Shadow DOM, то нужно перейти к нему через `elem.shadowRoot`. Можно и создать новое Shadow DOM-дерево из JavaScript, например:
```html
<!--+ run autorun -->
<!--+ run autorun="no-epub" -->
<p id="elem">Доброе утро, страна!</p>
<script>
@ -157,335 +157,6 @@ Shadow DOM примера выше в инструментах разработ
На момент написания статьи `shadowRoot` можно получить только для Shadow DOM, созданного описанным выше способом, но не встроенного, как в элементах типа `<input type="date">`.
[/warn]
## Шаблоны <template>
Элемент `<template>` предназначен для хранения "образца" разметки, невидимого и предназначенного для вставки куда-либо.
Конечно, есть много способов записать произвольный невидимый текст в HTML. В чём же особенность `<template>`?
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не только показывается, но и считается находящимся вообще "вне документа".
Однако, вместе с тем, оно всё же обрабатывается браузером (а значит должно быть корректным HTML) и записывается как `DocumentFragment` в свойство тега `content`. Предполагается, что мы, при необходимости, возьмём `content` и вставим, куда надо.
Пример вставки шаблона `tmpl` в Shadow DOM элемента `elem`:
```html
<!--+ run autorun -->
<p id="elem">Доброе утро, страна!</p>
<template id="tmpl">
<h3><content></content></h3>
<p>Привет из подполья!</p>
<script> document.write('...document.write:Новость!'); </script>
</template>
<script>
var root = elem.createShadowRoot();
root.appendChild( tmpl.content.cloneNode(true) );
</script>
```
У нас получилось, что:
<ol>
<li>В элементе `#elem` содержатся данные в некоторой оговорённой разметке.</li>
<li>Шаблон `#tmpl` указывает, как их отобразить, куда и в какие HTML-теги завернуть содержимое `#elem`.</li>
<li>Это содержимое добавляется в Shadow DOM тега. Технически, шаблон можно использовать и без Shadow DOM, но тогда не сработает тег `<content>`.</li>
</ol>
Важные детали:
<ul>
<li>В отличие от вставки через `innerHTML` и от обычного `DocumentFragment`, скрипт внутри шаблона выполнится при вставке. Содержимое шаблона изначально "вне документа" и "оживает", когда оно попадает в него. Это относится ко всему -- картинки начинают загружаться, видео -- проигрываться и т.п.</li>
<li>Мы вставляем не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать один шаблон много раз.</li>
</ul>
## Стили
Стилизация Shadow DOM покрывается более общей спецификацией ["CSS Scoping"](http://drafts.csswg.org/css-scoping/).
**По умолчанию стили внутри Shadow DOM относятся только к его содержимому.**
Например:
```html
<!--+ run autorun -->
<p>Жили мы тихо-мирно, и тут...</p>
<p id="elem">Доброе утро, страна!</p>
<template id="tmpl">
*!*
<style> p { color: red; } </style>
*/!*
<h3><content></content></h3>
<p>Привет из подполья!</p>
</template>
<script>
var root = elem.createShadowRoot();
root.appendChild( tmpl.content.cloneNode(true) );
</script>
```
При запуске окрашенным в красный цвет окажется только `<p>` внутри Shadow DOM.
...Но при помощи специальных селекторов переходить через эту границу!
### Извне стиль для Shadow DOM
Если нужно со страницы стилизовать или выбрать элементы внутри Shadow DOM, то можно использовать селекторы:
<ul>
<li>**`::shadow` -- выбирает корень Shadow DOM.**
Например, `#elem::shadow div` найдёт внутри Shadow DOM `#elem` элементы `div`.</li>
<li>**`/deep/` -- особого вида CSS-селектор для всех элементов Shadow DOM, который полностью игнорирует границы между DOM'ами, включая вложенные подэлементы, у которых тоже может быть свой Shadow DOM.**
Например, `#elem /deep/ span` найдёт все `span` внутри Shadow DOM `#elem`, но кроме того, если в `#elem` есть подэлементы, у которых свой Shadow DOM, то оно продолжит поиск в них.
Вот пример, когда внутри одного Shadow DOM есть `<input type="date">`, у которого тоже есть Shadow DOM:
```html
<!--+ run -->
<style>
##elem::shadow span {
/* для span только внутри Shadow DOM #elem */
text-decoration: underline;
}
##elem /deep/ span {
/* для span внутри Shadow DOM #elem и далее внутри input[type=date] */
color: red;
}
</style>
<p id="elem"></p>
<script>
var root = elem.createShadowRoot();
root.innerHTML = "<span>Текущее время:</span> <input type='date'>";
</script>
```
</li>
<li>Кроме того, на Shadow DOM действует CSS-наследование, если свойство поддерживает его по умолчанию.
В этом примере CSS-стили для `body` наследуются на внутренние элементы, включая Shadow DOM:
```html
<!--+ run autorun -->
<style>
body {
color: red;
font-style: italic;
}
</style>
<p id="elem"></p>
<script>
elem.createShadowRoot().innerHTML = "<span>Привет, мир!</span>";
</script>
```
Внутренний элемент станет красным курсивом.
</li>
</ul>
[warn header="Нельзя получить содержимое встроенных элементов"]
Описанные CSS-селекторы можно использовать не только в CSS, но и в `querySelector`.
Исключением являются встроенные элементы типа `<input type="date">`, для которых CSS-селекторы работают, но получить их содержимое нельзя.
Например:
```html
<!--+ run -->
<p id="elem"></p>
<script>
var root = elem.createShadowRoot();
root.innerHTML = "<span>Текущее время:</span> <input type='date'>";
// выберет только span из #elem
// вообще-то, должен выбрать и span из вложенных Shadow DOM,
// но в текущей браузерной реализации для встроенных элементов - не умеет
alert(document.querySelector('#elem /deep/ span').length); // 1
</script>
```
[/warn]
### Стиль Shadow DOM в зависимости от хозяина
Следующие селекторы позволяют выбрать элемент-хозяин:
<ul>
<li>**`:host` выбирает элемент-хозяин**, в котором, живёт Shadow DOM.</li>
<li>**`:host(селектор хозяина)` выбирает элемент-хозяин, если он подходит под селектор.**
Например:
```css
:host(.important) {
/* сработает, если хозяин имеет класс important */
}
```
Этот селектор используется для темизации хозяина "изнутри", в зависимости от его классов и атрибутов.
**Хозяин :host выбирается в именно в контексте Shadow DOM.**
То есть, это доступ не к внешнему элементу, а, скорее, к корню текущего Shadow DOM.
После `:host(...)` мы можем указать селекторы и стили, которые нужно применить, если хозяин удовлетворяет тому или иному условию, например:
```html
<style>
:host p { color: green; }
:host(.important) p { color: red; }
</style>
```
Эти селекторы сработают для `<p>` внутри Shadow DOM, причём второй -- только если у хозяина стоит класс `important`.
</li>
<li>**`:host-context(селектор хозяина)` выбирает элемент-хозяин, если какой-либо из его родителей удовлетворяет селектору.**
Например:
```css
:host-context(h1) p {
/* селектор сработает для p, если хозяин находится внутри h1 */
}
```
Это используется для расширенной темизации, теперь уже не только в зависимости от его атрибутов, но и от того, внутри каких элементов он находится.
</li>
</ul>
Пример использования селектора `:host()` для темизации содержимого:
```html
<!--+ run autorun -->
*!*
<p class="message info">Доброе утро, страна!</p>
*/!*
*!*
<p class="message warning">Внимание-внимание! Говорит информбюро!</p>
*/!*
<template id="tmpl">
<style>
.content {
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
*!*
:host(.info) .content {
color: green;
}
:host(.warning) .content {
color: red;
}
*/!*
</style>
<div class="content"><content></content></div>
</template>
<script>
var elems = document.querySelectorAll('p.message');
elems[0].createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
elems[1].createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
</script>
```
### Стиль для содержимого <content>
Тег `<content>` не меняет DOM, а указывает, что где показывать. Поэтому если элемент изначально находится в элементе-хозяине -- внешний документ сохраняет к нему доступ.
К нему будут применены стили и сработают селекторы, всё как обычно.
Например, здесь применится стиль для `<span>`:
```html
<!--+ run -->
<style>
*!*
span { text-decoration: underline; }
*/!*
</style>
<p id="elem"><span>Доброе утро, страна!</span></p>
<template id="tmpl">
<h3><content></content></h3>
<p>Привет из подполья!</p>
</template>
<script>
elem.createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
</script>
```
В примере выше заголовок "Доброе утро, страна!", который пришёл как `<span>` из внешнего документа, будет подчёркнут,
...Но, поскольку эти узлы показываются внутри Shadow DOM, то ему тоже может понадобится к ним доступ.
**Для обращения к "содержимому" `<content>` используется псевдоэлемент `::content`.**
Например, `content[select="h1"]::content span` найдёт элемент `<content select="h1">` и его содержимом* отыщет `<span>`.
Селектор `::content` подразумевает `*::content`, так что `::content span` стилизует все `<span>` внутри всех `<content>`.
Например:
```html
<!--+ run -->
<style>
*!*
span { text-decoration: underline; }
*/!*
</style>
<p id="elem"><span>Доброе утро, страна!</span></p>
<template id="tmpl">
<style>
*!*
::content span { color: green; }
*/!*
</style>
<h3><content></content></h3>
<p>Привет из подполья!</p>
</template>
<script>
elem.createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
</script>
```
Если запустить пример выше, то текст внутри `<h3>` станет зелёным и подчёркнутым одновременно.
Приоритет селекторов расчитывается по [обычным правилам специфичности](http://www.w3.org/TR/css3-selectors/#specificity), если же приоритеты стилей на странице и в Shadow DOM и на странице равны, то, как описано в секции [Cascading](http://dev.w3.org/csswg/css-scoping/#cascading), побеждает страница, а для `!important`-стиля побеждает Shadow DOM.
</li>
</ul>
[summary]
Если обобщить -- инкапсуляция Shadow DOM имеет односторонний характер:
<ul>
<li>Изнутри Shadow DOM можно стилизовать только сам Shadow DOM и узлы, показываемые в `<content>`.</li>
<li>Со страницы можно иметь доступ и стилизовать элементы, изначально находящиеся внутри хозяина -- напрямую, а узлы внутри Shadow DOM -- при помощи селекторов `::shadow` и `/deep/`.</li>
</ul>
[/summary]
## Итого
@ -493,16 +164,12 @@ Shadow DOM -- это средство для создания отдельног
<ul>
<li>Ряд браузерных элементов со сложной структурой уже имеют Shadow DOM.</li>
<li>Можно создать Shadow DOM внутри любого элемента вызовом `elem.createShadowRoot()`. В дальнейшем его корень будет доступен как `elem.shadowRoot`.</li>
<li>Можно создать Shadow DOM внутри любого элемента вызовом `elem.createShadowRoot()`. В дальнейшем его корень будет доступен как `elem.shadowRoot`. У встроенных элементов он недоступен.</li>
<li>Как только у элемента появляется Shadow DOM, его изначальное содержимое скрывается. Теперь показывается только Shadow DOM, который может указать, какое содержимое хозяина куда вставлять, при помощи элемента `<content>`. Можно указать селектор `<content select="селектор">` и размещать разное содержимое в разных местах Shadow DOM.</li>
<li>Стили и `querySelector`, объявленные внутри Shadow DOM, по умолчанию относятся только к его содержимому, могут обращаться к содержимому `<content>`, но не к основной странице.</li>
<li>Стили и `querySelector` с внешней страницы могут преодолевать границу между DOM при помощи селекторов `::shadow` и `/deep/`.</li>
<li>Элемент `<content>` перемещает содержимое исходного элемента в Shadow DOM только визуально, в структуре DOM оно остаётся на тех же местах.</li>
</ul>
Спецификации, затрагивающие Shadow DOM:
Подробнее спецификация описана по адресу [](http://w3c.github.io/webcomponents/spec/shadow/).
Далее мы рассмотрим работу с шаблонами, которые также являются частью платформы Web Components и не заменяют существующие шаблонные системы, но дополняют их важными встроенными в браузер возможностями.
<ul>
<li>[Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/) -- самая полная спецификация по свойствам и методам Shadow DOM, деталям обработки событий.</li>
<li>[Introduction to Web Components](http://w3c.github.io/webcomponents/explainer/) -- обо всём понемногу.</li>
<li>[CSS Scoping](http://drafts.csswg.org/css-scoping/) -- спецификация по CSS-селекторам, в том числе Shadow DOM.</li>
</ul>

View file

@ -1,4 +0,0 @@
<link rel="import" href="ui-tabs.html">
<link rel="import" href="ui-dialog.html">
...

View file

@ -1,4 +0,0 @@
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script> alert('Библиотеки подключены!'); </script>

View file

@ -1,2 +0,0 @@
<link rel="import" href="libs.html">
...template и код для диалогов...

View file

@ -1,2 +0,0 @@
<link rel="import" href="libs.html">
...template и код для табов...

View file

@ -1,7 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<link rel="import" id="link" href="timer.html">
</head>
<body></body>
</html>

View file

@ -1,25 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<style>
#timer { color: red; }
</style>
</head>
<body>
<p id="timer">0</p>
<script>
var localDocument = document.currentScript.ownerDocument;
var timer = localDocument.getElementById('timer');
var timerId = setInterval(function() {
timer.innerHTML++;
}, 1000);
document.body.appendChild(timer);
</script>
</body>
</html>

View file

@ -0,0 +1,58 @@
# Шаблоны <template>
Элемент `<template>` предназначен для хранения "образца" разметки, невидимого и предназначенного для вставки куда-либо.
Конечно, есть много способов записать произвольный невидимый текст в HTML. В чём же особенность `<template>`?
Его отличие от обычных тегов в том, что его содержимое обрабатывается особым образом. Оно не только скрыто, но и считается находящимся вообще "вне документа". А при вставке автоматически "оживает", выполняются из него скрипты, начинает проигрываться видео и т.п.
[cut]
Содержимое тега `<template>`, в отличие, к примеру, от шаблонов или `<script type="неизвестный тип">`, обрабатывается браузером. А значит, должно быть корректным HTML.
Оно доступно как `DocumentFragment` в свойстве тега `content`. Предполагается, что мы, при необходимости, возьмём `content` и вставим, куда надо.
## Вставка шаблона
Пример вставки шаблона `tmpl` в Shadow DOM элемента `elem`:
```html
<!--+ run autorun="no-epub" -->
<p id="elem">Доброе утро, страна!</p>
<template id="tmpl">
<h3><content></content></h3>
<p>Привет из подполья!</p>
<script> document.write('...document.write:Новость!'); </script>
</template>
<script>
var root = elem.createShadowRoot();
root.appendChild( tmpl.content.cloneNode(true) );
</script>
```
У нас получилось, что:
<ol>
<li>В элементе `#elem` содержатся данные в некоторой оговорённой разметке.</li>
<li>Шаблон `#tmpl` указывает, как их отобразить, куда и в какие HTML-теги завернуть содержимое `#elem`.</li>
<li>Здесь шаблон показывается в Shadow DOM тега. Технически, это не обязательно, шаблон можно использовать и без Shadow DOM, но тогда не сработает тег `<content>`.</li>
</ol>
Можно также заметить, что в скрипт из шаблона выполнился. Это важнейшее отличие вставки шаблона от вставки HTML через `innerHTML` и от обычного `DocumentFragment`.
Также мы вставили не сам `tmpl.content`, а его клон. Это обычная практика, чтобы можно было использовать один шаблон много раз.
## Итого
Тег `<template>` не призван заменить системы шаблонизации. В нём нет хитрых операторов итерации, привязок к данным.
Его основная особенность -- это возможность вставки "живого" содержимого, вместе со скриптами.
И, конечно, мелочь, но удобно, что он не требует никаких библиотек.

View file

@ -0,0 +1,291 @@
# Стили и селекторы
Стилизация Shadow DOM покрывается более общей спецификацией ["CSS Scoping"](http://drafts.csswg.org/css-scoping/).
По умолчанию стили внутри Shadow DOM относятся только к его содержимому.
[cut]
Например:
```html
<!--+ run autorun="no-epub" -->
<p>Жили мы тихо-мирно, и тут...</p>
<p id="elem">Доброе утро, страна!</p>
<template id="tmpl">
*!*
<style> p { color: red; } </style>
*/!*
<h3><content></content></h3>
<p>Привет из подполья!</p>
</template>
<script>
var root = elem.createShadowRoot();
root.appendChild( tmpl.content.cloneNode(true) );
</script>
```
При запуске окрашенным в красный цвет окажется только `<p>` внутри Shadow DOM. Обратим внимание, окрасился именно тот элемент, который находится непосредственно в Shadow DOM. А элементы, которые отображены в Shadow DOM при помощи `<content>`, этот стиль не получили -- у них есть свои, заданные на внешней странице.
## Внешний стиль для Shadow DOM
Граница между Shadow DOM и основным DOM, хоть и существует, но при помощи специальных селекторов её можно переходить.
Если нужно с основной страницы стилизовать или выбрать элементы внутри Shadow DOM, то можно использовать селекторы:
<ul>
<li>**`::shadow` -- выбирает корень Shadow DOM.**
Выбранный элемент сам по себе не создаёт CSS box, но служит отправной точкой для дальшейшей выборки уже внутри дерева Shadow DOM.
Например, `#elem::shadow > div` найдёт внутри Shadow DOM `#elem` элементы `div` первого уровня.</li>
<li>**`>>>` -- особого вида CSS-селектор для всех элементов Shadow DOM, который полностью игнорирует границы между DOM'ами, включая вложенные подэлементы, у которых тоже может быть свой Shadow DOM.**
Например, `#elem >>> span` найдёт все `span` внутри Shadow DOM `#elem`, но кроме того, если в `#elem` есть подэлементы, у которых свой Shadow DOM, то оно продолжит поиск в них.
Вот пример, когда внутри одного Shadow DOM есть `<input type="date">`, у которого тоже есть Shadow DOM:
```html
<!--+ run -->
<style>
#elem::shadow span {
/* для span только внутри Shadow DOM #elem */
border-bottom: 1px dashed blue;
}
#elem >>> * {
/* для всех элементов внутри Shadow DOM #elem и далее внутри input[type=date] */
color: red;
}
</style>
<p id="elem"></p>
<script>
var root = elem.createShadowRoot();
root.innerHTML = "<span>Текущее время:</span> <input type='date'>";
</script>
```
</li>
<li>Кроме того, на Shadow DOM действует обычное CSS-наследование, если свойство поддерживает его по умолчанию.
В этом примере CSS-стили для `body` наследуются на внутренние элементы, включая Shadow DOM:
```html
<!--+ run autorun="no-epub" -->
<style>
body {
color: red;
font-style: italic;
}
</style>
<p id="elem"></p>
<script>
elem.createShadowRoot().innerHTML = "<span>Привет, мир!</span>";
</script>
```
Внутренний элемент станет красным курсивом.
</li>
</ul>
[warn header="Нельзя получить содержимое встроенных элементов"]
Описанные CSS-селекторы можно использовать не только в CSS, но и в `querySelector`.
Исключением являются встроенные элементы типа `<input type="date">`, для которых CSS-селекторы работают, но получить их содержимое нельзя.
Например:
```html
<!--+ run -->
<p id="elem"></p>
<script>
var root = elem.createShadowRoot();
root.innerHTML = "<span>Текущее время:</span> <input type='date'>";
// выберет только span из #elem
// вообще-то, должен выбрать span и из вложенных Shadow DOM,
// но для встроенных элементов - не умеет
alert(document.querySelectorAll('#elem::shadow span').length); // 1
</script>
```
[/warn]
## Стиль в зависимости от хозяина
Следующие селекторы позволяют изнутри Shadow DOM выбрать внешний элемент ("элемент-хозяин"):
<ul>
<li>`:host` выбирает элемент-хозяин, в котором, живёт Shadow DOM.
Хозяин :host выбирается в именно в контексте Shadow DOM.
То есть, это доступ не к внешнему элементу, а, скорее, к корню текущего Shadow DOM.
После `:host` мы можем указать селекторы и стили, которые нужно применить, если хозяин удовлетворяет тому или иному условию, например:
```html
<style>
:host > p { color: green; }
</style>
```
Этот селектор сработает для `<p>` первого уровня внутри Shadow DOM.
</li>
<li>`:host(селектор хозяина)` выбирает элемент-хозяин, если он подходит под селектор.
Этот селектор используется для темизации хозяина "изнутри", в зависимости от его классов и атрибутов. Он отлично добавляет просто `:host`, например:
```css
:host p { color: green; }
:host(.important) p { color: red; }
```
Здесь параграфы будут иметь `color:green`, но если у хозяина класс `.important`, то `color:red`.
</li>
<li>`:host-context(селектор хозяина)` выбирает элемент-хозяин, если какой-либо из его родителей удовлетворяет селектору, например:
```css
:host-context(h1) p {
/* селектор сработает для p, если хозяин находится внутри h1 */
}
```
Это используется для расширенной темизации, теперь уже не только в зависимости от его атрибутов, но и от того, внутри каких элементов он находится.
</li>
</ul>
Пример использования селектора `:host()` для разной расцветки Shadow DOM-сообщения, в зависимости от того, в каком оно `<p>`:
```html
<!--+ run autorun="no-epub" -->
*!*
<p class="message info">Доброе утро, страна!</p>
*/!*
*!*
<p class="message warning">Внимание-внимание! Говорит информбюро!</p>
*/!*
<template id="tmpl">
<style>
.content {
min-height: 20px;
padding: 19px;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
}
*!*
:host(.info) .content {
color: green;
}
:host(.warning) .content {
color: red;
}
*/!*
</style>
<div class="content"><content></content></div>
</template>
<script>
var elems = document.querySelectorAll('p.message');
elems[0].createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
elems[1].createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
</script>
```
## Стиль для content
Тег `<content>` не меняет DOM, а указывает, что где показывать. Поэтому если элемент изначально находится в элементе-хозяине -- внешний документ сохраняет к нему доступ.
К нему будут применены стили и сработают селекторы, всё как обычно.
Например, здесь применится стиль для `<span>`:
```html
<!--+ run autorun="no-epub" -->
<style>
*!*
span { text-decoration: underline; }
*/!*
</style>
<p id="elem"><span>Доброе утро, страна!</span></p>
<template id="tmpl">
<h3><content></content></h3>
<p>Привет из подполья!</p>
</template>
<script>
elem.createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
</script>
```
В примере выше заголовок "Доброе утро, страна!", который пришёл как `<span>` из внешнего документа, будет подчёркнут,
Итак, стили основного DOM-дерева применяются, всё в порядке.
Но что, если Shadow DOM тоже "имеет виды" на `<content>` и хочет стилизовать вставленное? Это тоже возможно.
**Для обращения к "содержимому" `<content>` из стилей внутри Shadow DOM используется псевдоэлемент `::content`.**
Например, изнутри Shadow DOM селектор `content[select="h1"]::content span` найдёт элемент `<content select="h1">` и его содержимом* отыщет `<span>`.
В примере ниже селектор `::content span` стилизует все `<span>` внутри всех `<content>`:
```html
<!--+ run -->
<style>
*!*
span { text-decoration: underline; }
*/!*
</style>
<p id="elem"><span>Доброе утро, страна!</span></p>
<template id="tmpl">
<style>
*!*
::content span { color: green; }
*/!*
</style>
<h3><content></content></h3>
<span>Привет из подполья!</span>
</template>
<script>
elem.createShadowRoot().appendChild( tmpl.content.cloneNode(true) );
</script>
```
Текст внутри `<h3>` -- зелёный и подчёркнутый одновременно, но стилизуется именно тот `<span>`, который показан в `<content>, а тот, который просто в Shadow DOM -- нет.
Приоритет селекторов расчитывается по [обычным правилам специфичности](http://www.w3.org/TR/css3-selectors/#specificity), если же приоритеты стилей на странице и в Shadow DOM и на странице равны, то, как описано в секции [Cascading](http://dev.w3.org/csswg/css-scoping/#cascading), побеждает страница, а для `!important`-стиля побеждает Shadow DOM.
## Итого
По умолчанию стили и селекторы из DOM-дерева действуют только на те элементы, в которых сами находятся.
Границу можно преодолевать, причём проще, естественно, от родителя к Shadow DOM, чем наоборот:
<ul>
<li>Снаружи можно выбирать и стилизовать элементы внутри Shadow DOM -- при помощи селекторов `::shadow` и `>>>`.</li>
<li>Изнутри Shadow DOM можно стилизовать не только то, что изначально в Shadow DOM, но и узлы, показываемые в `<content>`.</li>
<li>Также можно ставить стиль в зависимость от хозяиня при помощи селекторов `::host`, `::host-context`, но выбирать и стилизовать произвольные теги внутри хозяина нельзя.</li>
</ul>

View file

@ -1,2 +0,0 @@
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="http://code.jquery.com/ui/1.10.4/jquery-ui.js"></script>

View file

@ -16,9 +16,11 @@
Это хорошо, когда нужно действительно в одной странице отобразить содержимое другой.
А что, если нужно встроить другой документ как естественную часть текущего? С единым скриптовым пространством, едиными стилями. И желательно не иметь проблем с разными доменами: если уж мы действительно хотим подключить HTML с одного домена в страницу на другом -- мы должны иметь возможность это сделать без "плясок с бубном".
А что, если нужно встроить другой документ как естественную часть текущего? С единым скриптовым пространством, едиными стилями, но при этом -- другой документ.
Именно для этого предназначен `<link rel="import" href="...">`.
Например, это нужно для подгрузки внешних частей документа (веб-компонент) снаружи. И желательно не иметь проблем с разными доменами: если уж мы действительно хотим подключить HTML с одного домена в страницу на другом -- мы должны иметь возможность это сделать без "плясок с бубном".
Иначе говоря, `<link rel="import">` -- это аналог `<script>`, но для подключения не только скриптов, а документов, с шаблонами, библиотеками, веб-компонентами и т.п. Всё станет понятнее, когда мы посмотрим детали.
## Пример вставки
@ -29,7 +31,7 @@
```
<ul>
<li>В отличие от `<iframe>` тег `<link rel="import">` может быть в любом месте документа.</li>
<li>В отличие от `<iframe>` тег `<link rel="import">` может быть в любом месте документа, даже в `<head>`.</li>
<li>При вставке через `<iframe>` документ показывается внутри фрейма. В случае с `<link rel="import">` это не так, по умолчанию документ вообще не показывается.</li>
</ul>
@ -39,65 +41,43 @@
Мы сами решаем, где и когда его вставить.
Например:
[iframe src="import-show" link edit height="60" border="1"]
Основной документ:
В примере ниже `<link rel="import" href="timer.html">` подключает документ `timer.html` и, после его загрузки, вызывает функцию `show`. Эта функция через `link.import.querySelector('time')` выбирает интересующую часть подгруженного документа и вставляет её в текущий:
```html
<!--+ src="index.html" -->
<!--+ src="import-show/index.html" -->
```
Важные детали:
<ul>
<li>Загрузка осуществляется асинхронно, для того чтобы поймать момент загрузки -- используется событие `onload`, для ошибки -- `onerror`.</li>
<li>Подгруженный документ доступен как `link.import`. Это полноценный HTML-документ.</li>
</ul>
Файл `timer.html`:
В файле `timer.html` находится элемент и скрипт, который его "оживляет":
```html
<!--+ src="timer.html" -->
<!--+ src="import-show/timer.html" -->
```
[codetabs src="import-show" height=350]
Важные детали:
<ul>
<li>После загрузки все скрипты в подключённом `timer.html` выполняются в контексте основной страницы, так что `timer` и другие переменные станут глобальными переменными страницы.</li>
<li>Переменная `document` -- это документ основной страницы. Для доступа к импортированному, то есть текущему документу его можно получить как `document.currentScript.ownerDocument`.</li>
<li>Таймер в загруженном документе начинает работать сразу, новый документ активен, хотя до переноса узлов в основной документ этого не видно.</li>
<li>Переменная `document` -- это документ основной страницы. Для доступа к импортированному, то есть текущему документу изнутри `timer.html` его можно получить как `document.currentScript.ownerDocument`.</li>
<li>Таймер в загруженном документе начинает работать сразу, новый документ оживает сразу после загрузки, хотя до переноса узлов в основной документ этого может быть и не видно.</li>
</ul>
В примере выше содержимым импорта управлял основной документ, но `timer.html` мог бы и показать сам себя вызовом `document.body.appendChild(timer)` или вызвать функцию с внешнего документа, так как у них единая область видимости. Тогда не понадобился бы никакой `onload`.
Ещё пример вставки:
Ещё пример вставки, на этот раз документ только подключает `<link>`, а таймер вставляет себя сам:
[iframe src="import-style" link edit height="60" border="1"]
[codetabs src="import-style" height="200"]
Основной документ:
```html
<!--+ src="index.html" -->
```
Сейчас он просто загружает `timer.html` и всё. А вставит себя импорт сам.
Файл `timer.html`:
```html
<!--+ src="timer.html" -->
```
**Обратим внимание -- стили импорта попадают в контекст страницы.**
В примере выше импорт добавил и стиль для `#timer` и сам элемент.
Обратим внимание -- стили импорта попадают в контекст страницы. В примере выше импорт добавил и стиль для `#timer` и сам элемент.
## Веб-компоненты
Импорт задуман как часть платформы веб-компонент.
Импорт задуман как часть платформы Web Components.
Предполагается, что главный документ может импортировать файлы-определения, в которых будут все необходимые HTML, JS и CSS для элементов:
Предполагается, что главный документ может импортировать файлы-определения, в которых будут все необходимые HTML, JS и CSS для элементов, а затем использовать их.
Файл `index.html`:
Пример:
```html
<link rel="import" href="ui-tabs.html">
@ -115,15 +95,6 @@
Если файл `libs.html` импортирован два раза, то CSS и скрипты из него подключатся и выполнятся ровно один раз.
Файл `libs.html`:
```html
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script> alert('Библиотеки подключены!'); </script>
```
Это можно использовать, чтобы не подгружать одинаковые зависимости много раз. И сама страница и её импорты, и их подимпорты, и так далее, могут подключать `libs.html` без опасения лишний раз перезагрузить и выполнить скрипты.
Например:
@ -137,7 +108,7 @@
```
</li>
<li>`ui-tabs.html`:
<li>`ui-tabs.html` подключает `libs.html`:
```html
<link rel="import" href="libs.html">
@ -146,7 +117,7 @@
</li>
<li>`ui-dialog.html`:
<li>`ui-dialog.html` также использует `libs.html`:
```html
<link rel="import" href="libs.html">
@ -156,9 +127,8 @@
</li>
</ul>
[edit src="import-libs"/]
Открыв пример, вы увидите, что скрипты `libs.html` сработают один раз.
Файл `libs.html` при этом будет подключен только один раз. Это позволяет не бояться лишнего дублирования библиотек, используемых при описании множества компонент.
## Итого
@ -168,6 +138,6 @@
<li>Скриптовое пространство и стили со страницей будут общие.</li>
<li>Документ DOM -- отдельный, он доступен как `link.import` снаружи, а из внутреннего скрипта -- через `document.currentScript.ownerDocument`. Можно без проблем переносить элементы из главного документа в импорт и наоборот.</li>
<li>Импорты могут содержать другие импорты.</li>
<li>Если какой-то URL импортируется повторно -- подключается уже готовый документ, без повторного выполнения скриптов в нём. Это можно использовать для удобного управления зависимостями.</li>
<li>Если какой-то URL импортируется повторно -- подключается уже готовый документ, без повторного выполнения скриптов в нём. Это позволяет избежать дублирования при использовании одной библиотеки во множестве мест.</li>
</ul>

View file

@ -5,8 +5,8 @@
<script>
function show() {
var p = link.import.querySelector('p')
document.body.appendChild(p);
var time = link.import.querySelector('time')
document.body.appendChild(time);
};
</script>

View file

@ -2,7 +2,7 @@
<html>
<body>
<p id="timer">0</p>
<time id="timer">0</time>
<script>
var localDocument = document.currentScript.ownerDocument;

View file

@ -1,6 +1,6 @@
# Веб-компонент в сборе
В этой главе мы посмотрим на расширенный пример веб-компонента, включающий в себя описанные ранее технологии: Custom Elements, Shadow DOM, CSS Scoping и, конечно же, Imports.
В этой главе мы посмотрим на итоговый пример веб-компонента, включающий в себя описанные ранее технологии: Custom Elements, Shadow DOM, CSS Scoping и, конечно же, Imports.
[cut]
@ -31,7 +31,7 @@
Этот код ничем не отличается от использования обычного элемента, поэтому перейдём дальше, к содержимому `ui-message.html`
### Шаблон
## Шаблон для ui-message
Файл `ui-message.html` можно начать с шаблона:
@ -64,21 +64,21 @@
</template>
```
Этот шаблон рисует `<div class="content">` и заполняет его содержимым элемента.
Этот шаблон рисует `<div class="content">` и заполняет его содержимым элемента-хозяина.
Важные детали:
<ul>
<li>Самое важное правило здесь `:host { display:block }`.
Оно обязательно! . Это правило задаёт, что элемент-хозяин, то есть `<ui-message>`, будет иметь `display:block`. По умолчанию у элементов стоит `display: inline`, а это значит, что ни ширину ни `margin` указать не получится
Обратим внимание -- `display` относится к базовым свойствам элемента, поэтому задаётся в Shadow DOM. Внешняя страница надстраивает свои свойства поверх "стандартных".</li>
<li>Последующие правила `:host(.info) .content` и `:host(.warning) .content` стилизуют уже не хозяина, а элемент `.content` в зависимости от того, какой на хозяине класс.</li>
Оно обязательно! . Это правило задаёт, что корень DOM-дерева будет иметь `display:block`. По умолчанию `:host` не создаёт CSS-блок, а это значит, что ни ширину ни отступы указать не получится.</li>
<li>Последующие правила `:host(.info) .content` и `:host(.warning) .content` стилизуют содержимое в зависимости от того, какой на хозяине класс.</li>
</ul>
### Скрипт
## Скрипт для ui-message
<script>
В файле `ui-message.html` мы создадим новый элемент `<ui-message>`:
```js
// (1) получить шаблон
var localDocument = document.currentScript.ownerDocument;
var tmpl = localDocument.getElementById('tmpl');
@ -95,8 +95,7 @@ MessageProto.createdCallback = function() {
document.registerElement('ui-message', {
prototype: MessageProto
});
</script>
[/html]
```
Все компоненты этого кода мы подробно разбирали ранее:
@ -106,13 +105,15 @@ document.registerElement('ui-message', {
<li>С момента регистрации все уже существующие элементы `<ui-message>` будут превращены в описанные здесь. И будущие, конечно, тоже.</li>
</ol>
В действии:
Компонент в действии:
[iframe src="message" border="1" edit link/]
[codetabs src="message" height=200]
## Компонент ui-slider
## Компонент ui-slider с jQuery
Теперь создадим слайдер с использованием библиотеки [jQuery UI](http://jqueryui.com).
Компонент может использовать и внешние библиотеки.
Для примера создадим слайдер с использованием библиотеки [jQuery UI](http://jqueryui.com).
Компонент `ui-slider` будет показывать слайдер с минимальным и максимальным значением из атрибутов `min/max` и генерировать событие `slide` при его перемещении.
@ -132,7 +133,9 @@ document.registerElement('ui-message', {
<div id="value">0</div>
```
Файл `ui-slider.html` мы разберём по частям.
## Файл компонента ui-slider
Файл `ui-slider.html`, задающий компонент, мы разберём по частям.
### Заголовок
@ -153,22 +156,21 @@ document.registerElement('ui-message', {
Содержимое `jquery.html`:
```html
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="http://code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<!--+ src="ui-slider/jquery.html" -->
```
### Шаблон
Шаблон будет помещён в Shadow DOM. В нём должны быть стили и элементы, необходимые слайдеру.
Конкретно для слайдера из разметки достаточно одного элемента `<div id="slider"></div>, который затем будет обработан jQuery UI.
Конкретно для слайдера из разметки достаточно одного элемента `<div id="slider"></div>`, который затем будет обработан jQuery UI.
Кроме того, в шаблоне должны быть стили:
```html
<template id="tmpl">
<style>
@import url(http://code.jquery.com/ui/1.10.4/themes/ui-lightness/jquery-ui.css);
@import url(http://code.jquery.com/ui/1.11.3/themes/ui-lightness/jquery-ui.css);
:host {
display: block;
@ -182,7 +184,7 @@ document.registerElement('ui-message', {
Скрипт для нового элемента похож на тот, что делали раньше, но теперь он использует jQuery UI для создания слайдера внутри своего Shadow DOM.
Для его понимания желательно знать jQuery, но я намеренно свёл использование этой библиотеки к минимуму. Ниже будет описание по шагам.
Для его понимания желательно знать jQuery, хотя в коде ниже я намеренно свёл использование этой библиотеки к минимуму.
```js
var localDocument = document.currentScript.ownerDocument;
@ -230,7 +232,7 @@ document.registerElement('ui-slider', {
Полный код с примером:
[iframe src="ui-slider" edit link border="1"/]
[codetabs src="ui-slider" height=300]
Его можно далее улучшать, например добавить геттер и сеттер для значения `value`:
@ -247,9 +249,9 @@ Object.defineProperty(SliderProto, 'value', {
Если добавить этот код, то к значению `<ui-slider>` можно будет обращаться как `elem.value`, аналогично всяким встроенным `<input>`.
**Попробуйте пример выше. Он не совсем работает!**
## Проблема с jQuery
Слайдер прокручивается первый раз, но второй раз он как-то странно "прыгает".
Попробуйте пример выше. Он не совсем работает. Слайдер прокручивается первый раз, но второй раз он как-то странно "прыгает".
Чтобы понять, почему это происходит, я заглянул в исходники jQuery UI и, после отладки происходящего, натолкнулся на проблемный код.
@ -270,7 +272,7 @@ if ( !jQuery.contains( elem.ownerDocument, elem ) ) {
Получилось, что элемент не в документе и одновременно он имеет размеры. Такого разработчики jQuery не предусмотрели.
Можно, конечно, побежать исправлять jQuery, но давайте подумаем.
Можно, конечно, побежать исправлять jQuery, но давайте подумаем, может быть так оно и должно быть?
С точки зрения здравого смысла, Shadow DOM является частью текущего документа. Это соответствует и духу [текущей спецификации](http://w3c.github.io/webcomponents/spec/shadow/), где shadow tree рассматривается в контексте document tree.
@ -278,18 +280,19 @@ if ( !jQuery.contains( elem.ownerDocument, elem ) ) {
Почему же `false`? Причина проста -- описанный в [другом стандарте](http://www.w3.org/TR/dom/#dom-node-contains) механизм работы `contains` по сути состоит в проходе вверх от `elem` по цепочке `parentNode`, пока либо встретим искомый элемент, тогда ответ `true`, а иначе `false`. В случае с Shadow DOM этот путь закончится на корне Shadow DOM-дерева, оно ведь не является потомком хозяина. Метод `contains` не знает ничего про саму возможность Shadow DOM, поэтому и выходит, что результат `false`.
Так что срочно слать патчи в jQuery здесь рановато, скорее необходимо ещё подумать над стандартами.
Это один из тех небольших, но важных нюансов, которые показывают, почему стандарты всё ещё в разработке.
## Итого
<ul>
<li>С использованием современных технологий можно делать компоненты. Но это, всё же, дело будущего. Все стандарты находятся в процессе доработки, готовятся новые.</li>
<li>На текущий момент нельзя взять произвольную библиотеку, даже такую распространённую как jQuery, и работать с Shadow DOM с её использованием. Выше была продемонстрирована одна проблема, но возможны и другие.</li>
<li>Можно использовать произвольную библиотеку, такую как jQuery, и работать с Shadow DOM с её использованием. Но возможны проблемки. Выше была продемонстрирована одна из них, могут быть и другие.</li>
</ul>
Самый известный из полифиллов на тему веб-компонент -- это [Polymer](http://www.polymer-project.org). Он старается их эмулировать по возможности кросс-браузерно, но пока что это довольно-таки сложно, в частности, необходима дополнительная разметка.
Текущее состояние веб-стандартов -- "взгляд в будущее". Наверно, будет здорово, когда оно наступит :)
Текущее состояние веб-стандартов -- "взгляд в будущее". Наверно, будет здорово, когда оно наступит.

View file

@ -0,0 +1,2 @@
<script src="http://code.jquery.com/jquery-2.1.3.js"></script>
<script src="https://code.jquery.com/ui/1.11.3/jquery-ui.js"></script>

View file

@ -7,7 +7,7 @@
<template id="tmpl">
<style>
@import url(http://code.jquery.com/ui/1.10.4/themes/ui-lightness/jquery-ui.css);
@import url(http://code.jquery.com/ui/1.11.3/themes/ui-lightness/jquery-ui.css);
:host {
display: block;
@ -32,8 +32,8 @@ SliderProto.createdCallback = function() {
var self = this;
this.$slider.slider({
min: this.getAttribute('min') || 0,
max: this.getAttribute('max') || 100,
min: +this.getAttribute('min') || 0,
max: +this.getAttribute('max') || 100,
value: this.getAttribute('value') || 0,
slide: function() {
var event = new CustomEvent("slide", {