Skip to content

Commit 6eafca5

Browse files
committed
Add a querySelectorAll implementation.
1 parent b7be7d7 commit 6eafca5

8 files changed

+401
-301
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ node_modules/
66
# build output
77
/template-shadowroot.*
88
/test/
9+
/_implementation/

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"test:watch": "karma start"
1111
},
1212
"files": [
13-
"template-shadowroot.js"
13+
"template-shadowroot.js",
14+
"_implementation"
1415
],
1516
"repository": {
1617
"type": "git",

src/_implementation/feature_detect.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* The complete set of authors may be found at
7+
* http://polymer.github.io/AUTHORS.txt
8+
* The complete set of contributors may be found at
9+
* http://polymer.github.io/CONTRIBUTORS.txt
10+
* Code distributed by Google as part of the polymer project is also
11+
* subject to an additional IP rights grant found at
12+
* http://polymer.github.io/PATENTS.txt
13+
*/
14+
15+
// This isn't ideal. Setting .innerHTML is not compatible with some
16+
// TrustedTypes CSP policies. Discussion at:
17+
// https://github.com/mfreed7/declarative-shadow-dom/issues/3
18+
let hasNative: boolean|undefined;
19+
export function hasNativeDeclarativeShadowRoots(): boolean {
20+
if (hasNative === undefined) {
21+
const div = document.createElement('div');
22+
div.innerHTML = `<div><template shadowroot="open"></template></div>`;
23+
hasNative = !!div.firstElementChild!.shadowRoot;
24+
}
25+
return hasNative;
26+
}

src/_implementation/manual_walk.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* The complete set of authors may be found at
7+
* http://polymer.github.io/AUTHORS.txt
8+
* The complete set of contributors may be found at
9+
* http://polymer.github.io/CONTRIBUTORS.txt
10+
* Code distributed by Google as part of the polymer project is also
11+
* subject to an additional IP rights grant found at
12+
* http://polymer.github.io/PATENTS.txt
13+
*/
14+
15+
import { hasNativeDeclarativeShadowRoots } from './feature_detect.js';
16+
import { isTemplate, isElement, hasNoParentElement } from './util.js';
17+
18+
/*
19+
* Traverses the DOM to find all <template> elements with a `shadowroot`
20+
* attribute and move their content into a ShadowRoot on their parent element.
21+
*
22+
* This processing is done bottom up so that when top-level <template>
23+
* elements are hydrated, their contents are already hydrated and in the
24+
* final correct structure of elements and shadow roots.
25+
*/
26+
export const hydrateShadowRoots = (root: ParentNode) => {
27+
if (hasNativeDeclarativeShadowRoots()) {
28+
return; // nothing to do
29+
}
30+
31+
// Approaches to try and benchmark:
32+
// - manual walk (current implementation)
33+
// - querySelectorAll
34+
// - TreeWalker
35+
36+
// Stack of nested templates that we're currently processing. Use to
37+
// remember how to get from a <template>.content DocumentFragment back to
38+
// its owner <template>
39+
const templateStack: Array<HTMLTemplateElement> = [];
40+
41+
let currentNode: Element|DocumentFragment|null = root.firstElementChild;
42+
43+
// The outer loop traverses down, looking for <template shadowroot>
44+
// elements. The inner loop traverses back up, hydrating them in a postorder
45+
// traversal.
46+
while (currentNode !== root && currentNode !== null) {
47+
if (isTemplate(currentNode)) {
48+
templateStack.push(currentNode);
49+
currentNode = currentNode.content;
50+
} else if (currentNode.firstElementChild !== null) {
51+
// Traverse down
52+
currentNode = currentNode.firstElementChild;
53+
} else if (
54+
isElement(currentNode) && currentNode.nextElementSibling !== null) {
55+
// Element is empty, but has a next sibling. Traverse that.
56+
currentNode = currentNode.nextElementSibling;
57+
} else {
58+
// Element is empty and the last child. Traverse to next aunt/grandaunt.
59+
60+
// Store templates we hydrate for one loop so that we can remove them
61+
// *after* traversing to their successor.
62+
let template: HTMLTemplateElement|undefined;
63+
64+
while (currentNode !== root && currentNode !== null) {
65+
if (hasNoParentElement(currentNode)) {
66+
// We must be at a <template>'s content fragment.
67+
template = templateStack.pop()!;
68+
const host = template.parentElement!;
69+
const mode = template.getAttribute('shadowroot');
70+
currentNode = template;
71+
if (mode === 'open' || mode === 'closed') {
72+
const delegatesFocus =
73+
template.hasAttribute('shadowrootdelegatesfocus');
74+
try {
75+
const shadow = host.attachShadow({mode, delegatesFocus});
76+
shadow.append(template.content);
77+
} catch {
78+
// there was already a closed shadow root, so do nothing, and
79+
// don't delete the template
80+
}
81+
} else {
82+
template = undefined;
83+
}
84+
} else {
85+
const nextSibling: Element|null|undefined =
86+
currentNode.nextElementSibling;
87+
if (nextSibling != null) {
88+
currentNode = nextSibling;
89+
if (template !== undefined) {
90+
template.parentElement!.removeChild(template);
91+
}
92+
break;
93+
}
94+
const nextAunt: Element|null|undefined =
95+
currentNode.parentElement?.nextElementSibling;
96+
if (nextAunt != null) {
97+
currentNode = nextAunt;
98+
if (template !== undefined) {
99+
template.parentElement!.removeChild(template);
100+
}
101+
break;
102+
}
103+
currentNode = currentNode.parentElement;
104+
if (template !== undefined) {
105+
template.parentElement!.removeChild(template);
106+
template = undefined;
107+
}
108+
}
109+
}
110+
}
111+
}
112+
};
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* The complete set of authors may be found at
7+
* http://polymer.github.io/AUTHORS.txt
8+
* The complete set of contributors may be found at
9+
* http://polymer.github.io/CONTRIBUTORS.txt
10+
* Code distributed by Google as part of the polymer project is also
11+
* subject to an additional IP rights grant found at
12+
* http://polymer.github.io/PATENTS.txt
13+
*/
14+
15+
import {hasNativeDeclarativeShadowRoots} from './feature_detect.js';
16+
17+
/*
18+
* Traverses the DOM to find all <template> elements with a `shadowroot`
19+
* attribute and move their content into a ShadowRoot on their parent element.
20+
*
21+
* This processing is done bottom up so that when top-level <template>
22+
* elements are hydrated, their contents are already hydrated and in the
23+
* final correct structure of elements and shadow roots.
24+
*/
25+
export const hydrateShadowRoots = (root: Element|DocumentFragment) => {
26+
if (hasNativeDeclarativeShadowRoots()) {
27+
return; // nothing to do
28+
}
29+
30+
for (const template of Array.from(root.querySelectorAll('template'))) {
31+
hydrateShadowRoots(template.content);
32+
33+
const host = template.parentElement!;
34+
const mode = template.getAttribute('shadowroot');
35+
if (mode === 'open' || mode === 'closed') {
36+
const delegatesFocus = template.hasAttribute('shadowrootdelegatesfocus');
37+
try {
38+
const shadow = host.attachShadow({mode, delegatesFocus});
39+
shadow.append(template.content);
40+
} catch {
41+
// there was already a closed shadow root, so do nothing, and
42+
// don't delete the template
43+
}
44+
host.removeChild(template);
45+
}
46+
}
47+
};

src/_implementation/util.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
4+
* This code may only be used under the BSD style license found at
5+
* http://polymer.github.io/LICENSE.txt
6+
* The complete set of authors may be found at
7+
* http://polymer.github.io/AUTHORS.txt
8+
* The complete set of contributors may be found at
9+
* http://polymer.github.io/CONTRIBUTORS.txt
10+
* Code distributed by Google as part of the polymer project is also
11+
* subject to an additional IP rights grant found at
12+
* http://polymer.github.io/PATENTS.txt
13+
*/
14+
15+
export const hasNoParentElement =
16+
(e: Element|DocumentFragment): e is DocumentFragment =>
17+
e.parentElement === null;
18+
export const isTemplate = (e: Node): e is HTMLTemplateElement =>
19+
(e as Partial<Element>).tagName === 'TEMPLATE';
20+
export const isElement = (e: Node): e is HTMLElement =>
21+
e.nodeType === Node.ELEMENT_NODE;

src/template-shadowroot.ts

+3-115
Original file line numberDiff line numberDiff line change
@@ -12,119 +12,7 @@
1212
* http://polymer.github.io/PATENTS.txt
1313
*/
1414

15-
// This isn't ideal. Setting .innerHTML is not compatible with some
16-
// TrustedTypes CSP policies. Discussion at:
17-
// https://github.com/mfreed7/declarative-shadow-dom/issues/3
18-
let hasNative: boolean|undefined;
19-
export function hasNativeDeclarativeShadowRoots(): boolean {
20-
if (hasNative === undefined) {
21-
const div = document.createElement('div');
22-
div.innerHTML = `<div><template shadowroot="open"></template></div>`;
23-
hasNative = !!div.firstElementChild!.shadowRoot;
24-
}
25-
return hasNative;
26-
}
15+
import {hasNativeDeclarativeShadowRoots} from './_implementation/feature_detect.js';
16+
import {hydrateShadowRoots} from './_implementation/manual_walk.js';
2717

28-
/*
29-
* Traverses the DOM to find all <template> elements with a `shadowroot`
30-
* attribute and move their content into a ShadowRoot on their parent element.
31-
*
32-
* This processing is done bottom up so that when top-level <template>
33-
* elements are hydrated, their contents are already hydrated and in the
34-
* final correct structure of elements and shadow roots.
35-
*/
36-
export const hydrateShadowRoots = (root: ParentNode) => {
37-
if (hasNativeDeclarativeShadowRoots()) {
38-
return; // nothing to do
39-
}
40-
41-
// Approaches to try and benchmark:
42-
// - manual walk (current implementation)
43-
// - querySelectorAll
44-
// - TreeWalker
45-
46-
// Stack of nested templates that we're currently processing. Use to
47-
// remember how to get from a <template>.content DocumentFragment back to
48-
// its owner <template>
49-
const templateStack: Array<HTMLTemplateElement> = [];
50-
51-
let currentNode: Element|DocumentFragment|null = root.firstElementChild;
52-
53-
// The outer loop traverses down, looking for <template shadowroot>
54-
// elements. The inner loop traverses back up, hydrating them in a postorder
55-
// traversal.
56-
while (currentNode !== root && currentNode !== null) {
57-
if (isTemplate(currentNode)) {
58-
templateStack.push(currentNode);
59-
currentNode = currentNode.content;
60-
} else if (currentNode.firstElementChild !== null) {
61-
// Traverse down
62-
currentNode = currentNode.firstElementChild;
63-
} else if (
64-
isElement(currentNode) && currentNode.nextElementSibling !== null) {
65-
// Element is empty, but has a next sibling. Traverse that.
66-
currentNode = currentNode.nextElementSibling;
67-
} else {
68-
// Element is empty and the last child. Traverse to next aunt/grandaunt.
69-
70-
// Store templates we hydrate for one loop so that we can remove them
71-
// *after* traversing to their successor.
72-
let template: HTMLTemplateElement|undefined;
73-
74-
while (currentNode !== root && currentNode !== null) {
75-
if (hasNoParentElement(currentNode)) {
76-
// We must be at a <template>'s content fragment.
77-
template = templateStack.pop()!;
78-
const host = template.parentElement!;
79-
const mode = template.getAttribute('shadowroot');
80-
currentNode = template;
81-
if (mode === 'open' || mode === 'closed') {
82-
const delegatesFocus =
83-
template.hasAttribute('shadowrootdelegatesfocus');
84-
try {
85-
const shadow = host.attachShadow({mode, delegatesFocus});
86-
shadow.append(template.content);
87-
} catch {
88-
// there was already a closed shadow root, so do nothing, and
89-
// don't delete the template
90-
}
91-
} else {
92-
template = undefined;
93-
}
94-
} else {
95-
const nextSibling: Element|null|undefined =
96-
currentNode.nextElementSibling;
97-
if (nextSibling != null) {
98-
currentNode = nextSibling;
99-
if (template !== undefined) {
100-
template.parentElement!.removeChild(template);
101-
}
102-
break;
103-
}
104-
const nextAunt: Element|null|undefined =
105-
currentNode.parentElement?.nextElementSibling;
106-
if (nextAunt != null) {
107-
currentNode = nextAunt;
108-
if (template !== undefined) {
109-
template.parentElement!.removeChild(template);
110-
}
111-
break;
112-
}
113-
currentNode = currentNode.parentElement;
114-
if (template !== undefined) {
115-
template.parentElement!.removeChild(template);
116-
template = undefined;
117-
}
118-
}
119-
}
120-
}
121-
}
122-
};
123-
124-
const hasNoParentElement =
125-
(e: Element|DocumentFragment): e is DocumentFragment =>
126-
e.parentElement === null;
127-
const isTemplate = (e: Node): e is HTMLTemplateElement =>
128-
(e as Partial<Element>).tagName === 'TEMPLATE';
129-
const isElement = (e: Node): e is HTMLElement =>
130-
e.nodeType === Node.ELEMENT_NODE;
18+
export {hasNativeDeclarativeShadowRoots, hydrateShadowRoots};

0 commit comments

Comments
 (0)