--- a/devtools/client/inspector/markup/test/actor_events_form.js
+++ b/devtools/client/inspector/markup/test/actor_events_form.js
@@ -6,17 +6,17 @@
// This test actor is used for testing the addition of custom form data
// on NodeActor. Custom form property is set when 'form' event is sent
// by NodeActor actor (see 'onNodeActorForm' method).
const EventEmitter = require("devtools/shared/event-emitter");
const {ActorClassWithSpec, Actor, FrontClassWithSpec, Front, generateActorSpec} =
require("devtools/shared/protocol");
-const {NodeActor} = require("devtools/server/actors/inspector/inspector");
+const {NodeActor} = require("devtools/server/actors/inspector/node-actor");
var eventsSpec = generateActorSpec({
typeName: "eventsFormActor",
methods: {
attach: {
request: {},
response: {}
--- a/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_textcontent_edit_01.js
@@ -2,17 +2,17 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test editing a node's text content
const TEST_URL = URL_ROOT + "doc_markup_edit.html";
-const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector/inspector");
+const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector/walker-actor");
add_task(function* () {
let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
info("Expanding all nodes");
yield inspector.markup.expandAll();
yield waitForMultipleChildrenUpdates(inspector);
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -13,17 +13,17 @@ const {
hasPseudoClassLock,
isNodeValid,
moveInfobar,
} = require("./utils/markup");
const {
setIgnoreLayoutChanges,
getCurrentZoom,
} = require("devtools/shared/layout/utils");
-const inspector = require("devtools/server/actors/inspector/inspector");
+const { getNodeDisplayName } = require("devtools/server/actors/inspector/utils");
const nodeConstants = require("devtools/shared/dom-node-constants");
// Note that the order of items in this array is important because it is used
// for drawing the BoxModelHighlighter's path elements correctly.
const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
// Width of boxmodelhighlighter guides
const GUIDE_STROKE_WIDTH = 1;
@@ -671,17 +671,17 @@ class BoxModelHighlighter extends AutoRe
if (!this.currentNode) {
return;
}
let {bindingElement: node, pseudo} =
getBindingElementAndPseudo(this.currentNode);
// Update the tag, id, classes, pseudo-classes and dimensions
- let displayName = inspector.getNodeDisplayName(node);
+ let displayName = getNodeDisplayName(node);
let id = node.id ? "#" + node.id : "";
let classList = (node.classList || []).length
? "." + [...node.classList].join(".")
: "";
let pseudos = this._getPseudoClasses(node).join("");
copy from devtools/server/actors/inspector/inspector.js
copy to devtools/server/actors/inspector/document-walker.js
--- a/devtools/server/actors/inspector/inspector.js
+++ b/devtools/server/actors/inspector/document-walker.js
@@ -1,3055 +1,24 @@
/* 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";
-/**
- * Here's the server side of the remote inspector.
- *
- * The WalkerActor is the client's view of the debuggee's DOM. It's gives
- * the client a tree of NodeActor objects.
- *
- * The walker presents the DOM tree mostly unmodified from the source DOM
- * tree, but with a few key differences:
- *
- * - Empty text nodes are ignored. This is pretty typical of developer
- * tools, but maybe we should reconsider that on the server side.
- * - iframes with documents loaded have the loaded document as the child,
- * the walker provides one big tree for the whole document tree.
- *
- * There are a few ways to get references to NodeActors:
- *
- * - When you first get a WalkerActor reference, it comes with a free
- * reference to the root document's node.
- * - Given a node, you can ask for children, siblings, and parents.
- * - You can issue querySelector and querySelectorAll requests to find
- * other elements.
- * - Requests that return arbitrary nodes from the tree (like querySelector
- * and querySelectorAll) will also return any nodes the client hasn't
- * seen in order to have a complete set of parents.
- *
- * Once you have a NodeFront, you should be able to answer a few questions
- * without further round trips, like the node's name, namespace/tagName,
- * attributes, etc. Other questions (like a text node's full nodeValue)
- * might require another round trip.
- *
- * The protocol guarantees that the client will always know the parent of
- * any node that is returned by the server. This means that some requests
- * (like querySelector) will include the extra nodes needed to satisfy this
- * requirement. The client keeps track of this parent relationship, so the
- * node fronts form a tree that is a subset of the actual DOM tree.
- *
- *
- * We maintain this guarantee to support the ability to release subtrees on
- * the client - when a node is disconnected from the DOM tree we want to be
- * able to free the client objects for all the children nodes.
- *
- * So to be able to answer "all the children of a given node that we have
- * seen on the client side", we guarantee that every time we've seen a node,
- * we connect it up through its parents.
- */
-
const {Cc, Ci, Cu} = require("chrome");
-const Services = require("Services");
-const protocol = require("devtools/shared/protocol");
-const {LongStringActor} = require("devtools/server/actors/string");
-const promise = require("promise");
-const defer = require("devtools/shared/defer");
-const {Task} = require("devtools/shared/task");
-const EventEmitter = require("devtools/shared/event-emitter");
-const InspectorUtils = require("InspectorUtils");
-const {walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
-const {nodeSpec, nodeListSpec} = require("devtools/shared/specs/node");
-
-loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-loader.lazyRequireGetter(this, "EyeDropper", "devtools/server/actors/highlighters/eye-dropper", true);
-loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
-loader.lazyRequireGetter(this, "PageStyleActor", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
-loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
-loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
-loader.lazyRequireGetter(this, "HighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "HighlighterEnvironment", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
-loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isShadowAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
-
-loader.lazyServiceGetter(this, "DOMParser",
- "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
-
-loader.lazyServiceGetter(this, "eventListenerService",
- "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
-
-const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
-const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
-const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
-const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
-const SVG_NS = "http://www.w3.org/2000/svg";
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const IMAGE_FETCHING_TIMEOUT = 500;
-
-// Minimum delay between two "new-mutations" events.
-const MUTATIONS_THROTTLING_DELAY = 100;
-// List of mutation types that should -not- be throttled.
-const IMMEDIATE_MUTATIONS = [
- "documentUnload",
- "frameLoad",
- "newRoot",
- "pseudoClassLock",
-];
+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";
-// The possible completions to a ':' with added score to give certain values
-// some preference.
-const PSEUDO_SELECTORS = [
- [":active", 1],
- [":hover", 1],
- [":focus", 1],
- [":visited", 0],
- [":link", 0],
- [":first-letter", 0],
- [":first-child", 2],
- [":before", 2],
- [":after", 2],
- [":lang(", 0],
- [":not(", 3],
- [":first-of-type", 0],
- [":last-of-type", 0],
- [":only-of-type", 0],
- [":only-child", 2],
- [":nth-child(", 3],
- [":nth-last-child(", 0],
- [":nth-of-type(", 0],
- [":nth-last-of-type(", 0],
- [":last-child", 2],
- [":root", 0],
- [":empty", 0],
- [":target", 0],
- [":enabled", 0],
- [":disabled", 0],
- [":checked", 1],
- ["::selection", 0]
-];
-
-var HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
- .__fx-devtools-hide-shortcut__ {
- visibility: hidden !important;
- }
-
- :-moz-devtools-highlighted {
- outline: 2px dashed #F06!important;
- outline-offset: -2px !important;
- }
-`);
-
-/**
- * We only send nodeValue up to a certain size by default. This stuff
- * controls that size.
- */
-exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
-var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
-
-exports.getValueSummaryLength = function () {
- return gValueSummaryLength;
-};
-
-exports.setValueSummaryLength = function (val) {
- gValueSummaryLength = val;
-};
-
-/**
- * Returns the properly cased version of the node's tag name, which can be
- * used when displaying said name in the UI.
- *
- * @param {Node} rawNode
- * Node for which we want the display name
- * @return {String}
- * Properly cased version of the node tag name
- */
-const getNodeDisplayName = function (rawNode) {
- if (rawNode.nodeName && !rawNode.localName) {
- // The localName & prefix APIs have been moved from the Node interface to the Element
- // interface. Use Node.nodeName as a fallback.
- return rawNode.nodeName;
- }
- return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
-};
-exports.getNodeDisplayName = getNodeDisplayName;
-
-/**
- * Server side of the node actor.
- */
-var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
- initialize: function (walker, node) {
- protocol.Actor.prototype.initialize.call(this, null);
- this.walker = walker;
- this.rawNode = node;
- this._eventParsers = new EventParsers().parsers;
-
- // Storing the original display of the node, to track changes when reflows
- // occur
- this.wasDisplayed = this.isDisplayed;
- },
-
- toString: function () {
- return "[NodeActor " + this.actorID + " for " +
- this.rawNode.toString() + "]";
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- isDocumentElement: function () {
- return this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.documentElement === this.rawNode;
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- if (this.mutationObserver) {
- if (!Cu.isDeadWrapper(this.mutationObserver)) {
- this.mutationObserver.disconnect();
- }
- this.mutationObserver = null;
- }
- this.rawNode = null;
- this.walker = null;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function (detail) {
- if (detail === "actorid") {
- return this.actorID;
- }
-
- let parentNode = this.walker.parentNode(this);
- let inlineTextChild = this.walker.inlineTextChild(this);
-
- let form = {
- actor: this.actorID,
- baseURI: this.rawNode.baseURI,
- parent: parentNode ? parentNode.actorID : undefined,
- nodeType: this.rawNode.nodeType,
- namespaceURI: this.rawNode.namespaceURI,
- nodeName: this.rawNode.nodeName,
- nodeValue: this.rawNode.nodeValue,
- displayName: getNodeDisplayName(this.rawNode),
- numChildren: this.numChildren,
- inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
-
- // doctype attributes
- name: this.rawNode.name,
- publicId: this.rawNode.publicId,
- systemId: this.rawNode.systemId,
-
- attrs: this.writeAttrs(),
- isBeforePseudoElement: this.isBeforePseudoElement,
- isAfterPseudoElement: this.isAfterPseudoElement,
- isAnonymous: isAnonymous(this.rawNode),
- isNativeAnonymous: isNativeAnonymous(this.rawNode),
- isXBLAnonymous: isXBLAnonymous(this.rawNode),
- isShadowAnonymous: isShadowAnonymous(this.rawNode),
- pseudoClassLocks: this.writePseudoClassLocks(),
-
- isDisplayed: this.isDisplayed,
- isInHTMLDocument: this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.contentType === "text/html",
- hasEventListeners: this._hasEventListeners,
- };
-
- if (this.isDocumentElement()) {
- form.isDocumentElement = true;
- }
-
- // Add an extra API for custom properties added by other
- // modules/extensions.
- form.setFormProperty = (name, value) => {
- if (!form.props) {
- form.props = {};
- }
- form.props[name] = value;
- };
-
- // Fire an event so, other modules can create its own properties
- // that should be passed to the client (within the form.props field).
- EventEmitter.emit(NodeActor, "form", {
- target: this,
- data: form
- });
-
- return form;
- },
-
- /**
- * Watch the given document node for mutations using the DOM observer
- * API.
- */
- watchDocument: function (callback) {
- let node = this.rawNode;
- // Create the observer on the node's actor. The node will make sure
- // the observer is cleaned up when the actor is released.
- let observer = new node.defaultView.MutationObserver(callback);
- observer.mergeAttributeRecords = true;
- observer.observe(node, {
- nativeAnonymousChildList: true,
- attributes: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true
- });
- this.mutationObserver = observer;
- },
-
- get isBeforePseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_before";
- },
-
- get isAfterPseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_after";
- },
-
- // Estimate the number of children that the walker will return without making
- // a call to children() if possible.
- get numChildren() {
- // For pseudo elements, childNodes.length returns 1, but the walker
- // will return 0.
- if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
- return 0;
- }
-
- let rawNode = this.rawNode;
- let numChildren = rawNode.childNodes.length;
- let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
- rawNode.ownerDocument.getAnonymousNodes(rawNode);
-
- let hasContentDocument = rawNode.contentDocument;
- let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
- if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
- // This might be an iframe with virtual children.
- numChildren = 1;
- }
-
- // Normal counting misses ::before/::after. Also, some anonymous children
- // may ultimately be skipped, so we have to consult with the walker.
- if (numChildren === 0 || hasAnonChildren) {
- numChildren = this.walker.children(this).nodes.length;
- }
-
- return numChildren;
- },
-
- get computedStyle() {
- return CssLogic.getComputedStyle(this.rawNode);
- },
-
- /**
- * Is the node's display computed style value other than "none"
- */
- get isDisplayed() {
- // Consider all non-element nodes as displayed.
- if (isNodeDead(this) ||
- this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
- this.isAfterPseudoElement ||
- this.isBeforePseudoElement) {
- return true;
- }
-
- let style = this.computedStyle;
- if (!style) {
- return true;
- }
-
- return style.display !== "none";
- },
-
- /**
- * Are there event listeners that are listening on this node? This method
- * uses all parsers registered via event-parsers.js.registerEventParser() to
- * check if there are any event listeners.
- */
- get _hasEventListeners() {
- let parsers = this._eventParsers;
- for (let [, {hasListeners}] of parsers) {
- try {
- if (hasListeners && hasListeners(this.rawNode)) {
- return true;
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
- return false;
- },
-
- writeAttrs: function () {
- if (!this.rawNode.attributes) {
- return undefined;
- }
-
- return [...this.rawNode.attributes].map(attr => {
- return {namespace: attr.namespace, name: attr.name, value: attr.value };
- });
- },
-
- writePseudoClassLocks: function () {
- if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return undefined;
- }
- let ret = undefined;
- for (let pseudo of PSEUDO_CLASSES) {
- if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
- ret = ret || [];
- ret.push(pseudo);
- }
- }
- return ret;
- },
-
- /**
- * Gets event listeners and adds their information to the events array.
- *
- * @param {Node} node
- * Node for which we are to get listeners.
- */
- getEventListeners: function (node) {
- let parsers = this._eventParsers;
- let dbg = this.parent().tabActor.makeDebugger();
- let listenerArray = [];
-
- for (let [, {getListeners, normalizeListener}] of parsers) {
- try {
- let listeners = getListeners(node);
-
- if (!listeners) {
- continue;
- }
-
- for (let listener of listeners) {
- if (normalizeListener) {
- listener.normalizeListener = normalizeListener;
- }
-
- this.processHandlerForEvent(node, listenerArray, dbg, listener);
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
-
- listenerArray.sort((a, b) => {
- return a.type.localeCompare(b.type);
- });
-
- return listenerArray;
- },
-
- /**
- * Process a handler
- *
- * @param {Node} node
- * The node for which we want information.
- * @param {Array} listenerArray
- * listenerArray contains all event objects that we have gathered
- * so far.
- * @param {Debugger} dbg
- * JSDebugger instance.
- * @param {Object} eventInfo
- * See event-parsers.js.registerEventParser() for a description of the
- * eventInfo object.
- *
- * @return {Array}
- * An array of objects where a typical object looks like this:
- * {
- * type: "click",
- * handler: function() { doSomething() },
- * origin: "http://www.mozilla.com",
- * searchString: 'onclick="doSomething()"',
- * tags: tags,
- * DOM0: true,
- * capturing: true,
- * hide: {
- * DOM0: true
- * },
- * native: false
- * }
- */
- processHandlerForEvent: function (node, listenerArray, dbg, listener) {
- let { handler } = listener;
- let global = Cu.getGlobalForObject(handler);
- let globalDO = dbg.addDebuggee(global);
- let listenerDO = globalDO.makeDebuggeeValue(handler);
-
- let { normalizeListener } = listener;
-
- if (normalizeListener) {
- listenerDO = normalizeListener(listenerDO, listener);
- }
-
- let { capturing } = listener;
- let dom0 = false;
- let functionSource = handler.toString();
- let hide = listener.hide || {};
- let line = 0;
- let native = false;
- let override = listener.override || {};
- let tags = listener.tags || "";
- let type = listener.type || "";
- let url = "";
-
- // If the listener is an object with a 'handleEvent' method, use that.
- if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
- let desc;
-
- while (!desc && listenerDO) {
- desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
- listenerDO = listenerDO.proto;
- }
-
- if (desc && desc.value) {
- listenerDO = desc.value;
- }
- }
-
- // If the listener is bound to a different context then we need to switch
- // to the bound function.
- if (listenerDO.isBoundFunction) {
- listenerDO = listenerDO.boundTargetFunction;
- }
-
- let { isArrowFunction, name, script, parameterNames } = listenerDO;
-
- if (script) {
- let scriptSource = script.source.text;
-
- // Scripts are provided via script tags. If it wasn't provided by a
- // script tag it must be a DOM0 event.
- if (script.source.element) {
- dom0 = script.source.element.class !== "HTMLScriptElement";
- } else {
- dom0 = false;
- }
-
- line = script.startLine;
- url = script.url;
-
- // Checking for the string "[native code]" is the only way at this point
- // to check for native code. Even if this provides a false positive then
- // grabbing the source code a second time is harmless.
- if (functionSource === "[object Object]" ||
- functionSource === "[object XULElement]" ||
- functionSource.includes("[native code]")) {
- functionSource =
- scriptSource.substr(script.sourceStart, script.sourceLength);
-
- // At this point the script looks like this:
- // () { ... }
- // We prefix this with "function" if it is not a fat arrow function.
- if (!isArrowFunction) {
- functionSource = "function " + functionSource;
- }
- }
- } else {
- // If the listener is a native one (provided by C++ code) then we have no
- // access to the script. We use the native flag to prevent showing the
- // debugger button because the script is not available.
- native = true;
- }
-
- // Fat arrow function text always contains the parameters. Function
- // parameters are often missing e.g. if Array.sort is used as a handler.
- // If they are missing we provide the parameters ourselves.
- if (parameterNames && parameterNames.length > 0) {
- let prefix = "function " + name + "()";
- let paramString = parameterNames.join(", ");
-
- if (functionSource.startsWith(prefix)) {
- functionSource = functionSource.substr(prefix.length);
-
- functionSource = `function ${name} (${paramString})${functionSource}`;
- }
- }
-
- // If the listener is native code we display the filename "[native code]."
- // This is the official string and should *not* be translated.
- let origin;
- if (native) {
- origin = "[native code]";
- } else {
- origin = url + ((dom0 || line === 0) ? "" : ":" + line);
- }
-
- let eventObj = {
- type: override.type || type,
- handler: override.handler || functionSource.trim(),
- origin: override.origin || origin,
- tags: override.tags || tags,
- DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
- capturing: typeof override.capturing !== "undefined" ?
- override.capturing : capturing,
- hide: typeof override.hide !== "undefined" ? override.hide : hide,
- native
- };
-
- // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
- // generated dynamically from e.g. an onclick="" attribute so the script
- // doesn't actually exist.
- if (native || dom0) {
- eventObj.hide.debugger = true;
- }
-
- listenerArray.push(eventObj);
-
- dbg.removeDebuggee(globalDO);
- },
-
- /**
- * Returns a LongStringActor with the node's value.
- */
- getNodeValue: function () {
- return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
- },
-
- /**
- * Set the node's value to a given string.
- */
- setNodeValue: function (value) {
- this.rawNode.nodeValue = value;
- },
-
- /**
- * Get a unique selector string for this node.
- */
- getUniqueSelector: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return findCssSelector(this.rawNode);
- },
-
- /**
- * Get the full CSS path for this node.
- *
- * @return {String} A CSS selector with a part for the node and each of its ancestors.
- */
- getCssPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getCssPath(this.rawNode);
- },
-
- /**
- * Get the XPath for this node.
- *
- * @return {String} The XPath for finding this node on the page.
- */
- getXPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getXPath(this.rawNode);
- },
-
- /**
- * Scroll the selected node into view.
- */
- scrollIntoView: function () {
- this.rawNode.scrollIntoView(true);
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageData: function (maxDim) {
- return imageToImageData(this.rawNode, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Get all event listeners that are listening on this node.
- */
- getEventListenerInfo: function () {
- let node = this.rawNode;
-
- if (this.rawNode.nodeName.toLowerCase() === "html") {
- let winListeners = this.getEventListeners(node.ownerGlobal) || [];
- let docElementListeners = this.getEventListeners(node) || [];
- let docListeners = this.getEventListeners(node.parentNode) || [];
-
- return [...winListeners, ...docElementListeners, ...docListeners];
- }
- return this.getEventListeners(node);
- },
-
- /**
- * Modify a node's attributes. Passed an array of modifications
- * similar in format to "attributes" mutations.
- * {
- * attributeName: <string>
- * attributeNamespace: <optional string>
- * newValue: <optional string> - If null or undefined, the attribute
- * will be removed.
- * }
- *
- * Returns when the modifications have been made. Mutations will
- * be queued for any changes made.
- */
- modifyAttributes: function (modifications) {
- let rawNode = this.rawNode;
- for (let change of modifications) {
- if (change.newValue == null) {
- if (change.attributeNamespace) {
- rawNode.removeAttributeNS(change.attributeNamespace,
- change.attributeName);
- } else {
- rawNode.removeAttribute(change.attributeName);
- }
- } else if (change.attributeNamespace) {
- rawNode.setAttributeNS(change.attributeNamespace, change.attributeName,
- change.newValue);
- } else {
- rawNode.setAttribute(change.attributeName, change.newValue);
- }
- }
- },
-
- /**
- * Given the font and fill style, get the image data of a canvas with the
- * preview text and font.
- * Returns an imageData object with the actual data being a LongStringActor
- * and the width of the text as a string.
- * The image data is transmitted as a base64 encoded png data-uri.
- */
- getFontFamilyDataURL: function (font, fillStyle = "black") {
- let doc = this.rawNode.ownerDocument;
- let options = {
- previewText: FONT_FAMILY_PREVIEW_TEXT,
- previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
- fillStyle: fillStyle
- };
- let { dataURL, size } = getFontPreviewData(font, doc, options);
-
- return { data: LongStringActor(this.conn, dataURL), size: size };
- },
-
- /**
- * Finds the computed background color of the closest parent with
- * a set background color.
- * Returns a string with the background color of the form
- * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
- * background color is found.
- */
- getClosestBackgroundColor: function () {
- let current = this.rawNode;
- while (current) {
- let computedStyle = CssLogic.getComputedStyle(current);
- let currentStyle = computedStyle.getPropertyValue("background-color");
- if (colorUtils.isValidCSSColor(currentStyle)) {
- let currentCssColor = new colorUtils.CssColor(currentStyle);
- if (!currentCssColor.isTransparent()) {
- return currentCssColor.rgba;
- }
- }
- current = current.parentNode;
- }
- return "rgba(255, 255, 255, 1)";
- }
-});
-
-/**
- * Server side of a node list as returned by querySelectorAll()
- */
-var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
- typeName: "domnodelist",
-
- initialize: function (walker, nodeList) {
- protocol.Actor.prototype.initialize.call(this);
- this.walker = walker;
- this.nodeList = nodeList || [];
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- /**
- * Items returned by this actor should belong to the parent walker.
- */
- marshallPool: function () {
- return this.walker;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- length: this.nodeList ? this.nodeList.length : 0
- };
- },
-
- /**
- * Get a single node from the node list.
- */
- item: function (index) {
- return this.walker.attachElement(this.nodeList[index]);
- },
-
- /**
- * Get a range of the items from the node list.
- */
- items: function (start = 0, end = this.nodeList.length) {
- let items = Array.prototype.slice.call(this.nodeList, start, end)
- .map(item => this.walker._ref(item));
- return this.walker.attachElements(items);
- },
-
- release: function () {}
-});
-
-/**
- * Server side of the DOM walker.
- */
-var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
- /**
- * Create the WalkerActor
- * @param DebuggerServerConnection conn
- * The server connection.
- */
- initialize: function (conn, tabActor, options) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
- this.rootWin = tabActor.window;
- this.rootDoc = this.rootWin.document;
- this._refMap = new Map();
- this._pendingMutations = [];
- this._activePseudoClassLocks = new Set();
- this.showAllAnonymousContent = options.showAllAnonymousContent;
-
- this.walkerSearch = new WalkerSearch(this);
-
- // Nodes which have been removed from the client's known
- // ownership tree are considered "orphaned", and stored in
- // this set.
- this._orphaned = new Set();
-
- // The client can tell the walker that it is interested in a node
- // even when it is orphaned with the `retainNode` method. This
- // list contains orphaned nodes that were so retained.
- this._retainedOrphans = new Set();
-
- this.onMutations = this.onMutations.bind(this);
- this.onFrameLoad = this.onFrameLoad.bind(this);
- this.onFrameUnload = this.onFrameUnload.bind(this);
- this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
- MUTATIONS_THROTTLING_DELAY);
-
- tabActor.on("will-navigate", this.onFrameUnload);
- tabActor.on("window-ready", this.onFrameLoad);
-
- // Ensure that the root document node actor is ready and
- // managed.
- this.rootNode = this.document();
-
- this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
- this._onReflows = this._onReflows.bind(this);
- this.layoutChangeObserver.on("reflows", this._onReflows);
- this._onResize = this._onResize.bind(this);
- this.layoutChangeObserver.on("resize", this._onResize);
-
- this._onEventListenerChange = this._onEventListenerChange.bind(this);
- eventListenerService.addListenerChangeListener(this._onEventListenerChange);
- },
-
- /**
- * Callback for eventListenerService.addListenerChangeListener
- * @param nsISimpleEnumerator changesEnum
- * enumerator of nsIEventListenerChange
- */
- _onEventListenerChange: function (changesEnum) {
- let changes = changesEnum.enumerate();
- while (changes.hasMoreElements()) {
- let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
- let target = current.target;
-
- if (this._refMap.has(target)) {
- let actor = this.getNode(target);
- let mutation = {
- type: "events",
- target: actor.actorID,
- hasEventListeners: actor._hasEventListeners
- };
- this.queueMutation(mutation);
- }
- }
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- root: this.rootNode.form(),
- traits: {
- // FF42+ Inspector starts managing the Walker, while the inspector also
- // starts cleaning itself up automatically on client disconnection.
- // So that there is no need to manually release the walker anymore.
- autoReleased: true,
- // XXX: It seems silly that we need to tell the front which capabilities
- // its actor has in this way when the target can use actorHasMethod. If
- // this was ported to the protocol (Bug 1157048) we could call that
- // inside of custom front methods and not need to do traits for this.
- multiFrameQuerySelectorAll: true,
- textSearch: true,
- }
- };
- },
-
- toString: function () {
- return "[WalkerActor " + this.actorID + "]";
- },
-
- getDocumentWalker: function (node, whatToShow, skipTo) {
- // Allow native anon content (like <video> controls) if preffed on
- let nodeFilter = this.showAllAnonymousContent
- ? allAnonymousContentTreeWalkerFilter
- : standardTreeWalkerFilter;
- return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter, skipTo);
- },
-
- destroy: function () {
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
- protocol.Actor.prototype.destroy.call(this);
- try {
- this.clearPseudoClassLocks();
- this._activePseudoClassLocks = null;
-
- this._hoveredNode = null;
- this.rootWin = null;
- this.rootDoc = null;
- this.rootNode = null;
- this.layoutHelpers = null;
- this._orphaned = null;
- this._retainedOrphans = null;
- this._refMap = null;
-
- this.tabActor.off("will-navigate", this.onFrameUnload);
- this.tabActor.off("window-ready", this.onFrameLoad);
-
- this.onFrameLoad = null;
- this.onFrameUnload = null;
-
- this.walkerSearch.destroy();
-
- this.layoutChangeObserver.off("reflows", this._onReflows);
- this.layoutChangeObserver.off("resize", this._onResize);
- this.layoutChangeObserver = null;
- releaseLayoutChangesObserver(this.tabActor);
-
- eventListenerService.removeListenerChangeListener(
- this._onEventListenerChange);
-
- this.onMutations = null;
-
- this.layoutActor = null;
- this.tabActor = null;
-
- this.emit("destroyed");
- } catch (e) {
- console.error(e);
- }
- },
-
- release: function () {},
-
- unmanage: function (actor) {
- if (actor instanceof NodeActor) {
- if (this._activePseudoClassLocks &&
- this._activePseudoClassLocks.has(actor)) {
- this.clearPseudoClassLocks(actor);
- }
- this._refMap.delete(actor.rawNode);
- }
- protocol.Actor.prototype.unmanage.call(this, actor);
- },
-
- /**
- * Determine if the walker has come across this DOM node before.
- * @param {DOMNode} rawNode
- * @return {Boolean}
- */
- hasNode: function (rawNode) {
- return this._refMap.has(rawNode);
- },
-
- /**
- * If the walker has come across this DOM node before, then get the
- * corresponding node actor.
- * @param {DOMNode} rawNode
- * @return {NodeActor}
- */
- getNode: function (rawNode) {
- return this._refMap.get(rawNode);
- },
-
- _ref: function (node) {
- let actor = this.getNode(node);
- if (actor) {
- return actor;
- }
-
- actor = new NodeActor(this, node);
-
- // Add the node actor as a child of this walker actor, assigning
- // it an actorID.
- this.manage(actor);
- this._refMap.set(node, actor);
-
- if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
- actor.watchDocument(this.onMutations);
- }
- return actor;
- },
-
- _onReflows: function (reflows) {
- // Going through the nodes the walker knows about, see which ones have
- // had their display changed and send a display-change event if any
- let changes = [];
- for (let [node, actor] of this._refMap) {
- if (Cu.isDeadWrapper(node)) {
- continue;
- }
-
- let isDisplayed = actor.isDisplayed;
- if (isDisplayed !== actor.wasDisplayed) {
- changes.push(actor);
- // Updating the original value
- actor.wasDisplayed = isDisplayed;
- }
- }
-
- if (changes.length) {
- this.emit("display-change", changes);
- }
- },
-
- /**
- * When the browser window gets resized, relay the event to the front.
- */
- _onResize: function () {
- this.emit("resize");
- },
-
- /**
- * This is kept for backward-compatibility reasons with older remote targets.
- * Targets prior to bug 916443.
- *
- * pick/cancelPick are used to pick a node on click on the content
- * document. But in their implementation prior to bug 916443, they don't allow
- * highlighting on hover.
- * The client-side now uses the highlighter actor's pick and cancelPick
- * methods instead. The client-side uses the the highlightable trait found in
- * the root actor to determine which version of pick to use.
- *
- * As for highlight, the new highlighter actor is used instead of the walker's
- * highlight method. Same here though, the client-side uses the highlightable
- * trait to dertermine which to use.
- *
- * Keeping these actor methods for now allows newer client-side debuggers to
- * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
- */
- pick: function () {},
- cancelPick: function () {},
- highlight: function (node) {},
-
- /**
- * Ensures that the node is attached and it can be accessed from the root.
- *
- * @param {(Node|NodeActor)} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNode type.
- */
- attachElement: function (node) {
- let { nodes, newParents } = this.attachElements([node]);
- return {
- node: nodes[0],
- newParents: newParents
- };
- },
-
- /**
- * Ensures that the nodes are attached and they can be accessed from the root.
- *
- * @param {(Node[]|NodeActor[])} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNodeArray type.
- */
- attachElements: function (nodes) {
- let nodeActors = [];
- let newParents = new Set();
- for (let node of nodes) {
- if (!(node instanceof NodeActor)) {
- // If an anonymous node was passed in and we aren't supposed to know
- // about it, then consult with the document walker as the source of
- // truth about which elements exist.
- if (!this.showAllAnonymousContent && isAnonymous(node)) {
- node = this.getDocumentWalker(node).currentNode;
- }
-
- node = this._ref(node);
- }
-
- this.ensurePathToRoot(node, newParents);
- // If nodes may be an array of raw nodes, we're sure to only have
- // NodeActors with the following array.
- nodeActors.push(node);
- }
-
- return {
- nodes: nodeActors,
- newParents: [...newParents]
- };
- },
-
- /**
- * Return the document node that contains the given node,
- * or the root node if no node is specified.
- * @param NodeActor node
- * The node whose document is needed, or null to
- * return the root.
- */
- document: function (node) {
- let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
- return this._ref(doc);
- },
-
- /**
- * Return the documentElement for the document containing the
- * given node.
- * @param NodeActor node
- * The node whose documentElement is requested, or null
- * to use the root document.
- */
- documentElement: function (node) {
- let elt = isNodeDead(node)
- ? this.rootDoc.documentElement
- : nodeDocument(node.rawNode).documentElement;
- return this._ref(elt);
- },
-
- /**
- * Return all parents of the given node, ordered from immediate parent
- * to root.
- * @param NodeActor node
- * The node whose parents are requested.
- * @param object options
- * Named options, including:
- * `sameDocument`: If true, parents will be restricted to the same
- * document as the node.
- * `sameTypeRootTreeItem`: If true, this will not traverse across
- * different types of docshells.
- */
- parents: function (node, options = {}) {
- if (isNodeDead(node)) {
- return [];
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let parents = [];
- let cur;
- while ((cur = walker.parentNode())) {
- if (options.sameDocument &&
- nodeDocument(cur) != nodeDocument(node.rawNode)) {
- break;
- }
-
- if (options.sameTypeRootTreeItem &&
- nodeDocshell(cur).sameTypeRootTreeItem !=
- nodeDocshell(node.rawNode).sameTypeRootTreeItem) {
- break;
- }
-
- parents.push(this._ref(cur));
- }
- return parents;
- },
-
- parentNode: function (node) {
- let walker = this.getDocumentWalker(node.rawNode);
- let parent = walker.parentNode();
- if (parent) {
- return this._ref(parent);
- }
- return null;
- },
-
- /**
- * If the given NodeActor only has a single text node as a child with a text
- * content small enough to be inlined, return that child's NodeActor.
- *
- * @param NodeActor node
- */
- inlineTextChild: function (node) {
- // Quick checks to prevent creating a new walker if possible.
- if (node.isBeforePseudoElement ||
- node.isAfterPseudoElement ||
- node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
- node.rawNode.children.length > 0) {
- return undefined;
- }
-
- let docWalker = this.getDocumentWalker(node.rawNode);
- let firstChild = docWalker.firstChild();
-
- // Bail out if:
- // - more than one child
- // - unique child is not a text node
- // - unique child is a text node, but is too long to be inlined
- if (!firstChild ||
- docWalker.nextSibling() ||
- firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
- firstChild.nodeValue.length > gValueSummaryLength
- ) {
- return undefined;
- }
-
- return this._ref(firstChild);
- },
-
- /**
- * Mark a node as 'retained'.
- *
- * A retained node is not released when `releaseNode` is called on its
- * parent, or when a parent is released with the `cleanup` option to
- * `getMutations`.
- *
- * When a retained node's parent is released, a retained mode is added to
- * the walker's "retained orphans" list.
- *
- * Retained nodes can be deleted by providing the `force` option to
- * `releaseNode`. They will also be released when their document
- * has been destroyed.
- *
- * Retaining a node makes no promise about its children; They can
- * still be removed by normal means.
- */
- retainNode: function (node) {
- node.retained = true;
- },
-
- /**
- * Remove the 'retained' mark from a node. If the node was a
- * retained orphan, release it.
- */
- unretainNode: function (node) {
- node.retained = false;
- if (this._retainedOrphans.has(node)) {
- this._retainedOrphans.delete(node);
- this.releaseNode(node);
- }
- },
-
- /**
- * Release actors for a node and all child nodes.
- */
- releaseNode: function (node, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- if (node.retained && !options.force) {
- this._retainedOrphans.add(node);
- return;
- }
-
- if (node.retained) {
- // Forcing a retained node to go away.
- this._retainedOrphans.delete(node);
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
-
- let child = walker.firstChild();
- while (child) {
- let childActor = this.getNode(child);
- if (childActor) {
- this.releaseNode(childActor, options);
- }
- child = walker.nextSibling();
- }
-
- node.destroy();
- },
-
- /**
- * Add any nodes between `node` and the walker's root node that have not
- * yet been seen by the client.
- */
- ensurePathToRoot: function (node, newParents = new Set()) {
- if (!node) {
- return newParents;
- }
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let parent = this.getNode(cur);
- if (!parent) {
- // This parent didn't exist, so hasn't been seen by the client yet.
- newParents.add(this._ref(cur));
- } else {
- // This parent did exist, so the client knows about it.
- return newParents;
- }
- }
- return newParents;
- },
-
- /**
- * Return children of the given node. By default this method will return
- * all children of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- children: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- if (options.center && options.start) {
- throw Error("Can't specify both 'center' and 'start' options.");
- }
- let maxNodes = options.maxNodes || -1;
- if (maxNodes == -1) {
- maxNodes = Number.MAX_VALUE;
- }
-
- // We're going to create a few document walkers with the same filter,
- // make it easier.
- let getFilteredWalker = documentWalkerNode => {
- let { whatToShow } = options;
- // Use SKIP_TO_SIBLING to force the walker to use a sibling of the provided node
- // in case this one is incompatible with the walker's filter function.
- return this.getDocumentWalker(documentWalkerNode, whatToShow, SKIP_TO_SIBLING);
- };
-
- // Need to know the first and last child.
- let rawNode = node.rawNode;
- let firstChild = getFilteredWalker(rawNode).firstChild();
- let lastChild = getFilteredWalker(rawNode).lastChild();
-
- if (!firstChild) {
- // No children, we're done.
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let start;
- if (options.center) {
- start = options.center.rawNode;
- } else if (options.start) {
- start = options.start.rawNode;
- } else {
- start = firstChild;
- }
-
- let nodes = [];
-
- // Start by reading backward from the starting point if we're centering...
- let backwardWalker = getFilteredWalker(start);
- if (backwardWalker.currentNode != firstChild && options.center) {
- backwardWalker.previousSibling();
- let backwardCount = Math.floor(maxNodes / 2);
- let backwardNodes = this._readBackward(backwardWalker, backwardCount);
- nodes = backwardNodes;
- }
-
- // Then read forward by any slack left in the max children...
- let forwardWalker = getFilteredWalker(start);
- let forwardCount = maxNodes - nodes.length;
- nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
-
- // If there's any room left, it means we've run all the way to the end.
- // If we're centering, check if there are more items to read at the front.
- let remaining = maxNodes - nodes.length;
- if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
- let firstNodes = this._readBackward(backwardWalker, remaining);
-
- // Then put it all back together.
- nodes = firstNodes.concat(nodes);
- }
-
- return {
- hasFirst: nodes[0].rawNode == firstChild,
- hasLast: nodes[nodes.length - 1].rawNode == lastChild,
- nodes: nodes
- };
- },
-
- /**
- * Return siblings of the given node. By default this method will return
- * all siblings of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * If `start` or `center` are not specified, this method will center on the
- * node whose siblings are requested.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- siblings: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow)
- .parentNode();
- if (!parentNode) {
- return {
- hasFirst: true,
- hasLast: true,
- nodes: [node]
- };
- }
-
- if (!(options.start || options.center)) {
- options.center = node;
- }
-
- return this.children(this._ref(parentNode), options);
- },
-
- /**
- * Get the next sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- nextSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.nextSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Get the previous sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- previousSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.previousSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Helper function for the `children` method: Read forward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readForward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.nextSibling();
- } while (node && --count);
- return ret;
- },
-
- /**
- * Helper function for the `children` method: Read backward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readBackward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.previousSibling();
- } while (node && --count);
- ret.reverse();
- return ret;
- },
-
- /**
- * Return the first node in the document that matches the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelector: function (baseNode, selector) {
- if (isNodeDead(baseNode)) {
- return {};
- }
-
- let node = baseNode.rawNode.querySelector(selector);
- if (!node) {
- return {};
- }
-
- return this.attachElement(node);
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelectorAll: function (baseNode, selector) {
- let nodeList = null;
-
- try {
- nodeList = baseNode.rawNode.querySelectorAll(selector);
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
-
- return new NodeListActor(this, nodeList);
- },
-
- /**
- * Get a list of nodes that match the given selector in all known frames of
- * the current content page.
- * @param {String} selector.
- * @return {Array}
- */
- _multiFrameQuerySelectorAll: function (selector) {
- let nodes = [];
-
- for (let {document} of this.tabActor.windows) {
- try {
- nodes = [...nodes, ...document.querySelectorAll(selector)];
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
- }
-
- return nodes;
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector in all
- * frames of the current content page.
- * @param {String} selector
- */
- multiFrameQuerySelectorAll: function (selector) {
- return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector));
- },
-
- /**
- * Search the document for a given string.
- * Results will be searched with the walker-search module (searches through
- * tag names, attribute names and values, and text contents).
- *
- * @returns {searchresult}
- * - {NodeList} list
- * - {Array<Object>} metadata. Extra information with indices that
- * match up with node list.
- */
- search: function (query) {
- let results = this.walkerSearch.search(query);
- let nodeList = new NodeListActor(this, results.map(r => r.node));
-
- return {
- list: nodeList,
- metadata: []
- };
- },
-
- /**
- * Returns a list of matching results for CSS selector autocompletion.
- *
- * @param string query
- * The selector query being completed
- * @param string completing
- * The exact token being completed out of the query
- * @param string selectorState
- * One of "pseudo", "id", "tag", "class", "null"
- */
- getSuggestionsForQuery: function (query, completing, selectorState) {
- let sugs = {
- classes: new Map(),
- tags: new Map(),
- ids: new Map()
- };
- let result = [];
- let nodes = null;
- // Filtering and sorting the results so that protocol transfer is miminal.
- switch (selectorState) {
- case "pseudo":
- result = PSEUDO_SELECTORS.filter(item => {
- return item[0].startsWith(":" + completing);
- });
- break;
-
- case "class":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[class]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- if (className.startsWith(completing)) {
- result.push(["." + CSS.escape(className), count, selectorState]);
- }
- }
- break;
-
- case "id":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[id]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- }
- for (let [id, count] of sugs.ids) {
- if (id.startsWith(completing) && id !== "") {
- result.push(["#" + CSS.escape(id), count, selectorState]);
- }
- }
- break;
-
- case "tag":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("*");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- }
- for (let [tag, count] of sugs.tags) {
- if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
- result.push([tag, count, selectorState]);
- }
- }
-
- // For state 'tag' (no preceding # or .) and when there's no query (i.e.
- // only one word) then search for the matching classes and ids
- if (!query) {
- result = [
- ...result,
- ...this.getSuggestionsForQuery(null, completing, "class")
- .suggestions,
- ...this.getSuggestionsForQuery(null, completing, "id")
- .suggestions
- ];
- }
-
- break;
-
- case "null":
- nodes = this._multiFrameQuerySelectorAll(query);
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- for (let [tag, count] of sugs.tags) {
- tag && result.push([tag, count]);
- }
- for (let [id, count] of sugs.ids) {
- id && result.push(["#" + id, count]);
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- className && result.push(["." + className, count]);
- }
- }
-
- // Sort by count (desc) and name (asc)
- result = result.sort((a, b) => {
- // Computed a sortable string with first the inverted count, then the name
- let sortA = (10000 - a[1]) + a[0];
- let sortB = (10000 - b[1]) + b[0];
-
- // Prefixing ids, classes and tags, to group results
- let firstA = a[0].substring(0, 1);
- let firstB = b[0].substring(0, 1);
-
- if (firstA === "#") {
- sortA = "2" + sortA;
- } else if (firstA === ".") {
- sortA = "1" + sortA;
- } else {
- sortA = "0" + sortA;
- }
-
- if (firstB === "#") {
- sortB = "2" + sortB;
- } else if (firstB === ".") {
- sortB = "1" + sortB;
- } else {
- sortB = "0" + sortB;
- }
-
- // String compare
- return sortA.localeCompare(sortB);
- });
-
- result.slice(0, 25);
-
- return {
- query: query,
- suggestions: result
- };
- },
-
- /**
- * Add a pseudo-class lock to a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be added
- * to parent nodes.
- * `enabled`: False if the pseudo-class should be locked
- * to 'off'. Defaults to true.
- *
- * @returns An empty packet. A "pseudoClassLock" mutation will
- * be queued for any changed nodes.
- */
- addPseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- // There can be only one node locked per pseudo, so dismiss all existing
- // ones
- for (let locked of this._activePseudoClassLocks) {
- if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- let enabled = options.enabled === undefined ||
- options.enabled;
- this._addPseudoClassLock(node, pseudo, enabled);
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._addPseudoClassLock(curNode, pseudo, enabled);
- }
- },
-
- _queuePseudoClassMutation: function (node) {
- this.queueMutation({
- target: node.actorID,
- type: "pseudoClassLock",
- pseudoClassLocks: node.writePseudoClassLocks()
- });
- },
-
- _addPseudoClassLock: function (node, pseudo, enabled) {
- if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
- this._activePseudoClassLocks.add(node);
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- hideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
- node.rawNode.classList.add(HIDDEN_CLASS);
- },
-
- unhideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- node.rawNode.classList.remove(HIDDEN_CLASS);
- },
-
- /**
- * Remove a pseudo-class lock from a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be removed
- * from parent nodes.
- *
- * @returns An empty response. "pseudoClassLock" mutations
- * will be emitted for any changed nodes.
- */
- removePseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- this._removePseudoClassLock(node, pseudo);
-
- // Remove pseudo class for children as we don't want to allow
- // turning it on for some childs without setting it on some parents
- for (let locked of this._activePseudoClassLocks) {
- if (node.rawNode.contains(locked.rawNode) &&
- InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._removePseudoClassLock(curNode, pseudo);
- }
- },
-
- _removePseudoClassLock: function (node, pseudo) {
- if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
- if (!node.writePseudoClassLocks()) {
- this._activePseudoClassLocks.delete(node);
- }
-
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- /**
- * Clear all the pseudo-classes on a given node or all nodes.
- * @param {NodeActor} node Optional node to clear pseudo-classes on
- */
- clearPseudoClassLocks: function (node) {
- if (node && isNodeDead(node)) {
- return;
- }
-
- if (node) {
- InspectorUtils.clearPseudoClassLocks(node.rawNode);
- this._activePseudoClassLocks.delete(node);
- this._queuePseudoClassMutation(node);
- } else {
- for (let locked of this._activePseudoClassLocks) {
- InspectorUtils.clearPseudoClassLocks(locked.rawNode);
- this._activePseudoClassLocks.delete(locked);
- this._queuePseudoClassMutation(locked);
- }
- }
- },
-
- /**
- * Get a node's innerHTML property.
- */
- innerHTML: function (node) {
- let html = "";
- if (!isNodeDead(node)) {
- html = node.rawNode.innerHTML;
- }
- return LongStringActor(this.conn, html);
- },
-
- /**
- * Set a node's innerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setInnerHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let rawNode = node.rawNode;
- if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) {
- throw new Error("Can only change innerHTML to element nodes");
- }
- // eslint-disable-next-line no-unsanitized/property
- rawNode.innerHTML = value;
- },
-
- /**
- * Get a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- */
- outerHTML: function (node) {
- let outerHTML = "";
- if (!isNodeDead(node)) {
- outerHTML = node.rawNode.outerHTML;
- }
- return LongStringActor(this.conn, outerHTML);
- },
-
- /**
- * Set a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setOuterHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let parsedDOM = DOMParser.parseFromString(value, "text/html");
- let rawNode = node.rawNode;
- let parentNode = rawNode.parentNode;
-
- // Special case for head and body. Setting document.body.outerHTML
- // creates an extra <head> tag, and document.head.outerHTML creates
- // an extra <body>. So instead we will call replaceChild with the
- // parsed DOM, assuming that they aren't trying to set both tags at once.
- if (rawNode.tagName === "BODY") {
- if (parsedDOM.head.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.body, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (rawNode.tagName === "HEAD") {
- if (parsedDOM.body.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.head, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (node.isDocumentElement()) {
- // Unable to set outerHTML on the document element. Fall back by
- // setting attributes manually, then replace the body and head elements.
- let finalAttributeModifications = [];
- let attributeModifications = {};
- for (let attribute of rawNode.attributes) {
- attributeModifications[attribute.name] = null;
- }
- for (let attribute of parsedDOM.documentElement.attributes) {
- attributeModifications[attribute.name] = attribute.value;
- }
- for (let key in attributeModifications) {
- finalAttributeModifications.push({
- attributeName: key,
- newValue: attributeModifications[key]
- });
- }
- node.modifyAttributes(finalAttributeModifications);
- rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
- rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- },
-
- /**
- * Insert adjacent HTML to a node.
- *
- * @param {Node} node
- * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
- * "afterEnd" (see Element.insertAdjacentHTML).
- * @param {string} value The HTML content.
- */
- insertAdjacentHTML: function (node, position, value) {
- if (isNodeDead(node)) {
- return {node: [], newParents: []};
- }
-
- let rawNode = node.rawNode;
- let isInsertAsSibling = position === "beforeBegin" ||
- position === "afterEnd";
-
- // Don't insert anything adjacent to the document element.
- if (isInsertAsSibling && node.isDocumentElement()) {
- throw new Error("Can't insert adjacent element to the root.");
- }
-
- let rawParentNode = rawNode.parentNode;
- if (!rawParentNode && isInsertAsSibling) {
- throw new Error("Can't insert as sibling without parent node.");
- }
-
- // We can't use insertAdjacentHTML, because we want to return the nodes
- // being created (so the front can remove them if the user undoes
- // the change). So instead, use Range.createContextualFragment().
- let range = rawNode.ownerDocument.createRange();
- if (position === "beforeBegin" || position === "afterEnd") {
- range.selectNode(rawNode);
- } else {
- range.selectNodeContents(rawNode);
- }
- let docFrag = range.createContextualFragment(value);
- let newRawNodes = Array.from(docFrag.childNodes);
- switch (position) {
- case "beforeBegin":
- rawParentNode.insertBefore(docFrag, rawNode);
- break;
- case "afterEnd":
- // Note: if the second argument is null, rawParentNode.insertBefore
- // behaves like rawParentNode.appendChild.
- rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
- break;
- case "afterBegin":
- rawNode.insertBefore(docFrag, rawNode.firstChild);
- break;
- case "beforeEnd":
- rawNode.appendChild(docFrag);
- break;
- default:
- throw new Error("Invalid position value. Must be either " +
- "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'.");
- }
-
- return this.attachElements(newRawNodes);
- },
-
- /**
- * Duplicate a specified node
- *
- * @param {NodeActor} node The node to duplicate.
- */
- duplicateNode: function ({rawNode}) {
- let clonedNode = rawNode.cloneNode(true);
- rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
- },
-
- /**
- * Test whether a node is a document or a document element.
- *
- * @param {NodeActor} node The node to remove.
- * @return {boolean} True if the node is a document or a document element.
- */
- isDocumentOrDocumentElementNode: function (node) {
- return ((node.rawNode.ownerDocument &&
- node.rawNode.ownerDocument.documentElement === this.rawNode) ||
- node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
- },
-
- /**
- * Removes a node from its parent node.
- *
- * @param {NodeActor} node The node to remove.
- * @returns The node's nextSibling before it was removed.
- */
- removeNode: function (node) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes.");
- }
-
- let nextSibling = this.nextSibling(node);
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- return nextSibling;
- },
-
- /**
- * Removes an array of nodes from their parent node.
- *
- * @param {NodeActor[]} nodes The nodes to remove.
- */
- removeNodes: function (nodes) {
- // Check that all nodes are valid before processing the removals.
- for (let node of nodes) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes");
- }
- }
-
- for (let node of nodes) {
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- }
- },
-
- /**
- * Insert a node into the DOM.
- */
- insertBefore: function (node, parent, sibling) {
- if (isNodeDead(node) ||
- isNodeDead(parent) ||
- (sibling && isNodeDead(sibling))) {
- return;
- }
-
- let rawNode = node.rawNode;
- let rawParent = parent.rawNode;
- let rawSibling = sibling ? sibling.rawNode : null;
-
- // Don't bother inserting a node if the document position isn't going
- // to change. This prevents needless iframes reloading and mutations.
- if (rawNode.parentNode === rawParent) {
- let currentNextSibling = this.nextSibling(node);
- currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
- null;
-
- if (rawNode === rawSibling || currentNextSibling === rawSibling) {
- return;
- }
- }
-
- rawParent.insertBefore(rawNode, rawSibling);
- },
-
- /**
- * Editing a node's tagname actually means creating a new node with the same
- * attributes, removing the node and inserting the new one instead.
- * This method does not return anything as mutation events are taking care of
- * informing the consumers about changes.
- */
- editTagName: function (node, tagName) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let oldNode = node.rawNode;
-
- // Create a new element with the same attributes as the current element and
- // prepare to replace the current node with it.
- let newNode;
- try {
- newNode = nodeDocument(oldNode).createElement(tagName);
- } catch (x) {
- // Failed to create a new element with that tag name, ignore the change,
- // and signal the error to the front.
- return Promise.reject(new Error("Could not change node's tagName to " + tagName));
- }
-
- let attrs = oldNode.attributes;
- for (let i = 0; i < attrs.length; i++) {
- newNode.setAttribute(attrs[i].name, attrs[i].value);
- }
-
- // Insert the new node, and transfer the old node's children.
- oldNode.parentNode.insertBefore(newNode, oldNode);
- while (oldNode.firstChild) {
- newNode.appendChild(oldNode.firstChild);
- }
-
- oldNode.remove();
- return null;
- },
-
- /**
- * Get any pending mutation records. Must be called by the client after
- * the `new-mutations` notification is received. Returns an array of
- * mutation records.
- *
- * Mutation records have a basic structure:
- *
- * {
- * type: attributes|characterData|childList,
- * target: <domnode actor ID>,
- * }
- *
- * And additional attributes based on the mutation type:
- *
- * `attributes` type:
- * attributeName: <string> - the attribute that changed
- * attributeNamespace: <string> - the attribute's namespace URI, if any.
- * newValue: <string> - The new value of the attribute, if any.
- *
- * `characterData` type:
- * newValue: <string> - the new nodeValue for the node
- *
- * `childList` type is returned when the set of children for a node
- * has changed. Includes extra data, which can be used by the client to
- * maintain its ownership subtree.
- *
- * added: array of <domnode actor ID> - The list of actors *previously
- * seen by the client* that were added to the target node.
- * removed: array of <domnode actor ID> The list of actors *previously
- * seen by the client* that were removed from the target node.
- * inlineTextChild: If the node now has a single text child, it will
- * be sent here.
- *
- * Actors that are included in a MutationRecord's `removed` but
- * not in an `added` have been removed from the client's ownership
- * tree (either by being moved under a node the client has seen yet
- * or by being removed from the tree entirely), and is considered
- * 'orphaned'.
- *
- * Keep in mind that if a node that the client hasn't seen is moved
- * into or out of the target node, it will not be included in the
- * removedNodes and addedNodes list, so if the client is interested
- * in the new set of children it needs to issue a `children` request.
- */
- getMutations: function (options = {}) {
- let pending = this._pendingMutations || [];
- this._pendingMutations = [];
- this._waitingForGetMutations = false;
-
- if (options.cleanup) {
- for (let node of this._orphaned) {
- // Release the orphaned node. Nodes or children that have been
- // retained will be moved to this._retainedOrphans.
- this.releaseNode(node);
- }
- this._orphaned = new Set();
- }
-
- return pending;
- },
-
- queueMutation: function (mutation) {
- if (!this.actorID || this._destroyed) {
- // We've been destroyed, don't bother queueing this mutation.
- return;
- }
-
- // Add the mutation to the list of mutations to be retrieved next.
- this._pendingMutations.push(mutation);
-
- // Bail out if we already emitted a new-mutations event and are waiting for a client
- // to retrieve them.
- if (this._waitingForGetMutations) {
- return;
- }
-
- if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
- this._emitNewMutations();
- } else {
- /**
- * If many mutations are fired at the same time, clients might sequentially request
- * children/siblings for updated nodes, which can be costly. By throttling the calls
- * to getMutations, duplicated mutations will be ignored.
- */
- this._throttledEmitNewMutations();
- }
- },
-
- _emitNewMutations: function () {
- if (!this.actorID || this._destroyed) {
- // Bail out if the actor was destroyed after throttling this call.
- return;
- }
-
- if (this._waitingForGetMutations || this._pendingMutations.length == 0) {
- // Bail out if we already fired the new-mutation event or if no mutations are
- // waiting to be retrieved.
- return;
- }
-
- this._waitingForGetMutations = true;
- this.emit("new-mutations");
- },
-
- /**
- * Handles mutations from the DOM mutation observer API.
- *
- * @param array[MutationRecord] mutations
- * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
- */
- onMutations: function (mutations) {
- // Notify any observers that want *all* mutations (even on nodes that aren't
- // referenced). This is not sent over the protocol so can only be used by
- // scripts running in the server process.
- this.emit("any-mutation");
-
- for (let change of mutations) {
- let targetActor = this.getNode(change.target);
- if (!targetActor) {
- continue;
- }
- let targetNode = change.target;
- let type = change.type;
- let mutation = {
- type: type,
- target: targetActor.actorID,
- };
-
- if (type === "attributes") {
- mutation.attributeName = change.attributeName;
- mutation.attributeNamespace = change.attributeNamespace || undefined;
- mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
- targetNode.getAttribute(mutation.attributeName)
- : null;
- } else if (type === "characterData") {
- mutation.newValue = targetNode.nodeValue;
- this._maybeQueueInlineTextChildMutation(change, targetNode);
- } else if (type === "childList" || type === "nativeAnonymousChildList") {
- // Get the list of removed and added actors that the client has seen
- // so that it can keep its ownership tree up to date.
- let removedActors = [];
- let addedActors = [];
- for (let removed of change.removedNodes) {
- let removedActor = this.getNode(removed);
- if (!removedActor) {
- // If the client never encountered this actor we don't need to
- // mention that it was removed.
- continue;
- }
- // While removed from the tree, nodes are saved as orphaned.
- this._orphaned.add(removedActor);
- removedActors.push(removedActor.actorID);
- }
- for (let added of change.addedNodes) {
- let addedActor = this.getNode(added);
- if (!addedActor) {
- // If the client never encounted this actor we don't need to tell
- // it about its addition for ownership tree purposes - if the
- // client wants to see the new nodes it can ask for children.
- continue;
- }
- // The actor is reconnected to the ownership tree, unorphan
- // it and let the client know so that its ownership tree is up
- // to date.
- this._orphaned.delete(addedActor);
- addedActors.push(addedActor.actorID);
- }
-
- mutation.numChildren = targetActor.numChildren;
- mutation.removed = removedActors;
- mutation.added = addedActors;
-
- let inlineTextChild = this.inlineTextChild(targetActor);
- if (inlineTextChild) {
- mutation.inlineTextChild = inlineTextChild.form();
- }
- }
- this.queueMutation(mutation);
- }
- },
-
- /**
- * Check if the provided mutation could change the way the target element is
- * inlined with its parent node. If it might, a custom mutation of type
- * "inlineTextChild" will be queued.
- *
- * @param {MutationRecord} mutation
- * A characterData type mutation
- */
- _maybeQueueInlineTextChildMutation: function (mutation) {
- let {oldValue, target} = mutation;
- let newValue = target.nodeValue;
- let limit = gValueSummaryLength;
-
- if ((oldValue.length <= limit && newValue.length <= limit) ||
- (oldValue.length > limit && newValue.length > limit)) {
- // Bail out if the new & old values are both below/above the size limit.
- return;
- }
-
- let parentActor = this.getNode(target.parentNode);
- if (!parentActor || parentActor.rawNode.children.length > 0) {
- // If the parent node has other children, a character data mutation will
- // not change anything regarding inlining text nodes.
- return;
- }
-
- let inlineTextChild = this.inlineTextChild(parentActor);
- this.queueMutation({
- type: "inlineTextChild",
- target: parentActor.actorID,
- inlineTextChild:
- inlineTextChild ? inlineTextChild.form() : undefined
- });
- },
-
- onFrameLoad: function ({ window, isTopLevel }) {
- let { readyState } = window.document;
- if (readyState != "interactive" && readyState != "complete") {
- window.addEventListener("DOMContentLoaded",
- this.onFrameLoad.bind(this, { window, isTopLevel }),
- { once: true });
- return;
- }
- if (isTopLevel) {
- // If we initialize the inspector while the document is loading,
- // we may already have a root document set in the constructor.
- if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) &&
- this.rootDoc.defaultView) {
- this.onFrameUnload({ window: this.rootDoc.defaultView });
- }
- // Update all DOM objects references to target the new document.
- this.rootWin = window;
- this.rootDoc = window.document;
- this.rootNode = this.document();
- this.queueMutation({
- type: "newRoot",
- target: this.rootNode.form()
- });
- return;
- }
- let frame = getFrameElement(window);
- let frameActor = this.getNode(frame);
- if (!frameActor) {
- return;
- }
-
- this.queueMutation({
- type: "frameLoad",
- target: frameActor.actorID,
- });
-
- // Send a childList mutation on the frame.
- this.queueMutation({
- type: "childList",
- target: frameActor.actorID,
- added: [],
- removed: []
- });
- },
-
- // Returns true if domNode is in window or a subframe.
- _childOfWindow: function (window, domNode) {
- let win = nodeDocument(domNode).defaultView;
- while (win) {
- if (win === window) {
- return true;
- }
- win = getFrameElement(win);
- }
- return false;
- },
-
- onFrameUnload: function ({ window }) {
- // Any retained orphans that belong to this document
- // or its children need to be released, and a mutation sent
- // to notify of that.
- let releasedOrphans = [];
-
- for (let retained of this._retainedOrphans) {
- if (Cu.isDeadWrapper(retained.rawNode) ||
- this._childOfWindow(window, retained.rawNode)) {
- this._retainedOrphans.delete(retained);
- releasedOrphans.push(retained.actorID);
- this.releaseNode(retained, { force: true });
- }
- }
-
- if (releasedOrphans.length > 0) {
- this.queueMutation({
- target: this.rootNode.actorID,
- type: "unretained",
- nodes: releasedOrphans
- });
- }
-
- let doc = window.document;
- let documentActor = this.getNode(doc);
- if (!documentActor) {
- return;
- }
-
- if (this.rootDoc === doc) {
- this.rootDoc = null;
- this.rootNode = null;
- }
-
- this.queueMutation({
- type: "documentUnload",
- target: documentActor.actorID
- });
-
- let walker = this.getDocumentWalker(doc);
- let parentNode = walker.parentNode();
- if (parentNode) {
- // Send a childList mutation on the frame so that clients know
- // they should reread the children list.
- this.queueMutation({
- type: "childList",
- target: this.getNode(parentNode).actorID,
- added: [],
- removed: []
- });
- }
-
- // Need to force a release of this node, because those nodes can't
- // be accessed anymore.
- this.releaseNode(documentActor, { force: true });
- },
-
- /**
- * Check if a node is attached to the DOM tree of the current page.
- * @param {nsIDomNode} rawNode
- * @return {Boolean} false if the node is removed from the tree or within a
- * document fragment
- */
- _isInDOMTree: function (rawNode) {
- let walker = this.getDocumentWalker(rawNode);
- let current = walker.currentNode;
-
- // Reaching the top of tree
- while (walker.parentNode()) {
- current = walker.currentNode;
- }
-
- // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
- // attached
- if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
- current !== this.rootDoc) {
- return false;
- }
-
- // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
- return true;
- },
-
- /**
- * @see _isInDomTree
- */
- isInDOMTree: function (node) {
- if (isNodeDead(node)) {
- return false;
- }
- return this._isInDOMTree(node.rawNode);
- },
-
- /**
- * Given an ObjectActor (identified by its ID), commonly used in the debugger,
- * webconsole and variablesView, return the corresponding inspector's
- * NodeActor
- */
- getNodeActorFromObjectActor: function (objectActorID) {
- let actor = this.conn.getActor(objectActorID);
- if (!actor) {
- return null;
- }
-
- let debuggerObject = this.conn.getActor(objectActorID).obj;
- let rawNode = debuggerObject.unsafeDereference();
-
- if (!this._isInDOMTree(rawNode)) {
- return null;
- }
-
- // This is a special case for the document object whereby it is considered
- // as document.documentElement (the <html> node)
- if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
- rawNode = rawNode.documentElement;
- }
-
- return this.attachElement(rawNode);
- },
-
- /**
- * Given a windowID return the NodeActor for the corresponding frameElement,
- * unless it's the root window
- */
- getNodeActorFromWindowID: function (windowID) {
- let win;
-
- try {
- win = Services.wm.getOuterWindowWithId(windowID);
- } catch (e) {
- // ignore
- }
-
- if (!win) {
- return { error: "noWindow",
- message: "The related docshell is destroyed or not found" };
- } else if (!win.frameElement) {
- // the frame element of the root document is privileged & thus
- // inaccessible, so return the document body/element instead
- return this.attachElement(win.document.body || win.document.documentElement);
- }
-
- return this.attachElement(win.frameElement);
- },
-
- /**
- * Given a StyleSheetActor (identified by its ID), commonly used in the
- * style-editor, get its ownerNode and return the corresponding walker's
- * NodeActor.
- * Note that getNodeFromActor was added later and can now be used instead.
- */
- getStyleSheetOwnerNode: function (styleSheetActorID) {
- return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
- },
-
- /**
- * This method can be used to retrieve NodeActor for DOM nodes from other
- * actors in a way that they can later be highlighted in the page, or
- * selected in the inspector.
- * If an actor has a reference to a DOM node, and the UI needs to know about
- * this DOM node (and possibly select it in the inspector), the UI should
- * first retrieve a reference to the walkerFront:
- *
- * // Make sure the inspector/walker have been initialized first.
- * toolbox.initInspector().then(() => {
- * // Retrieve the walker.
- * let walker = toolbox.walker;
- * });
- *
- * And then call this method:
- *
- * // Get the nodeFront from my actor, passing the ID and properties path.
- * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
- * // Use the nodeFront, e.g. select the node in the inspector.
- * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
- * });
- *
- * @param {String} actorID The ID for the actor that has a reference to the
- * DOM node.
- * @param {Array} path Where, on the actor, is the DOM node stored. If in the
- * scope of the actor, the node is available as `this.data.node`, then this
- * should be ["data", "node"].
- * @return {NodeActor} The attached NodeActor, or null if it couldn't be
- * found.
- */
- getNodeFromActor: function (actorID, path) {
- let actor = this.conn.getActor(actorID);
- if (!actor) {
- return null;
- }
-
- let obj = actor;
- for (let name of path) {
- if (!(name in obj)) {
- return null;
- }
- obj = obj[name];
- }
-
- return this.attachElement(obj);
- },
-
- /**
- * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
- * information.
- *
- * @return {LayoutActor}
- */
- getLayoutInspector: function () {
- if (!this.layoutActor) {
- this.layoutActor = new LayoutActor(this.conn, this.tabActor, this);
- }
-
- return this.layoutActor;
- },
-
- /**
- * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
- * returns null.
- */
- getOffsetParent: function (node) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let offsetParent = node.rawNode.offsetParent;
-
- if (!offsetParent) {
- return null;
- }
-
- return this._ref(offsetParent);
- },
-});
-
-/**
- * Server side of the inspector actor, which is used to create
- * inspector-related actors, including the walker.
- */
-exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
- initialize: function (conn, tabActor) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
-
- this._onColorPicked = this._onColorPicked.bind(this);
- this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
- this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- this.destroyEyeDropper();
-
- this._highlighterPromise = null;
- this._pageStylePromise = null;
- this._walkerPromise = null;
- this.walker = null;
- this.tabActor = null;
- },
-
- get window() {
- return this.tabActor.window;
- },
-
- getWalker: function (options = {}) {
- if (this._walkerPromise) {
- return this._walkerPromise;
- }
-
- let deferred = defer();
- this._walkerPromise = deferred.promise;
-
- let window = this.window;
- let domReady = () => {
- let tabActor = this.tabActor;
- window.removeEventListener("DOMContentLoaded", domReady, true);
- this.walker = WalkerActor(this.conn, tabActor, options);
- this.manage(this.walker);
- this.walker.once("destroyed", () => {
- this._walkerPromise = null;
- this._pageStylePromise = null;
- });
- deferred.resolve(this.walker);
- };
-
- if (window.document.readyState === "loading") {
- window.addEventListener("DOMContentLoaded", domReady, true);
- } else {
- domReady();
- }
-
- return this._walkerPromise;
- },
-
- getPageStyle: function () {
- if (this._pageStylePromise) {
- return this._pageStylePromise;
- }
-
- this._pageStylePromise = this.getWalker().then(walker => {
- let pageStyle = PageStyleActor(this);
- this.manage(pageStyle);
- return pageStyle;
- });
- return this._pageStylePromise;
- },
-
- /**
- * The most used highlighter actor is the HighlighterActor which can be
- * conveniently retrieved by this method.
- * The same instance will always be returned by this method when called
- * several times.
- * The highlighter actor returned here is used to highlighter elements's
- * box-models from the markup-view, box model, console, debugger, ... as
- * well as select elements with the pointer (pick).
- *
- * @param {Boolean} autohide Optionally autohide the highlighter after an
- * element has been picked
- * @return {HighlighterActor}
- */
- getHighlighter: function (autohide) {
- if (this._highlighterPromise) {
- return this._highlighterPromise;
- }
-
- this._highlighterPromise = this.getWalker().then(walker => {
- let highlighter = HighlighterActor(this, autohide);
- this.manage(highlighter);
- return highlighter;
- });
- return this._highlighterPromise;
- },
-
- /**
- * If consumers need to display several highlighters at the same time or
- * different types of highlighters, then this method should be used, passing
- * the type name of the highlighter needed as argument.
- * A new instance will be created everytime the method is called, so it's up
- * to the consumer to release it when it is not needed anymore
- *
- * @param {String} type The type of highlighter to create
- * @return {Highlighter} The highlighter actor instance or null if the
- * typeName passed doesn't match any available highlighter
- */
- getHighlighterByType: function (typeName) {
- if (isTypeRegistered(typeName)) {
- return CustomHighlighterActor(this, typeName);
- }
- return null;
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageDataFromURL: function (url, maxDim) {
- let img = new this.window.Image();
- img.src = url;
-
- // imageToImageData waits for the image to load.
- return imageToImageData(img, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Resolve a URL to its absolute form, in the scope of a given content window.
- * @param {String} url.
- * @param {NodeActor} node If provided, the owner window of this node will be
- * used to resolve the URL. Otherwise, the top-level content window will be
- * used instead.
- * @return {String} url.
- */
- resolveRelativeURL: function (url, node) {
- let document = isNodeDead(node)
- ? this.window.document
- : nodeDocument(node.rawNode);
-
- if (!document) {
- return url;
- }
-
- let baseURI = Services.io.newURI(document.location.href);
- return Services.io.newURI(url, null, baseURI).spec;
- },
-
- /**
- * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
- * Note that for now, a new instance is created every time to deal with page navigation.
- */
- createEyeDropper: function () {
- this.destroyEyeDropper();
- this._highlighterEnv = new HighlighterEnvironment();
- this._highlighterEnv.initFromTabActor(this.tabActor);
- this._eyeDropper = new EyeDropper(this._highlighterEnv);
- },
-
- /**
- * Destroy the current eye-dropper highlighter instance.
- */
- destroyEyeDropper: function () {
- if (this._eyeDropper) {
- this.cancelPickColorFromPage();
- this._eyeDropper.destroy();
- this._eyeDropper = null;
- this._highlighterEnv.destroy();
- this._highlighterEnv = null;
- }
- },
-
- /**
- * Pick a color from the page using the eye-dropper. This method doesn't return anything
- * but will cause events to be sent to the front when a color is picked or when the user
- * cancels the picker.
- * @param {Object} options
- */
- pickColorFromPage: function (options) {
- this.createEyeDropper();
- this._eyeDropper.show(this.window.document.documentElement, options);
- this._eyeDropper.once("selected", this._onColorPicked);
- this._eyeDropper.once("canceled", this._onColorPickCanceled);
- this.tabActor.once("will-navigate", this.destroyEyeDropper);
- },
-
- /**
- * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
- * highlighter is for the user to click in the page and select a color. If you need to
- * dismiss the eye-dropper programatically instead, use this method.
- */
- cancelPickColorFromPage: function () {
- if (this._eyeDropper) {
- this._eyeDropper.hide();
- this._eyeDropper.off("selected", this._onColorPicked);
- this._eyeDropper.off("canceled", this._onColorPickCanceled);
- this.tabActor.off("will-navigate", this.destroyEyeDropper);
- }
- },
-
- /**
- * Check if the current document supports highlighters using a canvasFrame anonymous
- * content container (ie all highlighters except the SimpleOutlineHighlighter).
- * It is impossible to detect the feature programmatically as some document types simply
- * don't render the canvasFrame without throwing any error.
- */
- supportsHighlighters: function () {
- let doc = this.tabActor.window.document;
- let ns = doc.documentElement.namespaceURI;
-
- // XUL documents do not support insertAnonymousContent().
- if (ns === XUL_NS) {
- return false;
- }
-
- // SVG documents do not render the canvasFrame (see Bug 1157592).
- if (ns === SVG_NS) {
- return false;
- }
-
- return true;
- },
-
- _onColorPicked: function (e, color) {
- this.emit("color-picked", color);
- },
-
- _onColorPickCanceled: function () {
- this.emit("color-pick-canceled");
- }
-});
-
-// Exported for test purposes.
-exports._documentWalker = DocumentWalker;
-
-function nodeDocument(node) {
- if (Cu.isDeadWrapper(node)) {
- return null;
- }
- return node.ownerDocument ||
- (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
-}
-
-function nodeDocshell(node) {
- let doc = node ? nodeDocument(node) : null;
- let win = doc ? doc.defaultView : null;
- if (win) {
- return win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell);
- }
- return null;
-}
-
-function isNodeDead(node) {
- return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
-}
-
/**
* Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
* See inDeepTreeWalker for more information about the methods.
*
* @param {DOMNode} node
* @param {Window} rootWin
* @param {Number} whatToShow
* See nodeFilterConstants / inIDeepTreeWalker for options.
@@ -3057,19 +26,19 @@ function isNodeDead(node) {
* A custom filter function Taking in a DOMNode and returning an Int. See
* WalkerActor.nodeFilter for an example.
* @param {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.
*/
function DocumentWalker(node, rootWin,
- whatToShow = nodeFilterConstants.SHOW_ALL,
- filter = standardTreeWalkerFilter,
- skipTo = SKIP_TO_PARENT) {
+ whatToShow = nodeFilterConstants.SHOW_ALL,
+ filter = standardTreeWalkerFilter,
+ skipTo = SKIP_TO_PARENT) {
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 = true;
this.walker.showSubDocuments = true;
@@ -3078,16 +47,17 @@ function DocumentWalker(node, rootWin,
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;
@@ -3202,201 +172,11 @@ DocumentWalker.prototype = {
return null;
},
isSkippedNode: function (node) {
return this.filter(node) === nodeFilterConstants.FILTER_SKIP;
},
};
-function isInXULDocument(el) {
- let doc = nodeDocument(el);
- return doc &&
- doc.documentElement &&
- doc.documentElement.namespaceURI === XUL_NS;
-}
-
-/**
- * This DeepTreeWalker filter skips whitespace text nodes and anonymous
- * content with the exception of ::before and ::after and anonymous content
- * in XUL document (needed to show all elements in the browser toolbox).
- */
-function standardTreeWalkerFilter(node) {
- // ::before and ::after are native anonymous content, but we always
- // want to show them
- if (node.nodeName === "_moz_generated_content_before" ||
- node.nodeName === "_moz_generated_content_after") {
- return nodeFilterConstants.FILTER_ACCEPT;
- }
-
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
-
- // Ignore all native and XBL anonymous content inside a non-XUL document.
- // We need to do this to skip things like form controls, scrollbars,
- // video controls, etc (see bug 1187482).
- if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
- isNativeAnonymous(node))) {
- return nodeFilterConstants.FILTER_SKIP;
- }
-
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
- * it also includes all anonymous content (like internal form controls).
- */
-function allAnonymousContentTreeWalkerFilter(node) {
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * Is the given node a text node composed of whitespace only?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function isWhitespaceTextNode(node) {
- return node.nodeType == Ci.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
-}
-
-/**
- * Does the given node have non-0 width and height?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function nodeHasSize(node) {
- if (!node.getBoxQuads) {
- return false;
- }
-
- let quads = node.getBoxQuads();
- return quads.length && quads.some(quad => quad.bounds.width && quad.bounds.height);
-}
-
-/**
- * Returns a promise that is settled once the given HTMLImageElement has
- * finished loading.
- *
- * @param {HTMLImageElement} image - The image element.
- * @param {Number} timeout - Maximum amount of time the image is allowed to load
- * before the waiting is aborted. Ignored if flags.testing is set.
- *
- * @return {Promise} that is fulfilled once the image has loaded. If the image
- * fails to load or the load takes too long, the promise is rejected.
- */
-function ensureImageLoaded(image, timeout) {
- let { HTMLImageElement } = image.ownerGlobal;
- if (!(image instanceof HTMLImageElement)) {
- return promise.reject("image must be an HTMLImageELement");
- }
-
- if (image.complete) {
- // The image has already finished loading.
- return promise.resolve();
- }
-
- // This image is still loading.
- let onLoad = AsyncUtils.listenOnce(image, "load");
-
- // Reject if loading fails.
- let onError = AsyncUtils.listenOnce(image, "error").then(() => {
- return promise.reject("Image '" + image.src + "' failed to load.");
- });
-
- // Don't timeout when testing. This is never settled.
- let onAbort = new Promise(() => {});
-
- if (!flags.testing) {
- // Tests are not running. Reject the promise after given timeout.
- onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
- return promise.reject("Image '" + image.src + "' took too long to load.");
- });
- }
-
- // See which happens first.
- return promise.race([onLoad, onError, onAbort]);
-}
-
-/**
- * Given an <img> or <canvas> element, return the image data-uri. If @param node
- * is an <img> element, the method waits a while for the image to load before
- * the data is generated. If the image does not finish loading in a reasonable
- * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
- *
- * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
- * element, or Image() object. Other types cause the method to reject.
- * @param {Number} maxDim - Optionally pass a maximum size you want the longest
- * side of the image to be resized to before getting the image data.
-
- * @return {Promise} A promise that is fulfilled with an object containing the
- * data-uri and size-related information:
- * { data: "...",
- * size: {
- * naturalWidth: 400,
- * naturalHeight: 300,
- * resized: true }
- * }.
- *
- * If something goes wrong, the promise is rejected.
- */
-var imageToImageData = Task.async(function* (node, maxDim) {
- let { HTMLCanvasElement, HTMLImageElement } = node.ownerGlobal;
-
- let isImg = node instanceof HTMLImageElement;
- let isCanvas = node instanceof HTMLCanvasElement;
-
- if (!isImg && !isCanvas) {
- throw new Error("node is not a <canvas> or <img> element.");
- }
-
- if (isImg) {
- // Ensure that the image is ready.
- yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
- }
-
- // Get the image resize ratio if a maxDim was provided
- let resizeRatio = 1;
- let imgWidth = node.naturalWidth || node.width;
- let imgHeight = node.naturalHeight || node.height;
- let imgMax = Math.max(imgWidth, imgHeight);
- if (maxDim && imgMax > maxDim) {
- resizeRatio = maxDim / imgMax;
- }
-
- // Extract the image data
- let imageData;
- // The image may already be a data-uri, in which case, save ourselves the
- // trouble of converting via the canvas.drawImage.toDataURL method, but only
- // if the image doesn't need resizing
- if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
- imageData = node.src;
- } else {
- // Create a canvas to copy the rawNode into and get the imageData from
- let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
- canvas.width = imgWidth * resizeRatio;
- canvas.height = imgHeight * resizeRatio;
- let ctx = canvas.getContext("2d");
-
- // Copy the rawNode image or canvas in the new canvas and extract data
- ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
- imageData = canvas.toDataURL("image/png");
- }
-
- return {
- data: imageData,
- size: {
- naturalWidth: imgWidth,
- naturalHeight: imgHeight,
- resized: resizeRatio !== 1
- }
- };
-});
+exports.DocumentWalker = DocumentWalker;
+exports.SKIP_TO_PARENT = SKIP_TO_PARENT;
+exports.SKIP_TO_SIBLING = SKIP_TO_SIBLING;
--- a/devtools/server/actors/inspector/inspector.js
+++ b/devtools/server/actors/inspector/inspector.js
@@ -45,2739 +45,34 @@
* the client - when a node is disconnected from the DOM tree we want to be
* able to free the client objects for all the children nodes.
*
* So to be able to answer "all the children of a given node that we have
* seen on the client side", we guarantee that every time we've seen a node,
* we connect it up through its parents.
*/
-const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const protocol = require("devtools/shared/protocol");
const {LongStringActor} = require("devtools/server/actors/string");
-const promise = require("promise");
const defer = require("devtools/shared/defer");
-const {Task} = require("devtools/shared/task");
-const EventEmitter = require("devtools/shared/event-emitter");
-const InspectorUtils = require("InspectorUtils");
-const {walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
-const {nodeSpec, nodeListSpec} = require("devtools/shared/specs/node");
+const {inspectorSpec} = require("devtools/shared/specs/inspector");
-loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
+loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
+loader.lazyRequireGetter(this, "WalkerActor", "devtools/server/actors/inspector/walker-actor", true);
loader.lazyRequireGetter(this, "EyeDropper", "devtools/server/actors/highlighters/eye-dropper", true);
-loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
loader.lazyRequireGetter(this, "PageStyleActor", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
-loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
-loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
loader.lazyRequireGetter(this, "HighlighterActor", "devtools/server/actors/highlighters", true);
loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true);
loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
loader.lazyRequireGetter(this, "HighlighterEnvironment", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
-loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isShadowAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
-loader.lazyServiceGetter(this, "DOMParser",
- "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
-
-loader.lazyServiceGetter(this, "eventListenerService",
- "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
-
-const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
-const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
-const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
-const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
const SVG_NS = "http://www.w3.org/2000/svg";
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const IMAGE_FETCHING_TIMEOUT = 500;
-
-// Minimum delay between two "new-mutations" events.
-const MUTATIONS_THROTTLING_DELAY = 100;
-// List of mutation types that should -not- be throttled.
-const IMMEDIATE_MUTATIONS = [
- "documentUnload",
- "frameLoad",
- "newRoot",
- "pseudoClassLock",
-];
-
-// 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";
-
-// The possible completions to a ':' with added score to give certain values
-// some preference.
-const PSEUDO_SELECTORS = [
- [":active", 1],
- [":hover", 1],
- [":focus", 1],
- [":visited", 0],
- [":link", 0],
- [":first-letter", 0],
- [":first-child", 2],
- [":before", 2],
- [":after", 2],
- [":lang(", 0],
- [":not(", 3],
- [":first-of-type", 0],
- [":last-of-type", 0],
- [":only-of-type", 0],
- [":only-child", 2],
- [":nth-child(", 3],
- [":nth-last-child(", 0],
- [":nth-of-type(", 0],
- [":nth-last-of-type(", 0],
- [":last-child", 2],
- [":root", 0],
- [":empty", 0],
- [":target", 0],
- [":enabled", 0],
- [":disabled", 0],
- [":checked", 1],
- ["::selection", 0]
-];
-
-var HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
- .__fx-devtools-hide-shortcut__ {
- visibility: hidden !important;
- }
-
- :-moz-devtools-highlighted {
- outline: 2px dashed #F06!important;
- outline-offset: -2px !important;
- }
-`);
-
-/**
- * We only send nodeValue up to a certain size by default. This stuff
- * controls that size.
- */
-exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
-var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
-
-exports.getValueSummaryLength = function () {
- return gValueSummaryLength;
-};
-
-exports.setValueSummaryLength = function (val) {
- gValueSummaryLength = val;
-};
-
-/**
- * Returns the properly cased version of the node's tag name, which can be
- * used when displaying said name in the UI.
- *
- * @param {Node} rawNode
- * Node for which we want the display name
- * @return {String}
- * Properly cased version of the node tag name
- */
-const getNodeDisplayName = function (rawNode) {
- if (rawNode.nodeName && !rawNode.localName) {
- // The localName & prefix APIs have been moved from the Node interface to the Element
- // interface. Use Node.nodeName as a fallback.
- return rawNode.nodeName;
- }
- return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
-};
-exports.getNodeDisplayName = getNodeDisplayName;
-
-/**
- * Server side of the node actor.
- */
-var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
- initialize: function (walker, node) {
- protocol.Actor.prototype.initialize.call(this, null);
- this.walker = walker;
- this.rawNode = node;
- this._eventParsers = new EventParsers().parsers;
-
- // Storing the original display of the node, to track changes when reflows
- // occur
- this.wasDisplayed = this.isDisplayed;
- },
-
- toString: function () {
- return "[NodeActor " + this.actorID + " for " +
- this.rawNode.toString() + "]";
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- isDocumentElement: function () {
- return this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.documentElement === this.rawNode;
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- if (this.mutationObserver) {
- if (!Cu.isDeadWrapper(this.mutationObserver)) {
- this.mutationObserver.disconnect();
- }
- this.mutationObserver = null;
- }
- this.rawNode = null;
- this.walker = null;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function (detail) {
- if (detail === "actorid") {
- return this.actorID;
- }
-
- let parentNode = this.walker.parentNode(this);
- let inlineTextChild = this.walker.inlineTextChild(this);
-
- let form = {
- actor: this.actorID,
- baseURI: this.rawNode.baseURI,
- parent: parentNode ? parentNode.actorID : undefined,
- nodeType: this.rawNode.nodeType,
- namespaceURI: this.rawNode.namespaceURI,
- nodeName: this.rawNode.nodeName,
- nodeValue: this.rawNode.nodeValue,
- displayName: getNodeDisplayName(this.rawNode),
- numChildren: this.numChildren,
- inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
-
- // doctype attributes
- name: this.rawNode.name,
- publicId: this.rawNode.publicId,
- systemId: this.rawNode.systemId,
-
- attrs: this.writeAttrs(),
- isBeforePseudoElement: this.isBeforePseudoElement,
- isAfterPseudoElement: this.isAfterPseudoElement,
- isAnonymous: isAnonymous(this.rawNode),
- isNativeAnonymous: isNativeAnonymous(this.rawNode),
- isXBLAnonymous: isXBLAnonymous(this.rawNode),
- isShadowAnonymous: isShadowAnonymous(this.rawNode),
- pseudoClassLocks: this.writePseudoClassLocks(),
-
- isDisplayed: this.isDisplayed,
- isInHTMLDocument: this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.contentType === "text/html",
- hasEventListeners: this._hasEventListeners,
- };
-
- if (this.isDocumentElement()) {
- form.isDocumentElement = true;
- }
-
- // Add an extra API for custom properties added by other
- // modules/extensions.
- form.setFormProperty = (name, value) => {
- if (!form.props) {
- form.props = {};
- }
- form.props[name] = value;
- };
-
- // Fire an event so, other modules can create its own properties
- // that should be passed to the client (within the form.props field).
- EventEmitter.emit(NodeActor, "form", {
- target: this,
- data: form
- });
-
- return form;
- },
-
- /**
- * Watch the given document node for mutations using the DOM observer
- * API.
- */
- watchDocument: function (callback) {
- let node = this.rawNode;
- // Create the observer on the node's actor. The node will make sure
- // the observer is cleaned up when the actor is released.
- let observer = new node.defaultView.MutationObserver(callback);
- observer.mergeAttributeRecords = true;
- observer.observe(node, {
- nativeAnonymousChildList: true,
- attributes: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true
- });
- this.mutationObserver = observer;
- },
-
- get isBeforePseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_before";
- },
-
- get isAfterPseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_after";
- },
-
- // Estimate the number of children that the walker will return without making
- // a call to children() if possible.
- get numChildren() {
- // For pseudo elements, childNodes.length returns 1, but the walker
- // will return 0.
- if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
- return 0;
- }
-
- let rawNode = this.rawNode;
- let numChildren = rawNode.childNodes.length;
- let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
- rawNode.ownerDocument.getAnonymousNodes(rawNode);
-
- let hasContentDocument = rawNode.contentDocument;
- let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
- if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
- // This might be an iframe with virtual children.
- numChildren = 1;
- }
-
- // Normal counting misses ::before/::after. Also, some anonymous children
- // may ultimately be skipped, so we have to consult with the walker.
- if (numChildren === 0 || hasAnonChildren) {
- numChildren = this.walker.children(this).nodes.length;
- }
-
- return numChildren;
- },
-
- get computedStyle() {
- return CssLogic.getComputedStyle(this.rawNode);
- },
-
- /**
- * Is the node's display computed style value other than "none"
- */
- get isDisplayed() {
- // Consider all non-element nodes as displayed.
- if (isNodeDead(this) ||
- this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
- this.isAfterPseudoElement ||
- this.isBeforePseudoElement) {
- return true;
- }
-
- let style = this.computedStyle;
- if (!style) {
- return true;
- }
-
- return style.display !== "none";
- },
-
- /**
- * Are there event listeners that are listening on this node? This method
- * uses all parsers registered via event-parsers.js.registerEventParser() to
- * check if there are any event listeners.
- */
- get _hasEventListeners() {
- let parsers = this._eventParsers;
- for (let [, {hasListeners}] of parsers) {
- try {
- if (hasListeners && hasListeners(this.rawNode)) {
- return true;
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
- return false;
- },
-
- writeAttrs: function () {
- if (!this.rawNode.attributes) {
- return undefined;
- }
-
- return [...this.rawNode.attributes].map(attr => {
- return {namespace: attr.namespace, name: attr.name, value: attr.value };
- });
- },
-
- writePseudoClassLocks: function () {
- if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return undefined;
- }
- let ret = undefined;
- for (let pseudo of PSEUDO_CLASSES) {
- if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
- ret = ret || [];
- ret.push(pseudo);
- }
- }
- return ret;
- },
-
- /**
- * Gets event listeners and adds their information to the events array.
- *
- * @param {Node} node
- * Node for which we are to get listeners.
- */
- getEventListeners: function (node) {
- let parsers = this._eventParsers;
- let dbg = this.parent().tabActor.makeDebugger();
- let listenerArray = [];
-
- for (let [, {getListeners, normalizeListener}] of parsers) {
- try {
- let listeners = getListeners(node);
-
- if (!listeners) {
- continue;
- }
-
- for (let listener of listeners) {
- if (normalizeListener) {
- listener.normalizeListener = normalizeListener;
- }
-
- this.processHandlerForEvent(node, listenerArray, dbg, listener);
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
-
- listenerArray.sort((a, b) => {
- return a.type.localeCompare(b.type);
- });
-
- return listenerArray;
- },
-
- /**
- * Process a handler
- *
- * @param {Node} node
- * The node for which we want information.
- * @param {Array} listenerArray
- * listenerArray contains all event objects that we have gathered
- * so far.
- * @param {Debugger} dbg
- * JSDebugger instance.
- * @param {Object} eventInfo
- * See event-parsers.js.registerEventParser() for a description of the
- * eventInfo object.
- *
- * @return {Array}
- * An array of objects where a typical object looks like this:
- * {
- * type: "click",
- * handler: function() { doSomething() },
- * origin: "http://www.mozilla.com",
- * searchString: 'onclick="doSomething()"',
- * tags: tags,
- * DOM0: true,
- * capturing: true,
- * hide: {
- * DOM0: true
- * },
- * native: false
- * }
- */
- processHandlerForEvent: function (node, listenerArray, dbg, listener) {
- let { handler } = listener;
- let global = Cu.getGlobalForObject(handler);
- let globalDO = dbg.addDebuggee(global);
- let listenerDO = globalDO.makeDebuggeeValue(handler);
-
- let { normalizeListener } = listener;
-
- if (normalizeListener) {
- listenerDO = normalizeListener(listenerDO, listener);
- }
-
- let { capturing } = listener;
- let dom0 = false;
- let functionSource = handler.toString();
- let hide = listener.hide || {};
- let line = 0;
- let native = false;
- let override = listener.override || {};
- let tags = listener.tags || "";
- let type = listener.type || "";
- let url = "";
-
- // If the listener is an object with a 'handleEvent' method, use that.
- if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
- let desc;
-
- while (!desc && listenerDO) {
- desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
- listenerDO = listenerDO.proto;
- }
-
- if (desc && desc.value) {
- listenerDO = desc.value;
- }
- }
-
- // If the listener is bound to a different context then we need to switch
- // to the bound function.
- if (listenerDO.isBoundFunction) {
- listenerDO = listenerDO.boundTargetFunction;
- }
-
- let { isArrowFunction, name, script, parameterNames } = listenerDO;
-
- if (script) {
- let scriptSource = script.source.text;
-
- // Scripts are provided via script tags. If it wasn't provided by a
- // script tag it must be a DOM0 event.
- if (script.source.element) {
- dom0 = script.source.element.class !== "HTMLScriptElement";
- } else {
- dom0 = false;
- }
-
- line = script.startLine;
- url = script.url;
-
- // Checking for the string "[native code]" is the only way at this point
- // to check for native code. Even if this provides a false positive then
- // grabbing the source code a second time is harmless.
- if (functionSource === "[object Object]" ||
- functionSource === "[object XULElement]" ||
- functionSource.includes("[native code]")) {
- functionSource =
- scriptSource.substr(script.sourceStart, script.sourceLength);
-
- // At this point the script looks like this:
- // () { ... }
- // We prefix this with "function" if it is not a fat arrow function.
- if (!isArrowFunction) {
- functionSource = "function " + functionSource;
- }
- }
- } else {
- // If the listener is a native one (provided by C++ code) then we have no
- // access to the script. We use the native flag to prevent showing the
- // debugger button because the script is not available.
- native = true;
- }
-
- // Fat arrow function text always contains the parameters. Function
- // parameters are often missing e.g. if Array.sort is used as a handler.
- // If they are missing we provide the parameters ourselves.
- if (parameterNames && parameterNames.length > 0) {
- let prefix = "function " + name + "()";
- let paramString = parameterNames.join(", ");
-
- if (functionSource.startsWith(prefix)) {
- functionSource = functionSource.substr(prefix.length);
-
- functionSource = `function ${name} (${paramString})${functionSource}`;
- }
- }
-
- // If the listener is native code we display the filename "[native code]."
- // This is the official string and should *not* be translated.
- let origin;
- if (native) {
- origin = "[native code]";
- } else {
- origin = url + ((dom0 || line === 0) ? "" : ":" + line);
- }
-
- let eventObj = {
- type: override.type || type,
- handler: override.handler || functionSource.trim(),
- origin: override.origin || origin,
- tags: override.tags || tags,
- DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
- capturing: typeof override.capturing !== "undefined" ?
- override.capturing : capturing,
- hide: typeof override.hide !== "undefined" ? override.hide : hide,
- native
- };
-
- // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
- // generated dynamically from e.g. an onclick="" attribute so the script
- // doesn't actually exist.
- if (native || dom0) {
- eventObj.hide.debugger = true;
- }
-
- listenerArray.push(eventObj);
-
- dbg.removeDebuggee(globalDO);
- },
-
- /**
- * Returns a LongStringActor with the node's value.
- */
- getNodeValue: function () {
- return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
- },
-
- /**
- * Set the node's value to a given string.
- */
- setNodeValue: function (value) {
- this.rawNode.nodeValue = value;
- },
-
- /**
- * Get a unique selector string for this node.
- */
- getUniqueSelector: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return findCssSelector(this.rawNode);
- },
-
- /**
- * Get the full CSS path for this node.
- *
- * @return {String} A CSS selector with a part for the node and each of its ancestors.
- */
- getCssPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getCssPath(this.rawNode);
- },
-
- /**
- * Get the XPath for this node.
- *
- * @return {String} The XPath for finding this node on the page.
- */
- getXPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getXPath(this.rawNode);
- },
-
- /**
- * Scroll the selected node into view.
- */
- scrollIntoView: function () {
- this.rawNode.scrollIntoView(true);
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageData: function (maxDim) {
- return imageToImageData(this.rawNode, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Get all event listeners that are listening on this node.
- */
- getEventListenerInfo: function () {
- let node = this.rawNode;
-
- if (this.rawNode.nodeName.toLowerCase() === "html") {
- let winListeners = this.getEventListeners(node.ownerGlobal) || [];
- let docElementListeners = this.getEventListeners(node) || [];
- let docListeners = this.getEventListeners(node.parentNode) || [];
-
- return [...winListeners, ...docElementListeners, ...docListeners];
- }
- return this.getEventListeners(node);
- },
-
- /**
- * Modify a node's attributes. Passed an array of modifications
- * similar in format to "attributes" mutations.
- * {
- * attributeName: <string>
- * attributeNamespace: <optional string>
- * newValue: <optional string> - If null or undefined, the attribute
- * will be removed.
- * }
- *
- * Returns when the modifications have been made. Mutations will
- * be queued for any changes made.
- */
- modifyAttributes: function (modifications) {
- let rawNode = this.rawNode;
- for (let change of modifications) {
- if (change.newValue == null) {
- if (change.attributeNamespace) {
- rawNode.removeAttributeNS(change.attributeNamespace,
- change.attributeName);
- } else {
- rawNode.removeAttribute(change.attributeName);
- }
- } else if (change.attributeNamespace) {
- rawNode.setAttributeNS(change.attributeNamespace, change.attributeName,
- change.newValue);
- } else {
- rawNode.setAttribute(change.attributeName, change.newValue);
- }
- }
- },
-
- /**
- * Given the font and fill style, get the image data of a canvas with the
- * preview text and font.
- * Returns an imageData object with the actual data being a LongStringActor
- * and the width of the text as a string.
- * The image data is transmitted as a base64 encoded png data-uri.
- */
- getFontFamilyDataURL: function (font, fillStyle = "black") {
- let doc = this.rawNode.ownerDocument;
- let options = {
- previewText: FONT_FAMILY_PREVIEW_TEXT,
- previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
- fillStyle: fillStyle
- };
- let { dataURL, size } = getFontPreviewData(font, doc, options);
-
- return { data: LongStringActor(this.conn, dataURL), size: size };
- },
-
- /**
- * Finds the computed background color of the closest parent with
- * a set background color.
- * Returns a string with the background color of the form
- * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
- * background color is found.
- */
- getClosestBackgroundColor: function () {
- let current = this.rawNode;
- while (current) {
- let computedStyle = CssLogic.getComputedStyle(current);
- let currentStyle = computedStyle.getPropertyValue("background-color");
- if (colorUtils.isValidCSSColor(currentStyle)) {
- let currentCssColor = new colorUtils.CssColor(currentStyle);
- if (!currentCssColor.isTransparent()) {
- return currentCssColor.rgba;
- }
- }
- current = current.parentNode;
- }
- return "rgba(255, 255, 255, 1)";
- }
-});
-
-/**
- * Server side of a node list as returned by querySelectorAll()
- */
-var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
- typeName: "domnodelist",
-
- initialize: function (walker, nodeList) {
- protocol.Actor.prototype.initialize.call(this);
- this.walker = walker;
- this.nodeList = nodeList || [];
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- /**
- * Items returned by this actor should belong to the parent walker.
- */
- marshallPool: function () {
- return this.walker;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- length: this.nodeList ? this.nodeList.length : 0
- };
- },
-
- /**
- * Get a single node from the node list.
- */
- item: function (index) {
- return this.walker.attachElement(this.nodeList[index]);
- },
-
- /**
- * Get a range of the items from the node list.
- */
- items: function (start = 0, end = this.nodeList.length) {
- let items = Array.prototype.slice.call(this.nodeList, start, end)
- .map(item => this.walker._ref(item));
- return this.walker.attachElements(items);
- },
-
- release: function () {}
-});
-
-/**
- * Server side of the DOM walker.
- */
-var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
- /**
- * Create the WalkerActor
- * @param DebuggerServerConnection conn
- * The server connection.
- */
- initialize: function (conn, tabActor, options) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
- this.rootWin = tabActor.window;
- this.rootDoc = this.rootWin.document;
- this._refMap = new Map();
- this._pendingMutations = [];
- this._activePseudoClassLocks = new Set();
- this.showAllAnonymousContent = options.showAllAnonymousContent;
-
- this.walkerSearch = new WalkerSearch(this);
-
- // Nodes which have been removed from the client's known
- // ownership tree are considered "orphaned", and stored in
- // this set.
- this._orphaned = new Set();
-
- // The client can tell the walker that it is interested in a node
- // even when it is orphaned with the `retainNode` method. This
- // list contains orphaned nodes that were so retained.
- this._retainedOrphans = new Set();
-
- this.onMutations = this.onMutations.bind(this);
- this.onFrameLoad = this.onFrameLoad.bind(this);
- this.onFrameUnload = this.onFrameUnload.bind(this);
- this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
- MUTATIONS_THROTTLING_DELAY);
-
- tabActor.on("will-navigate", this.onFrameUnload);
- tabActor.on("window-ready", this.onFrameLoad);
-
- // Ensure that the root document node actor is ready and
- // managed.
- this.rootNode = this.document();
-
- this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
- this._onReflows = this._onReflows.bind(this);
- this.layoutChangeObserver.on("reflows", this._onReflows);
- this._onResize = this._onResize.bind(this);
- this.layoutChangeObserver.on("resize", this._onResize);
-
- this._onEventListenerChange = this._onEventListenerChange.bind(this);
- eventListenerService.addListenerChangeListener(this._onEventListenerChange);
- },
-
- /**
- * Callback for eventListenerService.addListenerChangeListener
- * @param nsISimpleEnumerator changesEnum
- * enumerator of nsIEventListenerChange
- */
- _onEventListenerChange: function (changesEnum) {
- let changes = changesEnum.enumerate();
- while (changes.hasMoreElements()) {
- let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
- let target = current.target;
-
- if (this._refMap.has(target)) {
- let actor = this.getNode(target);
- let mutation = {
- type: "events",
- target: actor.actorID,
- hasEventListeners: actor._hasEventListeners
- };
- this.queueMutation(mutation);
- }
- }
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- root: this.rootNode.form(),
- traits: {
- // FF42+ Inspector starts managing the Walker, while the inspector also
- // starts cleaning itself up automatically on client disconnection.
- // So that there is no need to manually release the walker anymore.
- autoReleased: true,
- // XXX: It seems silly that we need to tell the front which capabilities
- // its actor has in this way when the target can use actorHasMethod. If
- // this was ported to the protocol (Bug 1157048) we could call that
- // inside of custom front methods and not need to do traits for this.
- multiFrameQuerySelectorAll: true,
- textSearch: true,
- }
- };
- },
-
- toString: function () {
- return "[WalkerActor " + this.actorID + "]";
- },
-
- getDocumentWalker: function (node, whatToShow, skipTo) {
- // Allow native anon content (like <video> controls) if preffed on
- let nodeFilter = this.showAllAnonymousContent
- ? allAnonymousContentTreeWalkerFilter
- : standardTreeWalkerFilter;
- return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter, skipTo);
- },
-
- destroy: function () {
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
- protocol.Actor.prototype.destroy.call(this);
- try {
- this.clearPseudoClassLocks();
- this._activePseudoClassLocks = null;
-
- this._hoveredNode = null;
- this.rootWin = null;
- this.rootDoc = null;
- this.rootNode = null;
- this.layoutHelpers = null;
- this._orphaned = null;
- this._retainedOrphans = null;
- this._refMap = null;
-
- this.tabActor.off("will-navigate", this.onFrameUnload);
- this.tabActor.off("window-ready", this.onFrameLoad);
-
- this.onFrameLoad = null;
- this.onFrameUnload = null;
-
- this.walkerSearch.destroy();
-
- this.layoutChangeObserver.off("reflows", this._onReflows);
- this.layoutChangeObserver.off("resize", this._onResize);
- this.layoutChangeObserver = null;
- releaseLayoutChangesObserver(this.tabActor);
-
- eventListenerService.removeListenerChangeListener(
- this._onEventListenerChange);
-
- this.onMutations = null;
-
- this.layoutActor = null;
- this.tabActor = null;
-
- this.emit("destroyed");
- } catch (e) {
- console.error(e);
- }
- },
-
- release: function () {},
-
- unmanage: function (actor) {
- if (actor instanceof NodeActor) {
- if (this._activePseudoClassLocks &&
- this._activePseudoClassLocks.has(actor)) {
- this.clearPseudoClassLocks(actor);
- }
- this._refMap.delete(actor.rawNode);
- }
- protocol.Actor.prototype.unmanage.call(this, actor);
- },
-
- /**
- * Determine if the walker has come across this DOM node before.
- * @param {DOMNode} rawNode
- * @return {Boolean}
- */
- hasNode: function (rawNode) {
- return this._refMap.has(rawNode);
- },
-
- /**
- * If the walker has come across this DOM node before, then get the
- * corresponding node actor.
- * @param {DOMNode} rawNode
- * @return {NodeActor}
- */
- getNode: function (rawNode) {
- return this._refMap.get(rawNode);
- },
-
- _ref: function (node) {
- let actor = this.getNode(node);
- if (actor) {
- return actor;
- }
-
- actor = new NodeActor(this, node);
-
- // Add the node actor as a child of this walker actor, assigning
- // it an actorID.
- this.manage(actor);
- this._refMap.set(node, actor);
-
- if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
- actor.watchDocument(this.onMutations);
- }
- return actor;
- },
-
- _onReflows: function (reflows) {
- // Going through the nodes the walker knows about, see which ones have
- // had their display changed and send a display-change event if any
- let changes = [];
- for (let [node, actor] of this._refMap) {
- if (Cu.isDeadWrapper(node)) {
- continue;
- }
-
- let isDisplayed = actor.isDisplayed;
- if (isDisplayed !== actor.wasDisplayed) {
- changes.push(actor);
- // Updating the original value
- actor.wasDisplayed = isDisplayed;
- }
- }
-
- if (changes.length) {
- this.emit("display-change", changes);
- }
- },
-
- /**
- * When the browser window gets resized, relay the event to the front.
- */
- _onResize: function () {
- this.emit("resize");
- },
-
- /**
- * This is kept for backward-compatibility reasons with older remote targets.
- * Targets prior to bug 916443.
- *
- * pick/cancelPick are used to pick a node on click on the content
- * document. But in their implementation prior to bug 916443, they don't allow
- * highlighting on hover.
- * The client-side now uses the highlighter actor's pick and cancelPick
- * methods instead. The client-side uses the the highlightable trait found in
- * the root actor to determine which version of pick to use.
- *
- * As for highlight, the new highlighter actor is used instead of the walker's
- * highlight method. Same here though, the client-side uses the highlightable
- * trait to dertermine which to use.
- *
- * Keeping these actor methods for now allows newer client-side debuggers to
- * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
- */
- pick: function () {},
- cancelPick: function () {},
- highlight: function (node) {},
-
- /**
- * Ensures that the node is attached and it can be accessed from the root.
- *
- * @param {(Node|NodeActor)} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNode type.
- */
- attachElement: function (node) {
- let { nodes, newParents } = this.attachElements([node]);
- return {
- node: nodes[0],
- newParents: newParents
- };
- },
-
- /**
- * Ensures that the nodes are attached and they can be accessed from the root.
- *
- * @param {(Node[]|NodeActor[])} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNodeArray type.
- */
- attachElements: function (nodes) {
- let nodeActors = [];
- let newParents = new Set();
- for (let node of nodes) {
- if (!(node instanceof NodeActor)) {
- // If an anonymous node was passed in and we aren't supposed to know
- // about it, then consult with the document walker as the source of
- // truth about which elements exist.
- if (!this.showAllAnonymousContent && isAnonymous(node)) {
- node = this.getDocumentWalker(node).currentNode;
- }
-
- node = this._ref(node);
- }
-
- this.ensurePathToRoot(node, newParents);
- // If nodes may be an array of raw nodes, we're sure to only have
- // NodeActors with the following array.
- nodeActors.push(node);
- }
-
- return {
- nodes: nodeActors,
- newParents: [...newParents]
- };
- },
-
- /**
- * Return the document node that contains the given node,
- * or the root node if no node is specified.
- * @param NodeActor node
- * The node whose document is needed, or null to
- * return the root.
- */
- document: function (node) {
- let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
- return this._ref(doc);
- },
-
- /**
- * Return the documentElement for the document containing the
- * given node.
- * @param NodeActor node
- * The node whose documentElement is requested, or null
- * to use the root document.
- */
- documentElement: function (node) {
- let elt = isNodeDead(node)
- ? this.rootDoc.documentElement
- : nodeDocument(node.rawNode).documentElement;
- return this._ref(elt);
- },
-
- /**
- * Return all parents of the given node, ordered from immediate parent
- * to root.
- * @param NodeActor node
- * The node whose parents are requested.
- * @param object options
- * Named options, including:
- * `sameDocument`: If true, parents will be restricted to the same
- * document as the node.
- * `sameTypeRootTreeItem`: If true, this will not traverse across
- * different types of docshells.
- */
- parents: function (node, options = {}) {
- if (isNodeDead(node)) {
- return [];
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let parents = [];
- let cur;
- while ((cur = walker.parentNode())) {
- if (options.sameDocument &&
- nodeDocument(cur) != nodeDocument(node.rawNode)) {
- break;
- }
-
- if (options.sameTypeRootTreeItem &&
- nodeDocshell(cur).sameTypeRootTreeItem !=
- nodeDocshell(node.rawNode).sameTypeRootTreeItem) {
- break;
- }
-
- parents.push(this._ref(cur));
- }
- return parents;
- },
-
- parentNode: function (node) {
- let walker = this.getDocumentWalker(node.rawNode);
- let parent = walker.parentNode();
- if (parent) {
- return this._ref(parent);
- }
- return null;
- },
-
- /**
- * If the given NodeActor only has a single text node as a child with a text
- * content small enough to be inlined, return that child's NodeActor.
- *
- * @param NodeActor node
- */
- inlineTextChild: function (node) {
- // Quick checks to prevent creating a new walker if possible.
- if (node.isBeforePseudoElement ||
- node.isAfterPseudoElement ||
- node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
- node.rawNode.children.length > 0) {
- return undefined;
- }
-
- let docWalker = this.getDocumentWalker(node.rawNode);
- let firstChild = docWalker.firstChild();
-
- // Bail out if:
- // - more than one child
- // - unique child is not a text node
- // - unique child is a text node, but is too long to be inlined
- if (!firstChild ||
- docWalker.nextSibling() ||
- firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
- firstChild.nodeValue.length > gValueSummaryLength
- ) {
- return undefined;
- }
-
- return this._ref(firstChild);
- },
-
- /**
- * Mark a node as 'retained'.
- *
- * A retained node is not released when `releaseNode` is called on its
- * parent, or when a parent is released with the `cleanup` option to
- * `getMutations`.
- *
- * When a retained node's parent is released, a retained mode is added to
- * the walker's "retained orphans" list.
- *
- * Retained nodes can be deleted by providing the `force` option to
- * `releaseNode`. They will also be released when their document
- * has been destroyed.
- *
- * Retaining a node makes no promise about its children; They can
- * still be removed by normal means.
- */
- retainNode: function (node) {
- node.retained = true;
- },
-
- /**
- * Remove the 'retained' mark from a node. If the node was a
- * retained orphan, release it.
- */
- unretainNode: function (node) {
- node.retained = false;
- if (this._retainedOrphans.has(node)) {
- this._retainedOrphans.delete(node);
- this.releaseNode(node);
- }
- },
-
- /**
- * Release actors for a node and all child nodes.
- */
- releaseNode: function (node, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- if (node.retained && !options.force) {
- this._retainedOrphans.add(node);
- return;
- }
-
- if (node.retained) {
- // Forcing a retained node to go away.
- this._retainedOrphans.delete(node);
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
-
- let child = walker.firstChild();
- while (child) {
- let childActor = this.getNode(child);
- if (childActor) {
- this.releaseNode(childActor, options);
- }
- child = walker.nextSibling();
- }
-
- node.destroy();
- },
-
- /**
- * Add any nodes between `node` and the walker's root node that have not
- * yet been seen by the client.
- */
- ensurePathToRoot: function (node, newParents = new Set()) {
- if (!node) {
- return newParents;
- }
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let parent = this.getNode(cur);
- if (!parent) {
- // This parent didn't exist, so hasn't been seen by the client yet.
- newParents.add(this._ref(cur));
- } else {
- // This parent did exist, so the client knows about it.
- return newParents;
- }
- }
- return newParents;
- },
-
- /**
- * Return children of the given node. By default this method will return
- * all children of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- children: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- if (options.center && options.start) {
- throw Error("Can't specify both 'center' and 'start' options.");
- }
- let maxNodes = options.maxNodes || -1;
- if (maxNodes == -1) {
- maxNodes = Number.MAX_VALUE;
- }
-
- // We're going to create a few document walkers with the same filter,
- // make it easier.
- let getFilteredWalker = documentWalkerNode => {
- let { whatToShow } = options;
- // Use SKIP_TO_SIBLING to force the walker to use a sibling of the provided node
- // in case this one is incompatible with the walker's filter function.
- return this.getDocumentWalker(documentWalkerNode, whatToShow, SKIP_TO_SIBLING);
- };
-
- // Need to know the first and last child.
- let rawNode = node.rawNode;
- let firstChild = getFilteredWalker(rawNode).firstChild();
- let lastChild = getFilteredWalker(rawNode).lastChild();
-
- if (!firstChild) {
- // No children, we're done.
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let start;
- if (options.center) {
- start = options.center.rawNode;
- } else if (options.start) {
- start = options.start.rawNode;
- } else {
- start = firstChild;
- }
-
- let nodes = [];
-
- // Start by reading backward from the starting point if we're centering...
- let backwardWalker = getFilteredWalker(start);
- if (backwardWalker.currentNode != firstChild && options.center) {
- backwardWalker.previousSibling();
- let backwardCount = Math.floor(maxNodes / 2);
- let backwardNodes = this._readBackward(backwardWalker, backwardCount);
- nodes = backwardNodes;
- }
-
- // Then read forward by any slack left in the max children...
- let forwardWalker = getFilteredWalker(start);
- let forwardCount = maxNodes - nodes.length;
- nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
-
- // If there's any room left, it means we've run all the way to the end.
- // If we're centering, check if there are more items to read at the front.
- let remaining = maxNodes - nodes.length;
- if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
- let firstNodes = this._readBackward(backwardWalker, remaining);
-
- // Then put it all back together.
- nodes = firstNodes.concat(nodes);
- }
-
- return {
- hasFirst: nodes[0].rawNode == firstChild,
- hasLast: nodes[nodes.length - 1].rawNode == lastChild,
- nodes: nodes
- };
- },
-
- /**
- * Return siblings of the given node. By default this method will return
- * all siblings of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * If `start` or `center` are not specified, this method will center on the
- * node whose siblings are requested.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- siblings: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow)
- .parentNode();
- if (!parentNode) {
- return {
- hasFirst: true,
- hasLast: true,
- nodes: [node]
- };
- }
-
- if (!(options.start || options.center)) {
- options.center = node;
- }
-
- return this.children(this._ref(parentNode), options);
- },
-
- /**
- * Get the next sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- nextSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.nextSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Get the previous sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- previousSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.previousSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Helper function for the `children` method: Read forward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readForward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.nextSibling();
- } while (node && --count);
- return ret;
- },
-
- /**
- * Helper function for the `children` method: Read backward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readBackward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.previousSibling();
- } while (node && --count);
- ret.reverse();
- return ret;
- },
-
- /**
- * Return the first node in the document that matches the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelector: function (baseNode, selector) {
- if (isNodeDead(baseNode)) {
- return {};
- }
-
- let node = baseNode.rawNode.querySelector(selector);
- if (!node) {
- return {};
- }
-
- return this.attachElement(node);
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelectorAll: function (baseNode, selector) {
- let nodeList = null;
-
- try {
- nodeList = baseNode.rawNode.querySelectorAll(selector);
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
-
- return new NodeListActor(this, nodeList);
- },
-
- /**
- * Get a list of nodes that match the given selector in all known frames of
- * the current content page.
- * @param {String} selector.
- * @return {Array}
- */
- _multiFrameQuerySelectorAll: function (selector) {
- let nodes = [];
-
- for (let {document} of this.tabActor.windows) {
- try {
- nodes = [...nodes, ...document.querySelectorAll(selector)];
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
- }
-
- return nodes;
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector in all
- * frames of the current content page.
- * @param {String} selector
- */
- multiFrameQuerySelectorAll: function (selector) {
- return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector));
- },
-
- /**
- * Search the document for a given string.
- * Results will be searched with the walker-search module (searches through
- * tag names, attribute names and values, and text contents).
- *
- * @returns {searchresult}
- * - {NodeList} list
- * - {Array<Object>} metadata. Extra information with indices that
- * match up with node list.
- */
- search: function (query) {
- let results = this.walkerSearch.search(query);
- let nodeList = new NodeListActor(this, results.map(r => r.node));
-
- return {
- list: nodeList,
- metadata: []
- };
- },
-
- /**
- * Returns a list of matching results for CSS selector autocompletion.
- *
- * @param string query
- * The selector query being completed
- * @param string completing
- * The exact token being completed out of the query
- * @param string selectorState
- * One of "pseudo", "id", "tag", "class", "null"
- */
- getSuggestionsForQuery: function (query, completing, selectorState) {
- let sugs = {
- classes: new Map(),
- tags: new Map(),
- ids: new Map()
- };
- let result = [];
- let nodes = null;
- // Filtering and sorting the results so that protocol transfer is miminal.
- switch (selectorState) {
- case "pseudo":
- result = PSEUDO_SELECTORS.filter(item => {
- return item[0].startsWith(":" + completing);
- });
- break;
-
- case "class":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[class]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- if (className.startsWith(completing)) {
- result.push(["." + CSS.escape(className), count, selectorState]);
- }
- }
- break;
-
- case "id":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[id]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- }
- for (let [id, count] of sugs.ids) {
- if (id.startsWith(completing) && id !== "") {
- result.push(["#" + CSS.escape(id), count, selectorState]);
- }
- }
- break;
-
- case "tag":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("*");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- }
- for (let [tag, count] of sugs.tags) {
- if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
- result.push([tag, count, selectorState]);
- }
- }
-
- // For state 'tag' (no preceding # or .) and when there's no query (i.e.
- // only one word) then search for the matching classes and ids
- if (!query) {
- result = [
- ...result,
- ...this.getSuggestionsForQuery(null, completing, "class")
- .suggestions,
- ...this.getSuggestionsForQuery(null, completing, "id")
- .suggestions
- ];
- }
-
- break;
-
- case "null":
- nodes = this._multiFrameQuerySelectorAll(query);
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- for (let [tag, count] of sugs.tags) {
- tag && result.push([tag, count]);
- }
- for (let [id, count] of sugs.ids) {
- id && result.push(["#" + id, count]);
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- className && result.push(["." + className, count]);
- }
- }
-
- // Sort by count (desc) and name (asc)
- result = result.sort((a, b) => {
- // Computed a sortable string with first the inverted count, then the name
- let sortA = (10000 - a[1]) + a[0];
- let sortB = (10000 - b[1]) + b[0];
-
- // Prefixing ids, classes and tags, to group results
- let firstA = a[0].substring(0, 1);
- let firstB = b[0].substring(0, 1);
-
- if (firstA === "#") {
- sortA = "2" + sortA;
- } else if (firstA === ".") {
- sortA = "1" + sortA;
- } else {
- sortA = "0" + sortA;
- }
-
- if (firstB === "#") {
- sortB = "2" + sortB;
- } else if (firstB === ".") {
- sortB = "1" + sortB;
- } else {
- sortB = "0" + sortB;
- }
-
- // String compare
- return sortA.localeCompare(sortB);
- });
-
- result.slice(0, 25);
-
- return {
- query: query,
- suggestions: result
- };
- },
-
- /**
- * Add a pseudo-class lock to a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be added
- * to parent nodes.
- * `enabled`: False if the pseudo-class should be locked
- * to 'off'. Defaults to true.
- *
- * @returns An empty packet. A "pseudoClassLock" mutation will
- * be queued for any changed nodes.
- */
- addPseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- // There can be only one node locked per pseudo, so dismiss all existing
- // ones
- for (let locked of this._activePseudoClassLocks) {
- if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- let enabled = options.enabled === undefined ||
- options.enabled;
- this._addPseudoClassLock(node, pseudo, enabled);
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._addPseudoClassLock(curNode, pseudo, enabled);
- }
- },
-
- _queuePseudoClassMutation: function (node) {
- this.queueMutation({
- target: node.actorID,
- type: "pseudoClassLock",
- pseudoClassLocks: node.writePseudoClassLocks()
- });
- },
-
- _addPseudoClassLock: function (node, pseudo, enabled) {
- if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
- this._activePseudoClassLocks.add(node);
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- hideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
- node.rawNode.classList.add(HIDDEN_CLASS);
- },
-
- unhideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- node.rawNode.classList.remove(HIDDEN_CLASS);
- },
-
- /**
- * Remove a pseudo-class lock from a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be removed
- * from parent nodes.
- *
- * @returns An empty response. "pseudoClassLock" mutations
- * will be emitted for any changed nodes.
- */
- removePseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- this._removePseudoClassLock(node, pseudo);
-
- // Remove pseudo class for children as we don't want to allow
- // turning it on for some childs without setting it on some parents
- for (let locked of this._activePseudoClassLocks) {
- if (node.rawNode.contains(locked.rawNode) &&
- InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._removePseudoClassLock(curNode, pseudo);
- }
- },
-
- _removePseudoClassLock: function (node, pseudo) {
- if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
- if (!node.writePseudoClassLocks()) {
- this._activePseudoClassLocks.delete(node);
- }
-
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- /**
- * Clear all the pseudo-classes on a given node or all nodes.
- * @param {NodeActor} node Optional node to clear pseudo-classes on
- */
- clearPseudoClassLocks: function (node) {
- if (node && isNodeDead(node)) {
- return;
- }
-
- if (node) {
- InspectorUtils.clearPseudoClassLocks(node.rawNode);
- this._activePseudoClassLocks.delete(node);
- this._queuePseudoClassMutation(node);
- } else {
- for (let locked of this._activePseudoClassLocks) {
- InspectorUtils.clearPseudoClassLocks(locked.rawNode);
- this._activePseudoClassLocks.delete(locked);
- this._queuePseudoClassMutation(locked);
- }
- }
- },
-
- /**
- * Get a node's innerHTML property.
- */
- innerHTML: function (node) {
- let html = "";
- if (!isNodeDead(node)) {
- html = node.rawNode.innerHTML;
- }
- return LongStringActor(this.conn, html);
- },
-
- /**
- * Set a node's innerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setInnerHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let rawNode = node.rawNode;
- if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) {
- throw new Error("Can only change innerHTML to element nodes");
- }
- // eslint-disable-next-line no-unsanitized/property
- rawNode.innerHTML = value;
- },
-
- /**
- * Get a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- */
- outerHTML: function (node) {
- let outerHTML = "";
- if (!isNodeDead(node)) {
- outerHTML = node.rawNode.outerHTML;
- }
- return LongStringActor(this.conn, outerHTML);
- },
-
- /**
- * Set a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setOuterHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let parsedDOM = DOMParser.parseFromString(value, "text/html");
- let rawNode = node.rawNode;
- let parentNode = rawNode.parentNode;
-
- // Special case for head and body. Setting document.body.outerHTML
- // creates an extra <head> tag, and document.head.outerHTML creates
- // an extra <body>. So instead we will call replaceChild with the
- // parsed DOM, assuming that they aren't trying to set both tags at once.
- if (rawNode.tagName === "BODY") {
- if (parsedDOM.head.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.body, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (rawNode.tagName === "HEAD") {
- if (parsedDOM.body.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.head, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (node.isDocumentElement()) {
- // Unable to set outerHTML on the document element. Fall back by
- // setting attributes manually, then replace the body and head elements.
- let finalAttributeModifications = [];
- let attributeModifications = {};
- for (let attribute of rawNode.attributes) {
- attributeModifications[attribute.name] = null;
- }
- for (let attribute of parsedDOM.documentElement.attributes) {
- attributeModifications[attribute.name] = attribute.value;
- }
- for (let key in attributeModifications) {
- finalAttributeModifications.push({
- attributeName: key,
- newValue: attributeModifications[key]
- });
- }
- node.modifyAttributes(finalAttributeModifications);
- rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
- rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- },
-
- /**
- * Insert adjacent HTML to a node.
- *
- * @param {Node} node
- * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
- * "afterEnd" (see Element.insertAdjacentHTML).
- * @param {string} value The HTML content.
- */
- insertAdjacentHTML: function (node, position, value) {
- if (isNodeDead(node)) {
- return {node: [], newParents: []};
- }
-
- let rawNode = node.rawNode;
- let isInsertAsSibling = position === "beforeBegin" ||
- position === "afterEnd";
-
- // Don't insert anything adjacent to the document element.
- if (isInsertAsSibling && node.isDocumentElement()) {
- throw new Error("Can't insert adjacent element to the root.");
- }
-
- let rawParentNode = rawNode.parentNode;
- if (!rawParentNode && isInsertAsSibling) {
- throw new Error("Can't insert as sibling without parent node.");
- }
-
- // We can't use insertAdjacentHTML, because we want to return the nodes
- // being created (so the front can remove them if the user undoes
- // the change). So instead, use Range.createContextualFragment().
- let range = rawNode.ownerDocument.createRange();
- if (position === "beforeBegin" || position === "afterEnd") {
- range.selectNode(rawNode);
- } else {
- range.selectNodeContents(rawNode);
- }
- let docFrag = range.createContextualFragment(value);
- let newRawNodes = Array.from(docFrag.childNodes);
- switch (position) {
- case "beforeBegin":
- rawParentNode.insertBefore(docFrag, rawNode);
- break;
- case "afterEnd":
- // Note: if the second argument is null, rawParentNode.insertBefore
- // behaves like rawParentNode.appendChild.
- rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
- break;
- case "afterBegin":
- rawNode.insertBefore(docFrag, rawNode.firstChild);
- break;
- case "beforeEnd":
- rawNode.appendChild(docFrag);
- break;
- default:
- throw new Error("Invalid position value. Must be either " +
- "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'.");
- }
-
- return this.attachElements(newRawNodes);
- },
-
- /**
- * Duplicate a specified node
- *
- * @param {NodeActor} node The node to duplicate.
- */
- duplicateNode: function ({rawNode}) {
- let clonedNode = rawNode.cloneNode(true);
- rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
- },
-
- /**
- * Test whether a node is a document or a document element.
- *
- * @param {NodeActor} node The node to remove.
- * @return {boolean} True if the node is a document or a document element.
- */
- isDocumentOrDocumentElementNode: function (node) {
- return ((node.rawNode.ownerDocument &&
- node.rawNode.ownerDocument.documentElement === this.rawNode) ||
- node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
- },
-
- /**
- * Removes a node from its parent node.
- *
- * @param {NodeActor} node The node to remove.
- * @returns The node's nextSibling before it was removed.
- */
- removeNode: function (node) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes.");
- }
-
- let nextSibling = this.nextSibling(node);
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- return nextSibling;
- },
-
- /**
- * Removes an array of nodes from their parent node.
- *
- * @param {NodeActor[]} nodes The nodes to remove.
- */
- removeNodes: function (nodes) {
- // Check that all nodes are valid before processing the removals.
- for (let node of nodes) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes");
- }
- }
-
- for (let node of nodes) {
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- }
- },
-
- /**
- * Insert a node into the DOM.
- */
- insertBefore: function (node, parent, sibling) {
- if (isNodeDead(node) ||
- isNodeDead(parent) ||
- (sibling && isNodeDead(sibling))) {
- return;
- }
-
- let rawNode = node.rawNode;
- let rawParent = parent.rawNode;
- let rawSibling = sibling ? sibling.rawNode : null;
-
- // Don't bother inserting a node if the document position isn't going
- // to change. This prevents needless iframes reloading and mutations.
- if (rawNode.parentNode === rawParent) {
- let currentNextSibling = this.nextSibling(node);
- currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
- null;
-
- if (rawNode === rawSibling || currentNextSibling === rawSibling) {
- return;
- }
- }
-
- rawParent.insertBefore(rawNode, rawSibling);
- },
-
- /**
- * Editing a node's tagname actually means creating a new node with the same
- * attributes, removing the node and inserting the new one instead.
- * This method does not return anything as mutation events are taking care of
- * informing the consumers about changes.
- */
- editTagName: function (node, tagName) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let oldNode = node.rawNode;
-
- // Create a new element with the same attributes as the current element and
- // prepare to replace the current node with it.
- let newNode;
- try {
- newNode = nodeDocument(oldNode).createElement(tagName);
- } catch (x) {
- // Failed to create a new element with that tag name, ignore the change,
- // and signal the error to the front.
- return Promise.reject(new Error("Could not change node's tagName to " + tagName));
- }
-
- let attrs = oldNode.attributes;
- for (let i = 0; i < attrs.length; i++) {
- newNode.setAttribute(attrs[i].name, attrs[i].value);
- }
-
- // Insert the new node, and transfer the old node's children.
- oldNode.parentNode.insertBefore(newNode, oldNode);
- while (oldNode.firstChild) {
- newNode.appendChild(oldNode.firstChild);
- }
-
- oldNode.remove();
- return null;
- },
-
- /**
- * Get any pending mutation records. Must be called by the client after
- * the `new-mutations` notification is received. Returns an array of
- * mutation records.
- *
- * Mutation records have a basic structure:
- *
- * {
- * type: attributes|characterData|childList,
- * target: <domnode actor ID>,
- * }
- *
- * And additional attributes based on the mutation type:
- *
- * `attributes` type:
- * attributeName: <string> - the attribute that changed
- * attributeNamespace: <string> - the attribute's namespace URI, if any.
- * newValue: <string> - The new value of the attribute, if any.
- *
- * `characterData` type:
- * newValue: <string> - the new nodeValue for the node
- *
- * `childList` type is returned when the set of children for a node
- * has changed. Includes extra data, which can be used by the client to
- * maintain its ownership subtree.
- *
- * added: array of <domnode actor ID> - The list of actors *previously
- * seen by the client* that were added to the target node.
- * removed: array of <domnode actor ID> The list of actors *previously
- * seen by the client* that were removed from the target node.
- * inlineTextChild: If the node now has a single text child, it will
- * be sent here.
- *
- * Actors that are included in a MutationRecord's `removed` but
- * not in an `added` have been removed from the client's ownership
- * tree (either by being moved under a node the client has seen yet
- * or by being removed from the tree entirely), and is considered
- * 'orphaned'.
- *
- * Keep in mind that if a node that the client hasn't seen is moved
- * into or out of the target node, it will not be included in the
- * removedNodes and addedNodes list, so if the client is interested
- * in the new set of children it needs to issue a `children` request.
- */
- getMutations: function (options = {}) {
- let pending = this._pendingMutations || [];
- this._pendingMutations = [];
- this._waitingForGetMutations = false;
-
- if (options.cleanup) {
- for (let node of this._orphaned) {
- // Release the orphaned node. Nodes or children that have been
- // retained will be moved to this._retainedOrphans.
- this.releaseNode(node);
- }
- this._orphaned = new Set();
- }
-
- return pending;
- },
-
- queueMutation: function (mutation) {
- if (!this.actorID || this._destroyed) {
- // We've been destroyed, don't bother queueing this mutation.
- return;
- }
-
- // Add the mutation to the list of mutations to be retrieved next.
- this._pendingMutations.push(mutation);
-
- // Bail out if we already emitted a new-mutations event and are waiting for a client
- // to retrieve them.
- if (this._waitingForGetMutations) {
- return;
- }
-
- if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
- this._emitNewMutations();
- } else {
- /**
- * If many mutations are fired at the same time, clients might sequentially request
- * children/siblings for updated nodes, which can be costly. By throttling the calls
- * to getMutations, duplicated mutations will be ignored.
- */
- this._throttledEmitNewMutations();
- }
- },
-
- _emitNewMutations: function () {
- if (!this.actorID || this._destroyed) {
- // Bail out if the actor was destroyed after throttling this call.
- return;
- }
-
- if (this._waitingForGetMutations || this._pendingMutations.length == 0) {
- // Bail out if we already fired the new-mutation event or if no mutations are
- // waiting to be retrieved.
- return;
- }
-
- this._waitingForGetMutations = true;
- this.emit("new-mutations");
- },
-
- /**
- * Handles mutations from the DOM mutation observer API.
- *
- * @param array[MutationRecord] mutations
- * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
- */
- onMutations: function (mutations) {
- // Notify any observers that want *all* mutations (even on nodes that aren't
- // referenced). This is not sent over the protocol so can only be used by
- // scripts running in the server process.
- this.emit("any-mutation");
-
- for (let change of mutations) {
- let targetActor = this.getNode(change.target);
- if (!targetActor) {
- continue;
- }
- let targetNode = change.target;
- let type = change.type;
- let mutation = {
- type: type,
- target: targetActor.actorID,
- };
-
- if (type === "attributes") {
- mutation.attributeName = change.attributeName;
- mutation.attributeNamespace = change.attributeNamespace || undefined;
- mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
- targetNode.getAttribute(mutation.attributeName)
- : null;
- } else if (type === "characterData") {
- mutation.newValue = targetNode.nodeValue;
- this._maybeQueueInlineTextChildMutation(change, targetNode);
- } else if (type === "childList" || type === "nativeAnonymousChildList") {
- // Get the list of removed and added actors that the client has seen
- // so that it can keep its ownership tree up to date.
- let removedActors = [];
- let addedActors = [];
- for (let removed of change.removedNodes) {
- let removedActor = this.getNode(removed);
- if (!removedActor) {
- // If the client never encountered this actor we don't need to
- // mention that it was removed.
- continue;
- }
- // While removed from the tree, nodes are saved as orphaned.
- this._orphaned.add(removedActor);
- removedActors.push(removedActor.actorID);
- }
- for (let added of change.addedNodes) {
- let addedActor = this.getNode(added);
- if (!addedActor) {
- // If the client never encounted this actor we don't need to tell
- // it about its addition for ownership tree purposes - if the
- // client wants to see the new nodes it can ask for children.
- continue;
- }
- // The actor is reconnected to the ownership tree, unorphan
- // it and let the client know so that its ownership tree is up
- // to date.
- this._orphaned.delete(addedActor);
- addedActors.push(addedActor.actorID);
- }
-
- mutation.numChildren = targetActor.numChildren;
- mutation.removed = removedActors;
- mutation.added = addedActors;
-
- let inlineTextChild = this.inlineTextChild(targetActor);
- if (inlineTextChild) {
- mutation.inlineTextChild = inlineTextChild.form();
- }
- }
- this.queueMutation(mutation);
- }
- },
-
- /**
- * Check if the provided mutation could change the way the target element is
- * inlined with its parent node. If it might, a custom mutation of type
- * "inlineTextChild" will be queued.
- *
- * @param {MutationRecord} mutation
- * A characterData type mutation
- */
- _maybeQueueInlineTextChildMutation: function (mutation) {
- let {oldValue, target} = mutation;
- let newValue = target.nodeValue;
- let limit = gValueSummaryLength;
-
- if ((oldValue.length <= limit && newValue.length <= limit) ||
- (oldValue.length > limit && newValue.length > limit)) {
- // Bail out if the new & old values are both below/above the size limit.
- return;
- }
-
- let parentActor = this.getNode(target.parentNode);
- if (!parentActor || parentActor.rawNode.children.length > 0) {
- // If the parent node has other children, a character data mutation will
- // not change anything regarding inlining text nodes.
- return;
- }
-
- let inlineTextChild = this.inlineTextChild(parentActor);
- this.queueMutation({
- type: "inlineTextChild",
- target: parentActor.actorID,
- inlineTextChild:
- inlineTextChild ? inlineTextChild.form() : undefined
- });
- },
-
- onFrameLoad: function ({ window, isTopLevel }) {
- let { readyState } = window.document;
- if (readyState != "interactive" && readyState != "complete") {
- window.addEventListener("DOMContentLoaded",
- this.onFrameLoad.bind(this, { window, isTopLevel }),
- { once: true });
- return;
- }
- if (isTopLevel) {
- // If we initialize the inspector while the document is loading,
- // we may already have a root document set in the constructor.
- if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) &&
- this.rootDoc.defaultView) {
- this.onFrameUnload({ window: this.rootDoc.defaultView });
- }
- // Update all DOM objects references to target the new document.
- this.rootWin = window;
- this.rootDoc = window.document;
- this.rootNode = this.document();
- this.queueMutation({
- type: "newRoot",
- target: this.rootNode.form()
- });
- return;
- }
- let frame = getFrameElement(window);
- let frameActor = this.getNode(frame);
- if (!frameActor) {
- return;
- }
-
- this.queueMutation({
- type: "frameLoad",
- target: frameActor.actorID,
- });
-
- // Send a childList mutation on the frame.
- this.queueMutation({
- type: "childList",
- target: frameActor.actorID,
- added: [],
- removed: []
- });
- },
-
- // Returns true if domNode is in window or a subframe.
- _childOfWindow: function (window, domNode) {
- let win = nodeDocument(domNode).defaultView;
- while (win) {
- if (win === window) {
- return true;
- }
- win = getFrameElement(win);
- }
- return false;
- },
-
- onFrameUnload: function ({ window }) {
- // Any retained orphans that belong to this document
- // or its children need to be released, and a mutation sent
- // to notify of that.
- let releasedOrphans = [];
-
- for (let retained of this._retainedOrphans) {
- if (Cu.isDeadWrapper(retained.rawNode) ||
- this._childOfWindow(window, retained.rawNode)) {
- this._retainedOrphans.delete(retained);
- releasedOrphans.push(retained.actorID);
- this.releaseNode(retained, { force: true });
- }
- }
-
- if (releasedOrphans.length > 0) {
- this.queueMutation({
- target: this.rootNode.actorID,
- type: "unretained",
- nodes: releasedOrphans
- });
- }
-
- let doc = window.document;
- let documentActor = this.getNode(doc);
- if (!documentActor) {
- return;
- }
-
- if (this.rootDoc === doc) {
- this.rootDoc = null;
- this.rootNode = null;
- }
-
- this.queueMutation({
- type: "documentUnload",
- target: documentActor.actorID
- });
-
- let walker = this.getDocumentWalker(doc);
- let parentNode = walker.parentNode();
- if (parentNode) {
- // Send a childList mutation on the frame so that clients know
- // they should reread the children list.
- this.queueMutation({
- type: "childList",
- target: this.getNode(parentNode).actorID,
- added: [],
- removed: []
- });
- }
-
- // Need to force a release of this node, because those nodes can't
- // be accessed anymore.
- this.releaseNode(documentActor, { force: true });
- },
-
- /**
- * Check if a node is attached to the DOM tree of the current page.
- * @param {nsIDomNode} rawNode
- * @return {Boolean} false if the node is removed from the tree or within a
- * document fragment
- */
- _isInDOMTree: function (rawNode) {
- let walker = this.getDocumentWalker(rawNode);
- let current = walker.currentNode;
-
- // Reaching the top of tree
- while (walker.parentNode()) {
- current = walker.currentNode;
- }
-
- // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
- // attached
- if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
- current !== this.rootDoc) {
- return false;
- }
-
- // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
- return true;
- },
-
- /**
- * @see _isInDomTree
- */
- isInDOMTree: function (node) {
- if (isNodeDead(node)) {
- return false;
- }
- return this._isInDOMTree(node.rawNode);
- },
-
- /**
- * Given an ObjectActor (identified by its ID), commonly used in the debugger,
- * webconsole and variablesView, return the corresponding inspector's
- * NodeActor
- */
- getNodeActorFromObjectActor: function (objectActorID) {
- let actor = this.conn.getActor(objectActorID);
- if (!actor) {
- return null;
- }
-
- let debuggerObject = this.conn.getActor(objectActorID).obj;
- let rawNode = debuggerObject.unsafeDereference();
-
- if (!this._isInDOMTree(rawNode)) {
- return null;
- }
-
- // This is a special case for the document object whereby it is considered
- // as document.documentElement (the <html> node)
- if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
- rawNode = rawNode.documentElement;
- }
-
- return this.attachElement(rawNode);
- },
-
- /**
- * Given a windowID return the NodeActor for the corresponding frameElement,
- * unless it's the root window
- */
- getNodeActorFromWindowID: function (windowID) {
- let win;
-
- try {
- win = Services.wm.getOuterWindowWithId(windowID);
- } catch (e) {
- // ignore
- }
-
- if (!win) {
- return { error: "noWindow",
- message: "The related docshell is destroyed or not found" };
- } else if (!win.frameElement) {
- // the frame element of the root document is privileged & thus
- // inaccessible, so return the document body/element instead
- return this.attachElement(win.document.body || win.document.documentElement);
- }
-
- return this.attachElement(win.frameElement);
- },
-
- /**
- * Given a StyleSheetActor (identified by its ID), commonly used in the
- * style-editor, get its ownerNode and return the corresponding walker's
- * NodeActor.
- * Note that getNodeFromActor was added later and can now be used instead.
- */
- getStyleSheetOwnerNode: function (styleSheetActorID) {
- return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
- },
-
- /**
- * This method can be used to retrieve NodeActor for DOM nodes from other
- * actors in a way that they can later be highlighted in the page, or
- * selected in the inspector.
- * If an actor has a reference to a DOM node, and the UI needs to know about
- * this DOM node (and possibly select it in the inspector), the UI should
- * first retrieve a reference to the walkerFront:
- *
- * // Make sure the inspector/walker have been initialized first.
- * toolbox.initInspector().then(() => {
- * // Retrieve the walker.
- * let walker = toolbox.walker;
- * });
- *
- * And then call this method:
- *
- * // Get the nodeFront from my actor, passing the ID and properties path.
- * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
- * // Use the nodeFront, e.g. select the node in the inspector.
- * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
- * });
- *
- * @param {String} actorID The ID for the actor that has a reference to the
- * DOM node.
- * @param {Array} path Where, on the actor, is the DOM node stored. If in the
- * scope of the actor, the node is available as `this.data.node`, then this
- * should be ["data", "node"].
- * @return {NodeActor} The attached NodeActor, or null if it couldn't be
- * found.
- */
- getNodeFromActor: function (actorID, path) {
- let actor = this.conn.getActor(actorID);
- if (!actor) {
- return null;
- }
-
- let obj = actor;
- for (let name of path) {
- if (!(name in obj)) {
- return null;
- }
- obj = obj[name];
- }
-
- return this.attachElement(obj);
- },
-
- /**
- * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
- * information.
- *
- * @return {LayoutActor}
- */
- getLayoutInspector: function () {
- if (!this.layoutActor) {
- this.layoutActor = new LayoutActor(this.conn, this.tabActor, this);
- }
-
- return this.layoutActor;
- },
-
- /**
- * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
- * returns null.
- */
- getOffsetParent: function (node) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let offsetParent = node.rawNode.offsetParent;
-
- if (!offsetParent) {
- return null;
- }
-
- return this._ref(offsetParent);
- },
-});
/**
* Server side of the inspector actor, which is used to create
* inspector-related actors, including the walker.
*/
exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
initialize: function (conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
@@ -2902,36 +197,36 @@ exports.InspectorActor = protocol.ActorC
* is important as the resizing occurs server-side so that image-data being
* transfered in the longstring back to the client will be that much smaller
*/
getImageDataFromURL: function (url, maxDim) {
let img = new this.window.Image();
img.src = url;
// imageToImageData waits for the image to load.
- return imageToImageData(img, maxDim).then(imageData => {
+ return InspectorActorUtils.imageToImageData(img, maxDim).then(imageData => {
return {
data: LongStringActor(this.conn, imageData.data),
size: imageData.size
};
});
},
/**
* Resolve a URL to its absolute form, in the scope of a given content window.
* @param {String} url.
* @param {NodeActor} node If provided, the owner window of this node will be
* used to resolve the URL. Otherwise, the top-level content window will be
* used instead.
* @return {String} url.
*/
resolveRelativeURL: function (url, node) {
- let document = isNodeDead(node)
+ let document = InspectorActorUtils.isNodeDead(node)
? this.window.document
- : nodeDocument(node.rawNode);
+ : InspectorActorUtils.nodeDocument(node.rawNode);
if (!document) {
return url;
}
let baseURI = Services.io.newURI(document.location.href);
return Services.io.newURI(url, null, baseURI).spec;
},
@@ -3014,389 +309,8 @@ exports.InspectorActor = protocol.ActorC
_onColorPicked: function (e, color) {
this.emit("color-picked", color);
},
_onColorPickCanceled: function () {
this.emit("color-pick-canceled");
}
});
-
-// Exported for test purposes.
-exports._documentWalker = DocumentWalker;
-
-function nodeDocument(node) {
- if (Cu.isDeadWrapper(node)) {
- return null;
- }
- return node.ownerDocument ||
- (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
-}
-
-function nodeDocshell(node) {
- let doc = node ? nodeDocument(node) : null;
- let win = doc ? doc.defaultView : null;
- if (win) {
- return win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell);
- }
- return null;
-}
-
-function isNodeDead(node) {
- return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
-}
-
-/**
- * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
- * See inDeepTreeWalker for more information about the methods.
- *
- * @param {DOMNode} node
- * @param {Window} rootWin
- * @param {Number} whatToShow
- * See nodeFilterConstants / inIDeepTreeWalker for options.
- * @param {Function} filter
- * A custom filter function Taking in a DOMNode and returning an Int. See
- * WalkerActor.nodeFilter for an example.
- * @param {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.
- */
-function DocumentWalker(node, rootWin,
- whatToShow = nodeFilterConstants.SHOW_ALL,
- filter = standardTreeWalkerFilter,
- skipTo = SKIP_TO_PARENT) {
- 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 = true;
- 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 () {
- return this.walker.parentNode();
- },
-
- nextNode: function () {
- let 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 () {
- let 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 () {
- let 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.
- let 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 && previous.previousSibling;
- next = 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;
- },
-};
-
-function isInXULDocument(el) {
- let doc = nodeDocument(el);
- return doc &&
- doc.documentElement &&
- doc.documentElement.namespaceURI === XUL_NS;
-}
-
-/**
- * This DeepTreeWalker filter skips whitespace text nodes and anonymous
- * content with the exception of ::before and ::after and anonymous content
- * in XUL document (needed to show all elements in the browser toolbox).
- */
-function standardTreeWalkerFilter(node) {
- // ::before and ::after are native anonymous content, but we always
- // want to show them
- if (node.nodeName === "_moz_generated_content_before" ||
- node.nodeName === "_moz_generated_content_after") {
- return nodeFilterConstants.FILTER_ACCEPT;
- }
-
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
-
- // Ignore all native and XBL anonymous content inside a non-XUL document.
- // We need to do this to skip things like form controls, scrollbars,
- // video controls, etc (see bug 1187482).
- if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
- isNativeAnonymous(node))) {
- return nodeFilterConstants.FILTER_SKIP;
- }
-
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
- * it also includes all anonymous content (like internal form controls).
- */
-function allAnonymousContentTreeWalkerFilter(node) {
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * Is the given node a text node composed of whitespace only?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function isWhitespaceTextNode(node) {
- return node.nodeType == Ci.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
-}
-
-/**
- * Does the given node have non-0 width and height?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function nodeHasSize(node) {
- if (!node.getBoxQuads) {
- return false;
- }
-
- let quads = node.getBoxQuads();
- return quads.length && quads.some(quad => quad.bounds.width && quad.bounds.height);
-}
-
-/**
- * Returns a promise that is settled once the given HTMLImageElement has
- * finished loading.
- *
- * @param {HTMLImageElement} image - The image element.
- * @param {Number} timeout - Maximum amount of time the image is allowed to load
- * before the waiting is aborted. Ignored if flags.testing is set.
- *
- * @return {Promise} that is fulfilled once the image has loaded. If the image
- * fails to load or the load takes too long, the promise is rejected.
- */
-function ensureImageLoaded(image, timeout) {
- let { HTMLImageElement } = image.ownerGlobal;
- if (!(image instanceof HTMLImageElement)) {
- return promise.reject("image must be an HTMLImageELement");
- }
-
- if (image.complete) {
- // The image has already finished loading.
- return promise.resolve();
- }
-
- // This image is still loading.
- let onLoad = AsyncUtils.listenOnce(image, "load");
-
- // Reject if loading fails.
- let onError = AsyncUtils.listenOnce(image, "error").then(() => {
- return promise.reject("Image '" + image.src + "' failed to load.");
- });
-
- // Don't timeout when testing. This is never settled.
- let onAbort = new Promise(() => {});
-
- if (!flags.testing) {
- // Tests are not running. Reject the promise after given timeout.
- onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
- return promise.reject("Image '" + image.src + "' took too long to load.");
- });
- }
-
- // See which happens first.
- return promise.race([onLoad, onError, onAbort]);
-}
-
-/**
- * Given an <img> or <canvas> element, return the image data-uri. If @param node
- * is an <img> element, the method waits a while for the image to load before
- * the data is generated. If the image does not finish loading in a reasonable
- * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
- *
- * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
- * element, or Image() object. Other types cause the method to reject.
- * @param {Number} maxDim - Optionally pass a maximum size you want the longest
- * side of the image to be resized to before getting the image data.
-
- * @return {Promise} A promise that is fulfilled with an object containing the
- * data-uri and size-related information:
- * { data: "...",
- * size: {
- * naturalWidth: 400,
- * naturalHeight: 300,
- * resized: true }
- * }.
- *
- * If something goes wrong, the promise is rejected.
- */
-var imageToImageData = Task.async(function* (node, maxDim) {
- let { HTMLCanvasElement, HTMLImageElement } = node.ownerGlobal;
-
- let isImg = node instanceof HTMLImageElement;
- let isCanvas = node instanceof HTMLCanvasElement;
-
- if (!isImg && !isCanvas) {
- throw new Error("node is not a <canvas> or <img> element.");
- }
-
- if (isImg) {
- // Ensure that the image is ready.
- yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
- }
-
- // Get the image resize ratio if a maxDim was provided
- let resizeRatio = 1;
- let imgWidth = node.naturalWidth || node.width;
- let imgHeight = node.naturalHeight || node.height;
- let imgMax = Math.max(imgWidth, imgHeight);
- if (maxDim && imgMax > maxDim) {
- resizeRatio = maxDim / imgMax;
- }
-
- // Extract the image data
- let imageData;
- // The image may already be a data-uri, in which case, save ourselves the
- // trouble of converting via the canvas.drawImage.toDataURL method, but only
- // if the image doesn't need resizing
- if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
- imageData = node.src;
- } else {
- // Create a canvas to copy the rawNode into and get the imageData from
- let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
- canvas.width = imgWidth * resizeRatio;
- canvas.height = imgHeight * resizeRatio;
- let ctx = canvas.getContext("2d");
-
- // Copy the rawNode image or canvas in the new canvas and extract data
- ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
- imageData = canvas.toDataURL("image/png");
- }
-
- return {
- data: imageData,
- size: {
- naturalWidth: imgWidth,
- naturalHeight: imgHeight,
- resized: resizeRatio !== 1
- }
- };
-});
--- a/devtools/server/actors/inspector/moz.build
+++ b/devtools/server/actors/inspector/moz.build
@@ -1,12 +1,16 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
- 'inspector.js',
+ 'document-walker.js',
+ 'inspector.js',
+ 'node-actor.js',
+ 'utils.js',
+ 'walker-actor.js',
)
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Developer Tools: Inspector')
copy from devtools/server/actors/inspector/inspector.js
copy to devtools/server/actors/inspector/node-actor.js
--- a/devtools/server/actors/inspector/inspector.js
+++ b/devtools/server/actors/inspector/node-actor.js
@@ -1,213 +1,48 @@
/* 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";
-/**
- * Here's the server side of the remote inspector.
- *
- * The WalkerActor is the client's view of the debuggee's DOM. It's gives
- * the client a tree of NodeActor objects.
- *
- * The walker presents the DOM tree mostly unmodified from the source DOM
- * tree, but with a few key differences:
- *
- * - Empty text nodes are ignored. This is pretty typical of developer
- * tools, but maybe we should reconsider that on the server side.
- * - iframes with documents loaded have the loaded document as the child,
- * the walker provides one big tree for the whole document tree.
- *
- * There are a few ways to get references to NodeActors:
- *
- * - When you first get a WalkerActor reference, it comes with a free
- * reference to the root document's node.
- * - Given a node, you can ask for children, siblings, and parents.
- * - You can issue querySelector and querySelectorAll requests to find
- * other elements.
- * - Requests that return arbitrary nodes from the tree (like querySelector
- * and querySelectorAll) will also return any nodes the client hasn't
- * seen in order to have a complete set of parents.
- *
- * Once you have a NodeFront, you should be able to answer a few questions
- * without further round trips, like the node's name, namespace/tagName,
- * attributes, etc. Other questions (like a text node's full nodeValue)
- * might require another round trip.
- *
- * The protocol guarantees that the client will always know the parent of
- * any node that is returned by the server. This means that some requests
- * (like querySelector) will include the extra nodes needed to satisfy this
- * requirement. The client keeps track of this parent relationship, so the
- * node fronts form a tree that is a subset of the actual DOM tree.
- *
- *
- * We maintain this guarantee to support the ability to release subtrees on
- * the client - when a node is disconnected from the DOM tree we want to be
- * able to free the client objects for all the children nodes.
- *
- * So to be able to answer "all the children of a given node that we have
- * seen on the client side", we guarantee that every time we've seen a node,
- * we connect it up through its parents.
- */
+const {Ci, Cu} = require("chrome");
-const {Cc, Ci, Cu} = require("chrome");
-const Services = require("Services");
const protocol = require("devtools/shared/protocol");
-const {LongStringActor} = require("devtools/server/actors/string");
-const promise = require("promise");
-const defer = require("devtools/shared/defer");
-const {Task} = require("devtools/shared/task");
-const EventEmitter = require("devtools/shared/event-emitter");
+const {nodeSpec, nodeListSpec} = require("devtools/shared/specs/node");
+
const InspectorUtils = require("InspectorUtils");
-const {walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
-const {nodeSpec, nodeListSpec} = require("devtools/shared/specs/node");
+loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-loader.lazyRequireGetter(this, "EyeDropper", "devtools/server/actors/highlighters/eye-dropper", true);
-loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
-loader.lazyRequireGetter(this, "PageStyleActor", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
-loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
-loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
-loader.lazyRequireGetter(this, "HighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "HighlighterEnvironment", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
-loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
+
loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
loader.lazyRequireGetter(this, "isShadowAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
+loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyServiceGetter(this, "DOMParser",
- "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
+loader.lazyRequireGetter(this, "InspectorActorUtils", "devtools/server/actors/inspector/utils");
+loader.lazyRequireGetter(this, "LongStringActor", "devtools/server/actors/string", true);
+loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
+loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
+loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
-loader.lazyServiceGetter(this, "eventListenerService",
- "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
+const EventEmitter = require("devtools/shared/event-emitter");
+const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
-const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
-const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
-const SVG_NS = "http://www.w3.org/2000/svg";
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const IMAGE_FETCHING_TIMEOUT = 500;
-
-// Minimum delay between two "new-mutations" events.
-const MUTATIONS_THROTTLING_DELAY = 100;
-// List of mutation types that should -not- be throttled.
-const IMMEDIATE_MUTATIONS = [
- "documentUnload",
- "frameLoad",
- "newRoot",
- "pseudoClassLock",
-];
-
-// 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";
-
-// The possible completions to a ':' with added score to give certain values
-// some preference.
-const PSEUDO_SELECTORS = [
- [":active", 1],
- [":hover", 1],
- [":focus", 1],
- [":visited", 0],
- [":link", 0],
- [":first-letter", 0],
- [":first-child", 2],
- [":before", 2],
- [":after", 2],
- [":lang(", 0],
- [":not(", 3],
- [":first-of-type", 0],
- [":last-of-type", 0],
- [":only-of-type", 0],
- [":only-child", 2],
- [":nth-child(", 3],
- [":nth-last-child(", 0],
- [":nth-of-type(", 0],
- [":nth-last-of-type(", 0],
- [":last-child", 2],
- [":root", 0],
- [":empty", 0],
- [":target", 0],
- [":enabled", 0],
- [":disabled", 0],
- [":checked", 1],
- ["::selection", 0]
-];
-
-var HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
- .__fx-devtools-hide-shortcut__ {
- visibility: hidden !important;
- }
-
- :-moz-devtools-highlighted {
- outline: 2px dashed #F06!important;
- outline-offset: -2px !important;
- }
-`);
-
-/**
- * We only send nodeValue up to a certain size by default. This stuff
- * controls that size.
- */
-exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
-var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
-
-exports.getValueSummaryLength = function () {
- return gValueSummaryLength;
-};
-
-exports.setValueSummaryLength = function (val) {
- gValueSummaryLength = val;
-};
-
-/**
- * Returns the properly cased version of the node's tag name, which can be
- * used when displaying said name in the UI.
- *
- * @param {Node} rawNode
- * Node for which we want the display name
- * @return {String}
- * Properly cased version of the node tag name
- */
-const getNodeDisplayName = function (rawNode) {
- if (rawNode.nodeName && !rawNode.localName) {
- // The localName & prefix APIs have been moved from the Node interface to the Element
- // interface. Use Node.nodeName as a fallback.
- return rawNode.nodeName;
- }
- return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
-};
-exports.getNodeDisplayName = getNodeDisplayName;
/**
* Server side of the node actor.
*/
-var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
+const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
initialize: function (walker, node) {
protocol.Actor.prototype.initialize.call(this, null);
this.walker = walker;
this.rawNode = node;
this._eventParsers = new EventParsers().parsers;
// Storing the original display of the node, to track changes when reflows
// occur
@@ -257,17 +92,17 @@ var NodeActor = exports.NodeActor = prot
let form = {
actor: this.actorID,
baseURI: this.rawNode.baseURI,
parent: parentNode ? parentNode.actorID : undefined,
nodeType: this.rawNode.nodeType,
namespaceURI: this.rawNode.namespaceURI,
nodeName: this.rawNode.nodeName,
nodeValue: this.rawNode.nodeValue,
- displayName: getNodeDisplayName(this.rawNode),
+ displayName: InspectorActorUtils.getNodeDisplayName(this.rawNode),
numChildren: this.numChildren,
inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
// doctype attributes
name: this.rawNode.name,
publicId: this.rawNode.publicId,
systemId: this.rawNode.systemId,
@@ -372,17 +207,17 @@ var NodeActor = exports.NodeActor = prot
return CssLogic.getComputedStyle(this.rawNode);
},
/**
* Is the node's display computed style value other than "none"
*/
get isDisplayed() {
// Consider all non-element nodes as displayed.
- if (isNodeDead(this) ||
+ if (InspectorActorUtils.isNodeDead(this) ||
this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
this.isAfterPseudoElement ||
this.isBeforePseudoElement) {
return true;
}
let style = this.computedStyle;
if (!style) {
@@ -696,17 +531,17 @@ var NodeActor = exports.NodeActor = prot
* The image data is transmitted as a base64 encoded png data-uri.
* The method rejects if the node isn't an image or if the image is missing
*
* Accepts a maxDim request parameter to resize images that are larger. This
* is important as the resizing occurs server-side so that image-data being
* transfered in the longstring back to the client will be that much smaller
*/
getImageData: function (maxDim) {
- return imageToImageData(this.rawNode, maxDim).then(imageData => {
+ return InspectorActorUtils.imageToImageData(this.rawNode, maxDim).then(imageData => {
return {
data: LongStringActor(this.conn, imageData.data),
size: imageData.size
};
});
},
/**
@@ -798,17 +633,17 @@ var NodeActor = exports.NodeActor = prot
}
return "rgba(255, 255, 255, 1)";
}
});
/**
* Server side of a node list as returned by querySelectorAll()
*/
-var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
+const NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
typeName: "domnodelist",
initialize: function (walker, nodeList) {
protocol.Actor.prototype.initialize.call(this);
this.walker = walker;
this.nodeList = nodeList || [];
},
@@ -853,2550 +688,10 @@ var NodeListActor = exports.NodeListActo
let items = Array.prototype.slice.call(this.nodeList, start, end)
.map(item => this.walker._ref(item));
return this.walker.attachElements(items);
},
release: function () {}
});
-/**
- * Server side of the DOM walker.
- */
-var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
- /**
- * Create the WalkerActor
- * @param DebuggerServerConnection conn
- * The server connection.
- */
- initialize: function (conn, tabActor, options) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
- this.rootWin = tabActor.window;
- this.rootDoc = this.rootWin.document;
- this._refMap = new Map();
- this._pendingMutations = [];
- this._activePseudoClassLocks = new Set();
- this.showAllAnonymousContent = options.showAllAnonymousContent;
-
- this.walkerSearch = new WalkerSearch(this);
-
- // Nodes which have been removed from the client's known
- // ownership tree are considered "orphaned", and stored in
- // this set.
- this._orphaned = new Set();
-
- // The client can tell the walker that it is interested in a node
- // even when it is orphaned with the `retainNode` method. This
- // list contains orphaned nodes that were so retained.
- this._retainedOrphans = new Set();
-
- this.onMutations = this.onMutations.bind(this);
- this.onFrameLoad = this.onFrameLoad.bind(this);
- this.onFrameUnload = this.onFrameUnload.bind(this);
- this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
- MUTATIONS_THROTTLING_DELAY);
-
- tabActor.on("will-navigate", this.onFrameUnload);
- tabActor.on("window-ready", this.onFrameLoad);
-
- // Ensure that the root document node actor is ready and
- // managed.
- this.rootNode = this.document();
-
- this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
- this._onReflows = this._onReflows.bind(this);
- this.layoutChangeObserver.on("reflows", this._onReflows);
- this._onResize = this._onResize.bind(this);
- this.layoutChangeObserver.on("resize", this._onResize);
-
- this._onEventListenerChange = this._onEventListenerChange.bind(this);
- eventListenerService.addListenerChangeListener(this._onEventListenerChange);
- },
-
- /**
- * Callback for eventListenerService.addListenerChangeListener
- * @param nsISimpleEnumerator changesEnum
- * enumerator of nsIEventListenerChange
- */
- _onEventListenerChange: function (changesEnum) {
- let changes = changesEnum.enumerate();
- while (changes.hasMoreElements()) {
- let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
- let target = current.target;
-
- if (this._refMap.has(target)) {
- let actor = this.getNode(target);
- let mutation = {
- type: "events",
- target: actor.actorID,
- hasEventListeners: actor._hasEventListeners
- };
- this.queueMutation(mutation);
- }
- }
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- root: this.rootNode.form(),
- traits: {
- // FF42+ Inspector starts managing the Walker, while the inspector also
- // starts cleaning itself up automatically on client disconnection.
- // So that there is no need to manually release the walker anymore.
- autoReleased: true,
- // XXX: It seems silly that we need to tell the front which capabilities
- // its actor has in this way when the target can use actorHasMethod. If
- // this was ported to the protocol (Bug 1157048) we could call that
- // inside of custom front methods and not need to do traits for this.
- multiFrameQuerySelectorAll: true,
- textSearch: true,
- }
- };
- },
-
- toString: function () {
- return "[WalkerActor " + this.actorID + "]";
- },
-
- getDocumentWalker: function (node, whatToShow, skipTo) {
- // Allow native anon content (like <video> controls) if preffed on
- let nodeFilter = this.showAllAnonymousContent
- ? allAnonymousContentTreeWalkerFilter
- : standardTreeWalkerFilter;
- return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter, skipTo);
- },
-
- destroy: function () {
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
- protocol.Actor.prototype.destroy.call(this);
- try {
- this.clearPseudoClassLocks();
- this._activePseudoClassLocks = null;
-
- this._hoveredNode = null;
- this.rootWin = null;
- this.rootDoc = null;
- this.rootNode = null;
- this.layoutHelpers = null;
- this._orphaned = null;
- this._retainedOrphans = null;
- this._refMap = null;
-
- this.tabActor.off("will-navigate", this.onFrameUnload);
- this.tabActor.off("window-ready", this.onFrameLoad);
-
- this.onFrameLoad = null;
- this.onFrameUnload = null;
-
- this.walkerSearch.destroy();
-
- this.layoutChangeObserver.off("reflows", this._onReflows);
- this.layoutChangeObserver.off("resize", this._onResize);
- this.layoutChangeObserver = null;
- releaseLayoutChangesObserver(this.tabActor);
-
- eventListenerService.removeListenerChangeListener(
- this._onEventListenerChange);
-
- this.onMutations = null;
-
- this.layoutActor = null;
- this.tabActor = null;
-
- this.emit("destroyed");
- } catch (e) {
- console.error(e);
- }
- },
-
- release: function () {},
-
- unmanage: function (actor) {
- if (actor instanceof NodeActor) {
- if (this._activePseudoClassLocks &&
- this._activePseudoClassLocks.has(actor)) {
- this.clearPseudoClassLocks(actor);
- }
- this._refMap.delete(actor.rawNode);
- }
- protocol.Actor.prototype.unmanage.call(this, actor);
- },
-
- /**
- * Determine if the walker has come across this DOM node before.
- * @param {DOMNode} rawNode
- * @return {Boolean}
- */
- hasNode: function (rawNode) {
- return this._refMap.has(rawNode);
- },
-
- /**
- * If the walker has come across this DOM node before, then get the
- * corresponding node actor.
- * @param {DOMNode} rawNode
- * @return {NodeActor}
- */
- getNode: function (rawNode) {
- return this._refMap.get(rawNode);
- },
-
- _ref: function (node) {
- let actor = this.getNode(node);
- if (actor) {
- return actor;
- }
-
- actor = new NodeActor(this, node);
-
- // Add the node actor as a child of this walker actor, assigning
- // it an actorID.
- this.manage(actor);
- this._refMap.set(node, actor);
-
- if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
- actor.watchDocument(this.onMutations);
- }
- return actor;
- },
-
- _onReflows: function (reflows) {
- // Going through the nodes the walker knows about, see which ones have
- // had their display changed and send a display-change event if any
- let changes = [];
- for (let [node, actor] of this._refMap) {
- if (Cu.isDeadWrapper(node)) {
- continue;
- }
-
- let isDisplayed = actor.isDisplayed;
- if (isDisplayed !== actor.wasDisplayed) {
- changes.push(actor);
- // Updating the original value
- actor.wasDisplayed = isDisplayed;
- }
- }
-
- if (changes.length) {
- this.emit("display-change", changes);
- }
- },
-
- /**
- * When the browser window gets resized, relay the event to the front.
- */
- _onResize: function () {
- this.emit("resize");
- },
-
- /**
- * This is kept for backward-compatibility reasons with older remote targets.
- * Targets prior to bug 916443.
- *
- * pick/cancelPick are used to pick a node on click on the content
- * document. But in their implementation prior to bug 916443, they don't allow
- * highlighting on hover.
- * The client-side now uses the highlighter actor's pick and cancelPick
- * methods instead. The client-side uses the the highlightable trait found in
- * the root actor to determine which version of pick to use.
- *
- * As for highlight, the new highlighter actor is used instead of the walker's
- * highlight method. Same here though, the client-side uses the highlightable
- * trait to dertermine which to use.
- *
- * Keeping these actor methods for now allows newer client-side debuggers to
- * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
- */
- pick: function () {},
- cancelPick: function () {},
- highlight: function (node) {},
-
- /**
- * Ensures that the node is attached and it can be accessed from the root.
- *
- * @param {(Node|NodeActor)} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNode type.
- */
- attachElement: function (node) {
- let { nodes, newParents } = this.attachElements([node]);
- return {
- node: nodes[0],
- newParents: newParents
- };
- },
-
- /**
- * Ensures that the nodes are attached and they can be accessed from the root.
- *
- * @param {(Node[]|NodeActor[])} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNodeArray type.
- */
- attachElements: function (nodes) {
- let nodeActors = [];
- let newParents = new Set();
- for (let node of nodes) {
- if (!(node instanceof NodeActor)) {
- // If an anonymous node was passed in and we aren't supposed to know
- // about it, then consult with the document walker as the source of
- // truth about which elements exist.
- if (!this.showAllAnonymousContent && isAnonymous(node)) {
- node = this.getDocumentWalker(node).currentNode;
- }
-
- node = this._ref(node);
- }
-
- this.ensurePathToRoot(node, newParents);
- // If nodes may be an array of raw nodes, we're sure to only have
- // NodeActors with the following array.
- nodeActors.push(node);
- }
-
- return {
- nodes: nodeActors,
- newParents: [...newParents]
- };
- },
-
- /**
- * Return the document node that contains the given node,
- * or the root node if no node is specified.
- * @param NodeActor node
- * The node whose document is needed, or null to
- * return the root.
- */
- document: function (node) {
- let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
- return this._ref(doc);
- },
-
- /**
- * Return the documentElement for the document containing the
- * given node.
- * @param NodeActor node
- * The node whose documentElement is requested, or null
- * to use the root document.
- */
- documentElement: function (node) {
- let elt = isNodeDead(node)
- ? this.rootDoc.documentElement
- : nodeDocument(node.rawNode).documentElement;
- return this._ref(elt);
- },
-
- /**
- * Return all parents of the given node, ordered from immediate parent
- * to root.
- * @param NodeActor node
- * The node whose parents are requested.
- * @param object options
- * Named options, including:
- * `sameDocument`: If true, parents will be restricted to the same
- * document as the node.
- * `sameTypeRootTreeItem`: If true, this will not traverse across
- * different types of docshells.
- */
- parents: function (node, options = {}) {
- if (isNodeDead(node)) {
- return [];
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let parents = [];
- let cur;
- while ((cur = walker.parentNode())) {
- if (options.sameDocument &&
- nodeDocument(cur) != nodeDocument(node.rawNode)) {
- break;
- }
-
- if (options.sameTypeRootTreeItem &&
- nodeDocshell(cur).sameTypeRootTreeItem !=
- nodeDocshell(node.rawNode).sameTypeRootTreeItem) {
- break;
- }
-
- parents.push(this._ref(cur));
- }
- return parents;
- },
-
- parentNode: function (node) {
- let walker = this.getDocumentWalker(node.rawNode);
- let parent = walker.parentNode();
- if (parent) {
- return this._ref(parent);
- }
- return null;
- },
-
- /**
- * If the given NodeActor only has a single text node as a child with a text
- * content small enough to be inlined, return that child's NodeActor.
- *
- * @param NodeActor node
- */
- inlineTextChild: function (node) {
- // Quick checks to prevent creating a new walker if possible.
- if (node.isBeforePseudoElement ||
- node.isAfterPseudoElement ||
- node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
- node.rawNode.children.length > 0) {
- return undefined;
- }
-
- let docWalker = this.getDocumentWalker(node.rawNode);
- let firstChild = docWalker.firstChild();
-
- // Bail out if:
- // - more than one child
- // - unique child is not a text node
- // - unique child is a text node, but is too long to be inlined
- if (!firstChild ||
- docWalker.nextSibling() ||
- firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
- firstChild.nodeValue.length > gValueSummaryLength
- ) {
- return undefined;
- }
-
- return this._ref(firstChild);
- },
-
- /**
- * Mark a node as 'retained'.
- *
- * A retained node is not released when `releaseNode` is called on its
- * parent, or when a parent is released with the `cleanup` option to
- * `getMutations`.
- *
- * When a retained node's parent is released, a retained mode is added to
- * the walker's "retained orphans" list.
- *
- * Retained nodes can be deleted by providing the `force` option to
- * `releaseNode`. They will also be released when their document
- * has been destroyed.
- *
- * Retaining a node makes no promise about its children; They can
- * still be removed by normal means.
- */
- retainNode: function (node) {
- node.retained = true;
- },
-
- /**
- * Remove the 'retained' mark from a node. If the node was a
- * retained orphan, release it.
- */
- unretainNode: function (node) {
- node.retained = false;
- if (this._retainedOrphans.has(node)) {
- this._retainedOrphans.delete(node);
- this.releaseNode(node);
- }
- },
-
- /**
- * Release actors for a node and all child nodes.
- */
- releaseNode: function (node, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- if (node.retained && !options.force) {
- this._retainedOrphans.add(node);
- return;
- }
-
- if (node.retained) {
- // Forcing a retained node to go away.
- this._retainedOrphans.delete(node);
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
-
- let child = walker.firstChild();
- while (child) {
- let childActor = this.getNode(child);
- if (childActor) {
- this.releaseNode(childActor, options);
- }
- child = walker.nextSibling();
- }
-
- node.destroy();
- },
-
- /**
- * Add any nodes between `node` and the walker's root node that have not
- * yet been seen by the client.
- */
- ensurePathToRoot: function (node, newParents = new Set()) {
- if (!node) {
- return newParents;
- }
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let parent = this.getNode(cur);
- if (!parent) {
- // This parent didn't exist, so hasn't been seen by the client yet.
- newParents.add(this._ref(cur));
- } else {
- // This parent did exist, so the client knows about it.
- return newParents;
- }
- }
- return newParents;
- },
-
- /**
- * Return children of the given node. By default this method will return
- * all children of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- children: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- if (options.center && options.start) {
- throw Error("Can't specify both 'center' and 'start' options.");
- }
- let maxNodes = options.maxNodes || -1;
- if (maxNodes == -1) {
- maxNodes = Number.MAX_VALUE;
- }
-
- // We're going to create a few document walkers with the same filter,
- // make it easier.
- let getFilteredWalker = documentWalkerNode => {
- let { whatToShow } = options;
- // Use SKIP_TO_SIBLING to force the walker to use a sibling of the provided node
- // in case this one is incompatible with the walker's filter function.
- return this.getDocumentWalker(documentWalkerNode, whatToShow, SKIP_TO_SIBLING);
- };
-
- // Need to know the first and last child.
- let rawNode = node.rawNode;
- let firstChild = getFilteredWalker(rawNode).firstChild();
- let lastChild = getFilteredWalker(rawNode).lastChild();
-
- if (!firstChild) {
- // No children, we're done.
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let start;
- if (options.center) {
- start = options.center.rawNode;
- } else if (options.start) {
- start = options.start.rawNode;
- } else {
- start = firstChild;
- }
-
- let nodes = [];
-
- // Start by reading backward from the starting point if we're centering...
- let backwardWalker = getFilteredWalker(start);
- if (backwardWalker.currentNode != firstChild && options.center) {
- backwardWalker.previousSibling();
- let backwardCount = Math.floor(maxNodes / 2);
- let backwardNodes = this._readBackward(backwardWalker, backwardCount);
- nodes = backwardNodes;
- }
-
- // Then read forward by any slack left in the max children...
- let forwardWalker = getFilteredWalker(start);
- let forwardCount = maxNodes - nodes.length;
- nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
-
- // If there's any room left, it means we've run all the way to the end.
- // If we're centering, check if there are more items to read at the front.
- let remaining = maxNodes - nodes.length;
- if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
- let firstNodes = this._readBackward(backwardWalker, remaining);
-
- // Then put it all back together.
- nodes = firstNodes.concat(nodes);
- }
-
- return {
- hasFirst: nodes[0].rawNode == firstChild,
- hasLast: nodes[nodes.length - 1].rawNode == lastChild,
- nodes: nodes
- };
- },
-
- /**
- * Return siblings of the given node. By default this method will return
- * all siblings of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * If `start` or `center` are not specified, this method will center on the
- * node whose siblings are requested.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- siblings: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow)
- .parentNode();
- if (!parentNode) {
- return {
- hasFirst: true,
- hasLast: true,
- nodes: [node]
- };
- }
-
- if (!(options.start || options.center)) {
- options.center = node;
- }
-
- return this.children(this._ref(parentNode), options);
- },
-
- /**
- * Get the next sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- nextSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.nextSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Get the previous sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- previousSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.previousSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Helper function for the `children` method: Read forward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readForward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.nextSibling();
- } while (node && --count);
- return ret;
- },
-
- /**
- * Helper function for the `children` method: Read backward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readBackward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.previousSibling();
- } while (node && --count);
- ret.reverse();
- return ret;
- },
-
- /**
- * Return the first node in the document that matches the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelector: function (baseNode, selector) {
- if (isNodeDead(baseNode)) {
- return {};
- }
-
- let node = baseNode.rawNode.querySelector(selector);
- if (!node) {
- return {};
- }
-
- return this.attachElement(node);
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelectorAll: function (baseNode, selector) {
- let nodeList = null;
-
- try {
- nodeList = baseNode.rawNode.querySelectorAll(selector);
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
-
- return new NodeListActor(this, nodeList);
- },
-
- /**
- * Get a list of nodes that match the given selector in all known frames of
- * the current content page.
- * @param {String} selector.
- * @return {Array}
- */
- _multiFrameQuerySelectorAll: function (selector) {
- let nodes = [];
-
- for (let {document} of this.tabActor.windows) {
- try {
- nodes = [...nodes, ...document.querySelectorAll(selector)];
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
- }
-
- return nodes;
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector in all
- * frames of the current content page.
- * @param {String} selector
- */
- multiFrameQuerySelectorAll: function (selector) {
- return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector));
- },
-
- /**
- * Search the document for a given string.
- * Results will be searched with the walker-search module (searches through
- * tag names, attribute names and values, and text contents).
- *
- * @returns {searchresult}
- * - {NodeList} list
- * - {Array<Object>} metadata. Extra information with indices that
- * match up with node list.
- */
- search: function (query) {
- let results = this.walkerSearch.search(query);
- let nodeList = new NodeListActor(this, results.map(r => r.node));
-
- return {
- list: nodeList,
- metadata: []
- };
- },
-
- /**
- * Returns a list of matching results for CSS selector autocompletion.
- *
- * @param string query
- * The selector query being completed
- * @param string completing
- * The exact token being completed out of the query
- * @param string selectorState
- * One of "pseudo", "id", "tag", "class", "null"
- */
- getSuggestionsForQuery: function (query, completing, selectorState) {
- let sugs = {
- classes: new Map(),
- tags: new Map(),
- ids: new Map()
- };
- let result = [];
- let nodes = null;
- // Filtering and sorting the results so that protocol transfer is miminal.
- switch (selectorState) {
- case "pseudo":
- result = PSEUDO_SELECTORS.filter(item => {
- return item[0].startsWith(":" + completing);
- });
- break;
-
- case "class":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[class]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- if (className.startsWith(completing)) {
- result.push(["." + CSS.escape(className), count, selectorState]);
- }
- }
- break;
-
- case "id":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[id]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- }
- for (let [id, count] of sugs.ids) {
- if (id.startsWith(completing) && id !== "") {
- result.push(["#" + CSS.escape(id), count, selectorState]);
- }
- }
- break;
-
- case "tag":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("*");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- }
- for (let [tag, count] of sugs.tags) {
- if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
- result.push([tag, count, selectorState]);
- }
- }
-
- // For state 'tag' (no preceding # or .) and when there's no query (i.e.
- // only one word) then search for the matching classes and ids
- if (!query) {
- result = [
- ...result,
- ...this.getSuggestionsForQuery(null, completing, "class")
- .suggestions,
- ...this.getSuggestionsForQuery(null, completing, "id")
- .suggestions
- ];
- }
-
- break;
-
- case "null":
- nodes = this._multiFrameQuerySelectorAll(query);
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- for (let [tag, count] of sugs.tags) {
- tag && result.push([tag, count]);
- }
- for (let [id, count] of sugs.ids) {
- id && result.push(["#" + id, count]);
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- className && result.push(["." + className, count]);
- }
- }
-
- // Sort by count (desc) and name (asc)
- result = result.sort((a, b) => {
- // Computed a sortable string with first the inverted count, then the name
- let sortA = (10000 - a[1]) + a[0];
- let sortB = (10000 - b[1]) + b[0];
-
- // Prefixing ids, classes and tags, to group results
- let firstA = a[0].substring(0, 1);
- let firstB = b[0].substring(0, 1);
-
- if (firstA === "#") {
- sortA = "2" + sortA;
- } else if (firstA === ".") {
- sortA = "1" + sortA;
- } else {
- sortA = "0" + sortA;
- }
-
- if (firstB === "#") {
- sortB = "2" + sortB;
- } else if (firstB === ".") {
- sortB = "1" + sortB;
- } else {
- sortB = "0" + sortB;
- }
-
- // String compare
- return sortA.localeCompare(sortB);
- });
-
- result.slice(0, 25);
-
- return {
- query: query,
- suggestions: result
- };
- },
-
- /**
- * Add a pseudo-class lock to a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be added
- * to parent nodes.
- * `enabled`: False if the pseudo-class should be locked
- * to 'off'. Defaults to true.
- *
- * @returns An empty packet. A "pseudoClassLock" mutation will
- * be queued for any changed nodes.
- */
- addPseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- // There can be only one node locked per pseudo, so dismiss all existing
- // ones
- for (let locked of this._activePseudoClassLocks) {
- if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- let enabled = options.enabled === undefined ||
- options.enabled;
- this._addPseudoClassLock(node, pseudo, enabled);
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._addPseudoClassLock(curNode, pseudo, enabled);
- }
- },
-
- _queuePseudoClassMutation: function (node) {
- this.queueMutation({
- target: node.actorID,
- type: "pseudoClassLock",
- pseudoClassLocks: node.writePseudoClassLocks()
- });
- },
-
- _addPseudoClassLock: function (node, pseudo, enabled) {
- if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
- this._activePseudoClassLocks.add(node);
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- hideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
- node.rawNode.classList.add(HIDDEN_CLASS);
- },
-
- unhideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- node.rawNode.classList.remove(HIDDEN_CLASS);
- },
-
- /**
- * Remove a pseudo-class lock from a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be removed
- * from parent nodes.
- *
- * @returns An empty response. "pseudoClassLock" mutations
- * will be emitted for any changed nodes.
- */
- removePseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- this._removePseudoClassLock(node, pseudo);
-
- // Remove pseudo class for children as we don't want to allow
- // turning it on for some childs without setting it on some parents
- for (let locked of this._activePseudoClassLocks) {
- if (node.rawNode.contains(locked.rawNode) &&
- InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._removePseudoClassLock(curNode, pseudo);
- }
- },
-
- _removePseudoClassLock: function (node, pseudo) {
- if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
- if (!node.writePseudoClassLocks()) {
- this._activePseudoClassLocks.delete(node);
- }
-
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- /**
- * Clear all the pseudo-classes on a given node or all nodes.
- * @param {NodeActor} node Optional node to clear pseudo-classes on
- */
- clearPseudoClassLocks: function (node) {
- if (node && isNodeDead(node)) {
- return;
- }
-
- if (node) {
- InspectorUtils.clearPseudoClassLocks(node.rawNode);
- this._activePseudoClassLocks.delete(node);
- this._queuePseudoClassMutation(node);
- } else {
- for (let locked of this._activePseudoClassLocks) {
- InspectorUtils.clearPseudoClassLocks(locked.rawNode);
- this._activePseudoClassLocks.delete(locked);
- this._queuePseudoClassMutation(locked);
- }
- }
- },
-
- /**
- * Get a node's innerHTML property.
- */
- innerHTML: function (node) {
- let html = "";
- if (!isNodeDead(node)) {
- html = node.rawNode.innerHTML;
- }
- return LongStringActor(this.conn, html);
- },
-
- /**
- * Set a node's innerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setInnerHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let rawNode = node.rawNode;
- if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) {
- throw new Error("Can only change innerHTML to element nodes");
- }
- // eslint-disable-next-line no-unsanitized/property
- rawNode.innerHTML = value;
- },
-
- /**
- * Get a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- */
- outerHTML: function (node) {
- let outerHTML = "";
- if (!isNodeDead(node)) {
- outerHTML = node.rawNode.outerHTML;
- }
- return LongStringActor(this.conn, outerHTML);
- },
-
- /**
- * Set a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setOuterHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let parsedDOM = DOMParser.parseFromString(value, "text/html");
- let rawNode = node.rawNode;
- let parentNode = rawNode.parentNode;
-
- // Special case for head and body. Setting document.body.outerHTML
- // creates an extra <head> tag, and document.head.outerHTML creates
- // an extra <body>. So instead we will call replaceChild with the
- // parsed DOM, assuming that they aren't trying to set both tags at once.
- if (rawNode.tagName === "BODY") {
- if (parsedDOM.head.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.body, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (rawNode.tagName === "HEAD") {
- if (parsedDOM.body.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.head, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (node.isDocumentElement()) {
- // Unable to set outerHTML on the document element. Fall back by
- // setting attributes manually, then replace the body and head elements.
- let finalAttributeModifications = [];
- let attributeModifications = {};
- for (let attribute of rawNode.attributes) {
- attributeModifications[attribute.name] = null;
- }
- for (let attribute of parsedDOM.documentElement.attributes) {
- attributeModifications[attribute.name] = attribute.value;
- }
- for (let key in attributeModifications) {
- finalAttributeModifications.push({
- attributeName: key,
- newValue: attributeModifications[key]
- });
- }
- node.modifyAttributes(finalAttributeModifications);
- rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
- rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- },
-
- /**
- * Insert adjacent HTML to a node.
- *
- * @param {Node} node
- * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
- * "afterEnd" (see Element.insertAdjacentHTML).
- * @param {string} value The HTML content.
- */
- insertAdjacentHTML: function (node, position, value) {
- if (isNodeDead(node)) {
- return {node: [], newParents: []};
- }
-
- let rawNode = node.rawNode;
- let isInsertAsSibling = position === "beforeBegin" ||
- position === "afterEnd";
-
- // Don't insert anything adjacent to the document element.
- if (isInsertAsSibling && node.isDocumentElement()) {
- throw new Error("Can't insert adjacent element to the root.");
- }
-
- let rawParentNode = rawNode.parentNode;
- if (!rawParentNode && isInsertAsSibling) {
- throw new Error("Can't insert as sibling without parent node.");
- }
-
- // We can't use insertAdjacentHTML, because we want to return the nodes
- // being created (so the front can remove them if the user undoes
- // the change). So instead, use Range.createContextualFragment().
- let range = rawNode.ownerDocument.createRange();
- if (position === "beforeBegin" || position === "afterEnd") {
- range.selectNode(rawNode);
- } else {
- range.selectNodeContents(rawNode);
- }
- let docFrag = range.createContextualFragment(value);
- let newRawNodes = Array.from(docFrag.childNodes);
- switch (position) {
- case "beforeBegin":
- rawParentNode.insertBefore(docFrag, rawNode);
- break;
- case "afterEnd":
- // Note: if the second argument is null, rawParentNode.insertBefore
- // behaves like rawParentNode.appendChild.
- rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
- break;
- case "afterBegin":
- rawNode.insertBefore(docFrag, rawNode.firstChild);
- break;
- case "beforeEnd":
- rawNode.appendChild(docFrag);
- break;
- default:
- throw new Error("Invalid position value. Must be either " +
- "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'.");
- }
-
- return this.attachElements(newRawNodes);
- },
-
- /**
- * Duplicate a specified node
- *
- * @param {NodeActor} node The node to duplicate.
- */
- duplicateNode: function ({rawNode}) {
- let clonedNode = rawNode.cloneNode(true);
- rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
- },
-
- /**
- * Test whether a node is a document or a document element.
- *
- * @param {NodeActor} node The node to remove.
- * @return {boolean} True if the node is a document or a document element.
- */
- isDocumentOrDocumentElementNode: function (node) {
- return ((node.rawNode.ownerDocument &&
- node.rawNode.ownerDocument.documentElement === this.rawNode) ||
- node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
- },
-
- /**
- * Removes a node from its parent node.
- *
- * @param {NodeActor} node The node to remove.
- * @returns The node's nextSibling before it was removed.
- */
- removeNode: function (node) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes.");
- }
-
- let nextSibling = this.nextSibling(node);
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- return nextSibling;
- },
-
- /**
- * Removes an array of nodes from their parent node.
- *
- * @param {NodeActor[]} nodes The nodes to remove.
- */
- removeNodes: function (nodes) {
- // Check that all nodes are valid before processing the removals.
- for (let node of nodes) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes");
- }
- }
-
- for (let node of nodes) {
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- }
- },
-
- /**
- * Insert a node into the DOM.
- */
- insertBefore: function (node, parent, sibling) {
- if (isNodeDead(node) ||
- isNodeDead(parent) ||
- (sibling && isNodeDead(sibling))) {
- return;
- }
-
- let rawNode = node.rawNode;
- let rawParent = parent.rawNode;
- let rawSibling = sibling ? sibling.rawNode : null;
-
- // Don't bother inserting a node if the document position isn't going
- // to change. This prevents needless iframes reloading and mutations.
- if (rawNode.parentNode === rawParent) {
- let currentNextSibling = this.nextSibling(node);
- currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
- null;
-
- if (rawNode === rawSibling || currentNextSibling === rawSibling) {
- return;
- }
- }
-
- rawParent.insertBefore(rawNode, rawSibling);
- },
-
- /**
- * Editing a node's tagname actually means creating a new node with the same
- * attributes, removing the node and inserting the new one instead.
- * This method does not return anything as mutation events are taking care of
- * informing the consumers about changes.
- */
- editTagName: function (node, tagName) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let oldNode = node.rawNode;
-
- // Create a new element with the same attributes as the current element and
- // prepare to replace the current node with it.
- let newNode;
- try {
- newNode = nodeDocument(oldNode).createElement(tagName);
- } catch (x) {
- // Failed to create a new element with that tag name, ignore the change,
- // and signal the error to the front.
- return Promise.reject(new Error("Could not change node's tagName to " + tagName));
- }
-
- let attrs = oldNode.attributes;
- for (let i = 0; i < attrs.length; i++) {
- newNode.setAttribute(attrs[i].name, attrs[i].value);
- }
-
- // Insert the new node, and transfer the old node's children.
- oldNode.parentNode.insertBefore(newNode, oldNode);
- while (oldNode.firstChild) {
- newNode.appendChild(oldNode.firstChild);
- }
-
- oldNode.remove();
- return null;
- },
-
- /**
- * Get any pending mutation records. Must be called by the client after
- * the `new-mutations` notification is received. Returns an array of
- * mutation records.
- *
- * Mutation records have a basic structure:
- *
- * {
- * type: attributes|characterData|childList,
- * target: <domnode actor ID>,
- * }
- *
- * And additional attributes based on the mutation type:
- *
- * `attributes` type:
- * attributeName: <string> - the attribute that changed
- * attributeNamespace: <string> - the attribute's namespace URI, if any.
- * newValue: <string> - The new value of the attribute, if any.
- *
- * `characterData` type:
- * newValue: <string> - the new nodeValue for the node
- *
- * `childList` type is returned when the set of children for a node
- * has changed. Includes extra data, which can be used by the client to
- * maintain its ownership subtree.
- *
- * added: array of <domnode actor ID> - The list of actors *previously
- * seen by the client* that were added to the target node.
- * removed: array of <domnode actor ID> The list of actors *previously
- * seen by the client* that were removed from the target node.
- * inlineTextChild: If the node now has a single text child, it will
- * be sent here.
- *
- * Actors that are included in a MutationRecord's `removed` but
- * not in an `added` have been removed from the client's ownership
- * tree (either by being moved under a node the client has seen yet
- * or by being removed from the tree entirely), and is considered
- * 'orphaned'.
- *
- * Keep in mind that if a node that the client hasn't seen is moved
- * into or out of the target node, it will not be included in the
- * removedNodes and addedNodes list, so if the client is interested
- * in the new set of children it needs to issue a `children` request.
- */
- getMutations: function (options = {}) {
- let pending = this._pendingMutations || [];
- this._pendingMutations = [];
- this._waitingForGetMutations = false;
-
- if (options.cleanup) {
- for (let node of this._orphaned) {
- // Release the orphaned node. Nodes or children that have been
- // retained will be moved to this._retainedOrphans.
- this.releaseNode(node);
- }
- this._orphaned = new Set();
- }
-
- return pending;
- },
-
- queueMutation: function (mutation) {
- if (!this.actorID || this._destroyed) {
- // We've been destroyed, don't bother queueing this mutation.
- return;
- }
-
- // Add the mutation to the list of mutations to be retrieved next.
- this._pendingMutations.push(mutation);
-
- // Bail out if we already emitted a new-mutations event and are waiting for a client
- // to retrieve them.
- if (this._waitingForGetMutations) {
- return;
- }
-
- if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
- this._emitNewMutations();
- } else {
- /**
- * If many mutations are fired at the same time, clients might sequentially request
- * children/siblings for updated nodes, which can be costly. By throttling the calls
- * to getMutations, duplicated mutations will be ignored.
- */
- this._throttledEmitNewMutations();
- }
- },
-
- _emitNewMutations: function () {
- if (!this.actorID || this._destroyed) {
- // Bail out if the actor was destroyed after throttling this call.
- return;
- }
-
- if (this._waitingForGetMutations || this._pendingMutations.length == 0) {
- // Bail out if we already fired the new-mutation event or if no mutations are
- // waiting to be retrieved.
- return;
- }
-
- this._waitingForGetMutations = true;
- this.emit("new-mutations");
- },
-
- /**
- * Handles mutations from the DOM mutation observer API.
- *
- * @param array[MutationRecord] mutations
- * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
- */
- onMutations: function (mutations) {
- // Notify any observers that want *all* mutations (even on nodes that aren't
- // referenced). This is not sent over the protocol so can only be used by
- // scripts running in the server process.
- this.emit("any-mutation");
-
- for (let change of mutations) {
- let targetActor = this.getNode(change.target);
- if (!targetActor) {
- continue;
- }
- let targetNode = change.target;
- let type = change.type;
- let mutation = {
- type: type,
- target: targetActor.actorID,
- };
-
- if (type === "attributes") {
- mutation.attributeName = change.attributeName;
- mutation.attributeNamespace = change.attributeNamespace || undefined;
- mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
- targetNode.getAttribute(mutation.attributeName)
- : null;
- } else if (type === "characterData") {
- mutation.newValue = targetNode.nodeValue;
- this._maybeQueueInlineTextChildMutation(change, targetNode);
- } else if (type === "childList" || type === "nativeAnonymousChildList") {
- // Get the list of removed and added actors that the client has seen
- // so that it can keep its ownership tree up to date.
- let removedActors = [];
- let addedActors = [];
- for (let removed of change.removedNodes) {
- let removedActor = this.getNode(removed);
- if (!removedActor) {
- // If the client never encountered this actor we don't need to
- // mention that it was removed.
- continue;
- }
- // While removed from the tree, nodes are saved as orphaned.
- this._orphaned.add(removedActor);
- removedActors.push(removedActor.actorID);
- }
- for (let added of change.addedNodes) {
- let addedActor = this.getNode(added);
- if (!addedActor) {
- // If the client never encounted this actor we don't need to tell
- // it about its addition for ownership tree purposes - if the
- // client wants to see the new nodes it can ask for children.
- continue;
- }
- // The actor is reconnected to the ownership tree, unorphan
- // it and let the client know so that its ownership tree is up
- // to date.
- this._orphaned.delete(addedActor);
- addedActors.push(addedActor.actorID);
- }
-
- mutation.numChildren = targetActor.numChildren;
- mutation.removed = removedActors;
- mutation.added = addedActors;
-
- let inlineTextChild = this.inlineTextChild(targetActor);
- if (inlineTextChild) {
- mutation.inlineTextChild = inlineTextChild.form();
- }
- }
- this.queueMutation(mutation);
- }
- },
-
- /**
- * Check if the provided mutation could change the way the target element is
- * inlined with its parent node. If it might, a custom mutation of type
- * "inlineTextChild" will be queued.
- *
- * @param {MutationRecord} mutation
- * A characterData type mutation
- */
- _maybeQueueInlineTextChildMutation: function (mutation) {
- let {oldValue, target} = mutation;
- let newValue = target.nodeValue;
- let limit = gValueSummaryLength;
-
- if ((oldValue.length <= limit && newValue.length <= limit) ||
- (oldValue.length > limit && newValue.length > limit)) {
- // Bail out if the new & old values are both below/above the size limit.
- return;
- }
-
- let parentActor = this.getNode(target.parentNode);
- if (!parentActor || parentActor.rawNode.children.length > 0) {
- // If the parent node has other children, a character data mutation will
- // not change anything regarding inlining text nodes.
- return;
- }
-
- let inlineTextChild = this.inlineTextChild(parentActor);
- this.queueMutation({
- type: "inlineTextChild",
- target: parentActor.actorID,
- inlineTextChild:
- inlineTextChild ? inlineTextChild.form() : undefined
- });
- },
-
- onFrameLoad: function ({ window, isTopLevel }) {
- let { readyState } = window.document;
- if (readyState != "interactive" && readyState != "complete") {
- window.addEventListener("DOMContentLoaded",
- this.onFrameLoad.bind(this, { window, isTopLevel }),
- { once: true });
- return;
- }
- if (isTopLevel) {
- // If we initialize the inspector while the document is loading,
- // we may already have a root document set in the constructor.
- if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) &&
- this.rootDoc.defaultView) {
- this.onFrameUnload({ window: this.rootDoc.defaultView });
- }
- // Update all DOM objects references to target the new document.
- this.rootWin = window;
- this.rootDoc = window.document;
- this.rootNode = this.document();
- this.queueMutation({
- type: "newRoot",
- target: this.rootNode.form()
- });
- return;
- }
- let frame = getFrameElement(window);
- let frameActor = this.getNode(frame);
- if (!frameActor) {
- return;
- }
-
- this.queueMutation({
- type: "frameLoad",
- target: frameActor.actorID,
- });
-
- // Send a childList mutation on the frame.
- this.queueMutation({
- type: "childList",
- target: frameActor.actorID,
- added: [],
- removed: []
- });
- },
-
- // Returns true if domNode is in window or a subframe.
- _childOfWindow: function (window, domNode) {
- let win = nodeDocument(domNode).defaultView;
- while (win) {
- if (win === window) {
- return true;
- }
- win = getFrameElement(win);
- }
- return false;
- },
-
- onFrameUnload: function ({ window }) {
- // Any retained orphans that belong to this document
- // or its children need to be released, and a mutation sent
- // to notify of that.
- let releasedOrphans = [];
-
- for (let retained of this._retainedOrphans) {
- if (Cu.isDeadWrapper(retained.rawNode) ||
- this._childOfWindow(window, retained.rawNode)) {
- this._retainedOrphans.delete(retained);
- releasedOrphans.push(retained.actorID);
- this.releaseNode(retained, { force: true });
- }
- }
-
- if (releasedOrphans.length > 0) {
- this.queueMutation({
- target: this.rootNode.actorID,
- type: "unretained",
- nodes: releasedOrphans
- });
- }
-
- let doc = window.document;
- let documentActor = this.getNode(doc);
- if (!documentActor) {
- return;
- }
-
- if (this.rootDoc === doc) {
- this.rootDoc = null;
- this.rootNode = null;
- }
-
- this.queueMutation({
- type: "documentUnload",
- target: documentActor.actorID
- });
-
- let walker = this.getDocumentWalker(doc);
- let parentNode = walker.parentNode();
- if (parentNode) {
- // Send a childList mutation on the frame so that clients know
- // they should reread the children list.
- this.queueMutation({
- type: "childList",
- target: this.getNode(parentNode).actorID,
- added: [],
- removed: []
- });
- }
-
- // Need to force a release of this node, because those nodes can't
- // be accessed anymore.
- this.releaseNode(documentActor, { force: true });
- },
-
- /**
- * Check if a node is attached to the DOM tree of the current page.
- * @param {nsIDomNode} rawNode
- * @return {Boolean} false if the node is removed from the tree or within a
- * document fragment
- */
- _isInDOMTree: function (rawNode) {
- let walker = this.getDocumentWalker(rawNode);
- let current = walker.currentNode;
-
- // Reaching the top of tree
- while (walker.parentNode()) {
- current = walker.currentNode;
- }
-
- // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
- // attached
- if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
- current !== this.rootDoc) {
- return false;
- }
-
- // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
- return true;
- },
-
- /**
- * @see _isInDomTree
- */
- isInDOMTree: function (node) {
- if (isNodeDead(node)) {
- return false;
- }
- return this._isInDOMTree(node.rawNode);
- },
-
- /**
- * Given an ObjectActor (identified by its ID), commonly used in the debugger,
- * webconsole and variablesView, return the corresponding inspector's
- * NodeActor
- */
- getNodeActorFromObjectActor: function (objectActorID) {
- let actor = this.conn.getActor(objectActorID);
- if (!actor) {
- return null;
- }
-
- let debuggerObject = this.conn.getActor(objectActorID).obj;
- let rawNode = debuggerObject.unsafeDereference();
-
- if (!this._isInDOMTree(rawNode)) {
- return null;
- }
-
- // This is a special case for the document object whereby it is considered
- // as document.documentElement (the <html> node)
- if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
- rawNode = rawNode.documentElement;
- }
-
- return this.attachElement(rawNode);
- },
-
- /**
- * Given a windowID return the NodeActor for the corresponding frameElement,
- * unless it's the root window
- */
- getNodeActorFromWindowID: function (windowID) {
- let win;
-
- try {
- win = Services.wm.getOuterWindowWithId(windowID);
- } catch (e) {
- // ignore
- }
-
- if (!win) {
- return { error: "noWindow",
- message: "The related docshell is destroyed or not found" };
- } else if (!win.frameElement) {
- // the frame element of the root document is privileged & thus
- // inaccessible, so return the document body/element instead
- return this.attachElement(win.document.body || win.document.documentElement);
- }
-
- return this.attachElement(win.frameElement);
- },
-
- /**
- * Given a StyleSheetActor (identified by its ID), commonly used in the
- * style-editor, get its ownerNode and return the corresponding walker's
- * NodeActor.
- * Note that getNodeFromActor was added later and can now be used instead.
- */
- getStyleSheetOwnerNode: function (styleSheetActorID) {
- return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
- },
-
- /**
- * This method can be used to retrieve NodeActor for DOM nodes from other
- * actors in a way that they can later be highlighted in the page, or
- * selected in the inspector.
- * If an actor has a reference to a DOM node, and the UI needs to know about
- * this DOM node (and possibly select it in the inspector), the UI should
- * first retrieve a reference to the walkerFront:
- *
- * // Make sure the inspector/walker have been initialized first.
- * toolbox.initInspector().then(() => {
- * // Retrieve the walker.
- * let walker = toolbox.walker;
- * });
- *
- * And then call this method:
- *
- * // Get the nodeFront from my actor, passing the ID and properties path.
- * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
- * // Use the nodeFront, e.g. select the node in the inspector.
- * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
- * });
- *
- * @param {String} actorID The ID for the actor that has a reference to the
- * DOM node.
- * @param {Array} path Where, on the actor, is the DOM node stored. If in the
- * scope of the actor, the node is available as `this.data.node`, then this
- * should be ["data", "node"].
- * @return {NodeActor} The attached NodeActor, or null if it couldn't be
- * found.
- */
- getNodeFromActor: function (actorID, path) {
- let actor = this.conn.getActor(actorID);
- if (!actor) {
- return null;
- }
-
- let obj = actor;
- for (let name of path) {
- if (!(name in obj)) {
- return null;
- }
- obj = obj[name];
- }
-
- return this.attachElement(obj);
- },
-
- /**
- * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
- * information.
- *
- * @return {LayoutActor}
- */
- getLayoutInspector: function () {
- if (!this.layoutActor) {
- this.layoutActor = new LayoutActor(this.conn, this.tabActor, this);
- }
-
- return this.layoutActor;
- },
-
- /**
- * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
- * returns null.
- */
- getOffsetParent: function (node) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let offsetParent = node.rawNode.offsetParent;
-
- if (!offsetParent) {
- return null;
- }
-
- return this._ref(offsetParent);
- },
-});
-
-/**
- * Server side of the inspector actor, which is used to create
- * inspector-related actors, including the walker.
- */
-exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
- initialize: function (conn, tabActor) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
-
- this._onColorPicked = this._onColorPicked.bind(this);
- this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
- this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- this.destroyEyeDropper();
-
- this._highlighterPromise = null;
- this._pageStylePromise = null;
- this._walkerPromise = null;
- this.walker = null;
- this.tabActor = null;
- },
-
- get window() {
- return this.tabActor.window;
- },
-
- getWalker: function (options = {}) {
- if (this._walkerPromise) {
- return this._walkerPromise;
- }
-
- let deferred = defer();
- this._walkerPromise = deferred.promise;
-
- let window = this.window;
- let domReady = () => {
- let tabActor = this.tabActor;
- window.removeEventListener("DOMContentLoaded", domReady, true);
- this.walker = WalkerActor(this.conn, tabActor, options);
- this.manage(this.walker);
- this.walker.once("destroyed", () => {
- this._walkerPromise = null;
- this._pageStylePromise = null;
- });
- deferred.resolve(this.walker);
- };
-
- if (window.document.readyState === "loading") {
- window.addEventListener("DOMContentLoaded", domReady, true);
- } else {
- domReady();
- }
-
- return this._walkerPromise;
- },
-
- getPageStyle: function () {
- if (this._pageStylePromise) {
- return this._pageStylePromise;
- }
-
- this._pageStylePromise = this.getWalker().then(walker => {
- let pageStyle = PageStyleActor(this);
- this.manage(pageStyle);
- return pageStyle;
- });
- return this._pageStylePromise;
- },
-
- /**
- * The most used highlighter actor is the HighlighterActor which can be
- * conveniently retrieved by this method.
- * The same instance will always be returned by this method when called
- * several times.
- * The highlighter actor returned here is used to highlighter elements's
- * box-models from the markup-view, box model, console, debugger, ... as
- * well as select elements with the pointer (pick).
- *
- * @param {Boolean} autohide Optionally autohide the highlighter after an
- * element has been picked
- * @return {HighlighterActor}
- */
- getHighlighter: function (autohide) {
- if (this._highlighterPromise) {
- return this._highlighterPromise;
- }
-
- this._highlighterPromise = this.getWalker().then(walker => {
- let highlighter = HighlighterActor(this, autohide);
- this.manage(highlighter);
- return highlighter;
- });
- return this._highlighterPromise;
- },
-
- /**
- * If consumers need to display several highlighters at the same time or
- * different types of highlighters, then this method should be used, passing
- * the type name of the highlighter needed as argument.
- * A new instance will be created everytime the method is called, so it's up
- * to the consumer to release it when it is not needed anymore
- *
- * @param {String} type The type of highlighter to create
- * @return {Highlighter} The highlighter actor instance or null if the
- * typeName passed doesn't match any available highlighter
- */
- getHighlighterByType: function (typeName) {
- if (isTypeRegistered(typeName)) {
- return CustomHighlighterActor(this, typeName);
- }
- return null;
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageDataFromURL: function (url, maxDim) {
- let img = new this.window.Image();
- img.src = url;
-
- // imageToImageData waits for the image to load.
- return imageToImageData(img, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Resolve a URL to its absolute form, in the scope of a given content window.
- * @param {String} url.
- * @param {NodeActor} node If provided, the owner window of this node will be
- * used to resolve the URL. Otherwise, the top-level content window will be
- * used instead.
- * @return {String} url.
- */
- resolveRelativeURL: function (url, node) {
- let document = isNodeDead(node)
- ? this.window.document
- : nodeDocument(node.rawNode);
-
- if (!document) {
- return url;
- }
-
- let baseURI = Services.io.newURI(document.location.href);
- return Services.io.newURI(url, null, baseURI).spec;
- },
-
- /**
- * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
- * Note that for now, a new instance is created every time to deal with page navigation.
- */
- createEyeDropper: function () {
- this.destroyEyeDropper();
- this._highlighterEnv = new HighlighterEnvironment();
- this._highlighterEnv.initFromTabActor(this.tabActor);
- this._eyeDropper = new EyeDropper(this._highlighterEnv);
- },
-
- /**
- * Destroy the current eye-dropper highlighter instance.
- */
- destroyEyeDropper: function () {
- if (this._eyeDropper) {
- this.cancelPickColorFromPage();
- this._eyeDropper.destroy();
- this._eyeDropper = null;
- this._highlighterEnv.destroy();
- this._highlighterEnv = null;
- }
- },
-
- /**
- * Pick a color from the page using the eye-dropper. This method doesn't return anything
- * but will cause events to be sent to the front when a color is picked or when the user
- * cancels the picker.
- * @param {Object} options
- */
- pickColorFromPage: function (options) {
- this.createEyeDropper();
- this._eyeDropper.show(this.window.document.documentElement, options);
- this._eyeDropper.once("selected", this._onColorPicked);
- this._eyeDropper.once("canceled", this._onColorPickCanceled);
- this.tabActor.once("will-navigate", this.destroyEyeDropper);
- },
-
- /**
- * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
- * highlighter is for the user to click in the page and select a color. If you need to
- * dismiss the eye-dropper programatically instead, use this method.
- */
- cancelPickColorFromPage: function () {
- if (this._eyeDropper) {
- this._eyeDropper.hide();
- this._eyeDropper.off("selected", this._onColorPicked);
- this._eyeDropper.off("canceled", this._onColorPickCanceled);
- this.tabActor.off("will-navigate", this.destroyEyeDropper);
- }
- },
-
- /**
- * Check if the current document supports highlighters using a canvasFrame anonymous
- * content container (ie all highlighters except the SimpleOutlineHighlighter).
- * It is impossible to detect the feature programmatically as some document types simply
- * don't render the canvasFrame without throwing any error.
- */
- supportsHighlighters: function () {
- let doc = this.tabActor.window.document;
- let ns = doc.documentElement.namespaceURI;
-
- // XUL documents do not support insertAnonymousContent().
- if (ns === XUL_NS) {
- return false;
- }
-
- // SVG documents do not render the canvasFrame (see Bug 1157592).
- if (ns === SVG_NS) {
- return false;
- }
-
- return true;
- },
-
- _onColorPicked: function (e, color) {
- this.emit("color-picked", color);
- },
-
- _onColorPickCanceled: function () {
- this.emit("color-pick-canceled");
- }
-});
-
-// Exported for test purposes.
-exports._documentWalker = DocumentWalker;
-
-function nodeDocument(node) {
- if (Cu.isDeadWrapper(node)) {
- return null;
- }
- return node.ownerDocument ||
- (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
-}
-
-function nodeDocshell(node) {
- let doc = node ? nodeDocument(node) : null;
- let win = doc ? doc.defaultView : null;
- if (win) {
- return win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell);
- }
- return null;
-}
-
-function isNodeDead(node) {
- return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
-}
-
-/**
- * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
- * See inDeepTreeWalker for more information about the methods.
- *
- * @param {DOMNode} node
- * @param {Window} rootWin
- * @param {Number} whatToShow
- * See nodeFilterConstants / inIDeepTreeWalker for options.
- * @param {Function} filter
- * A custom filter function Taking in a DOMNode and returning an Int. See
- * WalkerActor.nodeFilter for an example.
- * @param {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.
- */
-function DocumentWalker(node, rootWin,
- whatToShow = nodeFilterConstants.SHOW_ALL,
- filter = standardTreeWalkerFilter,
- skipTo = SKIP_TO_PARENT) {
- 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 = true;
- 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 () {
- return this.walker.parentNode();
- },
-
- nextNode: function () {
- let 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 () {
- let 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 () {
- let 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.
- let 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 && previous.previousSibling;
- next = 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;
- },
-};
-
-function isInXULDocument(el) {
- let doc = nodeDocument(el);
- return doc &&
- doc.documentElement &&
- doc.documentElement.namespaceURI === XUL_NS;
-}
-
-/**
- * This DeepTreeWalker filter skips whitespace text nodes and anonymous
- * content with the exception of ::before and ::after and anonymous content
- * in XUL document (needed to show all elements in the browser toolbox).
- */
-function standardTreeWalkerFilter(node) {
- // ::before and ::after are native anonymous content, but we always
- // want to show them
- if (node.nodeName === "_moz_generated_content_before" ||
- node.nodeName === "_moz_generated_content_after") {
- return nodeFilterConstants.FILTER_ACCEPT;
- }
-
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
-
- // Ignore all native and XBL anonymous content inside a non-XUL document.
- // We need to do this to skip things like form controls, scrollbars,
- // video controls, etc (see bug 1187482).
- if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
- isNativeAnonymous(node))) {
- return nodeFilterConstants.FILTER_SKIP;
- }
-
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
- * it also includes all anonymous content (like internal form controls).
- */
-function allAnonymousContentTreeWalkerFilter(node) {
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * Is the given node a text node composed of whitespace only?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function isWhitespaceTextNode(node) {
- return node.nodeType == Ci.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
-}
-
-/**
- * Does the given node have non-0 width and height?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function nodeHasSize(node) {
- if (!node.getBoxQuads) {
- return false;
- }
-
- let quads = node.getBoxQuads();
- return quads.length && quads.some(quad => quad.bounds.width && quad.bounds.height);
-}
-
-/**
- * Returns a promise that is settled once the given HTMLImageElement has
- * finished loading.
- *
- * @param {HTMLImageElement} image - The image element.
- * @param {Number} timeout - Maximum amount of time the image is allowed to load
- * before the waiting is aborted. Ignored if flags.testing is set.
- *
- * @return {Promise} that is fulfilled once the image has loaded. If the image
- * fails to load or the load takes too long, the promise is rejected.
- */
-function ensureImageLoaded(image, timeout) {
- let { HTMLImageElement } = image.ownerGlobal;
- if (!(image instanceof HTMLImageElement)) {
- return promise.reject("image must be an HTMLImageELement");
- }
-
- if (image.complete) {
- // The image has already finished loading.
- return promise.resolve();
- }
-
- // This image is still loading.
- let onLoad = AsyncUtils.listenOnce(image, "load");
-
- // Reject if loading fails.
- let onError = AsyncUtils.listenOnce(image, "error").then(() => {
- return promise.reject("Image '" + image.src + "' failed to load.");
- });
-
- // Don't timeout when testing. This is never settled.
- let onAbort = new Promise(() => {});
-
- if (!flags.testing) {
- // Tests are not running. Reject the promise after given timeout.
- onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
- return promise.reject("Image '" + image.src + "' took too long to load.");
- });
- }
-
- // See which happens first.
- return promise.race([onLoad, onError, onAbort]);
-}
-
-/**
- * Given an <img> or <canvas> element, return the image data-uri. If @param node
- * is an <img> element, the method waits a while for the image to load before
- * the data is generated. If the image does not finish loading in a reasonable
- * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
- *
- * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
- * element, or Image() object. Other types cause the method to reject.
- * @param {Number} maxDim - Optionally pass a maximum size you want the longest
- * side of the image to be resized to before getting the image data.
-
- * @return {Promise} A promise that is fulfilled with an object containing the
- * data-uri and size-related information:
- * { data: "...",
- * size: {
- * naturalWidth: 400,
- * naturalHeight: 300,
- * resized: true }
- * }.
- *
- * If something goes wrong, the promise is rejected.
- */
-var imageToImageData = Task.async(function* (node, maxDim) {
- let { HTMLCanvasElement, HTMLImageElement } = node.ownerGlobal;
-
- let isImg = node instanceof HTMLImageElement;
- let isCanvas = node instanceof HTMLCanvasElement;
-
- if (!isImg && !isCanvas) {
- throw new Error("node is not a <canvas> or <img> element.");
- }
-
- if (isImg) {
- // Ensure that the image is ready.
- yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
- }
-
- // Get the image resize ratio if a maxDim was provided
- let resizeRatio = 1;
- let imgWidth = node.naturalWidth || node.width;
- let imgHeight = node.naturalHeight || node.height;
- let imgMax = Math.max(imgWidth, imgHeight);
- if (maxDim && imgMax > maxDim) {
- resizeRatio = maxDim / imgMax;
- }
-
- // Extract the image data
- let imageData;
- // The image may already be a data-uri, in which case, save ourselves the
- // trouble of converting via the canvas.drawImage.toDataURL method, but only
- // if the image doesn't need resizing
- if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
- imageData = node.src;
- } else {
- // Create a canvas to copy the rawNode into and get the imageData from
- let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
- canvas.width = imgWidth * resizeRatio;
- canvas.height = imgHeight * resizeRatio;
- let ctx = canvas.getContext("2d");
-
- // Copy the rawNode image or canvas in the new canvas and extract data
- ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
- imageData = canvas.toDataURL("image/png");
- }
-
- return {
- data: imageData,
- size: {
- naturalWidth: imgWidth,
- naturalHeight: imgHeight,
- resized: resizeRatio !== 1
- }
- };
-});
+exports.NodeActor = NodeActor;
+exports.NodeListActor = NodeListActor;
copy from devtools/server/actors/inspector/inspector.js
copy to devtools/server/actors/inspector/utils.js
--- a/devtools/server/actors/inspector/inspector.js
+++ b/devtools/server/actors/inspector/utils.js
@@ -1,190 +1,31 @@
/* 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";
-/**
- * Here's the server side of the remote inspector.
- *
- * The WalkerActor is the client's view of the debuggee's DOM. It's gives
- * the client a tree of NodeActor objects.
- *
- * The walker presents the DOM tree mostly unmodified from the source DOM
- * tree, but with a few key differences:
- *
- * - Empty text nodes are ignored. This is pretty typical of developer
- * tools, but maybe we should reconsider that on the server side.
- * - iframes with documents loaded have the loaded document as the child,
- * the walker provides one big tree for the whole document tree.
- *
- * There are a few ways to get references to NodeActors:
- *
- * - When you first get a WalkerActor reference, it comes with a free
- * reference to the root document's node.
- * - Given a node, you can ask for children, siblings, and parents.
- * - You can issue querySelector and querySelectorAll requests to find
- * other elements.
- * - Requests that return arbitrary nodes from the tree (like querySelector
- * and querySelectorAll) will also return any nodes the client hasn't
- * seen in order to have a complete set of parents.
- *
- * Once you have a NodeFront, you should be able to answer a few questions
- * without further round trips, like the node's name, namespace/tagName,
- * attributes, etc. Other questions (like a text node's full nodeValue)
- * might require another round trip.
- *
- * The protocol guarantees that the client will always know the parent of
- * any node that is returned by the server. This means that some requests
- * (like querySelector) will include the extra nodes needed to satisfy this
- * requirement. The client keeps track of this parent relationship, so the
- * node fronts form a tree that is a subset of the actual DOM tree.
- *
- *
- * We maintain this guarantee to support the ability to release subtrees on
- * the client - when a node is disconnected from the DOM tree we want to be
- * able to free the client objects for all the children nodes.
- *
- * So to be able to answer "all the children of a given node that we have
- * seen on the client side", we guarantee that every time we've seen a node,
- * we connect it up through its parents.
- */
+const {Ci, Cu} = require("chrome");
+
+const promise = require("promise");
+const {Task} = require("devtools/shared/task");
-const {Cc, Ci, Cu} = require("chrome");
-const Services = require("Services");
-const protocol = require("devtools/shared/protocol");
-const {LongStringActor} = require("devtools/server/actors/string");
-const promise = require("promise");
-const defer = require("devtools/shared/defer");
-const {Task} = require("devtools/shared/task");
-const EventEmitter = require("devtools/shared/event-emitter");
-const InspectorUtils = require("InspectorUtils");
-
-const {walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
-const {nodeSpec, nodeListSpec} = require("devtools/shared/specs/node");
-
+loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
+loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-loader.lazyRequireGetter(this, "EyeDropper", "devtools/server/actors/highlighters/eye-dropper", true);
-loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
-loader.lazyRequireGetter(this, "PageStyleActor", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
-loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
-loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
-loader.lazyRequireGetter(this, "HighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "HighlighterEnvironment", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
-loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
+
loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isShadowAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
-loader.lazyServiceGetter(this, "DOMParser",
- "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
-
-loader.lazyServiceGetter(this, "eventListenerService",
- "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
-
-const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
-const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
-const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
-const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
-const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const IMAGE_FETCHING_TIMEOUT = 500;
-// Minimum delay between two "new-mutations" events.
-const MUTATIONS_THROTTLING_DELAY = 100;
-// List of mutation types that should -not- be throttled.
-const IMMEDIATE_MUTATIONS = [
- "documentUnload",
- "frameLoad",
- "newRoot",
- "pseudoClassLock",
-];
-
-// 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";
-
-// The possible completions to a ':' with added score to give certain values
-// some preference.
-const PSEUDO_SELECTORS = [
- [":active", 1],
- [":hover", 1],
- [":focus", 1],
- [":visited", 0],
- [":link", 0],
- [":first-letter", 0],
- [":first-child", 2],
- [":before", 2],
- [":after", 2],
- [":lang(", 0],
- [":not(", 3],
- [":first-of-type", 0],
- [":last-of-type", 0],
- [":only-of-type", 0],
- [":only-child", 2],
- [":nth-child(", 3],
- [":nth-last-child(", 0],
- [":nth-of-type(", 0],
- [":nth-last-of-type(", 0],
- [":last-child", 2],
- [":root", 0],
- [":empty", 0],
- [":target", 0],
- [":enabled", 0],
- [":disabled", 0],
- [":checked", 1],
- ["::selection", 0]
-];
-
-var HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
- .__fx-devtools-hide-shortcut__ {
- visibility: hidden !important;
- }
-
- :-moz-devtools-highlighted {
- outline: 2px dashed #F06!important;
- outline-offset: -2px !important;
- }
-`);
-
-/**
- * We only send nodeValue up to a certain size by default. This stuff
- * controls that size.
- */
-exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
-var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
-
-exports.getValueSummaryLength = function () {
- return gValueSummaryLength;
-};
-
-exports.setValueSummaryLength = function (val) {
- gValueSummaryLength = val;
-};
-
/**
* Returns the properly cased version of the node's tag name, which can be
* used when displaying said name in the UI.
*
* @param {Node} rawNode
* Node for which we want the display name
* @return {String}
* Properly cased version of the node tag name
@@ -192,3026 +33,29 @@ exports.setValueSummaryLength = function
const getNodeDisplayName = function (rawNode) {
if (rawNode.nodeName && !rawNode.localName) {
// The localName & prefix APIs have been moved from the Node interface to the Element
// interface. Use Node.nodeName as a fallback.
return rawNode.nodeName;
}
return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
};
-exports.getNodeDisplayName = getNodeDisplayName;
-
-/**
- * Server side of the node actor.
- */
-var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
- initialize: function (walker, node) {
- protocol.Actor.prototype.initialize.call(this, null);
- this.walker = walker;
- this.rawNode = node;
- this._eventParsers = new EventParsers().parsers;
-
- // Storing the original display of the node, to track changes when reflows
- // occur
- this.wasDisplayed = this.isDisplayed;
- },
-
- toString: function () {
- return "[NodeActor " + this.actorID + " for " +
- this.rawNode.toString() + "]";
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- isDocumentElement: function () {
- return this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.documentElement === this.rawNode;
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- if (this.mutationObserver) {
- if (!Cu.isDeadWrapper(this.mutationObserver)) {
- this.mutationObserver.disconnect();
- }
- this.mutationObserver = null;
- }
- this.rawNode = null;
- this.walker = null;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function (detail) {
- if (detail === "actorid") {
- return this.actorID;
- }
-
- let parentNode = this.walker.parentNode(this);
- let inlineTextChild = this.walker.inlineTextChild(this);
-
- let form = {
- actor: this.actorID,
- baseURI: this.rawNode.baseURI,
- parent: parentNode ? parentNode.actorID : undefined,
- nodeType: this.rawNode.nodeType,
- namespaceURI: this.rawNode.namespaceURI,
- nodeName: this.rawNode.nodeName,
- nodeValue: this.rawNode.nodeValue,
- displayName: getNodeDisplayName(this.rawNode),
- numChildren: this.numChildren,
- inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
-
- // doctype attributes
- name: this.rawNode.name,
- publicId: this.rawNode.publicId,
- systemId: this.rawNode.systemId,
-
- attrs: this.writeAttrs(),
- isBeforePseudoElement: this.isBeforePseudoElement,
- isAfterPseudoElement: this.isAfterPseudoElement,
- isAnonymous: isAnonymous(this.rawNode),
- isNativeAnonymous: isNativeAnonymous(this.rawNode),
- isXBLAnonymous: isXBLAnonymous(this.rawNode),
- isShadowAnonymous: isShadowAnonymous(this.rawNode),
- pseudoClassLocks: this.writePseudoClassLocks(),
-
- isDisplayed: this.isDisplayed,
- isInHTMLDocument: this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.contentType === "text/html",
- hasEventListeners: this._hasEventListeners,
- };
-
- if (this.isDocumentElement()) {
- form.isDocumentElement = true;
- }
-
- // Add an extra API for custom properties added by other
- // modules/extensions.
- form.setFormProperty = (name, value) => {
- if (!form.props) {
- form.props = {};
- }
- form.props[name] = value;
- };
-
- // Fire an event so, other modules can create its own properties
- // that should be passed to the client (within the form.props field).
- EventEmitter.emit(NodeActor, "form", {
- target: this,
- data: form
- });
-
- return form;
- },
-
- /**
- * Watch the given document node for mutations using the DOM observer
- * API.
- */
- watchDocument: function (callback) {
- let node = this.rawNode;
- // Create the observer on the node's actor. The node will make sure
- // the observer is cleaned up when the actor is released.
- let observer = new node.defaultView.MutationObserver(callback);
- observer.mergeAttributeRecords = true;
- observer.observe(node, {
- nativeAnonymousChildList: true,
- attributes: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true
- });
- this.mutationObserver = observer;
- },
-
- get isBeforePseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_before";
- },
-
- get isAfterPseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_after";
- },
-
- // Estimate the number of children that the walker will return without making
- // a call to children() if possible.
- get numChildren() {
- // For pseudo elements, childNodes.length returns 1, but the walker
- // will return 0.
- if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
- return 0;
- }
-
- let rawNode = this.rawNode;
- let numChildren = rawNode.childNodes.length;
- let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
- rawNode.ownerDocument.getAnonymousNodes(rawNode);
-
- let hasContentDocument = rawNode.contentDocument;
- let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
- if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
- // This might be an iframe with virtual children.
- numChildren = 1;
- }
-
- // Normal counting misses ::before/::after. Also, some anonymous children
- // may ultimately be skipped, so we have to consult with the walker.
- if (numChildren === 0 || hasAnonChildren) {
- numChildren = this.walker.children(this).nodes.length;
- }
-
- return numChildren;
- },
-
- get computedStyle() {
- return CssLogic.getComputedStyle(this.rawNode);
- },
-
- /**
- * Is the node's display computed style value other than "none"
- */
- get isDisplayed() {
- // Consider all non-element nodes as displayed.
- if (isNodeDead(this) ||
- this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
- this.isAfterPseudoElement ||
- this.isBeforePseudoElement) {
- return true;
- }
-
- let style = this.computedStyle;
- if (!style) {
- return true;
- }
-
- return style.display !== "none";
- },
-
- /**
- * Are there event listeners that are listening on this node? This method
- * uses all parsers registered via event-parsers.js.registerEventParser() to
- * check if there are any event listeners.
- */
- get _hasEventListeners() {
- let parsers = this._eventParsers;
- for (let [, {hasListeners}] of parsers) {
- try {
- if (hasListeners && hasListeners(this.rawNode)) {
- return true;
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
- return false;
- },
-
- writeAttrs: function () {
- if (!this.rawNode.attributes) {
- return undefined;
- }
-
- return [...this.rawNode.attributes].map(attr => {
- return {namespace: attr.namespace, name: attr.name, value: attr.value };
- });
- },
-
- writePseudoClassLocks: function () {
- if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return undefined;
- }
- let ret = undefined;
- for (let pseudo of PSEUDO_CLASSES) {
- if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
- ret = ret || [];
- ret.push(pseudo);
- }
- }
- return ret;
- },
-
- /**
- * Gets event listeners and adds their information to the events array.
- *
- * @param {Node} node
- * Node for which we are to get listeners.
- */
- getEventListeners: function (node) {
- let parsers = this._eventParsers;
- let dbg = this.parent().tabActor.makeDebugger();
- let listenerArray = [];
-
- for (let [, {getListeners, normalizeListener}] of parsers) {
- try {
- let listeners = getListeners(node);
-
- if (!listeners) {
- continue;
- }
-
- for (let listener of listeners) {
- if (normalizeListener) {
- listener.normalizeListener = normalizeListener;
- }
-
- this.processHandlerForEvent(node, listenerArray, dbg, listener);
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
-
- listenerArray.sort((a, b) => {
- return a.type.localeCompare(b.type);
- });
-
- return listenerArray;
- },
-
- /**
- * Process a handler
- *
- * @param {Node} node
- * The node for which we want information.
- * @param {Array} listenerArray
- * listenerArray contains all event objects that we have gathered
- * so far.
- * @param {Debugger} dbg
- * JSDebugger instance.
- * @param {Object} eventInfo
- * See event-parsers.js.registerEventParser() for a description of the
- * eventInfo object.
- *
- * @return {Array}
- * An array of objects where a typical object looks like this:
- * {
- * type: "click",
- * handler: function() { doSomething() },
- * origin: "http://www.mozilla.com",
- * searchString: 'onclick="doSomething()"',
- * tags: tags,
- * DOM0: true,
- * capturing: true,
- * hide: {
- * DOM0: true
- * },
- * native: false
- * }
- */
- processHandlerForEvent: function (node, listenerArray, dbg, listener) {
- let { handler } = listener;
- let global = Cu.getGlobalForObject(handler);
- let globalDO = dbg.addDebuggee(global);
- let listenerDO = globalDO.makeDebuggeeValue(handler);
-
- let { normalizeListener } = listener;
-
- if (normalizeListener) {
- listenerDO = normalizeListener(listenerDO, listener);
- }
-
- let { capturing } = listener;
- let dom0 = false;
- let functionSource = handler.toString();
- let hide = listener.hide || {};
- let line = 0;
- let native = false;
- let override = listener.override || {};
- let tags = listener.tags || "";
- let type = listener.type || "";
- let url = "";
-
- // If the listener is an object with a 'handleEvent' method, use that.
- if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
- let desc;
-
- while (!desc && listenerDO) {
- desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
- listenerDO = listenerDO.proto;
- }
-
- if (desc && desc.value) {
- listenerDO = desc.value;
- }
- }
-
- // If the listener is bound to a different context then we need to switch
- // to the bound function.
- if (listenerDO.isBoundFunction) {
- listenerDO = listenerDO.boundTargetFunction;
- }
-
- let { isArrowFunction, name, script, parameterNames } = listenerDO;
-
- if (script) {
- let scriptSource = script.source.text;
-
- // Scripts are provided via script tags. If it wasn't provided by a
- // script tag it must be a DOM0 event.
- if (script.source.element) {
- dom0 = script.source.element.class !== "HTMLScriptElement";
- } else {
- dom0 = false;
- }
-
- line = script.startLine;
- url = script.url;
-
- // Checking for the string "[native code]" is the only way at this point
- // to check for native code. Even if this provides a false positive then
- // grabbing the source code a second time is harmless.
- if (functionSource === "[object Object]" ||
- functionSource === "[object XULElement]" ||
- functionSource.includes("[native code]")) {
- functionSource =
- scriptSource.substr(script.sourceStart, script.sourceLength);
-
- // At this point the script looks like this:
- // () { ... }
- // We prefix this with "function" if it is not a fat arrow function.
- if (!isArrowFunction) {
- functionSource = "function " + functionSource;
- }
- }
- } else {
- // If the listener is a native one (provided by C++ code) then we have no
- // access to the script. We use the native flag to prevent showing the
- // debugger button because the script is not available.
- native = true;
- }
-
- // Fat arrow function text always contains the parameters. Function
- // parameters are often missing e.g. if Array.sort is used as a handler.
- // If they are missing we provide the parameters ourselves.
- if (parameterNames && parameterNames.length > 0) {
- let prefix = "function " + name + "()";
- let paramString = parameterNames.join(", ");
-
- if (functionSource.startsWith(prefix)) {
- functionSource = functionSource.substr(prefix.length);
-
- functionSource = `function ${name} (${paramString})${functionSource}`;
- }
- }
-
- // If the listener is native code we display the filename "[native code]."
- // This is the official string and should *not* be translated.
- let origin;
- if (native) {
- origin = "[native code]";
- } else {
- origin = url + ((dom0 || line === 0) ? "" : ":" + line);
- }
-
- let eventObj = {
- type: override.type || type,
- handler: override.handler || functionSource.trim(),
- origin: override.origin || origin,
- tags: override.tags || tags,
- DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
- capturing: typeof override.capturing !== "undefined" ?
- override.capturing : capturing,
- hide: typeof override.hide !== "undefined" ? override.hide : hide,
- native
- };
-
- // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
- // generated dynamically from e.g. an onclick="" attribute so the script
- // doesn't actually exist.
- if (native || dom0) {
- eventObj.hide.debugger = true;
- }
-
- listenerArray.push(eventObj);
-
- dbg.removeDebuggee(globalDO);
- },
-
- /**
- * Returns a LongStringActor with the node's value.
- */
- getNodeValue: function () {
- return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
- },
-
- /**
- * Set the node's value to a given string.
- */
- setNodeValue: function (value) {
- this.rawNode.nodeValue = value;
- },
-
- /**
- * Get a unique selector string for this node.
- */
- getUniqueSelector: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return findCssSelector(this.rawNode);
- },
-
- /**
- * Get the full CSS path for this node.
- *
- * @return {String} A CSS selector with a part for the node and each of its ancestors.
- */
- getCssPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getCssPath(this.rawNode);
- },
-
- /**
- * Get the XPath for this node.
- *
- * @return {String} The XPath for finding this node on the page.
- */
- getXPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getXPath(this.rawNode);
- },
-
- /**
- * Scroll the selected node into view.
- */
- scrollIntoView: function () {
- this.rawNode.scrollIntoView(true);
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageData: function (maxDim) {
- return imageToImageData(this.rawNode, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Get all event listeners that are listening on this node.
- */
- getEventListenerInfo: function () {
- let node = this.rawNode;
-
- if (this.rawNode.nodeName.toLowerCase() === "html") {
- let winListeners = this.getEventListeners(node.ownerGlobal) || [];
- let docElementListeners = this.getEventListeners(node) || [];
- let docListeners = this.getEventListeners(node.parentNode) || [];
-
- return [...winListeners, ...docElementListeners, ...docListeners];
- }
- return this.getEventListeners(node);
- },
-
- /**
- * Modify a node's attributes. Passed an array of modifications
- * similar in format to "attributes" mutations.
- * {
- * attributeName: <string>
- * attributeNamespace: <optional string>
- * newValue: <optional string> - If null or undefined, the attribute
- * will be removed.
- * }
- *
- * Returns when the modifications have been made. Mutations will
- * be queued for any changes made.
- */
- modifyAttributes: function (modifications) {
- let rawNode = this.rawNode;
- for (let change of modifications) {
- if (change.newValue == null) {
- if (change.attributeNamespace) {
- rawNode.removeAttributeNS(change.attributeNamespace,
- change.attributeName);
- } else {
- rawNode.removeAttribute(change.attributeName);
- }
- } else if (change.attributeNamespace) {
- rawNode.setAttributeNS(change.attributeNamespace, change.attributeName,
- change.newValue);
- } else {
- rawNode.setAttribute(change.attributeName, change.newValue);
- }
- }
- },
-
- /**
- * Given the font and fill style, get the image data of a canvas with the
- * preview text and font.
- * Returns an imageData object with the actual data being a LongStringActor
- * and the width of the text as a string.
- * The image data is transmitted as a base64 encoded png data-uri.
- */
- getFontFamilyDataURL: function (font, fillStyle = "black") {
- let doc = this.rawNode.ownerDocument;
- let options = {
- previewText: FONT_FAMILY_PREVIEW_TEXT,
- previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
- fillStyle: fillStyle
- };
- let { dataURL, size } = getFontPreviewData(font, doc, options);
-
- return { data: LongStringActor(this.conn, dataURL), size: size };
- },
-
- /**
- * Finds the computed background color of the closest parent with
- * a set background color.
- * Returns a string with the background color of the form
- * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
- * background color is found.
- */
- getClosestBackgroundColor: function () {
- let current = this.rawNode;
- while (current) {
- let computedStyle = CssLogic.getComputedStyle(current);
- let currentStyle = computedStyle.getPropertyValue("background-color");
- if (colorUtils.isValidCSSColor(currentStyle)) {
- let currentCssColor = new colorUtils.CssColor(currentStyle);
- if (!currentCssColor.isTransparent()) {
- return currentCssColor.rgba;
- }
- }
- current = current.parentNode;
- }
- return "rgba(255, 255, 255, 1)";
- }
-});
-
-/**
- * Server side of a node list as returned by querySelectorAll()
- */
-var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
- typeName: "domnodelist",
-
- initialize: function (walker, nodeList) {
- protocol.Actor.prototype.initialize.call(this);
- this.walker = walker;
- this.nodeList = nodeList || [];
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- /**
- * Items returned by this actor should belong to the parent walker.
- */
- marshallPool: function () {
- return this.walker;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- length: this.nodeList ? this.nodeList.length : 0
- };
- },
-
- /**
- * Get a single node from the node list.
- */
- item: function (index) {
- return this.walker.attachElement(this.nodeList[index]);
- },
-
- /**
- * Get a range of the items from the node list.
- */
- items: function (start = 0, end = this.nodeList.length) {
- let items = Array.prototype.slice.call(this.nodeList, start, end)
- .map(item => this.walker._ref(item));
- return this.walker.attachElements(items);
- },
-
- release: function () {}
-});
-
-/**
- * Server side of the DOM walker.
- */
-var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
- /**
- * Create the WalkerActor
- * @param DebuggerServerConnection conn
- * The server connection.
- */
- initialize: function (conn, tabActor, options) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
- this.rootWin = tabActor.window;
- this.rootDoc = this.rootWin.document;
- this._refMap = new Map();
- this._pendingMutations = [];
- this._activePseudoClassLocks = new Set();
- this.showAllAnonymousContent = options.showAllAnonymousContent;
-
- this.walkerSearch = new WalkerSearch(this);
-
- // Nodes which have been removed from the client's known
- // ownership tree are considered "orphaned", and stored in
- // this set.
- this._orphaned = new Set();
-
- // The client can tell the walker that it is interested in a node
- // even when it is orphaned with the `retainNode` method. This
- // list contains orphaned nodes that were so retained.
- this._retainedOrphans = new Set();
-
- this.onMutations = this.onMutations.bind(this);
- this.onFrameLoad = this.onFrameLoad.bind(this);
- this.onFrameUnload = this.onFrameUnload.bind(this);
- this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
- MUTATIONS_THROTTLING_DELAY);
-
- tabActor.on("will-navigate", this.onFrameUnload);
- tabActor.on("window-ready", this.onFrameLoad);
-
- // Ensure that the root document node actor is ready and
- // managed.
- this.rootNode = this.document();
-
- this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
- this._onReflows = this._onReflows.bind(this);
- this.layoutChangeObserver.on("reflows", this._onReflows);
- this._onResize = this._onResize.bind(this);
- this.layoutChangeObserver.on("resize", this._onResize);
-
- this._onEventListenerChange = this._onEventListenerChange.bind(this);
- eventListenerService.addListenerChangeListener(this._onEventListenerChange);
- },
-
- /**
- * Callback for eventListenerService.addListenerChangeListener
- * @param nsISimpleEnumerator changesEnum
- * enumerator of nsIEventListenerChange
- */
- _onEventListenerChange: function (changesEnum) {
- let changes = changesEnum.enumerate();
- while (changes.hasMoreElements()) {
- let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
- let target = current.target;
-
- if (this._refMap.has(target)) {
- let actor = this.getNode(target);
- let mutation = {
- type: "events",
- target: actor.actorID,
- hasEventListeners: actor._hasEventListeners
- };
- this.queueMutation(mutation);
- }
- }
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- root: this.rootNode.form(),
- traits: {
- // FF42+ Inspector starts managing the Walker, while the inspector also
- // starts cleaning itself up automatically on client disconnection.
- // So that there is no need to manually release the walker anymore.
- autoReleased: true,
- // XXX: It seems silly that we need to tell the front which capabilities
- // its actor has in this way when the target can use actorHasMethod. If
- // this was ported to the protocol (Bug 1157048) we could call that
- // inside of custom front methods and not need to do traits for this.
- multiFrameQuerySelectorAll: true,
- textSearch: true,
- }
- };
- },
-
- toString: function () {
- return "[WalkerActor " + this.actorID + "]";
- },
-
- getDocumentWalker: function (node, whatToShow, skipTo) {
- // Allow native anon content (like <video> controls) if preffed on
- let nodeFilter = this.showAllAnonymousContent
- ? allAnonymousContentTreeWalkerFilter
- : standardTreeWalkerFilter;
- return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter, skipTo);
- },
-
- destroy: function () {
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
- protocol.Actor.prototype.destroy.call(this);
- try {
- this.clearPseudoClassLocks();
- this._activePseudoClassLocks = null;
-
- this._hoveredNode = null;
- this.rootWin = null;
- this.rootDoc = null;
- this.rootNode = null;
- this.layoutHelpers = null;
- this._orphaned = null;
- this._retainedOrphans = null;
- this._refMap = null;
-
- this.tabActor.off("will-navigate", this.onFrameUnload);
- this.tabActor.off("window-ready", this.onFrameLoad);
-
- this.onFrameLoad = null;
- this.onFrameUnload = null;
-
- this.walkerSearch.destroy();
-
- this.layoutChangeObserver.off("reflows", this._onReflows);
- this.layoutChangeObserver.off("resize", this._onResize);
- this.layoutChangeObserver = null;
- releaseLayoutChangesObserver(this.tabActor);
-
- eventListenerService.removeListenerChangeListener(
- this._onEventListenerChange);
-
- this.onMutations = null;
-
- this.layoutActor = null;
- this.tabActor = null;
-
- this.emit("destroyed");
- } catch (e) {
- console.error(e);
- }
- },
-
- release: function () {},
-
- unmanage: function (actor) {
- if (actor instanceof NodeActor) {
- if (this._activePseudoClassLocks &&
- this._activePseudoClassLocks.has(actor)) {
- this.clearPseudoClassLocks(actor);
- }
- this._refMap.delete(actor.rawNode);
- }
- protocol.Actor.prototype.unmanage.call(this, actor);
- },
-
- /**
- * Determine if the walker has come across this DOM node before.
- * @param {DOMNode} rawNode
- * @return {Boolean}
- */
- hasNode: function (rawNode) {
- return this._refMap.has(rawNode);
- },
-
- /**
- * If the walker has come across this DOM node before, then get the
- * corresponding node actor.
- * @param {DOMNode} rawNode
- * @return {NodeActor}
- */
- getNode: function (rawNode) {
- return this._refMap.get(rawNode);
- },
-
- _ref: function (node) {
- let actor = this.getNode(node);
- if (actor) {
- return actor;
- }
-
- actor = new NodeActor(this, node);
-
- // Add the node actor as a child of this walker actor, assigning
- // it an actorID.
- this.manage(actor);
- this._refMap.set(node, actor);
-
- if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
- actor.watchDocument(this.onMutations);
- }
- return actor;
- },
-
- _onReflows: function (reflows) {
- // Going through the nodes the walker knows about, see which ones have
- // had their display changed and send a display-change event if any
- let changes = [];
- for (let [node, actor] of this._refMap) {
- if (Cu.isDeadWrapper(node)) {
- continue;
- }
-
- let isDisplayed = actor.isDisplayed;
- if (isDisplayed !== actor.wasDisplayed) {
- changes.push(actor);
- // Updating the original value
- actor.wasDisplayed = isDisplayed;
- }
- }
-
- if (changes.length) {
- this.emit("display-change", changes);
- }
- },
-
- /**
- * When the browser window gets resized, relay the event to the front.
- */
- _onResize: function () {
- this.emit("resize");
- },
-
- /**
- * This is kept for backward-compatibility reasons with older remote targets.
- * Targets prior to bug 916443.
- *
- * pick/cancelPick are used to pick a node on click on the content
- * document. But in their implementation prior to bug 916443, they don't allow
- * highlighting on hover.
- * The client-side now uses the highlighter actor's pick and cancelPick
- * methods instead. The client-side uses the the highlightable trait found in
- * the root actor to determine which version of pick to use.
- *
- * As for highlight, the new highlighter actor is used instead of the walker's
- * highlight method. Same here though, the client-side uses the highlightable
- * trait to dertermine which to use.
- *
- * Keeping these actor methods for now allows newer client-side debuggers to
- * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
- */
- pick: function () {},
- cancelPick: function () {},
- highlight: function (node) {},
-
- /**
- * Ensures that the node is attached and it can be accessed from the root.
- *
- * @param {(Node|NodeActor)} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNode type.
- */
- attachElement: function (node) {
- let { nodes, newParents } = this.attachElements([node]);
- return {
- node: nodes[0],
- newParents: newParents
- };
- },
-
- /**
- * Ensures that the nodes are attached and they can be accessed from the root.
- *
- * @param {(Node[]|NodeActor[])} nodes The nodes
- * @return {Object} An object compatible with the disconnectedNodeArray type.
- */
- attachElements: function (nodes) {
- let nodeActors = [];
- let newParents = new Set();
- for (let node of nodes) {
- if (!(node instanceof NodeActor)) {
- // If an anonymous node was passed in and we aren't supposed to know
- // about it, then consult with the document walker as the source of
- // truth about which elements exist.
- if (!this.showAllAnonymousContent && isAnonymous(node)) {
- node = this.getDocumentWalker(node).currentNode;
- }
-
- node = this._ref(node);
- }
-
- this.ensurePathToRoot(node, newParents);
- // If nodes may be an array of raw nodes, we're sure to only have
- // NodeActors with the following array.
- nodeActors.push(node);
- }
-
- return {
- nodes: nodeActors,
- newParents: [...newParents]
- };
- },
-
- /**
- * Return the document node that contains the given node,
- * or the root node if no node is specified.
- * @param NodeActor node
- * The node whose document is needed, or null to
- * return the root.
- */
- document: function (node) {
- let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
- return this._ref(doc);
- },
-
- /**
- * Return the documentElement for the document containing the
- * given node.
- * @param NodeActor node
- * The node whose documentElement is requested, or null
- * to use the root document.
- */
- documentElement: function (node) {
- let elt = isNodeDead(node)
- ? this.rootDoc.documentElement
- : nodeDocument(node.rawNode).documentElement;
- return this._ref(elt);
- },
-
- /**
- * Return all parents of the given node, ordered from immediate parent
- * to root.
- * @param NodeActor node
- * The node whose parents are requested.
- * @param object options
- * Named options, including:
- * `sameDocument`: If true, parents will be restricted to the same
- * document as the node.
- * `sameTypeRootTreeItem`: If true, this will not traverse across
- * different types of docshells.
- */
- parents: function (node, options = {}) {
- if (isNodeDead(node)) {
- return [];
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let parents = [];
- let cur;
- while ((cur = walker.parentNode())) {
- if (options.sameDocument &&
- nodeDocument(cur) != nodeDocument(node.rawNode)) {
- break;
- }
-
- if (options.sameTypeRootTreeItem &&
- nodeDocshell(cur).sameTypeRootTreeItem !=
- nodeDocshell(node.rawNode).sameTypeRootTreeItem) {
- break;
- }
-
- parents.push(this._ref(cur));
- }
- return parents;
- },
-
- parentNode: function (node) {
- let walker = this.getDocumentWalker(node.rawNode);
- let parent = walker.parentNode();
- if (parent) {
- return this._ref(parent);
- }
- return null;
- },
-
- /**
- * If the given NodeActor only has a single text node as a child with a text
- * content small enough to be inlined, return that child's NodeActor.
- *
- * @param NodeActor node
- */
- inlineTextChild: function (node) {
- // Quick checks to prevent creating a new walker if possible.
- if (node.isBeforePseudoElement ||
- node.isAfterPseudoElement ||
- node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
- node.rawNode.children.length > 0) {
- return undefined;
- }
-
- let docWalker = this.getDocumentWalker(node.rawNode);
- let firstChild = docWalker.firstChild();
-
- // Bail out if:
- // - more than one child
- // - unique child is not a text node
- // - unique child is a text node, but is too long to be inlined
- if (!firstChild ||
- docWalker.nextSibling() ||
- firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
- firstChild.nodeValue.length > gValueSummaryLength
- ) {
- return undefined;
- }
-
- return this._ref(firstChild);
- },
-
- /**
- * Mark a node as 'retained'.
- *
- * A retained node is not released when `releaseNode` is called on its
- * parent, or when a parent is released with the `cleanup` option to
- * `getMutations`.
- *
- * When a retained node's parent is released, a retained mode is added to
- * the walker's "retained orphans" list.
- *
- * Retained nodes can be deleted by providing the `force` option to
- * `releaseNode`. They will also be released when their document
- * has been destroyed.
- *
- * Retaining a node makes no promise about its children; They can
- * still be removed by normal means.
- */
- retainNode: function (node) {
- node.retained = true;
- },
-
- /**
- * Remove the 'retained' mark from a node. If the node was a
- * retained orphan, release it.
- */
- unretainNode: function (node) {
- node.retained = false;
- if (this._retainedOrphans.has(node)) {
- this._retainedOrphans.delete(node);
- this.releaseNode(node);
- }
- },
-
- /**
- * Release actors for a node and all child nodes.
- */
- releaseNode: function (node, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- if (node.retained && !options.force) {
- this._retainedOrphans.add(node);
- return;
- }
-
- if (node.retained) {
- // Forcing a retained node to go away.
- this._retainedOrphans.delete(node);
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
-
- let child = walker.firstChild();
- while (child) {
- let childActor = this.getNode(child);
- if (childActor) {
- this.releaseNode(childActor, options);
- }
- child = walker.nextSibling();
- }
-
- node.destroy();
- },
-
- /**
- * Add any nodes between `node` and the walker's root node that have not
- * yet been seen by the client.
- */
- ensurePathToRoot: function (node, newParents = new Set()) {
- if (!node) {
- return newParents;
- }
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let parent = this.getNode(cur);
- if (!parent) {
- // This parent didn't exist, so hasn't been seen by the client yet.
- newParents.add(this._ref(cur));
- } else {
- // This parent did exist, so the client knows about it.
- return newParents;
- }
- }
- return newParents;
- },
-
- /**
- * Return children of the given node. By default this method will return
- * all children of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- children: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- if (options.center && options.start) {
- throw Error("Can't specify both 'center' and 'start' options.");
- }
- let maxNodes = options.maxNodes || -1;
- if (maxNodes == -1) {
- maxNodes = Number.MAX_VALUE;
- }
-
- // We're going to create a few document walkers with the same filter,
- // make it easier.
- let getFilteredWalker = documentWalkerNode => {
- let { whatToShow } = options;
- // Use SKIP_TO_SIBLING to force the walker to use a sibling of the provided node
- // in case this one is incompatible with the walker's filter function.
- return this.getDocumentWalker(documentWalkerNode, whatToShow, SKIP_TO_SIBLING);
- };
-
- // Need to know the first and last child.
- let rawNode = node.rawNode;
- let firstChild = getFilteredWalker(rawNode).firstChild();
- let lastChild = getFilteredWalker(rawNode).lastChild();
-
- if (!firstChild) {
- // No children, we're done.
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let start;
- if (options.center) {
- start = options.center.rawNode;
- } else if (options.start) {
- start = options.start.rawNode;
- } else {
- start = firstChild;
- }
-
- let nodes = [];
-
- // Start by reading backward from the starting point if we're centering...
- let backwardWalker = getFilteredWalker(start);
- if (backwardWalker.currentNode != firstChild && options.center) {
- backwardWalker.previousSibling();
- let backwardCount = Math.floor(maxNodes / 2);
- let backwardNodes = this._readBackward(backwardWalker, backwardCount);
- nodes = backwardNodes;
- }
-
- // Then read forward by any slack left in the max children...
- let forwardWalker = getFilteredWalker(start);
- let forwardCount = maxNodes - nodes.length;
- nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
-
- // If there's any room left, it means we've run all the way to the end.
- // If we're centering, check if there are more items to read at the front.
- let remaining = maxNodes - nodes.length;
- if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
- let firstNodes = this._readBackward(backwardWalker, remaining);
-
- // Then put it all back together.
- nodes = firstNodes.concat(nodes);
- }
-
- return {
- hasFirst: nodes[0].rawNode == firstChild,
- hasLast: nodes[nodes.length - 1].rawNode == lastChild,
- nodes: nodes
- };
- },
-
- /**
- * Return siblings of the given node. By default this method will return
- * all siblings of the node, but there are options that can restrict this
- * to a more manageable subset.
- *
- * If `start` or `center` are not specified, this method will center on the
- * node whose siblings are requested.
- *
- * @param NodeActor node
- * The node whose children you're curious about.
- * @param object options
- * Named options:
- * `maxNodes`: The set of nodes returned by the method will be no longer
- * than maxNodes.
- * `start`: If a node is specified, the list of nodes will start
- * with the given child. Mutally exclusive with `center`.
- * `center`: If a node is specified, the given node will be as centered
- * as possible in the list, given how close to the ends of the child
- * list it is. Mutually exclusive with `start`.
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- *
- * @returns an object with three items:
- * hasFirst: true if the first child of the node is included in the list.
- * hasLast: true if the last child of the node is included in the list.
- * nodes: Child nodes returned by the request.
- */
- siblings: function (node, options = {}) {
- if (isNodeDead(node)) {
- return { hasFirst: true, hasLast: true, nodes: [] };
- }
-
- let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow)
- .parentNode();
- if (!parentNode) {
- return {
- hasFirst: true,
- hasLast: true,
- nodes: [node]
- };
- }
-
- if (!(options.start || options.center)) {
- options.center = node;
- }
-
- return this.children(this._ref(parentNode), options);
- },
-
- /**
- * Get the next sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- nextSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.nextSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Get the previous sibling of a given node. Getting nodes one at a time
- * might be inefficient, be careful.
- *
- * @param object options
- * Named options:
- * `whatToShow`: A bitmask of node types that should be included. See
- * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
- */
- previousSibling: function (node, options = {}) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
- let sibling = walker.previousSibling();
- return sibling ? this._ref(sibling) : null;
- },
-
- /**
- * Helper function for the `children` method: Read forward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readForward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.nextSibling();
- } while (node && --count);
- return ret;
- },
-
- /**
- * Helper function for the `children` method: Read backward in the sibling
- * list into an array with `count` items, including the current node.
- */
- _readBackward: function (walker, count) {
- let ret = [];
-
- let node = walker.currentNode;
- do {
- if (!walker.isSkippedNode(node)) {
- // The walker can be on a node that would be filtered out if it didn't find any
- // other node to fallback to.
- ret.push(this._ref(node));
- }
- node = walker.previousSibling();
- } while (node && --count);
- ret.reverse();
- return ret;
- },
-
- /**
- * Return the first node in the document that matches the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelector: function (baseNode, selector) {
- if (isNodeDead(baseNode)) {
- return {};
- }
-
- let node = baseNode.rawNode.querySelector(selector);
- if (!node) {
- return {};
- }
-
- return this.attachElement(node);
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector.
- * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
- *
- * @param NodeActor baseNode
- * @param string selector
- */
- querySelectorAll: function (baseNode, selector) {
- let nodeList = null;
-
- try {
- nodeList = baseNode.rawNode.querySelectorAll(selector);
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
-
- return new NodeListActor(this, nodeList);
- },
-
- /**
- * Get a list of nodes that match the given selector in all known frames of
- * the current content page.
- * @param {String} selector.
- * @return {Array}
- */
- _multiFrameQuerySelectorAll: function (selector) {
- let nodes = [];
-
- for (let {document} of this.tabActor.windows) {
- try {
- nodes = [...nodes, ...document.querySelectorAll(selector)];
- } catch (e) {
- // Bad selector. Do nothing as the selector can come from a searchbox.
- }
- }
-
- return nodes;
- },
-
- /**
- * Return a NodeListActor with all nodes that match the given selector in all
- * frames of the current content page.
- * @param {String} selector
- */
- multiFrameQuerySelectorAll: function (selector) {
- return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector));
- },
-
- /**
- * Search the document for a given string.
- * Results will be searched with the walker-search module (searches through
- * tag names, attribute names and values, and text contents).
- *
- * @returns {searchresult}
- * - {NodeList} list
- * - {Array<Object>} metadata. Extra information with indices that
- * match up with node list.
- */
- search: function (query) {
- let results = this.walkerSearch.search(query);
- let nodeList = new NodeListActor(this, results.map(r => r.node));
-
- return {
- list: nodeList,
- metadata: []
- };
- },
-
- /**
- * Returns a list of matching results for CSS selector autocompletion.
- *
- * @param string query
- * The selector query being completed
- * @param string completing
- * The exact token being completed out of the query
- * @param string selectorState
- * One of "pseudo", "id", "tag", "class", "null"
- */
- getSuggestionsForQuery: function (query, completing, selectorState) {
- let sugs = {
- classes: new Map(),
- tags: new Map(),
- ids: new Map()
- };
- let result = [];
- let nodes = null;
- // Filtering and sorting the results so that protocol transfer is miminal.
- switch (selectorState) {
- case "pseudo":
- result = PSEUDO_SELECTORS.filter(item => {
- return item[0].startsWith(":" + completing);
- });
- break;
-
- case "class":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[class]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- if (className.startsWith(completing)) {
- result.push(["." + CSS.escape(className), count, selectorState]);
- }
- }
- break;
-
- case "id":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("[id]");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- }
- for (let [id, count] of sugs.ids) {
- if (id.startsWith(completing) && id !== "") {
- result.push(["#" + CSS.escape(id), count, selectorState]);
- }
- }
- break;
-
- case "tag":
- if (!query) {
- nodes = this._multiFrameQuerySelectorAll("*");
- } else {
- nodes = this._multiFrameQuerySelectorAll(query);
- }
- for (let node of nodes) {
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- }
- for (let [tag, count] of sugs.tags) {
- if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
- result.push([tag, count, selectorState]);
- }
- }
-
- // For state 'tag' (no preceding # or .) and when there's no query (i.e.
- // only one word) then search for the matching classes and ids
- if (!query) {
- result = [
- ...result,
- ...this.getSuggestionsForQuery(null, completing, "class")
- .suggestions,
- ...this.getSuggestionsForQuery(null, completing, "id")
- .suggestions
- ];
- }
-
- break;
-
- case "null":
- nodes = this._multiFrameQuerySelectorAll(query);
- for (let node of nodes) {
- sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
- let tag = node.localName;
- sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
- for (let className of node.classList) {
- sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
- }
- }
- for (let [tag, count] of sugs.tags) {
- tag && result.push([tag, count]);
- }
- for (let [id, count] of sugs.ids) {
- id && result.push(["#" + id, count]);
- }
- sugs.classes.delete("");
- sugs.classes.delete(HIDDEN_CLASS);
- for (let [className, count] of sugs.classes) {
- className && result.push(["." + className, count]);
- }
- }
-
- // Sort by count (desc) and name (asc)
- result = result.sort((a, b) => {
- // Computed a sortable string with first the inverted count, then the name
- let sortA = (10000 - a[1]) + a[0];
- let sortB = (10000 - b[1]) + b[0];
-
- // Prefixing ids, classes and tags, to group results
- let firstA = a[0].substring(0, 1);
- let firstB = b[0].substring(0, 1);
-
- if (firstA === "#") {
- sortA = "2" + sortA;
- } else if (firstA === ".") {
- sortA = "1" + sortA;
- } else {
- sortA = "0" + sortA;
- }
-
- if (firstB === "#") {
- sortB = "2" + sortB;
- } else if (firstB === ".") {
- sortB = "1" + sortB;
- } else {
- sortB = "0" + sortB;
- }
-
- // String compare
- return sortA.localeCompare(sortB);
- });
-
- result.slice(0, 25);
-
- return {
- query: query,
- suggestions: result
- };
- },
-
- /**
- * Add a pseudo-class lock to a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be added
- * to parent nodes.
- * `enabled`: False if the pseudo-class should be locked
- * to 'off'. Defaults to true.
- *
- * @returns An empty packet. A "pseudoClassLock" mutation will
- * be queued for any changed nodes.
- */
- addPseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- // There can be only one node locked per pseudo, so dismiss all existing
- // ones
- for (let locked of this._activePseudoClassLocks) {
- if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- let enabled = options.enabled === undefined ||
- options.enabled;
- this._addPseudoClassLock(node, pseudo, enabled);
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._addPseudoClassLock(curNode, pseudo, enabled);
- }
- },
-
- _queuePseudoClassMutation: function (node) {
- this.queueMutation({
- target: node.actorID,
- type: "pseudoClassLock",
- pseudoClassLocks: node.writePseudoClassLocks()
- });
- },
-
- _addPseudoClassLock: function (node, pseudo, enabled) {
- if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
- this._activePseudoClassLocks.add(node);
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- hideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
- node.rawNode.classList.add(HIDDEN_CLASS);
- },
-
- unhideNode: function (node) {
- if (isNodeDead(node)) {
- return;
- }
-
- node.rawNode.classList.remove(HIDDEN_CLASS);
- },
-
- /**
- * Remove a pseudo-class lock from a node.
- *
- * @param NodeActor node
- * @param string pseudo
- * A pseudoclass: ':hover', ':active', ':focus'
- * @param options
- * Options object:
- * `parents`: True if the pseudo-class should be removed
- * from parent nodes.
- *
- * @returns An empty response. "pseudoClassLock" mutations
- * will be emitted for any changed nodes.
- */
- removePseudoClassLock: function (node, pseudo, options = {}) {
- if (isNodeDead(node)) {
- return;
- }
-
- this._removePseudoClassLock(node, pseudo);
-
- // Remove pseudo class for children as we don't want to allow
- // turning it on for some childs without setting it on some parents
- for (let locked of this._activePseudoClassLocks) {
- if (node.rawNode.contains(locked.rawNode) &&
- InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
- this._removePseudoClassLock(locked, pseudo);
- }
- }
-
- if (!options.parents) {
- return;
- }
-
- let walker = this.getDocumentWalker(node.rawNode);
- let cur;
- while ((cur = walker.parentNode())) {
- let curNode = this._ref(cur);
- this._removePseudoClassLock(curNode, pseudo);
- }
- },
-
- _removePseudoClassLock: function (node, pseudo) {
- if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
- return false;
- }
- InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
- if (!node.writePseudoClassLocks()) {
- this._activePseudoClassLocks.delete(node);
- }
-
- this._queuePseudoClassMutation(node);
- return true;
- },
-
- /**
- * Clear all the pseudo-classes on a given node or all nodes.
- * @param {NodeActor} node Optional node to clear pseudo-classes on
- */
- clearPseudoClassLocks: function (node) {
- if (node && isNodeDead(node)) {
- return;
- }
-
- if (node) {
- InspectorUtils.clearPseudoClassLocks(node.rawNode);
- this._activePseudoClassLocks.delete(node);
- this._queuePseudoClassMutation(node);
- } else {
- for (let locked of this._activePseudoClassLocks) {
- InspectorUtils.clearPseudoClassLocks(locked.rawNode);
- this._activePseudoClassLocks.delete(locked);
- this._queuePseudoClassMutation(locked);
- }
- }
- },
-
- /**
- * Get a node's innerHTML property.
- */
- innerHTML: function (node) {
- let html = "";
- if (!isNodeDead(node)) {
- html = node.rawNode.innerHTML;
- }
- return LongStringActor(this.conn, html);
- },
-
- /**
- * Set a node's innerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setInnerHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let rawNode = node.rawNode;
- if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) {
- throw new Error("Can only change innerHTML to element nodes");
- }
- // eslint-disable-next-line no-unsanitized/property
- rawNode.innerHTML = value;
- },
-
- /**
- * Get a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- */
- outerHTML: function (node) {
- let outerHTML = "";
- if (!isNodeDead(node)) {
- outerHTML = node.rawNode.outerHTML;
- }
- return LongStringActor(this.conn, outerHTML);
- },
-
- /**
- * Set a node's outerHTML property.
- *
- * @param {NodeActor} node The node.
- * @param {string} value The piece of HTML content.
- */
- setOuterHTML: function (node, value) {
- if (isNodeDead(node)) {
- return;
- }
-
- let parsedDOM = DOMParser.parseFromString(value, "text/html");
- let rawNode = node.rawNode;
- let parentNode = rawNode.parentNode;
-
- // Special case for head and body. Setting document.body.outerHTML
- // creates an extra <head> tag, and document.head.outerHTML creates
- // an extra <body>. So instead we will call replaceChild with the
- // parsed DOM, assuming that they aren't trying to set both tags at once.
- if (rawNode.tagName === "BODY") {
- if (parsedDOM.head.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.body, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (rawNode.tagName === "HEAD") {
- if (parsedDOM.body.innerHTML === "") {
- parentNode.replaceChild(parsedDOM.head, rawNode);
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- } else if (node.isDocumentElement()) {
- // Unable to set outerHTML on the document element. Fall back by
- // setting attributes manually, then replace the body and head elements.
- let finalAttributeModifications = [];
- let attributeModifications = {};
- for (let attribute of rawNode.attributes) {
- attributeModifications[attribute.name] = null;
- }
- for (let attribute of parsedDOM.documentElement.attributes) {
- attributeModifications[attribute.name] = attribute.value;
- }
- for (let key in attributeModifications) {
- finalAttributeModifications.push({
- attributeName: key,
- newValue: attributeModifications[key]
- });
- }
- node.modifyAttributes(finalAttributeModifications);
- rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
- rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
- } else {
- // eslint-disable-next-line no-unsanitized/property
- rawNode.outerHTML = value;
- }
- },
-
- /**
- * Insert adjacent HTML to a node.
- *
- * @param {Node} node
- * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
- * "afterEnd" (see Element.insertAdjacentHTML).
- * @param {string} value The HTML content.
- */
- insertAdjacentHTML: function (node, position, value) {
- if (isNodeDead(node)) {
- return {node: [], newParents: []};
- }
-
- let rawNode = node.rawNode;
- let isInsertAsSibling = position === "beforeBegin" ||
- position === "afterEnd";
-
- // Don't insert anything adjacent to the document element.
- if (isInsertAsSibling && node.isDocumentElement()) {
- throw new Error("Can't insert adjacent element to the root.");
- }
-
- let rawParentNode = rawNode.parentNode;
- if (!rawParentNode && isInsertAsSibling) {
- throw new Error("Can't insert as sibling without parent node.");
- }
-
- // We can't use insertAdjacentHTML, because we want to return the nodes
- // being created (so the front can remove them if the user undoes
- // the change). So instead, use Range.createContextualFragment().
- let range = rawNode.ownerDocument.createRange();
- if (position === "beforeBegin" || position === "afterEnd") {
- range.selectNode(rawNode);
- } else {
- range.selectNodeContents(rawNode);
- }
- let docFrag = range.createContextualFragment(value);
- let newRawNodes = Array.from(docFrag.childNodes);
- switch (position) {
- case "beforeBegin":
- rawParentNode.insertBefore(docFrag, rawNode);
- break;
- case "afterEnd":
- // Note: if the second argument is null, rawParentNode.insertBefore
- // behaves like rawParentNode.appendChild.
- rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
- break;
- case "afterBegin":
- rawNode.insertBefore(docFrag, rawNode.firstChild);
- break;
- case "beforeEnd":
- rawNode.appendChild(docFrag);
- break;
- default:
- throw new Error("Invalid position value. Must be either " +
- "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'.");
- }
-
- return this.attachElements(newRawNodes);
- },
-
- /**
- * Duplicate a specified node
- *
- * @param {NodeActor} node The node to duplicate.
- */
- duplicateNode: function ({rawNode}) {
- let clonedNode = rawNode.cloneNode(true);
- rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
- },
-
- /**
- * Test whether a node is a document or a document element.
- *
- * @param {NodeActor} node The node to remove.
- * @return {boolean} True if the node is a document or a document element.
- */
- isDocumentOrDocumentElementNode: function (node) {
- return ((node.rawNode.ownerDocument &&
- node.rawNode.ownerDocument.documentElement === this.rawNode) ||
- node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
- },
-
- /**
- * Removes a node from its parent node.
- *
- * @param {NodeActor} node The node to remove.
- * @returns The node's nextSibling before it was removed.
- */
- removeNode: function (node) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes.");
- }
-
- let nextSibling = this.nextSibling(node);
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- return nextSibling;
- },
-
- /**
- * Removes an array of nodes from their parent node.
- *
- * @param {NodeActor[]} nodes The nodes to remove.
- */
- removeNodes: function (nodes) {
- // Check that all nodes are valid before processing the removals.
- for (let node of nodes) {
- if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
- throw Error("Cannot remove document, document elements or dead nodes");
- }
- }
-
- for (let node of nodes) {
- node.rawNode.remove();
- // Mutation events will take care of the rest.
- }
- },
-
- /**
- * Insert a node into the DOM.
- */
- insertBefore: function (node, parent, sibling) {
- if (isNodeDead(node) ||
- isNodeDead(parent) ||
- (sibling && isNodeDead(sibling))) {
- return;
- }
-
- let rawNode = node.rawNode;
- let rawParent = parent.rawNode;
- let rawSibling = sibling ? sibling.rawNode : null;
-
- // Don't bother inserting a node if the document position isn't going
- // to change. This prevents needless iframes reloading and mutations.
- if (rawNode.parentNode === rawParent) {
- let currentNextSibling = this.nextSibling(node);
- currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
- null;
-
- if (rawNode === rawSibling || currentNextSibling === rawSibling) {
- return;
- }
- }
-
- rawParent.insertBefore(rawNode, rawSibling);
- },
-
- /**
- * Editing a node's tagname actually means creating a new node with the same
- * attributes, removing the node and inserting the new one instead.
- * This method does not return anything as mutation events are taking care of
- * informing the consumers about changes.
- */
- editTagName: function (node, tagName) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let oldNode = node.rawNode;
-
- // Create a new element with the same attributes as the current element and
- // prepare to replace the current node with it.
- let newNode;
- try {
- newNode = nodeDocument(oldNode).createElement(tagName);
- } catch (x) {
- // Failed to create a new element with that tag name, ignore the change,
- // and signal the error to the front.
- return Promise.reject(new Error("Could not change node's tagName to " + tagName));
- }
-
- let attrs = oldNode.attributes;
- for (let i = 0; i < attrs.length; i++) {
- newNode.setAttribute(attrs[i].name, attrs[i].value);
- }
-
- // Insert the new node, and transfer the old node's children.
- oldNode.parentNode.insertBefore(newNode, oldNode);
- while (oldNode.firstChild) {
- newNode.appendChild(oldNode.firstChild);
- }
-
- oldNode.remove();
- return null;
- },
-
- /**
- * Get any pending mutation records. Must be called by the client after
- * the `new-mutations` notification is received. Returns an array of
- * mutation records.
- *
- * Mutation records have a basic structure:
- *
- * {
- * type: attributes|characterData|childList,
- * target: <domnode actor ID>,
- * }
- *
- * And additional attributes based on the mutation type:
- *
- * `attributes` type:
- * attributeName: <string> - the attribute that changed
- * attributeNamespace: <string> - the attribute's namespace URI, if any.
- * newValue: <string> - The new value of the attribute, if any.
- *
- * `characterData` type:
- * newValue: <string> - the new nodeValue for the node
- *
- * `childList` type is returned when the set of children for a node
- * has changed. Includes extra data, which can be used by the client to
- * maintain its ownership subtree.
- *
- * added: array of <domnode actor ID> - The list of actors *previously
- * seen by the client* that were added to the target node.
- * removed: array of <domnode actor ID> The list of actors *previously
- * seen by the client* that were removed from the target node.
- * inlineTextChild: If the node now has a single text child, it will
- * be sent here.
- *
- * Actors that are included in a MutationRecord's `removed` but
- * not in an `added` have been removed from the client's ownership
- * tree (either by being moved under a node the client has seen yet
- * or by being removed from the tree entirely), and is considered
- * 'orphaned'.
- *
- * Keep in mind that if a node that the client hasn't seen is moved
- * into or out of the target node, it will not be included in the
- * removedNodes and addedNodes list, so if the client is interested
- * in the new set of children it needs to issue a `children` request.
- */
- getMutations: function (options = {}) {
- let pending = this._pendingMutations || [];
- this._pendingMutations = [];
- this._waitingForGetMutations = false;
-
- if (options.cleanup) {
- for (let node of this._orphaned) {
- // Release the orphaned node. Nodes or children that have been
- // retained will be moved to this._retainedOrphans.
- this.releaseNode(node);
- }
- this._orphaned = new Set();
- }
-
- return pending;
- },
-
- queueMutation: function (mutation) {
- if (!this.actorID || this._destroyed) {
- // We've been destroyed, don't bother queueing this mutation.
- return;
- }
-
- // Add the mutation to the list of mutations to be retrieved next.
- this._pendingMutations.push(mutation);
-
- // Bail out if we already emitted a new-mutations event and are waiting for a client
- // to retrieve them.
- if (this._waitingForGetMutations) {
- return;
- }
-
- if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
- this._emitNewMutations();
- } else {
- /**
- * If many mutations are fired at the same time, clients might sequentially request
- * children/siblings for updated nodes, which can be costly. By throttling the calls
- * to getMutations, duplicated mutations will be ignored.
- */
- this._throttledEmitNewMutations();
- }
- },
-
- _emitNewMutations: function () {
- if (!this.actorID || this._destroyed) {
- // Bail out if the actor was destroyed after throttling this call.
- return;
- }
-
- if (this._waitingForGetMutations || this._pendingMutations.length == 0) {
- // Bail out if we already fired the new-mutation event or if no mutations are
- // waiting to be retrieved.
- return;
- }
-
- this._waitingForGetMutations = true;
- this.emit("new-mutations");
- },
-
- /**
- * Handles mutations from the DOM mutation observer API.
- *
- * @param array[MutationRecord] mutations
- * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
- */
- onMutations: function (mutations) {
- // Notify any observers that want *all* mutations (even on nodes that aren't
- // referenced). This is not sent over the protocol so can only be used by
- // scripts running in the server process.
- this.emit("any-mutation");
-
- for (let change of mutations) {
- let targetActor = this.getNode(change.target);
- if (!targetActor) {
- continue;
- }
- let targetNode = change.target;
- let type = change.type;
- let mutation = {
- type: type,
- target: targetActor.actorID,
- };
-
- if (type === "attributes") {
- mutation.attributeName = change.attributeName;
- mutation.attributeNamespace = change.attributeNamespace || undefined;
- mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
- targetNode.getAttribute(mutation.attributeName)
- : null;
- } else if (type === "characterData") {
- mutation.newValue = targetNode.nodeValue;
- this._maybeQueueInlineTextChildMutation(change, targetNode);
- } else if (type === "childList" || type === "nativeAnonymousChildList") {
- // Get the list of removed and added actors that the client has seen
- // so that it can keep its ownership tree up to date.
- let removedActors = [];
- let addedActors = [];
- for (let removed of change.removedNodes) {
- let removedActor = this.getNode(removed);
- if (!removedActor) {
- // If the client never encountered this actor we don't need to
- // mention that it was removed.
- continue;
- }
- // While removed from the tree, nodes are saved as orphaned.
- this._orphaned.add(removedActor);
- removedActors.push(removedActor.actorID);
- }
- for (let added of change.addedNodes) {
- let addedActor = this.getNode(added);
- if (!addedActor) {
- // If the client never encounted this actor we don't need to tell
- // it about its addition for ownership tree purposes - if the
- // client wants to see the new nodes it can ask for children.
- continue;
- }
- // The actor is reconnected to the ownership tree, unorphan
- // it and let the client know so that its ownership tree is up
- // to date.
- this._orphaned.delete(addedActor);
- addedActors.push(addedActor.actorID);
- }
-
- mutation.numChildren = targetActor.numChildren;
- mutation.removed = removedActors;
- mutation.added = addedActors;
-
- let inlineTextChild = this.inlineTextChild(targetActor);
- if (inlineTextChild) {
- mutation.inlineTextChild = inlineTextChild.form();
- }
- }
- this.queueMutation(mutation);
- }
- },
-
- /**
- * Check if the provided mutation could change the way the target element is
- * inlined with its parent node. If it might, a custom mutation of type
- * "inlineTextChild" will be queued.
- *
- * @param {MutationRecord} mutation
- * A characterData type mutation
- */
- _maybeQueueInlineTextChildMutation: function (mutation) {
- let {oldValue, target} = mutation;
- let newValue = target.nodeValue;
- let limit = gValueSummaryLength;
-
- if ((oldValue.length <= limit && newValue.length <= limit) ||
- (oldValue.length > limit && newValue.length > limit)) {
- // Bail out if the new & old values are both below/above the size limit.
- return;
- }
-
- let parentActor = this.getNode(target.parentNode);
- if (!parentActor || parentActor.rawNode.children.length > 0) {
- // If the parent node has other children, a character data mutation will
- // not change anything regarding inlining text nodes.
- return;
- }
-
- let inlineTextChild = this.inlineTextChild(parentActor);
- this.queueMutation({
- type: "inlineTextChild",
- target: parentActor.actorID,
- inlineTextChild:
- inlineTextChild ? inlineTextChild.form() : undefined
- });
- },
-
- onFrameLoad: function ({ window, isTopLevel }) {
- let { readyState } = window.document;
- if (readyState != "interactive" && readyState != "complete") {
- window.addEventListener("DOMContentLoaded",
- this.onFrameLoad.bind(this, { window, isTopLevel }),
- { once: true });
- return;
- }
- if (isTopLevel) {
- // If we initialize the inspector while the document is loading,
- // we may already have a root document set in the constructor.
- if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) &&
- this.rootDoc.defaultView) {
- this.onFrameUnload({ window: this.rootDoc.defaultView });
- }
- // Update all DOM objects references to target the new document.
- this.rootWin = window;
- this.rootDoc = window.document;
- this.rootNode = this.document();
- this.queueMutation({
- type: "newRoot",
- target: this.rootNode.form()
- });
- return;
- }
- let frame = getFrameElement(window);
- let frameActor = this.getNode(frame);
- if (!frameActor) {
- return;
- }
-
- this.queueMutation({
- type: "frameLoad",
- target: frameActor.actorID,
- });
-
- // Send a childList mutation on the frame.
- this.queueMutation({
- type: "childList",
- target: frameActor.actorID,
- added: [],
- removed: []
- });
- },
-
- // Returns true if domNode is in window or a subframe.
- _childOfWindow: function (window, domNode) {
- let win = nodeDocument(domNode).defaultView;
- while (win) {
- if (win === window) {
- return true;
- }
- win = getFrameElement(win);
- }
- return false;
- },
-
- onFrameUnload: function ({ window }) {
- // Any retained orphans that belong to this document
- // or its children need to be released, and a mutation sent
- // to notify of that.
- let releasedOrphans = [];
-
- for (let retained of this._retainedOrphans) {
- if (Cu.isDeadWrapper(retained.rawNode) ||
- this._childOfWindow(window, retained.rawNode)) {
- this._retainedOrphans.delete(retained);
- releasedOrphans.push(retained.actorID);
- this.releaseNode(retained, { force: true });
- }
- }
-
- if (releasedOrphans.length > 0) {
- this.queueMutation({
- target: this.rootNode.actorID,
- type: "unretained",
- nodes: releasedOrphans
- });
- }
-
- let doc = window.document;
- let documentActor = this.getNode(doc);
- if (!documentActor) {
- return;
- }
-
- if (this.rootDoc === doc) {
- this.rootDoc = null;
- this.rootNode = null;
- }
-
- this.queueMutation({
- type: "documentUnload",
- target: documentActor.actorID
- });
-
- let walker = this.getDocumentWalker(doc);
- let parentNode = walker.parentNode();
- if (parentNode) {
- // Send a childList mutation on the frame so that clients know
- // they should reread the children list.
- this.queueMutation({
- type: "childList",
- target: this.getNode(parentNode).actorID,
- added: [],
- removed: []
- });
- }
-
- // Need to force a release of this node, because those nodes can't
- // be accessed anymore.
- this.releaseNode(documentActor, { force: true });
- },
-
- /**
- * Check if a node is attached to the DOM tree of the current page.
- * @param {nsIDomNode} rawNode
- * @return {Boolean} false if the node is removed from the tree or within a
- * document fragment
- */
- _isInDOMTree: function (rawNode) {
- let walker = this.getDocumentWalker(rawNode);
- let current = walker.currentNode;
-
- // Reaching the top of tree
- while (walker.parentNode()) {
- current = walker.currentNode;
- }
-
- // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
- // attached
- if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
- current !== this.rootDoc) {
- return false;
- }
-
- // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
- return true;
- },
-
- /**
- * @see _isInDomTree
- */
- isInDOMTree: function (node) {
- if (isNodeDead(node)) {
- return false;
- }
- return this._isInDOMTree(node.rawNode);
- },
-
- /**
- * Given an ObjectActor (identified by its ID), commonly used in the debugger,
- * webconsole and variablesView, return the corresponding inspector's
- * NodeActor
- */
- getNodeActorFromObjectActor: function (objectActorID) {
- let actor = this.conn.getActor(objectActorID);
- if (!actor) {
- return null;
- }
-
- let debuggerObject = this.conn.getActor(objectActorID).obj;
- let rawNode = debuggerObject.unsafeDereference();
-
- if (!this._isInDOMTree(rawNode)) {
- return null;
- }
-
- // This is a special case for the document object whereby it is considered
- // as document.documentElement (the <html> node)
- if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
- rawNode = rawNode.documentElement;
- }
-
- return this.attachElement(rawNode);
- },
-
- /**
- * Given a windowID return the NodeActor for the corresponding frameElement,
- * unless it's the root window
- */
- getNodeActorFromWindowID: function (windowID) {
- let win;
-
- try {
- win = Services.wm.getOuterWindowWithId(windowID);
- } catch (e) {
- // ignore
- }
-
- if (!win) {
- return { error: "noWindow",
- message: "The related docshell is destroyed or not found" };
- } else if (!win.frameElement) {
- // the frame element of the root document is privileged & thus
- // inaccessible, so return the document body/element instead
- return this.attachElement(win.document.body || win.document.documentElement);
- }
-
- return this.attachElement(win.frameElement);
- },
-
- /**
- * Given a StyleSheetActor (identified by its ID), commonly used in the
- * style-editor, get its ownerNode and return the corresponding walker's
- * NodeActor.
- * Note that getNodeFromActor was added later and can now be used instead.
- */
- getStyleSheetOwnerNode: function (styleSheetActorID) {
- return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
- },
-
- /**
- * This method can be used to retrieve NodeActor for DOM nodes from other
- * actors in a way that they can later be highlighted in the page, or
- * selected in the inspector.
- * If an actor has a reference to a DOM node, and the UI needs to know about
- * this DOM node (and possibly select it in the inspector), the UI should
- * first retrieve a reference to the walkerFront:
- *
- * // Make sure the inspector/walker have been initialized first.
- * toolbox.initInspector().then(() => {
- * // Retrieve the walker.
- * let walker = toolbox.walker;
- * });
- *
- * And then call this method:
- *
- * // Get the nodeFront from my actor, passing the ID and properties path.
- * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
- * // Use the nodeFront, e.g. select the node in the inspector.
- * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
- * });
- *
- * @param {String} actorID The ID for the actor that has a reference to the
- * DOM node.
- * @param {Array} path Where, on the actor, is the DOM node stored. If in the
- * scope of the actor, the node is available as `this.data.node`, then this
- * should be ["data", "node"].
- * @return {NodeActor} The attached NodeActor, or null if it couldn't be
- * found.
- */
- getNodeFromActor: function (actorID, path) {
- let actor = this.conn.getActor(actorID);
- if (!actor) {
- return null;
- }
-
- let obj = actor;
- for (let name of path) {
- if (!(name in obj)) {
- return null;
- }
- obj = obj[name];
- }
-
- return this.attachElement(obj);
- },
-
- /**
- * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
- * information.
- *
- * @return {LayoutActor}
- */
- getLayoutInspector: function () {
- if (!this.layoutActor) {
- this.layoutActor = new LayoutActor(this.conn, this.tabActor, this);
- }
-
- return this.layoutActor;
- },
-
- /**
- * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
- * returns null.
- */
- getOffsetParent: function (node) {
- if (isNodeDead(node)) {
- return null;
- }
-
- let offsetParent = node.rawNode.offsetParent;
-
- if (!offsetParent) {
- return null;
- }
-
- return this._ref(offsetParent);
- },
-});
-
-/**
- * Server side of the inspector actor, which is used to create
- * inspector-related actors, including the walker.
- */
-exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
- initialize: function (conn, tabActor) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
-
- this._onColorPicked = this._onColorPicked.bind(this);
- this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
- this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- this.destroyEyeDropper();
-
- this._highlighterPromise = null;
- this._pageStylePromise = null;
- this._walkerPromise = null;
- this.walker = null;
- this.tabActor = null;
- },
-
- get window() {
- return this.tabActor.window;
- },
-
- getWalker: function (options = {}) {
- if (this._walkerPromise) {
- return this._walkerPromise;
- }
-
- let deferred = defer();
- this._walkerPromise = deferred.promise;
-
- let window = this.window;
- let domReady = () => {
- let tabActor = this.tabActor;
- window.removeEventListener("DOMContentLoaded", domReady, true);
- this.walker = WalkerActor(this.conn, tabActor, options);
- this.manage(this.walker);
- this.walker.once("destroyed", () => {
- this._walkerPromise = null;
- this._pageStylePromise = null;
- });
- deferred.resolve(this.walker);
- };
-
- if (window.document.readyState === "loading") {
- window.addEventListener("DOMContentLoaded", domReady, true);
- } else {
- domReady();
- }
-
- return this._walkerPromise;
- },
-
- getPageStyle: function () {
- if (this._pageStylePromise) {
- return this._pageStylePromise;
- }
-
- this._pageStylePromise = this.getWalker().then(walker => {
- let pageStyle = PageStyleActor(this);
- this.manage(pageStyle);
- return pageStyle;
- });
- return this._pageStylePromise;
- },
-
- /**
- * The most used highlighter actor is the HighlighterActor which can be
- * conveniently retrieved by this method.
- * The same instance will always be returned by this method when called
- * several times.
- * The highlighter actor returned here is used to highlighter elements's
- * box-models from the markup-view, box model, console, debugger, ... as
- * well as select elements with the pointer (pick).
- *
- * @param {Boolean} autohide Optionally autohide the highlighter after an
- * element has been picked
- * @return {HighlighterActor}
- */
- getHighlighter: function (autohide) {
- if (this._highlighterPromise) {
- return this._highlighterPromise;
- }
-
- this._highlighterPromise = this.getWalker().then(walker => {
- let highlighter = HighlighterActor(this, autohide);
- this.manage(highlighter);
- return highlighter;
- });
- return this._highlighterPromise;
- },
-
- /**
- * If consumers need to display several highlighters at the same time or
- * different types of highlighters, then this method should be used, passing
- * the type name of the highlighter needed as argument.
- * A new instance will be created everytime the method is called, so it's up
- * to the consumer to release it when it is not needed anymore
- *
- * @param {String} type The type of highlighter to create
- * @return {Highlighter} The highlighter actor instance or null if the
- * typeName passed doesn't match any available highlighter
- */
- getHighlighterByType: function (typeName) {
- if (isTypeRegistered(typeName)) {
- return CustomHighlighterActor(this, typeName);
- }
- return null;
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageDataFromURL: function (url, maxDim) {
- let img = new this.window.Image();
- img.src = url;
-
- // imageToImageData waits for the image to load.
- return imageToImageData(img, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Resolve a URL to its absolute form, in the scope of a given content window.
- * @param {String} url.
- * @param {NodeActor} node If provided, the owner window of this node will be
- * used to resolve the URL. Otherwise, the top-level content window will be
- * used instead.
- * @return {String} url.
- */
- resolveRelativeURL: function (url, node) {
- let document = isNodeDead(node)
- ? this.window.document
- : nodeDocument(node.rawNode);
-
- if (!document) {
- return url;
- }
-
- let baseURI = Services.io.newURI(document.location.href);
- return Services.io.newURI(url, null, baseURI).spec;
- },
-
- /**
- * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
- * Note that for now, a new instance is created every time to deal with page navigation.
- */
- createEyeDropper: function () {
- this.destroyEyeDropper();
- this._highlighterEnv = new HighlighterEnvironment();
- this._highlighterEnv.initFromTabActor(this.tabActor);
- this._eyeDropper = new EyeDropper(this._highlighterEnv);
- },
-
- /**
- * Destroy the current eye-dropper highlighter instance.
- */
- destroyEyeDropper: function () {
- if (this._eyeDropper) {
- this.cancelPickColorFromPage();
- this._eyeDropper.destroy();
- this._eyeDropper = null;
- this._highlighterEnv.destroy();
- this._highlighterEnv = null;
- }
- },
-
- /**
- * Pick a color from the page using the eye-dropper. This method doesn't return anything
- * but will cause events to be sent to the front when a color is picked or when the user
- * cancels the picker.
- * @param {Object} options
- */
- pickColorFromPage: function (options) {
- this.createEyeDropper();
- this._eyeDropper.show(this.window.document.documentElement, options);
- this._eyeDropper.once("selected", this._onColorPicked);
- this._eyeDropper.once("canceled", this._onColorPickCanceled);
- this.tabActor.once("will-navigate", this.destroyEyeDropper);
- },
-
- /**
- * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
- * highlighter is for the user to click in the page and select a color. If you need to
- * dismiss the eye-dropper programatically instead, use this method.
- */
- cancelPickColorFromPage: function () {
- if (this._eyeDropper) {
- this._eyeDropper.hide();
- this._eyeDropper.off("selected", this._onColorPicked);
- this._eyeDropper.off("canceled", this._onColorPickCanceled);
- this.tabActor.off("will-navigate", this.destroyEyeDropper);
- }
- },
-
- /**
- * Check if the current document supports highlighters using a canvasFrame anonymous
- * content container (ie all highlighters except the SimpleOutlineHighlighter).
- * It is impossible to detect the feature programmatically as some document types simply
- * don't render the canvasFrame without throwing any error.
- */
- supportsHighlighters: function () {
- let doc = this.tabActor.window.document;
- let ns = doc.documentElement.namespaceURI;
-
- // XUL documents do not support insertAnonymousContent().
- if (ns === XUL_NS) {
- return false;
- }
-
- // SVG documents do not render the canvasFrame (see Bug 1157592).
- if (ns === SVG_NS) {
- return false;
- }
-
- return true;
- },
-
- _onColorPicked: function (e, color) {
- this.emit("color-picked", color);
- },
-
- _onColorPickCanceled: function () {
- this.emit("color-pick-canceled");
- }
-});
-
-// Exported for test purposes.
-exports._documentWalker = DocumentWalker;
function nodeDocument(node) {
if (Cu.isDeadWrapper(node)) {
return null;
}
return node.ownerDocument ||
(node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
}
-function nodeDocshell(node) {
- let doc = node ? nodeDocument(node) : null;
- let win = doc ? doc.defaultView : null;
- if (win) {
- return win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell);
- }
- return null;
-}
-
function isNodeDead(node) {
return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
}
-/**
- * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
- * See inDeepTreeWalker for more information about the methods.
- *
- * @param {DOMNode} node
- * @param {Window} rootWin
- * @param {Number} whatToShow
- * See nodeFilterConstants / inIDeepTreeWalker for options.
- * @param {Function} filter
- * A custom filter function Taking in a DOMNode and returning an Int. See
- * WalkerActor.nodeFilter for an example.
- * @param {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.
- */
-function DocumentWalker(node, rootWin,
- whatToShow = nodeFilterConstants.SHOW_ALL,
- filter = standardTreeWalkerFilter,
- skipTo = SKIP_TO_PARENT) {
- 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 = true;
- 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 () {
- return this.walker.parentNode();
- },
-
- nextNode: function () {
- let 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 () {
- let 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 () {
- let 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.
- let 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 && previous.previousSibling;
- next = 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;
- },
-};
-
function isInXULDocument(el) {
let doc = nodeDocument(el);
return doc &&
doc.documentElement &&
doc.documentElement.namespaceURI === XUL_NS;
}
/**
@@ -3343,17 +187,17 @@ function ensureImageLoaded(image, timeou
* size: {
* naturalWidth: 400,
* naturalHeight: 300,
* resized: true }
* }.
*
* If something goes wrong, the promise is rejected.
*/
-var imageToImageData = Task.async(function* (node, maxDim) {
+const imageToImageData = Task.async(function* (node, maxDim) {
let { HTMLCanvasElement, HTMLImageElement } = node.ownerGlobal;
let isImg = node instanceof HTMLImageElement;
let isCanvas = node instanceof HTMLCanvasElement;
if (!isImg && !isCanvas) {
throw new Error("node is not a <canvas> or <img> element.");
}
@@ -3395,8 +239,17 @@ var imageToImageData = Task.async(functi
data: imageData,
size: {
naturalWidth: imgWidth,
naturalHeight: imgHeight,
resized: resizeRatio !== 1
}
};
});
+
+module.exports = {
+ allAnonymousContentTreeWalkerFilter,
+ getNodeDisplayName,
+ imageToImageData,
+ isNodeDead,
+ nodeDocument,
+ standardTreeWalkerFilter,
+};
copy from devtools/server/actors/inspector/inspector.js
copy to devtools/server/actors/inspector/walker-actor.js
--- a/devtools/server/actors/inspector/inspector.js
+++ b/devtools/server/actors/inspector/walker-actor.js
@@ -1,131 +1,58 @@
/* 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";
-/**
- * Here's the server side of the remote inspector.
- *
- * The WalkerActor is the client's view of the debuggee's DOM. It's gives
- * the client a tree of NodeActor objects.
- *
- * The walker presents the DOM tree mostly unmodified from the source DOM
- * tree, but with a few key differences:
- *
- * - Empty text nodes are ignored. This is pretty typical of developer
- * tools, but maybe we should reconsider that on the server side.
- * - iframes with documents loaded have the loaded document as the child,
- * the walker provides one big tree for the whole document tree.
- *
- * There are a few ways to get references to NodeActors:
- *
- * - When you first get a WalkerActor reference, it comes with a free
- * reference to the root document's node.
- * - Given a node, you can ask for children, siblings, and parents.
- * - You can issue querySelector and querySelectorAll requests to find
- * other elements.
- * - Requests that return arbitrary nodes from the tree (like querySelector
- * and querySelectorAll) will also return any nodes the client hasn't
- * seen in order to have a complete set of parents.
- *
- * Once you have a NodeFront, you should be able to answer a few questions
- * without further round trips, like the node's name, namespace/tagName,
- * attributes, etc. Other questions (like a text node's full nodeValue)
- * might require another round trip.
- *
- * The protocol guarantees that the client will always know the parent of
- * any node that is returned by the server. This means that some requests
- * (like querySelector) will include the extra nodes needed to satisfy this
- * requirement. The client keeps track of this parent relationship, so the
- * node fronts form a tree that is a subset of the actual DOM tree.
- *
- *
- * We maintain this guarantee to support the ability to release subtrees on
- * the client - when a node is disconnected from the DOM tree we want to be
- * able to free the client objects for all the children nodes.
- *
- * So to be able to answer "all the children of a given node that we have
- * seen on the client side", we guarantee that every time we've seen a node,
- * we connect it up through its parents.
- */
+const {Ci, Cu} = require("chrome");
-const {Cc, Ci, Cu} = require("chrome");
const Services = require("Services");
const protocol = require("devtools/shared/protocol");
+const {walkerSpec} = require("devtools/shared/specs/inspector");
const {LongStringActor} = require("devtools/server/actors/string");
-const promise = require("promise");
-const defer = require("devtools/shared/defer");
-const {Task} = require("devtools/shared/task");
-const EventEmitter = require("devtools/shared/event-emitter");
const InspectorUtils = require("InspectorUtils");
-const {walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
-const {nodeSpec, nodeListSpec} = require("devtools/shared/specs/node");
+loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
+loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
-loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
-loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
-loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getCssPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "getXPath", "devtools/shared/inspector/css-logic", true);
-loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
-loader.lazyRequireGetter(this, "EyeDropper", "devtools/server/actors/highlighters/eye-dropper", true);
-loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
-loader.lazyRequireGetter(this, "PageStyleActor", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "getFontPreviewData", "devtools/server/actors/styles", true);
-loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
-loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
+loader.lazyRequireGetter(this, "allAnonymousContentTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
+loader.lazyRequireGetter(this, "isNodeDead", "devtools/server/actors/inspector/utils", true);
+loader.lazyRequireGetter(this, "nodeDocument", "devtools/server/actors/inspector/utils", true);
+loader.lazyRequireGetter(this, "standardTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
+
+loader.lazyRequireGetter(this, "DocumentWalker", "devtools/server/actors/inspector/document-walker", true);
+loader.lazyRequireGetter(this, "SKIP_TO_SIBLING", "devtools/server/actors/inspector/document-walker", true);
+loader.lazyRequireGetter(this, "NodeActor", "devtools/server/actors/inspector/node-actor", true);
+loader.lazyRequireGetter(this, "NodeListActor", "devtools/server/actors/inspector/node-actor", true);
loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
-loader.lazyRequireGetter(this, "HighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "HighlighterEnvironment", "devtools/server/actors/highlighters", true);
-loader.lazyRequireGetter(this, "EventParsers", "devtools/server/event-parsers", true);
-loader.lazyRequireGetter(this, "isAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isNativeAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isXBLAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "isShadowAnonymous", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "getFrameElement", "devtools/shared/layout/utils", true);
-loader.lazyRequireGetter(this, "loadSheet", "devtools/shared/layout/utils", true);
loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
-loader.lazyRequireGetter(this, "nodeFilterConstants", "devtools/shared/dom-node-filter-constants");
-
-loader.lazyServiceGetter(this, "DOMParser",
- "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
+loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
loader.lazyServiceGetter(this, "eventListenerService",
"@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
-const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
-const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
-const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
-const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
-const SVG_NS = "http://www.w3.org/2000/svg";
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const IMAGE_FETCHING_TIMEOUT = 500;
+loader.lazyServiceGetter(this, "DOMParser",
+ "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
// Minimum delay between two "new-mutations" events.
const MUTATIONS_THROTTLING_DELAY = 100;
// List of mutation types that should -not- be throttled.
const IMMEDIATE_MUTATIONS = [
"documentUnload",
"frameLoad",
"newRoot",
"pseudoClassLock",
];
-// 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";
+const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
// The possible completions to a ':' with added score to give certain values
// some preference.
const PSEUDO_SELECTORS = [
[":active", 1],
[":hover", 1],
[":focus", 1],
[":visited", 0],
@@ -149,17 +76,17 @@ const PSEUDO_SELECTORS = [
[":empty", 0],
[":target", 0],
[":enabled", 0],
[":disabled", 0],
[":checked", 1],
["::selection", 0]
];
-var HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
+const HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
.__fx-devtools-hide-shortcut__ {
visibility: hidden !important;
}
:-moz-devtools-highlighted {
outline: 2px dashed #F06!important;
outline-offset: -2px !important;
}
@@ -176,694 +103,16 @@ exports.getValueSummaryLength = function
return gValueSummaryLength;
};
exports.setValueSummaryLength = function (val) {
gValueSummaryLength = val;
};
/**
- * Returns the properly cased version of the node's tag name, which can be
- * used when displaying said name in the UI.
- *
- * @param {Node} rawNode
- * Node for which we want the display name
- * @return {String}
- * Properly cased version of the node tag name
- */
-const getNodeDisplayName = function (rawNode) {
- if (rawNode.nodeName && !rawNode.localName) {
- // The localName & prefix APIs have been moved from the Node interface to the Element
- // interface. Use Node.nodeName as a fallback.
- return rawNode.nodeName;
- }
- return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
-};
-exports.getNodeDisplayName = getNodeDisplayName;
-
-/**
- * Server side of the node actor.
- */
-var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
- initialize: function (walker, node) {
- protocol.Actor.prototype.initialize.call(this, null);
- this.walker = walker;
- this.rawNode = node;
- this._eventParsers = new EventParsers().parsers;
-
- // Storing the original display of the node, to track changes when reflows
- // occur
- this.wasDisplayed = this.isDisplayed;
- },
-
- toString: function () {
- return "[NodeActor " + this.actorID + " for " +
- this.rawNode.toString() + "]";
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- isDocumentElement: function () {
- return this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.documentElement === this.rawNode;
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- if (this.mutationObserver) {
- if (!Cu.isDeadWrapper(this.mutationObserver)) {
- this.mutationObserver.disconnect();
- }
- this.mutationObserver = null;
- }
- this.rawNode = null;
- this.walker = null;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function (detail) {
- if (detail === "actorid") {
- return this.actorID;
- }
-
- let parentNode = this.walker.parentNode(this);
- let inlineTextChild = this.walker.inlineTextChild(this);
-
- let form = {
- actor: this.actorID,
- baseURI: this.rawNode.baseURI,
- parent: parentNode ? parentNode.actorID : undefined,
- nodeType: this.rawNode.nodeType,
- namespaceURI: this.rawNode.namespaceURI,
- nodeName: this.rawNode.nodeName,
- nodeValue: this.rawNode.nodeValue,
- displayName: getNodeDisplayName(this.rawNode),
- numChildren: this.numChildren,
- inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
-
- // doctype attributes
- name: this.rawNode.name,
- publicId: this.rawNode.publicId,
- systemId: this.rawNode.systemId,
-
- attrs: this.writeAttrs(),
- isBeforePseudoElement: this.isBeforePseudoElement,
- isAfterPseudoElement: this.isAfterPseudoElement,
- isAnonymous: isAnonymous(this.rawNode),
- isNativeAnonymous: isNativeAnonymous(this.rawNode),
- isXBLAnonymous: isXBLAnonymous(this.rawNode),
- isShadowAnonymous: isShadowAnonymous(this.rawNode),
- pseudoClassLocks: this.writePseudoClassLocks(),
-
- isDisplayed: this.isDisplayed,
- isInHTMLDocument: this.rawNode.ownerDocument &&
- this.rawNode.ownerDocument.contentType === "text/html",
- hasEventListeners: this._hasEventListeners,
- };
-
- if (this.isDocumentElement()) {
- form.isDocumentElement = true;
- }
-
- // Add an extra API for custom properties added by other
- // modules/extensions.
- form.setFormProperty = (name, value) => {
- if (!form.props) {
- form.props = {};
- }
- form.props[name] = value;
- };
-
- // Fire an event so, other modules can create its own properties
- // that should be passed to the client (within the form.props field).
- EventEmitter.emit(NodeActor, "form", {
- target: this,
- data: form
- });
-
- return form;
- },
-
- /**
- * Watch the given document node for mutations using the DOM observer
- * API.
- */
- watchDocument: function (callback) {
- let node = this.rawNode;
- // Create the observer on the node's actor. The node will make sure
- // the observer is cleaned up when the actor is released.
- let observer = new node.defaultView.MutationObserver(callback);
- observer.mergeAttributeRecords = true;
- observer.observe(node, {
- nativeAnonymousChildList: true,
- attributes: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true
- });
- this.mutationObserver = observer;
- },
-
- get isBeforePseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_before";
- },
-
- get isAfterPseudoElement() {
- return this.rawNode.nodeName === "_moz_generated_content_after";
- },
-
- // Estimate the number of children that the walker will return without making
- // a call to children() if possible.
- get numChildren() {
- // For pseudo elements, childNodes.length returns 1, but the walker
- // will return 0.
- if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
- return 0;
- }
-
- let rawNode = this.rawNode;
- let numChildren = rawNode.childNodes.length;
- let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
- rawNode.ownerDocument.getAnonymousNodes(rawNode);
-
- let hasContentDocument = rawNode.contentDocument;
- let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
- if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
- // This might be an iframe with virtual children.
- numChildren = 1;
- }
-
- // Normal counting misses ::before/::after. Also, some anonymous children
- // may ultimately be skipped, so we have to consult with the walker.
- if (numChildren === 0 || hasAnonChildren) {
- numChildren = this.walker.children(this).nodes.length;
- }
-
- return numChildren;
- },
-
- get computedStyle() {
- return CssLogic.getComputedStyle(this.rawNode);
- },
-
- /**
- * Is the node's display computed style value other than "none"
- */
- get isDisplayed() {
- // Consider all non-element nodes as displayed.
- if (isNodeDead(this) ||
- this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
- this.isAfterPseudoElement ||
- this.isBeforePseudoElement) {
- return true;
- }
-
- let style = this.computedStyle;
- if (!style) {
- return true;
- }
-
- return style.display !== "none";
- },
-
- /**
- * Are there event listeners that are listening on this node? This method
- * uses all parsers registered via event-parsers.js.registerEventParser() to
- * check if there are any event listeners.
- */
- get _hasEventListeners() {
- let parsers = this._eventParsers;
- for (let [, {hasListeners}] of parsers) {
- try {
- if (hasListeners && hasListeners(this.rawNode)) {
- return true;
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
- return false;
- },
-
- writeAttrs: function () {
- if (!this.rawNode.attributes) {
- return undefined;
- }
-
- return [...this.rawNode.attributes].map(attr => {
- return {namespace: attr.namespace, name: attr.name, value: attr.value };
- });
- },
-
- writePseudoClassLocks: function () {
- if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
- return undefined;
- }
- let ret = undefined;
- for (let pseudo of PSEUDO_CLASSES) {
- if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
- ret = ret || [];
- ret.push(pseudo);
- }
- }
- return ret;
- },
-
- /**
- * Gets event listeners and adds their information to the events array.
- *
- * @param {Node} node
- * Node for which we are to get listeners.
- */
- getEventListeners: function (node) {
- let parsers = this._eventParsers;
- let dbg = this.parent().tabActor.makeDebugger();
- let listenerArray = [];
-
- for (let [, {getListeners, normalizeListener}] of parsers) {
- try {
- let listeners = getListeners(node);
-
- if (!listeners) {
- continue;
- }
-
- for (let listener of listeners) {
- if (normalizeListener) {
- listener.normalizeListener = normalizeListener;
- }
-
- this.processHandlerForEvent(node, listenerArray, dbg, listener);
- }
- } catch (e) {
- // An object attached to the node looked like a listener but wasn't...
- // do nothing.
- }
- }
-
- listenerArray.sort((a, b) => {
- return a.type.localeCompare(b.type);
- });
-
- return listenerArray;
- },
-
- /**
- * Process a handler
- *
- * @param {Node} node
- * The node for which we want information.
- * @param {Array} listenerArray
- * listenerArray contains all event objects that we have gathered
- * so far.
- * @param {Debugger} dbg
- * JSDebugger instance.
- * @param {Object} eventInfo
- * See event-parsers.js.registerEventParser() for a description of the
- * eventInfo object.
- *
- * @return {Array}
- * An array of objects where a typical object looks like this:
- * {
- * type: "click",
- * handler: function() { doSomething() },
- * origin: "http://www.mozilla.com",
- * searchString: 'onclick="doSomething()"',
- * tags: tags,
- * DOM0: true,
- * capturing: true,
- * hide: {
- * DOM0: true
- * },
- * native: false
- * }
- */
- processHandlerForEvent: function (node, listenerArray, dbg, listener) {
- let { handler } = listener;
- let global = Cu.getGlobalForObject(handler);
- let globalDO = dbg.addDebuggee(global);
- let listenerDO = globalDO.makeDebuggeeValue(handler);
-
- let { normalizeListener } = listener;
-
- if (normalizeListener) {
- listenerDO = normalizeListener(listenerDO, listener);
- }
-
- let { capturing } = listener;
- let dom0 = false;
- let functionSource = handler.toString();
- let hide = listener.hide || {};
- let line = 0;
- let native = false;
- let override = listener.override || {};
- let tags = listener.tags || "";
- let type = listener.type || "";
- let url = "";
-
- // If the listener is an object with a 'handleEvent' method, use that.
- if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
- let desc;
-
- while (!desc && listenerDO) {
- desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
- listenerDO = listenerDO.proto;
- }
-
- if (desc && desc.value) {
- listenerDO = desc.value;
- }
- }
-
- // If the listener is bound to a different context then we need to switch
- // to the bound function.
- if (listenerDO.isBoundFunction) {
- listenerDO = listenerDO.boundTargetFunction;
- }
-
- let { isArrowFunction, name, script, parameterNames } = listenerDO;
-
- if (script) {
- let scriptSource = script.source.text;
-
- // Scripts are provided via script tags. If it wasn't provided by a
- // script tag it must be a DOM0 event.
- if (script.source.element) {
- dom0 = script.source.element.class !== "HTMLScriptElement";
- } else {
- dom0 = false;
- }
-
- line = script.startLine;
- url = script.url;
-
- // Checking for the string "[native code]" is the only way at this point
- // to check for native code. Even if this provides a false positive then
- // grabbing the source code a second time is harmless.
- if (functionSource === "[object Object]" ||
- functionSource === "[object XULElement]" ||
- functionSource.includes("[native code]")) {
- functionSource =
- scriptSource.substr(script.sourceStart, script.sourceLength);
-
- // At this point the script looks like this:
- // () { ... }
- // We prefix this with "function" if it is not a fat arrow function.
- if (!isArrowFunction) {
- functionSource = "function " + functionSource;
- }
- }
- } else {
- // If the listener is a native one (provided by C++ code) then we have no
- // access to the script. We use the native flag to prevent showing the
- // debugger button because the script is not available.
- native = true;
- }
-
- // Fat arrow function text always contains the parameters. Function
- // parameters are often missing e.g. if Array.sort is used as a handler.
- // If they are missing we provide the parameters ourselves.
- if (parameterNames && parameterNames.length > 0) {
- let prefix = "function " + name + "()";
- let paramString = parameterNames.join(", ");
-
- if (functionSource.startsWith(prefix)) {
- functionSource = functionSource.substr(prefix.length);
-
- functionSource = `function ${name} (${paramString})${functionSource}`;
- }
- }
-
- // If the listener is native code we display the filename "[native code]."
- // This is the official string and should *not* be translated.
- let origin;
- if (native) {
- origin = "[native code]";
- } else {
- origin = url + ((dom0 || line === 0) ? "" : ":" + line);
- }
-
- let eventObj = {
- type: override.type || type,
- handler: override.handler || functionSource.trim(),
- origin: override.origin || origin,
- tags: override.tags || tags,
- DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
- capturing: typeof override.capturing !== "undefined" ?
- override.capturing : capturing,
- hide: typeof override.hide !== "undefined" ? override.hide : hide,
- native
- };
-
- // Hide the debugger icon for DOM0 and native listeners. DOM0 listeners are
- // generated dynamically from e.g. an onclick="" attribute so the script
- // doesn't actually exist.
- if (native || dom0) {
- eventObj.hide.debugger = true;
- }
-
- listenerArray.push(eventObj);
-
- dbg.removeDebuggee(globalDO);
- },
-
- /**
- * Returns a LongStringActor with the node's value.
- */
- getNodeValue: function () {
- return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
- },
-
- /**
- * Set the node's value to a given string.
- */
- setNodeValue: function (value) {
- this.rawNode.nodeValue = value;
- },
-
- /**
- * Get a unique selector string for this node.
- */
- getUniqueSelector: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return findCssSelector(this.rawNode);
- },
-
- /**
- * Get the full CSS path for this node.
- *
- * @return {String} A CSS selector with a part for the node and each of its ancestors.
- */
- getCssPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getCssPath(this.rawNode);
- },
-
- /**
- * Get the XPath for this node.
- *
- * @return {String} The XPath for finding this node on the page.
- */
- getXPath: function () {
- if (Cu.isDeadWrapper(this.rawNode)) {
- return "";
- }
- return getXPath(this.rawNode);
- },
-
- /**
- * Scroll the selected node into view.
- */
- scrollIntoView: function () {
- this.rawNode.scrollIntoView(true);
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageData: function (maxDim) {
- return imageToImageData(this.rawNode, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Get all event listeners that are listening on this node.
- */
- getEventListenerInfo: function () {
- let node = this.rawNode;
-
- if (this.rawNode.nodeName.toLowerCase() === "html") {
- let winListeners = this.getEventListeners(node.ownerGlobal) || [];
- let docElementListeners = this.getEventListeners(node) || [];
- let docListeners = this.getEventListeners(node.parentNode) || [];
-
- return [...winListeners, ...docElementListeners, ...docListeners];
- }
- return this.getEventListeners(node);
- },
-
- /**
- * Modify a node's attributes. Passed an array of modifications
- * similar in format to "attributes" mutations.
- * {
- * attributeName: <string>
- * attributeNamespace: <optional string>
- * newValue: <optional string> - If null or undefined, the attribute
- * will be removed.
- * }
- *
- * Returns when the modifications have been made. Mutations will
- * be queued for any changes made.
- */
- modifyAttributes: function (modifications) {
- let rawNode = this.rawNode;
- for (let change of modifications) {
- if (change.newValue == null) {
- if (change.attributeNamespace) {
- rawNode.removeAttributeNS(change.attributeNamespace,
- change.attributeName);
- } else {
- rawNode.removeAttribute(change.attributeName);
- }
- } else if (change.attributeNamespace) {
- rawNode.setAttributeNS(change.attributeNamespace, change.attributeName,
- change.newValue);
- } else {
- rawNode.setAttribute(change.attributeName, change.newValue);
- }
- }
- },
-
- /**
- * Given the font and fill style, get the image data of a canvas with the
- * preview text and font.
- * Returns an imageData object with the actual data being a LongStringActor
- * and the width of the text as a string.
- * The image data is transmitted as a base64 encoded png data-uri.
- */
- getFontFamilyDataURL: function (font, fillStyle = "black") {
- let doc = this.rawNode.ownerDocument;
- let options = {
- previewText: FONT_FAMILY_PREVIEW_TEXT,
- previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
- fillStyle: fillStyle
- };
- let { dataURL, size } = getFontPreviewData(font, doc, options);
-
- return { data: LongStringActor(this.conn, dataURL), size: size };
- },
-
- /**
- * Finds the computed background color of the closest parent with
- * a set background color.
- * Returns a string with the background color of the form
- * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
- * background color is found.
- */
- getClosestBackgroundColor: function () {
- let current = this.rawNode;
- while (current) {
- let computedStyle = CssLogic.getComputedStyle(current);
- let currentStyle = computedStyle.getPropertyValue("background-color");
- if (colorUtils.isValidCSSColor(currentStyle)) {
- let currentCssColor = new colorUtils.CssColor(currentStyle);
- if (!currentCssColor.isTransparent()) {
- return currentCssColor.rgba;
- }
- }
- current = current.parentNode;
- }
- return "rgba(255, 255, 255, 1)";
- }
-});
-
-/**
- * Server side of a node list as returned by querySelectorAll()
- */
-var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
- typeName: "domnodelist",
-
- initialize: function (walker, nodeList) {
- protocol.Actor.prototype.initialize.call(this);
- this.walker = walker;
- this.nodeList = nodeList || [];
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
- },
-
- /**
- * Instead of storing a connection object, the NodeActor gets its connection
- * from its associated walker.
- */
- get conn() {
- return this.walker.conn;
- },
-
- /**
- * Items returned by this actor should belong to the parent walker.
- */
- marshallPool: function () {
- return this.walker;
- },
-
- // Returns the JSON representation of this object over the wire.
- form: function () {
- return {
- actor: this.actorID,
- length: this.nodeList ? this.nodeList.length : 0
- };
- },
-
- /**
- * Get a single node from the node list.
- */
- item: function (index) {
- return this.walker.attachElement(this.nodeList[index]);
- },
-
- /**
- * Get a range of the items from the node list.
- */
- items: function (start = 0, end = this.nodeList.length) {
- let items = Array.prototype.slice.call(this.nodeList, start, end)
- .map(item => this.walker._ref(item));
- return this.walker.attachElements(items);
- },
-
- release: function () {}
-});
-
-/**
* Server side of the DOM walker.
*/
var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
/**
* Create the WalkerActor
* @param DebuggerServerConnection conn
* The server connection.
*/
@@ -2769,634 +2018,19 @@ var WalkerActor = protocol.ActorClassWit
if (!offsetParent) {
return null;
}
return this._ref(offsetParent);
},
});
-/**
- * Server side of the inspector actor, which is used to create
- * inspector-related actors, including the walker.
- */
-exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
- initialize: function (conn, tabActor) {
- protocol.Actor.prototype.initialize.call(this, conn);
- this.tabActor = tabActor;
-
- this._onColorPicked = this._onColorPicked.bind(this);
- this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
- this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
- },
-
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
-
- this.destroyEyeDropper();
-
- this._highlighterPromise = null;
- this._pageStylePromise = null;
- this._walkerPromise = null;
- this.walker = null;
- this.tabActor = null;
- },
-
- get window() {
- return this.tabActor.window;
- },
-
- getWalker: function (options = {}) {
- if (this._walkerPromise) {
- return this._walkerPromise;
- }
-
- let deferred = defer();
- this._walkerPromise = deferred.promise;
-
- let window = this.window;
- let domReady = () => {
- let tabActor = this.tabActor;
- window.removeEventListener("DOMContentLoaded", domReady, true);
- this.walker = WalkerActor(this.conn, tabActor, options);
- this.manage(this.walker);
- this.walker.once("destroyed", () => {
- this._walkerPromise = null;
- this._pageStylePromise = null;
- });
- deferred.resolve(this.walker);
- };
-
- if (window.document.readyState === "loading") {
- window.addEventListener("DOMContentLoaded", domReady, true);
- } else {
- domReady();
- }
-
- return this._walkerPromise;
- },
-
- getPageStyle: function () {
- if (this._pageStylePromise) {
- return this._pageStylePromise;
- }
-
- this._pageStylePromise = this.getWalker().then(walker => {
- let pageStyle = PageStyleActor(this);
- this.manage(pageStyle);
- return pageStyle;
- });
- return this._pageStylePromise;
- },
-
- /**
- * The most used highlighter actor is the HighlighterActor which can be
- * conveniently retrieved by this method.
- * The same instance will always be returned by this method when called
- * several times.
- * The highlighter actor returned here is used to highlighter elements's
- * box-models from the markup-view, box model, console, debugger, ... as
- * well as select elements with the pointer (pick).
- *
- * @param {Boolean} autohide Optionally autohide the highlighter after an
- * element has been picked
- * @return {HighlighterActor}
- */
- getHighlighter: function (autohide) {
- if (this._highlighterPromise) {
- return this._highlighterPromise;
- }
-
- this._highlighterPromise = this.getWalker().then(walker => {
- let highlighter = HighlighterActor(this, autohide);
- this.manage(highlighter);
- return highlighter;
- });
- return this._highlighterPromise;
- },
-
- /**
- * If consumers need to display several highlighters at the same time or
- * different types of highlighters, then this method should be used, passing
- * the type name of the highlighter needed as argument.
- * A new instance will be created everytime the method is called, so it's up
- * to the consumer to release it when it is not needed anymore
- *
- * @param {String} type The type of highlighter to create
- * @return {Highlighter} The highlighter actor instance or null if the
- * typeName passed doesn't match any available highlighter
- */
- getHighlighterByType: function (typeName) {
- if (isTypeRegistered(typeName)) {
- return CustomHighlighterActor(this, typeName);
- }
- return null;
- },
-
- /**
- * Get the node's image data if any (for canvas and img nodes).
- * Returns an imageData object with the actual data being a LongStringActor
- * and a size json object.
- * The image data is transmitted as a base64 encoded png data-uri.
- * The method rejects if the node isn't an image or if the image is missing
- *
- * Accepts a maxDim request parameter to resize images that are larger. This
- * is important as the resizing occurs server-side so that image-data being
- * transfered in the longstring back to the client will be that much smaller
- */
- getImageDataFromURL: function (url, maxDim) {
- let img = new this.window.Image();
- img.src = url;
-
- // imageToImageData waits for the image to load.
- return imageToImageData(img, maxDim).then(imageData => {
- return {
- data: LongStringActor(this.conn, imageData.data),
- size: imageData.size
- };
- });
- },
-
- /**
- * Resolve a URL to its absolute form, in the scope of a given content window.
- * @param {String} url.
- * @param {NodeActor} node If provided, the owner window of this node will be
- * used to resolve the URL. Otherwise, the top-level content window will be
- * used instead.
- * @return {String} url.
- */
- resolveRelativeURL: function (url, node) {
- let document = isNodeDead(node)
- ? this.window.document
- : nodeDocument(node.rawNode);
-
- if (!document) {
- return url;
- }
-
- let baseURI = Services.io.newURI(document.location.href);
- return Services.io.newURI(url, null, baseURI).spec;
- },
-
- /**
- * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
- * Note that for now, a new instance is created every time to deal with page navigation.
- */
- createEyeDropper: function () {
- this.destroyEyeDropper();
- this._highlighterEnv = new HighlighterEnvironment();
- this._highlighterEnv.initFromTabActor(this.tabActor);
- this._eyeDropper = new EyeDropper(this._highlighterEnv);
- },
-
- /**
- * Destroy the current eye-dropper highlighter instance.
- */
- destroyEyeDropper: function () {
- if (this._eyeDropper) {
- this.cancelPickColorFromPage();
- this._eyeDropper.destroy();
- this._eyeDropper = null;
- this._highlighterEnv.destroy();
- this._highlighterEnv = null;
- }
- },
-
- /**
- * Pick a color from the page using the eye-dropper. This method doesn't return anything
- * but will cause events to be sent to the front when a color is picked or when the user
- * cancels the picker.
- * @param {Object} options
- */
- pickColorFromPage: function (options) {
- this.createEyeDropper();
- this._eyeDropper.show(this.window.document.documentElement, options);
- this._eyeDropper.once("selected", this._onColorPicked);
- this._eyeDropper.once("canceled", this._onColorPickCanceled);
- this.tabActor.once("will-navigate", this.destroyEyeDropper);
- },
-
- /**
- * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
- * highlighter is for the user to click in the page and select a color. If you need to
- * dismiss the eye-dropper programatically instead, use this method.
- */
- cancelPickColorFromPage: function () {
- if (this._eyeDropper) {
- this._eyeDropper.hide();
- this._eyeDropper.off("selected", this._onColorPicked);
- this._eyeDropper.off("canceled", this._onColorPickCanceled);
- this.tabActor.off("will-navigate", this.destroyEyeDropper);
- }
- },
-
- /**
- * Check if the current document supports highlighters using a canvasFrame anonymous
- * content container (ie all highlighters except the SimpleOutlineHighlighter).
- * It is impossible to detect the feature programmatically as some document types simply
- * don't render the canvasFrame without throwing any error.
- */
- supportsHighlighters: function () {
- let doc = this.tabActor.window.document;
- let ns = doc.documentElement.namespaceURI;
-
- // XUL documents do not support insertAnonymousContent().
- if (ns === XUL_NS) {
- return false;
- }
-
- // SVG documents do not render the canvasFrame (see Bug 1157592).
- if (ns === SVG_NS) {
- return false;
- }
-
- return true;
- },
-
- _onColorPicked: function (e, color) {
- this.emit("color-picked", color);
- },
-
- _onColorPickCanceled: function () {
- this.emit("color-pick-canceled");
- }
-});
-
-// Exported for test purposes.
-exports._documentWalker = DocumentWalker;
-
-function nodeDocument(node) {
- if (Cu.isDeadWrapper(node)) {
- return null;
- }
- return node.ownerDocument ||
- (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
-}
-
function nodeDocshell(node) {
let doc = node ? nodeDocument(node) : null;
let win = doc ? doc.defaultView : null;
if (win) {
return win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
}
return null;
}
-function isNodeDead(node) {
- return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
-}
-
-/**
- * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
- * See inDeepTreeWalker for more information about the methods.
- *
- * @param {DOMNode} node
- * @param {Window} rootWin
- * @param {Number} whatToShow
- * See nodeFilterConstants / inIDeepTreeWalker for options.
- * @param {Function} filter
- * A custom filter function Taking in a DOMNode and returning an Int. See
- * WalkerActor.nodeFilter for an example.
- * @param {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.
- */
-function DocumentWalker(node, rootWin,
- whatToShow = nodeFilterConstants.SHOW_ALL,
- filter = standardTreeWalkerFilter,
- skipTo = SKIP_TO_PARENT) {
- 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 = true;
- 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 () {
- return this.walker.parentNode();
- },
-
- nextNode: function () {
- let 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 () {
- let 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 () {
- let 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.
- let 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 && previous.previousSibling;
- next = 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;
- },
-};
-
-function isInXULDocument(el) {
- let doc = nodeDocument(el);
- return doc &&
- doc.documentElement &&
- doc.documentElement.namespaceURI === XUL_NS;
-}
-
-/**
- * This DeepTreeWalker filter skips whitespace text nodes and anonymous
- * content with the exception of ::before and ::after and anonymous content
- * in XUL document (needed to show all elements in the browser toolbox).
- */
-function standardTreeWalkerFilter(node) {
- // ::before and ::after are native anonymous content, but we always
- // want to show them
- if (node.nodeName === "_moz_generated_content_before" ||
- node.nodeName === "_moz_generated_content_after") {
- return nodeFilterConstants.FILTER_ACCEPT;
- }
-
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
-
- // Ignore all native and XBL anonymous content inside a non-XUL document.
- // We need to do this to skip things like form controls, scrollbars,
- // video controls, etc (see bug 1187482).
- if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
- isNativeAnonymous(node))) {
- return nodeFilterConstants.FILTER_SKIP;
- }
-
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
- * it also includes all anonymous content (like internal form controls).
- */
-function allAnonymousContentTreeWalkerFilter(node) {
- // Ignore empty whitespace text nodes that do not impact the layout.
- if (isWhitespaceTextNode(node)) {
- return nodeHasSize(node)
- ? nodeFilterConstants.FILTER_ACCEPT
- : nodeFilterConstants.FILTER_SKIP;
- }
- return nodeFilterConstants.FILTER_ACCEPT;
-}
-
-/**
- * Is the given node a text node composed of whitespace only?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function isWhitespaceTextNode(node) {
- return node.nodeType == Ci.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
-}
-
-/**
- * Does the given node have non-0 width and height?
- * @param {DOMNode} node
- * @return {Boolean}
- */
-function nodeHasSize(node) {
- if (!node.getBoxQuads) {
- return false;
- }
-
- let quads = node.getBoxQuads();
- return quads.length && quads.some(quad => quad.bounds.width && quad.bounds.height);
-}
-
-/**
- * Returns a promise that is settled once the given HTMLImageElement has
- * finished loading.
- *
- * @param {HTMLImageElement} image - The image element.
- * @param {Number} timeout - Maximum amount of time the image is allowed to load
- * before the waiting is aborted. Ignored if flags.testing is set.
- *
- * @return {Promise} that is fulfilled once the image has loaded. If the image
- * fails to load or the load takes too long, the promise is rejected.
- */
-function ensureImageLoaded(image, timeout) {
- let { HTMLImageElement } = image.ownerGlobal;
- if (!(image instanceof HTMLImageElement)) {
- return promise.reject("image must be an HTMLImageELement");
- }
-
- if (image.complete) {
- // The image has already finished loading.
- return promise.resolve();
- }
-
- // This image is still loading.
- let onLoad = AsyncUtils.listenOnce(image, "load");
-
- // Reject if loading fails.
- let onError = AsyncUtils.listenOnce(image, "error").then(() => {
- return promise.reject("Image '" + image.src + "' failed to load.");
- });
-
- // Don't timeout when testing. This is never settled.
- let onAbort = new Promise(() => {});
-
- if (!flags.testing) {
- // Tests are not running. Reject the promise after given timeout.
- onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
- return promise.reject("Image '" + image.src + "' took too long to load.");
- });
- }
-
- // See which happens first.
- return promise.race([onLoad, onError, onAbort]);
-}
-
-/**
- * Given an <img> or <canvas> element, return the image data-uri. If @param node
- * is an <img> element, the method waits a while for the image to load before
- * the data is generated. If the image does not finish loading in a reasonable
- * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
- *
- * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
- * element, or Image() object. Other types cause the method to reject.
- * @param {Number} maxDim - Optionally pass a maximum size you want the longest
- * side of the image to be resized to before getting the image data.
-
- * @return {Promise} A promise that is fulfilled with an object containing the
- * data-uri and size-related information:
- * { data: "...",
- * size: {
- * naturalWidth: 400,
- * naturalHeight: 300,
- * resized: true }
- * }.
- *
- * If something goes wrong, the promise is rejected.
- */
-var imageToImageData = Task.async(function* (node, maxDim) {
- let { HTMLCanvasElement, HTMLImageElement } = node.ownerGlobal;
-
- let isImg = node instanceof HTMLImageElement;
- let isCanvas = node instanceof HTMLCanvasElement;
-
- if (!isImg && !isCanvas) {
- throw new Error("node is not a <canvas> or <img> element.");
- }
-
- if (isImg) {
- // Ensure that the image is ready.
- yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
- }
-
- // Get the image resize ratio if a maxDim was provided
- let resizeRatio = 1;
- let imgWidth = node.naturalWidth || node.width;
- let imgHeight = node.naturalHeight || node.height;
- let imgMax = Math.max(imgWidth, imgHeight);
- if (maxDim && imgMax > maxDim) {
- resizeRatio = maxDim / imgMax;
- }
-
- // Extract the image data
- let imageData;
- // The image may already be a data-uri, in which case, save ourselves the
- // trouble of converting via the canvas.drawImage.toDataURL method, but only
- // if the image doesn't need resizing
- if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
- imageData = node.src;
- } else {
- // Create a canvas to copy the rawNode into and get the imageData from
- let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
- canvas.width = imgWidth * resizeRatio;
- canvas.height = imgHeight * resizeRatio;
- let ctx = canvas.getContext("2d");
-
- // Copy the rawNode image or canvas in the new canvas and extract data
- ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
- imageData = canvas.toDataURL("image/png");
- }
-
- return {
- data: imageData,
- size: {
- naturalWidth: imgWidth,
- naturalHeight: imgHeight,
- resized: resizeRatio !== 1
- }
- };
-});
+exports.WalkerActor = WalkerActor;
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -33,17 +33,16 @@ DevToolsModules(
'errordocs.js',
'eventlooplag.js',
'frame.js',
'framerate.js',
'gcli.js',
'heap-snapshot-file.js',
'highlighters.css',
'highlighters.js',
- 'inspector.js',
'layout.js',
'memory.js',
'monitor.js',
'object.js',
'perf.js',
'performance-recording.js',
'performance.js',
'preference.js',
--- a/devtools/server/actors/webextension-inspected-window.js
+++ b/devtools/server/actors/webextension-inspected-window.js
@@ -6,17 +6,17 @@
const protocol = require("devtools/shared/protocol");
const {Ci, Cu, Cr} = require("chrome");
const {DebuggerServer} = require("devtools/server/main");
const Services = require("Services");
-loader.lazyGetter(this, "NodeActor", () => require("devtools/server/actors/inspector/inspector").NodeActor, true);
+loader.lazyGetter(this, "NodeActor", () => require("devtools/server/actors/inspector/node-actor").NodeActor, true);
const {
XPCOMUtils,
} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
const {
webExtensionInspectedWindowSpec,
} = require("devtools/shared/specs/webextension-inspected-window");
--- a/devtools/server/tests/mochitest/inspector-helpers.js
+++ b/devtools/server/tests/mochitest/inspector-helpers.js
@@ -10,17 +10,17 @@ const {require} = Cu.import("resource://
const {DebuggerClient} = require("devtools/shared/client/debugger-client");
const {DebuggerServer} = require("devtools/server/main");
const { Task } = require("devtools/shared/task");
const Services = require("Services");
// promise is still used in tests using this helper
const promise = require("promise"); // eslint-disable-line no-unused-vars
const defer = require("devtools/shared/defer");
-const {_documentWalker} = require("devtools/server/actors/inspector/inspector");
+const {DocumentWalker: _documentWalker} = require("devtools/server/actors/inspector/document-walker");
// Always log packets when running tests.
Services.prefs.setBoolPref("devtools.debugger.log", true);
SimpleTest.registerCleanupFunction(function () {
Services.prefs.clearUserPref("devtools.debugger.log");
});
if (!DebuggerServer.initialized) {
--- a/devtools/server/tests/mochitest/test_inspector-anonymous.html
+++ b/devtools/server/tests/mochitest/test_inspector-anonymous.html
@@ -11,18 +11,19 @@ https://bugzilla.mozilla.org/show_bug.cg
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript" src="inspector-helpers.js"></script>
<script type="application/javascript">
"use strict";
window.onload = function () {
const {InspectorFront} =
require("devtools/shared/fronts/inspector");
- const {_documentWalker} =
- require("devtools/server/actors/inspector/inspector");
+ const {DocumentWalker: _documentWalker} =
+ require("devtools/server/actors/inspector/document-walker");
+
const nodeFilterConstants =
require("devtools/shared/dom-node-filter-constants");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const isStylo = SpecialPowers.DOMWindowUtils.isStyledByServo;
SpecialPowers.pushPrefEnv({"set": [
["dom.webcomponents.shadowdom.enabled", true]
--- a/devtools/server/tests/mochitest/test_inspector-insert.html
+++ b/devtools/server/tests/mochitest/test_inspector-insert.html
@@ -8,17 +8,17 @@ https://bugzilla.mozilla.org/show_bug.cg
<title>Test for Bug </title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript" src="inspector-helpers.js"></script>
<script type="application/javascript">
"use strict";
-const inspector = require("devtools/server/actors/inspector/inspector");
+const {DocumentWalker} = require("devtools/server/actors/inspector/document-walker");
window.onload = function () {
SimpleTest.waitForExplicitFinish();
runNextTest();
};
let gWalker = null;
let gInspectee = null;
@@ -49,17 +49,17 @@ addAsyncTest(function* testRearrange() {
children = yield gWalker.children(longlist);
is(nodeA, children.nodes[children.nodes.length - 1],
"a should now be the last returned child.");
// Now move it to the middle of the list.
let nextNode = children.nodes[13];
yield gWalker.insertBefore(nodeA, longlist, nextNode);
let sibling =
- new inspector._documentWalker(gInspectee.querySelector("#a"), window).nextSibling();
+ new DocumentWalker(gInspectee.querySelector("#a"), window).nextSibling();
is(sibling, nextNode.rawNode(), "Node should match the expected next node.");
children = yield gWalker.children(longlist);
is(nodeA, children.nodes[13], "a should be where we expect it.");
is(nextNode, children.nodes[14], "next node should be where we expect it.");
runNextTest();
});
--- a/devtools/server/tests/mochitest/test_inspector-mutations-value.html
+++ b/devtools/server/tests/mochitest/test_inspector-mutations-value.html
@@ -8,27 +8,27 @@ https://bugzilla.mozilla.org/show_bug.cg
<title>Test for Bug </title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript" src="inspector-helpers.js"></script>
<script type="application/javascript">
"use strict";
-const inspector = require("devtools/server/actors/inspector/inspector");
+const WalkerActor = require("devtools/server/actors/inspector/walker-actor");
window.onload = function () {
SimpleTest.waitForExplicitFinish();
runNextTest();
};
const testSummaryLength = 10;
-inspector.setValueSummaryLength(testSummaryLength);
+WalkerActor.setValueSummaryLength(testSummaryLength);
SimpleTest.registerCleanupFunction(function () {
- inspector.setValueSummaryLength(inspector.DEFAULT_VALUE_SUMMARY_LENGTH);
+ WalkerActor.setValueSummaryLength(WalkerActor.DEFAULT_VALUE_SUMMARY_LENGTH);
});
let gInspectee = null;
let gWalker = null;
let valueNode;
var valueFront;
var longStringFront;
var longString = "stringstringstringstringstringstringstringstringstringstringstring";
--- a/devtools/server/tests/mochitest/test_inspector-traversal.html
+++ b/devtools/server/tests/mochitest/test_inspector-traversal.html
@@ -279,21 +279,21 @@ addTest(function testFrameTraversal() {
let expected = expectedParents.shift();
is(parent.nodeName, expected, "Got expected parent");
}
}).then(runNextTest));
});
addTest(function testLongValue() {
const testSummaryLength = 10;
- const inspector = require("devtools/server/actors/inspector/inspector");
+ const WalkerActor = require("devtools/server/actors/inspector/walker-actor");
- inspector.setValueSummaryLength(testSummaryLength);
+ WalkerActor.setValueSummaryLength(testSummaryLength);
SimpleTest.registerCleanupFunction(function () {
- inspector.setValueSummaryLength(inspector.DEFAULT_VALUE_SUMMARY_LENGTH);
+ WalkerActor.setValueSummaryLength(WalkerActor.DEFAULT_VALUE_SUMMARY_LENGTH);
});
let longstringText = gInspectee.getElementById("longstring").firstChild.nodeValue;
promiseDone(gWalker.querySelector(gWalker.rootNode, "#longstring").then(node => {
ok(!node.inlineTextChild, "Text is too long to be inlined");
// Now we need to get the text node child...
return gWalker.children(node, { maxNodes: 1 });
--- a/devtools/server/tests/unit/test_nodelistactor.js
+++ b/devtools/server/tests/unit/test_nodelistactor.js
@@ -1,17 +1,17 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that a NodeListActor initialized with null nodelist doesn't cause
// exceptions when calling NodeListActor.form.
-const { NodeListActor } = require("devtools/server/actors/inspector/inspector");
+const { NodeListActor } = require("devtools/server/actors/inspector/node-actor");
function run_test() {
check_actor_for_list(null);
check_actor_for_list([]);
check_actor_for_list(["fakenode"]);
}
function check_actor_for_list(nodelist) {