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