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 /** | |
8 * Use this annotation to publish a field as an attribute. For example: | |
9 * | |
10 * class MyPlaybackElement extends PolymerElement { | |
11 * // This will be available as an HTML attribute, for example: | |
12 * // <my-playback volume="11"> | |
13 * @published double volume; | |
14 * } | |
15 */ | |
16 // TODO(jmesserly): does @published imply @observable or vice versa? | |
17 const published = const PublishedProperty(); | |
18 | |
19 /** An annotation used to publish a field as an attribute. See [published]. */ | |
20 class PublishedProperty extends ObservableProperty { | |
21 const PublishedProperty(); | |
22 } | |
23 | |
24 // TODO(jmesserly): make this the mixin so we can have Polymer type extensions, | |
25 // and move the implementation of PolymerElement in here. Once done it will look | |
26 // like: | |
27 // abstract class Polymer { ... all the things ... } | |
28 // typedef PolymerElement = HtmlElement with Polymer, Observable; | |
29 abstract class Polymer { | |
30 // TODO(jmesserly): should this really be public? | |
31 /** Regular expression that matches data-bindings. */ | |
32 static final bindPattern = new RegExp(r'\{\{([^{}]*)}}'); | |
33 | |
34 /** | |
35 * Like [document.register] but for Polymer elements. | |
36 * | |
37 * Use the [name] to specify custom elment's tag name, for example: | |
38 * "fancy-button" if the tag is used as `<fancy-button>`. | |
39 * | |
40 * The [type] is the type to construct. If not supplied, it defaults to | |
41 * [PolymerElement]. | |
42 */ | |
43 // NOTE: this is called "element" in src/declaration/polymer-element.js, and | |
44 // exported as "Polymer". | |
45 static void register(String name, [Type type]) { | |
46 //console.log('registering [' + name + ']'); | |
47 if (type == null) type = PolymerElement; | |
48 _registerClassMirror(name, reflectClass(type)); | |
49 } | |
50 | |
51 // TODO(jmesserly): we use ClassMirror internall for now, until it is possible | |
52 // to get from ClassMirror -> Type. | |
53 static void _registerClassMirror(String name, ClassMirror type) { | |
54 _typesByName[name] = type; | |
55 // notify the registrar waiting for 'name', if any | |
56 _notifyType(name); | |
57 } | |
58 } | |
59 | |
60 /** | |
61 * The base class for Polymer elements. It provides convience features on top | |
62 * of the custom elements web standard. | |
63 */ | |
64 class PolymerElement extends CustomElement with ObservableMixin { | |
65 // Fully ported from revision: | |
66 // https://github.com/Polymer/polymer/blob/4dc481c11505991a7c43228d3797d28f212 67779 | |
67 // | |
68 // src/instance/attributes.js | |
69 // src/instance/base.js | |
70 // src/instance/events.js | |
71 // src/instance/mdv.js | |
72 // src/instance/properties.js | |
73 // src/instance/utils.js | |
74 // | |
75 // Not yet ported: | |
76 // src/instance/style.js -- blocked on ShadowCSS.shimPolyfillDirectives | |
77 | |
78 /// The one syntax to rule them all. | |
79 static final BindingDelegate _polymerSyntax = new PolymerExpressions(); | |
80 | |
81 static int _preparingElements = 0; | |
82 | |
83 PolymerDeclaration _declaration; | |
84 | |
85 /** The most derived `<polymer-element>` declaration for this element. */ | |
86 PolymerDeclaration get declaration => _declaration; | |
87 | |
88 Map<String, StreamSubscription> _elementObservers; | |
89 bool _unbound; // lazy-initialized | |
90 Job _unbindAllJob; | |
91 | |
92 bool get _elementPrepared => _declaration != null; | |
93 | |
94 bool get applyAuthorStyles => false; | |
95 bool get resetStyleInheritance => false; | |
96 bool get alwaysPrepare => false; | |
97 | |
98 /** | |
99 * Shadow roots created by [parseElement]. See [getShadowRoot]. | |
100 */ | |
101 final _shadowRoots = new HashMap<String, ShadowRoot>(); | |
102 | |
103 /** Map of items in the shadow root(s) by their [Element.id]. */ | |
104 // TODO(jmesserly): various issues: | |
105 // * wrap in UnmodifiableMapView? | |
106 // * should we have an object that implements noSuchMethod? | |
107 // * should the map have a key order (e.g. LinkedHash or SplayTree)? | |
108 // * should this be a live list? Polymer doesn't, maybe due to JS limitations? | |
109 // For now I picked the most performant choice: non-live HashMap. | |
110 final Map<String, Element> $ = new HashMap<String, Element>(); | |
111 | |
112 /** | |
113 * Gets the shadow root associated with the corresponding custom element. | |
114 * | |
115 * This is identical to [shadowRoot], unless there are multiple levels of | |
116 * inheritance and they each have their own shadow root. For example, | |
117 * this can happen if the base class and subclass both have `<template>` tags | |
118 * in their `<polymer-element>` tags. | |
119 */ | |
120 // TODO(jmesserly): Polymer does not have this feature. Reconcile. | |
121 ShadowRoot getShadowRoot(String customTagName) => _shadowRoots[customTagName]; | |
122 | |
123 ShadowRoot createShadowRoot([name]) { | |
124 if (name != null) { | |
125 throw new ArgumentError('name argument must not be supplied.'); | |
126 } | |
127 | |
128 // Provides ability to traverse from ShadowRoot to the host. | |
129 // TODO(jmessery): remove once we have this ability on the DOM. | |
130 final root = super.createShadowRoot(); | |
131 _shadowHost[root] = host; | |
132 return root; | |
133 } | |
134 | |
135 /** | |
136 * Invoke [callback] in [wait], unless the job is re-registered, | |
137 * which resets the timer. For example: | |
138 * | |
139 * _myJob = job(_myJob, callback, const Duration(milliseconds: 100)); | |
140 * | |
141 * Returns a job handle which can be used to re-register a job. | |
142 */ | |
143 Job job(Job job, void callback(), Duration wait) => | |
blois
2013/09/27 21:40:52
Why is this needed?
Jennifer Messerly
2013/09/30 17:44:37
agreed, this is a good question for Polymer.js guy
| |
144 runJob(job, callback, wait); | |
145 | |
146 // TODO(jmesserly): I am not sure if we should have the | |
147 // created/createdCallback distinction. See post here: | |
148 // https://groups.google.com/d/msg/polymer-dev/W0ZUpU5caIM/v5itFnvnehEJ | |
149 // Same issue with inserted and removed. | |
150 void created() { | |
151 if (document.window != null || alwaysPrepare || _preparingElements > 0) { | |
blois
2013/09/27 21:40:52
Any idea what the scenarios are where these are cr
Jennifer Messerly
2013/09/30 17:44:37
yeah, it's to workaround <template> upgrading elem
| |
152 prepareElement(); | |
153 } | |
154 } | |
155 | |
156 void prepareElement() { | |
157 // Dart note: get the _declaration, which also marks _elementPrepared | |
158 _declaration = _getDeclaration(reflect(this).type); | |
159 // do this first so we can observe changes during initialization | |
160 observeProperties(); | |
161 // install boilerplate attributes | |
162 copyInstanceAttributes(); | |
163 // process input attributes | |
164 takeAttributes(); | |
165 // add event listeners | |
166 addHostListeners(); | |
167 // guarantees that while preparing, any sub-elements will also be prepared | |
168 _preparingElements++; | |
169 // process declarative resources | |
170 parseDeclarations(_declaration); | |
171 _preparingElements--; | |
172 // user entry point | |
173 ready(); | |
174 } | |
175 | |
176 /** Called when [prepareElement] is finished. */ | |
177 void ready() {} | |
178 | |
179 void inserted() { | |
180 if (!_elementPrepared) { | |
181 prepareElement(); | |
182 } | |
183 cancelUnbindAll(preventCascade: true); | |
184 } | |
185 | |
186 void removed() { | |
187 asyncUnbindAll(); | |
188 } | |
189 | |
190 /** Recursive ancestral <element> initialization, oldest first. */ | |
191 void parseDeclarations(PolymerDeclaration declaration) { | |
192 if (declaration != null) { | |
193 parseDeclarations(declaration.superDeclaration); | |
194 parseDeclaration(declaration.host); | |
195 } | |
196 } | |
197 | |
198 /** | |
199 * Parse input `<polymer-element>` as needed, override for custom behavior. | |
200 */ | |
201 void parseDeclaration(Element elementElement) { | |
202 var root = shadowFromTemplate(fetchTemplate(elementElement)); | |
203 | |
204 // Dart note: this is extra code compared to Polymer to support | |
205 // the getShadowRoot method. | |
206 if (root == null) return; | |
207 | |
208 var name = elementElement.attributes['name']; | |
209 if (name == null) return; | |
210 _shadowRoots[name] = root; | |
211 } | |
212 | |
213 /** | |
214 * Return a shadow-root template (if desired), override for custom behavior. | |
215 */ | |
216 Element fetchTemplate(Element elementElement) => | |
217 elementElement.query('template'); | |
218 | |
219 /** Utility function that creates a shadow root from a `<template>`. */ | |
220 ShadowRoot shadowFromTemplate(Element template) { | |
221 if (template == null) return null; | |
222 // cache elder shadow root (if any) | |
223 var elderRoot = this.shadowRoot; | |
224 // make a shadow root | |
225 var root = createShadowRoot(); | |
226 // migrate flag(s)( | |
227 root.applyAuthorStyles = applyAuthorStyles; | |
228 root.resetStyleInheritance = resetStyleInheritance; | |
229 // stamp template | |
230 // which includes parsing and applying MDV bindings before being | |
231 // inserted (to avoid {{}} in attribute values) | |
232 // e.g. to prevent <img src="images/{{icon}}"> from generating a 404. | |
233 var dom = instanceTemplate(template); | |
234 // append to shadow dom | |
235 root.append(dom); | |
236 // perform post-construction initialization tasks on shadow root | |
237 shadowRootReady(root, template); | |
238 // return the created shadow root | |
239 return root; | |
240 } | |
241 | |
242 void shadowRootReady(ShadowRoot root, Element template) { | |
243 // locate nodes with id and store references to them in this.$ hash | |
244 marshalNodeReferences(root); | |
245 // add local events of interest... | |
246 addInstanceListeners(root, template); | |
247 // TODO(jmesserly): port this | |
248 // set up pointer gestures | |
249 // PointerGestures.register(root); | |
250 } | |
251 | |
252 /** Locate nodes with id and store references to them in [$] hash. */ | |
253 void marshalNodeReferences(ShadowRoot root) { | |
254 if (root == null) return; | |
255 for (var n in root.queryAll('[id]')) { | |
256 $[n.id] = n; | |
257 } | |
258 } | |
259 | |
260 void attributeChanged(String name, String oldValue) { | |
blois
2013/09/27 21:40:52
FYI- the spec has this as attributeChanged(name, o
Jennifer Messerly
2013/09/30 17:44:37
SGTM. I guess Polymer.js will be dealing with this
| |
261 if (name != 'class' && name != 'style') { | |
262 attributeToProperty(name, attributes[name]); | |
263 } | |
264 } | |
265 | |
266 // TODO(jmesserly): use stream or future here? | |
267 void onMutation(Node node, void listener(MutationObserver obs)) { | |
268 new MutationObserver((records, MutationObserver observer) { | |
269 listener(observer); | |
270 observer.disconnect(); | |
271 })..observe(node, childList: true, subtree: true); | |
272 } | |
273 | |
274 void copyInstanceAttributes() { | |
275 _declaration._instanceAttributes.forEach((name, value) { | |
276 attributes[name] = value; | |
277 }); | |
278 } | |
279 | |
280 void takeAttributes() { | |
281 if (_declaration._publishLC == null) return; | |
282 attributes.forEach(attributeToProperty); | |
283 } | |
284 | |
285 /** | |
286 * If attribute [name] is mapped to a property, deserialize | |
287 * [value] into that property. | |
288 */ | |
289 void attributeToProperty(String name, String value) { | |
290 // try to match this attribute to a property (attributes are | |
291 // all lower-case, so this is case-insensitive search) | |
292 var property = propertyForAttribute(name); | |
293 if (property == null) return; | |
294 | |
295 // filter out 'mustached' values, these are to be | |
296 // replaced with bound-data and are not yet values | |
297 // themselves. | |
298 if (value == null || value.contains(Polymer.bindPattern)) return; | |
299 | |
300 // get original value | |
301 final self = reflect(this); | |
302 final defaultValue = self.getField(property.simpleName).reflectee; | |
303 | |
304 // deserialize Boolean or Number values from attribute | |
305 final newValue = deserializeValue(value, defaultValue, | |
306 _inferPropertyType(defaultValue, property)); | |
307 | |
308 // only act if the value has changed | |
309 if (!identical(newValue, defaultValue)) { | |
310 // install new value (has side-effects) | |
311 self.setField(property.simpleName, newValue); | |
312 } | |
313 } | |
314 | |
315 /** Return the published property matching name, or null. */ | |
316 // TODO(jmesserly): should we just return Symbol here? | |
317 DeclarationMirror propertyForAttribute(String name) { | |
318 final publishLC = _declaration._publishLC; | |
319 if (publishLC == null) return null; | |
320 //console.log('propertyForAttribute:', name, 'matches', match); | |
321 return publishLC[name]; | |
322 } | |
323 | |
324 /** | |
325 * Convert representation of [value] based on [type] and [defaultValue]. | |
326 */ | |
327 // TODO(jmesserly): this should probably take a ClassMirror instead of | |
328 // TypeMirror, but it is currently impossible to get from a TypeMirror to a | |
329 // ClassMirror. | |
330 Object deserializeValue(String value, Object defaultValue, TypeMirror type) => | |
331 deserialize.deserializeValue(value, defaultValue, type); | |
332 | |
333 String serializeValue(Object value, TypeMirror inferredType) { | |
334 if (value == null) return null; | |
335 | |
336 final type = inferredType.qualifiedName; | |
337 if (type == const Symbol('dart.core.bool')) { | |
338 return _toBoolean(value) ? '' : null; | |
339 } else if (type == const Symbol('dart.core.String') | |
340 || type == const Symbol('dart.core.int') | |
341 || type == const Symbol('dart.core.double')) { | |
342 return '$value'; | |
343 } | |
344 return null; | |
345 } | |
346 | |
347 void reflectPropertyToAttribute(String name) { | |
348 // TODO(sjmiles): consider memoizing this | |
349 final self = reflect(this); | |
350 // try to intelligently serialize property value | |
351 // TODO(jmesserly): cache symbol? | |
352 final propValue = self.getField(new Symbol(name)).reflectee; | |
353 final property = _declaration._publish[name]; | |
354 var inferredType = _inferPropertyType(propValue, property); | |
355 final serializedValue = serializeValue(propValue, inferredType); | |
356 // boolean properties must reflect as boolean attributes | |
357 if (serializedValue != null) { | |
358 attributes[name] = serializedValue; | |
359 // TODO(sorvell): we should remove attr for all properties | |
360 // that have undefined serialization; however, we will need to | |
361 // refine the attr reflection system to achieve this; pica, for example, | |
362 // relies on having inferredType object properties not removed as | |
363 // attrs. | |
364 } else if (inferredType.qualifiedName == const Symbol('dart.core.bool')) { | |
365 attributes.remove(name); | |
366 } | |
367 } | |
368 | |
369 /** | |
370 * Creates the document fragment to use for each instance of the custom | |
371 * element, given the `<template>` node. By default this is equivalent to: | |
372 * | |
373 * template.createInstance(this, polymerSyntax); | |
374 * | |
375 * Where polymerSyntax is a singleton `PolymerExpressions` instance from the | |
376 * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions ) | |
377 * package. | |
378 * | |
379 * You can override this method to change the instantiation behavior of the | |
380 * template, for example to use a different data-binding syntax. | |
381 */ | |
382 DocumentFragment instanceTemplate(Element template) => | |
383 template.createInstance(this, _polymerSyntax); | |
384 | |
385 NodeBinding bind(String name, model, String path) { | |
386 // note: binding is a prepare signal. This allows us to be sure that any | |
387 // property changes that occur as a result of binding will be observed. | |
388 if (!_elementPrepared) prepareElement(); | |
389 | |
390 var property = propertyForAttribute(name); | |
391 if (property != null) { | |
392 unbind(name); | |
393 // use n-way Polymer binding | |
394 var observer = bindProperty(property.simpleName, model, path); | |
395 // reflect bound property to attribute when binding | |
396 // to ensure binding is not left on attribute if property | |
397 // does not update due to not changing. | |
398 reflectPropertyToAttribute(name); | |
399 return bindings[name] = observer; | |
400 } else { | |
401 return super.bind(name, model, path); | |
402 } | |
403 } | |
404 | |
405 void asyncUnbindAll() { | |
406 if (_unbound == true) return; | |
407 _unbindLog.info('[$localName] asyncUnbindAll'); | |
408 _unbindAllJob = job(_unbindAllJob, unbindAll, const Duration(seconds: 0)); | |
409 } | |
410 | |
411 void unbindAll() { | |
412 if (_unbound == true) return; | |
413 | |
414 unbindAllProperties(); | |
415 super.unbindAll(); | |
416 _unbindNodeTree(shadowRoot); | |
417 // TODO(sjmiles): must also unbind inherited shadow roots | |
418 _unbound = true; | |
419 } | |
420 | |
421 void cancelUnbindAll({bool preventCascade}) { | |
422 if (_unbound == true) { | |
423 _unbindLog.warning( | |
424 '[$localName] already unbound, cannot cancel unbindAll'); | |
425 return; | |
426 } | |
427 _unbindLog.info('[$localName] cancelUnbindAll'); | |
428 if (_unbindAllJob != null) { | |
429 _unbindAllJob.stop(); | |
430 _unbindAllJob = null; | |
431 } | |
432 | |
433 // cancel unbinding our shadow tree iff we're not in the process of | |
434 // cascading our tree (as we do, for example, when the element is inserted). | |
435 if (preventCascade == true) return; | |
436 _forNodeTree(shadowRoot, (n) { | |
437 if (n is PolymerElement) { | |
438 (n as PolymerElement).cancelUnbindAll(); | |
439 } | |
440 }); | |
441 } | |
442 | |
443 static void _unbindNodeTree(Node node) { | |
444 _forNodeTree(node, (node) => node.unbindAll()); | |
445 } | |
446 | |
447 static void _forNodeTree(Node node, void callback(Node node)) { | |
448 if (node == null) return; | |
449 | |
450 callback(node); | |
451 for (var child = node.firstChild; child != null; child = child.nextNode) { | |
452 _forNodeTree(child, callback); | |
453 } | |
454 } | |
455 | |
456 /** Set up property observers. */ | |
457 void observeProperties() { | |
458 // TODO(sjmiles): | |
459 // we observe published properties so we can reflect them to attributes | |
460 // ~100% of our team's applications would work without this reflection, | |
461 // perhaps we can make it optional somehow | |
462 // | |
463 // add user's observers | |
464 final observe = _declaration._observe; | |
465 final publish = _declaration._publish; | |
466 if (observe != null) { | |
467 observe.forEach((name, value) { | |
468 if (publish != null && publish.containsKey(name)) { | |
469 observeBoth(name, value); | |
470 } else { | |
471 observeProperty(name, value); | |
472 } | |
473 }); | |
474 } | |
475 // add observers for published properties | |
476 if (publish != null) { | |
477 publish.forEach((name, value) { | |
478 if (observe == null || !observe.containsKey(name)) { | |
479 observeAttributeProperty(name); | |
480 } | |
481 }); | |
482 } | |
483 } | |
484 | |
485 void _observe(String name, void callback(newValue, oldValue)) { | |
486 _observeLog.info('[$localName] watching [$name]'); | |
487 // TODO(jmesserly): this is a little different than the JS version so we | |
488 // can pass the oldValue, which is missing from Dart's PathObserver. | |
489 // This probably gives us worse performance. | |
490 var path = new PathObserver(this, name); | |
491 Object oldValue = null; | |
492 _registerObserver(name, path.changes.listen((_) { | |
493 final newValue = path.value; | |
494 final old = oldValue; | |
495 oldValue = newValue; | |
496 callback(newValue, old); | |
497 })); | |
498 } | |
499 | |
500 void _registerObserver(String name, StreamSubscription sub) { | |
501 if (_elementObservers == null) { | |
502 _elementObservers = new Map<String, StreamSubscription>(); | |
503 } | |
504 _elementObservers[name] = sub; | |
505 } | |
506 | |
507 void observeAttributeProperty(String name) { | |
508 _observe(name, (value, old) => reflectPropertyToAttribute(name)); | |
509 } | |
510 | |
511 void observeProperty(String name, Symbol method) { | |
512 final self = reflect(this); | |
513 _observe(name, (value, old) => self.invoke(method, [old])); | |
514 } | |
515 | |
516 void observeBoth(String name, Symbol methodName) { | |
517 final self = reflect(this); | |
518 _observe(name, (value, old) { | |
519 reflectPropertyToAttribute(name); | |
520 self.invoke(methodName, [old]); | |
521 }); | |
522 } | |
523 | |
524 void unbindProperty(String name) { | |
525 if (_elementObservers == null) return; | |
526 var sub = _elementObservers.remove(name); | |
527 if (sub != null) sub.cancel(); | |
528 } | |
529 | |
530 void unbindAllProperties() { | |
531 if (_elementObservers == null) return; | |
532 for (var sub in _elementObservers.values) sub.cancel(); | |
533 _elementObservers.clear(); | |
534 } | |
535 | |
536 /** | |
537 * Bind a [property] in this object to a [path] in model. *Note* in Dart it | |
538 * is necessary to also define the field: | |
539 * | |
540 * var myProperty; | |
541 * | |
542 * created() { | |
543 * super.created(); | |
544 * bindProperty(#myProperty, this, 'myModel.path.to.otherProp'); | |
545 * } | |
546 */ | |
547 // TODO(jmesserly): replace with something more localized, like: | |
548 // @ComputedField('myModel.path.to.otherProp'); | |
549 NodeBinding bindProperty(Symbol name, Object model, String path) => | |
550 // apply Polymer two-way reference binding | |
551 _bindProperties(this, name, model, path); | |
552 | |
553 /** | |
554 * bind a property in A to a path in B by converting A[property] to a | |
555 * getter/setter pair that accesses B[...path...] | |
556 */ | |
557 static NodeBinding _bindProperties(PolymerElement inA, Symbol inProperty, | |
558 Object inB, String inPath) { | |
559 | |
560 if (_bindLog.isLoggable(Level.INFO)) { | |
561 _bindLog.info('[$inB]: bindProperties: [$inPath] to ' | |
562 '[${inA.localName}].[$inProperty]'); | |
563 } | |
564 | |
565 // Dart note: normally we only reach this code when we know it's a | |
566 // property, but if someone uses bindProperty directly they might get a | |
567 // NoSuchMethodError either from the getField below, or from the setField | |
568 // inside PolymerBinding. That doesn't seem unreasonable, but it's a slight | |
569 // difference from Polymer.js behavior. | |
570 | |
571 // capture A's value if B's value is null or undefined, | |
572 // otherwise use B's value | |
573 var path = new PathObserver(inB, inPath); | |
574 if (path.value == null) { | |
575 path.value = reflect(inA).getField(inProperty).reflectee; | |
576 } | |
577 return new _PolymerBinding(inA, inProperty, inB, inPath); | |
578 } | |
579 | |
580 /** Attach event listeners on the host (this) element. */ | |
581 void addHostListeners() { | |
582 var events = _declaration._eventDelegates; | |
583 if (events.isEmpty) return; | |
584 | |
585 if (_eventsLog.isLoggable(Level.INFO)) { | |
586 _eventsLog.info('[$localName] addHostListeners: $events'); | |
587 } | |
588 addNodeListeners(this, events.keys, hostEventListener); | |
589 } | |
590 | |
591 /** Attach event listeners inside a shadow [root]. */ | |
592 void addInstanceListeners(ShadowRoot root, Element template) { | |
593 var templateDelegates = _declaration._templateDelegates; | |
594 if (templateDelegates == null) return; | |
595 var events = templateDelegates[template]; | |
596 if (events == null) return; | |
597 | |
598 if (_eventsLog.isLoggable(Level.INFO)) { | |
599 _eventsLog.info('[$localName] addInstanceListeners: $events'); | |
600 } | |
601 addNodeListeners(root, events, instanceEventListener); | |
602 } | |
603 | |
604 void addNodeListeners(Node node, Iterable<String> events, | |
605 void listener(Event e)) { | |
606 | |
607 for (var name in events) { | |
608 addNodeListener(node, name, listener); | |
609 } | |
610 } | |
611 | |
612 void addNodeListener(Node node, String event, void listener(Event e)) { | |
613 new EventStreamProvider(event).forTarget(node).listen(listener); | |
blois
2013/09/27 21:40:52
how about just node.on[event]
Jennifer Messerly
2013/09/30 17:44:37
Doh, I thought it was deprecated; if it's un-depre
| |
614 } | |
615 | |
616 void hostEventListener(Event event) { | |
617 // TODO(jmesserly): do we need this check? It was using cancelBubble, see: | |
618 // https://github.com/Polymer/polymer/issues/292 | |
619 if (!event.bubbles) return; | |
620 | |
621 bool log = _eventsLog.isLoggable(Level.INFO); | |
622 if (log) { | |
623 _eventsLog.info('>>> [$localName]: hostEventListener(${event.type})'); | |
624 } | |
625 | |
626 var h = findEventDelegate(event); | |
627 if (h) { | |
628 if (log) _eventsLog.info('[$localName] found host handler name [$h]'); | |
629 var detail = event is CustomEvent ? | |
630 (event as CustomEvent).detail : null; | |
631 // TODO(jmesserly): cache the symbols? | |
632 dispatchMethod(new Symbol(h), [event, detail, this]); | |
633 } | |
634 | |
635 if (log) { | |
636 _eventsLog.info('<<< [$localName]: hostEventListener(${event.type})'); | |
637 } | |
638 } | |
639 | |
640 String findEventDelegate(Event event) => | |
641 _declaration._eventDelegates[_eventNameFromType(event.type)]; | |
642 | |
643 /** Call [methodName] method on [this] with [args], if the method exists. */ | |
644 // TODO(jmesserly): I removed the [node] argument as it was unused. Reconcile. | |
645 void dispatchMethod(Symbol methodName, List args) { | |
646 bool log = _eventsLog.isLoggable(Level.INFO); | |
647 if (log) _eventsLog.info('>>> [$localName]: dispatch $methodName'); | |
648 | |
649 // TODO(sigmund): consider making event listeners list all arguments | |
650 // explicitly. Unless VM mirrors are optimized first, this reflectClass call | |
651 // will be expensive once custom elements extend directly from Element (see | |
652 // dartbug.com/11108). | |
653 var self = reflect(this); | |
654 var method = self.type.methods[methodName]; | |
655 if (method != null) { | |
656 // This will either truncate the argument list or extend it with extra | |
657 // null arguments, so it will match the signature. | |
658 // TODO(sigmund): consider accepting optional arguments when we can tell | |
659 // them appart from named arguments (see http://dartbug.com/11334) | |
660 args.length = method.parameters.where((p) => !p.isOptional).length; | |
661 } | |
662 self.invoke(methodName, args); | |
663 | |
664 if (log) _eventsLog.info('<<< [$localName]: dispatch $methodName'); | |
665 | |
666 // TODO(jmesserly): workaround for HTML events not supporting zones. | |
667 performMicrotaskCheckpoint(); | |
668 } | |
669 | |
670 void instanceEventListener(Event event) { | |
671 _listenLocal(host, event); | |
672 } | |
673 | |
674 // TODO(sjmiles): much of the below privatized only because of the vague | |
675 // notion this code is too fiddly and we need to revisit the core feature | |
676 void _listenLocal(Element host, Event event) { | |
677 // TODO(jmesserly): do we need this check? It was using cancelBubble, see: | |
678 // https://github.com/Polymer/polymer/issues/292 | |
679 if (!event.bubbles) return; | |
680 | |
681 bool log = _eventsLog.isLoggable(Level.INFO); | |
682 if (log) _eventsLog.info('>>> [$localName]: listenLocal [${event.type}]'); | |
683 | |
684 final eventOn = '$_EVENT_PREFIX${_eventNameFromType(event.type)}'; | |
685 if (event.path == null) { | |
686 _listenLocalNoEventPath(host, event, eventOn); | |
687 } else { | |
688 _listenLocalEventPath(host, event, eventOn); | |
689 } | |
690 | |
691 if (log) _eventsLog.info('<<< [$localName]: listenLocal [${event.type}]'); | |
692 } | |
693 | |
694 static void _listenLocalEventPath(Element host, Event event, String eventOn) { | |
695 var c = null; | |
696 for (var target in event.path) { | |
697 // if we hit host, stop | |
698 if (identical(target, host)) return; | |
699 | |
700 // find a controller for the target, unless we already found `host` | |
701 // as a controller | |
702 c = identical(c, host) ? c : _findController(target); | |
703 | |
704 // if we have a controller, dispatch the event, and stop if the handler | |
705 // returns true | |
706 if (c != null && _handleEvent(c, target, event, eventOn)) { | |
707 return; | |
708 } | |
709 } | |
710 } | |
711 | |
712 // TODO(sorvell): remove when ShadowDOM polyfill supports event path. | |
713 // Note that _findController will not return the expected controller when the | |
714 // event target is a distributed node. This is because we cannot traverse | |
715 // from a composed node to a node in shadowRoot. | |
716 // This will be addressed via an event path api | |
717 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066 | |
718 static void _listenLocalNoEventPath(Element host, Event event, | |
719 String eventOn) { | |
720 | |
721 if (_eventsLog.isLoggable(Level.INFO)) { | |
722 _eventsLog.info('event.path() not supported for ${event.type}'); | |
723 } | |
724 | |
725 var target = event.target; | |
726 var c = null; | |
727 // if we hit dirt or host, stop | |
728 while (target != null && target != host) { | |
729 // find a controller for target `t`, unless we already found `host` | |
730 // as a controller | |
731 c = identical(c, host) ? c : _findController(target); | |
732 | |
733 // if we have a controller, dispatch the event, return 'true' if | |
734 // handler returns true | |
735 if (c != null && _handleEvent(c, target, event, eventOn)) { | |
736 return; | |
737 } | |
738 target = target.parent; | |
739 } | |
740 } | |
741 | |
742 // TODO(jmesserly): this won't find the correct host unless the ShadowRoot | |
743 // was created on a PolymerElement. | |
744 static Element _findController(Node node) { | |
745 while (node.parentNode != null) { | |
746 node = node.parentNode; | |
747 } | |
748 return _shadowHost[node]; | |
749 } | |
750 | |
751 static bool _handleEvent(Element ctrlr, Node node, Event event, | |
752 String eventOn) { | |
753 | |
754 // Note: local events are listened only in the shadow root. This dynamic | |
755 // lookup is used to distinguish determine whether the target actually has a | |
756 // listener, and if so, to determine lazily what's the target method. | |
757 var name = node is Element ? (node as Element).attributes[eventOn] : null; | |
758 if (name != null && _handleIfNotHandled(node, event)) { | |
759 if (_eventsLog.isLoggable(Level.INFO)) { | |
760 _eventsLog.info('[${ctrlr.localName}] found handler name [$name]'); | |
761 } | |
762 var detail = event is CustomEvent ? | |
763 (event as CustomEvent).detail : null; | |
764 | |
765 if (node != null) { | |
766 // TODO(jmesserly): cache symbols? | |
767 ctrlr.xtag.dispatchMethod(new Symbol(name), [event, detail, node]); | |
768 } | |
769 } | |
770 return event.cancelBubble; | |
771 } | |
772 | |
773 // TODO(jmesserly): I don't understand this bit. It seems to be a duplicate | |
774 // delivery prevention mechanism? | |
775 static bool _handleIfNotHandled(Node node, Event event) { | |
776 var list = _eventHandledTable[event]; | |
777 if (list == null) _eventHandledTable[event] = list = new Set<Node>(); | |
778 if (!list.contains(node)) { | |
779 list.add(node); | |
780 return true; | |
781 } | |
782 return false; | |
783 } | |
784 | |
785 /** | |
786 * Invokes a function asynchronously. | |
787 * This will call `Platform.flush()` and then return a `new Timer` | |
788 * with the provided [method] and [timeout]. | |
789 * | |
790 * If you would prefer to run the callback using | |
791 * [window.requestAnimationFrame], see the [async] method. | |
792 */ | |
793 // Dart note: "async" is split into 2 methods so it can have a sensible type | |
794 // signatures. Also removed the various features that don't make sense in a | |
795 // Dart world, like binding to "this" and taking arguments list. | |
796 Timer asyncTimer(void method(), Duration timeout) { | |
797 // when polyfilling Object.observe, ensure changes | |
798 // propagate before executing the async method | |
799 platform.flush(); | |
800 return new Timer(timeout, method); | |
blois
2013/09/27 21:40:52
Why is the flush performed during invoke and not a
Jennifer Messerly
2013/09/30 17:44:37
Agree, added to #13666
| |
801 } | |
802 | |
803 /** | |
804 * Invokes a function asynchronously. | |
805 * This will call `Platform.flush()` and then call | |
806 * [window.requestAnimationFrame] with the provided [method] and return the | |
807 * result. | |
808 * | |
809 * If you would prefer to run the callback after a given duration, see | |
810 * the [asyncTimer] method. | |
811 */ | |
812 int async(RequestAnimationFrameCallback method) { | |
813 // when polyfilling Object.observe, ensure changes | |
814 // propagate before executing the async method | |
815 platform.flush(); | |
816 return window.requestAnimationFrame(method); | |
817 } | |
818 | |
819 /** | |
820 * Fire a [CustomEvent] targeting [toNode], or this if toNode is not | |
821 * supplied. Returns the [detail] object. | |
822 */ | |
823 Object fire(String type, {Object detail, Node toNode, bool canBubble}) { | |
824 var node = toNode != null ? toNode : this; | |
825 //log.events && console.log('[%s]: sending [%s]', node.localName, inType); | |
826 node.dispatchEvent(new CustomEvent( | |
827 type, | |
828 canBubble: canBubble != null ? canBubble : true, | |
829 detail: detail | |
830 )); | |
831 return detail; | |
832 } | |
833 | |
834 /** | |
835 * Fire an event asynchronously. See [async] and [fire]. | |
836 */ | |
837 asyncFire(String type, {Object detail, Node toNode, bool canBubble}) { | |
838 // TODO(jmesserly): I'm not sure this method adds much in Dart, it's easy to | |
839 // add "() =>" | |
840 async((x) => fire( | |
841 type, detail: detail, toNode: toNode, canBubble: canBubble)); | |
842 } | |
843 | |
844 /** | |
845 * Remove [className] from [old], add class to [anew], if they exist. | |
846 */ | |
847 void classFollows(Element anew, Element old, String className) { | |
848 if (old != null) { | |
849 old.classes.remove(className); | |
850 } | |
851 if (anew != null) { | |
852 anew.classes.add(className); | |
853 } | |
854 } | |
855 } | |
856 | |
857 // Dart note: Polymer addresses n-way bindings by metaprogramming: redefine | |
858 // the property on the PolymerElement instance to always get its value from the | |
859 // model@path. We can't replicate this in Dart so we do the next best thing: | |
860 // listen to changes on both sides and update the values. | |
861 // TODO(jmesserly): our approach leads to race conditions in the bindings. | |
862 // See http://code.google.com/p/dart/issues/detail?id=13567 | |
863 class _PolymerBinding extends NodeBinding { | |
864 final InstanceMirror _target; | |
865 final Symbol _property; | |
866 StreamSubscription _sub; | |
867 Object _lastValue; | |
868 | |
869 _PolymerBinding(PolymerElement node, Symbol property, model, path) | |
870 : _target = reflect(node), | |
871 _property = property, | |
872 super(node, MirrorSystem.getName(property), model, path) { | |
873 | |
874 _sub = node.changes.listen(_propertyValueChanged); | |
875 } | |
876 | |
877 void close() { | |
878 if (closed) return; | |
879 _sub.cancel(); | |
880 super.close(); | |
881 } | |
882 | |
883 void boundValueChanged(newValue) { | |
884 _lastValue = newValue; | |
885 _target.setField(_property, newValue); | |
886 } | |
887 | |
888 void _propertyValueChanged(List<ChangeRecord> records) { | |
889 for (var record in records) { | |
890 if (record.changes(_property)) { | |
891 final newValue = _target.getField(_property).reflectee; | |
892 if (!identical(_lastValue, newValue)) { | |
893 value = newValue; | |
894 } | |
895 return; | |
896 } | |
897 } | |
898 } | |
899 } | |
900 | |
901 bool _toBoolean(value) => null != value && false != value; | |
902 | |
903 TypeMirror _propertyType(DeclarationMirror property) => | |
904 property is VariableMirror | |
905 ? (property as VariableMirror).type | |
906 : (property as MethodMirror).returnType; | |
907 | |
908 TypeMirror _inferPropertyType(Object value, DeclarationMirror property) { | |
909 var type = _propertyType(property); | |
910 if (type.qualifiedName == const Symbol('dart.core.Object') || | |
911 type.qualifiedName == const Symbol('dynamic')) { | |
912 // Attempt to infer field type from the default value. | |
913 if (value != null) { | |
914 type = reflect(value).type; | |
915 } | |
916 } | |
917 return type; | |
918 } | |
919 | |
920 final Logger _observeLog = new Logger('polymer.observe'); | |
921 final Logger _eventsLog = new Logger('polymer.events'); | |
922 final Logger _unbindLog = new Logger('polymer.unbind'); | |
923 final Logger _bindLog = new Logger('polymer.bind'); | |
924 | |
925 final Expando _shadowHost = new Expando<Element>(); | |
926 | |
927 final Expando _eventHandledTable = new Expando<Set<Node>>(); | |
OLD | NEW |