selection.js (9578B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 8 9 loader.lazyRequireGetter( 10 this, 11 "nodeConstants", 12 "resource://devtools/shared/dom-node-constants.js" 13 ); 14 15 /** 16 * Selection is a singleton belonging to the Toolbox that manages the current selected 17 * NodeFront. In addition, it provides some helpers about the context of the selected 18 * node. 19 * 20 * API 21 * 22 * new Selection() 23 * destroy() 24 * nodeFront (readonly) 25 * setNodeFront(node, origin="unknown") 26 * 27 * Helpers: 28 * 29 * window 30 * document 31 * isRoot() 32 * isNode() 33 * isHTMLNode() 34 * 35 * Check the nature of the node: 36 * 37 * isElementNode() 38 * isAttributeNode() 39 * isTextNode() 40 * isCDATANode() 41 * isEntityRefNode() 42 * isEntityNode() 43 * isProcessingInstructionNode() 44 * isCommentNode() 45 * isDocumentNode() 46 * isDocumentTypeNode() 47 * isDocumentFragmentNode() 48 * isNotationNode() 49 * 50 * Events: 51 * "new-node-front" when the inner node changed 52 * "attribute-changed" when an attribute is changed 53 * "detached-front" when the node (or one of its parents) is removed from 54 * the document 55 * "reparented" when the node (or one of its parents) is moved under 56 * a different node 57 */ 58 class Selection extends EventEmitter { 59 constructor() { 60 super(); 61 62 this.setNodeFront = this.setNodeFront.bind(this); 63 } 64 65 #nodeFront; 66 67 // The WalkerFront is dynamic and is always set to the selected NodeFront's WalkerFront. 68 #walker = null; 69 70 // A single node front can be represented twice on the client when the node is a slotted 71 // element. It will be displayed once as a direct child of the host element, and once as 72 // a child of a slot in the "shadow DOM". The latter is called the slotted version. 73 #isSlotted = false; 74 75 #searchQuery; 76 77 #onMutations = mutations => { 78 let attributeChange = false; 79 let pseudoChange = false; 80 let detached = false; 81 let detachedNodeParent = null; 82 83 for (const m of mutations) { 84 if (m.type == "attributes") { 85 attributeChange = true; 86 } 87 if (m.type == "pseudoClassLock") { 88 pseudoChange = true; 89 } 90 if (m.type == "childList") { 91 if ( 92 // If the node that was selected was removed… 93 !this.isConnected() && 94 // …directly in this mutation, let's pick its parent node 95 (m.removed.some(nodeFront => nodeFront == this.nodeFront) || 96 // in case we don't directly get the removed node, default to the first 97 // element being mutated in the array of mutations we received 98 !detachedNodeParent) 99 ) { 100 if (this.isNode()) { 101 detachedNodeParent = m.target; 102 } 103 detached = true; 104 } 105 } 106 } 107 108 // Fire our events depending on what changed in the mutations array 109 if (attributeChange) { 110 this.emit("attribute-changed"); 111 } 112 if (pseudoChange) { 113 this.emit("pseudoclass"); 114 } 115 if (detached) { 116 this.emit("detached-front", detachedNodeParent); 117 } 118 }; 119 120 destroy() { 121 this.setWalker(); 122 this.#nodeFront = null; 123 } 124 125 /** 126 * @param {WalkerFront|null} walker 127 */ 128 setWalker(walker = null) { 129 if (this.#walker) { 130 this.#removeWalkerFrontEventListeners(this.#walker); 131 } 132 133 this.#walker = walker; 134 if (this.#walker) { 135 this.#setWalkerFrontEventListeners(this.#walker); 136 } 137 } 138 139 /** 140 * Set event listeners on the passed walker front 141 * 142 * @param {WalkerFront} walker 143 */ 144 #setWalkerFrontEventListeners(walker) { 145 walker.on("mutations", this.#onMutations); 146 } 147 148 /** 149 * Remove event listeners we previously set on walker front 150 * 151 * @param {WalkerFront} walker 152 */ 153 #removeWalkerFrontEventListeners(walker) { 154 walker.off("mutations", this.#onMutations); 155 } 156 157 /** 158 * Called when a target front is destroyed. 159 * 160 * @param {TargetFront} front 161 * @fires detached-front 162 */ 163 onTargetDestroyed(targetFront) { 164 // if the current walker belongs to the target that is destroyed, emit a `detached-front` 165 // event so consumers can act accordingly (e.g. in the inspector, another node will be 166 // selected) 167 if ( 168 this.#walker && 169 !targetFront.isTopLevel && 170 this.#walker.targetFront == targetFront 171 ) { 172 this.#removeWalkerFrontEventListeners(this.#walker); 173 this.emit("detached-front"); 174 } 175 } 176 177 /** 178 * Update the currently selected node-front. 179 * 180 * @param {NodeFront} nodeFront 181 * The NodeFront being selected. 182 * @param {object} options (optional) 183 * @param {string} options.reason: Reason that triggered the selection, will be fired with 184 * the "new-node-front" event. 185 * @param {boolean} options.isSlotted: Is the selection representing the slotted version 186 * of the node. 187 * @param {string} options.searchQuery: If the selection was triggered by a search, the 188 * query of said search 189 */ 190 setNodeFront( 191 nodeFront, 192 { reason = "unknown", isSlotted = false, searchQuery = null } = {} 193 ) { 194 this.reason = reason; 195 196 // If an inlineTextChild text node is being set, then set it's parent instead. 197 const parentNode = nodeFront && nodeFront.parentNode(); 198 if (nodeFront && parentNode && parentNode.inlineTextChild === nodeFront) { 199 nodeFront = parentNode; 200 } 201 202 if (this.#nodeFront == null && nodeFront == null) { 203 // Avoid to notify multiple "unselected" events with a null/undefined nodeFront 204 // (e.g. once when the webpage start to navigate away from the current webpage, 205 // and then again while the new page is being loaded). 206 return; 207 } 208 209 this.emit("node-front-will-unset"); 210 211 this.#isSlotted = isSlotted; 212 this.#searchQuery = searchQuery; 213 this.#nodeFront = nodeFront; 214 215 if (nodeFront) { 216 this.setWalker(nodeFront.walkerFront); 217 } else { 218 this.setWalker(); 219 } 220 221 this.emit("new-node-front", nodeFront, this.reason); 222 } 223 224 get nodeFront() { 225 return this.#nodeFront; 226 } 227 228 isRoot() { 229 return ( 230 this.isNode() && this.isConnected() && this.#nodeFront.isDocumentElement 231 ); 232 } 233 234 isNode() { 235 return !!this.#nodeFront; 236 } 237 238 isConnected() { 239 let node = this.#nodeFront; 240 if (!node || node.isDestroyed()) { 241 return false; 242 } 243 244 while (node) { 245 if (node === this.#walker.rootNode) { 246 return true; 247 } 248 node = node.parentOrHost(); 249 } 250 return false; 251 } 252 253 isHTMLNode() { 254 const xhtmlNs = "http://www.w3.org/1999/xhtml"; 255 return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs; 256 } 257 258 isSVGNode() { 259 const svgNs = "http://www.w3.org/2000/svg"; 260 return this.isNode() && this.nodeFront.namespaceURI == svgNs; 261 } 262 263 isMathMLNode() { 264 const mathmlNs = "http://www.w3.org/1998/Math/MathML"; 265 return this.isNode() && this.nodeFront.namespaceURI == mathmlNs; 266 } 267 268 // Node type 269 270 isElementNode() { 271 return ( 272 this.isNode() && this.nodeFront.nodeType == nodeConstants.ELEMENT_NODE 273 ); 274 } 275 276 isPseudoElementNode() { 277 return this.isNode() && this.nodeFront.isPseudoElement; 278 } 279 280 isNativeAnonymousNode() { 281 return this.isNode() && this.nodeFront.isNativeAnonymous; 282 } 283 284 isAttributeNode() { 285 return ( 286 this.isNode() && this.nodeFront.nodeType == nodeConstants.ATTRIBUTE_NODE 287 ); 288 } 289 290 isTextNode() { 291 return this.isNode() && this.nodeFront.nodeType == nodeConstants.TEXT_NODE; 292 } 293 294 isCDATANode() { 295 return ( 296 this.isNode() && 297 this.nodeFront.nodeType == nodeConstants.CDATA_SECTION_NODE 298 ); 299 } 300 301 isEntityRefNode() { 302 return ( 303 this.isNode() && 304 this.nodeFront.nodeType == nodeConstants.ENTITY_REFERENCE_NODE 305 ); 306 } 307 308 isEntityNode() { 309 return ( 310 this.isNode() && this.nodeFront.nodeType == nodeConstants.ENTITY_NODE 311 ); 312 } 313 314 isProcessingInstructionNode() { 315 return ( 316 this.isNode() && 317 this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE 318 ); 319 } 320 321 isCommentNode() { 322 return ( 323 this.isNode() && 324 this.nodeFront.nodeType == nodeConstants.PROCESSING_INSTRUCTION_NODE 325 ); 326 } 327 328 isDocumentNode() { 329 return ( 330 this.isNode() && this.nodeFront.nodeType == nodeConstants.DOCUMENT_NODE 331 ); 332 } 333 334 /** 335 * @returns true if the selection is the <body> HTML element. 336 */ 337 isBodyNode() { 338 return ( 339 this.isHTMLNode() && 340 this.isConnected() && 341 this.nodeFront.nodeName === "BODY" 342 ); 343 } 344 345 /** 346 * @returns true if the selection is the <head> HTML element. 347 */ 348 isHeadNode() { 349 return ( 350 this.isHTMLNode() && 351 this.isConnected() && 352 this.nodeFront.nodeName === "HEAD" 353 ); 354 } 355 356 isDocumentTypeNode() { 357 return ( 358 this.isNode() && 359 this.nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE 360 ); 361 } 362 363 isDocumentFragmentNode() { 364 return ( 365 this.isNode() && 366 this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE 367 ); 368 } 369 370 isNotationNode() { 371 return ( 372 this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE 373 ); 374 } 375 376 isSlotted() { 377 return this.#isSlotted; 378 } 379 380 isShadowRootNode() { 381 return this.isNode() && this.nodeFront.isShadowRoot; 382 } 383 384 supportsScrollIntoView() { 385 return this.isElementNode(); 386 } 387 388 getSearchQuery() { 389 return this.#searchQuery; 390 } 391 } 392 393 module.exports = Selection;