diff options
Diffstat (limited to 'devtools/server/actors/inspector/document-walker.js')
-rw-r--r-- | devtools/server/actors/inspector/document-walker.js | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/devtools/server/actors/inspector/document-walker.js b/devtools/server/actors/inspector/document-walker.js new file mode 100644 index 0000000000..d44bae42d1 --- /dev/null +++ b/devtools/server/actors/inspector/document-walker.js @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Cc, Ci, Cu } = require("chrome"); + +loader.lazyRequireGetter( + this, + "isShadowRoot", + "devtools/shared/layout/utils", + true +); +loader.lazyRequireGetter( + this, + "nodeFilterConstants", + "devtools/shared/dom-node-filter-constants" +); +loader.lazyRequireGetter( + this, + "standardTreeWalkerFilter", + "devtools/server/actors/inspector/utils", + true +); + +// SKIP_TO_* arguments are used with the DocumentWalker, driving the strategy to use if +// the starting node is incompatible with the filter function of the walker. +const SKIP_TO_PARENT = "SKIP_TO_PARENT"; +const SKIP_TO_SIBLING = "SKIP_TO_SIBLING"; + +/** + * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods. + * See inDeepTreeWalker for more information about the methods. + * + * @param {DOMNode} node + * @param {Window} rootWin + * @param {Object} + * - {Number} whatToShow + * See nodeFilterConstants / inIDeepTreeWalker for options. + * - {Function} filter + * A custom filter function Taking in a DOMNode and returning an Int. See + * WalkerActor.nodeFilter for an example. + * - {String} skipTo + * Either SKIP_TO_PARENT or SKIP_TO_SIBLING. If the provided node is not + * compatible with the filter function for this walker, try to find a compatible + * one either in the parents or in the siblings of the node. + * - {Boolean} showAnonymousContent + * Pass true to let the walker return and traverse anonymous content. + * When navigating host elements to which shadow DOM is attached, the light tree + * will be visible only to a walker with showAnonymousContent=false. The shadow + * tree will only be visible to a walker with showAnonymousContent=true. + */ +function DocumentWalker( + node, + rootWin, + { + whatToShow = nodeFilterConstants.SHOW_ALL, + filter = standardTreeWalkerFilter, + skipTo = SKIP_TO_PARENT, + showAnonymousContent = true, + } = {} +) { + if (Cu.isDeadWrapper(rootWin) || !rootWin.location) { + throw new Error("Got an invalid root window in DocumentWalker"); + } + + this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance( + Ci.inIDeepTreeWalker + ); + this.walker.showAnonymousContent = showAnonymousContent; + this.walker.showSubDocuments = true; + this.walker.showDocumentsAsNodes = true; + this.walker.init(rootWin.document, whatToShow); + this.filter = filter; + + // Make sure that the walker knows about the initial node (which could + // be skipped due to a filter). + this.walker.currentNode = this.getStartingNode(node, skipTo); +} + +DocumentWalker.prototype = { + get whatToShow() { + return this.walker.whatToShow; + }, + get currentNode() { + return this.walker.currentNode; + }, + set currentNode(val) { + this.walker.currentNode = val; + }, + + parentNode: function() { + if (isShadowRoot(this.currentNode)) { + this.currentNode = this.currentNode.host; + return this.currentNode; + } + + const parentNode = this.currentNode.parentNode; + // deep-tree-walker currently does not return shadowRoot elements as parentNodes. + if (parentNode && isShadowRoot(parentNode)) { + this.currentNode = parentNode; + return this.currentNode; + } + return this.walker.parentNode(); + }, + + nextNode: function() { + const node = this.walker.currentNode; + if (!node) { + return null; + } + + let nextNode = this.walker.nextNode(); + while (nextNode && this.isSkippedNode(nextNode)) { + nextNode = this.walker.nextNode(); + } + + return nextNode; + }, + + firstChild: function() { + const node = this.walker.currentNode; + if (!node) { + return null; + } + + let firstChild = this.walker.firstChild(); + while (firstChild && this.isSkippedNode(firstChild)) { + firstChild = this.walker.nextSibling(); + } + + return firstChild; + }, + + lastChild: function() { + const node = this.walker.currentNode; + if (!node) { + return null; + } + + let lastChild = this.walker.lastChild(); + while (lastChild && this.isSkippedNode(lastChild)) { + lastChild = this.walker.previousSibling(); + } + + return lastChild; + }, + + previousSibling: function() { + let node = this.walker.previousSibling(); + while (node && this.isSkippedNode(node)) { + node = this.walker.previousSibling(); + } + return node; + }, + + nextSibling: function() { + let node = this.walker.nextSibling(); + while (node && this.isSkippedNode(node)) { + node = this.walker.nextSibling(); + } + return node; + }, + + getStartingNode: function(node, skipTo) { + // Keep a reference on the starting node in case we can't find a node compatible with + // the filter. + const startingNode = node; + + if (skipTo === SKIP_TO_PARENT) { + while (node && this.isSkippedNode(node)) { + node = node.parentNode; + } + } else if (skipTo === SKIP_TO_SIBLING) { + node = this.getClosestAcceptedSibling(node); + } + + return node || startingNode; + }, + + /** + * Loop on all of the provided node siblings until finding one that is compliant with + * the filter function. + */ + getClosestAcceptedSibling: function(node) { + if (this.filter(node) === nodeFilterConstants.FILTER_ACCEPT) { + // node is already valid, return immediately. + return node; + } + + // Loop on starting node siblings. + let previous = node; + let next = node; + while (previous || next) { + previous = previous?.previousSibling; + next = next?.nextSibling; + + if ( + previous && + this.filter(previous) === nodeFilterConstants.FILTER_ACCEPT + ) { + // A valid node was found in the previous siblings of the node. + return previous; + } + + if (next && this.filter(next) === nodeFilterConstants.FILTER_ACCEPT) { + // A valid node was found in the next siblings of the node. + return next; + } + } + + return null; + }, + + isSkippedNode: function(node) { + return this.filter(node) === nodeFilterConstants.FILTER_SKIP; + }, +}; + +exports.DocumentWalker = DocumentWalker; +exports.SKIP_TO_PARENT = SKIP_TO_PARENT; +exports.SKIP_TO_SIBLING = SKIP_TO_SIBLING; |