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