| OLD | NEW | 
|---|
| (Empty) |  | 
|  | 1 // Copyright (c) 2013, 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 part of polymer; | 
|  | 6 | 
|  | 7 /** | 
|  | 8  * Use this annotation to publish a field as an attribute. For example: | 
|  | 9  * | 
|  | 10  *     class MyPlaybackElement extends PolymerElement { | 
|  | 11  *       // This will be available as an HTML attribute, for example: | 
|  | 12  *       //     <my-playback volume="11"> | 
|  | 13  *       @published double volume; | 
|  | 14  *     } | 
|  | 15  */ | 
|  | 16 // TODO(jmesserly): does @published imply @observable or vice versa? | 
|  | 17 const published = const PublishedProperty(); | 
|  | 18 | 
|  | 19 /** An annotation used to publish a field as an attribute. See [published]. */ | 
|  | 20 class PublishedProperty extends ObservableProperty { | 
|  | 21   const PublishedProperty(); | 
|  | 22 } | 
|  | 23 | 
|  | 24 // TODO(jmesserly): make this the mixin so we can have Polymer type extensions, | 
|  | 25 // and move the implementation of PolymerElement in here. Once done it will look | 
|  | 26 // like: | 
|  | 27 // abstract class Polymer { ... all the things ... } | 
|  | 28 // typedef PolymerElement = HtmlElement with Polymer, Observable; | 
|  | 29 abstract class Polymer { | 
|  | 30   // TODO(jmesserly): should this really be public? | 
|  | 31   /** Regular expression that matches data-bindings. */ | 
|  | 32   static final bindPattern = new RegExp(r'\{\{([^{}]*)}}'); | 
|  | 33 | 
|  | 34   /** | 
|  | 35    * Like [document.register] but for Polymer elements. | 
|  | 36    * | 
|  | 37    * Use the [name] to specify custom elment's tag name, for example: | 
|  | 38    * "fancy-button" if the tag is used as `<fancy-button>`. | 
|  | 39    * | 
|  | 40    * The [type] is the type to construct. If not supplied, it defaults to | 
|  | 41    * [PolymerElement]. | 
|  | 42    */ | 
|  | 43   // NOTE: this is called "element" in src/declaration/polymer-element.js, and | 
|  | 44   // exported as "Polymer". | 
|  | 45   static void register(String name, [Type type]) { | 
|  | 46     //console.log('registering [' + name + ']'); | 
|  | 47     if (type == null) type = PolymerElement; | 
|  | 48     _registerClassMirror(name, reflectClass(type)); | 
|  | 49   } | 
|  | 50 | 
|  | 51   // TODO(jmesserly): we use ClassMirror internall for now, until it is possible | 
|  | 52   // to get from ClassMirror -> Type. | 
|  | 53   static void _registerClassMirror(String name, ClassMirror type) { | 
|  | 54     _typesByName[name] = type; | 
|  | 55     // notify the registrar waiting for 'name', if any | 
|  | 56     _notifyType(name); | 
|  | 57   } | 
|  | 58 } | 
|  | 59 | 
|  | 60 /** | 
|  | 61  * The base class for Polymer elements. It provides convience features on top | 
|  | 62  * of the custom elements web standard. | 
|  | 63  */ | 
|  | 64 class PolymerElement extends CustomElement with ObservableMixin { | 
|  | 65   // Fully ported from revision: | 
|  | 66   // https://github.com/Polymer/polymer/blob/4dc481c11505991a7c43228d3797d28f212
     67779 | 
|  | 67   // | 
|  | 68   //   src/instance/attributes.js | 
|  | 69   //   src/instance/base.js | 
|  | 70   //   src/instance/events.js | 
|  | 71   //   src/instance/mdv.js | 
|  | 72   //   src/instance/properties.js | 
|  | 73   //   src/instance/utils.js | 
|  | 74   // | 
|  | 75   // Not yet ported: | 
|  | 76   //   src/instance/style.js -- blocked on ShadowCSS.shimPolyfillDirectives | 
|  | 77 | 
|  | 78   /// The one syntax to rule them all. | 
|  | 79   static final BindingDelegate _polymerSyntax = new PolymerExpressions(); | 
|  | 80 | 
|  | 81   static int _preparingElements = 0; | 
|  | 82 | 
|  | 83   PolymerDeclaration _declaration; | 
|  | 84 | 
|  | 85   /** The most derived `<polymer-element>` declaration for this element. */ | 
|  | 86   PolymerDeclaration get declaration => _declaration; | 
|  | 87 | 
|  | 88   Map<String, StreamSubscription> _elementObservers; | 
|  | 89   bool _unbound; // lazy-initialized | 
|  | 90   Job _unbindAllJob; | 
|  | 91 | 
|  | 92   bool get _elementPrepared => _declaration != null; | 
|  | 93 | 
|  | 94   bool get applyAuthorStyles => false; | 
|  | 95   bool get resetStyleInheritance => false; | 
|  | 96   bool get alwaysPrepare => false; | 
|  | 97 | 
|  | 98   /** | 
|  | 99    * Shadow roots created by [parseElement]. See [getShadowRoot]. | 
|  | 100    */ | 
|  | 101   final _shadowRoots = new HashMap<String, ShadowRoot>(); | 
|  | 102 | 
|  | 103   /** Map of items in the shadow root(s) by their [Element.id]. */ | 
|  | 104   // TODO(jmesserly): various issues: | 
|  | 105   // * wrap in UnmodifiableMapView? | 
|  | 106   // * should we have an object that implements noSuchMethod? | 
|  | 107   // * should the map have a key order (e.g. LinkedHash or SplayTree)? | 
|  | 108   // * should this be a live list? Polymer doesn't, maybe due to JS limitations? | 
|  | 109   // For now I picked the most performant choice: non-live HashMap. | 
|  | 110   final Map<String, Element> $ = new HashMap<String, Element>(); | 
|  | 111 | 
|  | 112   /** | 
|  | 113    * Gets the shadow root associated with the corresponding custom element. | 
|  | 114    * | 
|  | 115    * This is identical to [shadowRoot], unless there are multiple levels of | 
|  | 116    * inheritance and they each have their own shadow root. For example, | 
|  | 117    * this can happen if the base class and subclass both have `<template>` tags | 
|  | 118    * in their `<polymer-element>` tags. | 
|  | 119    */ | 
|  | 120   // TODO(jmesserly): Polymer does not have this feature. Reconcile. | 
|  | 121   ShadowRoot getShadowRoot(String customTagName) => _shadowRoots[customTagName]; | 
|  | 122 | 
|  | 123   ShadowRoot createShadowRoot([name]) { | 
|  | 124     if (name != null) { | 
|  | 125       throw new ArgumentError('name argument must not be supplied.'); | 
|  | 126     } | 
|  | 127 | 
|  | 128     // Provides ability to traverse from ShadowRoot to the host. | 
|  | 129     // TODO(jmessery): remove once we have this ability on the DOM. | 
|  | 130     final root = super.createShadowRoot(); | 
|  | 131     _shadowHost[root] = host; | 
|  | 132     return root; | 
|  | 133   } | 
|  | 134 | 
|  | 135   /** | 
|  | 136    * Invoke [callback] in [wait], unless the job is re-registered, | 
|  | 137    * which resets the timer. For example: | 
|  | 138    * | 
|  | 139    *     _myJob = job(_myJob, callback, const Duration(milliseconds: 100)); | 
|  | 140    * | 
|  | 141    * Returns a job handle which can be used to re-register a job. | 
|  | 142    */ | 
|  | 143   Job job(Job job, void callback(), Duration wait) => | 
|  | 144       runJob(job, callback, wait); | 
|  | 145 | 
|  | 146   // TODO(jmesserly): I am not sure if we should have the | 
|  | 147   // created/createdCallback distinction. See post here: | 
|  | 148   // https://groups.google.com/d/msg/polymer-dev/W0ZUpU5caIM/v5itFnvnehEJ | 
|  | 149   // Same issue with inserted and removed. | 
|  | 150   void created() { | 
|  | 151     if (document.window != null || alwaysPrepare || _preparingElements > 0) { | 
|  | 152       prepareElement(); | 
|  | 153     } | 
|  | 154   } | 
|  | 155 | 
|  | 156   void prepareElement() { | 
|  | 157     // Dart note: get the _declaration, which also marks _elementPrepared | 
|  | 158     _declaration = _getDeclaration(reflect(this).type); | 
|  | 159     // do this first so we can observe changes during initialization | 
|  | 160     observeProperties(); | 
|  | 161     // install boilerplate attributes | 
|  | 162     copyInstanceAttributes(); | 
|  | 163     // process input attributes | 
|  | 164     takeAttributes(); | 
|  | 165     // add event listeners | 
|  | 166     addHostListeners(); | 
|  | 167     // guarantees that while preparing, any sub-elements will also be prepared | 
|  | 168     _preparingElements++; | 
|  | 169     // process declarative resources | 
|  | 170     parseDeclarations(_declaration); | 
|  | 171     _preparingElements--; | 
|  | 172     // user entry point | 
|  | 173     ready(); | 
|  | 174   } | 
|  | 175 | 
|  | 176   /** Called when [prepareElement] is finished. */ | 
|  | 177   void ready() {} | 
|  | 178 | 
|  | 179   void inserted() { | 
|  | 180     if (!_elementPrepared) { | 
|  | 181       prepareElement(); | 
|  | 182     } | 
|  | 183     cancelUnbindAll(preventCascade: true); | 
|  | 184   } | 
|  | 185 | 
|  | 186   void removed() { | 
|  | 187     asyncUnbindAll(); | 
|  | 188   } | 
|  | 189 | 
|  | 190   /** Recursive ancestral <element> initialization, oldest first. */ | 
|  | 191   void parseDeclarations(PolymerDeclaration declaration) { | 
|  | 192     if (declaration != null) { | 
|  | 193       parseDeclarations(declaration.superDeclaration); | 
|  | 194       parseDeclaration(declaration.host); | 
|  | 195     } | 
|  | 196   } | 
|  | 197 | 
|  | 198   /** | 
|  | 199    * Parse input `<polymer-element>` as needed, override for custom behavior. | 
|  | 200    */ | 
|  | 201   void parseDeclaration(Element elementElement) { | 
|  | 202     var root = shadowFromTemplate(fetchTemplate(elementElement)); | 
|  | 203 | 
|  | 204     // Dart note: this is extra code compared to Polymer to support | 
|  | 205     // the getShadowRoot method. | 
|  | 206     if (root == null) return; | 
|  | 207 | 
|  | 208     var name = elementElement.attributes['name']; | 
|  | 209     if (name == null) return; | 
|  | 210     _shadowRoots[name] = root; | 
|  | 211   } | 
|  | 212 | 
|  | 213   /** | 
|  | 214    * Return a shadow-root template (if desired), override for custom behavior. | 
|  | 215    */ | 
|  | 216   Element fetchTemplate(Element elementElement) => | 
|  | 217       elementElement.query('template'); | 
|  | 218 | 
|  | 219   /** Utility function that creates a shadow root from a `<template>`. */ | 
|  | 220   ShadowRoot shadowFromTemplate(Element template) { | 
|  | 221     if (template == null) return null; | 
|  | 222     // cache elder shadow root (if any) | 
|  | 223     var elderRoot = this.shadowRoot; | 
|  | 224     // make a shadow root | 
|  | 225     var root = createShadowRoot(); | 
|  | 226     // migrate flag(s)( | 
|  | 227     root.applyAuthorStyles = applyAuthorStyles; | 
|  | 228     root.resetStyleInheritance = resetStyleInheritance; | 
|  | 229     // stamp template | 
|  | 230     // which includes parsing and applying MDV bindings before being | 
|  | 231     // inserted (to avoid {{}} in attribute values) | 
|  | 232     // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | 
|  | 233     var dom = instanceTemplate(template); | 
|  | 234     // append to shadow dom | 
|  | 235     root.append(dom); | 
|  | 236     // perform post-construction initialization tasks on shadow root | 
|  | 237     shadowRootReady(root, template); | 
|  | 238     // return the created shadow root | 
|  | 239     return root; | 
|  | 240   } | 
|  | 241 | 
|  | 242   void shadowRootReady(ShadowRoot root, Element template) { | 
|  | 243     // locate nodes with id and store references to them in this.$ hash | 
|  | 244     marshalNodeReferences(root); | 
|  | 245     // add local events of interest... | 
|  | 246     addInstanceListeners(root, template); | 
|  | 247     // TODO(jmesserly): port this | 
|  | 248     // set up pointer gestures | 
|  | 249     // PointerGestures.register(root); | 
|  | 250   } | 
|  | 251 | 
|  | 252   /** Locate nodes with id and store references to them in [$] hash. */ | 
|  | 253   void marshalNodeReferences(ShadowRoot root) { | 
|  | 254     if (root == null) return; | 
|  | 255     for (var n in root.queryAll('[id]')) { | 
|  | 256       $[n.id] = n; | 
|  | 257     } | 
|  | 258   } | 
|  | 259 | 
|  | 260   void attributeChanged(String name, String oldValue) { | 
|  | 261     if (name != 'class' && name != 'style') { | 
|  | 262       attributeToProperty(name, attributes[name]); | 
|  | 263     } | 
|  | 264   } | 
|  | 265 | 
|  | 266   // TODO(jmesserly): use stream or future here? | 
|  | 267   void onMutation(Node node, void listener(MutationObserver obs)) { | 
|  | 268     new MutationObserver((records, MutationObserver observer) { | 
|  | 269       listener(observer); | 
|  | 270       observer.disconnect(); | 
|  | 271     })..observe(node, childList: true, subtree: true); | 
|  | 272   } | 
|  | 273 | 
|  | 274   void copyInstanceAttributes() { | 
|  | 275     _declaration._instanceAttributes.forEach((name, value) { | 
|  | 276       attributes[name] = value; | 
|  | 277     }); | 
|  | 278   } | 
|  | 279 | 
|  | 280   void takeAttributes() { | 
|  | 281     if (_declaration._publishLC == null) return; | 
|  | 282     attributes.forEach(attributeToProperty); | 
|  | 283   } | 
|  | 284 | 
|  | 285   /** | 
|  | 286    * If attribute [name] is mapped to a property, deserialize | 
|  | 287    * [value] into that property. | 
|  | 288    */ | 
|  | 289   void attributeToProperty(String name, String value) { | 
|  | 290     // try to match this attribute to a property (attributes are | 
|  | 291     // all lower-case, so this is case-insensitive search) | 
|  | 292     var property = propertyForAttribute(name); | 
|  | 293     if (property == null) return; | 
|  | 294 | 
|  | 295     // filter out 'mustached' values, these are to be | 
|  | 296     // replaced with bound-data and are not yet values | 
|  | 297     // themselves. | 
|  | 298     if (value == null || value.contains(Polymer.bindPattern)) return; | 
|  | 299 | 
|  | 300     // get original value | 
|  | 301     final self = reflect(this); | 
|  | 302     final defaultValue = self.getField(property.simpleName).reflectee; | 
|  | 303 | 
|  | 304     // deserialize Boolean or Number values from attribute | 
|  | 305     final newValue = deserializeValue(value, defaultValue, | 
|  | 306         _inferPropertyType(defaultValue, property)); | 
|  | 307 | 
|  | 308     // only act if the value has changed | 
|  | 309     if (!identical(newValue, defaultValue)) { | 
|  | 310       // install new value (has side-effects) | 
|  | 311       self.setField(property.simpleName, newValue); | 
|  | 312     } | 
|  | 313   } | 
|  | 314 | 
|  | 315   /** Return the published property matching name, or null. */ | 
|  | 316   // TODO(jmesserly): should we just return Symbol here? | 
|  | 317   DeclarationMirror propertyForAttribute(String name) { | 
|  | 318     final publishLC = _declaration._publishLC; | 
|  | 319     if (publishLC == null) return null; | 
|  | 320     //console.log('propertyForAttribute:', name, 'matches', match); | 
|  | 321     return publishLC[name]; | 
|  | 322   } | 
|  | 323 | 
|  | 324   /** | 
|  | 325    * Convert representation of [value] based on [type] and [defaultValue]. | 
|  | 326    */ | 
|  | 327   // TODO(jmesserly): this should probably take a ClassMirror instead of | 
|  | 328   // TypeMirror, but it is currently impossible to get from a TypeMirror to a | 
|  | 329   // ClassMirror. | 
|  | 330   Object deserializeValue(String value, Object defaultValue, TypeMirror type) => | 
|  | 331       deserialize.deserializeValue(value, defaultValue, type); | 
|  | 332 | 
|  | 333   String serializeValue(Object value, TypeMirror inferredType) { | 
|  | 334     if (value == null) return null; | 
|  | 335 | 
|  | 336     final type = inferredType.qualifiedName; | 
|  | 337     if (type == const Symbol('dart.core.bool')) { | 
|  | 338       return _toBoolean(value) ? '' : null; | 
|  | 339     } else if (type == const Symbol('dart.core.String') | 
|  | 340         || type == const Symbol('dart.core.int') | 
|  | 341         || type == const Symbol('dart.core.double')) { | 
|  | 342       return '$value'; | 
|  | 343     } | 
|  | 344     return null; | 
|  | 345   } | 
|  | 346 | 
|  | 347   void reflectPropertyToAttribute(String name) { | 
|  | 348     // TODO(sjmiles): consider memoizing this | 
|  | 349     final self = reflect(this); | 
|  | 350     // try to intelligently serialize property value | 
|  | 351     // TODO(jmesserly): cache symbol? | 
|  | 352     final propValue = self.getField(new Symbol(name)).reflectee; | 
|  | 353     final property = _declaration._publish[name]; | 
|  | 354     var inferredType = _inferPropertyType(propValue, property); | 
|  | 355     final serializedValue = serializeValue(propValue, inferredType); | 
|  | 356     // boolean properties must reflect as boolean attributes | 
|  | 357     if (serializedValue != null) { | 
|  | 358       attributes[name] = serializedValue; | 
|  | 359       // TODO(sorvell): we should remove attr for all properties | 
|  | 360       // that have undefined serialization; however, we will need to | 
|  | 361       // refine the attr reflection system to achieve this; pica, for example, | 
|  | 362       // relies on having inferredType object properties not removed as | 
|  | 363       // attrs. | 
|  | 364     } else if (inferredType.qualifiedName == const Symbol('dart.core.bool')) { | 
|  | 365       attributes.remove(name); | 
|  | 366     } | 
|  | 367   } | 
|  | 368 | 
|  | 369   /** | 
|  | 370    * Creates the document fragment to use for each instance of the custom | 
|  | 371    * element, given the `<template>` node. By default this is equivalent to: | 
|  | 372    * | 
|  | 373    *     template.createInstance(this, polymerSyntax); | 
|  | 374    * | 
|  | 375    * Where polymerSyntax is a singleton `PolymerExpressions` instance from the | 
|  | 376    * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions
     ) | 
|  | 377    * package. | 
|  | 378    * | 
|  | 379    * You can override this method to change the instantiation behavior of the | 
|  | 380    * template, for example to use a different data-binding syntax. | 
|  | 381    */ | 
|  | 382   DocumentFragment instanceTemplate(Element template) => | 
|  | 383       template.createInstance(this, _polymerSyntax); | 
|  | 384 | 
|  | 385   NodeBinding bind(String name, model, String path) { | 
|  | 386     // note: binding is a prepare signal. This allows us to be sure that any | 
|  | 387     // property changes that occur as a result of binding will be observed. | 
|  | 388     if (!_elementPrepared) prepareElement(); | 
|  | 389 | 
|  | 390     var property = propertyForAttribute(name); | 
|  | 391     if (property != null) { | 
|  | 392       unbind(name); | 
|  | 393       // use n-way Polymer binding | 
|  | 394       var observer = bindProperty(property.simpleName, model, path); | 
|  | 395       // reflect bound property to attribute when binding | 
|  | 396       // to ensure binding is not left on attribute if property | 
|  | 397       // does not update due to not changing. | 
|  | 398       reflectPropertyToAttribute(name); | 
|  | 399       return bindings[name] = observer; | 
|  | 400     } else { | 
|  | 401       return super.bind(name, model, path); | 
|  | 402     } | 
|  | 403   } | 
|  | 404 | 
|  | 405   void asyncUnbindAll() { | 
|  | 406     if (_unbound == true) return; | 
|  | 407     _unbindLog.info('[$localName] asyncUnbindAll'); | 
|  | 408     _unbindAllJob = job(_unbindAllJob, unbindAll, const Duration(seconds: 0)); | 
|  | 409   } | 
|  | 410 | 
|  | 411   void unbindAll() { | 
|  | 412     if (_unbound == true) return; | 
|  | 413 | 
|  | 414     unbindAllProperties(); | 
|  | 415     super.unbindAll(); | 
|  | 416     _unbindNodeTree(shadowRoot); | 
|  | 417     // TODO(sjmiles): must also unbind inherited shadow roots | 
|  | 418     _unbound = true; | 
|  | 419   } | 
|  | 420 | 
|  | 421   void cancelUnbindAll({bool preventCascade}) { | 
|  | 422     if (_unbound == true) { | 
|  | 423       _unbindLog.warning( | 
|  | 424           '[$localName] already unbound, cannot cancel unbindAll'); | 
|  | 425       return; | 
|  | 426     } | 
|  | 427     _unbindLog.info('[$localName] cancelUnbindAll'); | 
|  | 428     if (_unbindAllJob != null) { | 
|  | 429       _unbindAllJob.stop(); | 
|  | 430       _unbindAllJob = null; | 
|  | 431     } | 
|  | 432 | 
|  | 433     // cancel unbinding our shadow tree iff we're not in the process of | 
|  | 434     // cascading our tree (as we do, for example, when the element is inserted). | 
|  | 435     if (preventCascade == true) return; | 
|  | 436     _forNodeTree(shadowRoot, (n) { | 
|  | 437       if (n is PolymerElement) { | 
|  | 438         (n as PolymerElement).cancelUnbindAll(); | 
|  | 439       } | 
|  | 440     }); | 
|  | 441   } | 
|  | 442 | 
|  | 443   static void _unbindNodeTree(Node node) { | 
|  | 444     _forNodeTree(node, (node) => node.unbindAll()); | 
|  | 445   } | 
|  | 446 | 
|  | 447   static void _forNodeTree(Node node, void callback(Node node)) { | 
|  | 448     if (node == null) return; | 
|  | 449 | 
|  | 450     callback(node); | 
|  | 451     for (var child = node.firstChild; child != null; child = child.nextNode) { | 
|  | 452       _forNodeTree(child, callback); | 
|  | 453     } | 
|  | 454   } | 
|  | 455 | 
|  | 456   /** Set up property observers. */ | 
|  | 457   void observeProperties() { | 
|  | 458     // TODO(sjmiles): | 
|  | 459     // we observe published properties so we can reflect them to attributes | 
|  | 460     // ~100% of our team's applications would work without this reflection, | 
|  | 461     // perhaps we can make it optional somehow | 
|  | 462     // | 
|  | 463     // add user's observers | 
|  | 464     final observe = _declaration._observe; | 
|  | 465     final publish = _declaration._publish; | 
|  | 466     if (observe != null) { | 
|  | 467       observe.forEach((name, value) { | 
|  | 468         if (publish != null && publish.containsKey(name)) { | 
|  | 469           observeBoth(name, value); | 
|  | 470         } else { | 
|  | 471           observeProperty(name, value); | 
|  | 472         } | 
|  | 473       }); | 
|  | 474     } | 
|  | 475     // add observers for published properties | 
|  | 476     if (publish != null) { | 
|  | 477       publish.forEach((name, value) { | 
|  | 478         if (observe == null || !observe.containsKey(name)) { | 
|  | 479           observeAttributeProperty(name); | 
|  | 480         } | 
|  | 481       }); | 
|  | 482     } | 
|  | 483   } | 
|  | 484 | 
|  | 485   void _observe(String name, void callback(newValue, oldValue)) { | 
|  | 486     _observeLog.info('[$localName] watching [$name]'); | 
|  | 487     // TODO(jmesserly): this is a little different than the JS version so we | 
|  | 488     // can pass the oldValue, which is missing from Dart's PathObserver. | 
|  | 489     // This probably gives us worse performance. | 
|  | 490     var path = new PathObserver(this, name); | 
|  | 491     Object oldValue = null; | 
|  | 492     _registerObserver(name, path.changes.listen((_) { | 
|  | 493       final newValue = path.value; | 
|  | 494       final old = oldValue; | 
|  | 495       oldValue = newValue; | 
|  | 496       callback(newValue, old); | 
|  | 497     })); | 
|  | 498   } | 
|  | 499 | 
|  | 500   void _registerObserver(String name, StreamSubscription sub) { | 
|  | 501     if (_elementObservers == null) { | 
|  | 502       _elementObservers = new Map<String, StreamSubscription>(); | 
|  | 503     } | 
|  | 504     _elementObservers[name] = sub; | 
|  | 505   } | 
|  | 506 | 
|  | 507   void observeAttributeProperty(String name) { | 
|  | 508     _observe(name, (value, old) => reflectPropertyToAttribute(name)); | 
|  | 509   } | 
|  | 510 | 
|  | 511   void observeProperty(String name, Symbol method) { | 
|  | 512     final self = reflect(this); | 
|  | 513     _observe(name, (value, old) => self.invoke(method, [old])); | 
|  | 514   } | 
|  | 515 | 
|  | 516   void observeBoth(String name, Symbol methodName) { | 
|  | 517     final self = reflect(this); | 
|  | 518     _observe(name, (value, old) { | 
|  | 519       reflectPropertyToAttribute(name); | 
|  | 520       self.invoke(methodName, [old]); | 
|  | 521     }); | 
|  | 522   } | 
|  | 523 | 
|  | 524   void unbindProperty(String name) { | 
|  | 525     if (_elementObservers == null) return; | 
|  | 526     var sub = _elementObservers.remove(name); | 
|  | 527     if (sub != null) sub.cancel(); | 
|  | 528   } | 
|  | 529 | 
|  | 530   void unbindAllProperties() { | 
|  | 531     if (_elementObservers == null) return; | 
|  | 532     for (var sub in _elementObservers.values) sub.cancel(); | 
|  | 533     _elementObservers.clear(); | 
|  | 534   } | 
|  | 535 | 
|  | 536   /** | 
|  | 537    * Bind a [property] in this object to a [path] in model. *Note* in Dart it | 
|  | 538    * is necessary to also define the field: | 
|  | 539    * | 
|  | 540    *     var myProperty; | 
|  | 541    * | 
|  | 542    *     created() { | 
|  | 543    *       super.created(); | 
|  | 544    *       bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); | 
|  | 545    *     } | 
|  | 546    */ | 
|  | 547   // TODO(jmesserly): replace with something more localized, like: | 
|  | 548   // @ComputedField('myModel.path.to.otherProp'); | 
|  | 549   NodeBinding bindProperty(Symbol name, Object model, String path) => | 
|  | 550       // apply Polymer two-way reference binding | 
|  | 551       _bindProperties(this, name, model, path); | 
|  | 552 | 
|  | 553   /** | 
|  | 554    * bind a property in A to a path in B by converting A[property] to a | 
|  | 555    * getter/setter pair that accesses B[...path...] | 
|  | 556    */ | 
|  | 557   static NodeBinding _bindProperties(PolymerElement inA, Symbol inProperty, | 
|  | 558         Object inB, String inPath) { | 
|  | 559 | 
|  | 560     if (_bindLog.isLoggable(Level.INFO)) { | 
|  | 561       _bindLog.info('[$inB]: bindProperties: [$inPath] to ' | 
|  | 562           '[${inA.localName}].[$inProperty]'); | 
|  | 563     } | 
|  | 564 | 
|  | 565     // Dart note: normally we only reach this code when we know it's a | 
|  | 566     // property, but if someone uses bindProperty directly they might get a | 
|  | 567     // NoSuchMethodError either from the getField below, or from the setField | 
|  | 568     // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight | 
|  | 569     // difference from Polymer.js behavior. | 
|  | 570 | 
|  | 571     // capture A's value if B's value is null or undefined, | 
|  | 572     // otherwise use B's value | 
|  | 573     var path = new PathObserver(inB, inPath); | 
|  | 574     if (path.value == null) { | 
|  | 575       path.value = reflect(inA).getField(inProperty).reflectee; | 
|  | 576     } | 
|  | 577     return new _PolymerBinding(inA, inProperty, inB, inPath); | 
|  | 578   } | 
|  | 579 | 
|  | 580   /** Attach event listeners on the host (this) element. */ | 
|  | 581   void addHostListeners() { | 
|  | 582     var events = _declaration._eventDelegates; | 
|  | 583     if (events.isEmpty) return; | 
|  | 584 | 
|  | 585     if (_eventsLog.isLoggable(Level.INFO)) { | 
|  | 586       _eventsLog.info('[$localName] addHostListeners: $events'); | 
|  | 587     } | 
|  | 588     addNodeListeners(this, events.keys, hostEventListener); | 
|  | 589   } | 
|  | 590 | 
|  | 591   /** Attach event listeners inside a shadow [root]. */ | 
|  | 592   void addInstanceListeners(ShadowRoot root, Element template) { | 
|  | 593     var templateDelegates = _declaration._templateDelegates; | 
|  | 594     if (templateDelegates == null) return; | 
|  | 595     var events = templateDelegates[template]; | 
|  | 596     if (events == null) return; | 
|  | 597 | 
|  | 598     if (_eventsLog.isLoggable(Level.INFO)) { | 
|  | 599       _eventsLog.info('[$localName] addInstanceListeners: $events'); | 
|  | 600     } | 
|  | 601     addNodeListeners(root, events, instanceEventListener); | 
|  | 602   } | 
|  | 603 | 
|  | 604   void addNodeListeners(Node node, Iterable<String> events, | 
|  | 605       void listener(Event e)) { | 
|  | 606 | 
|  | 607     for (var name in events) { | 
|  | 608       addNodeListener(node, name, listener); | 
|  | 609     } | 
|  | 610   } | 
|  | 611 | 
|  | 612   void addNodeListener(Node node, String event, void listener(Event e)) { | 
|  | 613     node.on[event].listen(listener); | 
|  | 614   } | 
|  | 615 | 
|  | 616   void hostEventListener(Event event) { | 
|  | 617     // TODO(jmesserly): do we need this check? It was using cancelBubble, see: | 
|  | 618     // https://github.com/Polymer/polymer/issues/292 | 
|  | 619     if (!event.bubbles) return; | 
|  | 620 | 
|  | 621     bool log = _eventsLog.isLoggable(Level.INFO); | 
|  | 622     if (log) { | 
|  | 623       _eventsLog.info('>>> [$localName]: hostEventListener(${event.type})'); | 
|  | 624     } | 
|  | 625 | 
|  | 626     var h = findEventDelegate(event); | 
|  | 627     if (h != null) { | 
|  | 628       if (log) _eventsLog.info('[$localName] found host handler name [$h]'); | 
|  | 629       var detail = event is CustomEvent ? | 
|  | 630           (event as CustomEvent).detail : null; | 
|  | 631       // TODO(jmesserly): cache the symbols? | 
|  | 632       dispatchMethod(new Symbol(h), [event, detail, this]); | 
|  | 633     } | 
|  | 634 | 
|  | 635     if (log) { | 
|  | 636       _eventsLog.info('<<< [$localName]: hostEventListener(${event.type})'); | 
|  | 637     } | 
|  | 638   } | 
|  | 639 | 
|  | 640   String findEventDelegate(Event event) => | 
|  | 641       _declaration._eventDelegates[_eventNameFromType(event.type)]; | 
|  | 642 | 
|  | 643   /** Call [methodName] method on [this] with [args], if the method exists. */ | 
|  | 644   // TODO(jmesserly): I removed the [node] argument as it was unused. Reconcile. | 
|  | 645   void dispatchMethod(Symbol methodName, List args) { | 
|  | 646     bool log = _eventsLog.isLoggable(Level.INFO); | 
|  | 647     if (log) _eventsLog.info('>>> [$localName]: dispatch $methodName'); | 
|  | 648 | 
|  | 649     // TODO(sigmund): consider making event listeners list all arguments | 
|  | 650     // explicitly. Unless VM mirrors are optimized first, this reflectClass call | 
|  | 651     // will be expensive once custom elements extend directly from Element (see | 
|  | 652     // dartbug.com/11108). | 
|  | 653     var self = reflect(this); | 
|  | 654     var method = self.type.methods[methodName]; | 
|  | 655     if (method != null) { | 
|  | 656       // This will either truncate the argument list or extend it with extra | 
|  | 657       // null arguments, so it will match the signature. | 
|  | 658       // TODO(sigmund): consider accepting optional arguments when we can tell | 
|  | 659       // them appart from named arguments (see http://dartbug.com/11334) | 
|  | 660       args.length = method.parameters.where((p) => !p.isOptional).length; | 
|  | 661     } | 
|  | 662     self.invoke(methodName, args); | 
|  | 663 | 
|  | 664     if (log) _eventsLog.info('<<< [$localName]: dispatch $methodName'); | 
|  | 665 | 
|  | 666     // TODO(jmesserly): workaround for HTML events not supporting zones. | 
|  | 667     performMicrotaskCheckpoint(); | 
|  | 668   } | 
|  | 669 | 
|  | 670   void instanceEventListener(Event event) { | 
|  | 671     _listenLocal(host, event); | 
|  | 672   } | 
|  | 673 | 
|  | 674   // TODO(sjmiles): much of the below privatized only because of the vague | 
|  | 675   // notion this code is too fiddly and we need to revisit the core feature | 
|  | 676   void _listenLocal(Element host, Event event) { | 
|  | 677     // TODO(jmesserly): do we need this check? It was using cancelBubble, see: | 
|  | 678     // https://github.com/Polymer/polymer/issues/292 | 
|  | 679     if (!event.bubbles) return; | 
|  | 680 | 
|  | 681     bool log = _eventsLog.isLoggable(Level.INFO); | 
|  | 682     if (log) _eventsLog.info('>>> [$localName]: listenLocal [${event.type}]'); | 
|  | 683 | 
|  | 684     final eventOn = '$_EVENT_PREFIX${_eventNameFromType(event.type)}'; | 
|  | 685     if (event.path == null) { | 
|  | 686       _listenLocalNoEventPath(host, event, eventOn); | 
|  | 687     } else { | 
|  | 688       _listenLocalEventPath(host, event, eventOn); | 
|  | 689     } | 
|  | 690 | 
|  | 691     if (log) _eventsLog.info('<<< [$localName]: listenLocal [${event.type}]'); | 
|  | 692   } | 
|  | 693 | 
|  | 694   static void _listenLocalEventPath(Element host, Event event, String eventOn) { | 
|  | 695     var c = null; | 
|  | 696     for (var target in event.path) { | 
|  | 697       // if we hit host, stop | 
|  | 698       if (identical(target, host)) return; | 
|  | 699 | 
|  | 700       // find a controller for the target, unless we already found `host` | 
|  | 701       // as a controller | 
|  | 702       c = identical(c, host) ? c : _findController(target); | 
|  | 703 | 
|  | 704       // if we have a controller, dispatch the event, and stop if the handler | 
|  | 705       // returns true | 
|  | 706       if (c != null && _handleEvent(c, target, event, eventOn)) { | 
|  | 707         return; | 
|  | 708       } | 
|  | 709     } | 
|  | 710   } | 
|  | 711 | 
|  | 712   // TODO(sorvell): remove when ShadowDOM polyfill supports event path. | 
|  | 713   // Note that _findController will not return the expected controller when the | 
|  | 714   // event target is a distributed node.  This is because we cannot traverse | 
|  | 715   // from a composed node to a node in shadowRoot. | 
|  | 716   // This will be addressed via an event path api | 
|  | 717   // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066 | 
|  | 718   static void _listenLocalNoEventPath(Element host, Event event, | 
|  | 719       String eventOn) { | 
|  | 720 | 
|  | 721     if (_eventsLog.isLoggable(Level.INFO)) { | 
|  | 722       _eventsLog.info('event.path() not supported for ${event.type}'); | 
|  | 723     } | 
|  | 724 | 
|  | 725     var target = event.target; | 
|  | 726     var c = null; | 
|  | 727     // if we hit dirt or host, stop | 
|  | 728     while (target != null && target != host) { | 
|  | 729       // find a controller for target `t`, unless we already found `host` | 
|  | 730       // as a controller | 
|  | 731       c = identical(c, host) ? c : _findController(target); | 
|  | 732 | 
|  | 733       // if we have a controller, dispatch the event, return 'true' if | 
|  | 734       // handler returns true | 
|  | 735       if (c != null && _handleEvent(c, target, event, eventOn)) { | 
|  | 736         return; | 
|  | 737       } | 
|  | 738       target = target.parent; | 
|  | 739     } | 
|  | 740   } | 
|  | 741 | 
|  | 742   // TODO(jmesserly): this won't find the correct host unless the ShadowRoot | 
|  | 743   // was created on a PolymerElement. | 
|  | 744   static Element _findController(Node node) { | 
|  | 745     while (node.parentNode != null) { | 
|  | 746       node = node.parentNode; | 
|  | 747     } | 
|  | 748     return _shadowHost[node]; | 
|  | 749   } | 
|  | 750 | 
|  | 751   static bool _handleEvent(Element ctrlr, Node node, Event event, | 
|  | 752       String eventOn) { | 
|  | 753 | 
|  | 754     // Note: local events are listened only in the shadow root. This dynamic | 
|  | 755     // lookup is used to distinguish determine whether the target actually has a | 
|  | 756     // listener, and if so, to determine lazily what's the target method. | 
|  | 757     var name = node is Element ? (node as Element).attributes[eventOn] : null; | 
|  | 758     if (name != null && _handleIfNotHandled(node, event)) { | 
|  | 759       if (_eventsLog.isLoggable(Level.INFO)) { | 
|  | 760         _eventsLog.info('[${ctrlr.localName}] found handler name [$name]'); | 
|  | 761       } | 
|  | 762       var detail = event is CustomEvent ? | 
|  | 763           (event as CustomEvent).detail : null; | 
|  | 764 | 
|  | 765       if (node != null) { | 
|  | 766         // TODO(jmesserly): cache symbols? | 
|  | 767         ctrlr.xtag.dispatchMethod(new Symbol(name), [event, detail, node]); | 
|  | 768       } | 
|  | 769     } | 
|  | 770 | 
|  | 771     // TODO(jmesserly): do we need this? It was using cancelBubble, see: | 
|  | 772     // https://github.com/Polymer/polymer/issues/292 | 
|  | 773     return !event.bubbles; | 
|  | 774   } | 
|  | 775 | 
|  | 776   // TODO(jmesserly): I don't understand this bit. It seems to be a duplicate | 
|  | 777   // delivery prevention mechanism? | 
|  | 778   static bool _handleIfNotHandled(Node node, Event event) { | 
|  | 779     var list = _eventHandledTable[event]; | 
|  | 780     if (list == null) _eventHandledTable[event] = list = new Set<Node>(); | 
|  | 781     if (!list.contains(node)) { | 
|  | 782       list.add(node); | 
|  | 783       return true; | 
|  | 784     } | 
|  | 785     return false; | 
|  | 786   } | 
|  | 787 | 
|  | 788   /** | 
|  | 789    * Invokes a function asynchronously. | 
|  | 790    * This will call `Platform.flush()` and then return a `new Timer` | 
|  | 791    * with the provided [method] and [timeout]. | 
|  | 792    * | 
|  | 793    * If you would prefer to run the callback using | 
|  | 794    * [window.requestAnimationFrame], see the [async] method. | 
|  | 795    */ | 
|  | 796   // Dart note: "async" is split into 2 methods so it can have a sensible type | 
|  | 797   // signatures. Also removed the various features that don't make sense in a | 
|  | 798   // Dart world, like binding to "this" and taking arguments list. | 
|  | 799   Timer asyncTimer(void method(), Duration timeout) { | 
|  | 800     // when polyfilling Object.observe, ensure changes | 
|  | 801     // propagate before executing the async method | 
|  | 802     platform.flush(); | 
|  | 803     return new Timer(timeout, method); | 
|  | 804   } | 
|  | 805 | 
|  | 806   /** | 
|  | 807    * Invokes a function asynchronously. | 
|  | 808    * This will call `Platform.flush()` and then call | 
|  | 809    * [window.requestAnimationFrame] with the provided [method] and return the | 
|  | 810    * result. | 
|  | 811    * | 
|  | 812    * If you would prefer to run the callback after a given duration, see | 
|  | 813    * the [asyncTimer] method. | 
|  | 814    */ | 
|  | 815   int async(RequestAnimationFrameCallback method) { | 
|  | 816     // when polyfilling Object.observe, ensure changes | 
|  | 817     // propagate before executing the async method | 
|  | 818     platform.flush(); | 
|  | 819     return window.requestAnimationFrame(method); | 
|  | 820   } | 
|  | 821 | 
|  | 822   /** | 
|  | 823    * Fire a [CustomEvent] targeting [toNode], or this if toNode is not | 
|  | 824    * supplied. Returns the [detail] object. | 
|  | 825    */ | 
|  | 826   Object fire(String type, {Object detail, Node toNode, bool canBubble}) { | 
|  | 827     var node = toNode != null ? toNode : this; | 
|  | 828     //log.events && console.log('[%s]: sending [%s]', node.localName, inType); | 
|  | 829     node.dispatchEvent(new CustomEvent( | 
|  | 830       type, | 
|  | 831       canBubble: canBubble != null ? canBubble : true, | 
|  | 832       detail: detail | 
|  | 833     )); | 
|  | 834     return detail; | 
|  | 835   } | 
|  | 836 | 
|  | 837   /** | 
|  | 838    * Fire an event asynchronously. See [async] and [fire]. | 
|  | 839    */ | 
|  | 840   asyncFire(String type, {Object detail, Node toNode, bool canBubble}) { | 
|  | 841     // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to | 
|  | 842     // add "() =>" | 
|  | 843     async((x) => fire( | 
|  | 844         type, detail: detail, toNode: toNode, canBubble: canBubble)); | 
|  | 845   } | 
|  | 846 | 
|  | 847   /** | 
|  | 848    * Remove [className] from [old], add class to [anew], if they exist. | 
|  | 849    */ | 
|  | 850   void classFollows(Element anew, Element old, String className) { | 
|  | 851     if (old != null) { | 
|  | 852       old.classes.remove(className); | 
|  | 853     } | 
|  | 854     if (anew != null) { | 
|  | 855       anew.classes.add(className); | 
|  | 856     } | 
|  | 857   } | 
|  | 858 } | 
|  | 859 | 
|  | 860 // Dart note: Polymer addresses n-way bindings by metaprogramming: redefine | 
|  | 861 // the property on the PolymerElement instance to always get its value from the | 
|  | 862 // model@path. We can't replicate this in Dart so we do the next best thing: | 
|  | 863 // listen to changes on both sides and update the values. | 
|  | 864 // TODO(jmesserly): our approach leads to race conditions in the bindings. | 
|  | 865 // See http://code.google.com/p/dart/issues/detail?id=13567 | 
|  | 866 class _PolymerBinding extends NodeBinding { | 
|  | 867   final InstanceMirror _target; | 
|  | 868   final Symbol _property; | 
|  | 869   StreamSubscription _sub; | 
|  | 870   Object _lastValue; | 
|  | 871 | 
|  | 872   _PolymerBinding(PolymerElement node, Symbol property, model, path) | 
|  | 873       : _target = reflect(node), | 
|  | 874         _property = property, | 
|  | 875         super(node, MirrorSystem.getName(property), model, path) { | 
|  | 876 | 
|  | 877     _sub = node.changes.listen(_propertyValueChanged); | 
|  | 878   } | 
|  | 879 | 
|  | 880   void close() { | 
|  | 881     if (closed) return; | 
|  | 882     _sub.cancel(); | 
|  | 883     super.close(); | 
|  | 884   } | 
|  | 885 | 
|  | 886   void boundValueChanged(newValue) { | 
|  | 887     _lastValue = newValue; | 
|  | 888     _target.setField(_property, newValue); | 
|  | 889   } | 
|  | 890 | 
|  | 891   void _propertyValueChanged(List<ChangeRecord> records) { | 
|  | 892     for (var record in records) { | 
|  | 893       if (record.changes(_property)) { | 
|  | 894         final newValue = _target.getField(_property).reflectee; | 
|  | 895         if (!identical(_lastValue, newValue)) { | 
|  | 896           value = newValue; | 
|  | 897         } | 
|  | 898         return; | 
|  | 899       } | 
|  | 900     } | 
|  | 901   } | 
|  | 902 } | 
|  | 903 | 
|  | 904 bool _toBoolean(value) => null != value && false != value; | 
|  | 905 | 
|  | 906 TypeMirror _propertyType(DeclarationMirror property) => | 
|  | 907     property is VariableMirror | 
|  | 908         ? (property as VariableMirror).type | 
|  | 909         : (property as MethodMirror).returnType; | 
|  | 910 | 
|  | 911 TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { | 
|  | 912   var type = _propertyType(property); | 
|  | 913   if (type.qualifiedName == const Symbol('dart.core.Object') || | 
|  | 914       type.qualifiedName == const Symbol('dynamic')) { | 
|  | 915     // Attempt to infer field type from the default value. | 
|  | 916     if (value != null) { | 
|  | 917       type = reflect(value).type; | 
|  | 918     } | 
|  | 919   } | 
|  | 920   return type; | 
|  | 921 } | 
|  | 922 | 
|  | 923 final Logger _observeLog = new Logger('polymer.observe'); | 
|  | 924 final Logger _eventsLog = new Logger('polymer.events'); | 
|  | 925 final Logger _unbindLog = new Logger('polymer.unbind'); | 
|  | 926 final Logger _bindLog = new Logger('polymer.bind'); | 
|  | 927 | 
|  | 928 final Expando _shadowHost = new Expando<Element>(); | 
|  | 929 | 
|  | 930 final Expando _eventHandledTable = new Expando<Set<Node>>(); | 
| OLD | NEW | 
|---|