forked from jmesserly/dart-web-components
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwebcomponents.dart
348 lines (308 loc) · 11.9 KB
/
webcomponents.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/**
* Polyfill script for custom elements. To use this script, your app must
* create a CustomElementsManager with the appropriate lookup function before
* doing any DOM queries or modifications.
* Currently, all custom elements must be registered with the polyfill. To
* register custom elements, provide the appropriate lookup function to your
* CustomElementsManager.
*
* This script does an XMLHTTP request, so to test using custom elements with
* file:// URLs you must run Chrome with `--allow-file-access-from-files`.
*
* Other helpful Chrome flags are:
* `--enable-shadow-dom --enable-scoped-style --enable-devtools-experiments`
*/
#library('webcomponents');
#import('dart:html');
#source('lib/list_map.dart');
// typedefs
typedef WebComponent WebComponentFactory (ShadowRoot shadowRoot, Element elt);
typedef WebComponentFactory RegistryLookupFunction(String tagName);
// Globals
final int REQUEST_DONE = 4;
CustomElementsManager _manager;
CustomElementsManager get manager() => _manager;
void initializeComponents(RegistryLookupFunction lookup) {
_manager = new CustomElementsManager._internal(lookup);
manager._loadComponents();
}
/** A Dart wrapper for a web component. */
abstract class WebComponent {
/** The web component element wrapped by this class. */
abstract Element get element();
/** Invoked when this component gets created. */
abstract void created();
/** Invoked when this component gets inserted in the DOM tree. */
abstract void inserted();
/** Invoked when any attribute of the component is modified. */
abstract void attributeChanged(
String name, String oldValue, String newValue);
/** Invoked when this component is removed from the DOM tree. */
abstract void removed();
}
/** Loads and manages the custom elements on a page. */
class CustomElementsManager {
/**
* Maps tag names to our internal dart representation of the custom element.
*/
Map<String, _CustomDeclaration> _customDeclarations;
// TODO(samhop): evaluate possibility of using vsm's trick of storing
// arbitrary Dart objects directly on DOM objects rather than this map.
/** Maps DOM elements to the user-defiend corresponding dart objects. */
ListMap<Element, WebComponent> _customElements;
RegistryLookupFunction _lookup;
MutationObserver _insertionObserver;
CustomElementsManager._internal(this._lookup) {
// TODO(samhop): check for ShadowDOM support
_customDeclarations = <_CustomDeclaration>{};
// We use a ListMap because DOM objects aren't hashable right now.
// TODO(samhop): DOM objects (and everything else) should be hashable
_customElements = new ListMap<Element, WebComponent>();
initializeInsertedRemovedCallbacks(document);
}
/**
* Locate all external component files, load each of them, and expand
* declarations.
*/
void _loadComponents() {
queryAll('link[rel=components]').forEach((link) => _load(link.href));
_expandDeclarations();
}
/**
* Load the document at the given url and parse it to extract
* custom element declarations.
*/
void _load(String url) {
var request = new XMLHttpRequest();
// We use a blocking request here because no Dart is allowed to run
// until DOM content is loaded.
// TODO(samhop): give a decent timeout message if custom elements fail to
// load
request.open('GET', url, async: false);
request.on.readyStateChange.add((Event e) {
if (request.readyState == REQUEST_DONE) {
if (request.status >= 200 && request.status < 300
|| request.status == 304 || request.status == 0) {
var declarations = _parse(request.response);
declarations.forEach((declaration) {
_customDeclarations[declaration.name] = declaration;
});
} else {
window.console.error(
'Unable to load component: Status ${request.status}'
' - ${request.statusText}');
}
}
});
request.send();
}
/** Parse the given string of HTML to extract the custom declarations. */
List<_CustomDeclaration> _parse(String toParse) {
var declarations = new DocumentFragment.html(toParse);
var newDeclarations = [];
declarations.queryAll('element').forEach((element) {
newDeclarations.add(new _CustomDeclaration(element));
});
return newDeclarations;
}
/**
* Look for all custom elements under [root] and expand them appropriately.
* This calls the [created] event on a webcomponent, but it will not insert
* the component in the DOM tree (and hence it won't call [inserted].
*/
List expandDeclarations(Element root) =>
_expandDeclarations(root, insert: false);
/** Look for all custom elements uses and expand them appropriately. */
List _expandDeclarations([Element root, bool insert = true]) {
var newCustomElements = [];
var target;
var rootUnderTemplate = false;
if (root == null) {
target = document;
} else {
target = root;
rootUnderTemplate = root.matchesSelector('template *');
}
for (var declaration in _customDeclarations.getValues()) {
var selector = '${declaration.extendz}[is=${declaration.name}]';
var all = target.queryAll(selector);
// templates are innert and should not be expanded.
var activeElements = all.filter(
(e) => !e.matchesSelector('template *'));
if (root != null && root.matchesSelector(selector)
&& !rootUnderTemplate) {
activeElements.add(root);
}
for (var e in activeElements) {
var component = _customElements[e];
if (component == null) {
component = declaration.morph(e);
newCustomElements.add(component);
}
if (insert) {
component.inserted();
}
}
}
return newCustomElements;
}
/** Fire the [removed] event on all web components under [root]. */
void _removeComponents(Element root) {
for (var decl in _customDeclarations.getValues()) {
for (var e in root.queryAll('${decl.extendz}[is=${decl.name}]')) {
if (_customElements[e] != null) _customElements[e].removed();
}
if (root.matchesSelector('${decl.extendz}[is=${decl.name}]')) {
if (_customElements[root] != null) _customElements[root].removed();
}
}
}
/**
* Expands the given html string into a custom element.
* Assumes only one element use in htmlString (or only returns the
* first one) and assumes corresponding custom element is already
* registered.
*/
WebComponent expandHtml(String htmlString) {
return expandElement(new Element.html(htmlString));
}
/** Expand [element], assuming it is a webcomponent. */
WebComponent expandElement(Element element) {
var declaration = _customDeclarations[element.attributes['is']];
// TODO(jmesserly): this should throw an Exception
if (declaration == null) throw 'No such custom element declaration';
return declaration.morph(element);
}
WebComponent operator [](Element element) => _customElements[element];
// Initializes management of inserted and removed
// callbacks for WebComponents below root in the DOM. We need one of these
// for every shadow subtree, since mutation observers can't see across
// shadow boundaries.
void initializeInsertedRemovedCallbacks(Element root) {
_insertionObserver = new MutationObserver((mutations, observer) {
for (var mutation in mutations) {
// TODO(samhop): remove this test if it turns out that it always passes
if (mutation.type == 'childList') {
for (var node in mutation.addedNodes) {
if (node is Element) _expandDeclarations(node);
}
for (var node in mutation.removedNodes) {
if (node is Element) _removeComponents(node);
}
}
}
});
_insertionObserver.observe(root, childList: true, subtree: true);
}
}
bool _hasShadowRoot;
/**
* True if the browser supports the [ShadowRoot] element and it is enabled.
* See the [Shadow DOM spec](http://www.w3.org/TR/shadow-dom/) for more
* information about the ShadowRoot.
*/
bool get hasShadowRoot() {
if (_hasShadowRoot == null) {
try {
// TODO(jmesserly): it'd be nice if we could check this without causing
// an exception to be thrown.
new ShadowRoot(new DivElement());
_hasShadowRoot = true;
} catch (var e) {
_hasShadowRoot = false;
// Hide <template> elements.
// TODO(jmesserly): This is a workaround because we don't distribute
// children correctly. It's not actually the right fix.
var style = new Element.html(
@'<style type="text/css">template { display: none; }</style>');
document.head.nodes.add(style);
}
}
return _hasShadowRoot;
}
class _CustomDeclaration {
String name;
String extendz;
Element template;
bool applyAuthorStyles;
_CustomDeclaration(Element element) {
name = element.attributes['name'];
applyAuthorStyles = element.attributes.containsKey('apply-author-styles');
if (name == null) {
// TODO(samhop): friendlier errors
window.console.error('name attribute is required');
return;
}
extendz = element.attributes['extends'];
if (extendz == null || extendz.length == 0) {
window.console.error('extends attribute is required');
return;
}
template = element.query('template');
}
int hashCode() {
return name.hashCode();
}
operator ==(other) {
if (other is! _CustomDeclaration) {
return false;
} else {
return other.name == name &&
other.extendz == extendz &&
other.template == template;
}
}
// TODO(samhop): better docs
/**
* Modify the DOM for e, return a new Dart object corresponding to it.
* Returns null if this custom declaration has no template element.
*/
WebComponent morph(Element e) {
if (template == null) {
return null;
}
var shadowRoot;
if (hasShadowRoot) {
shadowRoot = new ShadowRoot(e);
shadowRoot.resetStyleInheritance = false;
if (applyAuthorStyles) {
shadowRoot.applyAuthorStyles = true;
}
} else {
// Remove the old ShadowRoot, if any
// TODO(jmesserly): can we avoid morphing the same node twice?
shadowRoot = e.query('.shadowroot');
if (shadowRoot != null && shadowRoot.parent == e) shadowRoot.remove();
// TODO(jmesserly): distribute children to insertion points.
shadowRoot = new Element.html('<div class="shadowroot"></div>');
e.nodes.add(shadowRoot);
}
template.nodes.forEach((node) => shadowRoot.nodes.add(node.clone(true)));
var newCustomElement = manager._lookup(this.name)(shadowRoot, e);
manager._customElements[e] = newCustomElement;
manager._expandDeclarations(shadowRoot, insert: false);
newCustomElement.created();
manager._expandDeclarations(shadowRoot, insert: true);
// TODO(samhop): investigate refactoring/redesigning the API so that
// components which don't need their attributes observed don't have an
// observer created, for perf reasons.
var attributeObserver = new MutationObserver((mutations, observer) {
for (var mutation in mutations) {
if (mutation.type == 'attributes') {
var attrName = mutation.attributeName;
Element element = mutation.target;
newCustomElement.attributeChanged(attrName,
mutation.oldValue, element.attributes[attrName]);
}
}
});
attributeObserver.observe(e, attributes: true, attributeOldValue: true);
// Listen for all insertions and deletions on the DOM so that we can
// catch custom elements being inserted and call the appropriate callbacks.
manager.initializeInsertedRemovedCallbacks(shadowRoot);
return newCustomElement;
}
}