Index: pkg/polymer/lib/src/instance.dart |
diff --git a/pkg/polymer/lib/src/instance.dart b/pkg/polymer/lib/src/instance.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..f0bff99c4637274b00dd6c5d15419547f71b76d9 |
--- /dev/null |
+++ b/pkg/polymer/lib/src/instance.dart |
@@ -0,0 +1,930 @@ |
+// Copyright (c) 2013, 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. |
+ |
+part of polymer; |
+ |
+/** |
+ * Use this annotation to publish a field as an attribute. For example: |
+ * |
+ * class MyPlaybackElement extends PolymerElement { |
+ * // This will be available as an HTML attribute, for example: |
+ * // <my-playback volume="11"> |
+ * @published double volume; |
+ * } |
+ */ |
+// TODO(jmesserly): does @published imply @observable or vice versa? |
+const published = const PublishedProperty(); |
+ |
+/** An annotation used to publish a field as an attribute. See [published]. */ |
+class PublishedProperty extends ObservableProperty { |
+ const PublishedProperty(); |
+} |
+ |
+// TODO(jmesserly): make this the mixin so we can have Polymer type extensions, |
+// and move the implementation of PolymerElement in here. Once done it will look |
+// like: |
+// abstract class Polymer { ... all the things ... } |
+// typedef PolymerElement = HtmlElement with Polymer, Observable; |
+abstract class Polymer { |
+ // TODO(jmesserly): should this really be public? |
+ /** Regular expression that matches data-bindings. */ |
+ static final bindPattern = new RegExp(r'\{\{([^{}]*)}}'); |
+ |
+ /** |
+ * Like [document.register] but for Polymer elements. |
+ * |
+ * Use the [name] to specify custom elment's tag name, for example: |
+ * "fancy-button" if the tag is used as `<fancy-button>`. |
+ * |
+ * The [type] is the type to construct. If not supplied, it defaults to |
+ * [PolymerElement]. |
+ */ |
+ // NOTE: this is called "element" in src/declaration/polymer-element.js, and |
+ // exported as "Polymer". |
+ static void register(String name, [Type type]) { |
+ //console.log('registering [' + name + ']'); |
+ if (type == null) type = PolymerElement; |
+ _registerClassMirror(name, reflectClass(type)); |
+ } |
+ |
+ // TODO(jmesserly): we use ClassMirror internall for now, until it is possible |
+ // to get from ClassMirror -> Type. |
+ static void _registerClassMirror(String name, ClassMirror type) { |
+ _typesByName[name] = type; |
+ // notify the registrar waiting for 'name', if any |
+ _notifyType(name); |
+ } |
+} |
+ |
+/** |
+ * The base class for Polymer elements. It provides convience features on top |
+ * of the custom elements web standard. |
+ */ |
+class PolymerElement extends CustomElement with ObservableMixin { |
+ // Fully ported from revision: |
+ // https://github.com/Polymer/polymer/blob/4dc481c11505991a7c43228d3797d28f21267779 |
+ // |
+ // src/instance/attributes.js |
+ // src/instance/base.js |
+ // src/instance/events.js |
+ // src/instance/mdv.js |
+ // src/instance/properties.js |
+ // src/instance/utils.js |
+ // |
+ // Not yet ported: |
+ // src/instance/style.js -- blocked on ShadowCSS.shimPolyfillDirectives |
+ |
+ /// The one syntax to rule them all. |
+ static final BindingDelegate _polymerSyntax = new PolymerExpressions(); |
+ |
+ static int _preparingElements = 0; |
+ |
+ PolymerDeclaration _declaration; |
+ |
+ /** The most derived `<polymer-element>` declaration for this element. */ |
+ PolymerDeclaration get declaration => _declaration; |
+ |
+ Map<String, StreamSubscription> _elementObservers; |
+ bool _unbound; // lazy-initialized |
+ Job _unbindAllJob; |
+ |
+ bool get _elementPrepared => _declaration != null; |
+ |
+ bool get applyAuthorStyles => false; |
+ bool get resetStyleInheritance => false; |
+ bool get alwaysPrepare => false; |
+ |
+ /** |
+ * Shadow roots created by [parseElement]. See [getShadowRoot]. |
+ */ |
+ final _shadowRoots = new HashMap<String, ShadowRoot>(); |
+ |
+ /** Map of items in the shadow root(s) by their [Element.id]. */ |
+ // TODO(jmesserly): various issues: |
+ // * wrap in UnmodifiableMapView? |
+ // * should we have an object that implements noSuchMethod? |
+ // * should the map have a key order (e.g. LinkedHash or SplayTree)? |
+ // * should this be a live list? Polymer doesn't, maybe due to JS limitations? |
+ // For now I picked the most performant choice: non-live HashMap. |
+ final Map<String, Element> $ = new HashMap<String, Element>(); |
+ |
+ /** |
+ * Gets the shadow root associated with the corresponding custom element. |
+ * |
+ * This is identical to [shadowRoot], unless there are multiple levels of |
+ * inheritance and they each have their own shadow root. For example, |
+ * this can happen if the base class and subclass both have `<template>` tags |
+ * in their `<polymer-element>` tags. |
+ */ |
+ // TODO(jmesserly): Polymer does not have this feature. Reconcile. |
+ ShadowRoot getShadowRoot(String customTagName) => _shadowRoots[customTagName]; |
+ |
+ ShadowRoot createShadowRoot([name]) { |
+ if (name != null) { |
+ throw new ArgumentError('name argument must not be supplied.'); |
+ } |
+ |
+ // Provides ability to traverse from ShadowRoot to the host. |
+ // TODO(jmessery): remove once we have this ability on the DOM. |
+ final root = super.createShadowRoot(); |
+ _shadowHost[root] = host; |
+ return root; |
+ } |
+ |
+ /** |
+ * Invoke [callback] in [wait], unless the job is re-registered, |
+ * which resets the timer. For example: |
+ * |
+ * _myJob = job(_myJob, callback, const Duration(milliseconds: 100)); |
+ * |
+ * Returns a job handle which can be used to re-register a job. |
+ */ |
+ Job job(Job job, void callback(), Duration wait) => |
+ runJob(job, callback, wait); |
+ |
+ // TODO(jmesserly): I am not sure if we should have the |
+ // created/createdCallback distinction. See post here: |
+ // https://groups.google.com/d/msg/polymer-dev/W0ZUpU5caIM/v5itFnvnehEJ |
+ // Same issue with inserted and removed. |
+ void created() { |
+ if (document.window != null || alwaysPrepare || _preparingElements > 0) { |
+ prepareElement(); |
+ } |
+ } |
+ |
+ void prepareElement() { |
+ // Dart note: get the _declaration, which also marks _elementPrepared |
+ _declaration = _getDeclaration(reflect(this).type); |
+ // do this first so we can observe changes during initialization |
+ observeProperties(); |
+ // install boilerplate attributes |
+ copyInstanceAttributes(); |
+ // process input attributes |
+ takeAttributes(); |
+ // add event listeners |
+ addHostListeners(); |
+ // guarantees that while preparing, any sub-elements will also be prepared |
+ _preparingElements++; |
+ // process declarative resources |
+ parseDeclarations(_declaration); |
+ _preparingElements--; |
+ // user entry point |
+ ready(); |
+ } |
+ |
+ /** Called when [prepareElement] is finished. */ |
+ void ready() {} |
+ |
+ void inserted() { |
+ if (!_elementPrepared) { |
+ prepareElement(); |
+ } |
+ cancelUnbindAll(preventCascade: true); |
+ } |
+ |
+ void removed() { |
+ asyncUnbindAll(); |
+ } |
+ |
+ /** Recursive ancestral <element> initialization, oldest first. */ |
+ void parseDeclarations(PolymerDeclaration declaration) { |
+ if (declaration != null) { |
+ parseDeclarations(declaration.superDeclaration); |
+ parseDeclaration(declaration.host); |
+ } |
+ } |
+ |
+ /** |
+ * Parse input `<polymer-element>` as needed, override for custom behavior. |
+ */ |
+ void parseDeclaration(Element elementElement) { |
+ var root = shadowFromTemplate(fetchTemplate(elementElement)); |
+ |
+ // Dart note: this is extra code compared to Polymer to support |
+ // the getShadowRoot method. |
+ if (root == null) return; |
+ |
+ var name = elementElement.attributes['name']; |
+ if (name == null) return; |
+ _shadowRoots[name] = root; |
+ } |
+ |
+ /** |
+ * Return a shadow-root template (if desired), override for custom behavior. |
+ */ |
+ Element fetchTemplate(Element elementElement) => |
+ elementElement.query('template'); |
+ |
+ /** Utility function that creates a shadow root from a `<template>`. */ |
+ ShadowRoot shadowFromTemplate(Element template) { |
+ if (template == null) return null; |
+ // cache elder shadow root (if any) |
+ var elderRoot = this.shadowRoot; |
+ // make a shadow root |
+ var root = createShadowRoot(); |
+ // migrate flag(s)( |
+ root.applyAuthorStyles = applyAuthorStyles; |
+ root.resetStyleInheritance = resetStyleInheritance; |
+ // stamp template |
+ // which includes parsing and applying MDV bindings before being |
+ // inserted (to avoid {{}} in attribute values) |
+ // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. |
+ var dom = instanceTemplate(template); |
+ // append to shadow dom |
+ root.append(dom); |
+ // perform post-construction initialization tasks on shadow root |
+ shadowRootReady(root, template); |
+ // return the created shadow root |
+ return root; |
+ } |
+ |
+ void shadowRootReady(ShadowRoot root, Element template) { |
+ // locate nodes with id and store references to them in this.$ hash |
+ marshalNodeReferences(root); |
+ // add local events of interest... |
+ addInstanceListeners(root, template); |
+ // TODO(jmesserly): port this |
+ // set up pointer gestures |
+ // PointerGestures.register(root); |
+ } |
+ |
+ /** Locate nodes with id and store references to them in [$] hash. */ |
+ void marshalNodeReferences(ShadowRoot root) { |
+ if (root == null) return; |
+ for (var n in root.queryAll('[id]')) { |
+ $[n.id] = n; |
+ } |
+ } |
+ |
+ void attributeChanged(String name, String oldValue) { |
+ if (name != 'class' && name != 'style') { |
+ attributeToProperty(name, attributes[name]); |
+ } |
+ } |
+ |
+ // TODO(jmesserly): use stream or future here? |
+ void onMutation(Node node, void listener(MutationObserver obs)) { |
+ new MutationObserver((records, MutationObserver observer) { |
+ listener(observer); |
+ observer.disconnect(); |
+ })..observe(node, childList: true, subtree: true); |
+ } |
+ |
+ void copyInstanceAttributes() { |
+ _declaration._instanceAttributes.forEach((name, value) { |
+ attributes[name] = value; |
+ }); |
+ } |
+ |
+ void takeAttributes() { |
+ if (_declaration._publishLC == null) return; |
+ attributes.forEach(attributeToProperty); |
+ } |
+ |
+ /** |
+ * If attribute [name] is mapped to a property, deserialize |
+ * [value] into that property. |
+ */ |
+ void attributeToProperty(String name, String value) { |
+ // try to match this attribute to a property (attributes are |
+ // all lower-case, so this is case-insensitive search) |
+ var property = propertyForAttribute(name); |
+ if (property == null) return; |
+ |
+ // filter out 'mustached' values, these are to be |
+ // replaced with bound-data and are not yet values |
+ // themselves. |
+ if (value == null || value.contains(Polymer.bindPattern)) return; |
+ |
+ // get original value |
+ final self = reflect(this); |
+ final defaultValue = self.getField(property.simpleName).reflectee; |
+ |
+ // deserialize Boolean or Number values from attribute |
+ final newValue = deserializeValue(value, defaultValue, |
+ _inferPropertyType(defaultValue, property)); |
+ |
+ // only act if the value has changed |
+ if (!identical(newValue, defaultValue)) { |
+ // install new value (has side-effects) |
+ self.setField(property.simpleName, newValue); |
+ } |
+ } |
+ |
+ /** Return the published property matching name, or null. */ |
+ // TODO(jmesserly): should we just return Symbol here? |
+ DeclarationMirror propertyForAttribute(String name) { |
+ final publishLC = _declaration._publishLC; |
+ if (publishLC == null) return null; |
+ //console.log('propertyForAttribute:', name, 'matches', match); |
+ return publishLC[name]; |
+ } |
+ |
+ /** |
+ * Convert representation of [value] based on [type] and [defaultValue]. |
+ */ |
+ // TODO(jmesserly): this should probably take a ClassMirror instead of |
+ // TypeMirror, but it is currently impossible to get from a TypeMirror to a |
+ // ClassMirror. |
+ Object deserializeValue(String value, Object defaultValue, TypeMirror type) => |
+ deserialize.deserializeValue(value, defaultValue, type); |
+ |
+ String serializeValue(Object value, TypeMirror inferredType) { |
+ if (value == null) return null; |
+ |
+ final type = inferredType.qualifiedName; |
+ if (type == const Symbol('dart.core.bool')) { |
+ return _toBoolean(value) ? '' : null; |
+ } else if (type == const Symbol('dart.core.String') |
+ || type == const Symbol('dart.core.int') |
+ || type == const Symbol('dart.core.double')) { |
+ return '$value'; |
+ } |
+ return null; |
+ } |
+ |
+ void reflectPropertyToAttribute(String name) { |
+ // TODO(sjmiles): consider memoizing this |
+ final self = reflect(this); |
+ // try to intelligently serialize property value |
+ // TODO(jmesserly): cache symbol? |
+ final propValue = self.getField(new Symbol(name)).reflectee; |
+ final property = _declaration._publish[name]; |
+ var inferredType = _inferPropertyType(propValue, property); |
+ final serializedValue = serializeValue(propValue, inferredType); |
+ // boolean properties must reflect as boolean attributes |
+ if (serializedValue != null) { |
+ attributes[name] = serializedValue; |
+ // TODO(sorvell): we should remove attr for all properties |
+ // that have undefined serialization; however, we will need to |
+ // refine the attr reflection system to achieve this; pica, for example, |
+ // relies on having inferredType object properties not removed as |
+ // attrs. |
+ } else if (inferredType.qualifiedName == const Symbol('dart.core.bool')) { |
+ attributes.remove(name); |
+ } |
+ } |
+ |
+ /** |
+ * Creates the document fragment to use for each instance of the custom |
+ * element, given the `<template>` node. By default this is equivalent to: |
+ * |
+ * template.createInstance(this, polymerSyntax); |
+ * |
+ * Where polymerSyntax is a singleton `PolymerExpressions` instance from the |
+ * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions) |
+ * package. |
+ * |
+ * You can override this method to change the instantiation behavior of the |
+ * template, for example to use a different data-binding syntax. |
+ */ |
+ DocumentFragment instanceTemplate(Element template) => |
+ template.createInstance(this, _polymerSyntax); |
+ |
+ NodeBinding bind(String name, model, String path) { |
+ // note: binding is a prepare signal. This allows us to be sure that any |
+ // property changes that occur as a result of binding will be observed. |
+ if (!_elementPrepared) prepareElement(); |
+ |
+ var property = propertyForAttribute(name); |
+ if (property != null) { |
+ unbind(name); |
+ // use n-way Polymer binding |
+ var observer = bindProperty(property.simpleName, model, path); |
+ // reflect bound property to attribute when binding |
+ // to ensure binding is not left on attribute if property |
+ // does not update due to not changing. |
+ reflectPropertyToAttribute(name); |
+ return bindings[name] = observer; |
+ } else { |
+ return super.bind(name, model, path); |
+ } |
+ } |
+ |
+ void asyncUnbindAll() { |
+ if (_unbound == true) return; |
+ _unbindLog.info('[$localName] asyncUnbindAll'); |
+ _unbindAllJob = job(_unbindAllJob, unbindAll, const Duration(seconds: 0)); |
+ } |
+ |
+ void unbindAll() { |
+ if (_unbound == true) return; |
+ |
+ unbindAllProperties(); |
+ super.unbindAll(); |
+ _unbindNodeTree(shadowRoot); |
+ // TODO(sjmiles): must also unbind inherited shadow roots |
+ _unbound = true; |
+ } |
+ |
+ void cancelUnbindAll({bool preventCascade}) { |
+ if (_unbound == true) { |
+ _unbindLog.warning( |
+ '[$localName] already unbound, cannot cancel unbindAll'); |
+ return; |
+ } |
+ _unbindLog.info('[$localName] cancelUnbindAll'); |
+ if (_unbindAllJob != null) { |
+ _unbindAllJob.stop(); |
+ _unbindAllJob = null; |
+ } |
+ |
+ // cancel unbinding our shadow tree iff we're not in the process of |
+ // cascading our tree (as we do, for example, when the element is inserted). |
+ if (preventCascade == true) return; |
+ _forNodeTree(shadowRoot, (n) { |
+ if (n is PolymerElement) { |
+ (n as PolymerElement).cancelUnbindAll(); |
+ } |
+ }); |
+ } |
+ |
+ static void _unbindNodeTree(Node node) { |
+ _forNodeTree(node, (node) => node.unbindAll()); |
+ } |
+ |
+ static void _forNodeTree(Node node, void callback(Node node)) { |
+ if (node == null) return; |
+ |
+ callback(node); |
+ for (var child = node.firstChild; child != null; child = child.nextNode) { |
+ _forNodeTree(child, callback); |
+ } |
+ } |
+ |
+ /** Set up property observers. */ |
+ void observeProperties() { |
+ // TODO(sjmiles): |
+ // we observe published properties so we can reflect them to attributes |
+ // ~100% of our team's applications would work without this reflection, |
+ // perhaps we can make it optional somehow |
+ // |
+ // add user's observers |
+ final observe = _declaration._observe; |
+ final publish = _declaration._publish; |
+ if (observe != null) { |
+ observe.forEach((name, value) { |
+ if (publish != null && publish.containsKey(name)) { |
+ observeBoth(name, value); |
+ } else { |
+ observeProperty(name, value); |
+ } |
+ }); |
+ } |
+ // add observers for published properties |
+ if (publish != null) { |
+ publish.forEach((name, value) { |
+ if (observe == null || !observe.containsKey(name)) { |
+ observeAttributeProperty(name); |
+ } |
+ }); |
+ } |
+ } |
+ |
+ void _observe(String name, void callback(newValue, oldValue)) { |
+ _observeLog.info('[$localName] watching [$name]'); |
+ // TODO(jmesserly): this is a little different than the JS version so we |
+ // can pass the oldValue, which is missing from Dart's PathObserver. |
+ // This probably gives us worse performance. |
+ var path = new PathObserver(this, name); |
+ Object oldValue = null; |
+ _registerObserver(name, path.changes.listen((_) { |
+ final newValue = path.value; |
+ final old = oldValue; |
+ oldValue = newValue; |
+ callback(newValue, old); |
+ })); |
+ } |
+ |
+ void _registerObserver(String name, StreamSubscription sub) { |
+ if (_elementObservers == null) { |
+ _elementObservers = new Map<String, StreamSubscription>(); |
+ } |
+ _elementObservers[name] = sub; |
+ } |
+ |
+ void observeAttributeProperty(String name) { |
+ _observe(name, (value, old) => reflectPropertyToAttribute(name)); |
+ } |
+ |
+ void observeProperty(String name, Symbol method) { |
+ final self = reflect(this); |
+ _observe(name, (value, old) => self.invoke(method, [old])); |
+ } |
+ |
+ void observeBoth(String name, Symbol methodName) { |
+ final self = reflect(this); |
+ _observe(name, (value, old) { |
+ reflectPropertyToAttribute(name); |
+ self.invoke(methodName, [old]); |
+ }); |
+ } |
+ |
+ void unbindProperty(String name) { |
+ if (_elementObservers == null) return; |
+ var sub = _elementObservers.remove(name); |
+ if (sub != null) sub.cancel(); |
+ } |
+ |
+ void unbindAllProperties() { |
+ if (_elementObservers == null) return; |
+ for (var sub in _elementObservers.values) sub.cancel(); |
+ _elementObservers.clear(); |
+ } |
+ |
+ /** |
+ * Bind a [property] in this object to a [path] in model. *Note* in Dart it |
+ * is necessary to also define the field: |
+ * |
+ * var myProperty; |
+ * |
+ * created() { |
+ * super.created(); |
+ * bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); |
+ * } |
+ */ |
+ // TODO(jmesserly): replace with something more localized, like: |
+ // @ComputedField('myModel.path.to.otherProp'); |
+ NodeBinding bindProperty(Symbol name, Object model, String path) => |
+ // apply Polymer two-way reference binding |
+ _bindProperties(this, name, model, path); |
+ |
+ /** |
+ * bind a property in A to a path in B by converting A[property] to a |
+ * getter/setter pair that accesses B[...path...] |
+ */ |
+ static NodeBinding _bindProperties(PolymerElement inA, Symbol inProperty, |
+ Object inB, String inPath) { |
+ |
+ if (_bindLog.isLoggable(Level.INFO)) { |
+ _bindLog.info('[$inB]: bindProperties: [$inPath] to ' |
+ '[${inA.localName}].[$inProperty]'); |
+ } |
+ |
+ // Dart note: normally we only reach this code when we know it's a |
+ // property, but if someone uses bindProperty directly they might get a |
+ // NoSuchMethodError either from the getField below, or from the setField |
+ // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight |
+ // difference from Polymer.js behavior. |
+ |
+ // capture A's value if B's value is null or undefined, |
+ // otherwise use B's value |
+ var path = new PathObserver(inB, inPath); |
+ if (path.value == null) { |
+ path.value = reflect(inA).getField(inProperty).reflectee; |
+ } |
+ return new _PolymerBinding(inA, inProperty, inB, inPath); |
+ } |
+ |
+ /** Attach event listeners on the host (this) element. */ |
+ void addHostListeners() { |
+ var events = _declaration._eventDelegates; |
+ if (events.isEmpty) return; |
+ |
+ if (_eventsLog.isLoggable(Level.INFO)) { |
+ _eventsLog.info('[$localName] addHostListeners: $events'); |
+ } |
+ addNodeListeners(this, events.keys, hostEventListener); |
+ } |
+ |
+ /** Attach event listeners inside a shadow [root]. */ |
+ void addInstanceListeners(ShadowRoot root, Element template) { |
+ var templateDelegates = _declaration._templateDelegates; |
+ if (templateDelegates == null) return; |
+ var events = templateDelegates[template]; |
+ if (events == null) return; |
+ |
+ if (_eventsLog.isLoggable(Level.INFO)) { |
+ _eventsLog.info('[$localName] addInstanceListeners: $events'); |
+ } |
+ addNodeListeners(root, events, instanceEventListener); |
+ } |
+ |
+ void addNodeListeners(Node node, Iterable<String> events, |
+ void listener(Event e)) { |
+ |
+ for (var name in events) { |
+ addNodeListener(node, name, listener); |
+ } |
+ } |
+ |
+ void addNodeListener(Node node, String event, void listener(Event e)) { |
+ node.on[event].listen(listener); |
+ } |
+ |
+ void hostEventListener(Event event) { |
+ // TODO(jmesserly): do we need this check? It was using cancelBubble, see: |
+ // https://github.com/Polymer/polymer/issues/292 |
+ if (!event.bubbles) return; |
+ |
+ bool log = _eventsLog.isLoggable(Level.INFO); |
+ if (log) { |
+ _eventsLog.info('>>> [$localName]: hostEventListener(${event.type})'); |
+ } |
+ |
+ var h = findEventDelegate(event); |
+ if (h != null) { |
+ if (log) _eventsLog.info('[$localName] found host handler name [$h]'); |
+ var detail = event is CustomEvent ? |
+ (event as CustomEvent).detail : null; |
+ // TODO(jmesserly): cache the symbols? |
+ dispatchMethod(new Symbol(h), [event, detail, this]); |
+ } |
+ |
+ if (log) { |
+ _eventsLog.info('<<< [$localName]: hostEventListener(${event.type})'); |
+ } |
+ } |
+ |
+ String findEventDelegate(Event event) => |
+ _declaration._eventDelegates[_eventNameFromType(event.type)]; |
+ |
+ /** Call [methodName] method on [this] with [args], if the method exists. */ |
+ // TODO(jmesserly): I removed the [node] argument as it was unused. Reconcile. |
+ void dispatchMethod(Symbol methodName, List args) { |
+ bool log = _eventsLog.isLoggable(Level.INFO); |
+ if (log) _eventsLog.info('>>> [$localName]: dispatch $methodName'); |
+ |
+ // TODO(sigmund): consider making event listeners list all arguments |
+ // explicitly. Unless VM mirrors are optimized first, this reflectClass call |
+ // will be expensive once custom elements extend directly from Element (see |
+ // dartbug.com/11108). |
+ var self = reflect(this); |
+ var method = self.type.methods[methodName]; |
+ if (method != null) { |
+ // This will either truncate the argument list or extend it with extra |
+ // null arguments, so it will match the signature. |
+ // TODO(sigmund): consider accepting optional arguments when we can tell |
+ // them appart from named arguments (see http://dartbug.com/11334) |
+ args.length = method.parameters.where((p) => !p.isOptional).length; |
+ } |
+ self.invoke(methodName, args); |
+ |
+ if (log) _eventsLog.info('<<< [$localName]: dispatch $methodName'); |
+ |
+ // TODO(jmesserly): workaround for HTML events not supporting zones. |
+ performMicrotaskCheckpoint(); |
+ } |
+ |
+ void instanceEventListener(Event event) { |
+ _listenLocal(host, event); |
+ } |
+ |
+ // TODO(sjmiles): much of the below privatized only because of the vague |
+ // notion this code is too fiddly and we need to revisit the core feature |
+ void _listenLocal(Element host, Event event) { |
+ // TODO(jmesserly): do we need this check? It was using cancelBubble, see: |
+ // https://github.com/Polymer/polymer/issues/292 |
+ if (!event.bubbles) return; |
+ |
+ bool log = _eventsLog.isLoggable(Level.INFO); |
+ if (log) _eventsLog.info('>>> [$localName]: listenLocal [${event.type}]'); |
+ |
+ final eventOn = '$_EVENT_PREFIX${_eventNameFromType(event.type)}'; |
+ if (event.path == null) { |
+ _listenLocalNoEventPath(host, event, eventOn); |
+ } else { |
+ _listenLocalEventPath(host, event, eventOn); |
+ } |
+ |
+ if (log) _eventsLog.info('<<< [$localName]: listenLocal [${event.type}]'); |
+ } |
+ |
+ static void _listenLocalEventPath(Element host, Event event, String eventOn) { |
+ var c = null; |
+ for (var target in event.path) { |
+ // if we hit host, stop |
+ if (identical(target, host)) return; |
+ |
+ // find a controller for the target, unless we already found `host` |
+ // as a controller |
+ c = identical(c, host) ? c : _findController(target); |
+ |
+ // if we have a controller, dispatch the event, and stop if the handler |
+ // returns true |
+ if (c != null && _handleEvent(c, target, event, eventOn)) { |
+ return; |
+ } |
+ } |
+ } |
+ |
+ // TODO(sorvell): remove when ShadowDOM polyfill supports event path. |
+ // Note that _findController will not return the expected controller when the |
+ // event target is a distributed node. This is because we cannot traverse |
+ // from a composed node to a node in shadowRoot. |
+ // This will be addressed via an event path api |
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066 |
+ static void _listenLocalNoEventPath(Element host, Event event, |
+ String eventOn) { |
+ |
+ if (_eventsLog.isLoggable(Level.INFO)) { |
+ _eventsLog.info('event.path() not supported for ${event.type}'); |
+ } |
+ |
+ var target = event.target; |
+ var c = null; |
+ // if we hit dirt or host, stop |
+ while (target != null && target != host) { |
+ // find a controller for target `t`, unless we already found `host` |
+ // as a controller |
+ c = identical(c, host) ? c : _findController(target); |
+ |
+ // if we have a controller, dispatch the event, return 'true' if |
+ // handler returns true |
+ if (c != null && _handleEvent(c, target, event, eventOn)) { |
+ return; |
+ } |
+ target = target.parent; |
+ } |
+ } |
+ |
+ // TODO(jmesserly): this won't find the correct host unless the ShadowRoot |
+ // was created on a PolymerElement. |
+ static Element _findController(Node node) { |
+ while (node.parentNode != null) { |
+ node = node.parentNode; |
+ } |
+ return _shadowHost[node]; |
+ } |
+ |
+ static bool _handleEvent(Element ctrlr, Node node, Event event, |
+ String eventOn) { |
+ |
+ // Note: local events are listened only in the shadow root. This dynamic |
+ // lookup is used to distinguish determine whether the target actually has a |
+ // listener, and if so, to determine lazily what's the target method. |
+ var name = node is Element ? (node as Element).attributes[eventOn] : null; |
+ if (name != null && _handleIfNotHandled(node, event)) { |
+ if (_eventsLog.isLoggable(Level.INFO)) { |
+ _eventsLog.info('[${ctrlr.localName}] found handler name [$name]'); |
+ } |
+ var detail = event is CustomEvent ? |
+ (event as CustomEvent).detail : null; |
+ |
+ if (node != null) { |
+ // TODO(jmesserly): cache symbols? |
+ ctrlr.xtag.dispatchMethod(new Symbol(name), [event, detail, node]); |
+ } |
+ } |
+ |
+ // TODO(jmesserly): do we need this? It was using cancelBubble, see: |
+ // https://github.com/Polymer/polymer/issues/292 |
+ return !event.bubbles; |
+ } |
+ |
+ // TODO(jmesserly): I don't understand this bit. It seems to be a duplicate |
+ // delivery prevention mechanism? |
+ static bool _handleIfNotHandled(Node node, Event event) { |
+ var list = _eventHandledTable[event]; |
+ if (list == null) _eventHandledTable[event] = list = new Set<Node>(); |
+ if (!list.contains(node)) { |
+ list.add(node); |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Invokes a function asynchronously. |
+ * This will call `Platform.flush()` and then return a `new Timer` |
+ * with the provided [method] and [timeout]. |
+ * |
+ * If you would prefer to run the callback using |
+ * [window.requestAnimationFrame], see the [async] method. |
+ */ |
+ // Dart note: "async" is split into 2 methods so it can have a sensible type |
+ // signatures. Also removed the various features that don't make sense in a |
+ // Dart world, like binding to "this" and taking arguments list. |
+ Timer asyncTimer(void method(), Duration timeout) { |
+ // when polyfilling Object.observe, ensure changes |
+ // propagate before executing the async method |
+ platform.flush(); |
+ return new Timer(timeout, method); |
+ } |
+ |
+ /** |
+ * Invokes a function asynchronously. |
+ * This will call `Platform.flush()` and then call |
+ * [window.requestAnimationFrame] with the provided [method] and return the |
+ * result. |
+ * |
+ * If you would prefer to run the callback after a given duration, see |
+ * the [asyncTimer] method. |
+ */ |
+ int async(RequestAnimationFrameCallback method) { |
+ // when polyfilling Object.observe, ensure changes |
+ // propagate before executing the async method |
+ platform.flush(); |
+ return window.requestAnimationFrame(method); |
+ } |
+ |
+ /** |
+ * Fire a [CustomEvent] targeting [toNode], or this if toNode is not |
+ * supplied. Returns the [detail] object. |
+ */ |
+ Object fire(String type, {Object detail, Node toNode, bool canBubble}) { |
+ var node = toNode != null ? toNode : this; |
+ //log.events && console.log('[%s]: sending [%s]', node.localName, inType); |
+ node.dispatchEvent(new CustomEvent( |
+ type, |
+ canBubble: canBubble != null ? canBubble : true, |
+ detail: detail |
+ )); |
+ return detail; |
+ } |
+ |
+ /** |
+ * Fire an event asynchronously. See [async] and [fire]. |
+ */ |
+ asyncFire(String type, {Object detail, Node toNode, bool canBubble}) { |
+ // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to |
+ // add "() =>" |
+ async((x) => fire( |
+ type, detail: detail, toNode: toNode, canBubble: canBubble)); |
+ } |
+ |
+ /** |
+ * Remove [className] from [old], add class to [anew], if they exist. |
+ */ |
+ void classFollows(Element anew, Element old, String className) { |
+ if (old != null) { |
+ old.classes.remove(className); |
+ } |
+ if (anew != null) { |
+ anew.classes.add(className); |
+ } |
+ } |
+} |
+ |
+// Dart note: Polymer addresses n-way bindings by metaprogramming: redefine |
+// the property on the PolymerElement instance to always get its value from the |
+// model@path. We can't replicate this in Dart so we do the next best thing: |
+// listen to changes on both sides and update the values. |
+// TODO(jmesserly): our approach leads to race conditions in the bindings. |
+// See http://code.google.com/p/dart/issues/detail?id=13567 |
+class _PolymerBinding extends NodeBinding { |
+ final InstanceMirror _target; |
+ final Symbol _property; |
+ StreamSubscription _sub; |
+ Object _lastValue; |
+ |
+ _PolymerBinding(PolymerElement node, Symbol property, model, path) |
+ : _target = reflect(node), |
+ _property = property, |
+ super(node, MirrorSystem.getName(property), model, path) { |
+ |
+ _sub = node.changes.listen(_propertyValueChanged); |
+ } |
+ |
+ void close() { |
+ if (closed) return; |
+ _sub.cancel(); |
+ super.close(); |
+ } |
+ |
+ void boundValueChanged(newValue) { |
+ _lastValue = newValue; |
+ _target.setField(_property, newValue); |
+ } |
+ |
+ void _propertyValueChanged(List<ChangeRecord> records) { |
+ for (var record in records) { |
+ if (record.changes(_property)) { |
+ final newValue = _target.getField(_property).reflectee; |
+ if (!identical(_lastValue, newValue)) { |
+ value = newValue; |
+ } |
+ return; |
+ } |
+ } |
+ } |
+} |
+ |
+bool _toBoolean(value) => null != value && false != value; |
+ |
+TypeMirror _propertyType(DeclarationMirror property) => |
+ property is VariableMirror |
+ ? (property as VariableMirror).type |
+ : (property as MethodMirror).returnType; |
+ |
+TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { |
+ var type = _propertyType(property); |
+ if (type.qualifiedName == const Symbol('dart.core.Object') || |
+ type.qualifiedName == const Symbol('dynamic')) { |
+ // Attempt to infer field type from the default value. |
+ if (value != null) { |
+ type = reflect(value).type; |
+ } |
+ } |
+ return type; |
+} |
+ |
+final Logger _observeLog = new Logger('polymer.observe'); |
+final Logger _eventsLog = new Logger('polymer.events'); |
+final Logger _unbindLog = new Logger('polymer.unbind'); |
+final Logger _bindLog = new Logger('polymer.bind'); |
+ |
+final Expando _shadowHost = new Expando<Element>(); |
+ |
+final Expando _eventHandledTable = new Expando<Set<Node>>(); |