Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(947)

Side by Side Diff: pkg/polymer/lib/polymer_element.dart

Issue 24149003: Port of github.com/polymer/polymer. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: rebase Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/polymer/lib/polymer.dart ('k') | pkg/polymer/lib/src/build/linter.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 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 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library polymer.polymer_element; 5 library polymer.polymer_element;
6 6
7 import 'dart:async'; 7 export 'polymer.dart' show PolymerElement, registerPolymerElement;
8 import 'dart:html';
9 import 'dart:mirrors';
10 import 'dart:js' as js;
11
12 import 'package:custom_element/custom_element.dart';
13 import 'package:mdv/mdv.dart' show NodeBinding;
14 import 'package:observe/observe.dart';
15 import 'package:observe/src/microtask.dart';
16 import 'package:polymer_expressions/polymer_expressions.dart';
17
18 import 'src/utils.dart' show toCamelCase, toHyphenedName;
19
20 /**
21 * Registers a [PolymerElement]. This is similar to [registerCustomElement]
22 * but it is designed to work with the `<element>` element and adds additional
23 * features.
24 */
25 void registerPolymerElement(String localName, PolymerElement create()) {
26 registerCustomElement(localName, () => create().._initialize(localName));
27 }
28
29 /**
30 * *Warning*: many features of this class are not fully implemented.
31 *
32 * The base class for Polymer elements. It provides convience features on top
33 * of the custom elements web standard.
34 *
35 * Currently it supports publishing attributes via:
36 *
37 * <element name="..." attributes="foo, bar, baz">
38 *
39 * Any attribute published this way can be used in a data binding expression,
40 * and it should contain a corresponding DOM field.
41 *
42 * *Warning*: due to dart2js mirror limititations, the mapping from HTML
43 * attribute to element property is a conversion from `dash-separated-words`
44 * to camelCase, rather than searching for a property with the same name.
45 */
46 // TODO(jmesserly): fix the dash-separated-words issue. Polymer uses lowercase.
47 class PolymerElement extends CustomElement with _EventsMixin {
48 // This is a partial port of:
49 // https://github.com/Polymer/polymer/blob/stable/src/attrs.js
50 // https://github.com/Polymer/polymer/blob/stable/src/bindProperties.js
51 // https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js
52 // https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js
53 // TODO(jmesserly): we still need to port more of the functionality
54
55 /// The one syntax to rule them all.
56 static BindingDelegate _polymerSyntax = new PolymerExpressions();
57 // TODO(sigmund): delete. The next line is only added to avoid warnings from
58 // the analyzer (see http://dartbug.com/11672)
59 Element get host => super.host;
60
61 bool get applyAuthorStyles => false;
62 bool get resetStyleInheritance => false;
63
64 /**
65 * The declaration of this polymer-element, used to extract template contents
66 * and other information.
67 */
68 static Map<String, Element> _declarations = {};
69 static Element getDeclaration(String localName) {
70 if (localName == null) return null;
71 var element = _declarations[localName];
72 if (element == null) {
73 element = document.query('polymer-element[name="$localName"]');
74 _declarations[localName] = element;
75 }
76 return element;
77 }
78
79 Map<String, PathObserver> _publishedAttrs;
80 Map<String, StreamSubscription> _bindings;
81 final List<String> _localNames = [];
82
83 void _initialize(String localName) {
84 if (localName == null) return;
85
86 var declaration = getDeclaration(localName);
87 if (declaration == null) return;
88
89 var extendee = declaration.attributes['extends'];
90 if (extendee != null) {
91 // Skip normal tags, only initialize parent custom elements.
92 if (extendee.contains('-')) _initialize(extendee);
93 }
94
95 _parseHostEvents(declaration);
96 _parseLocalEvents(declaration);
97 _publishAttributes(declaration);
98
99 var templateContent = declaration.query('template').content;
100 _shimStyling(templateContent, localName);
101
102 _localNames.add(localName);
103 }
104
105 void _publishAttributes(elementElement) {
106 _bindings = {};
107 _publishedAttrs = {};
108
109 var attrs = elementElement.attributes['attributes'];
110 if (attrs != null) {
111 // attributes='a b c' or attributes='a,b,c'
112 for (var name in attrs.split(attrs.contains(',') ? ',' : ' ')) {
113 name = name.trim();
114
115 // TODO(jmesserly): PathObserver is overkill here; it helps avoid
116 // "new Symbol" and other mirrors-related warnings.
117 _publishedAttrs[name] = new PathObserver(this, toCamelCase(name));
118 }
119 }
120 }
121
122 void created() {
123 // TODO(jmesserly): this breaks until we get some kind of type conversion.
124 // _publishedAttrs.forEach((name, propObserver) {
125 // var value = attributes[name];
126 // if (value != null) propObserver.value = value;
127 // });
128 _initShadowRoot();
129 _addHostListeners();
130 }
131
132 /**
133 * Creates the document fragment to use for each instance of the custom
134 * element, given the `<template>` node. By default this is equivalent to:
135 *
136 * template.createInstance(this, polymerSyntax);
137 *
138 * Where polymerSyntax is a singleton `PolymerExpressions` instance from the
139 * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions )
140 * package.
141 *
142 * You can override this method to change the instantiation behavior of the
143 * template, for example to use a different data-binding syntax.
144 */
145 DocumentFragment instanceTemplate(Element template) =>
146 template.createInstance(this, _polymerSyntax);
147
148 void _initShadowRoot() {
149 for (var localName in _localNames) {
150 var declaration = getDeclaration(localName);
151 var root = createShadowRoot(localName);
152 _addInstanceListeners(root, localName);
153
154 root.applyAuthorStyles = applyAuthorStyles;
155 root.resetStyleInheritance = resetStyleInheritance;
156
157 var templateNode = declaration.query('template');
158 if (templateNode == null) return;
159
160 // Create the contents of the element's ShadowRoot, and add them.
161 root.nodes.add(instanceTemplate(templateNode));
162 }
163 }
164
165 NodeBinding createBinding(String name, model, String path) {
166 var propObserver = _publishedAttrs[name];
167 if (propObserver != null) {
168 return new _PolymerBinding(this, name, model, path, propObserver);
169 }
170 return super.createBinding(name, model, path);
171 }
172
173 /**
174 * Using Polymer's platform/src/ShadowCSS.js passing the style tag's content.
175 */
176 void _shimStyling(DocumentFragment template, String localName) {
177 if (js.context == null) return;
178
179 var platform = js.context['Platform'];
180 if (platform == null) return;
181
182 var style = template.query('style');
183 if (style == null) return;
184
185 var shadowCss = platform['ShadowCSS'];
186 if (shadowCss == null) return;
187
188 // TODO(terry): Remove calls to shimShadowDOMStyling2 and replace with
189 // shimShadowDOMStyling when we support unwrapping dart:html
190 // Element to a JS DOM node.
191 var shimShadowDOMStyling2 = shadowCss['shimShadowDOMStyling2'];
192 if (shimShadowDOMStyling2 == null) return;
193
194 var scopedCSS = shimShadowDOMStyling2.apply(shadowCss,
195 [style.text, localName]);
196
197 // TODO(terry): Remove when shimShadowDOMStyling is called we don't need to
198 // replace original CSS with scoped CSS shimShadowDOMStyling
199 // does that.
200 style.text = scopedCSS;
201 }
202 }
203
204 class _PolymerBinding extends NodeBinding {
205 final PathObserver _publishedAttr;
206
207 _PolymerBinding(node, property, model, path, PathObserver this._publishedAttr)
208 : super(node, property, model, path);
209
210 void boundValueChanged(newValue) {
211 _publishedAttr.value = newValue;
212 }
213 }
214
215 /**
216 * Polymer features to handle the syntactic sugar on-* to declare to
217 * automatically map event handlers to instance methods of the [PolymerElement].
218 * This mixin is a port of:
219 * https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js
220 * https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js
221 */
222 abstract class _EventsMixin {
223 // TODO(sigmund): implement the Dart equivalent of 'inheritDelegates'
224 // Notes about differences in the implementation below:
225 // - _templateDelegates: polymer stores the template delegates directly on
226 // the template node (see in parseLocalEvents: 't.delegates = {}'). Here we
227 // simply use a separate map, where keys are the name of the
228 // custom-element.
229 // - _listenLocal we return true/false and propagate that up, JS
230 // implementation does't forward the return value.
231 // - we don't keep the side-table (weak hash map) of unhandled events (see
232 // handleIfNotHandled)
233 // - we don't use event.type to dispatch events, instead we save the event
234 // name with the event listeners. We do so to avoid translating back and
235 // forth between Dom and Dart event names.
236
237 // ---------------------------------------------------------------------------
238 // The following section was ported from:
239 // https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js
240 // ---------------------------------------------------------------------------
241
242 /** Maps event names and their associated method in the element class. */
243 final Map<String, String> _delegates = {};
244
245 /** Expected events per element node. */
246 // TODO(sigmund): investigate whether we need more than 1 set of local events
247 // per element (why does the js implementation stores 1 per template node?)
248 final Map<String, Set<String>> _templateDelegates =
249 new Map<String, Set<String>>();
250
251 /** [host] is needed by this mixin, but not defined here. */
252 Element get host;
253
254 /** Attribute prefix used for declarative event handlers. */
255 static const _eventPrefix = 'on-';
256
257 /** Whether an attribute declares an event. */
258 static bool _isEvent(String attr) => attr.startsWith(_eventPrefix);
259
260 /** Extracts events from the element tag attributes. */
261 void _parseHostEvents(elementElement) {
262 for (var attr in elementElement.attributes.keys.where(_isEvent)) {
263 _delegates[toCamelCase(attr)] = elementElement.attributes[attr];
264 }
265 }
266
267 /** Extracts events under the element's <template>. */
268 void _parseLocalEvents(elementElement) {
269 var name = elementElement.attributes["name"];
270 if (name == null) return;
271 var events = null;
272 for (var template in elementElement.queryAll('template')) {
273 var content = template.content;
274 if (content != null) {
275 for (var child in content.children) {
276 events = _accumulateEvents(child, events);
277 }
278 }
279 }
280 if (events != null) {
281 _templateDelegates[name] = events;
282 }
283 }
284
285 /** Returns all events names listened by [element] and it's children. */
286 static Set<String> _accumulateEvents(Element element, [Set<String> events]) {
287 events = events == null ? new Set<String>() : events;
288
289 // from: accumulateAttributeEvents, accumulateEvent
290 events.addAll(element.attributes.keys.where(_isEvent).map(toCamelCase));
291
292 // from: accumulateChildEvents
293 for (var child in element.children) {
294 _accumulateEvents(child, events);
295 }
296
297 // from: accumulateTemplatedEvents
298 if (element.isTemplate) {
299 var content = element.content;
300 if (content != null) {
301 for (var child in content.children) {
302 _accumulateEvents(child, events);
303 }
304 }
305 }
306 return events;
307 }
308
309 // ---------------------------------------------------------------------------
310 // The following section was ported from:
311 // https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js
312 // ---------------------------------------------------------------------------
313
314 /** Attaches event listeners on the [host] element. */
315 void _addHostListeners() {
316 for (var eventName in _delegates.keys) {
317 _addNodeListener(host, eventName,
318 (e) => _hostEventListener(eventName, e));
319 }
320 }
321
322 void _addNodeListener(node, String onEvent, Function listener) {
323 // If [node] is an element (typically when listening for host events) we
324 // use directly the '.onFoo' event stream of the element instance.
325 if (node is Element) {
326 reflect(node).getField(new Symbol(onEvent)).reflectee.listen(listener);
327 return;
328 }
329
330 // When [node] is not an element, most commonly when [node] is the
331 // shadow-root of the polymer-element, we find the appropriate static event
332 // stream providers and attach it to [node].
333 var eventProvider = _eventStreamProviders[onEvent];
334 if (eventProvider != null) {
335 eventProvider.forTarget(node).listen(listener);
336 return;
337 }
338
339 // When no provider is available, mainly because of custom-events, we use
340 // the underlying event listeners from the DOM.
341 var eventName = onEvent.substring(2).toLowerCase(); // onOneTwo => onetwo
342 // Most events names in Dart match those in JS in lowercase except for some
343 // few events listed in this map. We expect these cases to be handled above,
344 // but just in case we include them as a safety net here.
345 var jsNameFixes = const {
346 'animationend': 'webkitAnimationEnd',
347 'animationiteration': 'webkitAnimationIteration',
348 'animationstart': 'webkitAnimationStart',
349 'doubleclick': 'dblclick',
350 'fullscreenchange': 'webkitfullscreenchange',
351 'fullscreenerror': 'webkitfullscreenerror',
352 'keyadded': 'webkitkeyadded',
353 'keyerror': 'webkitkeyerror',
354 'keymessage': 'webkitkeymessage',
355 'needkey': 'webkitneedkey',
356 'speechchange': 'webkitSpeechChange',
357 };
358 var fixedName = jsNameFixes[eventName];
359 node.on[fixedName != null ? fixedName : eventName].listen(listener);
360 }
361
362 void _addInstanceListeners(ShadowRoot root, String elementName) {
363 var events = _templateDelegates[elementName];
364 if (events == null) return;
365 for (var eventName in events) {
366 _addNodeListener(root, eventName,
367 (e) => _instanceEventListener(eventName, e));
368 }
369 }
370
371 void _hostEventListener(String eventName, Event event) {
372 var method = _delegates[eventName];
373 if (event.bubbles && method != null) {
374 _dispatchMethod(this, method, event, host);
375 }
376 }
377
378 void _dispatchMethod(Object receiver, String methodName, Event event,
379 Node target) {
380 var detail = event is CustomEvent ? (event as CustomEvent).detail : null;
381 var args = [event, detail, target];
382
383 var method = new Symbol(methodName);
384 // TODO(sigmund): consider making event listeners list all arguments
385 // explicitly. Unless VM mirrors are optimized first, this reflectClass call
386 // will be expensive once custom elements extend directly from Element (see
387 // dartbug.com/11108).
388 var methodDecl = reflectClass(receiver.runtimeType).methods[method];
389 if (methodDecl != null) {
390 // This will either truncate the argument list or extend it with extra
391 // null arguments, so it will match the signature.
392 // TODO(sigmund): consider accepting optional arguments when we can tell
393 // them appart from named arguments (see http://dartbug.com/11334)
394 args.length = methodDecl.parameters.where((p) => !p.isOptional).length;
395 }
396 reflect(receiver).invoke(method, args);
397 performMicrotaskCheckpoint();
398 }
399
400 bool _instanceEventListener(String eventName, Event event) {
401 if (event.bubbles) {
402 if (event.path == null || !ShadowRoot.supported) {
403 return _listenLocalNoEventPath(eventName, event);
404 } else {
405 return _listenLocal(eventName, event);
406 }
407 }
408 return false;
409 }
410
411 bool _listenLocal(String eventName, Event event) {
412 var controller = null;
413 for (var target in event.path) {
414 // if we hit host, stop
415 if (target == host) return true;
416
417 // find a controller for the target, unless we already found `host`
418 // as a controller
419 controller = (controller == host) ? controller : _findController(target);
420
421 // if we have a controller, dispatch the event, and stop if the handler
422 // returns true
423 if (controller != null
424 && handleEvent(controller, eventName, event, target)) {
425 return true;
426 }
427 }
428 return false;
429 }
430
431 // TODO(sorvell): remove when ShadowDOM polyfill supports event path.
432 // Note that _findController will not return the expected controller when the
433 // event target is a distributed node. This is because we cannot traverse
434 // from a composed node to a node in shadowRoot.
435 // This will be addressed via an event path api
436 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066
437 bool _listenLocalNoEventPath(String eventName, Event event) {
438 var target = event.target;
439 var controller = null;
440 while (target != null && target != host) {
441 controller = (controller == host) ? controller : _findController(target);
442 if (controller != null
443 && handleEvent(controller, eventName, event, target)) {
444 return true;
445 }
446 target = target.parent;
447 }
448 return false;
449 }
450
451 // TODO(sigmund): investigate if this implementation is correct. Polymer looks
452 // up the shadow-root that contains [node] and uses a weak-hashmap to find the
453 // host associated with that root. This implementation assumes that the
454 // [node] is under [host]'s shadow-root.
455 Element _findController(Node node) => host.xtag;
456
457 bool handleEvent(
458 Element controller, String eventName, Event event, Element element) {
459 // Note: local events are listened only in the shadow root. This dynamic
460 // lookup is used to distinguish determine whether the target actually has a
461 // listener, and if so, to determine lazily what's the target method.
462 var methodName = element.attributes[toHyphenedName(eventName)];
463 if (methodName != null) {
464 _dispatchMethod(controller, methodName, event, element);
465 }
466 return event.bubbles;
467 }
468 }
469
470
471 /** Event stream providers per event name. */
472 // TODO(sigmund): after dartbug.com/11108 is fixed, consider eliminating this
473 // table and using reflection instead.
474 const Map<String, EventStreamProvider> _eventStreamProviders = const {
475 'onMouseWheel': Element.mouseWheelEvent,
476 'onTransitionEnd': Element.transitionEndEvent,
477 'onAbort': Element.abortEvent,
478 'onBeforeCopy': Element.beforeCopyEvent,
479 'onBeforeCut': Element.beforeCutEvent,
480 'onBeforePaste': Element.beforePasteEvent,
481 'onBlur': Element.blurEvent,
482 'onChange': Element.changeEvent,
483 'onClick': Element.clickEvent,
484 'onContextMenu': Element.contextMenuEvent,
485 'onCopy': Element.copyEvent,
486 'onCut': Element.cutEvent,
487 'onDoubleClick': Element.doubleClickEvent,
488 'onDrag': Element.dragEvent,
489 'onDragEnd': Element.dragEndEvent,
490 'onDragEnter': Element.dragEnterEvent,
491 'onDragLeave': Element.dragLeaveEvent,
492 'onDragOver': Element.dragOverEvent,
493 'onDragStart': Element.dragStartEvent,
494 'onDrop': Element.dropEvent,
495 'onError': Element.errorEvent,
496 'onFocus': Element.focusEvent,
497 'onInput': Element.inputEvent,
498 'onInvalid': Element.invalidEvent,
499 'onKeyDown': Element.keyDownEvent,
500 'onKeyPress': Element.keyPressEvent,
501 'onKeyUp': Element.keyUpEvent,
502 'onLoad': Element.loadEvent,
503 'onMouseDown': Element.mouseDownEvent,
504 'onMouseMove': Element.mouseMoveEvent,
505 'onMouseOut': Element.mouseOutEvent,
506 'onMouseOver': Element.mouseOverEvent,
507 'onMouseUp': Element.mouseUpEvent,
508 'onPaste': Element.pasteEvent,
509 'onReset': Element.resetEvent,
510 'onScroll': Element.scrollEvent,
511 'onSearch': Element.searchEvent,
512 'onSelect': Element.selectEvent,
513 'onSelectStart': Element.selectStartEvent,
514 'onSubmit': Element.submitEvent,
515 'onTouchCancel': Element.touchCancelEvent,
516 'onTouchEnd': Element.touchEndEvent,
517 'onTouchEnter': Element.touchEnterEvent,
518 'onTouchLeave': Element.touchLeaveEvent,
519 'onTouchMove': Element.touchMoveEvent,
520 'onTouchStart': Element.touchStartEvent,
521 'onFullscreenChange': Element.fullscreenChangeEvent,
522 'onFullscreenError': Element.fullscreenErrorEvent,
523 'onAutocomplete': FormElement.autocompleteEvent,
524 'onAutocompleteError': FormElement.autocompleteErrorEvent,
525 'onSpeechChange': InputElement.speechChangeEvent,
526 'onCanPlay': MediaElement.canPlayEvent,
527 'onCanPlayThrough': MediaElement.canPlayThroughEvent,
528 'onDurationChange': MediaElement.durationChangeEvent,
529 'onEmptied': MediaElement.emptiedEvent,
530 'onEnded': MediaElement.endedEvent,
531 'onLoadStart': MediaElement.loadStartEvent,
532 'onLoadedData': MediaElement.loadedDataEvent,
533 'onLoadedMetadata': MediaElement.loadedMetadataEvent,
534 'onPause': MediaElement.pauseEvent,
535 'onPlay': MediaElement.playEvent,
536 'onPlaying': MediaElement.playingEvent,
537 'onProgress': MediaElement.progressEvent,
538 'onRateChange': MediaElement.rateChangeEvent,
539 'onSeeked': MediaElement.seekedEvent,
540 'onSeeking': MediaElement.seekingEvent,
541 'onShow': MediaElement.showEvent,
542 'onStalled': MediaElement.stalledEvent,
543 'onSuspend': MediaElement.suspendEvent,
544 'onTimeUpdate': MediaElement.timeUpdateEvent,
545 'onVolumeChange': MediaElement.volumeChangeEvent,
546 'onWaiting': MediaElement.waitingEvent,
547 'onKeyAdded': MediaElement.keyAddedEvent,
548 'onKeyError': MediaElement.keyErrorEvent,
549 'onKeyMessage': MediaElement.keyMessageEvent,
550 'onNeedKey': MediaElement.needKeyEvent,
551 'onWebGlContextLost': CanvasElement.webGlContextLostEvent,
552 'onWebGlContextRestored': CanvasElement.webGlContextRestoredEvent,
553 'onPointerLockChange': Document.pointerLockChangeEvent,
554 'onPointerLockError': Document.pointerLockErrorEvent,
555 'onReadyStateChange': Document.readyStateChangeEvent,
556 'onSelectionChange': Document.selectionChangeEvent,
557 'onSecurityPolicyViolation': Document.securityPolicyViolationEvent,
558 };
OLDNEW
« no previous file with comments | « pkg/polymer/lib/polymer.dart ('k') | pkg/polymer/lib/src/build/linter.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698