PageSourceSearch

https://wpt.fyi/node_modules/@polymer/polymer/lib/utils/flattened-nodes-observer.js

js wpt.fyi collected 2026-09-24 08:48:54 UTC 10,217 bytes, 317 lines download raw bytes

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      const results = [];
92      for (let i = 0; i < wrapped.childNodes.length; i++) {
93        const node = wrapped.childNodes[i];
94        if (isSlot(node)) {
95          const slotNode = /** @type {!HTMLSlotElement} */ (node);
96          results.push(...wrap(slotNode).assignedNodes({ flatten: true }));
97        } else {
98          results.push(node);
99        }
100      }
101      return results;
102    }
103  }
104
105  /**
106   * @param {!HTMLElement} target Node on which to listen for changes.
107   * @param {?function(this: Element, { target: !HTMLElement, addedNodes: !Array<!Element>, removedNodes: !Array<!Element> }):void} callback Function called when there are additions
108   * or removals from the target's list of flattened nodes.
109   */
110  // eslint-disable-next-line
111  constructor(target, callback) {
112    /**
113     * @type {MutationObserver}
114     * @private
115     */
116    this._shadyChildrenObserver = null;
117    /**
118     * @type {MutationObserver}
119     * @private
120     */
121    this._nativeChildrenObserver = null;
122    this._connected = false;
123    /**
124     * @type {!HTMLElement}
125     * @private
126     */
127    this._target = target;
128    this.callback = callback;
129    this._effectiveNodes = [];
130    this._observer = null;
131    this._scheduled = false;
132    /**
133     * @type {function()}
134     * @private
135     */
136    this._boundSchedule = () => {
137      this._schedule();
138    };
139    this.connect();
140    this._schedule();
141  }
142
143  /**
144   * Activates an observer. This method is automatically called when
145   * a `FlattenedNodesObserver` is created. It should only be called to
146   * re-activate an observer that has been deactivated via the `disconnect` method.
147   *
148   * @return {void}
149   */
150  connect() {
151    if (isSlot(this._target)) {
152      this._listenSlots([this._target]);
153    } else if (wrap(this._target).children) {
154      this._listenSlots(
155          /** @type {!NodeList<!Node>} */ (wrap(this._target).children));
156      if (window.ShadyDOM) {
157        this._shadyChildrenObserver =
158          window.ShadyDOM.observeChildren(this._target, (mutations) => {
159            this._processMutations(mutations);
160          });
161      } else {
162        this._nativeChildrenObserver =
163          new MutationObserver((mutations) => {
164            this._processMutations(mutations);
165          });
166        this._nativeChildrenObserver.observe(this._target, {childList: true});
167      }
168    }
169    this._connected = true;
170  }
171
172  /**
173   * Deactivates the flattened nodes observer. After calling this method
174   * the observer callback will not be called when changes to flattened nodes
175   * occur. The `connect` method may be subsequently called to reactivate
176   * the observer.
177   *
178   * @return {void}
179   * @override
180   */
181  disconnect() {
182    if (isSlot(this._target)) {
183      this._unlistenSlots([this._target]);
184    } else if (wrap(this._target).children) {
185      this._unlistenSlots(
186          /** @type {!NodeList<!Node>} */ (wrap(this._target).children));
187      if (window.ShadyDOM && this._shadyChildrenObserver) {
188        window.ShadyDOM.unobserveChildren(this._shadyChildrenObserver);
189        this._shadyChildrenObserver = null;
190      } else if (this._nativeChildrenObserver) {
191        this._nativeChildrenObserver.disconnect();
192        this._nativeChildrenObserver = null;
193      }
194    }
195    this._connected = false;
196  }
197
198  /**
199   * @return {void}
200   * @private
201   */
202  _schedule() {
203    if (!this._scheduled) {
204      this._scheduled = true;
205      microTask.run(() => this.flush());
206    }
207  }
208
209  /**
210   * @param {Array<MutationRecord>} mutations Mutations signaled by the mutation observer
211   * @return {void}
212   * @private
213   */
214  _processMutations(mutations) {
215    this._processSlotMutations(mutations);
216    this.flush();
217  }
218
219  /**
220   * @param {Array<MutationRecord>} mutations Mutations signaled by the mutation observer
221   * @return {void}
222   * @private
223   */
224  _processSlotMutations(mutations) {
225    if (mutations) {
226      for (let i=0; i < mutations.length; i++) {
227        let mutation = mutations[i];
228        if (mutation.addedNodes) {
229          this._listenSlots(mutation.addedNodes);
230        }
231        if (mutation.removedNodes) {
232          this._unlistenSlots(mutation.removedNodes);
233        }
234      }
235    }
236  }
237
238  /**
239   * Flushes the observer causing any pending changes to be immediately
240   * delivered the observer callback. By default these changes are delivered
241   * asynchronously at the next microtask checkpoint.
242   *
243   * @return {boolean} Returns true if any pending changes caused the observer
244   * callback to run.
245   */
246  flush() {
247    if (!this._connected) {
248      return false;
249    }
250    if (window.ShadyDOM) {
251      ShadyDOM.flush();
252    }
253    if (this._nativeChildrenObserver) {
254      this._processSlotMutations(this._nativeChildrenObserver.takeRecords());
255    } else if (this._shadyChildrenObserver) {
256      this._processSlotMutations(this._shadyChildrenObserver.takeRecords());
257    }
258    this._scheduled = false;
259    let info = {
260      target: this._target,
261      addedNodes: [],
262      removedNodes: []
263    };
264    let newNodes = this.constructor.getFlattenedNodes(this._target);
265    let splices = calculateSplices(newNodes,
266      this._effectiveNodes);
267    // process removals
268    for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
269      for (let j=0, n; (j < s.removed.length) && (n=s.removed[j]); j++) {
270        info.removedNodes.push(n);
271      }
272    }
273    // process adds
274    for (let i=0, s; (i<splices.length) && (s=splices[i]); i++) {
275      for (let j=s.index; j < s.index + s.addedCount; j++) {
276        info.addedNodes.push(newNodes[j]);
277      }
278    }
279    // update cache
280    this._effectiveNodes = newNodes;
281    let didFlush = false;
282    if (info.addedNodes.length || info.removedNodes.length) {
283      didFlush = true;
284      this.callback.call(this._target, info);
285    }
286    return didFlush;
287  }
288
289  /**
290   * @param {!Array<!Node>|!NodeList<!Node>} nodeList Nodes that could change
291   * @return {void}
292   * @private
293   */
294  _listenSlots(nodeList) {
295    for (let i=0; i < nodeList.length; i++) {
296      let n = nodeList[i];
297      if (isSlot(n)) {
298        n.addEventListener('slotchange', this._boundSchedule);
299      }
300    }
301  }
302
303  /**
304   * @param {!Array<!Node>|!NodeList<!Node>} nodeList Nodes that could change
305   * @return {void}
306   * @private
307   */
308  _unlistenSlots(nodeList) {
309    for (let i=0; i < nodeList.length; i++) {
310      let n = nodeList[i];
311      if (isSlot(n)) {
312        n.removeEventListener('slotchange', this._boundSchedule);
313      }
314    }
315  }
316
317};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.