c1f75d038b43f267f9f90d74b440e4c77164a728
[factbooks] /
1 <!--
2 @license
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
9 -->
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">
13 <script>
14 (function() {
15   'use strict';
16
17   function isSlot(node) {
18     return (node.localName === 'slot');
19   }
20
21   /**
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.
30    *
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`.
35    *
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
39    * are asynchronous.
40    *
41    * @memberof Polymer
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`.
47    */
48   class FlattenedNodesObserver {
49
50     /**
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.
58      *
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`.
61     */
62     static getFlattenedNodes(node) {
63       if (isSlot(node)) {
64         return node.assignedNodes({flatten: true});
65       } else {
66         return Array.from(node.childNodes).map(node => {
67           if (isSlot(node)) {
68             return node.assignedNodes({flatten: true});
69           } else {
70             return [node];
71           }
72         }).reduce((a, b) => a.concat(b), []);
73       }
74     }
75
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 = () => {
88         this._schedule();
89       }
90       this.connect();
91       this._schedule();
92     }
93
94     /**
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.
98      */
99     connect() {
100       if (isSlot(this._target)) {
101         this._listenSlots([this._target]);
102       } else {
103         this._listenSlots(this._target.children);
104         if (window.ShadyDOM) {
105           this._shadyChildrenObserver =
106             ShadyDOM.observeChildren(this._target, (mutations) => {
107               this._processMutations(mutations);
108             });
109         } else {
110           this._nativeChildrenObserver =
111             new MutationObserver((mutations) => {
112               this._processMutations(mutations);
113             });
114           this._nativeChildrenObserver.observe(this._target, {childList: true});
115         }
116       }
117       this._connected = true;
118     }
119
120     /**
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
124      * the observer.
125      */
126     disconnect() {
127       if (isSlot(this._target)) {
128         this._unlistenSlots([this._target]);
129       } else {
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;
137         }
138       }
139       this._connected = false;
140     }
141
142     _schedule() {
143       if (!this._scheduled) {
144         this._scheduled = true;
145         Polymer.Async.microTask.run(() => this.flush());
146       }
147     }
148
149     _processMutations(mutations) {
150       this._processSlotMutations(mutations);
151       this.flush();
152     }
153
154     _processSlotMutations(mutations) {
155       if (mutations) {
156         for (let i=0; i < mutations.length; i++) {
157           let mutation = mutations[i];
158           if (mutation.addedNodes) {
159             this._listenSlots(mutation.addedNodes);
160           }
161           if (mutation.removedNodes) {
162             this._unlistenSlots(mutation.removedNodes);
163           }
164         }
165       }
166     }
167
168     /**
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.
172      *
173      * @return {boolean} Returns true if any pending changes caused the observer
174      * callback to run.
175      */
176     flush() {
177       if (!this._connected) {
178         return;
179       }
180       if (window.ShadyDOM) {
181         ShadyDOM.flush();
182       }
183       if (this._nativeChildrenObserver) {
184         this._processSlotMutations(this._nativeChildrenObserver.takeRecords());
185       } else if (this.shadyChildrenObserver) {
186         this._processSlotMutations(this._shadyChildrenObserver.takeRecords());
187       }
188       this._scheduled = false;
189       let info = {
190         target: this._target,
191         addedNodes: [],
192         removedNodes: []
193       };
194       let newNodes = this.constructor.getFlattenedNodes(this._target);
195       let splices = Polymer.ArraySplice.calculateSplices(newNodes,
196         this._effectiveNodes);
197       // process removals
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);
201         }
202       }
203       // process adds
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]);
207         }
208       }
209       // update cache
210       this._effectiveNodes = newNodes;
211       let didFlush = false;
212       if (info.addedNodes.length || info.removedNodes.length) {
213         didFlush = true;
214         this.callback.call(this._target, info);
215       }
216       return didFlush;
217     }
218
219     _listenSlots(nodeList) {
220       for (let i=0; i < nodeList.length; i++) {
221         let n = nodeList[i];
222         if (isSlot(n)) {
223           n.addEventListener('slotchange', this._boundSchedule);
224         }
225       }
226     }
227
228     _unlistenSlots(nodeList) {
229       for (let i=0; i < nodeList.length; i++) {
230         let n = nodeList[i];
231         if (isSlot(n)) {
232           n.removeEventListener('slotchange', this._boundSchedule);
233         }
234       }
235     }
236
237   }
238
239   Polymer.FlattenedNodesObserver = FlattenedNodesObserver;
240
241 })();
242 </script>