blob: f0bff99c4637274b00dd6c5d15419547f71b76d9 [file] [log] [blame]
// 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>>();