这份规范描述了一种把多个 DOM 树结合进入一个层级结构中,并且这些树可以在同一份文档中交互的方法。这使得 DOM 结构能够更好的组合。

一致性

所有的图表、示例、注意事项,都和明示的非正式性章节一样是非正式性的。其余的部分都是正式性的。

关键字“必须”、“禁止”、“要求”、“会”、“不会”、“应该”、“不应该”、“推荐”、“可以”和“可选的”等出现在文档的正式性部分中都应该遵循 [[!RFC2119]] 中的解释。但考虑到可读性,这些词汇在规范中并没有以大写形式出现。

为了帮助分层,且避免在规范的不同部分之间产生循环依赖,该文档由三个连贯的叙述内容组成:

  1. 搭建该规范的基础和背景,
  2. 先解释概念模型和算法,然后
  3. 通过 DOM 接口和 HTML 元素表达该模型。

某种意义上说,这三个部分分别相当于是 数学——推理环境、物理——对概念的逻辑推理、和机械——对实践应用的推理。

任何遵循该规范的用户代理必须根据概念模型决定的交互或状态,都会以算法的方式进行捕获。该算法被定义为等价处理的术语。这里的等价处理是强制约束的算法实现,要求相同的输入通过用户代理实现和规范算法的输出是完全一致的。

概念

介绍

请移步至 Shadow DOM 101 阅读这份非正式性介绍。

Shadow 树

文档树是指结点文档结点树 [[!DOM]]。

任何元素都可以有一个包含 0 个或更多的结点树关联有序列表

如果一个结点树是一个元素的关联列表的成员,则称该元素宿主结点树

Shadow 宿主是指宿主一个或多个结点树的元素。

Shadow 树是指被 shadow 宿主宿主结点树

Shadow 根是指 shadow 树的结点

如果超过一个 shadow 树宿主于相同的 shadow 宿主,则最近添加的 shadow 树被称为更年轻的 shadow 树同时最早添加的 shadow 树被称为更老的 shadow 树

如果没有比给定的 shadow 树更老的 shadow 树,则该 shadow 树被称为最老的 shadow 树

如果没有比给定的 shadow 树更年轻的 shadow 树,则该 shadow 树被称为最年轻的 shadow 树

更老的 shadow 根是指更老的 shadow 树的根结点。

更年轻的 shadow 根是指更年轻的 shadow 树的根结点。

最老的 shadow 根是指最老的 shadow 树的根结点。

最年轻的 shadow 根是指最年轻的 shadow 树的根结点。

方便起见,shadow 根提供了其自己的DOM 树存取器方法集合。除了 shadow 根的后代,没有结点可以访问这些方法。

树中树

树中树是指包含结点树

这里引入树中树的目的在于方便下面章节中的算法定义。

结点树被定义为结点之间的一套关系集合一样,树中树同样定义了结点树之间的一套关系集合:

一个 shadow 树的结点ownerDocument 属性必须引用该 shadow 树的 shadow 宿主文档

Window 对象中被命名的属性 [[!HTML]] 必须 访问该文档树中的结点

树中树的例子

一个树中树

如图,这里有七个结点树,分别命名为 A、B、C、D、E 和 F。 结点树 C、D 和 E 被宿主在同一个参与在结点树 A 上的 shadow 宿主。 结点树 C 是最老的 shadow 树。结点树 E 是最年轻的 shadow 树。 改图还存在下列关系:

  • A 的子树的有序列表是 [B, C]。
  • B 的子树的有序列表是 []。
  • C 的子树的有序列表是 [F, D]。
  • D 的子树的有序列表是 [E]。
  • E 的子树的有序列表是 []。

组合树

组合树是指由树中树里的多个结点树作为结点架构而成的一个结点树。详细的组合树结构算法会稍后被规范。

一个组合树

渲染一个文档树,或对其进行可视化展示时,组合树必须被替换作文档树

组合树必须渲染发生之前被更新。

分布

插入点

插入点是指一个被定义好的位置,当构造一个组合树的时候,别的结点树里的结点可以替换出现在这个结点的位置。

一个分布

分布是决定哪个结点出现在各个插入点的机制。分布的详细算法规范会在稍后看到。

内容插入点

内容插入点shadow 宿主分布子结点插入点。满足下列所有条件的内容元素就相当于一个内容插入点

Shadow 插入点

Shadow 插入点是会发布更老的 shadow 根的子节点插入点。满足下列所有条件的 shadow 元素就相当于一个 shadow 插入点

分布结果

每个树中树都有分布结果分布结果必须与下列描述等价

  1. 每个插入点有一个被称作已分布结点的有序列表,该列表由分布在该插入点结点组成。
  2. 每个非插入点结点有一个被称作目的插入点的有序列表,该列表由分布该结点插入点组成。

如果插入点 A结点 B目的插入点,则 AB最终目的

结点 A分布插入点 B 时,必然发生下列步骤:

当一个插入点是另一个 shadow 宿主的子结点时,需要特殊考虑。分布到其插入点结点会出现并相当于它们是分布上下文的 shadow 宿主的子结点。因此分布shadow 树结点可能已被分布自其父级树。

一个结点尽管被分布到了不止一个插入点,其在最终目的的组合树中只会出现一个。

一个重分布。在该图中,一个结点 child 1 被分布进了 insertion point 1。然后 child1 被重分布进了 insertion point 3Child 1 的目的插入点是 [insertion point 1, insertion point 3] 同时 insertion point 3child 1的最终目的。Insertion point 1insertion point 3 的被分布结点分别是 [child 1] 和 [child 1, child 3]。

分布算法

分布算法必须用来判定一个树中树分布结果必须等价于以下处理步骤:

输入
TREE-OF-TREES,一个树中树
输出
TREE-OF-TREES分布结果被更新
  1. 将所有归属于 TREE-OF-TREES结点分布结点目的插入点设为空
  2. ROOT-TREETREE-OF-TREES根树
  3. ROOT-TREE 作为输入,执行分布决议算法

分布决议算法必须用来判定一个给定的结点树及其后代树分布结果,且必须等价于以下处理步骤:

输入
NODE-TREE,一个结点树
输出
NODE-TREE包含的后代树分布结果会被更新
  1. 把每个参与 NODE-TREE 其中的 shadow 宿主定为 SHADOW-HOST,以树顺序
    1. SHADOW-HOST 作为输入执行池占据算法的结果设为 POOL
    2. 把每个 SHADOW-HOST 宿主shadow 树定为 SHADOW-TREE,按照从最年轻 shadow 树最老 shadow 树的顺序:
      1. SHADOW-TREEPOOL 作为输入执行池分布算法
    3. 把每个 SHADOW-HOST 宿主shadow 树定为 SHADOW-TREE,按照从最老 shadow 树最年轻 shadow 树的顺序:
      1. 把参与到 SHADOW-TREE 中的 shadow 插入点设为 SHADOW
      2. 如果存在这样的 SHADOW
        1. 如果 SHADOW-TREE 不是最老的 shadow 树
          1. 将相对于 SHADOW-TREE 更老的 shadow 数的根节点作为输入执行池占据算法的结果设为POOL
        2. 把每个 POOL 中的结点定为 CHILD
          1. CHILD 分布SHADOW
      3. SHADOW-TREE 作为输入递归执行分布决议算法

池占据算法必须用来占据一个给定结点的子结点,且必须必须等价于以下处理步骤:

输入
NODE,一个结点
输出
POOL,一个有序的结点列表
  1. POOL 设为一个空的有序列表。
  2. CHILD 的每个结点定为 NODE
    1. 如果 CHILD 是一个插入点
      1. 把所有 CHILD已分布结点中的结点添加到 POOL
    2. 否则:
      1. CHILD 添加到 POOL

池分布算法必须用来把一个池中的结点分布到其 shadow 树中的内容插入点中,且必须等价于以下处理步骤:

输入
SHADOW-TREE,一个 shadow 树
POOL,一个有序结点列表
输出
POOL 中的结点被分布到了该树中的内容插入点中。
  1. 把每个参与到 SHADOW-TREE 中的内容插入点定为 CONTENT,并按照树顺序:
    1. 把每个 POOL 里的结点定为NODE
      1. 如果 NODE 满足 CONTENT 的批评标准:
        1. NODE 分布CONTENT
        2. POOL 中移除 NODE
    2. 如果没有分布到 CONTENT 中的结点
      1. 把每个 CONTENT 的子节点定为 CHILD
        1. CHILD 分布CONTENT

如果没有结点被分布到一个内容插入点 CONTENT 中,则 CONTENT 的子结点会被分布到 CONTENT 中作为后备结点。

如果影响分布结果的任何条件发生改变,则分布结果必须先于任何对分布结果的使用被更新。

满足匹配标准

一个插入点匹配标准是一个复合选择器 [[!SELECTORS4]] 的集合。这些复合选择器被约束在了只包含这些单一选择器的范围内:

一个结点仅在下面的情形下满足匹配规则

  1. 所有集合中的复合选择器都只包含上述的单一选择器;且
  2. 结点至少匹配一个集合中的复合选择器或集合为空。

组合

组合树子节点算法必须用来判定一个组合树结点的子结点,且必须等价于以下处理步骤:

输入
NODE,一个参与到组合树中的结点
输出
CHILDREN,其组合树中的 NODE 的子节点。
  1. CHILDREN 设为空的有序结点列表
  2. 如果 NODE 是一个 shadow 宿主
    1. CHILD-POOL 设为 NODE 宿主最年轻 shadow 根的子节点。
  3. 否则:
    1. CHILD-POOL 设为 NODE 的子结点
  4. 对每个 CHILD-POOL 中的结点 CHILD
    1. 如果 CHILD 是一个插入点
      1. 对其插入点 CHILD分布结点中的每个结点 DISTRIBUTED-NODE:
        1. 如果 CHILDDISTRIBUTED-NODE最终目的,则把 DISTRIBUTED-NODE 添加到 CHILDREN
    2. 否则:
      1. CHILD 添加到 CHILDREN

对一个给定的树中树 TREE-OF-TREES 来说,由 TREE-OF-TREES 构造出的组合树必须等价于以下处理步骤:

事件

当一个Event(事件)在shadow tree中被dispatched(分发)时, 事件的路径要么穿过shadow trees要么在shadow root被终止. 其中一个例外情况就是mutation events(突发事件). mutation event types(突发事件) 一定 不能在shadow tree中被分发.

通常会被阻止的事件

following events(以下事件) 一定是通常会在node tree(节点树)root(根) node(节点)被阻止:

事件路径

event path calculation algorithm(事件路径算法)一定是被用来决定事件路径并且一定要通过下列步骤(相当于以下的步骤的过程):

输入
NODE(节点), 一个节点
EVENT(事件), 一个事件
输出
PATH(路径), 一个事件的路径, 一个事件目标的顺序列表
  1. PATH(路径)成为一个暂时为空的节点列表(有顺序的)
  2. CURRENT(当前的事件目标)作为NODE(节点)
  3. 增加CURRENT(当前的事件目标)的节点到PATH(路径)
  4. 如果存在CURRENT(当前的事件目标)则重复:
    1. 如果CURRENT(当前)目标插入点不为空:
      1. 对于每一个insertion point(插入点), INSERTION-POINT(插入点), 在CURRENT(当前的)destination insertion points(目标插入点):
        1. 如果INSERTION-POINT(插入点)是一个shadow insertion point(阴影插入点):
          1. SHADOW-ROOT作为INSERTION-POINT(插入点)root(根) node(节点)
          2. 如果SHADOW-ROOT不是最oldest shadow root(外层的阴影根):
            1. 增加比此根older shadow root(更外层的阴影根)相对于SHADOW-ROOTPATH(路径)
        2. 增加INSERTION-POINT(插入点)PATH(路径中)
      2. CURRENT(当前的事件目标)作为CURRENT(当前的事件目标)final destination(最终目标)
    2. 否则:
      1. 如果CURRENT(当前事件目标)是一个shadow root:
        1. 如果NODE(节点)CURRENT(当前事件的目标)在同一个node tree(节点树中)并且EVENT(事件)是需要 被阻止的事件:
          1. 停止使用此算法
        2. CURRENT(当前事件的目标)成为shadow host(阴影宿主),承载CURRENT(当前事件的目标)
        3. 增加CURRENT(当前事件的目标)PATH(路径中)
      2. 否则:
        1. CURRENT(当前的事件目标)作为CURRENT(当前事件的目标)parent(父) node(节点)
        2. 如果CURRENT(当前的事件目标)存在:
          1. 增加CURRENT(当前的事件目标)PATH(路径)

事件路径的示例

假设我们现在下列一个树:

一个示例树. 树的节点并没有关联到事件路径的示例中, 我们下面会解释, 目前先忽略这些树的节点.

这颗tree of trees(树)拥有以下的7棵node trees(节点树), 一棵document tree(文档树)和 6棵 shadow trees:

让我们假设这颗tree of trees(树)分布结果是:

在这种情况下, 如果一个事件在被分发到D上, 这个事件路径是:

[D, C, I, M, L, P, R, Q, O, N, K, J, H, G, U, T, S, F, E, X, W, V, B, A]

记录event path calculation algorithm(事件路径的算法) 是被设计用来实现以下目标:

  1. 如果这里有节点, CHILD(子节点)在事件路径中并且CHILD(子节点)拥有父节点, PARENT(父节点)在节点树中, 事件路径也应该包括 PARENT(父节点). PARENT(父节点)在事件路径中通常出现在CHILD(子节点)之后.
  2. 在每一个node tree(节点树)中节点的事件路径来自于一个直系祖先. 在每一个node tree(节点树)中是没有branch points(分支点)的.
>
一个事件路径和节点树的关系. 在图中, 在每个节点被展示出来的左分支代表在事件路径中每一个节点最初起始的位置. 在一个节点树中,一个父节点通常有大量比自身子元素更多的事件路径的起始位置.

这意味着如果我们获取一个node tree(节点树)的焦点并且忽视掉其他所有的 node trees(节点树), 事件路径将似乎看上就好像事件发生在我们所获取焦点的node tree(节点树)上. 这是一个非常重要的方面从某种意义上来说进行托管的阴影树在包含node tree(节点树)shadow host其中的情况下对于事件路径不具有任何影响 只要这个事件没有在descendant trees(子树)上被阻止.

举个例子, 通过document tree(文档树) 1的视图, 事件路径看上去将会是[D, C, B, A]. 通过shadow tree 2的视图, 事件路径看上去将会是[I, H, G, F, E]. 这种相似情况同样会应用于 node trees(节点树).

这也是值得我们指出的一种情况如果我们从事件路径中排除全部insertion points(插入点)shadow roots, 这种结果等价于包括所有的祖先节点在生成树中分发事件.

一个事件路径和composed tree(生成树)的关系. 事件路径使用了此例子中在左分支上个被展示出的节点同时composed tree(生成树)展示了右分支的节点. 如果我们从事件路上上排除所有insertion points(插入点)shadow roots, 这种情况等价于在composed tree(生成树)中包含所有有祖先的节点, D.

事件的重定向

在事件路径穿过大多数节点树的情况下, 事件信息包括事件的目标是被调整过的为了维持encapsulation(封装性). 事件retargeting(重定向)是一个当事件在每一个有祖先的节点上被分发时去计算相关联的目标. 一个relative target(相关联的目标)是一个node(节点),此给定了父元素的节点能最准确代表当前被分发事件的目标当保持封装性的时.

retargeting algorithm(重定向算法)被用来决定相关联的元素, 它must(必须) equivalent(等价)于经过下列的步奏:

输入
EVENT-PATH(事件路径), an event path(一个事件路径)
CURRENT-TARGET(当前的目标), 在某个地方是被事件监听所关联的一个node(节点).
Output
RELATIVE-TARGET(相关联的目标), 调整后的目标
  1. CURRENT-TARGET-TREE(当前目标树)成为node tree(节点树),CURRENT-TARGET(当前目标)其中
  2. ORIGINAL-TARGET(源目标)成为EVENT-PATH(路径)中的第一项
  3. ORIGINAL-TARGET-TREE(源目标树)成为node tree(节点树),ORIGINAL-TARGET(源目标)其中
  4. RELATIVE-TARGET-TREE(相关联的目标树)最小(CURRENT-TARGET-TREE(当前目标树)ORIGINAL-TARGET-TREE(源目标树)之间拥有共同的inclusive ancestor tree(包括祖先的元素)
  5. RELATIVE-TARGET(相关联的目标) 成为第一个在EVENT-PATH(事件路径)node(节点),此节点需要满足以下条件:
    1. node(节点)必须RELATIVE-TARGET-TREE(相关联的树)

重定向过程必须发送在一个事件被分发之前.

重定向的 关系目标

一些事件有一个relatedTarget(关联目标) [[!DOM-Level-3-Events]]属性, 这个属性是一个不是事件目标的node(节点), 但是和事件有关系.

举个实例, 一个mouseover事件的relatedTarget(关联目标)可能拥有一个node(节点)来自于鼠标被移动到事件target(目标).在relatedTarget(相关联目标)是一个shadow tree的情况下, 符合标准的UAs 必须不能把真是的值泄露出这颗树. 在relatedTarget(相关联目标)target(目标)都属于相同shadow tree的一部分, 符合标准的UAs 必须 阻止在阴影根上的事件去避免出现在mouseovermouseout事件触发来自于于同一节点的错误.

因此, 如果一个事件有 relatedTarget(相关联目标), 它的值和事件分发的范围 必须被调整. 通常来说:

  1. 对于给定的一个节点, relatedTarget(相关联的目标) 必须改为它的祖先 (或者它本身) 作为一个节点在同一个shadow tree
  2. 事件监听一定不能够在一个node(节点)上被调用,此时节点的target(目标)relatedTarget(相关联的目标)是相同的.

related target resolution algorithm(相关联目标分辨算法) 必须必须用来决定relatedTarget(相关联目标)属性的值并且必须equivalent(等价)于经过下列步骤的处理:

输入
EVENT(事件), 一个事件
CURRENT-TARGET(当前的目标), 一个被事件监听所调用的node(节点)
RELATED-TARGET(相关联的目标), 被事件所关联到的目标
输出
ADJUSTED-RELATED-TARGET(调整过的相关联的目标), 相对于CURRENT-TARGET(当前目标)adjusted related target(调整后的目标)
  1. CURRENT-TARGET-TREE(当前目标树)成为一个包含CURRENT-TARGET(当前目标)node tree(节点树)
  2. RELATED-TARGET-TREE(关联目标树)成为一个包含RELATED-TARGET(关联目标)node tree(节点树)
  3. RELATED-TARGET-EVENT-PATH(相关目标事件路径)RELATED-TARGET(关联目标)EVENT(事件)中通过event path calculation algorithm(事件路径算法)的结果输出
  4. 如果CURRENT-TARGET-TREE(当前目标树)RELATED-TARGET-TREE(关联目标树)都在同一个tree of trees(树):
    1. LOWEST-COMMON-ANCESTOR-TREE(共有度最高的祖先树)成为CURRENT-TARGET-TREE(当前目标树)RELATED-TARGET-TREE(关联目标树)inclusive ancestor tree(包含的祖先树)中共有度最高的
  5. 否则:
    1. LOWEST-COMMON-ANCESTOR-TREE(共有度最高的祖先树)成为RELATED-TARGET-TREE(相关联目标树)root tree(根树)
  6. 对于每一个inclusive ancestor tree(包含祖先的树), COMMON-ANCESTOR-TREE(共有祖先的树), LOWEST-COMMON-ANCESTOR-TREE(共有度最高的祖先树), 在递增的顺序上:
    1. ADJUSTED-RELATED-TARGET(调整后的相关的目标)成为RELATED-TARGET-EVENT-PATH(相关目标事件路径)中的第一个node(节点)只要符合以下的条件:
      1. node(节点) 包含在COMMON-ANCESTOR-TREE(共同的祖先树)
    2. 如果ADJUSTED-RELATED-TARGET(调整后的相关的目标)存在:
      1. 停止使用此算法

相关目标分辨算法的返回结果不总为null. 如果发生这种情况, 你需要对本标准提出bug并反馈给我们.

相关目标重定向的过程必须必须发生在一个事件分发之前.

重定向触摸事件

Touch(触摸) target(目标) [[!TOUCH-EVENTS]] 属性必须被调整就像一个relatedTarget(相关目标)的一个事件. 每一个Touch(触摸) target(目标)TouchList(触摸列表)的返回结果来自于TouchEvent(触摸事件)touches(), changedTouches()targetTouches()的结果一定是经过related target resolution algorithm(相关目标解析算法), 指定一个NODE(节点)Touch(触摸) target(目标)作为参数.

重定向得到焦点事件

focus(得到焦点), DOMFocusIn, blur(失去焦点), 和DOMFocusOut 事件 必须被认为是和带有relatedTarget(相关)的事件是同一种方式的, 符合的node(节点)的相关联的目标是在target(目标)得到焦点时又失去焦点时使用失去焦点时的结果或者 node(节点)获得焦点, 同时又造成它失去焦点target(目标) 行为的节点作为相关联的目标.

事件分发

在分发事件的时候:

在事件分发完成之上, Event(事件)对象 target(目标)currentTarget(当前目标) 一定 必须应该是最顶层的祖先relative target(相关目标). 只要一段script可能控制了Event对象在事件分发的范围内的传递, 这个步骤需要去避免暴露在shadow trees上的 nodes(节点).

事件重定向的示例

假设我们现在有一个媒体控件的用户实例, 通过下列这个树来描述它, 由document tree(文档树)shadow trees两部分组成. 在这个例子中, 我们将假设选择器被允许穿过阴影的边界并且我们将使用这些选择器去定义找到这些elements(元素). 当然, 我们也将创建一个虚构的shadow-root element(元素) 去划分阴影边界并且 表现出 shadow roots:

<div id="player">
    <shadow-root id="player-shadow-root">
        <div id="controls">
            <button id="play-button">PLAY</button>
            <input type="range" id="timeline">
                <shadow-root id="timeline-shadow-root">
                    <div id="slider-thumb" id="timeline-slider-thumb"></div>
                </shadow-root>
            </input>
            <div id="volume-slider-container">
                <input type="range" id="volume-slider">
                    <shadow-root id="volume-shadow-root">
                        <div id="slider-thumb" id="volume-slider-thumb"></div>
                    </shadow-root>
                </input>
            </div>
        </div>
    </shadow-root>
</div>
        

让我们通过一个选择器the volume slider's thumb (#volume-slider-thumb)去指出控件的位置, 由此 在这个节点上去触发 一个mouseover 事件. 对于这个事件, 让我们假装认为没有relatedTarget(相关联的目标).

每一个retargeting algorithm(重定向算法), 我们都需要设置以下的祖先和相关联的目标:

Ancestor Relative Target
#player #player
#player-shadow-root #volume-slider
#controls #volume-slider
#volume-slider-container #volume-slider
#volume-slider #volume-slider
#volume-shadow-root #volume-slider-thumb
#volume-slider-thumb #volume-slider-thumb

在我们分发 mouseover这个事件之后 使用 它们新计算出来的相关联目标, 用户决定去通过拇指去移动设备的时间线的位置 (#timeline-slider-thumb). 这次触发有一次mouseout事件对于the volume slider thumb 和一次mouseover 事件 对于 the timeline thumb.

让我们看看the volume thumb's的relatedTarget(相关联目标)的值是如何被 mouseout事件所影响的. 对于这次事件, relatedTarget(相关联事件)是the timeline thumb (#timeline-slider-thumb). 每一次的related target resolution algorithm(关联目标解析算法), 我们都应该设置以下的祖先和调整关联的目标:

Ancestor Relative Target Adjusted related Target
#player #player #player
#player-shadow-root #volume-slider #timeline
#controls #volume-slider #timeline
#volume-slider-container #volume-slider #timeline
#volume-slider #volume-slider #timeline
#volume-shadow-root #volume-slider-thumb #timeline
#volume-slider-thumb #volume-slider-thumb #timeline

节点, #player, target(目标)relatedTarget(关联目标) 同时都有相同的值 (#player), 这意味着它们并没有分发 node(节点)和它们的祖先上分发事件.

用户交互

范围 和 选区

选区 [[!EDITING]]([[!可编辑的区域]])是没有被定义的. 选区的开发和实现应该做到最好. 这是一个可能公认的错误的方法:

由于nodes(节点)在不同的node(节点)树上绝对不可能拥有相同的根(元素),它们可能绝对不会存在一个有效的DOM范围内(这个DOM范围包含了多数的node(节点)树).

因此选区 是可能存在于唯一一个 node(节点)树里, 因为选区被定义在一个单独的范围里. 通过方法window.getSelection()返回的选区绝对不会返回一个在shadow树里的选区.

shadow根对象的getSelection()方法返回的是当前选区是在当前(上下文)的shadow tree.

引导获取焦点

如果一个节点加入一个生成树(composed tree), 此节点 必须从[[!CSS3UI]]渲染树的顺序中被忽略

对于引导获取焦点的时序, 此引导顺序的时序对于一个给定的shadow tree A 必须 被插入到其他node tree(节点树) 引导顺序中 , 规则规则如下:

  1. 如果A最新的shadow tree:
    1. 宿主成为shadow host(阴影宿主),现在的阴影宿主就是宿主A
    2. B成为node tree(节点树),让节点树的宿主加入
    3. 对于A的引导顺序 必须 将A插入到B 的引导顺序:
      1. 立刻插入到宿主之后, 如果宿主可聚焦的; 或者
      2. 取代此宿主 ,如果 此宿主 被指定了属性 auto这个属性(auto这个属性决定了此宿主的位置)
  2. 如果不是那么:
    1. B成为一个优先级较高的shadow tree , 让B关联到A
    2. SHADOW成为一个shadow插入点插入 B
    3. 如果SHADOW 存在, 在SHADOW之后,对于A引导顺序必须被插入到B引导顺序 如果SHADOW 被指定了值 auto 去决定它的位置.

对于有指向性的引导焦点, 它取决于用户代理完整的 引导顺序shadow trees文本引导顺序.

激活的元素

为了坚持封装性, 激活的元素文档 对象的 focus(焦点) API 属性中的值 必须被调整的. 为了防止当调整这个值的时候丢失信息, 每一个 shadow root(阴影的根) 必须 也拥有一个激活元素的 属性用来保存在shadow tree中获取焦点激活元素的属性值

对于激活 元素 的算法调整 被用来决定激活元素的属性值, 激活元素必须经过如下列步骤一样的等价处理的过程:

输入
元素, 得到焦点的元素
ROOT(根), 要么是一个文档 或者是一个 shadow root
输出
校正, 一个已经被校正的激活元素属性的.
  1. 路径元素和null事件路径算法结果作为输入
  2. 调整过的路径重定向算法结果 作为输出

校订

contenteditable属性的值一定不能通过shadow host(阴影宿主) 传播到它本身的 shadow trees上.

辅助技术

用户代理可以通过辅助技术遍历composed tree(生成树), 因此可以使完整的WAI-ARIA(可访问的富因特网应用程序) [[!WAI-ARIA]]语义在shadow trees中使用.

Shadow Trees中的HTML元素

相对来说, 一个shadow tree可以看作是存在于部分文档和文档本身之间某处的一个文档碎片. 当阴影树被渲染的时候, 单个shadow tree 目的是维持它本身在文档的传统中的标准. 于此同时, 由于阴影树是抽象封装, 阴影树不能影响文档树. 因此, 阴影树中的HTML元素 必须 表现为指定的 [[!HTML]]的行为在shadow trees中, 除了少数例外的情况

无效的HTML元素

HTML元素一个子集合确定 行为表现为 无效, 或者不属于document tree(文档树). 在这种一致的document fragment(文档碎片)中的这种HTML元素如何表现. 这样 elements(元素)有:

剩下的其他HTML元素shadow trees里 必须 表现为就像它们出现在document tree(文档树)中的表现.

HTML元素和它们的shadow trees

每一个 规范, 针对内容的渲染地方,一些HTM元素不是用来渲染它们的内容或针对特殊的需求所设计的. 是为了标准化HTML元素在composed tree(生成树)中渲染时的不同行为的,在shadow tree被创建和被填充进去的实例化元素的时候,所有的HTML元素必须具有相同的阴影树.这取决于用户代理定义阴影树.当然, 所有符合标准的用户代理必须满足以下的要求:

HTML Element Shadow Tree Requirements
img, iframe, embed, object, video, audio, canvas, map, input, textarea, progress, meter 如果这些元素可能有回调的内容, 包含一个内容的插入点. 这个(内容的插入点的)匹配条件的值是一个通用的选择器紧紧当这些元素需要展示回调内容时.其它方面, 包含没有内容插入点 或者一个内容插入点什么都无法匹配.
fieldset 包含两个内容的插入点要符合下列匹配条件:
  1. legend:first-of-type
  2. universal selector(普通选择符)
details 包含两个内容的插入点要符合下列匹配条件:
  1. summary:first-of-type
  2. universal selector(普通选择符)
剩下所有elements(元素) 包含一个内容插入点并且有universal selector(普通选择符)作为匹配条件

Elements(元素)和DOM Objects(DOM对象)

ShadowRoot对象

ShadowRoot对象代表此shadow root.

HTMLElement getElementById(DOMString elementId)
必须行为完全就像document.getElementById一样, 除了scoped(作用域)范围是在此shadow tree内.
NodeList getElementsByClassName(DOMString className)
必须行为完全就像document.getElementsByClassName一样, 除了scoped(作用域)范围是在此shadow tree.
NodeList getElementsByTagName(DOMString tagName)
必须行为完全就像document.getElementsByTagName, 除了scoped(作用域)范围是在此shadow tree.
NodeList getElementsByTagNameNS(DOMString? namespace, DOMString localName)
必须行为完全就像document.getElementsByTagNameNS, 除了scoped(作用域)范围是在此shadow tree.
Selection? getSelection()

返回当前在此shadow tree的区域

当此方法被调用, 它必须返回此shadow treeselection(区域).

Element? elementFromPoint(double x, double y)

返回一个element(元素)指定的坐标系.

最终, 它需要是CSSOM View Module[[!CSSOM-VIEW]]标准的一部分

当此方法被调用, 它必须返回下列步骤运行后的结果:

  1. 如果context object(上下文对象)不是一个ShadowRoot的实例, 那么抛出一个InvalidNodeTypeError(无效节点的错误)错误.
  2. 如果其中任意一个参数无效, x大于viewport(视窗)的宽度不包括已经被渲染出来的滚动条(如果有), 或者如果是y大于viewport(视窗)的高度不包括已经被渲染出来的滚动条(如果有), 返回null.
  3. HIT这样的方式成为描述element(元素)viewport(视窗)XY坐标, 这样的方式决定于是否通过打点测试
  4. PATH(路径)HIT里通过event path calculation algorithm(事件路径算法)所得到的结果HRnull作为输入
  5. 返回通过retargeting algorithm(重定向算法)所得到的结果PATH(路径)context object(上下文对象)作为输入
只读 attribute Element? activeElement

表示当前在shadow tree得到焦点的element(元素)

当读取时, 这个属性必须返回在当前在shadow tree中得到焦点的element(元素)的属性或者为null, 如果这个属性不是空的.

只读 attribute Element host

表示此shadow host(阴影宿主),此hosts(宿主)有此context object(上下文对象).

当读取时, 这个属性必须返回此shadow host(宿主),这个hosts(宿主)有此context object(上下文对象).

只读 attribute ShadowRoot? olderShadowRoot

表示此older shadow root(不是最新的阴影根)所关联到此context object(上下文对象)

当读取时, 此属性必须返回一个equivalent(等价)于下列步骤运行后的结果:

  1. 如果此context object(上下文对象)是在oldest shadow root(最原始的阴影根), 返回null.
  2. 返回一个older shadow root(不是最新的阴影根)所关联到的context object(上下文对象).

对于HTML elements(HTML元素), UA-provided shadow trees 一定是不能被使用的.

attribute DOMString innerHTML

表示ShadowRoot的标记上下文.

当读取该属性时, 此属性必须返回在此context object(上下文对象)中通过HTML fragment serialization algorithm(HTML文档序列化算法)所处理过的结果作为 shadow host(阴影宿主)

当设置该属性时,必须经过以下步骤:

  1. FRAGMENT(片段)经过调用fragment parsing algorithm(片段解析算法) [[!DOMPARSING]]使新设置的值作为MARKUP(标识)的结果, 并且此context object(上下文对象)作为shadow host(阴影宿主)
  2. Replace all(替换所有)shadow root中的FRAGMENT(片段)
只读 attribute StyleSheetList styleSheets

表示此shadow root的样式列表.

当读取该属性事时, 此属性必须返回一个StyleSheetList(样式表)序列包含此shadow root的样式表.

一个ShadowRoot 实例的 nodeType(节点类型)属性 必须 返回 DOCUMENT_FRAGMENT_NODE. 因此, 一个ShadowRoot实例的nodeName(节点名) 属性必须返回"#document-fragment".

调用方法cloneNode()方法去复制ShadowRoot 实例时 必须通常会抛出一个 DATA_CLONE_ERR异常.

Element(元素)接口的扩展

ShadowRoot方法createShadowRoot()
当此方法被调用, 必须经过以下的步奏:
  1. 创建一个新的ShadowRoot对象实例
  2. 增加此ShadowRoot对象到一个有序的阴影根列表上根关联上的此context object(上下文对象)作为最新的shadow root
  3. 返回一个ShadowRoot对象.
NodeList(节点列表) getDestinationInsertionPoints()
当此方法被调用, 此方法必须返回一个static(静态)NodeList(节点列表)由在destination insertion points(目标插入点)中的插入点context object(上下文对象)所组成的.

元素的content(内容)

元素的content(内容)表示一个在shadow tree中的insertion point(插入点).

如果一个content(内容)元素不符合insertion point(插入点)的条件,

上下文
预计flow content(流内容).
内容模块
可见的
内容的子元素
一切可以作为回调的内容
内容的属性
全局属性
select(选择), 设置一个comma-separated tokens(对于逗号分割的标识)
表示用于distributing(分发)shadow host(宿主对象)中的子nodes(节点)matching criteria(匹配标准). 每一个标识必须是一个compound selector(复合的选择器).
DOM接口
attribute DOMString select
必须 显示select(选择)属性.
NodeList getDistributedNodes()
当此方法被调用, 它必须返回一个经过下列步骤的结果:
  1. 如果此context object(上下文对象)是一个内容插入点:
    1. 返回一个静态NodeList(节点列表)由在context object(上下文对象)distributed nodes(分布的节点)所组成的
  2. 其它情况:
    1. 返回一个空的static(静态) NodeList(节点列表)对象.

shadow元素

shadow元素表示在shadow tree中的一个shadow insertion point(阴影插入点).

如果一个shadow元素不满足insertion point(插入点)的条件, 它必须就像HTMLUnknownElement(未知HTML元素)一样的渲染行为.

上下文
预计的流内容.
内容模块
可见的
阴影元素的子元素
可以是一切
DOM接口
attribute DOMString select
必须 显示select(选择)属性.
NodeList getDistributedNodes()
当此方法被调用, 它必须返回一个经过下列步骤的结果:
  1. 如果此context object(上下文对象)是一个内容插入点:
    1. 返回一个静态NodeList(节点列表)由在context object(上下文对象)distributed nodes(分布的节点)所组成的
  2. 其它情况:
    1. 返回一个空的static(静态) NodeList(节点列表)对象.

异常的Event(事件)的接口

只读 attribute object path

表示此事件对象路径.

当获取该属性时, 此属性必须 创建和返回一个新的JavaScript数组对象, 此数组对象从context object(上下文对象)的事件路径中拷贝而来.

使用Array(数组) 作为在WebIDL中返回类型的path(路径)属性. WebIDL的bugs: Arraysubclassing class, not interface.

Shadow DOM示例

Bob被要求在把一个简单的链接列表变成一个消息控件, 消息控件能够连接到2个不同类别的新闻: 突发新闻和最新新闻. 目前新闻报道的文档组成看上去就像这样:

<ul class="stories">
    <li><a href="//example.com/stories/1">A story</a></li>
    <li><a href="//example.com/stories/2">Another story</a></li>
    <li class="breaking"><a href="//example.com/stories/3">Also a story</a></li>
    <li><a href="//example.com/stories/4">Yet another story</a></li>
    <li><a href="//example.com/stories/4">Awesome story</a></li>
    <li class="breaking"><a href="//example.com/stories/5">Horrible story</a></li>
</ul>
      

为了管理新闻报道, Bob决定使用shadow DOM. 这样做允许Bob保持整个标签的整洁, 拥有可以控制插入点的权会使得通过类名分类新闻的任务变的非常简单. 在得到一杯Green Eye后, 他制作出了如下shadow tree,ul元素成为了宿主:

<div class="breaking">
    <ul>
        <content select=".breaking"></content> <!-- insertion point for breaking news -->
    </ul>
</div>
<div class="other">
    <ul>
        <content></content> <!-- insertion point for the rest of the news -->
    </ul>
</div>
      

Bob接下来根据来自于设计师的要求给新建立的消息控件样式,然后把它加入到shadow tree模型中:

<style>
    div.breaking {
        color: Red;
        font-size: 20px;
        border: 1px dashed Purple;
    }
    div.other {
        padding: 2px 0 0 0;
        border: 1px solid Cyan;
    }
</style>
      

他仔细考虑了下他的公司是否应该找一个新的设计师, Bob将模型转化成了代码:

function createStoryGroup(className, contentSelector)
{
    var group = document.createElement('div');
    group.className = className;
    // Empty string in select attribute or absence thereof work the same, so no need for special handling.
    group.innerHTML = '<ul><content select="' + contentSelector + '"></content></ul>';
    return group;
}

function createStyle()
{
    var style = document.createElement('style');
    style.textContent = 'div.breaking { color: Red;font-size: 20px; border: 1px dashed Purple; }' +
        'div.other { padding: 2px 0 0 0; border: 1px solid Cyan; }';
    return style;
}

function makeShadowTree(storyList)
{
    var root = storyList.createShadowRoot();
    root.appendChild(createStyle());
    root.appendChild(createStoryGroup('breaking', '.breaking'));
    root.appendChild(createStoryGroup('other', ''));
}

document.addEventListener('DOMContentLoaded', function() {
    [].forEach.call(document.querySelectorAll('ul.stories'), makeShadowTree);
});
      

干得好, Bob! 在咖啡还有半杯的情况下, 工作完成了. 意识到自己很厉害, Bob将自己的经验通过Puyo Puyo这样的方式传授给大家.

几个月过去了.

红旗飘飘了! 在Bob一年一度的研讨会中, Alice 负责增加 另一个, 临时的盒子在新闻控件中, 其中有许多关于选举的新闻. Alice阅读学习了Bob's代码, 阅读了shadow DOM的标准并且实现了, 多亏shadow tree复合的支持, 她并没有修改Bob的代码. 就像平常一样, 她的解决方法优雅而简单, 适当的在Bob的代码上做了点巧妙的变化:

// TODO(alice): BEGIN -- DELETE THIS CODE AFTER ELECTIONS ARE OVER.
var ELECTION_BOX_REMOVAL_DEADLINE = ...;

function createElectionStyle()
{
    var style = document.createElement('style');
    // TODO(alice): Check designer's desk for hallucinogens.
    style.textContent = 'div.election { color: Magenta; font-size: 24px; border: 2px dotted Fuchsia; }';
    return style;
}

function makeElectionShadowTree(storyList)
{
    var root = storyList.createShadowRoot();
    // Add and style election story box.
    root.appendChild(createElectionStyle());
    root.appendChild(createStoryGroup('election', '.election'));
    // Insert Bob's shadow tree under the election story box.
    root.appendChild(document.createElement('shadow'));
}

if (Date.now() < ELECTION_BOX_REMOVAL_DEADLINE) {
    document.addEventListener('DOMContentLoaded', function() {
        [].forEach.call(document.querySelectorAll('ul.stories'), makeElectionShadowTree);
    });
}
// TODO(alice): END -- DELETE THIS CODE AFTER ELECTIONS ARE OVER.
      

使用shadow 元素允许Alice去组合Bob's组件到自己的组件里—在没有改变产品任何一行代码的情况下. 她对自己笑了, Alice意识到Bob已经想出一个方式关于保持文档标签整洁的想法, 但是一个如此早的使用shadow tree组成这种很酷的方式去解决问题的人.

致谢

David Hyatt 开发了 XBL 1.0, 并且和Ian Hickson合写了XBL 2.0. 这两篇文档提供了极好的见解在函数闭包的问题并对本规范产生了巨大的影响.

Alex Russell和他非常有远见的想法引发了在shadow Dom这个主题上引发了狂热的浪潮和如何在web中实际应用起来.

Dominic Cooney, Hajime Morrita, and Roland Steiner不辞辛劳的工作在web平台范围里去解决函数闭包问题并且为这篇文档提供了稳固的基础.

编者也感谢像Alex Komoroske, Anne van Kesteren, Brandon Payton, Brian Kardell, Darin Fisher, Eric Bidelman, Deepak Sherveghar, Edward O'Connor, Elisée Maurer, Elliott Sprehn, Erik Arvidsson, Glenn Adams, Jonas Sicking, Malte Ubl, Mike Taylor, Oliver Nightingale, Olli Pettay, Rafael Weinstein, Richard Bradshaw, Ruud Steltenpool, Sam Dutton, Sergey G. Grekhov, Shinya Kawanaka, Tab Atkins, Takashi Sakamoto, and Yoshinori Sano,感谢他们对此标准的意见和协助.

此标准仍不完善. 还有许多工作需要做. 请通过阅读帮助我们并且记录bugs—千万不要忘记叫编者将你的名字添加到这里来.