mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
* Ensure `wrap` falls back to using `ShadyDOM.patch` when `noPatch` is not in use so that `className` can be used. * Ensure `Polymer.dom` uses `patch` when `noPatch` is not in use so that `className` can be used.
486 lines
13 KiB
JavaScript
486 lines
13 KiB
JavaScript
/**
|
|
@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, {
|
|
|
|
localTarget: {
|
|
get() {
|
|
return this.event.currentTarget;
|
|
},
|
|
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'
|
|
]);
|
|
|
|
// 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'
|
|
]);
|
|
|
|
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;
|
|
};
|