UNPKG

@polymer/polymer

Version:

The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to

497 lines (455 loc) 13.8 kB
/** @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ import '../utils/boot.js'; import { wrap } from '../utils/wrap.js'; import '../utils/settings.js'; import { FlattenedNodesObserver } from '../utils/flattened-nodes-observer.js'; export { flush, enqueueDebouncer as addDebouncer } from '../utils/flush.js'; /* eslint-disable no-unused-vars */ import { Debouncer } from '../utils/debounce.js'; // used in type annotations /* eslint-enable no-unused-vars */ const p = Element.prototype; /** * @const {function(this:Node, string): boolean} */ const normalizedMatchesSelector = p.matches || p.matchesSelector || p.mozMatchesSelector || p.msMatchesSelector || p.oMatchesSelector || p.webkitMatchesSelector; /** * Cross-platform `element.matches` shim. * * @function matchesSelector * @param {!Node} node Node to check selector against * @param {string} selector Selector to match * @return {boolean} True if node matched selector */ export const matchesSelector = function(node, selector) { return normalizedMatchesSelector.call(node, selector); }; /** * Node API wrapper class returned from `Polymer.dom.(target)` when * `target` is a `Node`. * @implements {PolymerDomApi} * @unrestricted */ class DomApiNative { /** * @param {!Node} node Node for which to create a Polymer.dom helper object. */ constructor(node) { if (window['ShadyDOM'] && window['ShadyDOM']['inUse']) { window['ShadyDOM']['patch'](node); } this.node = node; } /** * Returns an instance of `FlattenedNodesObserver` that * listens for node changes on this element. * * @param {function(this:HTMLElement, { target: !HTMLElement, addedNodes: !Array<!Element>, removedNodes: !Array<!Element> }):void} callback Called when direct or distributed children * of this element changes * @return {!PolymerDomApi.ObserveHandle} Observer instance * @override */ observeNodes(callback) { return new FlattenedNodesObserver( /** @type {!HTMLElement} */(this.node), callback); } /** * Disconnects an observer previously created via `observeNodes` * * @param {!PolymerDomApi.ObserveHandle} observerHandle Observer instance * to disconnect. * @return {void} * @override */ unobserveNodes(observerHandle) { observerHandle.disconnect(); } /** * Provided as a backwards-compatible API only. This method does nothing. * @return {void} */ notifyObserver() {} /** * Returns true if the provided node is contained with this element's * light-DOM children or shadow root, including any nested shadow roots * of children therein. * * @param {Node} node Node to test * @return {boolean} Returns true if the given `node` is contained within * this element's light or shadow DOM. * @override */ deepContains(node) { if (wrap(this.node).contains(node)) { return true; } let n = node; let doc = node.ownerDocument; // walk from node to `this` or `document` while (n && n !== doc && n !== this.node) { // use logical parentnode, or native ShadowRoot host n = wrap(n).parentNode || wrap(n).host; } return n === this.node; } /** * Returns the root node of this node. Equivalent to `getRootNode()`. * * @return {!Node} Top most element in the dom tree in which the node * exists. If the node is connected to a document this is either a * shadowRoot or the document; otherwise, it may be the node * itself or a node or document fragment containing it. * @override */ getOwnerRoot() { return wrap(this.node).getRootNode(); } /** * For slot elements, returns the nodes assigned to the slot; otherwise * an empty array. It is equivalent to `<slot>.addignedNodes({flatten:true})`. * * @return {!Array<!Node>} Array of assigned nodes * @override */ getDistributedNodes() { return (this.node.localName === 'slot') ? wrap(this.node).assignedNodes({flatten: true}) : []; } /** * Returns an array of all slots this element was distributed to. * * @return {!Array<!HTMLSlotElement>} Description * @override */ getDestinationInsertionPoints() { let ip$ = []; let n = wrap(this.node).assignedSlot; while (n) { ip$.push(n); n = wrap(n).assignedSlot; } return ip$; } /** * Calls `importNode` on the `ownerDocument` for this node. * * @param {!Node} node Node to import * @param {boolean} deep True if the node should be cloned deeply during * import * @return {Node} Clone of given node imported to this owner document */ importNode(node, deep) { let doc = this.node instanceof Document ? this.node : this.node.ownerDocument; return wrap(doc).importNode(node, deep); } /** * @return {!Array<!Node>} Returns a flattened list of all child nodes and * nodes assigned to child slots. * @override */ getEffectiveChildNodes() { return FlattenedNodesObserver.getFlattenedNodes( /** @type {!HTMLElement} */ (this.node)); } /** * Returns a filtered list of flattened child elements for this element based * on the given selector. * * @param {string} selector Selector to filter nodes against * @return {!Array<!HTMLElement>} List of flattened child elements * @override */ queryDistributedElements(selector) { let c$ = this.getEffectiveChildNodes(); let list = []; for (let i=0, l=c$.length, c; (i<l) && (c=c$[i]); i++) { if ((c.nodeType === Node.ELEMENT_NODE) && matchesSelector(c, selector)) { list.push(c); } } return list; } /** * For shadow roots, returns the currently focused element within this * shadow root. * * return {Node|undefined} Currently focused element * @override */ get activeElement() { let node = this.node; return node._activeElement !== undefined ? node._activeElement : node.activeElement; } } function forwardMethods(proto, methods) { for (let i=0; i < methods.length; i++) { let method = methods[i]; /* eslint-disable valid-jsdoc */ proto[method] = /** @this {DomApiNative} */ function() { return this.node[method].apply(this.node, arguments); }; /* eslint-enable */ } } function forwardReadOnlyProperties(proto, properties) { for (let i=0; i < properties.length; i++) { let name = properties[i]; Object.defineProperty(proto, name, { get: function() { const domApi = /** @type {DomApiNative} */(this); return domApi.node[name]; }, configurable: true }); } } function forwardProperties(proto, properties) { for (let i=0; i < properties.length; i++) { let name = properties[i]; Object.defineProperty(proto, name, { /** * @this {DomApiNative} * @return {*} . */ get: function() { return this.node[name]; }, /** * @this {DomApiNative} * @param {*} value . */ set: function(value) { this.node[name] = value; }, configurable: true }); } } /** * Event API wrapper class returned from `dom.(target)` when * `target` is an `Event`. */ export class EventApi { constructor(event) { this.event = event; } /** * Returns the first node on the `composedPath` of this event. * * @return {!EventTarget} The node this event was dispatched to */ get rootTarget() { return this.path[0]; } /** * Returns the local (re-targeted) target for this event. * * @return {!EventTarget} The local (re-targeted) target for this event. */ get localTarget() { return this.event.target; } /** * Returns the `composedPath` for this event. * @return {!Array<!EventTarget>} The nodes this event propagated through */ get path() { return this.event.composedPath(); } } /** * @function * @param {boolean=} deep * @return {!Node} */ DomApiNative.prototype.cloneNode; /** * @function * @param {!Node} node * @return {!Node} */ DomApiNative.prototype.appendChild; /** * @function * @param {!Node} newChild * @param {Node} refChild * @return {!Node} */ DomApiNative.prototype.insertBefore; /** * @function * @param {!Node} node * @return {!Node} */ DomApiNative.prototype.removeChild; /** * @function * @param {!Node} oldChild * @param {!Node} newChild * @return {!Node} */ DomApiNative.prototype.replaceChild; /** * @function * @param {string} name * @param {string} value * @return {void} */ DomApiNative.prototype.setAttribute; /** * @function * @param {string} name * @return {void} */ DomApiNative.prototype.removeAttribute; /** * @function * @param {string} selector * @return {?Element} */ DomApiNative.prototype.querySelector; /** * @function * @param {string} selector * @return {!NodeList<!Element>} */ DomApiNative.prototype.querySelectorAll; /** @type {?Node} */ DomApiNative.prototype.parentNode; /** @type {?Node} */ DomApiNative.prototype.firstChild; /** @type {?Node} */ DomApiNative.prototype.lastChild; /** @type {?Node} */ DomApiNative.prototype.nextSibling; /** @type {?Node} */ DomApiNative.prototype.previousSibling; /** @type {?HTMLElement} */ DomApiNative.prototype.firstElementChild; /** @type {?HTMLElement} */ DomApiNative.prototype.lastElementChild; /** @type {?HTMLElement} */ DomApiNative.prototype.nextElementSibling; /** @type {?HTMLElement} */ DomApiNative.prototype.previousElementSibling; /** @type {!Array<!Node>} */ DomApiNative.prototype.childNodes; /** @type {!Array<!HTMLElement>} */ DomApiNative.prototype.children; /** @type {?DOMTokenList} */ DomApiNative.prototype.classList; /** @type {string} */ DomApiNative.prototype.textContent; /** @type {string} */ DomApiNative.prototype.innerHTML; let DomApiImpl = DomApiNative; if (window['ShadyDOM'] && window['ShadyDOM']['inUse'] && window['ShadyDOM']['noPatch'] && window['ShadyDOM']['Wrapper']) { /** * @private * @extends {HTMLElement} */ class Wrapper extends window['ShadyDOM']['Wrapper'] {} // copy bespoke API onto wrapper Object.getOwnPropertyNames(DomApiNative.prototype).forEach((prop) => { if (prop != 'activeElement') { Wrapper.prototype[prop] = DomApiNative.prototype[prop]; } }); // Note, `classList` is here only for legacy compatibility since it does not // trigger distribution in v1 Shadow DOM. forwardReadOnlyProperties(Wrapper.prototype, [ 'classList' ]); DomApiImpl = Wrapper; Object.defineProperties(EventApi.prototype, { // Returns the "lowest" node in the same root as the event's currentTarget. // When in `noPatch` mode, this must be calculated by walking the event's // path. localTarget: { get() { const current = this.event.currentTarget; const currentRoot = current && dom(current).getOwnerRoot(); const p$ = this.path; for (let i = 0; i < p$.length; i++) { const e = p$[i]; if (dom(e).getOwnerRoot() === currentRoot) { return e; } } }, configurable: true }, path: { get() { return window['ShadyDOM']['composedPath'](this.event); }, configurable: true } }); } else { // Methods that can provoke distribution or must return the logical, not // composed tree. forwardMethods(DomApiNative.prototype, [ 'cloneNode', 'appendChild', 'insertBefore', 'removeChild', 'replaceChild', 'setAttribute', 'removeAttribute', 'querySelector', 'querySelectorAll', 'attachShadow' ]); // Properties that should return the logical, not composed tree. Note, `classList` // is here only for legacy compatibility since it does not trigger distribution // in v1 Shadow DOM. forwardReadOnlyProperties(DomApiNative.prototype, [ 'parentNode', 'firstChild', 'lastChild', 'nextSibling', 'previousSibling', 'firstElementChild', 'lastElementChild', 'nextElementSibling', 'previousElementSibling', 'childNodes', 'children', 'classList', 'shadowRoot' ]); forwardProperties(DomApiNative.prototype, [ 'textContent', 'innerHTML', 'className' ]); } export const DomApi = DomApiImpl; /** * Legacy DOM and Event manipulation API wrapper factory used to abstract * differences between native Shadow DOM and "Shady DOM" when polyfilling on * older browsers. * * Note that in Polymer 2.x use of `Polymer.dom` is no longer required and * in the majority of cases simply facades directly to the standard native * API. * * @summary Legacy DOM and Event manipulation API wrapper factory used to * abstract differences between native Shadow DOM and "Shady DOM." * @param {(Node|Event|DomApiNative|EventApi)=} obj Node or event to operate on * @return {!DomApiNative|!EventApi} Wrapper providing either node API or event API */ export const dom = function(obj) { obj = obj || document; if (obj instanceof DomApiImpl) { return /** @type {!DomApi} */(obj); } if (obj instanceof EventApi) { return /** @type {!EventApi} */(obj); } let helper = obj['__domApi']; if (!helper) { if (obj instanceof Event) { helper = new EventApi(obj); } else { helper = new DomApiImpl(/** @type {Node} */(obj)); } obj['__domApi'] = helper; } return helper; };