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 | import './boot.js';
|
11 |
|
12 | import { calculateSplices } from './array-splice.js';
|
13 | import { microTask } from './async.js';
|
14 | import { 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 | */
|
22 | function 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 | */
|
68 | export 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 |