blob: 6ea826dfaceb8a07df34532876928e8f8c3b5b5d [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.
///
/// You can also use [PublishedProperty] to provide additional information,
/// such as automatically syncing the property back to the attribute.
///
/// For example:
///
/// class MyPlaybackElement extends PolymerElement {
/// // This will be available as an HTML attribute, for example:
/// //
/// // <my-playback volume="11">
/// //
/// // It will support initialization and data-binding via <template>:
/// //
/// // <template>
/// // <my-playback volume="{{x}}">
/// // </template>
/// //
/// // If the template is instantiated or given a model, `x` will be
/// // used for this field and updated whenever `volume` changes.
/// @published double volume;
///
/// // This will be available as an HTML attribute, like above, but it
/// // will also serialize values set to the property to the attribute.
/// // In other words, attributes['volume2'] will contain a serialized
/// // version of this field.
/// @PublishedProperty(reflect: true) double volume2;
/// }
///
const published = const PublishedProperty();
/// An annotation used to publish a field as an attribute. See [published].
class PublishedProperty extends ObservableProperty {
/// Whether the property value should be reflected back to the HTML attribute.
final bool reflect;
const PublishedProperty({this.reflect: false});
}
/// Use this type to observe a property and have the method be called when it
/// changes. For example:
///
/// @ObserveProperty('foo.bar baz qux')
/// validate() {
/// // use this.foo.bar, this.baz, and this.qux in validation
/// ...
/// }
///
/// Note that you can observe a property path, and more than a single property
/// can be specified in a space-delimited list or as a constant List.
class ObserveProperty {
final _names;
List<String> get names {
var n = _names;
// TODO(jmesserly): the bogus '$n' is to workaround a dart2js bug, otherwise
// it generates an incorrect call site.
if (n is String) return '$n'.split(' ');
if (n is! Iterable) {
throw new UnsupportedError('ObserveProperty takes either an Iterable of '
'names, or a space separated String, instead of `$n`.');
}
return n;
}
const ObserveProperty(this._names);
}
/// Base class for PolymerElements deriving from HtmlElement.
///
/// See [Polymer].
class PolymerElement extends HtmlElement with Polymer, Observable {
PolymerElement.created() : super.created() {
polymerCreated();
}
}
/// The mixin class for Polymer elements. It provides convenience features on
/// top of the custom elements web standard.
///
/// If this class is used as a mixin,
/// you must call `polymerCreated()` from the body of your constructor.
abstract class Polymer implements Element, Observable, NodeBindExtension {
// 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;
_typesByName[name] = type;
// Dart note: here we notify JS of the element registration. We don't pass
// the Dart type because we will handle that in PolymerDeclaration.
// See _hookJsPolymerDeclaration for how this is done.
(js.context['Polymer'] as JsFunction).apply([name]);
}
/// Register a custom element that has no associated `<polymer-element>`.
/// Unlike [register] this will always perform synchronous registration and
/// by the time this method returns the element will be available using
/// [document.createElement] or by modifying the HTML to include the element.
static void registerSync(String name, Type type,
{String extendsTag, Document doc, Node template}) {
// Our normal registration, this will queue up the name->type association.
register(name, type);
// Build a polymer-element and initialize it to register
if (doc == null) doc = document;
var poly = doc.createElement('polymer-element');
poly.attributes['name'] = name;
if (extendsTag != null) poly.attributes['extends'] = extendsTag;
if (template != null) poly.append(template);
new JsObject.fromBrowserObject(poly).callMethod('init');
}
// Note: these are from src/declaration/import.js
// For now proxy to the JS methods, because we want to share the loader with
// polymer.js for interop purposes.
static Future importElements(Node elementOrFragment) {
var completer = new Completer();
js.context['Polymer'].callMethod('importElements',
[elementOrFragment, () => completer.complete()]);
return completer.future;
}
static Future importUrls(List urls) {
var completer = new Completer();
js.context['Polymer'].callMethod('importUrls',
[urls, () => completer.complete()]);
return completer.future;
}
static final Completer _onReady = new Completer();
/// Future indicating that the Polymer library has been loaded and is ready
/// for use.
static Future get onReady => _onReady.future;
/// The most derived `<polymer-element>` declaration for this element.
PolymerDeclaration get element => _element;
PolymerDeclaration _element;
/// Deprecated: use [element] instead.
@deprecated PolymerDeclaration get declaration => _element;
Map<String, StreamSubscription> _namedObservers;
List<Iterable<Bindable>> _observers = [];
bool _unbound; // lazy-initialized
PolymerJob _unbindAllJob;
CompoundObserver _propertyObserver;
bool _readied = false;
/// Returns the object that should be used as the event controller for
/// event bindings in this element's template. If set, this will override the
/// normal controller lookup.
// TODO(jmesserly): type seems wrong here? I'm guessing this should be any
// kind of model object. Also, should it be writable by anyone?
Polymer eventController;
bool get hasBeenAttached => _hasBeenAttached;
bool _hasBeenAttached = false;
/// 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): should expose this as an immutable map.
// Similar issue as $.
final Map<String, ShadowRoot> shadowRoots =
new LinkedHashMap<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?
// Note: this is observable to support $['someId'] being used in templates.
// The template is stamped before $ is populated, so we need observation if
// we want it to be usable in bindings.
@reflectable final Map<String, Element> $ =
new ObservableMap<String, Element>();
/// Use to override the default syntax for polymer-elements.
/// By default this will be null, which causes [instanceTemplate] to use
/// the template's bindingDelegate or the [element.syntax], in that order.
PolymerExpressions get syntax => null;
bool get _elementPrepared => _element != null;
/// Retrieves the custom element name. It should be used instead
/// of localName, see: https://github.com/Polymer/polymer-dev/issues/26
String get _name {
if (_element != null) return _element.name;
var isAttr = attributes['is'];
return (isAttr == null || isAttr == '') ? localName : isAttr;
}
/// By default the data bindings will be cleaned up when this custom element
/// is detached from the document. Overriding this to return `true` will
/// prevent that from happening.
bool get preventDispose => false;
/// If this class is used as a mixin, this method must be called from inside
/// of the `created()` constructor.
///
/// If this class is a superclass, calling `super.created()` is sufficient.
void polymerCreated() {
var t = nodeBind(this).templateInstance;
if (t != null && t.model != null) {
window.console.warn('Attributes on $_name were data bound '
'prior to Polymer upgrading the element. This may result in '
'incorrect binding types.');
}
prepareElement();
// TODO(sorvell): replace when ShadowDOMPolyfill issue is corrected
// https://github.com/Polymer/ShadowDOM/issues/420
if (!isTemplateStagingDocument(ownerDocument) || _hasShadowDomPolyfill) {
makeElementReady();
}
}
/// *Deprecated* use [shadowRoots] instead.
@deprecated
ShadowRoot getShadowRoot(String customTagName) => shadowRoots[customTagName];
void prepareElement() {
if (_elementPrepared) {
window.console.warn('Element already prepared: $_name');
return;
}
// Dart note: get the corresponding <polymer-element> declaration.
_element = _getDeclaration(_name);
// install property storage
createPropertyObserver();
// TODO (sorvell): temporarily open observer when created
openPropertyObserver();
// install boilerplate attributes
copyInstanceAttributes();
// process input attributes
takeAttributes();
// add event listeners
addHostListeners();
}
makeElementReady() {
if (_readied) return;
_readied = true;
// TODO(sorvell): We could create an entry point here
// for the user to compute property values.
// process declarative resources
parseDeclarations(_element);
// TODO(sorvell): CE polyfill uses unresolved attribute to simulate
// :unresolved; remove this attribute to be compatible with native
// CE.
attributes.remove('unresolved');
// user entry point
ready();
}
/// Called when [prepareElement] is finished.
void ready() {}
/// domReady can be used to access elements in dom (descendants,
/// ancestors, siblings) such that the developer is enured to upgrade
/// ordering. If the element definitions have loaded, domReady
/// can be used to access upgraded elements.
///
/// To use, override this method in your element.
void domReady() {}
void attached() {
if (!_elementPrepared) {
// Dart specific message for a common issue.
throw new StateError('polymerCreated was not called for custom element '
'$_name, this should normally be done in the .created() if Polymer '
'is used as a mixin.');
}
cancelUnbindAll();
if (!hasBeenAttached) {
_hasBeenAttached = true;
async((_) => domReady());
}
}
void detached() {
if (!preventDispose) asyncUnbindAll();
}
/// Recursive ancestral <element> initialization, oldest first.
void parseDeclarations(PolymerDeclaration declaration) {
if (declaration != null) {
parseDeclarations(declaration.superDeclaration);
parseDeclaration(declaration.element);
}
}
/// Parse input `<polymer-element>` as needed, override for custom behavior.
void parseDeclaration(Element elementElement) {
var template = fetchTemplate(elementElement);
if (template != null) {
var root = shadowFromTemplate(template);
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.querySelector('template');
/// Utility function that stamps a `<template>` into light-dom.
Node lightFromTemplate(Element template, [Node refNode]) {
if (template == null) return null;
// TODO(sorvell): mark this element as an event controller so that
// event listeners on bound nodes inside it will be called on it.
// Note, the expectation here is that events on all descendants
// should be handled by this element.
eventController = this;
// 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
if (refNode != null) {
append(dom);
} else {
insertBefore(dom, refNode);
}
// perform post-construction initialization tasks on ahem, light root
shadowRootReady(this);
// return the created shadow root
return dom;
}
/// Utility function that creates a shadow root from a `<template>`.
///
/// The base implementation will return a [ShadowRoot], but you can replace it
/// with your own code and skip ShadowRoot creation. In that case, you should
/// return `null`.
///
/// In your overridden method, you can use [instanceTemplate] to stamp the
/// template and initialize data binding, and [shadowRootReady] to intialize
/// other Polymer features like event handlers. It is fine to call
/// shadowRootReady with a node other than a ShadowRoot such as with `this`.
ShadowRoot shadowFromTemplate(Element template) {
if (template == null) return null;
// make a shadow root
var root = createShadowRoot();
// 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);
// return the created shadow root
return root;
}
void shadowRootReady(Node root) {
// locate nodes with id and store references to them in this.$ hash
marshalNodeReferences(root);
// set up polymer gestures
if (_PolymerGestures != null) {
_PolymerGestures.callMethod('register', [root]);
}
}
/// Locate nodes with id and store references to them in [$] hash.
void marshalNodeReferences(Node root) {
if (root == null) return;
for (var n in (root as dynamic).querySelectorAll('[id]')) {
$[n.id] = n;
}
}
void attributeChanged(String name, String oldValue, String newValue) {
if (name != 'class' && name != 'style') {
attributeToProperty(name, newValue);
}
}
// TODO(jmesserly): this could be a top level method.
/// Returns a future when `node` changes, or when its children or subtree
/// changes.
///
/// Use [MutationObserver] if you want to listen to a stream of changes.
Future<List<MutationRecord>> onMutation(Node node) {
var completer = new Completer();
new MutationObserver((mutations, observer) {
observer.disconnect();
completer.complete(mutations);
})..observe(node, childList: true, subtree: true);
return completer.future;
}
void copyInstanceAttributes() {
_element._instanceAttributes.forEach((name, value) {
attributes.putIfAbsent(name, () => value);
});
}
void takeAttributes() {
if (_element._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 decl = propertyForAttribute(name);
if (decl == 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;
final currentValue = smoke.read(this, decl.name);
// deserialize Boolean or Number values from attribute
var type = decl.type;
if ((type == Object || type == dynamic) && currentValue != null) {
// Attempt to infer field type from the current value.
type = currentValue.runtimeType;
}
final newValue = deserializeValue(value, currentValue, type);
// only act if the value has changed
if (!identical(newValue, currentValue)) {
// install new value (has side-effects)
smoke.write(this, decl.name, newValue);
}
}
/// Return the published property matching name, or null.
// TODO(jmesserly): should we just return Symbol here?
smoke.Declaration propertyForAttribute(String name) {
final publishLC = _element._publishLC;
if (publishLC == null) return null;
//console.log('propertyForAttribute:', name, 'matches', match);
return publishLC[name];
}
/// Convert representation of [value] based on [type] and [currentValue].
Object deserializeValue(String value, Object currentValue, Type type) =>
deserialize.deserializeValue(value, currentValue, type);
String serializeValue(Object value) {
if (value == null) return null;
if (value is bool) {
return _toBoolean(value) ? '' : null;
} else if (value is String || value is num) {
return '$value';
}
return null;
}
void reflectPropertyToAttribute(String path) {
// TODO(sjmiles): consider memoizing this
// try to intelligently serialize property value
final propValue = new PropertyPath(path).getValueFrom(this);
final serializedValue = serializeValue(propValue);
// boolean properties must reflect as boolean attributes
if (serializedValue != null) {
attributes[path] = 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 (propValue is bool) {
attributes.remove(path);
}
}
/// Creates the document fragment to use for each instance of the custom
/// element, given the `<template>` node. By default this is equivalent to:
///
/// templateBind(template).createInstance(this, polymerSyntax);
///
/// Where polymerSyntax is a singleton [PolymerExpressions] instance.
///
/// 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) {
var syntax = this.syntax;
var t = templateBind(template);
if (syntax == null && t.bindingDelegate == null) {
syntax = element.syntax;
}
var dom = t.createInstance(this, syntax);
registerObservers(getTemplateInstanceBindings(dom));
return dom;
}
Bindable bind(String name, bindable, {bool oneTime: false}) {
var decl = propertyForAttribute(name);
if (decl == null) {
// Cannot call super.bind because template_binding is its own package
return nodeBindFallback(this).bind(name, bindable, oneTime: oneTime);
} else {
// use n-way Polymer binding
var observer = bindProperty(decl.name, bindable, oneTime: oneTime);
// NOTE: reflecting binding information is typically required only for
// tooling. It has a performance cost so it's opt-in in Node.bind.
if (enableBindingsReflection && observer != null) {
// Dart note: this is not needed because of how _PolymerBinding works.
//observer.path = bindable.path_;
_recordBinding(name, observer);
}
var reflect = _element._reflect;
// Get back to the (possibly camel-case) name for the property.
var propName = smoke.symbolToName(decl.name);
if (reflect != null && reflect.contains(propName)) {
reflectPropertyToAttribute(propName);
}
return observer;
}
}
_recordBinding(String name, observer) {
if (bindings == null) bindings = {};
this.bindings[name] = observer;
}
bindFinished() => makeElementReady();
Map<String, Bindable> get bindings => nodeBindFallback(this).bindings;
set bindings(Map value) { nodeBindFallback(this).bindings = value; }
TemplateInstance get templateInstance =>
nodeBindFallback(this).templateInstance;
// TODO(sorvell): unbind/unbindAll has been removed, as public api, from
// TemplateBinding. We still need to close/dispose of observers but perhaps
// we should choose a more explicit name.
void asyncUnbindAll() {
if (_unbound == true) return;
_unbindLog.fine('[$_name] asyncUnbindAll');
_unbindAllJob = scheduleJob(_unbindAllJob, unbindAll);
}
void unbindAll() {
if (_unbound == true) return;
closeObservers();
closeNamedObservers();
_unbound = true;
}
void cancelUnbindAll() {
if (_unbound == true) {
_unbindLog.warning('[$_name] already unbound, cannot cancel unbindAll');
return;
}
_unbindLog.fine('[$_name] cancelUnbindAll');
if (_unbindAllJob != null) {
_unbindAllJob.stop();
_unbindAllJob = null;
}
}
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 createPropertyObserver() {
final observe = _element._observe;
if (observe != null) {
var o = _propertyObserver = new CompoundObserver();
// keep track of property observer so we can shut it down
registerObservers([o]);
for (var path in observe.keys) {
o.addPath(this, path);
// TODO(jmesserly): on the Polymer side it doesn't look like they
// will observe arrays unless it is a length == 1 path.
observeArrayValue(path, path.getValueFrom(this), null);
}
}
}
void openPropertyObserver() {
if (_propertyObserver != null) {
_propertyObserver.open(notifyPropertyChanges);
}
// Dart note: we need an extra listener.
// see comment on [_propertyChange].
if (_element._publish != null) {
changes.listen(_propertyChange);
}
}
/// Responds to property changes on this element.
void notifyPropertyChanges(List newValues, Map oldValues, List paths) {
final observe = _element._observe;
final called = new HashSet();
oldValues.forEach((i, oldValue) {
final newValue = newValues[i];
// Date note: we don't need any special checking for null and undefined.
// note: paths is of form [object, path, object, path]
final path = paths[2 * i + 1];
if (observe == null) return;
var methods = observe[path];
if (methods == null) return;
for (var method in methods) {
if (!called.add(method)) continue; // don't invoke more than once.
observeArrayValue(path, newValue, oldValue);
// Dart note: JS passes "arguments", so we pass along our args.
// TODO(sorvell): call method with the set of values it's expecting;
// e.g. 'foo bar': 'invalidate' expects the new and old values for
// foo and bar. Currently we give only one of these and then
// deliver all the arguments.
smoke.invoke(this, method,
[oldValue, newValue, newValues, oldValues, paths], adjust: true);
}
});
}
// Dart note: had to rename this to avoid colliding with
// Observable.deliverChanges. Even worse, super calls aren't possible or
// it prevents Polymer from being a mixin, so we can't override it even if
// we wanted to.
void deliverPropertyChanges() {
if (_propertyObserver != null) {
_propertyObserver.deliver();
}
}
// Dart note: this is not called by observe-js because we don't have
// the mechanism for defining properties on our proto.
// TODO(jmesserly): this has similar timing issues as our @published
// properties do generally -- it's async when it should be sync.
void _propertyChange(List<ChangeRecord> records) {
for (var record in records) {
if (record is! PropertyChangeRecord) continue;
final name = smoke.symbolToName(record.name);
final reflect = _element._reflect;
if (reflect != null && reflect.contains(name)) {
reflectPropertyToAttribute(name);
}
}
}
void observeArrayValue(PropertyPath name, Object value, Object old) {
final observe = _element._observe;
if (observe == null) return;
// we only care if there are registered side-effects
var callbacks = observe[name];
if (callbacks == null) return;
// if we are observing the previous value, stop
if (old is ObservableList) {
if (_observeLog.isLoggable(Level.FINE)) {
_observeLog.fine('[$_name] observeArrayValue: unregister $name');
}
closeNamedObserver('${name}__array');
}
// if the new value is an array, being observing it
if (value is ObservableList) {
if (_observeLog.isLoggable(Level.FINE)) {
_observeLog.fine('[$_name] observeArrayValue: register $name');
}
var sub = value.listChanges.listen((changes) {
for (var callback in callbacks) {
smoke.invoke(this, callback, [old], adjust: true);
}
});
registerNamedObserver('${name}__array', sub);
}
}
void registerObservers(Iterable<Bindable> observers) {
_observers.add(observers);
}
void closeObservers() {
_observers.forEach(closeObserverList);
_observers = [];
}
void closeObserverList(Iterable<Bindable> observers) {
for (var o in observers) {
if (o != null) o.close();
}
}
/// Bookkeeping observers for memory management.
void registerNamedObserver(String name, StreamSubscription sub) {
if (_namedObservers == null) {
_namedObservers = new Map<String, StreamSubscription>();
}
_namedObservers[name] = sub;
}
bool closeNamedObserver(String name) {
var sub = _namedObservers.remove(name);
if (sub == null) return false;
sub.cancel();
return true;
}
void closeNamedObservers() {
if (_namedObservers == null) return;
for (var sub in _namedObservers.values) {
if (sub != null) sub.cancel();
}
_namedObservers.clear();
_namedObservers = null;
}
/// Bind the [name] property in this element to [bindable]. *Note* in Dart it
/// is necessary to also define the field:
///
/// var myProperty;
///
/// ready() {
/// super.ready();
/// bindProperty(#myProperty,
/// new PathObserver(this, 'myModel.path.to.otherProp'));
/// }
Bindable bindProperty(Symbol name, Bindable bindable, {oneTime: false}) {
// 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.
if (_bindLog.isLoggable(Level.FINE)) {
_bindLog.fine('bindProperty: [$bindable] to [$_name].[$name]');
}
// capture A's value if B's value is null or undefined,
// otherwise use B's value
// TODO(sorvell): need to review, can do with ObserverTransform
var v = bindable.value;
if (v == null) {
bindable.value = smoke.read(this, name);
}
// TODO(jmesserly): we need to fix this -- it doesn't work like Polymer.js
// bindings. https://code.google.com/p/dart/issues/detail?id=18343
// apply Polymer two-way reference binding
//return Observer.bindToInstance(inA, inProperty, observable,
// resolveBindingValue);
return new _PolymerBinding(this, name, bindable);
}
/// Attach event listeners on the host (this) element.
void addHostListeners() {
var events = _element._eventDelegates;
if (events.isEmpty) return;
if (_eventsLog.isLoggable(Level.FINE)) {
_eventsLog.fine('[$_name] addHostListeners: $events');
}
// NOTE: host events look like bindings but really are not;
// (1) we don't want the attribute to be set and (2) we want to support
// multiple event listeners ('host' and 'instance') and Node.bind
// by default supports 1 thing being bound.
events.forEach((type, methodName) {
// Dart note: the getEventHandler method is on our PolymerExpressions.
var handler = element.syntax.getEventHandler(this, this, methodName);
addEventListener(type, handler);
});
}
/// Calls [methodOrCallback] with [args] if it is a closure, otherwise, treat
/// it as a method name in [object], and invoke it.
void dispatchMethod(object, callbackOrMethod, List args) {
bool log = _eventsLog.isLoggable(Level.FINE);
if (log) _eventsLog.fine('>>> [$_name]: dispatch $callbackOrMethod');
if (callbackOrMethod is Function) {
int maxArgs = smoke.maxArgs(callbackOrMethod);
if (maxArgs == -1) {
_eventsLog.warning(
'invalid callback: expected callback of 0, 1, 2, or 3 arguments');
}
args.length = maxArgs;
Function.apply(callbackOrMethod, args);
} else if (callbackOrMethod is String) {
smoke.invoke(object, smoke.nameToSymbol(callbackOrMethod), args,
adjust: true);
} else {
_eventsLog.warning('invalid callback');
}
if (log) _eventsLog.info('<<< [$_name]: dispatch $callbackOrMethod');
}
/// Call [methodName] method on this object with [args].
invokeMethod(Symbol methodName, List args) =>
smoke.invoke(this, methodName, args, adjust: true);
/// 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.
///
/// To cancel, call [Timer.cancel] on the result of this method.
Timer asyncTimer(void method(), Duration timeout) {
// 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.
// when polyfilling Object.observe, ensure changes
// propagate before executing the async method
scheduleMicrotask(Observable.dirtyCheck);
_Platform.callMethod('flush'); // for polymer-js interop
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.
///
/// If you would like to cancel this, use [cancelAsync].
int async(RequestAnimationFrameCallback method) {
// when polyfilling Object.observe, ensure changes
// propagate before executing the async method
scheduleMicrotask(Observable.dirtyCheck);
_Platform.callMethod('flush'); // for polymer-js interop
return window.requestAnimationFrame(method);
}
/// Cancel an operation scenduled by [async]. This is just shorthand for:
/// window.cancelAnimationFrame(id);
void cancelAsync(int id) => window.cancelAnimationFrame(id);
/// Fire a [CustomEvent] targeting [onNode], or `this` if onNode is not
/// supplied. Returns the new event.
CustomEvent fire(String type, {Object detail, Node onNode, bool canBubble,
bool cancelable}) {
var node = onNode != null ? onNode : this;
var event = new CustomEvent(
type,
canBubble: canBubble != null ? canBubble : true,
cancelable: cancelable != null ? cancelable : true,
detail: detail
);
node.dispatchEvent(event);
return event;
}
/// 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, onNode: 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);
}
}
/// Installs external stylesheets and <style> elements with the attribute
/// polymer-scope='controller' into the scope of element. This is intended
/// to be called during custom element construction.
void installControllerStyles() {
var scope = findStyleScope();
if (scope != null && !scopeHasNamedStyle(scope, localName)) {
// allow inherited controller styles
var decl = _element;
var cssText = new StringBuffer();
while (decl != null) {
cssText.write(decl.cssTextForScope(_STYLE_CONTROLLER_SCOPE));
decl = decl.superDeclaration;
}
if (cssText.isNotEmpty) {
installScopeCssText('$cssText', scope);
}
}
}
void installScopeStyle(style, [String name, Node scope]) {
if (scope == null) scope = findStyleScope();
if (name == null) name = '';
if (scope != null && !scopeHasNamedStyle(scope, '$_name$name')) {
var cssText = new StringBuffer();
if (style is Iterable) {
for (var s in style) {
cssText..writeln(s.text)..writeln();
}
} else {
cssText = (style as Node).text;
}
installScopeCssText('$cssText', scope, name);
}
}
void installScopeCssText(String cssText, [Node scope, String name]) {
if (scope == null) scope = findStyleScope();
if (name == null) name = '';
if (scope == null) return;
if (_ShadowCss != null) {
cssText = _shimCssText(cssText, scope is ShadowRoot ? scope.host : null);
}
var style = element.cssTextToScopeStyle(cssText,
_STYLE_CONTROLLER_SCOPE);
applyStyleToScope(style, scope);
// cache that this style has been applied
Set styles = _scopeStyles[scope];
if (styles == null) _scopeStyles[scope] = styles = new Set();
styles.add('$_name$name');
}
Node findStyleScope([node]) {
// find the shadow root that contains this element
var n = node;
if (n == null) n = this;
while (n.parentNode != null) {
n = n.parentNode;
}
return n;
}
bool scopeHasNamedStyle(Node scope, String name) {
Set styles = _scopeStyles[scope];
return styles != null && styles.contains(name);
}
static final _scopeStyles = new Expando();
static String _shimCssText(String cssText, [Element host]) {
var name = '';
var is_ = false;
if (host != null) {
name = host.localName;
is_ = host.attributes.containsKey('is');
}
var selector = _ShadowCss.callMethod('makeScopeSelector', [name, is_]);
return _ShadowCss.callMethod('shimCssText', [cssText, selector]);
}
static void applyStyleToScope(StyleElement style, Node scope) {
if (style == null) return;
if (scope == document) scope = document.head;
if (_hasShadowDomPolyfill) scope = document.head;
// TODO(sorvell): necessary for IE
// see https://connect.microsoft.com/IE/feedback/details/790212/
// cloning-a-style-element-and-adding-to-document-produces
// -unexpected-result#details
// var clone = style.cloneNode(true);
var clone = new StyleElement()..text = style.text;
var attr = style.attributes[_STYLE_SCOPE_ATTRIBUTE];
if (attr != null) {
clone.attributes[_STYLE_SCOPE_ATTRIBUTE] = attr;
}
// TODO(sorvell): probably too brittle; try to figure out
// where to put the element.
var refNode = scope.firstChild;
if (scope == document.head) {
var selector = 'style[$_STYLE_SCOPE_ATTRIBUTE]';
var styleElement = document.head.querySelectorAll(selector);
if (styleElement.isNotEmpty) {
refNode = styleElement.last.nextElementSibling;
}
}
scope.insertBefore(clone, refNode);
}
/// Invoke [callback] in [wait], unless the job is re-registered,
/// which resets the timer. If [wait] is not supplied, this will use
/// [window.requestAnimationFrame] instead of a [Timer].
///
/// For example:
///
/// _myJob = Polymer.scheduleJob(_myJob, callback);
///
/// Returns the newly created job.
// Dart note: renamed to scheduleJob to be a bit more consistent with Dart.
PolymerJob scheduleJob(PolymerJob job, void callback(), [Duration wait]) {
if (job == null) job = new PolymerJob._();
// Dart note: made start smarter, so we don't need to call stop.
return job..start(callback, wait);
}
}
// 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 Bindable {
final Polymer _target;
final Symbol _property;
final Bindable _bindable;
StreamSubscription _sub;
Object _lastValue;
_PolymerBinding(this._target, this._property, this._bindable) {
_sub = _target.changes.listen(_propertyValueChanged);
_updateNode(open(_updateNode));
}
void _updateNode(newValue) {
_lastValue = newValue;
smoke.write(_target, _property, newValue);
}
void _propertyValueChanged(List<ChangeRecord> records) {
for (var record in records) {
if (record is PropertyChangeRecord && record.name == _property) {
final newValue = smoke.read(_target, _property);
if (!identical(_lastValue, newValue)) {
this.value = newValue;
}
return;
}
}
}
open(callback(value)) => _bindable.open(callback);
get value => _bindable.value;
set value(newValue) => _bindable.value = newValue;
void close() {
if (_sub != null) {
_sub.cancel();
_sub = null;
}
_bindable.close();
}
}
bool _toBoolean(value) => null != value && false != value;
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 _eventHandledTable = new Expando<Set<Node>>();
final JsObject _PolymerGestures = js.context['PolymerGestures'];