@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
318 lines (303 loc) • 10.2 kB
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 './boot.js';
import { calculateSplices } from './array-splice.js';
import { microTask } from './async.js';
import { wrap } from './wrap.js';
/**
* Returns true if `node` is a slot element
* @param {!Node} node Node to test.
* @return {boolean} Returns true if the given `node` is a slot
* @private
*/
function isSlot(node) {
return (node.localName === 'slot');
}
/**
* Class that listens for changes (additions or removals) to
* "flattened nodes" on a given `node`. The list of flattened nodes consists
* of a node's children and, for any children that are `<slot>` elements,
* the expanded flattened list of `assignedNodes`.
* For example, if the observed node has children `<a></a><slot></slot><b></b>`
* and the `<slot>` has one `<div>` assigned to it, then the flattened
* nodes list is `<a></a><div></div><b></b>`. If the `<slot>` has other
* `<slot>` elements assigned to it, these are flattened as well.
*
* The provided `callback` is called whenever any change to this list
* of flattened nodes occurs, where an addition or removal of a node is
* considered a change. The `callback` is called with one argument, an object
* containing an array of any `addedNodes` and `removedNodes`.
*
* Note: the callback is called asynchronous to any changes
* at a microtask checkpoint. This is because observation is performed using
* `MutationObserver` and the `<slot>` element's `slotchange` event which
* are asynchronous.
*
* An example:
* ```js
* class TestSelfObserve extends PolymerElement {
* static get is() { return 'test-self-observe';}
* connectedCallback() {
* super.connectedCallback();
* this._observer = new FlattenedNodesObserver(this, (info) => {
* this.info = info;
* });
* }
* disconnectedCallback() {
* super.disconnectedCallback();
* this._observer.disconnect();
* }
* }
* customElements.define(TestSelfObserve.is, TestSelfObserve);
* ```
*
* @summary Class that listens for changes (additions or removals) to
* "flattened nodes" on a given `node`.
* @implements {PolymerDomApi.ObserveHandle}
*/
export let FlattenedNodesObserver = class {
/**
* Returns the list of flattened nodes for the given `node`.
* This list consists of a node's children and, for any children
* that are `<slot>` elements, the expanded flattened list of `assignedNodes`.
* For example, if the observed node has children `<a></a><slot></slot><b></b>`
* and the `<slot>` has one `<div>` assigned to it, then the flattened
* nodes list is `<a></a><div></div><b></b>`. If the `<slot>` has other
* `<slot>` elements assigned to it, these are flattened as well.
*
* @param {!HTMLElement|!HTMLSlotElement} node The node for which to
* return the list of flattened nodes.
* @return {!Array<!Node>} The list of flattened nodes for the given `node`.
* @nocollapse See https://github.com/google/closure-compiler/issues/2763
*/
// eslint-disable-next-line
static getFlattenedNodes(node) {
const wrapped = wrap(node);
if (isSlot(node)) {
node = /** @type {!HTMLSlotElement} */(node); // eslint-disable-line no-self-assign
return wrapped.assignedNodes({flatten: true});
} else {
const results = [];
for (let i = 0; i < wrapped.childNodes.length; i++) {
const node = wrapped.childNodes[i];
if (isSlot(node)) {
const slotNode = /** @type {!HTMLSlotElement} */ (node);
results.push(...wrap(slotNode).assignedNodes({ flatten: true }));
} else {
results.push(node);
}
}
return results;
}
}
/**
* @param {!HTMLElement} target Node on which to listen for changes.
* @param {?function(this: Element, { target: !HTMLElement, addedNodes: !Array<!Element>, removedNodes: !Array<!Element> }):void} callback Function called when there are additions
* or removals from the target's list of flattened nodes.
*/
// eslint-disable-next-line
constructor(target, callback) {
/**
* @type {MutationObserver}
* @private
*/
this._shadyChildrenObserver = null;
/**
* @type {MutationObserver}
* @private
*/
this._nativeChildrenObserver = null;
this._connected = false;
/**
* @type {!HTMLElement}
* @private
*/
this._target = target;
this.callback = callback;
this._effectiveNodes = [];
this._observer = null;
this._scheduled = false;
/**
* @type {function()}
* @private
*/
this._boundSchedule = () => {
this._schedule();
};
this.connect();
this._schedule();
}
/**
* Activates an observer. This method is automatically called when
* a `FlattenedNodesObserver` is created. It should only be called to
* re-activate an observer that has been deactivated via the `disconnect` method.
*
* @return {void}
*/
connect() {
if (isSlot(this._target)) {
this._listenSlots([this._target]);
} else if (wrap(this._target).children) {
this._listenSlots(
/** @type {!NodeList<!Node>} */ (wrap(this._target).children));
if (window.ShadyDOM) {
this._shadyChildrenObserver =
window.ShadyDOM.observeChildren(this._target, (mutations) => {
this._processMutations(mutations);
});
} else {
this._nativeChildrenObserver =
new MutationObserver((mutations) => {
this._processMutations(mutations);
});
this._nativeChildrenObserver.observe(this._target, {childList: true});
}
}
this._connected = true;
}
/**
* Deactivates the flattened nodes observer. After calling this method
* the observer callback will not be called when changes to flattened nodes
* occur. The `connect` method may be subsequently called to reactivate
* the observer.
*
* @return {void}
* @override
*/
disconnect() {
if (isSlot(this._target)) {
this._unlistenSlots([this._target]);
} else if (wrap(this._target).children) {
this._unlistenSlots(
/** @type {!NodeList<!Node>} */ (wrap(this._target).children));
if (window.ShadyDOM && this._shadyChildrenObserver) {
window.ShadyDOM.unobserveChildren(this._shadyChildrenObserver);
this._shadyChildrenObserver = null;
} else if (this._nativeChildrenObserver) {
this._nativeChildrenObserver.disconnect();
this._nativeChildrenObserver = null;
}
}
this._connected = false;
}
/**
* @return {void}
* @private
*/
_schedule() {
if (!this._scheduled) {
this._scheduled = true;
microTask.run(() => this.flush());
}
}
/**
* @param {Array<MutationRecord>} mutations Mutations signaled by the mutation observer
* @return {void}
* @private
*/
_processMutations(mutations) {
this._processSlotMutations(mutations);
this.flush();
}
/**
* @param {Array<MutationRecord>} mutations Mutations signaled by the mutation observer
* @return {void}
* @private
*/
_processSlotMutations(mutations) {
if (mutations) {
for (let i=0; i < mutations.length; i++) {
let mutation = mutations[i];
if (mutation.addedNodes) {
this._listenSlots(mutation.addedNodes);
}
if (mutation.removedNodes) {
this._unlistenSlots(mutation.removedNodes);
}
}
}
}
/**
* Flushes the observer causing any pending changes to be immediately
* delivered the observer callback. By default these changes are delivered
* asynchronously at the next microtask checkpoint.
*
* @return {boolean} Returns true if any pending changes caused the observer
* callback to run.
*/
flush() {
if (!this._connected) {
return false;
}
if (window.ShadyDOM) {
ShadyDOM.flush();
}
if (this._nativeChildrenObserver) {
this._processSlotMutations(this._nativeChildrenObserver.takeRecords());
} else if (this._shadyChildrenObserver) {
this._processSlotMutations(this._shadyChildrenObserver.takeRecords());
}
this._scheduled = false;
let info = {
target: this._target,
addedNodes: [],
removedNodes: []
};
let newNodes = this.constructor.getFlattenedNodes(this._target);
let splices = calculateSplices(newNodes,
this._effectiveNodes);
// process removals
for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
for (let j=0, n; (j < s.removed.length) && (n=s.removed[j]); j++) {
info.removedNodes.push(n);
}
}
// process adds
for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
for (let j=s.index; j < s.index + s.addedCount; j++) {
info.addedNodes.push(newNodes[j]);
}
}
// update cache
this._effectiveNodes = newNodes;
let didFlush = false;
if (info.addedNodes.length || info.removedNodes.length) {
didFlush = true;
this.callback.call(this._target, info);
}
return didFlush;
}
/**
* @param {!Array<!Node>|!NodeList<!Node>} nodeList Nodes that could change
* @return {void}
* @private
*/
_listenSlots(nodeList) {
for (let i=0; i < nodeList.length; i++) {
let n = nodeList[i];
if (isSlot(n)) {
n.addEventListener('slotchange', this._boundSchedule);
}
}
}
/**
* @param {!Array<!Node>|!NodeList<!Node>} nodeList Nodes that could change
* @return {void}
* @private
*/
_unlistenSlots(nodeList) {
for (let i=0; i < nodeList.length; i++) {
let n = nodeList[i];
if (isSlot(n)) {
n.removeEventListener('slotchange', this._boundSchedule);
}
}
}
};