document-walker.js (5574B)
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 loader.lazyRequireGetter( 8 this, 9 "nodeFilterConstants", 10 "resource://devtools/shared/dom-node-filter-constants.js" 11 ); 12 loader.lazyRequireGetter( 13 this, 14 "standardTreeWalkerFilter", 15 "resource://devtools/server/actors/inspector/utils.js", 16 true 17 ); 18 19 // SKIP_TO_* arguments are used with the DocumentWalker, driving the strategy to use if 20 // the starting node is incompatible with the filter function of the walker. 21 const SKIP_TO_PARENT = "SKIP_TO_PARENT"; 22 const SKIP_TO_SIBLING = "SKIP_TO_SIBLING"; 23 24 class DocumentWalker { 25 /** 26 * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods. 27 * See inDeepTreeWalker for more information about the methods. 28 * 29 * @param {DOMNode} node 30 * @param {Window} rootWin 31 * @param {object} 32 * - {Function} filter 33 * A custom filter function Taking in a DOMNode and returning an Int. See 34 * WalkerActor.nodeFilter for an example. 35 * - {String} skipTo 36 * Either SKIP_TO_PARENT or SKIP_TO_SIBLING. If the provided node is not 37 * compatible with the filter function for this walker, try to find a compatible 38 * one either in the parents or in the siblings of the node. 39 * - {Boolean} showAnonymousContent 40 * Pass true to let the walker return and traverse anonymous content. 41 * When navigating host elements to which shadow DOM is attached, the light tree 42 * will be visible only to a walker with showAnonymousContent=false. The shadow 43 * tree will only be visible to a walker with showAnonymousContent=true. 44 */ 45 constructor( 46 node, 47 rootWin, 48 { 49 filter = standardTreeWalkerFilter, 50 skipTo = SKIP_TO_PARENT, 51 showAnonymousContent = true, 52 } = {} 53 ) { 54 if (Cu.isDeadWrapper(rootWin) || !rootWin.location) { 55 throw new Error("Got an invalid root window in DocumentWalker"); 56 } 57 58 this.walker = Cc[ 59 "@mozilla.org/inspector/deep-tree-walker;1" 60 ].createInstance(Ci.inIDeepTreeWalker); 61 this.walker.showAnonymousContent = showAnonymousContent; 62 this.walker.showSubDocuments = true; 63 this.walker.showDocumentsAsNodes = true; 64 this.walker.init(rootWin.document); 65 this.filter = filter; 66 67 // Make sure that the walker knows about the initial node (which could 68 // be skipped due to a filter). 69 this.walker.currentNode = this.getStartingNode(node, skipTo); 70 } 71 72 get currentNode() { 73 return this.walker.currentNode; 74 } 75 set currentNode(val) { 76 this.walker.currentNode = val; 77 } 78 79 parentNode() { 80 return this.walker.parentNode(); 81 } 82 83 nextNode() { 84 const node = this.walker.currentNode; 85 if (!node) { 86 return null; 87 } 88 89 let nextNode = this.walker.nextNode(); 90 while (nextNode && this.isSkippedNode(nextNode)) { 91 nextNode = this.walker.nextNode(); 92 } 93 94 return nextNode; 95 } 96 97 firstChild() { 98 if (!this.walker.currentNode) { 99 return null; 100 } 101 102 let firstChild = this.walker.firstChild(); 103 while (firstChild && this.isSkippedNode(firstChild)) { 104 firstChild = this.walker.nextSibling(); 105 } 106 107 return firstChild; 108 } 109 110 lastChild() { 111 if (!this.walker.currentNode) { 112 return null; 113 } 114 115 let lastChild = this.walker.lastChild(); 116 while (lastChild && this.isSkippedNode(lastChild)) { 117 lastChild = this.walker.previousSibling(); 118 } 119 120 return lastChild; 121 } 122 123 previousSibling() { 124 let node = this.walker.previousSibling(); 125 while (node && this.isSkippedNode(node)) { 126 node = this.walker.previousSibling(); 127 } 128 return node; 129 } 130 131 nextSibling() { 132 let node = this.walker.nextSibling(); 133 while (node && this.isSkippedNode(node)) { 134 node = this.walker.nextSibling(); 135 } 136 return node; 137 } 138 139 getStartingNode(node, skipTo) { 140 // Keep a reference on the starting node in case we can't find a node compatible with 141 // the filter. 142 const startingNode = node; 143 144 if (skipTo === SKIP_TO_PARENT) { 145 while (node && this.isSkippedNode(node)) { 146 node = node.parentNode; 147 } 148 } else if (skipTo === SKIP_TO_SIBLING) { 149 node = this.getClosestAcceptedSibling(node); 150 } 151 152 return node || startingNode; 153 } 154 155 /** 156 * Loop on all of the provided node siblings until finding one that is compliant with 157 * the filter function. 158 */ 159 getClosestAcceptedSibling(node) { 160 if (this.filter(node) === nodeFilterConstants.FILTER_ACCEPT) { 161 // node is already valid, return immediately. 162 return node; 163 } 164 165 // Loop on starting node siblings. 166 let previous = node; 167 let next = node; 168 while (previous || next) { 169 previous = previous?.previousSibling; 170 next = next?.nextSibling; 171 172 if ( 173 previous && 174 this.filter(previous) === nodeFilterConstants.FILTER_ACCEPT 175 ) { 176 // A valid node was found in the previous siblings of the node. 177 return previous; 178 } 179 180 if (next && this.filter(next) === nodeFilterConstants.FILTER_ACCEPT) { 181 // A valid node was found in the next siblings of the node. 182 return next; 183 } 184 } 185 186 return null; 187 } 188 189 isSkippedNode(node) { 190 return this.filter(node) === nodeFilterConstants.FILTER_SKIP; 191 } 192 } 193 194 exports.DocumentWalker = DocumentWalker; 195 exports.SKIP_TO_PARENT = SKIP_TO_PARENT; 196 exports.SKIP_TO_SIBLING = SKIP_TO_SIBLING;