summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/inspector/document-walker.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/inspector/document-walker.js')
-rw-r--r--devtools/server/actors/inspector/document-walker.js223
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;