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

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

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

Powered by Google App Engine
This is Rietveld 408576698