OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * This library exposes the types in [watcher], [safe_html], [templating] and | |
7 * the [WebComponent] base class. See this article for more information about | |
8 * this library: <http://www.dartlang.org/articles/dart-web-components/>. | |
9 */ | |
10 library web_components; | |
11 | |
12 export 'watcher.dart'; | |
13 export 'safe_html.dart'; | |
14 export 'templating.dart'; | |
15 | |
16 import 'dart:html'; | |
17 | |
18 // Imported for the doc comment | |
19 import 'watcher.dart' as watcher; | |
20 import 'safe_html.dart' as safe_html; | |
21 import 'templating.dart' as templating; | |
22 | |
23 /** | |
24 * The base class for all Dart web components. In addition to the [Element] | |
25 * interface, it also provides lifecycle methods: | |
26 * - [created] | |
27 * - [inserted] | |
28 * - [attributeChanged] | |
29 * - [removed] | |
30 */ | |
31 abstract class WebComponent implements Element { | |
32 /** The web component element wrapped by this class. */ | |
33 final Element _element; | |
34 List _shadowRoots; | |
35 | |
36 /** | |
37 * Default constructor for web components. This contructor is only provided | |
38 * for tooling, and is *not* currently supported. | |
39 * Use [WebComponent.forElement] instead. | |
40 */ | |
41 WebComponent() : _element = null { | |
42 throw new UnsupportedError( | |
43 'Directly constructing web components is not currently supported. ' | |
44 'You need to use the WebComponent.forElement constructor to associate ' | |
45 'a component with its DOM element. If you run "bin/dwc.dart" on your ' | |
46 'component, the compiler will create the approriate construction ' | |
47 'logic.'); | |
48 } | |
49 | |
50 /** | |
51 * Temporary constructor until components extend [Element]. Attaches this | |
52 * component to the provided [element]. The element must not already have a | |
53 * component associated with it. | |
54 */ | |
55 WebComponent.forElement(Element element) : _element = element { | |
56 if (element == null || _element.xtag != null) { | |
57 throw new ArgumentError( | |
58 'element must be provided and not have its xtag property set'); | |
59 } | |
60 _element.xtag = this; | |
61 } | |
62 | |
63 /** | |
64 * **Note**: This is an implementation helper and should not need to be called | |
65 * from your code. | |
66 * | |
67 * Creates the [ShadowRoot] backing this component. | |
68 */ | |
69 createShadowRoot() { | |
70 if (_realShadowRoot) { | |
71 return new ShadowRoot(_element); | |
72 } | |
73 if (_shadowRoots == null) _shadowRoots = []; | |
74 _shadowRoots.add(new Element.html('<div class="shadowroot"></div>')); | |
75 return _shadowRoots.last; | |
76 } | |
77 | |
78 /** | |
79 * Invoked when this component gets created. | |
80 * Note that [root] will be a [ShadowRoot] if the browser supports Shadow DOM. | |
81 */ | |
82 void created() {} | |
83 | |
84 /** Invoked when this component gets inserted in the DOM tree. */ | |
85 void inserted() {} | |
86 | |
87 /** Invoked when this component is removed from the DOM tree. */ | |
88 void removed() {} | |
89 | |
90 // TODO(jmesserly): how do we implement this efficiently? | |
91 // See https://github.com/dart-lang/dart-web-components/issues/37 | |
92 /** Invoked when any attribute of the component is modified. */ | |
93 void attributeChanged( | |
94 String name, String oldValue, String newValue) {} | |
95 | |
96 /** | |
97 * **Note**: This is an implementation helper and should not need to be called | |
98 * from your code. | |
99 * | |
100 * If [ShadowRoot.supported] or [useShadowDom] is false, this distributes | |
101 * children to the insertion points of the emulated ShadowRoot. | |
102 * This is an implementation helper and should not need to be called from your | |
103 * code. | |
104 * | |
105 * This is an implementation of [composition][1] and [rendering][2] from the | |
106 * Shadow DOM spec. Currently the algorithm will replace children of this | |
107 * component with the DOM as it should be rendered. | |
108 * | |
109 * Note that because we're always expanding to the render tree, and nodes are | |
110 * expanded in a bottom up fashion, [reprojection][3] is handled naturally. | |
111 * | |
112 * [1]: http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.htm
l#composition | |
113 * [2]: http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.htm
l#rendering-shadow-trees | |
114 * [3]: http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.htm
l#reprojection | |
115 */ | |
116 void composeChildren() { | |
117 if (_realShadowRoot) return; | |
118 | |
119 if (_shadowRoots.length == 0) { | |
120 // TODO(jmesserly): this is a limitation of our codegen approach. | |
121 // We could keep the _shadowRoots around and clone(true) them, but then | |
122 // bindings wouldn't be properly associated. | |
123 throw new StateError('Distribution algorithm requires at least one shadow' | |
124 ' root and can only be run once.'); | |
125 } | |
126 | |
127 var treeStack = _shadowRoots; | |
128 | |
129 // Let TREE be the youngest tree in the HOST's tree stack | |
130 var tree = treeStack.removeLast(); | |
131 var youngestRoot = tree; | |
132 // Let POOL be the list of nodes | |
133 var pool = new List.from(nodes); | |
134 | |
135 // Note: reprojection logic is skipped here because composeChildren is | |
136 // run on each component in bottom up fashion. | |
137 | |
138 var shadowInsertionPoints = []; | |
139 var shadowInsertionTrees = []; | |
140 | |
141 while (true) { | |
142 // Run the distribution algorithm, supplying POOL and TREE as input | |
143 pool = _distributeNodes(tree, pool); | |
144 | |
145 // Let POINT be the first encountered active shadow insertion point in | |
146 // TREE, in tree order | |
147 var point = tree.query('shadow'); | |
148 if (point != null) { | |
149 if (treeStack.length > 0) { | |
150 // Find the next older tree, relative to TREE in the HOST's tree stack | |
151 // Set TREE to be this older tree | |
152 tree = treeStack.removeLast(); | |
153 // Assign TREE to the POINT | |
154 | |
155 // Note: we defer the actual tree replace operation until the end, so | |
156 // we can run _distributeNodes on this tree. This simplifies the query | |
157 // for content nodes in tree order. | |
158 shadowInsertionPoints.add(point); | |
159 shadowInsertionTrees.add(tree); | |
160 | |
161 // Continue to repeat | |
162 } else { | |
163 // If we've hit a built-in element, just use a content selector. | |
164 // This matches the behavior of built-in HTML elements. | |
165 // Since <content> can be implemented simply, we just inline it. | |
166 _distribute(point, pool); | |
167 | |
168 // If there is no older tree, stop. | |
169 break; | |
170 } | |
171 } else { | |
172 // If POINT exists: ... Otherwise, stop | |
173 break; | |
174 } | |
175 } | |
176 | |
177 // Handle shadow tree assignments that we deferred earlier. | |
178 for (int i = 0; i < shadowInsertionPoints.length; i++) { | |
179 var point = shadowInsertionPoints[i]; | |
180 var tree = shadowInsertionTrees[i]; | |
181 // Note: defensive copy is a workaround for http://dartbug.com/6684 | |
182 _distribute(point, new List.from(tree.nodes)); | |
183 } | |
184 | |
185 // Replace our child nodes with the ones in the youngest root. | |
186 nodes.clear(); | |
187 // Note: defensive copy is a workaround for http://dartbug.com/6684 | |
188 nodes.addAll(new List.from(youngestRoot.nodes)); | |
189 } | |
190 | |
191 | |
192 /** | |
193 * This is an implementation of the [distribution algorithm][1] from the | |
194 * Shadow DOM spec. | |
195 * | |
196 * [1]: http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.htm
l#dfn-distribution-algorithm | |
197 */ | |
198 List<Node> _distributeNodes(Element tree, List<Node> pool) { | |
199 // Repeat for each active insertion point in TREE, in tree order: | |
200 for (var insertionPoint in tree.queryAll('content')) { | |
201 if (!_isActive(insertionPoint)) continue; | |
202 // Let POINT be the current insertion point. | |
203 | |
204 // TODO(jmesserly): validate selector, as specified here: | |
205 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html
#matching-insertion-points | |
206 var select = insertionPoint.attributes['select']; | |
207 if (select == null || select == '') select = '*'; | |
208 | |
209 // Repeat for each node in POOL: | |
210 // 1. Let NODE be the current node | |
211 // 2. If the NODE matches POINT's matching criteria: | |
212 // 1. Distribute the NODE to POINT | |
213 // 2. Remove NODE from the POOL | |
214 | |
215 var matching = []; | |
216 var notMatching = []; | |
217 for (var node in pool) { | |
218 (_matches(node, select) ? matching : notMatching).add(node); | |
219 } | |
220 | |
221 if (matching.length == 0) { | |
222 // When an insertion point or a shadow insertion point has nothing | |
223 // assigned or distributed to them, the fallback content must be used | |
224 // instead when rendering. The fallback content is all descendants of | |
225 // the element that represents the insertion point. | |
226 matching = insertionPoint.nodes; | |
227 } | |
228 | |
229 _distribute(insertionPoint, matching); | |
230 | |
231 pool = notMatching; | |
232 } | |
233 | |
234 return pool; | |
235 } | |
236 | |
237 static bool _matches(Node node, String selector) { | |
238 if (node is Text) return selector == '*'; | |
239 // TODO(jmesserly): cannot use matchesSelector because of dartbug.com/4401 | |
240 //return (node as Element).matchesSelector(selector); | |
241 return (node.parent as Element).queryAll(selector).some((n) => n == node); | |
242 } | |
243 | |
244 static bool _isInsertionPoint(Element node) => | |
245 node.tagName == 'CONTENT' || node.tagName == 'SHADOW'; | |
246 | |
247 /** | |
248 * An insertion point is "active" if it is not the child of another insertion | |
249 * point. A child of an insertion point is "fallback" content and should not | |
250 * be considered during the distribution algorithm. | |
251 */ | |
252 static bool _isActive(Element node) { | |
253 assert(_isInsertionPoint(node)); | |
254 for (node = node.parent; node != null; node = node.parent) { | |
255 if (_isInsertionPoint(node)) return false; | |
256 } | |
257 return true; | |
258 } | |
259 | |
260 /** Distribute the [nodes] in place of an existing [insertionPoint]. */ | |
261 static void _distribute(Element insertionPoint, List<Node> nodes) { | |
262 assert(_isInsertionPoint(insertionPoint)); | |
263 for (var node in nodes) { | |
264 insertionPoint.parent.insertBefore(node, insertionPoint); | |
265 } | |
266 insertionPoint.remove(); | |
267 } | |
268 | |
269 // TODO(jmesserly): this forwarding is temporary until Dart supports | |
270 // subclassing Elements. | |
271 // TODO(jmesserly): we were missing the setter for title, are other things | |
272 // missing setters? | |
273 | |
274 List<Node> get nodes => _element.nodes; | |
275 | |
276 set nodes(Collection<Node> value) { _element.nodes = value; } | |
277 | |
278 /** | |
279 * Replaces this node with another node. | |
280 */ | |
281 Node replaceWith(Node otherNode) { _element.replaceWith(otherNode); } | |
282 | |
283 /** | |
284 * Removes this node from the DOM. | |
285 */ | |
286 void remove() => _element.remove(); | |
287 | |
288 Node get nextNode => _element.nextNode; | |
289 | |
290 Document get document => _element.document; | |
291 | |
292 Node get previousNode => _element.previousNode; | |
293 | |
294 String get text => _element.text; | |
295 | |
296 set text(String v) { _element.text = v; } | |
297 | |
298 bool contains(Node other) => _element.contains(other); | |
299 | |
300 bool hasChildNodes() => _element.hasChildNodes(); | |
301 | |
302 Node insertBefore(Node newChild, Node refChild) => | |
303 _element.insertBefore(newChild, refChild); | |
304 | |
305 Map<String, String> get attributes => _element.attributes; | |
306 set attributes(Map<String, String> value) { | |
307 _element.attributes = value; | |
308 } | |
309 | |
310 List<Element> get elements => _element.elements; | |
311 | |
312 set elements(Collection<Element> value) { | |
313 _element.elements = value; | |
314 } | |
315 | |
316 List<Element> get children => _element.children; | |
317 | |
318 set children(Collection<Element> value) { | |
319 _element.children = value; | |
320 } | |
321 | |
322 Set<String> get classes => _element.classes; | |
323 | |
324 set classes(Collection<String> value) { | |
325 _element.classes = value; | |
326 } | |
327 | |
328 Map<String, String> get dataAttributes => _element.dataAttributes; | |
329 set dataAttributes(Map<String, String> value) { | |
330 _element.dataAttributes = value; | |
331 } | |
332 | |
333 Map<String, String> getNamespacedAttributes(String namespace) => | |
334 _element.getNamespacedAttributes(namespace); | |
335 | |
336 Future<CSSStyleDeclaration> get computedStyle => _element.computedStyle; | |
337 | |
338 Future<CSSStyleDeclaration> getComputedStyle(String pseudoElement) | |
339 => _element.getComputedStyle(pseudoElement); | |
340 | |
341 Element clone(bool deep) => _element.clone(deep); | |
342 | |
343 Element get parent => _element.parent; | |
344 | |
345 ElementEvents get on => _element.on; | |
346 | |
347 String get contentEditable => _element.contentEditable; | |
348 | |
349 String get dir => _element.dir; | |
350 | |
351 bool get draggable => _element.draggable; | |
352 | |
353 bool get hidden => _element.hidden; | |
354 | |
355 String get id => _element.id; | |
356 | |
357 String get innerHTML => _element.innerHtml; | |
358 | |
359 void set innerHTML(String v) { | |
360 _element.innerHtml = v; | |
361 } | |
362 | |
363 String get innerHtml => _element.innerHtml; | |
364 void set innerHtml(String v) { | |
365 _element.innerHtml = v; | |
366 } | |
367 | |
368 bool get isContentEditable => _element.isContentEditable; | |
369 | |
370 String get lang => _element.lang; | |
371 | |
372 String get outerHtml => _element.outerHtml; | |
373 | |
374 bool get spellcheck => _element.spellcheck; | |
375 | |
376 int get tabIndex => _element.tabIndex; | |
377 | |
378 String get title => _element.title; | |
379 | |
380 set title(String value) { _element.title = value; } | |
381 | |
382 bool get translate => _element.translate; | |
383 | |
384 String get webkitdropzone => _element.webkitdropzone; | |
385 | |
386 void click() { _element.click(); } | |
387 | |
388 Element insertAdjacentElement(String where, Element element) => | |
389 _element.insertAdjacentElement(where, element); | |
390 | |
391 void insertAdjacentHtml(String where, String html) { | |
392 _element.insertAdjacentHtml(where, html); | |
393 } | |
394 | |
395 void insertAdjacentText(String where, String text) { | |
396 _element.insertAdjacentText(where, text); | |
397 } | |
398 | |
399 Map<String, String> get dataset => _element.dataset; | |
400 | |
401 Element get nextElementSibling => _element.nextElementSibling; | |
402 | |
403 Element get offsetParent => _element.offsetParent; | |
404 | |
405 Element get previousElementSibling => _element.previousElementSibling; | |
406 | |
407 CSSStyleDeclaration get style => _element.style; | |
408 | |
409 String get tagName => _element.tagName; | |
410 | |
411 void blur() { _element.blur(); } | |
412 | |
413 void focus() { _element.focus(); } | |
414 | |
415 void scrollByLines(int lines) { | |
416 _element.scrollByLines(lines); | |
417 } | |
418 | |
419 void scrollByPages(int pages) { | |
420 _element.scrollByPages(pages); | |
421 } | |
422 | |
423 void scrollIntoView([bool centerIfNeeded]) { | |
424 if (centerIfNeeded == null) { | |
425 _element.scrollIntoView(); | |
426 } else { | |
427 _element.scrollIntoView(centerIfNeeded); | |
428 } | |
429 } | |
430 | |
431 bool matchesSelector(String selectors) => _element.matchesSelector(selectors); | |
432 | |
433 void webkitRequestFullScreen(int flags) { | |
434 _element.webkitRequestFullScreen(flags); | |
435 } | |
436 | |
437 void webkitRequestFullscreen() { _element.webkitRequestFullscreen(); } | |
438 | |
439 void webkitRequestPointerLock() { _element.webkitRequestPointerLock(); } | |
440 | |
441 Element query(String selectors) => _element.query(selectors); | |
442 | |
443 List<Element> queryAll(String selectors) => _element.queryAll(selectors); | |
444 | |
445 HTMLCollection get $dom_children => _element.$dom_children; | |
446 | |
447 int get $dom_childElementCount => _element.$dom_childElementCount; | |
448 | |
449 String get $dom_className => _element.$dom_className; | |
450 set $dom_className(String value) { _element.$dom_className = value; } | |
451 | |
452 int get clientHeight => _element.clientHeight; | |
453 | |
454 int get clientLeft => _element.clientLeft; | |
455 | |
456 int get clientTop => _element.clientTop; | |
457 | |
458 int get clientWidth => _element.clientWidth; | |
459 | |
460 int get childElementCount => _element.childElementCount; | |
461 | |
462 Element get firstElementChild => _element.firstElementChild; | |
463 | |
464 Element get lastElementChild => _element.lastElementChild; | |
465 | |
466 Element get $dom_firstElementChild => _element.$dom_firstElementChild; | |
467 | |
468 Element get $dom_lastElementChild => _element.$dom_lastElementChild; | |
469 | |
470 int get offsetHeight => _element.offsetHeight; | |
471 | |
472 int get offsetLeft => _element.offsetLeft; | |
473 | |
474 int get offsetTop => _element.offsetTop; | |
475 | |
476 int get offsetWidth => _element.offsetWidth; | |
477 | |
478 int get scrollHeight => _element.scrollHeight; | |
479 | |
480 int get scrollLeft => _element.scrollLeft; | |
481 | |
482 int get scrollTop => _element.scrollTop; | |
483 | |
484 set scrollLeft(int value) { _element.scrollLeft = value; } | |
485 | |
486 set scrollTop(int value) { _element.scrollTop = value; } | |
487 | |
488 int get scrollWidth => _element.scrollWidth; | |
489 | |
490 String $dom_getAttribute(String name) => | |
491 _element.$dom_getAttribute(name); | |
492 | |
493 String $dom_getAttributeNS(String namespaceUri, String localName) => | |
494 _element.$dom_getAttributeNS(namespaceUri, localName); | |
495 | |
496 String $dom_setAttributeNS( | |
497 String namespaceUri, String localName, String value) { | |
498 _element.$dom_setAttributeNS(namespaceUri, localName, value); | |
499 } | |
500 | |
501 bool $dom_hasAttributeNS(String namespaceUri, String localName) => | |
502 _element.$dom_hasAttributeNS(namespaceUri, localName); | |
503 | |
504 void $dom_removeAttributeNS(String namespaceUri, String localName) => | |
505 _element.$dom_removeAttributeNS(namespaceUri, localName); | |
506 | |
507 ClientRect getBoundingClientRect() => _element.getBoundingClientRect(); | |
508 | |
509 List<ClientRect> getClientRects() => _element.getClientRects(); | |
510 | |
511 List<Node> $dom_getElementsByClassName(String name) => | |
512 _element.$dom_getElementsByClassName(name); | |
513 | |
514 List<Node> $dom_getElementsByTagName(String name) => | |
515 _element.$dom_getElementsByTagName(name); | |
516 | |
517 bool $dom_hasAttribute(String name) => | |
518 _element.$dom_hasAttribute(name); | |
519 | |
520 Element $dom_querySelector(String selectors) => | |
521 _element.$dom_querySelector(selectors); | |
522 | |
523 List<Node> $dom_querySelectorAll(String selectors) => | |
524 _element.$dom_querySelectorAll(selectors); | |
525 | |
526 void $dom_removeAttribute(String name) => | |
527 _element.$dom_removeAttribute(name); | |
528 | |
529 void $dom_setAttribute(String name, String value) => | |
530 _element.$dom_setAttribute(name, value); | |
531 | |
532 NamedNodeMap get $dom_attributes => _element.$dom_attributes; | |
533 | |
534 List<Node> get $dom_childNodes => _element.$dom_childNodes; | |
535 | |
536 Node get $dom_firstChild => _element.$dom_firstChild; | |
537 | |
538 Node get $dom_lastChild => _element.$dom_lastChild; | |
539 | |
540 String get $dom_localName => _element.$dom_localName; | |
541 | |
542 String get $dom_namespaceUri => _element.$dom_namespaceUri; | |
543 | |
544 int get nodeType => _element.nodeType; | |
545 | |
546 void $dom_addEventListener(String type, EventListener listener, | |
547 [bool useCapture]) { | |
548 _element.$dom_addEventListener(type, listener, useCapture); | |
549 } | |
550 | |
551 Node $dom_appendChild(Node newChild) => _element.$dom_appendChild(newChild); | |
552 | |
553 bool $dom_dispatchEvent(Event event) => _element.$dom_dispatchEvent(event); | |
554 | |
555 Node $dom_removeChild(Node oldChild) => _element.$dom_removeChild(oldChild); | |
556 | |
557 void $dom_removeEventListener(String type, EventListener listener, | |
558 [bool useCapture]) { | |
559 _element.$dom_removeEventListener(type, listener, useCapture); | |
560 } | |
561 | |
562 Node $dom_replaceChild(Node newChild, Node oldChild) => | |
563 _element.$dom_replaceChild(newChild, oldChild); | |
564 | |
565 get xtag => _element.xtag; | |
566 | |
567 set xtag(value) { _element.xtag = value; } | |
568 | |
569 void append(Element e) => _element.append(e); | |
570 | |
571 void appendText(String text) => _element.appendText(text); | |
572 | |
573 void appendHtml(String html) => _element.appendHtml(html); | |
574 } | |
575 | |
576 /** | |
577 * Set this to true to use native Shadow DOM if it is supported. | |
578 * Note that this will change behavior of [WebComponent] APIs for tree | |
579 * traversal. | |
580 */ | |
581 bool useShadowDom = false; | |
582 | |
583 bool get _realShadowRoot => useShadowDom && ShadowRoot.supported; | |
OLD | NEW |