Index: pkg/polymer/lib/polymer_element.dart |
diff --git a/pkg/polymer/lib/polymer_element.dart b/pkg/polymer/lib/polymer_element.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..141a50d73c46c74622c5f414a1149039ae805449 |
--- /dev/null |
+++ b/pkg/polymer/lib/polymer_element.dart |
@@ -0,0 +1,555 @@ |
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library polymer.polymer_element; |
+ |
+import 'dart:async'; |
+import 'dart:html'; |
+import 'dart:mirrors'; |
+ |
+import 'package:custom_element/custom_element.dart'; |
+import 'package:js/js.dart' as js; |
+import 'package:mdv/mdv.dart' show NodeBinding; |
+import 'package:observe/observe.dart'; |
+import 'package:observe/src/microtask.dart'; |
+import 'package:polymer_expressions/polymer_expressions.dart'; |
+ |
+import 'src/utils_observe.dart' show toCamelCase, toHyphenedName; |
+ |
+/** |
+ * Registers a [PolymerElement]. This is similar to [registerCustomElement] |
+ * but it is designed to work with the `<element>` element and adds additional |
+ * features. |
+ */ |
+void registerPolymerElement(String localName, PolymerElement create()) { |
+ registerCustomElement(localName, () => create().._initialize(localName)); |
+} |
+ |
+/** |
+ * *Warning*: many features of this class are not fully implemented. |
+ * |
+ * The base class for Polymer elements. It provides convience features on top |
+ * of the custom elements web standard. |
+ * |
+ * Currently it supports publishing attributes via: |
+ * |
+ * <element name="..." attributes="foo, bar, baz"> |
+ * |
+ * Any attribute published this way can be used in a data binding expression, |
+ * and it should contain a corresponding DOM field. |
+ * |
+ * *Warning*: due to dart2js mirror limititations, the mapping from HTML |
+ * attribute to element property is a conversion from `dash-separated-words` |
+ * to camelCase, rather than searching for a property with the same name. |
+ */ |
+// TODO(jmesserly): fix the dash-separated-words issue. Polymer uses lowercase. |
+class PolymerElement extends CustomElement with _EventsMixin { |
+ // This is a partial port of: |
+ // https://github.com/Polymer/polymer/blob/stable/src/attrs.js |
+ // https://github.com/Polymer/polymer/blob/stable/src/bindProperties.js |
+ // https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js |
+ // https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js |
+ // TODO(jmesserly): we still need to port more of the functionality |
+ |
+ /// The one syntax to rule them all. |
+ static BindingDelegate _polymerSyntax = new PolymerExpressions(); |
+ // TODO(sigmund): delete. The next line is only added to avoid warnings from |
+ // the analyzer (see http://dartbug.com/11672) |
+ Element get host => super.host; |
+ |
+ bool get applyAuthorStyles => false; |
+ bool get resetStyleInheritance => false; |
+ |
+ /** |
+ * The declaration of this polymer-element, used to extract template contents |
+ * and other information. |
+ */ |
+ static Map<String, Element> _declarations = {}; |
+ static Element getDeclaration(String localName) { |
+ if (localName == null) return null; |
+ var element = _declarations[localName]; |
+ if (element == null) { |
+ element = document.query('polymer-element[name="$localName"]'); |
+ _declarations[localName] = element; |
+ } |
+ return element; |
+ } |
+ |
+ Map<String, PathObserver> _publishedAttrs; |
+ Map<String, StreamSubscription> _bindings; |
+ final List<String> _localNames = []; |
+ |
+ void _initialize(String localName) { |
+ if (localName == null) return; |
+ |
+ var declaration = getDeclaration(localName); |
+ if (declaration == null) return; |
+ |
+ if (declaration.attributes['extends'] != null) { |
+ var base = declaration.attributes['extends']; |
+ // Skip normal tags, only initialize parent custom elements. |
+ if (base.contains('-')) _initialize(base); |
+ } |
+ |
+ _parseHostEvents(declaration); |
+ _parseLocalEvents(declaration); |
+ _publishAttributes(declaration); |
+ _localNames.add(localName); |
+ } |
+ |
+ void _publishAttributes(elementElement) { |
+ _bindings = {}; |
+ _publishedAttrs = {}; |
+ |
+ var attrs = elementElement.attributes['attributes']; |
+ if (attrs != null) { |
+ // attributes='a b c' or attributes='a,b,c' |
+ for (var name in attrs.split(attrs.contains(',') ? ',' : ' ')) { |
+ name = name.trim(); |
+ |
+ // TODO(jmesserly): PathObserver is overkill here; it helps avoid |
+ // "new Symbol" and other mirrors-related warnings. |
+ _publishedAttrs[name] = new PathObserver(this, toCamelCase(name)); |
+ } |
+ } |
+ } |
+ |
+ void created() { |
+ // TODO(jmesserly): this breaks until we get some kind of type conversion. |
+ // _publishedAttrs.forEach((name, propObserver) { |
+ // var value = attributes[name]; |
+ // if (value != null) propObserver.value = value; |
+ // }); |
+ _initShadowRoot(); |
+ _addHostListeners(); |
+ } |
+ |
+ /** |
+ * Creates the document fragment to use for each instance of the custom |
+ * element, given the `<template>` node. By default this is equivalent to: |
+ * |
+ * template.createInstance(this, polymerSyntax); |
+ * |
+ * Where polymerSyntax is a singleton `PolymerExpressions` instance from the |
+ * [polymer_expressions](https://pub.dartlang.org/packages/polymer_expressions) |
+ * package. |
+ * |
+ * You can override this method to change the instantiation behavior of the |
+ * template, for example to use a different data-binding syntax. |
+ */ |
+ DocumentFragment instanceTemplate(Element template) => |
+ template.createInstance(this, _polymerSyntax); |
+ |
+ void _initShadowRoot() { |
+ for (var localName in _localNames) { |
+ var declaration = getDeclaration(localName); |
+ var root = createShadowRoot(localName); |
+ _addInstanceListeners(root, localName); |
+ |
+ root.applyAuthorStyles = applyAuthorStyles; |
+ root.resetStyleInheritance = resetStyleInheritance; |
+ |
+ var templateNode = declaration.children.firstWhere( |
+ (n) => n.localName == 'template', orElse: () => null); |
+ if (templateNode == null) return; |
+ |
+ // Create the contents of the element's ShadowRoot, and add them. |
+ root.nodes.add(instanceTemplate(templateNode)); |
+ |
+ var extendsName = declaration.attributes['extends']; |
+ _shimCss(root, localName, extendsName); |
+ } |
+ } |
+ |
+ NodeBinding createBinding(String name, model, String path) { |
+ var propObserver = _publishedAttrs[name]; |
+ if (propObserver != null) { |
+ return new _PolymerBinding(this, name, model, path, propObserver); |
+ } |
+ return super.createBinding(name, model, path); |
+ } |
+ |
+ /** |
+ * Using Polymer's platform/src/ShadowCSS.js passing the style tag's content. |
+ */ |
+ void _shimCss(ShadowRoot root, String localName, String extendsName) { |
+ // TODO(terry): Remove warning, cast js.context to dynamic because of bug |
+ // https://code.google.com/p/dart/issues/detail?id=6111. The |
+ // js interop package will be patching this until bug is fixed. |
+ var platform = (js.context as dynamic).Platform; |
+ if (platform == null) return; |
+ var shadowCss = platform.ShadowCSS; |
+ if (shadowCss == null) return; |
+ |
+ // TODO(terry): Remove calls to shimShadowDOMStyling2 and replace with |
+ // shimShadowDOMStyling when we support unwrapping dart:html |
+ // Element to a JS DOM node. |
+ var shimShadowDOMStyling2 = shadowCss.shimShadowDOMStyling2; |
+ if (shimShadowDOMStyling2 == null) return; |
+ var style = root.query('style'); |
+ if (style == null) return; |
+ var scopedCSS = shimShadowDOMStyling2(style.text, localName); |
+ |
+ // TODO(terry): Remove when shimShadowDOMStyling is called we don't need to |
+ // replace original CSS with scoped CSS shimShadowDOMStyling |
+ // does that. |
+ style.text = scopedCSS; |
+ } |
+} |
+ |
+class _PolymerBinding extends NodeBinding { |
+ final PathObserver _publishedAttr; |
+ |
+ _PolymerBinding(node, property, model, path, PathObserver this._publishedAttr) |
+ : super(node, property, model, path); |
+ |
+ void boundValueChanged(newValue) { |
+ _publishedAttr.value = newValue; |
+ } |
+} |
+ |
+/** |
+ * Polymer features to handle the syntactic sugar on-* to declare to |
+ * automatically map event handlers to instance methods of the [PolymerElement]. |
+ * This mixin is a port of: |
+ * https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js |
+ * https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js |
+ */ |
+abstract class _EventsMixin { |
+ // TODO(sigmund): implement the Dart equivalent of 'inheritDelegates' |
+ // Notes about differences in the implementation below: |
+ // - _templateDelegates: polymer stores the template delegates directly on |
+ // the template node (see in parseLocalEvents: 't.delegates = {}'). Here we |
+ // simply use a separate map, where keys are the name of the |
+ // custom-element. |
+ // - _listenLocal we return true/false and propagate that up, JS |
+ // implementation does't forward the return value. |
+ // - we don't keep the side-table (weak hash map) of unhandled events (see |
+ // handleIfNotHandled) |
+ // - we don't use event.type to dispatch events, instead we save the event |
+ // name with the event listeners. We do so to avoid translating back and |
+ // forth between Dom and Dart event names. |
+ |
+ // --------------------------------------------------------------------------- |
+ // The following section was ported from: |
+ // https://github.com/Polymer/polymer/blob/7936ff8/src/declaration/events.js |
+ // --------------------------------------------------------------------------- |
+ |
+ /** Maps event names and their associated method in the element class. */ |
+ final Map<String, String> _delegates = {}; |
+ |
+ /** Expected events per element node. */ |
+ // TODO(sigmund): investigate whether we need more than 1 set of local events |
+ // per element (why does the js implementation stores 1 per template node?) |
+ final Map<String, Set<String>> _templateDelegates = |
+ new Map<String, Set<String>>(); |
+ |
+ /** [host] is needed by this mixin, but not defined here. */ |
+ Element get host; |
+ |
+ /** Attribute prefix used for declarative event handlers. */ |
+ static const _eventPrefix = 'on-'; |
+ |
+ /** Whether an attribute declares an event. */ |
+ static bool _isEvent(String attr) => attr.startsWith(_eventPrefix); |
+ |
+ /** Extracts events from the element tag attributes. */ |
+ void _parseHostEvents(elementElement) { |
+ for (var attr in elementElement.attributes.keys.where(_isEvent)) { |
+ _delegates[toCamelCase(attr)] = elementElement.attributes[attr]; |
+ } |
+ } |
+ |
+ /** Extracts events under the element's <template>. */ |
+ void _parseLocalEvents(elementElement) { |
+ var name = elementElement.attributes["name"]; |
+ if (name == null) return; |
+ var events = null; |
+ for (var template in elementElement.queryAll('template')) { |
+ var content = template.content; |
+ if (content != null) { |
+ for (var child in content.children) { |
+ events = _accumulateEvents(child, events); |
+ } |
+ } |
+ } |
+ if (events != null) { |
+ _templateDelegates[name] = events; |
+ } |
+ } |
+ |
+ /** Returns all events names listened by [element] and it's children. */ |
+ static Set<String> _accumulateEvents(Element element, [Set<String> events]) { |
+ events = events == null ? new Set<String>() : events; |
+ |
+ // from: accumulateAttributeEvents, accumulateEvent |
+ events.addAll(element.attributes.keys.where(_isEvent).map(toCamelCase)); |
+ |
+ // from: accumulateChildEvents |
+ for (var child in element.children) { |
+ _accumulateEvents(child, events); |
+ } |
+ |
+ // from: accumulateTemplatedEvents |
+ if (element.isTemplate) { |
+ var content = element.content; |
+ if (content != null) { |
+ for (var child in content.children) { |
+ _accumulateEvents(child, events); |
+ } |
+ } |
+ } |
+ return events; |
+ } |
+ |
+ // --------------------------------------------------------------------------- |
+ // The following section was ported from: |
+ // https://github.com/Polymer/polymer/blob/7936ff8/src/instance/events.js |
+ // --------------------------------------------------------------------------- |
+ |
+ /** Attaches event listeners on the [host] element. */ |
+ void _addHostListeners() { |
+ for (var eventName in _delegates.keys) { |
+ _addNodeListener(host, eventName, |
+ (e) => _hostEventListener(eventName, e)); |
+ } |
+ } |
+ |
+ void _addNodeListener(node, String onEvent, Function listener) { |
+ // If [node] is an element (typically when listening for host events) we |
+ // use directly the '.onFoo' event stream of the element instance. |
+ if (node is Element) { |
+ reflect(node).getField(new Symbol(onEvent)).reflectee.listen(listener); |
+ return; |
+ } |
+ |
+ // When [node] is not an element, most commonly when [node] is the |
+ // shadow-root of the polymer-element, we find the appropriate static event |
+ // stream providers and attach it to [node]. |
+ var eventProvider = _eventStreamProviders[onEvent]; |
+ if (eventProvider != null) { |
+ eventProvider.forTarget(node).listen(listener); |
+ return; |
+ } |
+ |
+ // When no provider is available, mainly because of custom-events, we use |
+ // the underlying event listeners from the DOM. |
+ var eventName = onEvent.substring(2).toLowerCase(); // onOneTwo => onetwo |
+ // Most events names in Dart match those in JS in lowercase except for some |
+ // few events listed in this map. We expect these cases to be handled above, |
+ // but just in case we include them as a safety net here. |
+ var jsNameFixes = const { |
+ 'animationend': 'webkitAnimationEnd', |
+ 'animationiteration': 'webkitAnimationIteration', |
+ 'animationstart': 'webkitAnimationStart', |
+ 'doubleclick': 'dblclick', |
+ 'fullscreenchange': 'webkitfullscreenchange', |
+ 'fullscreenerror': 'webkitfullscreenerror', |
+ 'keyadded': 'webkitkeyadded', |
+ 'keyerror': 'webkitkeyerror', |
+ 'keymessage': 'webkitkeymessage', |
+ 'needkey': 'webkitneedkey', |
+ 'speechchange': 'webkitSpeechChange', |
+ }; |
+ var fixedName = jsNameFixes[eventName]; |
+ node.on[fixedName != null ? fixedName : eventName].listen(listener); |
+ } |
+ |
+ void _addInstanceListeners(ShadowRoot root, String elementName) { |
+ var events = _templateDelegates[elementName]; |
+ if (events == null) return; |
+ for (var eventName in events) { |
+ _addNodeListener(root, eventName, |
+ (e) => _instanceEventListener(eventName, e)); |
+ } |
+ } |
+ |
+ void _hostEventListener(String eventName, Event event) { |
+ var method = _delegates[eventName]; |
+ if (event.bubbles && method != null) { |
+ _dispatchMethod(this, method, event, host); |
+ } |
+ } |
+ |
+ void _dispatchMethod(Object receiver, String methodName, Event event, |
+ Node target) { |
+ var detail = event is CustomEvent ? (event as CustomEvent).detail : null; |
+ var args = [event, detail, target]; |
+ |
+ var method = new Symbol(methodName); |
+ // TODO(sigmund): consider making event listeners list all arguments |
+ // explicitly. Unless VM mirrors are optimized first, this reflectClass call |
+ // will be expensive once custom elements extend directly from Element (see |
+ // dartbug.com/11108). |
+ var methodDecl = reflectClass(receiver.runtimeType).methods[method]; |
+ if (methodDecl != null) { |
+ // This will either truncate the argument list or extend it with extra |
+ // null arguments, so it will match the signature. |
+ // TODO(sigmund): consider accepting optional arguments when we can tell |
+ // them appart from named arguments (see http://dartbug.com/11334) |
+ args.length = methodDecl.parameters.where((p) => !p.isOptional).length; |
+ } |
+ reflect(receiver).invoke(method, args); |
+ performMicrotaskCheckpoint(); |
+ } |
+ |
+ bool _instanceEventListener(String eventName, Event event) { |
+ if (event.bubbles) { |
+ if (event.path == null || !ShadowRoot.supported) { |
+ return _listenLocalNoEventPath(eventName, event); |
+ } else { |
+ return _listenLocal(eventName, event); |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ bool _listenLocal(String eventName, Event event) { |
+ var controller = null; |
+ for (var target in event.path) { |
+ // if we hit host, stop |
+ if (target == host) return true; |
+ |
+ // find a controller for the target, unless we already found `host` |
+ // as a controller |
+ controller = (controller == host) ? controller : _findController(target); |
+ |
+ // if we have a controller, dispatch the event, and stop if the handler |
+ // returns true |
+ if (controller != null |
+ && handleEvent(controller, eventName, event, target)) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ // TODO(sorvell): remove when ShadowDOM polyfill supports event path. |
+ // Note that _findController will not return the expected controller when the |
+ // event target is a distributed node. This is because we cannot traverse |
+ // from a composed node to a node in shadowRoot. |
+ // This will be addressed via an event path api |
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=21066 |
+ bool _listenLocalNoEventPath(String eventName, Event event) { |
+ var target = event.target; |
+ var controller = null; |
+ while (target != null && target != host) { |
+ controller = (controller == host) ? controller : _findController(target); |
+ if (controller != null |
+ && handleEvent(controller, eventName, event, target)) { |
+ return true; |
+ } |
+ target = target.parent; |
+ } |
+ return false; |
+ } |
+ |
+ // TODO(sigmund): investigate if this implementation is correct. Polymer looks |
+ // up the shadow-root that contains [node] and uses a weak-hashmap to find the |
+ // host associated with that root. This implementation assumes that the |
+ // [node] is under [host]'s shadow-root. |
+ Element _findController(Node node) => host.xtag; |
+ |
+ bool handleEvent( |
+ Element controller, String eventName, Event event, Element element) { |
+ // Note: local events are listened only in the shadow root. This dynamic |
+ // lookup is used to distinguish determine whether the target actually has a |
+ // listener, and if so, to determine lazily what's the target method. |
+ var methodName = element.attributes[toHyphenedName(eventName)]; |
+ if (methodName != null) { |
+ _dispatchMethod(controller, methodName, event, element); |
+ } |
+ return event.bubbles; |
+ } |
+} |
+ |
+ |
+/** Event stream providers per event name. */ |
+// TODO(sigmund): after dartbug.com/11108 is fixed, consider eliminating this |
+// table and using reflection instead. |
+const Map<String, EventStreamProvider> _eventStreamProviders = const { |
+ 'onMouseWheel': Element.mouseWheelEvent, |
+ 'onTransitionEnd': Element.transitionEndEvent, |
+ 'onAbort': Element.abortEvent, |
+ 'onBeforeCopy': Element.beforeCopyEvent, |
+ 'onBeforeCut': Element.beforeCutEvent, |
+ 'onBeforePaste': Element.beforePasteEvent, |
+ 'onBlur': Element.blurEvent, |
+ 'onChange': Element.changeEvent, |
+ 'onClick': Element.clickEvent, |
+ 'onContextMenu': Element.contextMenuEvent, |
+ 'onCopy': Element.copyEvent, |
+ 'onCut': Element.cutEvent, |
+ 'onDoubleClick': Element.doubleClickEvent, |
+ 'onDrag': Element.dragEvent, |
+ 'onDragEnd': Element.dragEndEvent, |
+ 'onDragEnter': Element.dragEnterEvent, |
+ 'onDragLeave': Element.dragLeaveEvent, |
+ 'onDragOver': Element.dragOverEvent, |
+ 'onDragStart': Element.dragStartEvent, |
+ 'onDrop': Element.dropEvent, |
+ 'onError': Element.errorEvent, |
+ 'onFocus': Element.focusEvent, |
+ 'onInput': Element.inputEvent, |
+ 'onInvalid': Element.invalidEvent, |
+ 'onKeyDown': Element.keyDownEvent, |
+ 'onKeyPress': Element.keyPressEvent, |
+ 'onKeyUp': Element.keyUpEvent, |
+ 'onLoad': Element.loadEvent, |
+ 'onMouseDown': Element.mouseDownEvent, |
+ 'onMouseMove': Element.mouseMoveEvent, |
+ 'onMouseOut': Element.mouseOutEvent, |
+ 'onMouseOver': Element.mouseOverEvent, |
+ 'onMouseUp': Element.mouseUpEvent, |
+ 'onPaste': Element.pasteEvent, |
+ 'onReset': Element.resetEvent, |
+ 'onScroll': Element.scrollEvent, |
+ 'onSearch': Element.searchEvent, |
+ 'onSelect': Element.selectEvent, |
+ 'onSelectStart': Element.selectStartEvent, |
+ 'onSubmit': Element.submitEvent, |
+ 'onTouchCancel': Element.touchCancelEvent, |
+ 'onTouchEnd': Element.touchEndEvent, |
+ 'onTouchEnter': Element.touchEnterEvent, |
+ 'onTouchLeave': Element.touchLeaveEvent, |
+ 'onTouchMove': Element.touchMoveEvent, |
+ 'onTouchStart': Element.touchStartEvent, |
+ 'onFullscreenChange': Element.fullscreenChangeEvent, |
+ 'onFullscreenError': Element.fullscreenErrorEvent, |
+ 'onAutocomplete': FormElement.autocompleteEvent, |
+ 'onAutocompleteError': FormElement.autocompleteErrorEvent, |
+ 'onSpeechChange': InputElement.speechChangeEvent, |
+ 'onCanPlay': MediaElement.canPlayEvent, |
+ 'onCanPlayThrough': MediaElement.canPlayThroughEvent, |
+ 'onDurationChange': MediaElement.durationChangeEvent, |
+ 'onEmptied': MediaElement.emptiedEvent, |
+ 'onEnded': MediaElement.endedEvent, |
+ 'onLoadStart': MediaElement.loadStartEvent, |
+ 'onLoadedData': MediaElement.loadedDataEvent, |
+ 'onLoadedMetadata': MediaElement.loadedMetadataEvent, |
+ 'onPause': MediaElement.pauseEvent, |
+ 'onPlay': MediaElement.playEvent, |
+ 'onPlaying': MediaElement.playingEvent, |
+ 'onProgress': MediaElement.progressEvent, |
+ 'onRateChange': MediaElement.rateChangeEvent, |
+ 'onSeeked': MediaElement.seekedEvent, |
+ 'onSeeking': MediaElement.seekingEvent, |
+ 'onShow': MediaElement.showEvent, |
+ 'onStalled': MediaElement.stalledEvent, |
+ 'onSuspend': MediaElement.suspendEvent, |
+ 'onTimeUpdate': MediaElement.timeUpdateEvent, |
+ 'onVolumeChange': MediaElement.volumeChangeEvent, |
+ 'onWaiting': MediaElement.waitingEvent, |
+ 'onKeyAdded': MediaElement.keyAddedEvent, |
+ 'onKeyError': MediaElement.keyErrorEvent, |
+ 'onKeyMessage': MediaElement.keyMessageEvent, |
+ 'onNeedKey': MediaElement.needKeyEvent, |
+ 'onWebGlContextLost': CanvasElement.webGlContextLostEvent, |
+ 'onWebGlContextRestored': CanvasElement.webGlContextRestoredEvent, |
+ 'onPointerLockChange': Document.pointerLockChangeEvent, |
+ 'onPointerLockError': Document.pointerLockErrorEvent, |
+ 'onReadyStateChange': Document.readyStateChangeEvent, |
+ 'onSelectionChange': Document.selectionChangeEvent, |
+ 'onSecurityPolicyViolation': Document.securityPolicyViolationEvent, |
+}; |