UNPKG

10.2 kBJavaScriptView Raw
1/**
2@license
3Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9*/
10import './boot.js';
11
12import { calculateSplices } from './array-splice.js';
13import { microTask } from './async.js';
14import { wrap } from './wrap.js';
15
16/**
17 * Returns true if `node` is a slot element
18 * @param {!Node} node Node to test.
19 * @return {boolean} Returns true if the given `node` is a slot
20 * @private
21 */
22function isSlot(node) {
23 return (node.localName === 'slot');
24}
25
26/**
27 * Class that listens for changes (additions or removals) to
28 * "flattened nodes" on a given `node`. The list of flattened nodes consists
29 * of a node's children and, for any children that are `<slot>` elements,
30 * the expanded flattened list of `assignedNodes`.
31 * For example, if the observed node has children `<a></a><slot></slot><b></b>`
32 * and the `<slot>` has one `<div>` assigned to it, then the flattened
33 * nodes list is `<a></a><div></div><b></b>`. If the `<slot>` has other
34 * `<slot>` elements assigned to it, these are flattened as well.
35 *
36 * The provided `callback` is called whenever any change to this list
37 * of flattened nodes occurs, where an addition or removal of a node is
38 * considered a change. The `callback` is called with one argument, an object
39 * containing an array of any `addedNodes` and `removedNodes`.
40 *
41 * Note: the callback is called asynchronous to any changes
42 * at a microtask checkpoint. This is because observation is performed using
43 * `MutationObserver` and the `<slot>` element's `slotchange` event which
44 * are asynchronous.
45 *
46 * An example:
47 * ```js
48 * class TestSelfObserve extends PolymerElement {
49 * static get is() { return 'test-self-observe';}
50 * connectedCallback() {
51 * super.connectedCallback();
52 * this._observer = new FlattenedNodesObserver(this, (info) => {
53 * this.info = info;
54 * });
55 * }
56 * disconnectedCallback() {
57 * super.disconnectedCallback();
58 * this._observer.disconnect();
59 * }
60 * }
61 * customElements.define(TestSelfObserve.is, TestSelfObserve);
62 * ```
63 *
64 * @summary Class that listens for changes (additions or removals) to
65 * "flattened nodes" on a given `node`.
66 * @implements {PolymerDomApi.ObserveHandle}
67 */
68export let FlattenedNodesObserver = class {
69
70 /**
71 * Returns the list of flattened nodes for the given `node`.
72 * This list consists of a node's children and, for any children
73 * that are `<slot>` elements, the expanded flattened list of `assignedNodes`.
74 * For example, if the observed node has children `<a></a><slot></slot><b></b>`
75 * and the `<slot>` has one `<div>` assigned to it, then the flattened
76 * nodes list is `<a></a><div></div><b></b>`. If the `<slot>` has other
77 * `<slot>` elements assigned to it, these are flattened as well.
78 *
79 * @param {!HTMLElement|!HTMLSlotElement} node The node for which to
80 * return the list of flattened nodes.
81 * @return {!Array<!Node>} The list of flattened nodes for the given `node`.
82 * @nocollapse See https://github.com/google/closure-compiler/issues/2763
83 */
84 // eslint-disable-next-line
85 static getFlattenedNodes(node) {
86 const wrapped = wrap(node);
87 if (isSlot(node)) {
88 node = /** @type {!HTMLSlotElement} */(node); // eslint-disable-line no-self-assign
89 return wrapped.assignedNodes({flatten: true});
90 } else {
91 return Array.from(wrapped.childNodes).map((node) => {
92 if (isSlot(node)) {
93 node = /** @type {!HTMLSlotElement} */(node); // eslint-disable-line no-self-assign
94 return wrap(node).assignedNodes({flatten: true});
95 } else {
96 return [node];
97 }
98 }).reduce((a, b) => a.concat(b), []);
99 }
100 }
101
102 /**
103 * @param {!HTMLElement} target Node on which to listen for changes.
104 * @param {?function(this: Element, { target: !HTMLElement, addedNodes: !Array<!Element>, removedNodes: !Array<!Element> }):void} callback Function called when there are additions
105 * or removals from the target's list of flattened nodes.
106 */
107 // eslint-disable-next-line
108 constructor(target, callback) {
109 /**
110 * @type {MutationObserver}
111 * @private
112 */
113 this._shadyChildrenObserver = null;
114 /**
115 * @type {MutationObserver}
116 * @private
117 */
118 this._nativeChildrenObserver = null;
119 this._connected = false;
120 /**
121 * @type {!HTMLElement}
122 * @private
123 */
124 this._target = target;
125 this.callback = callback;
126 this._effectiveNodes = [];
127 this._observer = null;
128 this._scheduled = false;
129 /**
130 * @type {function()}
131 * @private
132 */
133 this._boundSchedule = () => {
134 this._schedule();
135 };
136 this.connect();
137 this._schedule();
138 }
139
140 /**
141 * Activates an observer. This method is automatically called when
142 * a `FlattenedNodesObserver` is created. It should only be called to
143 * re-activate an observer that has been deactivated via the `disconnect` method.
144 *
145 * @return {void}
146 */
147 connect() {
148 if (isSlot(this._target)) {
149 this._listenSlots([this._target]);
150 } else if (wrap(this._target).children) {
151 this._listenSlots(
152 /** @type {!NodeList<!Node>} */ (wrap(this._target).children));
153 if (window.ShadyDOM) {
154 this._shadyChildrenObserver =
155 window.ShadyDOM.observeChildren(this._target, (mutations) => {
156 this._processMutations(mutations);
157 });
158 } else {
159 this._nativeChildrenObserver =
160 new MutationObserver((mutations) => {
161 this._processMutations(mutations);
162 });
163 this._nativeChildrenObserver.observe(this._target, {childList: true});
164 }
165 }
166 this._connected = true;
167 }
168
169 /**
170 * Deactivates the flattened nodes observer. After calling this method
171 * the observer callback will not be called when changes to flattened nodes
172 * occur. The `connect` method may be subsequently called to reactivate
173 * the observer.
174 *
175 * @return {void}
176 * @override
177 */
178 disconnect() {
179 if (isSlot(this._target)) {
180 this._unlistenSlots([this._target]);
181 } else if (wrap(this._target).children) {
182 this._unlistenSlots(
183 /** @type {!NodeList<!Node>} */ (wrap(this._target).children));
184 if (window.ShadyDOM && this._shadyChildrenObserver) {
185 window.ShadyDOM.unobserveChildren(this._shadyChildrenObserver);
186 this._shadyChildrenObserver = null;
187 } else if (this._nativeChildrenObserver) {
188 this._nativeChildrenObserver.disconnect();
189 this._nativeChildrenObserver = null;
190 }
191 }
192 this._connected = false;
193 }
194
195 /**
196 * @return {void}
197 * @private
198 */
199 _schedule() {
200 if (!this._scheduled) {
201 this._scheduled = true;
202 microTask.run(() => this.flush());
203 }
204 }
205
206 /**
207 * @param {Array<MutationRecord>} mutations Mutations signaled by the mutation observer
208 * @return {void}
209 * @private
210 */
211 _processMutations(mutations) {
212 this._processSlotMutations(mutations);
213 this.flush();
214 }
215
216 /**
217 * @param {Array<MutationRecord>} mutations Mutations signaled by the mutation observer
218 * @return {void}
219 * @private
220 */
221 _processSlotMutations(mutations) {
222 if (mutations) {
223 for (let i=0; i < mutations.length; i++) {
224 let mutation = mutations[i];
225 if (mutation.addedNodes) {
226 this._listenSlots(mutation.addedNodes);
227 }
228 if (mutation.removedNodes) {
229 this._unlistenSlots(mutation.removedNodes);
230 }
231 }
232 }
233 }
234
235 /**
236 * Flushes the observer causing any pending changes to be immediately
237 * delivered the observer callback. By default these changes are delivered
238 * asynchronously at the next microtask checkpoint.
239 *
240 * @return {boolean} Returns true if any pending changes caused the observer
241 * callback to run.
242 */
243 flush() {
244 if (!this._connected) {
245 return false;
246 }
247 if (window.ShadyDOM) {
248 ShadyDOM.flush();
249 }
250 if (this._nativeChildrenObserver) {
251 this._processSlotMutations(this._nativeChildrenObserver.takeRecords());
252 } else if (this._shadyChildrenObserver) {
253 this._processSlotMutations(this._shadyChildrenObserver.takeRecords());
254 }
255 this._scheduled = false;
256 let info = {
257 target: this._target,
258 addedNodes: [],
259 removedNodes: []
260 };
261 let newNodes = this.constructor.getFlattenedNodes(this._target);
262 let splices = calculateSplices(newNodes,
263 this._effectiveNodes);
264 // process removals
265 for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
266 for (let j=0, n; (j < s.removed.length) && (n=s.removed[j]); j++) {
267 info.removedNodes.push(n);
268 }
269 }
270 // process adds
271 for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
272 for (let j=s.index; j < s.index + s.addedCount; j++) {
273 info.addedNodes.push(newNodes[j]);
274 }
275 }
276 // update cache
277 this._effectiveNodes = newNodes;
278 let didFlush = false;
279 if (info.addedNodes.length || info.removedNodes.length) {
280 didFlush = true;
281 this.callback.call(this._target, info);
282 }
283 return didFlush;
284 }
285
286 /**
287 * @param {!Array<!Node>|!NodeList<!Node>} nodeList Nodes that could change
288 * @return {void}
289 * @private
290 */
291 _listenSlots(nodeList) {
292 for (let i=0; i < nodeList.length; i++) {
293 let n = nodeList[i];
294 if (isSlot(n)) {
295 n.addEventListener('slotchange', this._boundSchedule);
296 }
297 }
298 }
299
300 /**
301 * @param {!Array<!Node>|!NodeList<!Node>} nodeList Nodes that could change
302 * @return {void}
303 * @private
304 */
305 _unlistenSlots(nodeList) {
306 for (let i=0; i < nodeList.length; i++) {
307 let n = nodeList[i];
308 if (isSlot(n)) {
309 n.removeEventListener('slotchange', this._boundSchedule);
310 }
311 }
312 }
313
314};
\No newline at end of file