OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 part of polymer; | |
6 | |
7 /// Use this annotation to publish a property as an attribute. | |
8 /// | |
9 /// You can also use [PublishedProperty] to provide additional information, | |
10 /// such as automatically syncing the property back to the attribute. | |
11 /// | |
12 /// For example: | |
13 /// | |
14 /// class MyPlaybackElement extends PolymerElement { | |
15 /// // This will be available as an HTML attribute, for example: | |
16 /// // | |
17 /// // <my-playback volume="11"> | |
18 /// // | |
19 /// // It will support initialization and data-binding via <template>: | |
20 /// // | |
21 /// // <template> | |
22 /// // <my-playback volume="{{x}}"> | |
23 /// // </template> | |
24 /// // | |
25 /// // If the template is instantiated or given a model, `x` will be | |
26 /// // used for this field and updated whenever `volume` changes. | |
27 /// @published | |
28 /// double get volume => readValue(#volume); | |
29 /// set volume(double newValue) => writeValue(#volume, newValue); | |
30 /// | |
31 /// // This will be available as an HTML attribute, like above, but it | |
32 /// // will also serialize values set to the property to the attribute. | |
33 /// // In other words, attributes['volume2'] will contain a serialized | |
34 /// // version of this field. | |
35 /// @PublishedProperty(reflect: true) | |
36 /// double get volume2 => readValue(#volume2); | |
37 /// set volume2(double newValue) => writeValue(#volume2, newValue); | |
38 /// } | |
39 /// | |
40 /// **Important note**: the pattern using `readValue` and `writeValue` | |
41 /// guarantees that reading the property will give you the latest value at any | |
42 /// given time, even if change notifications have not been propagated. | |
43 /// | |
44 /// We still support using @published on a field, but this will not | |
45 /// provide the same guarantees, so this is discouraged. For example: | |
46 /// | |
47 /// // Avoid this if possible. This will be available as an HTML | |
48 /// // attribute too, but you might need to delay reading volume3 | |
49 /// // asynchronously to guarantee that you read the latest value | |
50 /// // set through bindings. | |
51 /// @published double volume3; | |
52 const published = const PublishedProperty(); | |
53 | |
54 /// An annotation used to publish a field as an attribute. See [published]. | |
55 class PublishedProperty extends ObservableProperty { | |
56 /// Whether the property value should be reflected back to the HTML attribute. | |
57 final bool reflect; | |
58 | |
59 const PublishedProperty({this.reflect: false}); | |
60 } | |
61 | |
62 /// Use this type to observe a property and have the method be called when it | |
63 /// changes. For example: | |
64 /// | |
65 /// @ObserveProperty('foo.bar baz qux') | |
66 /// validate() { | |
67 /// // use this.foo.bar, this.baz, and this.qux in validation | |
68 /// ... | |
69 /// } | |
70 /// | |
71 /// Note that you can observe a property path, and more than a single property | |
72 /// can be specified in a space-delimited list or as a constant List. | |
73 class ObserveProperty { | |
74 final _names; | |
75 | |
76 List<String> get names { | |
77 var n = _names; | |
78 // TODO(jmesserly): the bogus '$n' is to workaround a dart2js bug, otherwise | |
79 // it generates an incorrect call site. | |
80 if (n is String) return '$n'.split(' '); | |
81 if (n is! Iterable) { | |
82 throw new UnsupportedError('ObserveProperty takes either an Iterable of ' | |
83 'names, or a space separated String, instead of `$n`.'); | |
84 } | |
85 return n; | |
86 } | |
87 | |
88 const ObserveProperty(this._names); | |
89 } | |
90 | |
91 /// Use this to create computed properties that are updated automatically. The | |
92 /// annotation includes a polymer expression that describes how this property | |
93 /// value can be expressed in terms of the values of other properties. For | |
94 /// example: | |
95 /// | |
96 /// class MyPlaybackElement extends PolymerElement { | |
97 /// @observable int x; | |
98 /// | |
99 /// // Reading xTimes2 will return x * 2. | |
100 /// @ComputedProperty('x * 2') | |
101 /// int get xTimes2 => readValue(#xTimes2); | |
102 /// | |
103 /// If the polymer expression is assignable, you can also define a setter for | |
104 /// it. For example: | |
105 /// | |
106 /// // Reading c will return a.b, writing c will update a.b. | |
107 /// @ComputedProperty('a.b') | |
108 /// get c => readValue(#c); | |
109 /// set c(newValue) => writeValue(#c, newValue); | |
110 /// | |
111 /// The expression can do anything that is allowed in a polymer expresssion, | |
112 /// even making calls to methods in your element. However, dependencies that are | |
113 /// only used within those methods and that are not visible in the polymer | |
114 /// expression, will not be observed. For example: | |
115 /// | |
116 /// // Because `x` only appears inside method `m`, we will not notice | |
117 /// // that `d` has changed if `x` is modified. However, `d` will be | |
118 /// // updated whenever `c` changes. | |
119 /// @ComputedProperty('m(c)') | |
120 /// get d => readValue(#d); | |
121 /// | |
122 /// m(c) => c + x; | |
123 class ComputedProperty { | |
124 /// A polymer expression, evaluated in the context of the custom element where | |
125 /// this annotation is used. | |
126 final String expression; | |
127 | |
128 const ComputedProperty(this.expression); | |
129 } | |
130 | |
131 /// Base class for PolymerElements deriving from HtmlElement. | |
132 /// | |
133 /// See [Polymer]. | |
134 class PolymerElement extends HtmlElement with Polymer, Observable { | |
135 PolymerElement.created() : super.created() { | |
136 polymerCreated(); | |
137 } | |
138 } | |
139 | |
140 /// The mixin class for Polymer elements. It provides convenience features on | |
141 /// top of the custom elements web standard. | |
142 /// | |
143 /// If this class is used as a mixin, | |
144 /// you must call `polymerCreated()` from the body of your constructor. | |
145 abstract class Polymer implements Element, Observable, NodeBindExtension { | |
146 | |
147 // TODO(jmesserly): should this really be public? | |
148 /// Regular expression that matches data-bindings. | |
149 static final bindPattern = new RegExp(r'\{\{([^{}]*)}}'); | |
150 | |
151 /// Like [document.register] but for Polymer elements. | |
152 /// | |
153 /// Use the [name] to specify custom elment's tag name, for example: | |
154 /// "fancy-button" if the tag is used as `<fancy-button>`. | |
155 /// | |
156 /// The [type] is the type to construct. If not supplied, it defaults to | |
157 /// [PolymerElement]. | |
158 // NOTE: this is called "element" in src/declaration/polymer-element.js, and | |
159 // exported as "Polymer". | |
160 static void register(String name, [Type type]) { | |
161 //console.log('registering [' + name + ']'); | |
162 if (type == null) type = PolymerElement; | |
163 | |
164 _typesByName[name] = type; | |
165 | |
166 // Dart note: here we notify JS of the element registration. We don't pass | |
167 // the Dart type because we will handle that in PolymerDeclaration. | |
168 // See _hookJsPolymerDeclaration for how this is done. | |
169 PolymerJs.constructor.apply([name]); | |
170 (js.context['HTMLElement']['register'] as JsFunction) | |
171 .apply([name, js.context['HTMLElement']['prototype']]); | |
172 } | |
173 | |
174 /// Register a custom element that has no associated `<polymer-element>`. | |
175 /// Unlike [register] this will always perform synchronous registration and | |
176 /// by the time this method returns the element will be available using | |
177 /// [document.createElement] or by modifying the HTML to include the element. | |
178 static void registerSync(String name, Type type, | |
179 {String extendsTag, Document doc, Node template}) { | |
180 | |
181 // Our normal registration, this will queue up the name->type association. | |
182 register(name, type); | |
183 | |
184 // Build a polymer-element and initialize it to register | |
185 if (doc == null) doc = document; | |
186 var poly = doc.createElement('polymer-element'); | |
187 poly.attributes['name'] = name; | |
188 if (extendsTag != null) poly.attributes['extends'] = extendsTag; | |
189 if (template != null) poly.append(template); | |
190 | |
191 // TODO(jmesserly): conceptually this is just: | |
192 // new JsObject.fromBrowserObject(poly).callMethod('init') | |
193 // | |
194 // However doing it that way hits an issue with JS-interop in IE10: we get a | |
195 // JsObject that wraps something other than `poly`, due to improper caching. | |
196 // By reusing _polymerElementProto that we used for 'register', we can | |
197 // then call apply on it to invoke init() with the correct `this` pointer. | |
198 JsFunction init = _polymerElementProto['init']; | |
199 init.apply([], thisArg: poly); | |
200 } | |
201 | |
202 // Warning for when people try to use `importElements` or `import`. | |
203 static const String _DYNAMIC_IMPORT_WARNING = 'Dynamically loading html ' | |
204 'imports has very limited support right now in dart, see ' | |
205 'http://dartbug.com/17873.'; | |
206 | |
207 /// Loads the set of HTMLImports contained in `node`. Returns a future that | |
208 /// resolves when all the imports have been loaded. This method can be used to | |
209 /// lazily load imports. For example, given a template: | |
210 /// | |
211 /// <template> | |
212 /// <link rel="import" href="my-import1.html"> | |
213 /// <link rel="import" href="my-import2.html"> | |
214 /// </template> | |
215 /// | |
216 /// Polymer.importElements(template.content) | |
217 /// .then((_) => print('imports lazily loaded')); | |
218 /// | |
219 /// Dart Note: This has very limited support in dart, http://dartbug.com/17873 | |
220 // Dart Note: From src/lib/import.js For now proxy to the JS methods, | |
221 // because we want to share the loader with polymer.js for interop purposes. | |
222 static Future importElements(Node elementOrFragment) { | |
223 print(_DYNAMIC_IMPORT_WARNING); | |
224 return PolymerJs.importElements(elementOrFragment); | |
225 } | |
226 | |
227 /// Loads an HTMLImport for each url specified in the `urls` array. Notifies | |
228 /// when all the imports have loaded by calling the `callback` function | |
229 /// argument. This method can be used to lazily load imports. For example, | |
230 /// For example, | |
231 /// | |
232 /// Polymer.import(['my-import1.html', 'my-import2.html']) | |
233 /// .then((_) => print('imports lazily loaded')); | |
234 /// | |
235 /// Dart Note: This has very limited support in dart, http://dartbug.com/17873 | |
236 // Dart Note: From src/lib/import.js. For now proxy to the JS methods, | |
237 // because we want to share the loader with polymer.js for interop purposes. | |
238 static Future import(List urls) { | |
239 print(_DYNAMIC_IMPORT_WARNING); | |
240 return PolymerJs.import(urls); | |
241 } | |
242 | |
243 /// Deprecated: Use `import` instead. | |
244 @deprecated | |
245 static Future importUrls(List urls) { | |
246 return import(urls); | |
247 } | |
248 | |
249 /// Completes when polymer js is ready. | |
250 static final Completer _onReady = new Completer(); | |
251 | |
252 /// Completes when all initialization is done. | |
253 static final Completer _onInitDone = new Completer(); | |
254 | |
255 /// Future indicating that the Polymer library has been loaded and is ready | |
256 /// for use. | |
257 static Future get onReady => | |
258 Future.wait([_onReady.future, _onInitDone.future]); | |
259 | |
260 /// Returns a list of elements that have had polymer-elements created but | |
261 /// are not yet ready to register. The list is an array of element | |
262 /// definitions. | |
263 static List<Element> get waitingFor => PolymerJs.waitingFor; | |
264 | |
265 /// Forces polymer to register any pending elements. Can be used to abort | |
266 /// waiting for elements that are partially defined. | |
267 static forceReady([int timeout]) => PolymerJs.forceReady(timeout); | |
268 | |
269 /// The most derived `<polymer-element>` declaration for this element. | |
270 PolymerDeclaration get element => _element; | |
271 PolymerDeclaration _element; | |
272 | |
273 /// Deprecated: use [element] instead. | |
274 @deprecated PolymerDeclaration get declaration => _element; | |
275 | |
276 Map<String, StreamSubscription> _namedObservers; | |
277 List<Bindable> _observers = []; | |
278 | |
279 bool _unbound; // lazy-initialized | |
280 PolymerJob _unbindAllJob; | |
281 | |
282 CompoundObserver _propertyObserver; | |
283 bool _readied = false; | |
284 | |
285 JsObject _jsElem; | |
286 | |
287 /// Returns the object that should be used as the event controller for | |
288 /// event bindings in this element's template. If set, this will override the | |
289 /// normal controller lookup. | |
290 // TODO(jmesserly): we need to use a JS-writable property as our backing | |
291 // store, because of elements such as: | |
292 // https://github.com/Polymer/core-overlay/blob/eeb14853/core-overlay-layer.ht
ml#L78 | |
293 get eventController => _jsElem['eventController']; | |
294 set eventController(value) { | |
295 _jsElem['eventController'] = value; | |
296 } | |
297 | |
298 bool get hasBeenAttached => _hasBeenAttached; | |
299 bool _hasBeenAttached = false; | |
300 | |
301 /// Gets the shadow root associated with the corresponding custom element. | |
302 /// | |
303 /// This is identical to [shadowRoot], unless there are multiple levels of | |
304 /// inheritance and they each have their own shadow root. For example, | |
305 /// this can happen if the base class and subclass both have `<template>` tags | |
306 /// in their `<polymer-element>` tags. | |
307 // TODO(jmesserly): should expose this as an immutable map. | |
308 // Similar issue as $. | |
309 final Map<String, ShadowRoot> shadowRoots = | |
310 new LinkedHashMap<String, ShadowRoot>(); | |
311 | |
312 /// Map of items in the shadow root(s) by their [Element.id]. | |
313 // TODO(jmesserly): various issues: | |
314 // * wrap in UnmodifiableMapView? | |
315 // * should we have an object that implements noSuchMethod? | |
316 // * should the map have a key order (e.g. LinkedHash or SplayTree)? | |
317 // * should this be a live list? Polymer doesn't, maybe due to JS limitations? | |
318 // Note: this is observable to support $['someId'] being used in templates. | |
319 // The template is stamped before $ is populated, so we need observation if | |
320 // we want it to be usable in bindings. | |
321 final Map<String, dynamic> $ = new ObservableMap<String, dynamic>(); | |
322 | |
323 /// Use to override the default syntax for polymer-elements. | |
324 /// By default this will be null, which causes [instanceTemplate] to use | |
325 /// the template's bindingDelegate or the [element.syntax], in that order. | |
326 PolymerExpressions get syntax => null; | |
327 | |
328 bool get _elementPrepared => _element != null; | |
329 | |
330 /// Retrieves the custom element name. It should be used instead | |
331 /// of localName, see: https://github.com/Polymer/polymer-dev/issues/26 | |
332 String get _name { | |
333 if (_element != null) return _element.name; | |
334 var isAttr = attributes['is']; | |
335 return (isAttr == null || isAttr == '') ? localName : isAttr; | |
336 } | |
337 | |
338 /// By default the data bindings will be cleaned up when this custom element | |
339 /// is detached from the document. Overriding this to return `true` will | |
340 /// prevent that from happening. | |
341 bool get preventDispose => false; | |
342 | |
343 /// Properties exposed by this element. | |
344 // Dart note: unlike Javascript we can't override the original property on | |
345 // the object, so we use this mechanism instead to define properties. See more | |
346 // details in [_PropertyAccessor]. | |
347 Map<Symbol, _PropertyAccessor> _properties = {}; | |
348 | |
349 /// Helper to implement a property with the given [name]. This is used for | |
350 /// normal and computed properties. Normal properties can provide the initial | |
351 /// value using the [initialValue] function. Computed properties ignore | |
352 /// [initialValue], their value is derived from the expression in the | |
353 /// [ComputedProperty] annotation that appears above the getter that uses this | |
354 /// helper. | |
355 readValue(Symbol name, [initialValue()]) { | |
356 var property = _properties[name]; | |
357 if (property == null) { | |
358 var value; | |
359 // Dart note: most computed properties are created in advance in | |
360 // createComputedProperties, but if one computed property depends on | |
361 // another, the declaration order might matter. Rather than trying to | |
362 // register them in order, we include here also the option of lazily | |
363 // creating the property accessor on the first read. | |
364 var binding = _getBindingForComputedProperty(name); | |
365 if (binding == null) { | |
366 // normal property | |
367 value = initialValue != null ? initialValue() : null; | |
368 } else { | |
369 value = binding.value; | |
370 } | |
371 property = _properties[name] = new _PropertyAccessor(name, this, value); | |
372 } | |
373 return property.value; | |
374 } | |
375 | |
376 /// Helper to implement a setter of a property with the given [name] on a | |
377 /// polymer element. This can be used on normal properties and also on | |
378 /// computed properties, as long as the expression used for the computed | |
379 /// property is assignable (see [ComputedProperty]). | |
380 writeValue(Symbol name, newValue) { | |
381 var property = _properties[name]; | |
382 if (property == null) { | |
383 // Note: computed properties are created in advance in | |
384 // createComputedProperties, so we should only need to create here | |
385 // non-computed properties. | |
386 property = _properties[name] = new _PropertyAccessor(name, this, null); | |
387 } | |
388 property.value = newValue; | |
389 } | |
390 | |
391 /// If this class is used as a mixin, this method must be called from inside | |
392 /// of the `created()` constructor. | |
393 /// | |
394 /// If this class is a superclass, calling `super.created()` is sufficient. | |
395 void polymerCreated() { | |
396 var t = nodeBind(this).templateInstance; | |
397 if (t != null && t.model != null) { | |
398 window.console.warn('Attributes on $_name were data bound ' | |
399 'prior to Polymer upgrading the element. This may result in ' | |
400 'incorrect binding types.'); | |
401 } | |
402 prepareElement(); | |
403 if (!isTemplateStagingDocument(ownerDocument)) { | |
404 _makeElementReady(); | |
405 } | |
406 } | |
407 | |
408 /// *Deprecated* use [shadowRoots] instead. | |
409 @deprecated | |
410 ShadowRoot getShadowRoot(String customTagName) => shadowRoots[customTagName]; | |
411 | |
412 void prepareElement() { | |
413 if (_elementPrepared) { | |
414 window.console.warn('Element already prepared: $_name'); | |
415 return; | |
416 } | |
417 _initJsObject(); | |
418 // Dart note: get the corresponding <polymer-element> declaration. | |
419 _element = _getDeclaration(_name); | |
420 // install property storage | |
421 createPropertyObserver(); | |
422 openPropertyObserver(); | |
423 // install boilerplate attributes | |
424 copyInstanceAttributes(); | |
425 // process input attributes | |
426 takeAttributes(); | |
427 // add event listeners | |
428 addHostListeners(); | |
429 } | |
430 | |
431 /// Initialize JS interop for this element. For now we just initialize the | |
432 /// JsObject, but in the future we could also initialize JS APIs here. | |
433 _initJsObject() { | |
434 _jsElem = new JsObject.fromBrowserObject(this); | |
435 } | |
436 | |
437 /// Deprecated: This is no longer a public method. | |
438 @deprecated | |
439 makeElementReady() => _makeElementReady(); | |
440 | |
441 _makeElementReady() { | |
442 if (_readied) return; | |
443 _readied = true; | |
444 createComputedProperties(); | |
445 | |
446 parseDeclarations(_element); | |
447 // NOTE: Support use of the `unresolved` attribute to help polyfill | |
448 // custom elements' `:unresolved` feature. | |
449 attributes.remove('unresolved'); | |
450 // user entry point | |
451 _readyLog.info(() => '[$this]: ready'); | |
452 ready(); | |
453 } | |
454 | |
455 /// Lifecycle method called when the element has populated it's `shadowRoot`, | |
456 /// prepared data-observation, and made itself ready for API interaction. | |
457 /// To wait until the element has been attached to the default view, use | |
458 /// [attached] or [domReady]. | |
459 void ready() {} | |
460 | |
461 /// Implement to access custom elements in dom descendants, ancestors, | |
462 /// or siblings. Because custom elements upgrade in document order, | |
463 /// elements accessed in `ready` or `attached` may not be upgraded. When | |
464 /// `domReady` is called, all registered custom elements are guaranteed | |
465 /// to have been upgraded. | |
466 void domReady() {} | |
467 | |
468 void attached() { | |
469 if (!_elementPrepared) { | |
470 // Dart specific message for a common issue. | |
471 throw new StateError('polymerCreated was not called for custom element ' | |
472 '$_name, this should normally be done in the .created() if Polymer ' | |
473 'is used as a mixin.'); | |
474 } | |
475 | |
476 cancelUnbindAll(); | |
477 if (!hasBeenAttached) { | |
478 _hasBeenAttached = true; | |
479 async((_) => domReady()); | |
480 } | |
481 } | |
482 | |
483 void detached() { | |
484 if (!preventDispose) asyncUnbindAll(); | |
485 } | |
486 | |
487 /// Walks the prototype-chain of this element and allows specific | |
488 /// classes a chance to process static declarations. | |
489 /// | |
490 /// In particular, each polymer-element has it's own `template`. | |
491 /// `parseDeclarations` is used to accumulate all element `template`s | |
492 /// from an inheritance chain. | |
493 /// | |
494 /// `parseDeclaration` static methods implemented in the chain are called | |
495 /// recursively, oldest first, with the `<polymer-element>` associated | |
496 /// with the current prototype passed as an argument. | |
497 /// | |
498 /// An element may override this method to customize shadow-root generation. | |
499 void parseDeclarations(PolymerDeclaration declaration) { | |
500 if (declaration != null) { | |
501 parseDeclarations(declaration.superDeclaration); | |
502 parseDeclaration(declaration.element); | |
503 } | |
504 } | |
505 | |
506 /// Perform init-time actions based on static information in the | |
507 /// `<polymer-element>` instance argument. | |
508 /// | |
509 /// For example, the standard implementation locates the template associated | |
510 /// with the given `<polymer-element>` and stamps it into a shadow-root to | |
511 /// implement shadow inheritance. | |
512 /// | |
513 /// An element may override this method for custom behavior. | |
514 void parseDeclaration(Element elementElement) { | |
515 var template = fetchTemplate(elementElement); | |
516 | |
517 if (template != null) { | |
518 var root = shadowFromTemplate(template); | |
519 | |
520 var name = elementElement.attributes['name']; | |
521 if (name == null) return; | |
522 shadowRoots[name] = root; | |
523 } | |
524 } | |
525 | |
526 /// Given a `<polymer-element>`, find an associated template (if any) to be | |
527 /// used for shadow-root generation. | |
528 /// | |
529 /// An element may override this method for custom behavior. | |
530 Element fetchTemplate(Element elementElement) => | |
531 elementElement.querySelector('template'); | |
532 | |
533 /// Utility function that stamps a `<template>` into light-dom. | |
534 Node lightFromTemplate(Element template, [Node refNode]) { | |
535 if (template == null) return null; | |
536 | |
537 // TODO(sorvell): mark this element as an event controller so that | |
538 // event listeners on bound nodes inside it will be called on it. | |
539 // Note, the expectation here is that events on all descendants | |
540 // should be handled by this element. | |
541 eventController = this; | |
542 | |
543 // stamp template | |
544 // which includes parsing and applying MDV bindings before being | |
545 // inserted (to avoid {{}} in attribute values) | |
546 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | |
547 var dom = instanceTemplate(template); | |
548 // append to shadow dom | |
549 if (refNode != null) { | |
550 append(dom); | |
551 } else { | |
552 insertBefore(dom, refNode); | |
553 } | |
554 // perform post-construction initialization tasks on ahem, light root | |
555 shadowRootReady(this); | |
556 // return the created shadow root | |
557 return dom; | |
558 } | |
559 | |
560 /// Utility function that creates a shadow root from a `<template>`. | |
561 /// | |
562 /// The base implementation will return a [ShadowRoot], but you can replace it | |
563 /// with your own code and skip ShadowRoot creation. In that case, you should | |
564 /// return `null`. | |
565 /// | |
566 /// In your overridden method, you can use [instanceTemplate] to stamp the | |
567 /// template and initialize data binding, and [shadowRootReady] to intialize | |
568 /// other Polymer features like event handlers. It is fine to call | |
569 /// shadowRootReady with a node other than a ShadowRoot such as with `this`. | |
570 ShadowRoot shadowFromTemplate(Element template) { | |
571 if (template == null) return null; | |
572 // make a shadow root | |
573 var root = createShadowRoot(); | |
574 // stamp template | |
575 // which includes parsing and applying MDV bindings before being | |
576 // inserted (to avoid {{}} in attribute values) | |
577 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | |
578 var dom = instanceTemplate(template); | |
579 // append to shadow dom | |
580 root.append(dom); | |
581 // perform post-construction initialization tasks on shadow root | |
582 shadowRootReady(root); | |
583 // return the created shadow root | |
584 return root; | |
585 } | |
586 | |
587 void shadowRootReady(Node root) { | |
588 // locate nodes with id and store references to them in this.$ hash | |
589 marshalNodeReferences(root); | |
590 } | |
591 | |
592 /// Locate nodes with id and store references to them in [$] hash. | |
593 void marshalNodeReferences(Node root) { | |
594 if (root == null) return; | |
595 for (var n in (root as dynamic).querySelectorAll('[id]')) { | |
596 $[n.id] = n; | |
597 } | |
598 } | |
599 | |
600 void attributeChanged(String name, String oldValue, String newValue) { | |
601 if (name != 'class' && name != 'style') { | |
602 attributeToProperty(name, newValue); | |
603 } | |
604 } | |
605 | |
606 // TODO(jmesserly): this could be a top level method. | |
607 /// Returns a future when `node` changes, or when its children or subtree | |
608 /// changes. | |
609 /// | |
610 /// Use [MutationObserver] if you want to listen to a stream of changes. | |
611 Future<List<MutationRecord>> onMutation(Node node) { | |
612 var completer = new Completer(); | |
613 new MutationObserver((mutations, observer) { | |
614 observer.disconnect(); | |
615 completer.complete(mutations); | |
616 })..observe(node, childList: true, subtree: true); | |
617 return completer.future; | |
618 } | |
619 | |
620 // copy attributes defined in the element declaration to the instance | |
621 // e.g. <polymer-element name="x-foo" tabIndex="0"> tabIndex is copied | |
622 // to the element instance here. | |
623 void copyInstanceAttributes() { | |
624 _element._instanceAttributes.forEach((name, value) { | |
625 attributes.putIfAbsent(name, () => value); | |
626 }); | |
627 } | |
628 | |
629 void takeAttributes() { | |
630 if (_element._publishLC == null) return; | |
631 attributes.forEach(attributeToProperty); | |
632 } | |
633 | |
634 /// If attribute [name] is mapped to a property, deserialize | |
635 /// [value] into that property. | |
636 void attributeToProperty(String name, String value) { | |
637 // try to match this attribute to a property (attributes are | |
638 // all lower-case, so this is case-insensitive search) | |
639 var decl = propertyForAttribute(name); | |
640 if (decl == null) return; | |
641 | |
642 // filter out 'mustached' values, these are to be | |
643 // replaced with bound-data and are not yet values | |
644 // themselves. | |
645 if (value == null || value.contains(Polymer.bindPattern)) return; | |
646 | |
647 final currentValue = smoke.read(this, decl.name); | |
648 | |
649 // deserialize Boolean or Number values from attribute | |
650 var type = decl.type; | |
651 if ((type == Object || type == dynamic) && currentValue != null) { | |
652 // Attempt to infer field type from the current value. | |
653 type = currentValue.runtimeType; | |
654 } | |
655 final newValue = deserializeValue(value, currentValue, type); | |
656 | |
657 // only act if the value has changed | |
658 if (!identical(newValue, currentValue)) { | |
659 // install new value (has side-effects) | |
660 smoke.write(this, decl.name, newValue); | |
661 } | |
662 } | |
663 | |
664 /// Return the published property matching name, or null. | |
665 // TODO(jmesserly): should we just return Symbol here? | |
666 smoke.Declaration propertyForAttribute(String name) { | |
667 final publishLC = _element._publishLC; | |
668 if (publishLC == null) return null; | |
669 return publishLC[name]; | |
670 } | |
671 | |
672 /// Convert representation of [value] based on [type] and [currentValue]. | |
673 Object deserializeValue(String value, Object currentValue, Type type) => | |
674 deserialize.deserializeValue(value, currentValue, type); | |
675 | |
676 String serializeValue(Object value) { | |
677 if (value == null) return null; | |
678 | |
679 if (value is bool) { | |
680 return _toBoolean(value) ? '' : null; | |
681 } else if (value is String || value is num) { | |
682 return '$value'; | |
683 } | |
684 return null; | |
685 } | |
686 | |
687 void reflectPropertyToAttribute(String path) { | |
688 // TODO(sjmiles): consider memoizing this | |
689 // try to intelligently serialize property value | |
690 final propValue = new PropertyPath(path).getValueFrom(this); | |
691 final serializedValue = serializeValue(propValue); | |
692 // boolean properties must reflect as boolean attributes | |
693 if (serializedValue != null) { | |
694 attributes[path] = serializedValue; | |
695 // TODO(sorvell): we should remove attr for all properties | |
696 // that have undefined serialization; however, we will need to | |
697 // refine the attr reflection system to achieve this; pica, for example, | |
698 // relies on having inferredType object properties not removed as | |
699 // attrs. | |
700 } else if (propValue is bool) { | |
701 attributes.remove(path); | |
702 } | |
703 } | |
704 | |
705 /// Creates the document fragment to use for each instance of the custom | |
706 /// element, given the `<template>` node. By default this is equivalent to: | |
707 /// | |
708 /// templateBind(template).createInstance(this, polymerSyntax); | |
709 /// | |
710 /// Where polymerSyntax is a singleton [PolymerExpressions] instance. | |
711 /// | |
712 /// You can override this method to change the instantiation behavior of the | |
713 /// template, for example to use a different data-binding syntax. | |
714 DocumentFragment instanceTemplate(Element template) { | |
715 // ensure template is decorated (lets things like <tr template ...> work) | |
716 TemplateBindExtension.decorate(template); | |
717 var syntax = this.syntax; | |
718 var t = templateBind(template); | |
719 if (syntax == null && t.bindingDelegate == null) { | |
720 syntax = element.syntax; | |
721 } | |
722 var dom = t.createInstance(this, syntax); | |
723 _observers.addAll(getTemplateInstanceBindings(dom)); | |
724 return dom; | |
725 } | |
726 | |
727 /// Called by TemplateBinding/NodeBind to setup a binding to the given | |
728 /// property. It's overridden here to support property bindings in addition to | |
729 /// attribute bindings that are supported by default. | |
730 Bindable bind(String name, bindable, {bool oneTime: false}) { | |
731 var decl = propertyForAttribute(name); | |
732 if (decl == null) { | |
733 // Cannot call super.bind because template_binding is its own package | |
734 return nodeBindFallback(this).bind(name, bindable, oneTime: oneTime); | |
735 } else { | |
736 // use n-way Polymer binding | |
737 var observer = bindProperty(decl.name, bindable, oneTime: oneTime); | |
738 // NOTE: reflecting binding information is typically required only for | |
739 // tooling. It has a performance cost so it's opt-in in Node.bind. | |
740 if (enableBindingsReflection && observer != null) { | |
741 // Dart note: this is not needed because of how _PolymerBinding works. | |
742 //observer.path = bindable.path_; | |
743 _recordBinding(name, observer); | |
744 } | |
745 var reflect = _element._reflect; | |
746 | |
747 // Get back to the (possibly camel-case) name for the property. | |
748 var propName = smoke.symbolToName(decl.name); | |
749 if (reflect != null && reflect.contains(propName)) { | |
750 reflectPropertyToAttribute(propName); | |
751 } | |
752 return observer; | |
753 } | |
754 } | |
755 | |
756 _recordBinding(String name, observer) { | |
757 if (bindings == null) bindings = {}; | |
758 this.bindings[name] = observer; | |
759 } | |
760 | |
761 /// Called by TemplateBinding when all bindings on an element have been | |
762 /// executed. This signals that all element inputs have been gathered and it's | |
763 /// safe to ready the element, create shadow-root and start data-observation. | |
764 bindFinished() => _makeElementReady(); | |
765 | |
766 Map<String, Bindable> get bindings => nodeBindFallback(this).bindings; | |
767 set bindings(Map value) { | |
768 nodeBindFallback(this).bindings = value; | |
769 } | |
770 | |
771 TemplateInstance get templateInstance => | |
772 nodeBindFallback(this).templateInstance; | |
773 | |
774 /// Called at detached time to signal that an element's bindings should be | |
775 /// cleaned up. This is done asynchronously so that users have the chance to | |
776 /// call `cancelUnbindAll` to prevent unbinding. | |
777 void asyncUnbindAll() { | |
778 if (_unbound == true) return; | |
779 _unbindLog.fine(() => '[$_name] asyncUnbindAll'); | |
780 _unbindAllJob = scheduleJob(_unbindAllJob, unbindAll); | |
781 } | |
782 | |
783 /// This method should rarely be used and only if `cancelUnbindAll` has been | |
784 /// called to prevent element unbinding. In this case, the element's bindings | |
785 /// will not be automatically cleaned up and it cannot be garbage collected by | |
786 /// by the system. If memory pressure is a concern or a large amount of | |
787 /// elements need to be managed in this way, `unbindAll` can be called to | |
788 /// deactivate the element's bindings and allow its memory to be reclaimed. | |
789 void unbindAll() { | |
790 if (_unbound == true) return; | |
791 closeObservers(); | |
792 closeNamedObservers(); | |
793 _unbound = true; | |
794 } | |
795 | |
796 //// Call in `detached` to prevent the element from unbinding when it is | |
797 //// detached from the dom. The element is unbound as a cleanup step that | |
798 //// allows its memory to be reclaimed. If `cancelUnbindAll` is used, consider | |
799 /// calling `unbindAll` when the element is no longer needed. This will allow | |
800 /// its memory to be reclaimed. | |
801 void cancelUnbindAll() { | |
802 if (_unbound == true) { | |
803 _unbindLog | |
804 .warning(() => '[$_name] already unbound, cannot cancel unbindAll'); | |
805 return; | |
806 } | |
807 _unbindLog.fine(() => '[$_name] cancelUnbindAll'); | |
808 if (_unbindAllJob != null) { | |
809 _unbindAllJob.stop(); | |
810 _unbindAllJob = null; | |
811 } | |
812 } | |
813 | |
814 static void _forNodeTree(Node node, void callback(Node node)) { | |
815 if (node == null) return; | |
816 | |
817 callback(node); | |
818 for (var child = node.firstChild; child != null; child = child.nextNode) { | |
819 _forNodeTree(child, callback); | |
820 } | |
821 } | |
822 | |
823 /// Creates a CompoundObserver to observe property changes. | |
824 /// NOTE, this is only done if there are any properties in the `_observe` | |
825 /// object. | |
826 void createPropertyObserver() { | |
827 final observe = _element._observe; | |
828 if (observe != null) { | |
829 var o = _propertyObserver = new CompoundObserver(); | |
830 // keep track of property observer so we can shut it down | |
831 _observers.add(o); | |
832 | |
833 for (var path in observe.keys) { | |
834 o.addPath(this, path); | |
835 | |
836 // TODO(jmesserly): on the Polymer side it doesn't look like they | |
837 // will observe arrays unless it is a length == 1 path. | |
838 observeArrayValue(path, path.getValueFrom(this), null); | |
839 } | |
840 } | |
841 } | |
842 | |
843 /// Start observing property changes. | |
844 void openPropertyObserver() { | |
845 if (_propertyObserver != null) { | |
846 _propertyObserver.open(notifyPropertyChanges); | |
847 } | |
848 | |
849 // Dart note: we need an extra listener only to continue supporting | |
850 // @published properties that follow the old syntax until we get rid of it. | |
851 // This workaround has timing issues so we prefer the new, not so nice, | |
852 // syntax. | |
853 if (_element._publish != null) { | |
854 changes.listen(_propertyChangeWorkaround); | |
855 } | |
856 } | |
857 | |
858 /// Handler for property changes; routes changes to observing methods. | |
859 /// Note: array valued properties are observed for array splices. | |
860 void notifyPropertyChanges(List newValues, Map oldValues, List paths) { | |
861 final observe = _element._observe; | |
862 final called = new HashSet(); | |
863 | |
864 oldValues.forEach((i, oldValue) { | |
865 final newValue = newValues[i]; | |
866 | |
867 // Date note: we don't need any special checking for null and undefined. | |
868 | |
869 // note: paths is of form [object, path, object, path] | |
870 final path = paths[2 * i + 1]; | |
871 if (observe == null) return; | |
872 | |
873 var methods = observe[path]; | |
874 if (methods == null) return; | |
875 | |
876 for (var method in methods) { | |
877 if (!called.add(method)) continue; // don't invoke more than once. | |
878 | |
879 observeArrayValue(path, newValue, oldValue); | |
880 // Dart note: JS passes "arguments", so we pass along our args. | |
881 // TODO(sorvell): call method with the set of values it's expecting; | |
882 // e.g. 'foo bar': 'invalidate' expects the new and old values for | |
883 // foo and bar. Currently we give only one of these and then | |
884 // deliver all the arguments. | |
885 smoke.invoke( | |
886 this, method, [oldValue, newValue, newValues, oldValues, paths], | |
887 adjust: true); | |
888 } | |
889 }); | |
890 } | |
891 | |
892 /// Force any pending property changes to synchronously deliver to handlers | |
893 /// specified in the `observe` object. | |
894 /// Note: normally changes are processed at microtask time. | |
895 /// | |
896 // Dart note: had to rename this to avoid colliding with | |
897 // Observable.deliverChanges. Even worse, super calls aren't possible or | |
898 // it prevents Polymer from being a mixin, so we can't override it even if | |
899 // we wanted to. | |
900 void deliverPropertyChanges() { | |
901 if (_propertyObserver != null) { | |
902 _propertyObserver.deliver(); | |
903 } | |
904 } | |
905 | |
906 // Dart note: this workaround is only for old-style @published properties, | |
907 // which have timing issues. See _bindOldStylePublishedProperty below. | |
908 // TODO(sigmund): deprecate this. | |
909 void _propertyChangeWorkaround(List<ChangeRecord> records) { | |
910 for (var record in records) { | |
911 if (record is! PropertyChangeRecord) continue; | |
912 | |
913 var name = record.name; | |
914 // The setter of a new-style property will create an accessor in | |
915 // _properties[name]. We can skip the workaround for those properties. | |
916 if (_properties[name] != null) continue; | |
917 _propertyChange(name, record.newValue, record.oldValue); | |
918 } | |
919 } | |
920 | |
921 void _propertyChange(Symbol nameSymbol, newValue, oldValue) { | |
922 _watchLog.info( | |
923 () => '[$this]: $nameSymbol changed from: $oldValue to: $newValue'); | |
924 var name = smoke.symbolToName(nameSymbol); | |
925 var reflect = _element._reflect; | |
926 if (reflect != null && reflect.contains(name)) { | |
927 reflectPropertyToAttribute(name); | |
928 } | |
929 } | |
930 | |
931 void observeArrayValue(PropertyPath name, Object value, Object old) { | |
932 final observe = _element._observe; | |
933 if (observe == null) return; | |
934 | |
935 // we only care if there are registered side-effects | |
936 var callbacks = observe[name]; | |
937 if (callbacks == null) return; | |
938 | |
939 // if we are observing the previous value, stop | |
940 if (old is ObservableList) { | |
941 _observeLog.fine(() => '[$_name] observeArrayValue: unregister $name'); | |
942 | |
943 closeNamedObserver('${name}__array'); | |
944 } | |
945 // if the new value is an array, begin observing it | |
946 if (value is ObservableList) { | |
947 _observeLog.fine(() => '[$_name] observeArrayValue: register $name'); | |
948 var sub = value.listChanges.listen((changes) { | |
949 for (var callback in callbacks) { | |
950 smoke.invoke(this, callback, [changes], adjust: true); | |
951 } | |
952 }); | |
953 registerNamedObserver('${name}__array', sub); | |
954 } | |
955 } | |
956 | |
957 emitPropertyChangeRecord(Symbol name, newValue, oldValue) { | |
958 if (identical(oldValue, newValue)) return; | |
959 _propertyChange(name, newValue, oldValue); | |
960 } | |
961 | |
962 bindToAccessor(Symbol name, Bindable bindable, {resolveBindingValue: false}) { | |
963 // Dart note: our pattern is to declare the initial value in the getter. We | |
964 // read it via smoke to ensure that the value is initialized correctly. | |
965 var oldValue = smoke.read(this, name); | |
966 var property = _properties[name]; | |
967 if (property == null) { | |
968 // We know that _properties[name] is null only for old-style @published | |
969 // properties. This fallback is here to make it easier to deprecate the | |
970 // old-style of published properties, which have bad timing guarantees | |
971 // (see comment in _PolymerBinding). | |
972 return _bindOldStylePublishedProperty(name, bindable, oldValue); | |
973 } | |
974 | |
975 property.bindable = bindable; | |
976 var value = bindable.open(property.updateValue); | |
977 | |
978 if (resolveBindingValue) { | |
979 // capture A's value if B's value is null or undefined, | |
980 // otherwise use B's value | |
981 var v = (value == null ? oldValue : value); | |
982 if (!identical(value, oldValue)) { | |
983 bindable.value = value = v; | |
984 } | |
985 } | |
986 | |
987 property.updateValue(value); | |
988 var o = new _CloseOnlyBinding(property); | |
989 _observers.add(o); | |
990 return o; | |
991 } | |
992 | |
993 // Dart note: this fallback uses our old-style binding mechanism to be able to | |
994 // link @published properties with bindings. This mechanism is backwards from | |
995 // what Javascript does because we can't override the original property. This | |
996 // workaround also brings some timing issues which are described in detail in | |
997 // dartbug.com/18343. | |
998 // TODO(sigmund): deprecate old-style @published properties. | |
999 _bindOldStylePublishedProperty(Symbol name, Bindable bindable, oldValue) { | |
1000 // capture A's value if B's value is null or undefined, | |
1001 // otherwise use B's value | |
1002 if (bindable.value == null) bindable.value = oldValue; | |
1003 | |
1004 var o = new _PolymerBinding(this, name, bindable); | |
1005 _observers.add(o); | |
1006 return o; | |
1007 } | |
1008 | |
1009 _getBindingForComputedProperty(Symbol name) { | |
1010 var exprString = element._computed[name]; | |
1011 if (exprString == null) return null; | |
1012 var expr = PolymerExpressions.getExpression(exprString); | |
1013 return PolymerExpressions.getBinding(expr, this, | |
1014 globals: element.syntax.globals); | |
1015 } | |
1016 | |
1017 createComputedProperties() { | |
1018 var computed = this.element._computed; | |
1019 for (var name in computed.keys) { | |
1020 try { | |
1021 // Dart note: this is done in Javascript by modifying the prototype in | |
1022 // declaration/properties.js, we can't do that, so we do it here. | |
1023 var binding = _getBindingForComputedProperty(name); | |
1024 | |
1025 // Follow up note: ideally we would only create the accessor object | |
1026 // here, but some computed properties might depend on others and | |
1027 // evaluating `binding.value` could try to read the value of another | |
1028 // computed property that we haven't created yet. For this reason we | |
1029 // also allow to also create the accessor in [readValue]. | |
1030 if (_properties[name] == null) { | |
1031 _properties[name] = new _PropertyAccessor(name, this, binding.value); | |
1032 } | |
1033 bindToAccessor(name, binding); | |
1034 } catch (e) { | |
1035 window.console.error('Failed to create computed property $name' | |
1036 ' (${computed[name]}): $e'); | |
1037 } | |
1038 } | |
1039 } | |
1040 | |
1041 // Dart note: to simplify the code above we made registerObserver calls | |
1042 // directly invoke _observers.add/addAll. | |
1043 void closeObservers() { | |
1044 for (var o in _observers) { | |
1045 if (o != null) o.close(); | |
1046 } | |
1047 _observers = []; | |
1048 } | |
1049 | |
1050 /// Bookkeeping observers for memory management. | |
1051 void registerNamedObserver(String name, StreamSubscription sub) { | |
1052 if (_namedObservers == null) { | |
1053 _namedObservers = new Map<String, StreamSubscription>(); | |
1054 } | |
1055 _namedObservers[name] = sub; | |
1056 } | |
1057 | |
1058 bool closeNamedObserver(String name) { | |
1059 var sub = _namedObservers.remove(name); | |
1060 if (sub == null) return false; | |
1061 sub.cancel(); | |
1062 return true; | |
1063 } | |
1064 | |
1065 void closeNamedObservers() { | |
1066 if (_namedObservers == null) return; | |
1067 for (var sub in _namedObservers.values) { | |
1068 if (sub != null) sub.cancel(); | |
1069 } | |
1070 _namedObservers.clear(); | |
1071 _namedObservers = null; | |
1072 } | |
1073 | |
1074 /// Bind the [name] property in this element to [bindable]. *Note* in Dart it | |
1075 /// is necessary to also define the field: | |
1076 /// | |
1077 /// var myProperty; | |
1078 /// | |
1079 /// ready() { | |
1080 /// super.ready(); | |
1081 /// bindProperty(#myProperty, | |
1082 /// new PathObserver(this, 'myModel.path.to.otherProp')); | |
1083 /// } | |
1084 Bindable bindProperty(Symbol name, bindableOrValue, {oneTime: false}) { | |
1085 // Dart note: normally we only reach this code when we know it's a | |
1086 // property, but if someone uses bindProperty directly they might get a | |
1087 // NoSuchMethodError either from the getField below, or from the setField | |
1088 // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight | |
1089 // difference from Polymer.js behavior. | |
1090 | |
1091 _bindLog.fine(() => 'bindProperty: [$bindableOrValue] to [$_name].[$name]'); | |
1092 | |
1093 if (oneTime) { | |
1094 if (bindableOrValue is Bindable) { | |
1095 _bindLog.warning(() => | |
1096 'bindProperty: expected non-bindable value n a one-time binding to ' | |
1097 '[$_name].[$name], but found $bindableOrValue.'); | |
1098 } | |
1099 smoke.write(this, name, bindableOrValue); | |
1100 return null; | |
1101 } | |
1102 | |
1103 return bindToAccessor(name, bindableOrValue, resolveBindingValue: true); | |
1104 } | |
1105 | |
1106 /// Attach event listeners on the host (this) element. | |
1107 void addHostListeners() { | |
1108 var events = _element._eventDelegates; | |
1109 if (events.isEmpty) return; | |
1110 | |
1111 _eventsLog.fine(() => '[$_name] addHostListeners: $events'); | |
1112 | |
1113 // NOTE: host events look like bindings but really are not; | |
1114 // (1) we don't want the attribute to be set and (2) we want to support | |
1115 // multiple event listeners ('host' and 'instance') and Node.bind | |
1116 // by default supports 1 thing being bound. | |
1117 events.forEach((type, methodName) { | |
1118 // Dart note: the getEventHandler method is on our PolymerExpressions. | |
1119 PolymerGesturesJs.addEventListener( | |
1120 this, type, | |
1121 Zone.current.bindUnaryCallback( | |
1122 element.syntax.getEventHandler(this, this, methodName))); | |
1123 }); | |
1124 } | |
1125 | |
1126 /// Calls [methodOrCallback] with [args] if it is a closure, otherwise, treat | |
1127 /// it as a method name in [object], and invoke it. | |
1128 void dispatchMethod(object, callbackOrMethod, List args) { | |
1129 _eventsLog.info(() => '>>> [$_name]: dispatch $callbackOrMethod'); | |
1130 | |
1131 if (callbackOrMethod is Function) { | |
1132 int maxArgs = smoke.maxArgs(callbackOrMethod); | |
1133 if (maxArgs == -1) { | |
1134 _eventsLog.warning( | |
1135 'invalid callback: expected callback of 0, 1, 2, or 3 arguments'); | |
1136 } | |
1137 args.length = maxArgs; | |
1138 Function.apply(callbackOrMethod, args); | |
1139 } else if (callbackOrMethod is String) { | |
1140 smoke.invoke(object, smoke.nameToSymbol(callbackOrMethod), args, | |
1141 adjust: true); | |
1142 } else { | |
1143 _eventsLog.warning('invalid callback'); | |
1144 } | |
1145 | |
1146 _eventsLog.fine(() => '<<< [$_name]: dispatch $callbackOrMethod'); | |
1147 } | |
1148 | |
1149 /// Call [methodName] method on this object with [args]. | |
1150 invokeMethod(Symbol methodName, List args) => | |
1151 smoke.invoke(this, methodName, args, adjust: true); | |
1152 | |
1153 /// Invokes a function asynchronously. | |
1154 /// This will call `Polymer.flush()` and then return a `new Timer` | |
1155 /// with the provided [method] and [timeout]. | |
1156 /// | |
1157 /// If you would prefer to run the callback using | |
1158 /// [window.requestAnimationFrame], see the [async] method. | |
1159 /// | |
1160 /// To cancel, call [Timer.cancel] on the result of this method. | |
1161 Timer asyncTimer(void method(), Duration timeout) { | |
1162 // Dart note: "async" is split into 2 methods so it can have a sensible type | |
1163 // signatures. Also removed the various features that don't make sense in a | |
1164 // Dart world, like binding to "this" and taking arguments list. | |
1165 | |
1166 // when polyfilling Object.observe, ensure changes | |
1167 // propagate before executing the async method | |
1168 scheduleMicrotask(Observable.dirtyCheck); | |
1169 PolymerJs.flush(); // for polymer-js interop | |
1170 return new Timer(timeout, method); | |
1171 } | |
1172 | |
1173 /// Invokes a function asynchronously. The context of the callback function is | |
1174 /// function is bound to 'this' automatically. Returns a handle which may be | |
1175 /// passed to cancelAsync to cancel the asynchronous call. | |
1176 /// | |
1177 /// If you would prefer to run the callback after a given duration, see | |
1178 /// the [asyncTimer] method. | |
1179 /// | |
1180 /// If you would like to cancel this, use [cancelAsync]. | |
1181 int async(RequestAnimationFrameCallback method) { | |
1182 // when polyfilling Object.observe, ensure changes | |
1183 // propagate before executing the async method | |
1184 scheduleMicrotask(Observable.dirtyCheck); | |
1185 PolymerJs.flush(); // for polymer-js interop | |
1186 return window.requestAnimationFrame(method); | |
1187 } | |
1188 | |
1189 /// Cancel an operation scheduled by [async]. | |
1190 void cancelAsync(int id) => window.cancelAnimationFrame(id); | |
1191 | |
1192 /// Fire a [CustomEvent] targeting [onNode], or `this` if onNode is not | |
1193 /// supplied. Returns the new event. | |
1194 CustomEvent fire(String type, | |
1195 {Object detail, Node onNode, bool canBubble, bool cancelable}) { | |
1196 var node = onNode != null ? onNode : this; | |
1197 var event = new CustomEvent(type, | |
1198 canBubble: canBubble != null ? canBubble : true, | |
1199 cancelable: cancelable != null ? cancelable : true, | |
1200 detail: detail); | |
1201 node.dispatchEvent(event); | |
1202 return event; | |
1203 } | |
1204 | |
1205 /// Fire an event asynchronously. See [async] and [fire]. | |
1206 asyncFire(String type, {Object detail, Node toNode, bool canBubble}) { | |
1207 // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to | |
1208 // add "() =>" | |
1209 async((x) => | |
1210 fire(type, detail: detail, onNode: toNode, canBubble: canBubble)); | |
1211 } | |
1212 | |
1213 /// Remove [className] from [old], add class to [anew], if they exist. | |
1214 void classFollows(Element anew, Element old, String className) { | |
1215 if (old != null) { | |
1216 old.classes.remove(className); | |
1217 } | |
1218 if (anew != null) { | |
1219 anew.classes.add(className); | |
1220 } | |
1221 } | |
1222 | |
1223 /// Installs external stylesheets and <style> elements with the attribute | |
1224 /// polymer-scope='controller' into the scope of element. This is intended | |
1225 /// to be called during custom element construction. | |
1226 void installControllerStyles() { | |
1227 var scope = findStyleScope(); | |
1228 if (scope != null && !scopeHasNamedStyle(scope, localName)) { | |
1229 // allow inherited controller styles | |
1230 var decl = _element; | |
1231 var cssText = new StringBuffer(); | |
1232 while (decl != null) { | |
1233 cssText.write(decl.cssTextForScope(_STYLE_CONTROLLER_SCOPE)); | |
1234 decl = decl.superDeclaration; | |
1235 } | |
1236 if (cssText.isNotEmpty) { | |
1237 installScopeCssText('$cssText', scope); | |
1238 } | |
1239 } | |
1240 } | |
1241 | |
1242 void installScopeStyle(style, [String name, Node scope]) { | |
1243 if (scope == null) scope = findStyleScope(); | |
1244 if (name == null) name = ''; | |
1245 | |
1246 if (scope != null && !scopeHasNamedStyle(scope, '$_name$name')) { | |
1247 var cssText = new StringBuffer(); | |
1248 if (style is Iterable) { | |
1249 for (var s in style) { | |
1250 cssText | |
1251 ..writeln(s.text) | |
1252 ..writeln(); | |
1253 } | |
1254 } else { | |
1255 cssText = (style as Node).text; | |
1256 } | |
1257 installScopeCssText('$cssText', scope, name); | |
1258 } | |
1259 } | |
1260 | |
1261 void installScopeCssText(String cssText, [Node scope, String name]) { | |
1262 if (scope == null) scope = findStyleScope(); | |
1263 if (name == null) name = ''; | |
1264 | |
1265 if (scope == null) return; | |
1266 | |
1267 if (_hasShadowDomPolyfill) { | |
1268 cssText = _shimCssText(cssText, scope is ShadowRoot ? scope.host : null); | |
1269 } | |
1270 var style = element.cssTextToScopeStyle(cssText, _STYLE_CONTROLLER_SCOPE); | |
1271 applyStyleToScope(style, scope); | |
1272 // cache that this style has been applied | |
1273 styleCacheForScope(scope).add('$_name$name'); | |
1274 } | |
1275 | |
1276 Node findStyleScope([node]) { | |
1277 // find the shadow root that contains this element | |
1278 var n = node; | |
1279 if (n == null) n = this; | |
1280 while (n.parentNode != null) { | |
1281 n = n.parentNode; | |
1282 } | |
1283 return n; | |
1284 } | |
1285 | |
1286 bool scopeHasNamedStyle(Node scope, String name) => | |
1287 styleCacheForScope(scope).contains(name); | |
1288 | |
1289 Map _polyfillScopeStyleCache = {}; | |
1290 | |
1291 Set styleCacheForScope(Node scope) { | |
1292 var styles; | |
1293 if (_hasShadowDomPolyfill) { | |
1294 var name = scope is ShadowRoot | |
1295 ? scope.host.localName | |
1296 : (scope as Element).localName; | |
1297 var styles = _polyfillScopeStyleCache[name]; | |
1298 if (styles == null) _polyfillScopeStyleCache[name] = styles = new Set(); | |
1299 } else { | |
1300 styles = _scopeStyles[scope]; | |
1301 if (styles == null) _scopeStyles[scope] = styles = new Set(); | |
1302 } | |
1303 return styles; | |
1304 } | |
1305 | |
1306 static final _scopeStyles = new Expando(); | |
1307 | |
1308 static String _shimCssText(String cssText, [Element host]) { | |
1309 var name = ''; | |
1310 var is_ = false; | |
1311 if (host != null) { | |
1312 name = host.localName; | |
1313 is_ = host.attributes.containsKey('is'); | |
1314 } | |
1315 var selector = _ShadowCss.callMethod('makeScopeSelector', [name, is_]); | |
1316 return _ShadowCss.callMethod('shimCssText', [cssText, selector]); | |
1317 } | |
1318 | |
1319 static void applyStyleToScope(StyleElement style, Node scope) { | |
1320 if (style == null) return; | |
1321 | |
1322 if (scope == document) scope = document.head; | |
1323 | |
1324 if (_hasShadowDomPolyfill) scope = document.head; | |
1325 | |
1326 // TODO(sorvell): necessary for IE | |
1327 // see https://connect.microsoft.com/IE/feedback/details/790212/ | |
1328 // cloning-a-style-element-and-adding-to-document-produces | |
1329 // -unexpected-result#details | |
1330 // var clone = style.cloneNode(true); | |
1331 var clone = new StyleElement()..text = style.text; | |
1332 | |
1333 var attr = style.attributes[_STYLE_SCOPE_ATTRIBUTE]; | |
1334 if (attr != null) { | |
1335 clone.attributes[_STYLE_SCOPE_ATTRIBUTE] = attr; | |
1336 } | |
1337 | |
1338 // TODO(sorvell): probably too brittle; try to figure out | |
1339 // where to put the element. | |
1340 var refNode = scope.firstChild; | |
1341 if (scope == document.head) { | |
1342 var selector = 'style[$_STYLE_SCOPE_ATTRIBUTE]'; | |
1343 var styleElement = document.head.querySelectorAll(selector); | |
1344 if (styleElement.isNotEmpty) { | |
1345 refNode = styleElement.last.nextElementSibling; | |
1346 } | |
1347 } | |
1348 scope.insertBefore(clone, refNode); | |
1349 } | |
1350 | |
1351 /// Invoke [callback] in [wait], unless the job is re-registered, | |
1352 /// which resets the timer. If [wait] is not supplied, this will use | |
1353 /// [window.requestAnimationFrame] instead of a [Timer]. | |
1354 /// | |
1355 /// For example: | |
1356 /// | |
1357 /// _myJob = Polymer.scheduleJob(_myJob, callback); | |
1358 /// | |
1359 /// Returns the newly created job. | |
1360 // Dart note: renamed to scheduleJob to be a bit more consistent with Dart. | |
1361 PolymerJob scheduleJob(PolymerJob job, void callback(), [Duration wait]) { | |
1362 if (job == null) job = new PolymerJob._(); | |
1363 // Dart note: made start smarter, so we don't need to call stop. | |
1364 return job..start(callback, wait); | |
1365 } | |
1366 | |
1367 // Deprecated: Please use injectBoundHtml. | |
1368 @deprecated | |
1369 DocumentFragment injectBoundHTML(String html, [Element element]) => | |
1370 injectBoundHtml(html, element: element); | |
1371 | |
1372 /// Inject HTML which contains markup bound to this element into | |
1373 /// a target element (replacing target element content). | |
1374 DocumentFragment injectBoundHtml(String html, {Element element, | |
1375 NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { | |
1376 var template = new TemplateElement() | |
1377 ..setInnerHtml(html, validator: validator, treeSanitizer: treeSanitizer); | |
1378 var fragment = this.instanceTemplate(template); | |
1379 if (element != null) { | |
1380 element.text = ''; | |
1381 element.append(fragment); | |
1382 } | |
1383 return fragment; | |
1384 } | |
1385 } | |
1386 | |
1387 // Dart note: this is related to _bindOldStylePublishedProperty. Polymer | |
1388 // addresses n-way bindings by metaprogramming: redefine the property on the | |
1389 // PolymerElement instance to always get its value from the model@path. This is | |
1390 // supported in Dart using a new style of @published property declaration using | |
1391 // the `readValue` and `writeValue` methods above. In the past we used to work | |
1392 // around this by listening to changes on both sides and updating the values. | |
1393 // This object provides the hooks to do this. | |
1394 // TODO(sigmund,jmesserly): delete after a deprecation period. | |
1395 class _PolymerBinding extends Bindable { | |
1396 final Polymer _target; | |
1397 final Symbol _property; | |
1398 final Bindable _bindable; | |
1399 StreamSubscription _sub; | |
1400 Object _lastValue; | |
1401 | |
1402 _PolymerBinding(this._target, this._property, this._bindable) { | |
1403 _sub = _target.changes.listen(_propertyValueChanged); | |
1404 _updateNode(open(_updateNode)); | |
1405 } | |
1406 | |
1407 void _updateNode(newValue) { | |
1408 _lastValue = newValue; | |
1409 smoke.write(_target, _property, newValue); | |
1410 // Note: we don't invoke emitPropertyChangeRecord here because that's | |
1411 // done by listening on changes on the PolymerElement. | |
1412 } | |
1413 | |
1414 void _propertyValueChanged(List<ChangeRecord> records) { | |
1415 for (var record in records) { | |
1416 if (record is PropertyChangeRecord && record.name == _property) { | |
1417 final newValue = smoke.read(_target, _property); | |
1418 if (!identical(_lastValue, newValue)) { | |
1419 this.value = newValue; | |
1420 } | |
1421 return; | |
1422 } | |
1423 } | |
1424 } | |
1425 | |
1426 open(callback(value)) => _bindable.open(callback); | |
1427 get value => _bindable.value; | |
1428 set value(newValue) => _bindable.value = newValue; | |
1429 | |
1430 void close() { | |
1431 if (_sub != null) { | |
1432 _sub.cancel(); | |
1433 _sub = null; | |
1434 } | |
1435 _bindable.close(); | |
1436 } | |
1437 } | |
1438 | |
1439 // Ported from an inline object in instance/properties.js#bindToAccessor. | |
1440 class _CloseOnlyBinding extends Bindable { | |
1441 final _PropertyAccessor accessor; | |
1442 | |
1443 _CloseOnlyBinding(this.accessor); | |
1444 | |
1445 open(callback) {} | |
1446 get value => null; | |
1447 set value(newValue) {} | |
1448 deliver() {} | |
1449 | |
1450 void close() { | |
1451 if (accessor.bindable == null) return; | |
1452 accessor.bindable.close(); | |
1453 accessor.bindable = null; | |
1454 } | |
1455 } | |
1456 | |
1457 bool _toBoolean(value) => null != value && false != value; | |
1458 | |
1459 final Logger _observeLog = new Logger('polymer.observe'); | |
1460 final Logger _eventsLog = new Logger('polymer.events'); | |
1461 final Logger _unbindLog = new Logger('polymer.unbind'); | |
1462 final Logger _bindLog = new Logger('polymer.bind'); | |
1463 final Logger _watchLog = new Logger('polymer.watch'); | |
1464 final Logger _readyLog = new Logger('polymer.ready'); | |
1465 | |
1466 final Expando _eventHandledTable = new Expando<Set<Node>>(); | |
OLD | NEW |