summaryrefslogtreecommitdiffstats
path: root/devtools/shared/fronts/inspector.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/fronts/inspector.js')
-rw-r--r--devtools/shared/fronts/inspector.js1007
1 files changed, 1007 insertions, 0 deletions
diff --git a/devtools/shared/fronts/inspector.js b/devtools/shared/fronts/inspector.js
new file mode 100644
index 000000000..c76b41fe7
--- /dev/null
+++ b/devtools/shared/fronts/inspector.js
@@ -0,0 +1,1007 @@
+/* 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";
+
+require("devtools/shared/fronts/styles");
+require("devtools/shared/fronts/highlighters");
+require("devtools/shared/fronts/layout");
+const { SimpleStringFront } = require("devtools/shared/fronts/string");
+const {
+ Front,
+ FrontClassWithSpec,
+ custom,
+ preEvent,
+ types
+} = require("devtools/shared/protocol.js");
+const {
+ inspectorSpec,
+ nodeSpec,
+ nodeListSpec,
+ walkerSpec
+} = require("devtools/shared/specs/inspector");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const { Task } = require("devtools/shared/task");
+const { Class } = require("sdk/core/heritage");
+const events = require("sdk/event/core");
+const object = require("sdk/util/object");
+const nodeConstants = require("devtools/shared/dom-node-constants.js");
+loader.lazyRequireGetter(this, "CommandUtils",
+ "devtools/client/shared/developer-toolbar", true);
+
+const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyAttributes` request.
+ */
+const AttributeModificationList = Class({
+ initialize: function (node) {
+ this.node = node;
+ this.modifications = [];
+ },
+
+ apply: function () {
+ let ret = this.node.modifyAttributes(this.modifications);
+ return ret;
+ },
+
+ destroy: function () {
+ this.node = null;
+ this.modification = null;
+ },
+
+ setAttributeNS: function (ns, name, value) {
+ this.modifications.push({
+ attributeNamespace: ns,
+ attributeName: name,
+ newValue: value
+ });
+ },
+
+ setAttribute: function (name, value) {
+ this.setAttributeNS(undefined, name, value);
+ },
+
+ removeAttributeNS: function (ns, name) {
+ this.setAttributeNS(ns, name, undefined);
+ },
+
+ removeAttribute: function (name) {
+ this.setAttributeNS(undefined, name, undefined);
+ }
+});
+
+/**
+ * Client side of the node actor.
+ *
+ * Node fronts are strored in a tree that mirrors the DOM tree on the
+ * server, but with a few key differences:
+ * - Not all children will be necessary loaded for each node.
+ * - The order of children isn't guaranteed to be the same as the DOM.
+ * Children are stored in a doubly-linked list, to make addition/removal
+ * and traversal quick.
+ *
+ * Due to the order/incompleteness of the child list, it is safe to use
+ * the parent node from clients, but the `children` request should be used
+ * to traverse children.
+ */
+const NodeFront = FrontClassWithSpec(nodeSpec, {
+ initialize: function (conn, form, detail, ctx) {
+ // The parent node
+ this._parent = null;
+ // The first child of this node.
+ this._child = null;
+ // The next sibling of this node.
+ this._next = null;
+ // The previous sibling of this node.
+ this._prev = null;
+ Front.prototype.initialize.call(this, conn, form, detail, ctx);
+ },
+
+ /**
+ * Destroy a node front. The node must have been removed from the
+ * ownership tree before this is called, unless the whole walker front
+ * is being destroyed.
+ */
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ // Update the object given a form representation off the wire.
+ form: function (form, detail, ctx) {
+ if (detail === "actorid") {
+ this.actorID = form;
+ return;
+ }
+
+ // backward-compatibility: shortValue indicates we are connected to old server
+ if (form.shortValue) {
+ // If the value is not complete, set nodeValue to null, it will be fetched
+ // when calling getNodeValue()
+ form.nodeValue = form.incompleteValue ? null : form.shortValue;
+ }
+
+ // Shallow copy of the form. We could just store a reference, but
+ // eventually we'll want to update some of the data.
+ this._form = object.merge(form);
+ this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
+
+ if (form.parent) {
+ // Get the owner actor for this actor (the walker), and find the
+ // parent node of this actor from it, creating a standin node if
+ // necessary.
+ let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
+ this.reparent(parentNodeFront);
+ }
+
+ if (form.inlineTextChild) {
+ this.inlineTextChild =
+ types.getType("domnode").read(form.inlineTextChild, ctx);
+ } else {
+ this.inlineTextChild = undefined;
+ }
+ },
+
+ /**
+ * Returns the parent NodeFront for this NodeFront.
+ */
+ parentNode: function () {
+ return this._parent;
+ },
+
+ /**
+ * Process a mutation entry as returned from the walker's `getMutations`
+ * request. Only tries to handle changes of the node's contents
+ * themselves (character data and attribute changes), the walker itself
+ * will keep the ownership tree up to date.
+ */
+ updateMutation: function (change) {
+ if (change.type === "attributes") {
+ // We'll need to lazily reparse the attributes after this change.
+ this._attrMap = undefined;
+
+ // Update any already-existing attributes.
+ let found = false;
+ for (let i = 0; i < this.attributes.length; i++) {
+ let attr = this.attributes[i];
+ if (attr.name == change.attributeName &&
+ attr.namespace == change.attributeNamespace) {
+ if (change.newValue !== null) {
+ attr.value = change.newValue;
+ } else {
+ this.attributes.splice(i, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+ // This is a new attribute. The null check is because of Bug 1192270,
+ // in the case of a newly added then removed attribute
+ if (!found && change.newValue !== null) {
+ this.attributes.push({
+ name: change.attributeName,
+ namespace: change.attributeNamespace,
+ value: change.newValue
+ });
+ }
+ } else if (change.type === "characterData") {
+ this._form.nodeValue = change.newValue;
+ } else if (change.type === "pseudoClassLock") {
+ this._form.pseudoClassLocks = change.pseudoClassLocks;
+ } else if (change.type === "events") {
+ this._form.hasEventListeners = change.hasEventListeners;
+ }
+ },
+
+ // Some accessors to make NodeFront feel more like an nsIDOMNode
+
+ get id() {
+ return this.getAttribute("id");
+ },
+
+ get nodeType() {
+ return this._form.nodeType;
+ },
+ get namespaceURI() {
+ return this._form.namespaceURI;
+ },
+ get nodeName() {
+ return this._form.nodeName;
+ },
+ get displayName() {
+ let {displayName, nodeName} = this._form;
+
+ // Keep `nodeName.toLowerCase()` for backward compatibility
+ return displayName || nodeName.toLowerCase();
+ },
+ get doctypeString() {
+ return "<!DOCTYPE " + this._form.name +
+ (this._form.publicId ? " PUBLIC \"" + this._form.publicId + "\"" : "") +
+ (this._form.systemId ? " \"" + this._form.systemId + "\"" : "") +
+ ">";
+ },
+
+ get baseURI() {
+ return this._form.baseURI;
+ },
+
+ get className() {
+ return this.getAttribute("class") || "";
+ },
+
+ get hasChildren() {
+ return this._form.numChildren > 0;
+ },
+ get numChildren() {
+ return this._form.numChildren;
+ },
+ get hasEventListeners() {
+ return this._form.hasEventListeners;
+ },
+
+ get isBeforePseudoElement() {
+ return this._form.isBeforePseudoElement;
+ },
+ get isAfterPseudoElement() {
+ return this._form.isAfterPseudoElement;
+ },
+ get isPseudoElement() {
+ return this.isBeforePseudoElement || this.isAfterPseudoElement;
+ },
+ get isAnonymous() {
+ return this._form.isAnonymous;
+ },
+ get isInHTMLDocument() {
+ return this._form.isInHTMLDocument;
+ },
+ get tagName() {
+ return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
+ },
+
+ get isDocumentElement() {
+ return !!this._form.isDocumentElement;
+ },
+
+ // doctype properties
+ get name() {
+ return this._form.name;
+ },
+ get publicId() {
+ return this._form.publicId;
+ },
+ get systemId() {
+ return this._form.systemId;
+ },
+
+ getAttribute: function (name) {
+ let attr = this._getAttribute(name);
+ return attr ? attr.value : null;
+ },
+ hasAttribute: function (name) {
+ this._cacheAttributes();
+ return (name in this._attrMap);
+ },
+
+ get hidden() {
+ let cls = this.getAttribute("class");
+ return cls && cls.indexOf(HIDDEN_CLASS) > -1;
+ },
+
+ get attributes() {
+ return this._form.attrs;
+ },
+
+ get pseudoClassLocks() {
+ return this._form.pseudoClassLocks || [];
+ },
+ hasPseudoClassLock: function (pseudo) {
+ return this.pseudoClassLocks.some(locked => locked === pseudo);
+ },
+
+ get isDisplayed() {
+ // The NodeActor's form contains the isDisplayed information as a boolean
+ // starting from FF32. Before that, the property is missing
+ return "isDisplayed" in this._form ? this._form.isDisplayed : true;
+ },
+
+ get isTreeDisplayed() {
+ let parent = this;
+ while (parent) {
+ if (!parent.isDisplayed) {
+ return false;
+ }
+ parent = parent.parentNode();
+ }
+ return true;
+ },
+
+ getNodeValue: custom(function () {
+ // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
+ // value of the node needs to be fetched on the server.
+ if (this._form.nodeValue === null && this._form.shortValue) {
+ return this._getNodeValue();
+ }
+
+ let str = this._form.nodeValue || "";
+ return promise.resolve(new SimpleStringFront(str));
+ }, {
+ impl: "_getNodeValue"
+ }),
+
+ // Accessors for custom form properties.
+
+ getFormProperty: function (name) {
+ return this._form.props ? this._form.props[name] : null;
+ },
+
+ hasFormProperty: function (name) {
+ return this._form.props ? (name in this._form.props) : null;
+ },
+
+ get formProperties() {
+ return this._form.props;
+ },
+
+ /**
+ * Return a new AttributeModificationList for this node.
+ */
+ startModifyingAttributes: function () {
+ return AttributeModificationList(this);
+ },
+
+ _cacheAttributes: function () {
+ if (typeof this._attrMap != "undefined") {
+ return;
+ }
+ this._attrMap = {};
+ for (let attr of this.attributes) {
+ this._attrMap[attr.name] = attr;
+ }
+ },
+
+ _getAttribute: function (name) {
+ this._cacheAttributes();
+ return this._attrMap[name] || undefined;
+ },
+
+ /**
+ * Set this node's parent. Note that the children saved in
+ * this tree are unordered and incomplete, so shouldn't be used
+ * instead of a `children` request.
+ */
+ reparent: function (parent) {
+ if (this._parent === parent) {
+ return;
+ }
+
+ if (this._parent && this._parent._child === this) {
+ this._parent._child = this._next;
+ }
+ if (this._prev) {
+ this._prev._next = this._next;
+ }
+ if (this._next) {
+ this._next._prev = this._prev;
+ }
+ this._next = null;
+ this._prev = null;
+ this._parent = parent;
+ if (!parent) {
+ // Subtree is disconnected, we're done
+ return;
+ }
+ this._next = parent._child;
+ if (this._next) {
+ this._next._prev = this;
+ }
+ parent._child = this;
+ },
+
+ /**
+ * Return all the known children of this node.
+ */
+ treeChildren: function () {
+ let ret = [];
+ for (let child = this._child; child != null; child = child._next) {
+ ret.push(child);
+ }
+ return ret;
+ },
+
+ /**
+ * Do we use a local target?
+ * Useful to know if a rawNode is available or not.
+ *
+ * This will, one day, be removed. External code should
+ * not need to know if the target is remote or not.
+ */
+ isLocalToBeDeprecated: function () {
+ return !!this.conn._transport._serverConnection;
+ },
+
+ /**
+ * Get an nsIDOMNode for the given node front. This only works locally,
+ * and is only intended as a stopgap during the transition to the remote
+ * protocol. If you depend on this you're likely to break soon.
+ */
+ rawNode: function (rawNode) {
+ if (!this.isLocalToBeDeprecated()) {
+ console.warn("Tried to use rawNode on a remote connection.");
+ return null;
+ }
+ const { DebuggerServer } = require("devtools/server/main");
+ let actor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
+ if (!actor) {
+ // Can happen if we try to get the raw node for an already-expired
+ // actor.
+ return null;
+ }
+ return actor.rawNode;
+ }
+});
+
+exports.NodeFront = NodeFront;
+
+/**
+ * Client side of a node list as returned by querySelectorAll()
+ */
+const NodeListFront = FrontClassWithSpec(nodeListSpec, {
+ initialize: function (client, form) {
+ Front.prototype.initialize.call(this, client, form);
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ marshallPool: function () {
+ return this.parent();
+ },
+
+ // Update the object given a form representation off the wire.
+ form: function (json) {
+ this.length = json.length;
+ },
+
+ item: custom(function (index) {
+ return this._item(index).then(response => {
+ return response.node;
+ });
+ }, {
+ impl: "_item"
+ }),
+
+ items: custom(function (start, end) {
+ return this._items(start, end).then(response => {
+ return response.nodes;
+ });
+ }, {
+ impl: "_items"
+ })
+});
+
+exports.NodeListFront = NodeListFront;
+
+/**
+ * Client side of the DOM walker.
+ */
+const WalkerFront = FrontClassWithSpec(walkerSpec, {
+ // Set to true if cleanup should be requested after every mutation list.
+ autoCleanup: true,
+
+ /**
+ * This is kept for backward-compatibility reasons with older remote target.
+ * Targets previous to bug 916443
+ */
+ pick: custom(function () {
+ return this._pick().then(response => {
+ return response.node;
+ });
+ }, {impl: "_pick"}),
+
+ initialize: function (client, form) {
+ this._createRootNodePromise();
+ Front.prototype.initialize.call(this, client, form);
+ this._orphaned = new Set();
+ this._retainedOrphans = new Set();
+ },
+
+ destroy: function () {
+ Front.prototype.destroy.call(this);
+ },
+
+ // Update the object given a form representation off the wire.
+ form: function (json) {
+ this.actorID = json.actor;
+ this.rootNode = types.getType("domnode").read(json.root, this);
+ this._rootNodeDeferred.resolve(this.rootNode);
+ // FF42+ the actor starts exposing traits
+ this.traits = json.traits || {};
+ },
+
+ /**
+ * Clients can use walker.rootNode to get the current root node of the
+ * walker, but during a reload the root node might be null. This
+ * method returns a promise that will resolve to the root node when it is
+ * set.
+ */
+ getRootNode: function () {
+ return this._rootNodeDeferred.promise;
+ },
+
+ /**
+ * Create the root node promise, triggering the "new-root" notification
+ * on resolution.
+ */
+ _createRootNodePromise: function () {
+ this._rootNodeDeferred = defer();
+ this._rootNodeDeferred.promise.then(() => {
+ events.emit(this, "new-root");
+ });
+ },
+
+ /**
+ * When reading an actor form off the wire, we want to hook it up to its
+ * parent front. The protocol guarantees that the parent will be seen
+ * by the client in either a previous or the current request.
+ * So if we've already seen this parent return it, otherwise create
+ * a bare-bones stand-in node. The stand-in node will be updated
+ * with a real form by the end of the deserialization.
+ */
+ ensureParentFront: function (id) {
+ let front = this.get(id);
+ if (front) {
+ return front;
+ }
+
+ return types.getType("domnode").read({ actor: id }, this, "standin");
+ },
+
+ /**
+ * See the documentation for WalkerActor.prototype.retainNode for
+ * information on retained nodes.
+ *
+ * From the client's perspective, `retainNode` can fail if the node in
+ * question is removed from the ownership tree before the `retainNode`
+ * request reaches the server. This can only happen if the client has
+ * asked the server to release nodes but hasn't gotten a response
+ * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
+ * set is outstanding.
+ *
+ * If either of those requests is outstanding AND releases the retained
+ * node, this request will fail with noSuchActor, but the ownership tree
+ * will stay in a consistent state.
+ *
+ * Because the protocol guarantees that requests will be processed and
+ * responses received in the order they were sent, we get the right
+ * semantics by setting our local retained flag on the node only AFTER
+ * a SUCCESSFUL retainNode call.
+ */
+ retainNode: custom(function (node) {
+ return this._retainNode(node).then(() => {
+ node.retained = true;
+ });
+ }, {
+ impl: "_retainNode",
+ }),
+
+ unretainNode: custom(function (node) {
+ return this._unretainNode(node).then(() => {
+ node.retained = false;
+ if (this._retainedOrphans.has(node)) {
+ this._retainedOrphans.delete(node);
+ this._releaseFront(node);
+ }
+ });
+ }, {
+ impl: "_unretainNode"
+ }),
+
+ releaseNode: custom(function (node, options = {}) {
+ // NodeFront.destroy will destroy children in the ownership tree too,
+ // mimicking what the server will do here.
+ let actorID = node.actorID;
+ this._releaseFront(node, !!options.force);
+ return this._releaseNode({ actorID: actorID });
+ }, {
+ impl: "_releaseNode"
+ }),
+
+ findInspectingNode: custom(function () {
+ return this._findInspectingNode().then(response => {
+ return response.node;
+ });
+ }, {
+ impl: "_findInspectingNode"
+ }),
+
+ querySelector: custom(function (queryNode, selector) {
+ return this._querySelector(queryNode, selector).then(response => {
+ return response.node;
+ });
+ }, {
+ impl: "_querySelector"
+ }),
+
+ getNodeActorFromObjectActor: custom(function (objectActorID) {
+ return this._getNodeActorFromObjectActor(objectActorID).then(response => {
+ return response ? response.node : null;
+ });
+ }, {
+ impl: "_getNodeActorFromObjectActor"
+ }),
+
+ getStyleSheetOwnerNode: custom(function (styleSheetActorID) {
+ return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => {
+ return response ? response.node : null;
+ });
+ }, {
+ impl: "_getStyleSheetOwnerNode"
+ }),
+
+ getNodeFromActor: custom(function (actorID, path) {
+ return this._getNodeFromActor(actorID, path).then(response => {
+ return response ? response.node : null;
+ });
+ }, {
+ impl: "_getNodeFromActor"
+ }),
+
+ /*
+ * Incrementally search the document for a given string.
+ * For modern servers, results will be searched with using the WalkerActor
+ * `search` function (includes tag names, attributes, and text contents).
+ * Only 1 result is sent back, and calling the method again with the same
+ * query will send the next result. When there are no more results to be sent
+ * back, null is sent.
+ * @param {String} query
+ * @param {Object} options
+ * - "reverse": search backwards
+ * - "selectorOnly": treat input as a selector string (don't search text
+ * tags, attributes, etc)
+ */
+ search: custom(Task.async(function* (query, options = { }) {
+ let nodeList;
+ let searchType;
+ let searchData = this.searchData = this.searchData || { };
+ let selectorOnly = !!options.selectorOnly;
+
+ // Backwards compat. Use selector only search if the new
+ // search functionality isn't implemented, or if the caller (tests)
+ // want it.
+ if (selectorOnly || !this.traits.textSearch) {
+ searchType = "selector";
+ if (this.traits.multiFrameQuerySelectorAll) {
+ nodeList = yield this.multiFrameQuerySelectorAll(query);
+ } else {
+ nodeList = yield this.querySelectorAll(this.rootNode, query);
+ }
+ } else {
+ searchType = "search";
+ let result = yield this._search(query, options);
+ nodeList = result.list;
+ }
+
+ // If this is a new search, start at the beginning.
+ if (searchData.query !== query ||
+ searchData.selectorOnly !== selectorOnly) {
+ searchData.selectorOnly = selectorOnly;
+ searchData.query = query;
+ searchData.index = -1;
+ }
+
+ if (!nodeList.length) {
+ return null;
+ }
+
+ // Move search result cursor and cycle if necessary.
+ searchData.index = options.reverse ? searchData.index - 1 :
+ searchData.index + 1;
+ if (searchData.index >= nodeList.length) {
+ searchData.index = 0;
+ }
+ if (searchData.index < 0) {
+ searchData.index = nodeList.length - 1;
+ }
+
+ // Send back the single node, along with any relevant search data
+ let node = yield nodeList.item(searchData.index);
+ return {
+ type: searchType,
+ node: node,
+ resultsLength: nodeList.length,
+ resultsIndex: searchData.index,
+ };
+ }), {
+ impl: "_search"
+ }),
+
+ _releaseFront: function (node, force) {
+ if (node.retained && !force) {
+ node.reparent(null);
+ this._retainedOrphans.add(node);
+ return;
+ }
+
+ if (node.retained) {
+ // Forcing a removal.
+ this._retainedOrphans.delete(node);
+ }
+
+ // Release any children
+ for (let child of node.treeChildren()) {
+ this._releaseFront(child, force);
+ }
+
+ // All children will have been removed from the node by this point.
+ node.reparent(null);
+ node.destroy();
+ },
+
+ /**
+ * Get any unprocessed mutation records and process them.
+ */
+ getMutations: custom(function (options = {}) {
+ return this._getMutations(options).then(mutations => {
+ let emitMutations = [];
+ for (let change of mutations) {
+ // The target is only an actorID, get the associated front.
+ let targetID;
+ let targetFront;
+
+ if (change.type === "newRoot") {
+ // We may receive a new root without receiving any documentUnload
+ // beforehand. Like when opening tools in middle of a document load.
+ if (this.rootNode) {
+ this._createRootNodePromise();
+ }
+ this.rootNode = types.getType("domnode").read(change.target, this);
+ this._rootNodeDeferred.resolve(this.rootNode);
+ targetID = this.rootNode.actorID;
+ targetFront = this.rootNode;
+ } else {
+ targetID = change.target;
+ targetFront = this.get(targetID);
+ }
+
+ if (!targetFront) {
+ console.trace("Got a mutation for an unexpected actor: " + targetID +
+ ", please file a bug on bugzilla.mozilla.org!");
+ continue;
+ }
+
+ let emittedMutation = object.merge(change, { target: targetFront });
+
+ if (change.type === "childList" ||
+ change.type === "nativeAnonymousChildList") {
+ // Update the ownership tree according to the mutation record.
+ let addedFronts = [];
+ let removedFronts = [];
+ for (let removed of change.removed) {
+ let removedFront = this.get(removed);
+ if (!removedFront) {
+ console.error("Got a removal of an actor we didn't know about: " +
+ removed);
+ continue;
+ }
+ // Remove from the ownership tree
+ removedFront.reparent(null);
+
+ // This node is orphaned unless we get it in the 'added' list
+ // eventually.
+ this._orphaned.add(removedFront);
+ removedFronts.push(removedFront);
+ }
+ for (let added of change.added) {
+ let addedFront = this.get(added);
+ if (!addedFront) {
+ console.error("Got an addition of an actor we didn't know " +
+ "about: " + added);
+ continue;
+ }
+ addedFront.reparent(targetFront);
+
+ // The actor is reconnected to the ownership tree, unorphan
+ // it.
+ this._orphaned.delete(addedFront);
+ addedFronts.push(addedFront);
+ }
+
+ // Before passing to users, replace the added and removed actor
+ // ids with front in the mutation record.
+ emittedMutation.added = addedFronts;
+ emittedMutation.removed = removedFronts;
+
+ // If this is coming from a DOM mutation, the actor's numChildren
+ // was passed in. Otherwise, it is simulated from a frame load or
+ // unload, so don't change the front's form.
+ if ("numChildren" in change) {
+ targetFront._form.numChildren = change.numChildren;
+ }
+ } else if (change.type === "frameLoad") {
+ // Nothing we need to do here, except verify that we don't have any
+ // document children, because we should have gotten a documentUnload
+ // first.
+ for (let child of targetFront.treeChildren()) {
+ if (child.nodeType === nodeConstants.DOCUMENT_NODE) {
+ console.trace("Got an unexpected frameLoad in the inspector, " +
+ "please file a bug on bugzilla.mozilla.org!");
+ }
+ }
+ } else if (change.type === "documentUnload") {
+ if (targetFront === this.rootNode) {
+ this._createRootNodePromise();
+ }
+
+ // We try to give fronts instead of actorIDs, but these fronts need
+ // to be destroyed now.
+ emittedMutation.target = targetFront.actorID;
+ emittedMutation.targetParent = targetFront.parentNode();
+
+ // Release the document node and all of its children, even retained.
+ this._releaseFront(targetFront, true);
+ } else if (change.type === "unretained") {
+ // Retained orphans were force-released without the intervention of
+ // client (probably a navigated frame).
+ for (let released of change.nodes) {
+ let releasedFront = this.get(released);
+ this._retainedOrphans.delete(released);
+ this._releaseFront(releasedFront, true);
+ }
+ } else {
+ targetFront.updateMutation(change);
+ }
+
+ // Update the inlineTextChild property of the target for a selected list of
+ // mutation types.
+ if (change.type === "inlineTextChild" ||
+ change.type === "childList" ||
+ change.type === "nativeAnonymousChildList") {
+ if (change.inlineTextChild) {
+ targetFront.inlineTextChild =
+ types.getType("domnode").read(change.inlineTextChild, this);
+ } else {
+ targetFront.inlineTextChild = undefined;
+ }
+ }
+
+ emitMutations.push(emittedMutation);
+ }
+
+ if (options.cleanup) {
+ for (let node of this._orphaned) {
+ // This will move retained nodes to this._retainedOrphans.
+ this._releaseFront(node);
+ }
+ this._orphaned = new Set();
+ }
+
+ events.emit(this, "mutations", emitMutations);
+ });
+ }, {
+ impl: "_getMutations"
+ }),
+
+ /**
+ * Handle the `new-mutations` notification by fetching the
+ * available mutation records.
+ */
+ onMutations: preEvent("new-mutations", function () {
+ // Fetch and process the mutations.
+ this.getMutations({cleanup: this.autoCleanup}).catch(() => {});
+ }),
+
+ isLocal: function () {
+ return !!this.conn._transport._serverConnection;
+ },
+
+ // XXX hack during transition to remote inspector: get a proper NodeFront
+ // for a given local node. Only works locally.
+ frontForRawNode: function (rawNode) {
+ if (!this.isLocal()) {
+ console.warn("Tried to use frontForRawNode on a remote connection.");
+ return null;
+ }
+ const { DebuggerServer } = require("devtools/server/main");
+ let walkerActor = DebuggerServer._searchAllConnectionsForActor(this.actorID);
+ if (!walkerActor) {
+ throw Error("Could not find client side for actor " + this.actorID);
+ }
+ let nodeActor = walkerActor._ref(rawNode);
+
+ // Pass the node through a read/write pair to create the client side actor.
+ let nodeType = types.getType("domnode");
+ let returnNode = nodeType.read(
+ nodeType.write(nodeActor, walkerActor), this);
+ let top = returnNode;
+ let extras = walkerActor.parents(nodeActor, {sameTypeRootTreeItem: true});
+ for (let extraActor of extras) {
+ top = nodeType.read(nodeType.write(extraActor, walkerActor), this);
+ }
+
+ if (top !== this.rootNode) {
+ // Imported an already-orphaned node.
+ this._orphaned.add(top);
+ walkerActor._orphaned
+ .add(DebuggerServer._searchAllConnectionsForActor(top.actorID));
+ }
+ return returnNode;
+ },
+
+ removeNode: custom(Task.async(function* (node) {
+ let previousSibling = yield this.previousSibling(node);
+ let nextSibling = yield this._removeNode(node);
+ return {
+ previousSibling: previousSibling,
+ nextSibling: nextSibling,
+ };
+ }), {
+ impl: "_removeNode"
+ }),
+});
+
+exports.WalkerFront = WalkerFront;
+
+/**
+ * Client side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+var InspectorFront = FrontClassWithSpec(inspectorSpec, {
+ initialize: function (client, tabForm) {
+ Front.prototype.initialize.call(this, client);
+ this.actorID = tabForm.inspectorActor;
+
+ // XXX: This is the first actor type in its hierarchy to use the protocol
+ // library, so we're going to self-own on the client side for now.
+ this.manage(this);
+ },
+
+ destroy: function () {
+ delete this.walker;
+ Front.prototype.destroy.call(this);
+ },
+
+ getWalker: custom(function (options = {}) {
+ return this._getWalker(options).then(walker => {
+ this.walker = walker;
+ return walker;
+ });
+ }, {
+ impl: "_getWalker"
+ }),
+
+ getPageStyle: custom(function () {
+ return this._getPageStyle().then(pageStyle => {
+ // We need a walker to understand node references from the
+ // node style.
+ if (this.walker) {
+ return pageStyle;
+ }
+ return this.getWalker().then(() => {
+ return pageStyle;
+ });
+ });
+ }, {
+ impl: "_getPageStyle"
+ }),
+
+ pickColorFromPage: custom(Task.async(function* (toolbox, options) {
+ if (toolbox) {
+ // If the eyedropper was already started using the gcli command, hide it so we don't
+ // end up with 2 instances of the eyedropper on the page.
+ let {target} = toolbox;
+ let requisition = yield CommandUtils.createRequisition(target, {
+ environment: CommandUtils.createEnvironment({target})
+ });
+ yield requisition.updateExec("eyedropper --hide");
+ }
+
+ yield this._pickColorFromPage(options);
+ }), {
+ impl: "_pickColorFromPage"
+ })
+});
+
+exports.InspectorFront = InspectorFront;