| 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>>();
|
|
|