3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7 Code distributed by Google as part of the polymer project is also
8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
10 <link rel="import" href="../utils/boot.html">
11 <link rel="import" href="../utils/array-splice.html">
12 <link rel="import" href="../utils/async.html">
17 function isSlot(node) {
18 return (node.localName === 'slot');
22 * Class that listens for changes (additions or removals) to
23 * "flattened nodes" on a given `node`. The list of flattened nodes consists
24 * of a node's children and, for any children that are <slot> elements,
25 * the expanded flattened list of `assignedNodes`.
26 * For example, if the observed node has children `<a></a><slot></slot><b></b>`
27 * and the `<slot>` has one `<div>` assigned to it, then the flattened
28 * nodes list is `<a></a><div></div><b></b>`. If the `<slot>` has other
29 * `<slot>` elements assigned to it, these are flattened as well.
31 * The provided `callback` is called whenever any change to this list
32 * of flattened nodes occurs, where an addition or removal of a node is
33 * considered a change. The `callback` is called with one argument, an object
34 * containing an array of any `addedNodes` and `removedNodes`.
36 * Note: the callback is called asynchronous to any changes
37 * at a microtask checkpoint. This is because observation is performed using
38 * `MutationObserver` and the `<slot> element's `slotchange` event which
42 * @param {Node} target Node on which to listen for changes.
43 * @param {Function} callback Function called when there are additions
44 * or removals from the target's list of flattened nodes.
45 * @summary Class that listens for changes (additions or removals) to
46 * "flattened nodes" on a given `node`.
48 class FlattenedNodesObserver {
51 * Returns the list of flattened nodes for the given `node`.
52 * This list consists of a node's children and, for any children
53 * that are <slot> elements, the expanded flattened list of `assignedNodes`.
54 * For example, if the observed node has children `<a></a><slot></slot><b></b>`
55 * and the `<slot>` has one `<div>` assigned to it, then the flattened
56 * nodes list is `<a></a><div></div><b></b>`. If the `<slot>` has other
57 * `<slot>` elements assigned to it, these are flattened as well.
59 * @param {Node} node The node for which to return the list of flattened nodes.
60 * @return {Array} The list of flattened nodes for the given `node`.
62 static getFlattenedNodes(node) {
64 return node.assignedNodes({flatten: true});
66 return Array.from(node.childNodes).map(node => {
68 return node.assignedNodes({flatten: true});
72 }).reduce((a, b) => a.concat(b), []);
76 constructor(target, callback) {
77 /** @type {MutationObserver} */
78 this._shadyChildrenObserver = null;
79 /** @type {MutationObserver} */
80 this._nativeChildrenObserver = null;
81 this._connected = false;
82 this._target = target;
83 this.callback = callback;
84 this._effectiveNodes = [];
85 this._observer = null;
86 this._scheduled = false;
87 this._boundSchedule = () => {
95 * Activates an observer. This method is automatically called when
96 * a `FlattenedNodesObserver` is created. It should only be called to
97 * re-activate an observer that has been deactivated via the `disconnect` method.
100 if (isSlot(this._target)) {
101 this._listenSlots([this._target]);
103 this._listenSlots(this._target.children);
104 if (window.ShadyDOM) {
105 this._shadyChildrenObserver =
106 ShadyDOM.observeChildren(this._target, (mutations) => {
107 this._processMutations(mutations);
110 this._nativeChildrenObserver =
111 new MutationObserver((mutations) => {
112 this._processMutations(mutations);
114 this._nativeChildrenObserver.observe(this._target, {childList: true});
117 this._connected = true;
121 * Deactivates the flattened nodes observer. After calling this method
122 * the observer callback will not be called when changes to flattened nodes
123 * occur. The `connect` method may be subsequently called to reactivate
127 if (isSlot(this._target)) {
128 this._unlistenSlots([this._target]);
130 this._unlistenSlots(this._target.children);
131 if (window.ShadyDOM && this._shadyChildrenObserver) {
132 ShadyDOM.unobserveChildren(this._shadyChildrenObserver);
133 this._shadyChildrenObserver = null;
134 } else if (this._nativeChildrenObserver) {
135 this._nativeChildrenObserver.disconnect();
136 this._nativeChildrenObserver = null;
139 this._connected = false;
143 if (!this._scheduled) {
144 this._scheduled = true;
145 Polymer.Async.microTask.run(() => this.flush());
149 _processMutations(mutations) {
150 this._processSlotMutations(mutations);
154 _processSlotMutations(mutations) {
156 for (let i=0; i < mutations.length; i++) {
157 let mutation = mutations[i];
158 if (mutation.addedNodes) {
159 this._listenSlots(mutation.addedNodes);
161 if (mutation.removedNodes) {
162 this._unlistenSlots(mutation.removedNodes);
169 * Flushes the observer causing any pending changes to be immediately
170 * delivered the observer callback. By default these changes are delivered
171 * asynchronously at the next microtask checkpoint.
173 * @return {boolean} Returns true if any pending changes caused the observer
177 if (!this._connected) {
180 if (window.ShadyDOM) {
183 if (this._nativeChildrenObserver) {
184 this._processSlotMutations(this._nativeChildrenObserver.takeRecords());
185 } else if (this.shadyChildrenObserver) {
186 this._processSlotMutations(this._shadyChildrenObserver.takeRecords());
188 this._scheduled = false;
190 target: this._target,
194 let newNodes = this.constructor.getFlattenedNodes(this._target);
195 let splices = Polymer.ArraySplice.calculateSplices(newNodes,
196 this._effectiveNodes);
198 for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
199 for (let j=0, n; (j < s.removed.length) && (n=s.removed[j]); j++) {
200 info.removedNodes.push(n);
204 for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
205 for (let j=s.index; j < s.index + s.addedCount; j++) {
206 info.addedNodes.push(newNodes[j]);
210 this._effectiveNodes = newNodes;
211 let didFlush = false;
212 if (info.addedNodes.length || info.removedNodes.length) {
214 this.callback.call(this._target, info);
219 _listenSlots(nodeList) {
220 for (let i=0; i < nodeList.length; i++) {
223 n.addEventListener('slotchange', this._boundSchedule);
228 _unlistenSlots(nodeList) {
229 for (let i=0; i < nodeList.length; i++) {
232 n.removeEventListener('slotchange', this._boundSchedule);
239 Polymer.FlattenedNodesObserver = FlattenedNodesObserver;