Files
polymer/lib/legacy/polymer.dom.js
Steven Orvell b13e656fae Fix className on browsers without good native accessors
* 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.
2019-04-11 10:36:50 -07:00

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;
};