// 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. /** * Custom Elements let authors define their own elements. Authors associate code * with custom tag names, and then use those custom tag names as they would any * standard tag. See * for more information. */ library custom_element; import 'dart:async'; import 'dart:html'; import 'package:mdv/mdv.dart' as mdv; import 'package:meta/meta.dart'; import 'src/custom_tag_name.dart'; // TODO(jmesserly): replace with a real custom element polyfill. // This is just something temporary. /** * *Warning*: this implementation is a work in progress. It only implements * the specification partially. * * Registers a custom HTML element with [localName] and the associated * constructor. This will ensure the element is detected and * * See the specification at: * */ void registerCustomElement(String localName, CustomElement create()) { if (_customElements == null) { _customElements = {}; mdv.instanceCreated.add(initCustomElements); // TODO(jmesserly): use MutationObserver to watch for inserts? } if (!isCustomTag(localName)) { throw new ArgumentError('$localName is not a valid custom element name, ' 'it should have at least one dash and not be a reserved name.'); } if (_customElements.containsKey(localName)) { throw new ArgumentError('custom element $localName already registered.'); } // TODO(jmesserly): validate this is a valid tag name, not a selector. _customElements[localName] = create; // Initialize elements already on the page. for (var query in [localName, '[is=$localName]']) { for (var element in document.queryAll(query)) { _initCustomElement(element, create); } } } /** * Creates a new element and returns it. If the [localName] has been registered * with [registerCustomElement], it will create the custom element. * * This is similar to `new Element.tag` in Dart and `document.createElement` * in JavaScript. * * *Warning*: this API is temporary until [dart:html] supports custom elements. */ Element createElement(String localName) => initCustomElements(new Element.tag(localName)); /** * Similar to `new Element.html`, but automatically creates registed custom * elements. * *Warning*: this API is temporary until [dart:html] supports custom elements. */ Element createElementFromHtml(String html) => initCustomElements(new Element.html(html)); /** * Initialize any registered custom elements recursively in the [node] tree. * For convenience this returns the [node] instance. * * *Warning*: this API is temporary until [dart:html] supports custom elements. */ Node initCustomElements(Node node) { for (var c = node.firstChild; c != null; c = c.nextNode) { initCustomElements(c); } if (node is Element) { var ctor = _customElements[(node as Element).localName]; if (ctor == null) { var attr = (node as Element).attributes['is']; if (attr != null) ctor = _customElements[attr]; } if (ctor != null) _initCustomElement(node, ctor); } return node; } /** * The base class for all Dart web components. In addition to the [Element] * interface, it also provides lifecycle methods: * - [created] * - [inserted] * - [attributeChanged] * - [removed] */ class CustomElement implements Element { /** The web component element wrapped by this class. */ Element _host; List _shadowRoots; /** * Shadow roots generated by dwc for each custom element, indexed by the * custom element tag name. */ Map _generatedRoots = {}; /** * Temporary property until components extend [Element]. An element can * only be associated with one host, and it is an error to use a web component * without an associated host element. */ Element get host { if (_host == null) throw new StateError('host element has not been set.'); return _host; } set host(Element value) { if (value == null) { throw new ArgumentError('host must not be null.'); } // TODO(jmesserly): xtag used to return "null" if unset, now it checks for // "this". Temporarily allow both. var xtag = value.xtag; if (xtag != null && xtag != value) { throw new ArgumentError('host must not have its xtag property set.'); } if (_host != null) { throw new StateError('host can only be set once.'); } value.xtag = this; _host = value; } /** * **Note**: This is an implementation helper and should not need to be called * from your code. * * Creates the [ShadowRoot] backing this component. */ createShadowRoot([String componentName]) { var root = host.createShadowRoot(); if (componentName != null) { _generatedRoots[componentName] = root; } return root; } getShadowRoot(String componentName) => _generatedRoots[componentName]; /** * Invoked when this component gets created. * Note that [root] will be a [ShadowRoot] if the browser supports Shadow DOM. */ void created() {} /** Invoked when this component gets inserted in the DOM tree. */ void inserted() {} /** Invoked when this component is removed from the DOM tree. */ void removed() {} // TODO(jmesserly): how do we implement this efficiently? // See https://github.com/dart-lang/web-ui/issues/37 /** Invoked when any attribute of the component is modified. */ void attributeChanged(String name, String oldValue, String newValue) {} get model => host.model; void set model(newModel) { host.model = newModel; } get templateInstance => host.templateInstance; get isTemplate => host.isTemplate; get ref => host.ref; get content => host.content; DocumentFragment createInstance(model, [BindingDelegate delegate]) => host.createInstance(model, delegate); createBinding(String name, model, String path) => host.createBinding(name, model, path); bind(String name, model, String path) => host.bind(name, model, path); void unbind(String name) => host.unbind(name); void unbindAll() => host.unbindAll(); get bindings => host.bindings; BindingDelegate get bindingDelegate => host.bindingDelegate; set bindingDelegate(BindingDelegate value) { host.bindingDelegate = value; } // TODO(efortuna): Update these when we decide what to do with these // properties. @deprecated String getAttribute(String name) => host.getAttribute(name); @deprecated String getAttributeNS(String namespaceUri, String localName) => host.getAttributeNS(namespaceUri, localName); @deprecated String setAttributeNS( String namespaceUri, String localName, String value) { host.setAttributeNS(namespaceUri, localName, value); } @deprecated void setAttribute(String name, String value) => host.setAttribute(name, value); @deprecated List get childNodes => host.childNodes; // TODO(jmesserly): this forwarding is temporary until Dart supports // subclassing Elements. // TODO(jmesserly): we were missing the setter for title, are other things // missing setters? List get nodes => host.nodes; set nodes(Iterable value) { host.nodes = value; } /** * Replaces this node with another node. */ Node replaceWith(Node otherNode) { host.replaceWith(otherNode); } /** * Removes this node from the DOM. */ void remove() => host.remove(); Node get nextNode => host.nextNode; String get nodeName => host.nodeName; Document get document => host.document; Node get previousNode => host.previousNode; String get text => host.text; set text(String v) { host.text = v; } bool contains(Node other) => host.contains(other); bool hasChildNodes() => host.hasChildNodes(); Node insertBefore(Node newChild, Node refChild) => host.insertBefore(newChild, refChild); Node insertAllBefore(Iterable newChild, Node refChild) => host.insertAllBefore(newChild, refChild); Map get attributes => host.attributes; set attributes(Map value) { host.attributes = value; } List get elements => host.children; set elements(List value) { host.children = value; } List get children => host.children; set children(List value) { host.children = value; } Set get classes => host.classes; set classes(Iterable value) { host.classes = value; } CssRect get contentEdge => host.contentEdge; CssRect get paddingEdge => host.paddingEdge; CssRect get borderEdge => host.borderEdge; CssRect get marginEdge => host.marginEdge; Point get documentOffset => host.documentOffset; Point offsetTo(Element parent) => host.offsetTo(parent); Map getNamespacedAttributes(String namespace) => host.getNamespacedAttributes(namespace); CssStyleDeclaration getComputedStyle([String pseudoElement]) => host.getComputedStyle(pseudoElement); Element clone(bool deep) => host.clone(deep); Element get parent => host.parent; Node get parentNode => host.parentNode; String get nodeValue => host.nodeValue; @deprecated // TODO(sigmund): restore the old return type and call host.on when // dartbug.com/8131 is fixed. dynamic get on { throw new UnsupportedError('on is deprecated'); } String get contentEditable => host.contentEditable; set contentEditable(String v) { host.contentEditable = v; } String get dir => host.dir; set dir(String v) { host.dir = v; } bool get draggable => host.draggable; set draggable(bool v) { host.draggable = v; } bool get hidden => host.hidden; set hidden(bool v) { host.hidden = v; } String get id => host.id; set id(String v) { host.id = v; } String get innerHTML => host.innerHtml; void set innerHTML(String v) { host.innerHtml = v; } String get innerHtml => host.innerHtml; void set innerHtml(String v) { host.innerHtml = v; } void setInnerHtml(String html, {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { host.setInnerHtml(html, validator: validator, treeSanitizer: treeSanitizer); } void set unsafeInnerHtml(String html) { host.unsafeInnerHtml = html; } DocumentFragment createFragment(String html, {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) => host.createFragment(html, validator: validator, treeSanitizer: treeSanitizer); InputMethodContext get inputMethodContext => host.inputMethodContext; bool get isContentEditable => host.isContentEditable; String get lang => host.lang; set lang(String v) { host.lang = v; } String get outerHtml => host.outerHtml; bool get spellcheck => host.spellcheck; set spellcheck(bool v) { host.spellcheck = v; } int get tabIndex => host.tabIndex; set tabIndex(int i) { host.tabIndex = i; } String get title => host.title; set title(String value) { host.title = value; } bool get translate => host.translate; set translate(bool v) { host.translate = v; } String get dropzone => host.dropzone; set dropzone(String v) { host.dropzone = v; } void click() { host.click(); } List getDestinationInsertionPoints() => host.getDestinationInsertionPoints(); Element insertAdjacentElement(String where, Element element) => host.insertAdjacentElement(where, element); void insertAdjacentHtml(String where, String html) { host.insertAdjacentHtml(where, html); } void insertAdjacentText(String where, String text) { host.insertAdjacentText(where, text); } Map get dataset => host.dataset; set dataset(Map value) { host.dataset = value; } Element get nextElementSibling => host.nextElementSibling; Element get offsetParent => host.offsetParent; Element get previousElementSibling => host.previousElementSibling; CssStyleDeclaration get style => host.style; String get tagName => host.tagName; String get pseudo => host.pseudo; void set pseudo(String value) { host.pseudo = value; } // Note: we are not polyfilling the shadow root here. This will be fixed when // we migrate to the JS Shadow DOM polyfills. You can still use getShadowRoot // to retrieve a node that behaves as the shadow root when Shadow DOM is not // enabled. ShadowRoot get shadowRoot => host.shadowRoot; void blur() { host.blur(); } void focus() { host.focus(); } void scrollByLines(int lines) { host.scrollByLines(lines); } void scrollByPages(int pages) { host.scrollByPages(pages); } void scrollIntoView([ScrollAlignment alignment]) { host.scrollIntoView(alignment); } bool matches(String selectors) => host.matches(selectors); bool matchesWithAncestors(String selectors) => host.matchesWithAncestors(selectors); @deprecated void requestFullScreen(int flags) { requestFullscreen(); } void requestFullscreen() { host.requestFullscreen(); } void requestPointerLock() { host.requestPointerLock(); } Element query(String selectors) => host.query(selectors); ElementList queryAll(String selectors) => host.queryAll(selectors); String get className => host.className; set className(String value) { host.className = value; } @deprecated int get clientHeight => client.height; @deprecated int get clientLeft => client.left; @deprecated int get clientTop => client.top; @deprecated int get clientWidth => client.width; Rect get client => host.client; @deprecated int get offsetHeight => offset.height; @deprecated int get offsetLeft => offset.left; @deprecated int get offsetTop => offset.top; @deprecated int get offsetWidth => offset.width; Rect get offset => host.offset; int get scrollHeight => host.scrollHeight; int get scrollLeft => host.scrollLeft; int get scrollTop => host.scrollTop; set scrollLeft(int value) { host.scrollLeft = value; } set scrollTop(int value) { host.scrollTop = value; } int get scrollWidth => host.scrollWidth; Rect getBoundingClientRect() => host.getBoundingClientRect(); List getClientRects() => host.getClientRects(); List getElementsByClassName(String name) => host.getElementsByClassName(name); Node get firstChild => host.firstChild; Node get lastChild => host.lastChild; String get localName => host.localName; String get namespaceUri => host.namespaceUri; int get nodeType => host.nodeType; void $dom_addEventListener(String type, EventListener listener, [bool useCapture]) { host.$dom_addEventListener(type, listener, useCapture); } bool dispatchEvent(Event event) => host.dispatchEvent(event); void $dom_removeEventListener(String type, EventListener listener, [bool useCapture]) { host.$dom_removeEventListener(type, listener, useCapture); } get xtag => host.xtag; set xtag(value) { host.xtag = value; } Node append(Node e) => host.append(e); void appendText(String text) => host.appendText(text); void appendHtml(String html) => host.appendHtml(html); String get regionOverset => host.regionOverset; List getRegionFlowRanges() => host.getRegionFlowRanges(); // TODO(jmesserly): rename "created" to "onCreated". void onCreated() => created(); Stream get onAbort => host.onAbort; Stream get onBeforeCopy => host.onBeforeCopy; Stream get onBeforeCut => host.onBeforeCut; Stream get onBeforePaste => host.onBeforePaste; Stream get onBlur => host.onBlur; Stream get onChange => host.onChange; Stream get onClick => host.onClick; Stream get onContextMenu => host.onContextMenu; Stream get onCopy => host.onCopy; Stream get onCut => host.onCut; Stream get onDoubleClick => host.onDoubleClick; Stream get onDrag => host.onDrag; Stream get onDragEnd => host.onDragEnd; Stream get onDragEnter => host.onDragEnter; Stream get onDragLeave => host.onDragLeave; Stream get onDragOver => host.onDragOver; Stream get onDragStart => host.onDragStart; Stream get onDrop => host.onDrop; Stream get onError => host.onError; Stream get onFocus => host.onFocus; Stream get onInput => host.onInput; Stream get onInvalid => host.onInvalid; Stream get onKeyDown => host.onKeyDown; Stream get onKeyPress => host.onKeyPress; Stream get onKeyUp => host.onKeyUp; Stream get onLoad => host.onLoad; Stream get onMouseDown => host.onMouseDown; Stream get onMouseEnter => host.onMouseEnter; Stream get onMouseLeave => host.onMouseLeave; Stream get onMouseMove => host.onMouseMove; Stream get onFullscreenChange => host.onFullscreenChange; Stream get onFullscreenError => host.onFullscreenError; Stream get onPaste => host.onPaste; Stream get onReset => host.onReset; Stream get onScroll => host.onScroll; Stream get onSearch => host.onSearch; Stream get onSelect => host.onSelect; Stream get onSelectStart => host.onSelectStart; Stream get onSubmit => host.onSubmit; Stream get onMouseOut => host.onMouseOut; Stream get onMouseOver => host.onMouseOver; Stream get onMouseUp => host.onMouseUp; Stream get onTouchCancel => host.onTouchCancel; Stream get onTouchEnd => host.onTouchEnd; Stream get onTouchEnter => host.onTouchEnter; Stream get onTouchLeave => host.onTouchLeave; Stream get onTouchMove => host.onTouchMove; Stream get onTouchStart => host.onTouchStart; Stream get onTransitionEnd => host.onTransitionEnd; // TODO(sigmund): do the normal forwarding when dartbug.com/7919 is fixed. Stream get onMouseWheel { throw new UnsupportedError('onMouseWheel is not supported'); } } Map _customElements; void _initCustomElement(Element node, CustomElement ctor()) { CustomElement element = ctor(); element.host = node; // TODO(jmesserly): replace lifecycle stuff with a proper polyfill. element.created(); _registerLifecycleInsert(element); } void _registerLifecycleInsert(CustomElement element) { runAsync(() { // TODO(jmesserly): bottom up or top down insert? var node = element.host; // TODO(jmesserly): need a better check to see if the node has been removed. if (node.parentNode == null) return; _registerLifecycleRemove(element); element.inserted(); }); } void _registerLifecycleRemove(CustomElement element) { // TODO(jmesserly): need fallback or polyfill for MutationObserver. if (!MutationObserver.supported) return; new MutationObserver((records, observer) { var node = element.host; for (var record in records) { for (var removed in record.removedNodes) { if (identical(node, removed)) { observer.disconnect(); element.removed(); return; } } } }).observe(element.parentNode, childList: true); }