1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/actors/inspector.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,2772 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +/** 1.11 + * Here's the server side of the remote inspector. 1.12 + * 1.13 + * The WalkerActor is the client's view of the debuggee's DOM. It's gives 1.14 + * the client a tree of NodeActor objects. 1.15 + * 1.16 + * The walker presents the DOM tree mostly unmodified from the source DOM 1.17 + * tree, but with a few key differences: 1.18 + * 1.19 + * - Empty text nodes are ignored. This is pretty typical of developer 1.20 + * tools, but maybe we should reconsider that on the server side. 1.21 + * - iframes with documents loaded have the loaded document as the child, 1.22 + * the walker provides one big tree for the whole document tree. 1.23 + * 1.24 + * There are a few ways to get references to NodeActors: 1.25 + * 1.26 + * - When you first get a WalkerActor reference, it comes with a free 1.27 + * reference to the root document's node. 1.28 + * - Given a node, you can ask for children, siblings, and parents. 1.29 + * - You can issue querySelector and querySelectorAll requests to find 1.30 + * other elements. 1.31 + * - Requests that return arbitrary nodes from the tree (like querySelector 1.32 + * and querySelectorAll) will also return any nodes the client hasn't 1.33 + * seen in order to have a complete set of parents. 1.34 + * 1.35 + * Once you have a NodeFront, you should be able to answer a few questions 1.36 + * without further round trips, like the node's name, namespace/tagName, 1.37 + * attributes, etc. Other questions (like a text node's full nodeValue) 1.38 + * might require another round trip. 1.39 + * 1.40 + * The protocol guarantees that the client will always know the parent of 1.41 + * any node that is returned by the server. This means that some requests 1.42 + * (like querySelector) will include the extra nodes needed to satisfy this 1.43 + * requirement. The client keeps track of this parent relationship, so the 1.44 + * node fronts form a tree that is a subset of the actual DOM tree. 1.45 + * 1.46 + * 1.47 + * We maintain this guarantee to support the ability to release subtrees on 1.48 + * the client - when a node is disconnected from the DOM tree we want to be 1.49 + * able to free the client objects for all the children nodes. 1.50 + * 1.51 + * So to be able to answer "all the children of a given node that we have 1.52 + * seen on the client side", we guarantee that every time we've seen a node, 1.53 + * we connect it up through its parents. 1.54 + */ 1.55 + 1.56 +const {Cc, Ci, Cu, Cr} = require("chrome"); 1.57 +const Services = require("Services"); 1.58 +const protocol = require("devtools/server/protocol"); 1.59 +const {Arg, Option, method, RetVal, types} = protocol; 1.60 +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); 1.61 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.62 +const object = require("sdk/util/object"); 1.63 +const events = require("sdk/event/core"); 1.64 +const {Unknown} = require("sdk/platform/xpcom"); 1.65 +const {Class} = require("sdk/core/heritage"); 1.66 +const {PageStyleActor} = require("devtools/server/actors/styles"); 1.67 +const {HighlighterActor} = require("devtools/server/actors/highlighter"); 1.68 + 1.69 +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; 1.70 +const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; 1.71 +const XHTML_NS = "http://www.w3.org/1999/xhtml"; 1.72 +const IMAGE_FETCHING_TIMEOUT = 500; 1.73 +// The possible completions to a ':' with added score to give certain values 1.74 +// some preference. 1.75 +const PSEUDO_SELECTORS = [ 1.76 + [":active", 1], 1.77 + [":hover", 1], 1.78 + [":focus", 1], 1.79 + [":visited", 0], 1.80 + [":link", 0], 1.81 + [":first-letter", 0], 1.82 + [":first-child", 2], 1.83 + [":before", 2], 1.84 + [":after", 2], 1.85 + [":lang(", 0], 1.86 + [":not(", 3], 1.87 + [":first-of-type", 0], 1.88 + [":last-of-type", 0], 1.89 + [":only-of-type", 0], 1.90 + [":only-child", 2], 1.91 + [":nth-child(", 3], 1.92 + [":nth-last-child(", 0], 1.93 + [":nth-of-type(", 0], 1.94 + [":nth-last-of-type(", 0], 1.95 + [":last-child", 2], 1.96 + [":root", 0], 1.97 + [":empty", 0], 1.98 + [":target", 0], 1.99 + [":enabled", 0], 1.100 + [":disabled", 0], 1.101 + [":checked", 1], 1.102 + ["::selection", 0] 1.103 +]; 1.104 + 1.105 + 1.106 +let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } "; 1.107 +HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } "; 1.108 + 1.109 +Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); 1.110 + 1.111 +loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); 1.112 + 1.113 +loader.lazyGetter(this, "DOMParser", function() { 1.114 + return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); 1.115 +}); 1.116 + 1.117 +exports.register = function(handle) { 1.118 + handle.addGlobalActor(InspectorActor, "inspectorActor"); 1.119 + handle.addTabActor(InspectorActor, "inspectorActor"); 1.120 +}; 1.121 + 1.122 +exports.unregister = function(handle) { 1.123 + handle.removeGlobalActor(InspectorActor); 1.124 + handle.removeTabActor(InspectorActor); 1.125 +}; 1.126 + 1.127 +// XXX: A poor man's makeInfallible until we move it out of transport.js 1.128 +// Which should be very soon. 1.129 +function makeInfallible(handler) { 1.130 + return function(...args) { 1.131 + try { 1.132 + return handler.apply(this, args); 1.133 + } catch(ex) { 1.134 + console.error(ex); 1.135 + } 1.136 + return undefined; 1.137 + } 1.138 +} 1.139 + 1.140 +// A resolve that hits the main loop first. 1.141 +function delayedResolve(value) { 1.142 + let deferred = promise.defer(); 1.143 + Services.tm.mainThread.dispatch(makeInfallible(function delayedResolveHandler() { 1.144 + deferred.resolve(value); 1.145 + }), 0); 1.146 + return deferred.promise; 1.147 +} 1.148 + 1.149 +types.addDictType("imageData", { 1.150 + // The image data 1.151 + data: "nullable:longstring", 1.152 + // The original image dimensions 1.153 + size: "json" 1.154 +}); 1.155 + 1.156 +/** 1.157 + * We only send nodeValue up to a certain size by default. This stuff 1.158 + * controls that size. 1.159 + */ 1.160 +exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50; 1.161 +var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH; 1.162 + 1.163 +exports.getValueSummaryLength = function() { 1.164 + return gValueSummaryLength; 1.165 +}; 1.166 + 1.167 +exports.setValueSummaryLength = function(val) { 1.168 + gValueSummaryLength = val; 1.169 +}; 1.170 + 1.171 +/** 1.172 + * Server side of the node actor. 1.173 + */ 1.174 +var NodeActor = exports.NodeActor = protocol.ActorClass({ 1.175 + typeName: "domnode", 1.176 + 1.177 + initialize: function(walker, node) { 1.178 + protocol.Actor.prototype.initialize.call(this, null); 1.179 + this.walker = walker; 1.180 + this.rawNode = node; 1.181 + }, 1.182 + 1.183 + toString: function() { 1.184 + return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"; 1.185 + }, 1.186 + 1.187 + /** 1.188 + * Instead of storing a connection object, the NodeActor gets its connection 1.189 + * from its associated walker. 1.190 + */ 1.191 + get conn() this.walker.conn, 1.192 + 1.193 + isDocumentElement: function() { 1.194 + return this.rawNode.ownerDocument && 1.195 + this.rawNode.ownerDocument.documentElement === this.rawNode; 1.196 + }, 1.197 + 1.198 + // Returns the JSON representation of this object over the wire. 1.199 + form: function(detail) { 1.200 + if (detail === "actorid") { 1.201 + return this.actorID; 1.202 + } 1.203 + 1.204 + let parentNode = this.walker.parentNode(this); 1.205 + 1.206 + // Estimate the number of children. 1.207 + let numChildren = this.rawNode.childNodes.length; 1.208 + if (numChildren === 0 && 1.209 + (this.rawNode.contentDocument || this.rawNode.getSVGDocument)) { 1.210 + // This might be an iframe with virtual children. 1.211 + numChildren = 1; 1.212 + } 1.213 + 1.214 + let form = { 1.215 + actor: this.actorID, 1.216 + baseURI: this.rawNode.baseURI, 1.217 + parent: parentNode ? parentNode.actorID : undefined, 1.218 + nodeType: this.rawNode.nodeType, 1.219 + namespaceURI: this.rawNode.namespaceURI, 1.220 + nodeName: this.rawNode.nodeName, 1.221 + numChildren: numChildren, 1.222 + 1.223 + // doctype attributes 1.224 + name: this.rawNode.name, 1.225 + publicId: this.rawNode.publicId, 1.226 + systemId: this.rawNode.systemId, 1.227 + 1.228 + attrs: this.writeAttrs(), 1.229 + 1.230 + pseudoClassLocks: this.writePseudoClassLocks(), 1.231 + }; 1.232 + 1.233 + if (this.isDocumentElement()) { 1.234 + form.isDocumentElement = true; 1.235 + } 1.236 + 1.237 + if (this.rawNode.nodeValue) { 1.238 + // We only include a short version of the value if it's longer than 1.239 + // gValueSummaryLength 1.240 + if (this.rawNode.nodeValue.length > gValueSummaryLength) { 1.241 + form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength); 1.242 + form.incompleteValue = true; 1.243 + } else { 1.244 + form.shortValue = this.rawNode.nodeValue; 1.245 + } 1.246 + } 1.247 + 1.248 + return form; 1.249 + }, 1.250 + 1.251 + writeAttrs: function() { 1.252 + if (!this.rawNode.attributes) { 1.253 + return undefined; 1.254 + } 1.255 + return [{namespace: attr.namespace, name: attr.name, value: attr.value } 1.256 + for (attr of this.rawNode.attributes)]; 1.257 + }, 1.258 + 1.259 + writePseudoClassLocks: function() { 1.260 + if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { 1.261 + return undefined; 1.262 + } 1.263 + let ret = undefined; 1.264 + for (let pseudo of PSEUDO_CLASSES) { 1.265 + if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) { 1.266 + ret = ret || []; 1.267 + ret.push(pseudo); 1.268 + } 1.269 + } 1.270 + return ret; 1.271 + }, 1.272 + 1.273 + /** 1.274 + * Returns a LongStringActor with the node's value. 1.275 + */ 1.276 + getNodeValue: method(function() { 1.277 + return new LongStringActor(this.conn, this.rawNode.nodeValue || ""); 1.278 + }, { 1.279 + request: {}, 1.280 + response: { 1.281 + value: RetVal("longstring") 1.282 + } 1.283 + }), 1.284 + 1.285 + /** 1.286 + * Set the node's value to a given string. 1.287 + */ 1.288 + setNodeValue: method(function(value) { 1.289 + this.rawNode.nodeValue = value; 1.290 + }, { 1.291 + request: { value: Arg(0) }, 1.292 + response: {} 1.293 + }), 1.294 + 1.295 + /** 1.296 + * Get the node's image data if any (for canvas and img nodes). 1.297 + * Returns an imageData object with the actual data being a LongStringActor 1.298 + * and a size json object. 1.299 + * The image data is transmitted as a base64 encoded png data-uri. 1.300 + * The method rejects if the node isn't an image or if the image is missing 1.301 + * 1.302 + * Accepts a maxDim request parameter to resize images that are larger. This 1.303 + * is important as the resizing occurs server-side so that image-data being 1.304 + * transfered in the longstring back to the client will be that much smaller 1.305 + */ 1.306 + getImageData: method(function(maxDim) { 1.307 + // imageToImageData may fail if the node isn't an image 1.308 + try { 1.309 + let imageData = imageToImageData(this.rawNode, maxDim); 1.310 + return promise.resolve({ 1.311 + data: LongStringActor(this.conn, imageData.data), 1.312 + size: imageData.size 1.313 + }); 1.314 + } catch(e) { 1.315 + return promise.reject(new Error("Image not available")); 1.316 + } 1.317 + }, { 1.318 + request: {maxDim: Arg(0, "nullable:number")}, 1.319 + response: RetVal("imageData") 1.320 + }), 1.321 + 1.322 + /** 1.323 + * Modify a node's attributes. Passed an array of modifications 1.324 + * similar in format to "attributes" mutations. 1.325 + * { 1.326 + * attributeName: <string> 1.327 + * attributeNamespace: <optional string> 1.328 + * newValue: <optional string> - If null or undefined, the attribute 1.329 + * will be removed. 1.330 + * } 1.331 + * 1.332 + * Returns when the modifications have been made. Mutations will 1.333 + * be queued for any changes made. 1.334 + */ 1.335 + modifyAttributes: method(function(modifications) { 1.336 + let rawNode = this.rawNode; 1.337 + for (let change of modifications) { 1.338 + if (change.newValue == null) { 1.339 + if (change.attributeNamespace) { 1.340 + rawNode.removeAttributeNS(change.attributeNamespace, change.attributeName); 1.341 + } else { 1.342 + rawNode.removeAttribute(change.attributeName); 1.343 + } 1.344 + } else { 1.345 + if (change.attributeNamespace) { 1.346 + rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, change.newValue); 1.347 + } else { 1.348 + rawNode.setAttribute(change.attributeName, change.newValue); 1.349 + } 1.350 + } 1.351 + } 1.352 + }, { 1.353 + request: { 1.354 + modifications: Arg(0, "array:json") 1.355 + }, 1.356 + response: {} 1.357 + }) 1.358 +}); 1.359 + 1.360 +/** 1.361 + * Client side of the node actor. 1.362 + * 1.363 + * Node fronts are strored in a tree that mirrors the DOM tree on the 1.364 + * server, but with a few key differences: 1.365 + * - Not all children will be necessary loaded for each node. 1.366 + * - The order of children isn't guaranteed to be the same as the DOM. 1.367 + * Children are stored in a doubly-linked list, to make addition/removal 1.368 + * and traversal quick. 1.369 + * 1.370 + * Due to the order/incompleteness of the child list, it is safe to use 1.371 + * the parent node from clients, but the `children` request should be used 1.372 + * to traverse children. 1.373 + */ 1.374 +let NodeFront = protocol.FrontClass(NodeActor, { 1.375 + initialize: function(conn, form, detail, ctx) { 1.376 + this._parent = null; // The parent node 1.377 + this._child = null; // The first child of this node. 1.378 + this._next = null; // The next sibling of this node. 1.379 + this._prev = null; // The previous sibling of this node. 1.380 + protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx); 1.381 + }, 1.382 + 1.383 + /** 1.384 + * Destroy a node front. The node must have been removed from the 1.385 + * ownership tree before this is called, unless the whole walker front 1.386 + * is being destroyed. 1.387 + */ 1.388 + destroy: function() { 1.389 + // If an observer was added on this node, shut it down. 1.390 + if (this.observer) { 1.391 + this.observer.disconnect(); 1.392 + this.observer = null; 1.393 + } 1.394 + 1.395 + protocol.Front.prototype.destroy.call(this); 1.396 + }, 1.397 + 1.398 + // Update the object given a form representation off the wire. 1.399 + form: function(form, detail, ctx) { 1.400 + if (detail === "actorid") { 1.401 + this.actorID = form; 1.402 + return; 1.403 + } 1.404 + // Shallow copy of the form. We could just store a reference, but 1.405 + // eventually we'll want to update some of the data. 1.406 + this._form = object.merge(form); 1.407 + this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; 1.408 + 1.409 + if (form.parent) { 1.410 + // Get the owner actor for this actor (the walker), and find the 1.411 + // parent node of this actor from it, creating a standin node if 1.412 + // necessary. 1.413 + let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent); 1.414 + this.reparent(parentNodeFront); 1.415 + } 1.416 + }, 1.417 + 1.418 + /** 1.419 + * Returns the parent NodeFront for this NodeFront. 1.420 + */ 1.421 + parentNode: function() { 1.422 + return this._parent; 1.423 + }, 1.424 + 1.425 + /** 1.426 + * Process a mutation entry as returned from the walker's `getMutations` 1.427 + * request. Only tries to handle changes of the node's contents 1.428 + * themselves (character data and attribute changes), the walker itself 1.429 + * will keep the ownership tree up to date. 1.430 + */ 1.431 + updateMutation: function(change) { 1.432 + if (change.type === "attributes") { 1.433 + // We'll need to lazily reparse the attributes after this change. 1.434 + this._attrMap = undefined; 1.435 + 1.436 + // Update any already-existing attributes. 1.437 + let found = false; 1.438 + for (let i = 0; i < this.attributes.length; i++) { 1.439 + let attr = this.attributes[i]; 1.440 + if (attr.name == change.attributeName && 1.441 + attr.namespace == change.attributeNamespace) { 1.442 + if (change.newValue !== null) { 1.443 + attr.value = change.newValue; 1.444 + } else { 1.445 + this.attributes.splice(i, 1); 1.446 + } 1.447 + found = true; 1.448 + break; 1.449 + } 1.450 + } 1.451 + // This is a new attribute. 1.452 + if (!found) { 1.453 + this.attributes.push({ 1.454 + name: change.attributeName, 1.455 + namespace: change.attributeNamespace, 1.456 + value: change.newValue 1.457 + }); 1.458 + } 1.459 + } else if (change.type === "characterData") { 1.460 + this._form.shortValue = change.newValue; 1.461 + this._form.incompleteValue = change.incompleteValue; 1.462 + } else if (change.type === "pseudoClassLock") { 1.463 + this._form.pseudoClassLocks = change.pseudoClassLocks; 1.464 + } 1.465 + }, 1.466 + 1.467 + // Some accessors to make NodeFront feel more like an nsIDOMNode 1.468 + 1.469 + get id() this.getAttribute("id"), 1.470 + 1.471 + get nodeType() this._form.nodeType, 1.472 + get namespaceURI() this._form.namespaceURI, 1.473 + get nodeName() this._form.nodeName, 1.474 + 1.475 + get baseURI() this._form.baseURI, 1.476 + 1.477 + get className() { 1.478 + return this.getAttribute("class") || ''; 1.479 + }, 1.480 + 1.481 + get hasChildren() this._form.numChildren > 0, 1.482 + get numChildren() this._form.numChildren, 1.483 + 1.484 + get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null, 1.485 + get shortValue() this._form.shortValue, 1.486 + get incompleteValue() !!this._form.incompleteValue, 1.487 + 1.488 + get isDocumentElement() !!this._form.isDocumentElement, 1.489 + 1.490 + // doctype properties 1.491 + get name() this._form.name, 1.492 + get publicId() this._form.publicId, 1.493 + get systemId() this._form.systemId, 1.494 + 1.495 + getAttribute: function(name) { 1.496 + let attr = this._getAttribute(name); 1.497 + return attr ? attr.value : null; 1.498 + }, 1.499 + hasAttribute: function(name) { 1.500 + this._cacheAttributes(); 1.501 + return (name in this._attrMap); 1.502 + }, 1.503 + 1.504 + get hidden() { 1.505 + let cls = this.getAttribute("class"); 1.506 + return cls && cls.indexOf(HIDDEN_CLASS) > -1; 1.507 + }, 1.508 + 1.509 + get attributes() this._form.attrs, 1.510 + 1.511 + get pseudoClassLocks() this._form.pseudoClassLocks || [], 1.512 + hasPseudoClassLock: function(pseudo) { 1.513 + return this.pseudoClassLocks.some(locked => locked === pseudo); 1.514 + }, 1.515 + 1.516 + getNodeValue: protocol.custom(function() { 1.517 + if (!this.incompleteValue) { 1.518 + return delayedResolve(new ShortLongString(this.shortValue)); 1.519 + } else { 1.520 + return this._getNodeValue(); 1.521 + } 1.522 + }, { 1.523 + impl: "_getNodeValue" 1.524 + }), 1.525 + 1.526 + /** 1.527 + * Return a new AttributeModificationList for this node. 1.528 + */ 1.529 + startModifyingAttributes: function() { 1.530 + return AttributeModificationList(this); 1.531 + }, 1.532 + 1.533 + _cacheAttributes: function() { 1.534 + if (typeof(this._attrMap) != "undefined") { 1.535 + return; 1.536 + } 1.537 + this._attrMap = {}; 1.538 + for (let attr of this.attributes) { 1.539 + this._attrMap[attr.name] = attr; 1.540 + } 1.541 + }, 1.542 + 1.543 + _getAttribute: function(name) { 1.544 + this._cacheAttributes(); 1.545 + return this._attrMap[name] || undefined; 1.546 + }, 1.547 + 1.548 + /** 1.549 + * Set this node's parent. Note that the children saved in 1.550 + * this tree are unordered and incomplete, so shouldn't be used 1.551 + * instead of a `children` request. 1.552 + */ 1.553 + reparent: function(parent) { 1.554 + if (this._parent === parent) { 1.555 + return; 1.556 + } 1.557 + 1.558 + if (this._parent && this._parent._child === this) { 1.559 + this._parent._child = this._next; 1.560 + } 1.561 + if (this._prev) { 1.562 + this._prev._next = this._next; 1.563 + } 1.564 + if (this._next) { 1.565 + this._next._prev = this._prev; 1.566 + } 1.567 + this._next = null; 1.568 + this._prev = null; 1.569 + this._parent = parent; 1.570 + if (!parent) { 1.571 + // Subtree is disconnected, we're done 1.572 + return; 1.573 + } 1.574 + this._next = parent._child; 1.575 + if (this._next) { 1.576 + this._next._prev = this; 1.577 + } 1.578 + parent._child = this; 1.579 + }, 1.580 + 1.581 + /** 1.582 + * Return all the known children of this node. 1.583 + */ 1.584 + treeChildren: function() { 1.585 + let ret = []; 1.586 + for (let child = this._child; child != null; child = child._next) { 1.587 + ret.push(child); 1.588 + } 1.589 + return ret; 1.590 + }, 1.591 + 1.592 + /** 1.593 + * Do we use a local target? 1.594 + * Useful to know if a rawNode is available or not. 1.595 + * 1.596 + * This will, one day, be removed. External code should 1.597 + * not need to know if the target is remote or not. 1.598 + */ 1.599 + isLocal_toBeDeprecated: function() { 1.600 + return !!this.conn._transport._serverConnection; 1.601 + }, 1.602 + 1.603 + /** 1.604 + * Get an nsIDOMNode for the given node front. This only works locally, 1.605 + * and is only intended as a stopgap during the transition to the remote 1.606 + * protocol. If you depend on this you're likely to break soon. 1.607 + */ 1.608 + rawNode: function(rawNode) { 1.609 + if (!this.conn._transport._serverConnection) { 1.610 + console.warn("Tried to use rawNode on a remote connection."); 1.611 + return null; 1.612 + } 1.613 + let actor = this.conn._transport._serverConnection.getActor(this.actorID); 1.614 + if (!actor) { 1.615 + // Can happen if we try to get the raw node for an already-expired 1.616 + // actor. 1.617 + return null; 1.618 + } 1.619 + return actor.rawNode; 1.620 + } 1.621 +}); 1.622 + 1.623 +/** 1.624 + * Returned from any call that might return a node that isn't connected to root by 1.625 + * nodes the child has seen, such as querySelector. 1.626 + */ 1.627 +types.addDictType("disconnectedNode", { 1.628 + // The actual node to return 1.629 + node: "domnode", 1.630 + 1.631 + // Nodes that are needed to connect the node to a node the client has already seen 1.632 + newParents: "array:domnode" 1.633 +}); 1.634 + 1.635 +types.addDictType("disconnectedNodeArray", { 1.636 + // The actual node list to return 1.637 + nodes: "array:domnode", 1.638 + 1.639 + // Nodes that are needed to connect those nodes to the root. 1.640 + newParents: "array:domnode" 1.641 +}); 1.642 + 1.643 +types.addDictType("dommutation", {}); 1.644 + 1.645 +/** 1.646 + * Server side of a node list as returned by querySelectorAll() 1.647 + */ 1.648 +var NodeListActor = exports.NodeListActor = protocol.ActorClass({ 1.649 + typeName: "domnodelist", 1.650 + 1.651 + initialize: function(walker, nodeList) { 1.652 + protocol.Actor.prototype.initialize.call(this); 1.653 + this.walker = walker; 1.654 + this.nodeList = nodeList; 1.655 + }, 1.656 + 1.657 + destroy: function() { 1.658 + protocol.Actor.prototype.destroy.call(this); 1.659 + }, 1.660 + 1.661 + /** 1.662 + * Instead of storing a connection object, the NodeActor gets its connection 1.663 + * from its associated walker. 1.664 + */ 1.665 + get conn() { 1.666 + return this.walker.conn; 1.667 + }, 1.668 + 1.669 + /** 1.670 + * Items returned by this actor should belong to the parent walker. 1.671 + */ 1.672 + marshallPool: function() { 1.673 + return this.walker; 1.674 + }, 1.675 + 1.676 + // Returns the JSON representation of this object over the wire. 1.677 + form: function() { 1.678 + return { 1.679 + actor: this.actorID, 1.680 + length: this.nodeList.length 1.681 + } 1.682 + }, 1.683 + 1.684 + /** 1.685 + * Get a single node from the node list. 1.686 + */ 1.687 + item: method(function(index) { 1.688 + return this.walker.attachElement(this.nodeList[index]); 1.689 + }, { 1.690 + request: { item: Arg(0) }, 1.691 + response: RetVal("disconnectedNode") 1.692 + }), 1.693 + 1.694 + /** 1.695 + * Get a range of the items from the node list. 1.696 + */ 1.697 + items: method(function(start=0, end=this.nodeList.length) { 1.698 + let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))]; 1.699 + let newParents = new Set(); 1.700 + for (let item of items) { 1.701 + this.walker.ensurePathToRoot(item, newParents); 1.702 + } 1.703 + return { 1.704 + nodes: items, 1.705 + newParents: [node for (node of newParents)] 1.706 + } 1.707 + }, { 1.708 + request: { 1.709 + start: Arg(0, "nullable:number"), 1.710 + end: Arg(1, "nullable:number") 1.711 + }, 1.712 + response: RetVal("disconnectedNodeArray") 1.713 + }), 1.714 + 1.715 + release: method(function() {}, { release: true }) 1.716 +}); 1.717 + 1.718 +/** 1.719 + * Client side of a node list as returned by querySelectorAll() 1.720 + */ 1.721 +var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, { 1.722 + initialize: function(client, form) { 1.723 + protocol.Front.prototype.initialize.call(this, client, form); 1.724 + }, 1.725 + 1.726 + destroy: function() { 1.727 + protocol.Front.prototype.destroy.call(this); 1.728 + }, 1.729 + 1.730 + marshallPool: function() { 1.731 + return this.parent(); 1.732 + }, 1.733 + 1.734 + // Update the object given a form representation off the wire. 1.735 + form: function(json) { 1.736 + this.length = json.length; 1.737 + }, 1.738 + 1.739 + item: protocol.custom(function(index) { 1.740 + return this._item(index).then(response => { 1.741 + return response.node; 1.742 + }); 1.743 + }, { 1.744 + impl: "_item" 1.745 + }), 1.746 + 1.747 + items: protocol.custom(function(start, end) { 1.748 + return this._items(start, end).then(response => { 1.749 + return response.nodes; 1.750 + }); 1.751 + }, { 1.752 + impl: "_items" 1.753 + }) 1.754 +}); 1.755 + 1.756 +// Some common request/response templates for the dom walker 1.757 + 1.758 +let nodeArrayMethod = { 1.759 + request: { 1.760 + node: Arg(0, "domnode"), 1.761 + maxNodes: Option(1), 1.762 + center: Option(1, "domnode"), 1.763 + start: Option(1, "domnode"), 1.764 + whatToShow: Option(1) 1.765 + }, 1.766 + response: RetVal(types.addDictType("domtraversalarray", { 1.767 + nodes: "array:domnode" 1.768 + })) 1.769 +}; 1.770 + 1.771 +let traversalMethod = { 1.772 + request: { 1.773 + node: Arg(0, "domnode"), 1.774 + whatToShow: Option(1) 1.775 + }, 1.776 + response: { 1.777 + node: RetVal("nullable:domnode") 1.778 + } 1.779 +} 1.780 + 1.781 +/** 1.782 + * Server side of the DOM walker. 1.783 + */ 1.784 +var WalkerActor = protocol.ActorClass({ 1.785 + typeName: "domwalker", 1.786 + 1.787 + events: { 1.788 + "new-mutations" : { 1.789 + type: "newMutations" 1.790 + }, 1.791 + "picker-node-picked" : { 1.792 + type: "pickerNodePicked", 1.793 + node: Arg(0, "disconnectedNode") 1.794 + }, 1.795 + "picker-node-hovered" : { 1.796 + type: "pickerNodeHovered", 1.797 + node: Arg(0, "disconnectedNode") 1.798 + }, 1.799 + "highlighter-ready" : { 1.800 + type: "highlighter-ready" 1.801 + }, 1.802 + "highlighter-hide" : { 1.803 + type: "highlighter-hide" 1.804 + } 1.805 + }, 1.806 + 1.807 + /** 1.808 + * Create the WalkerActor 1.809 + * @param DebuggerServerConnection conn 1.810 + * The server connection. 1.811 + */ 1.812 + initialize: function(conn, tabActor, options) { 1.813 + protocol.Actor.prototype.initialize.call(this, conn); 1.814 + this.tabActor = tabActor; 1.815 + this.rootWin = tabActor.window; 1.816 + this.rootDoc = this.rootWin.document; 1.817 + this._refMap = new Map(); 1.818 + this._pendingMutations = []; 1.819 + this._activePseudoClassLocks = new Set(); 1.820 + 1.821 + this.layoutHelpers = new LayoutHelpers(this.rootWin); 1.822 + 1.823 + // Nodes which have been removed from the client's known 1.824 + // ownership tree are considered "orphaned", and stored in 1.825 + // this set. 1.826 + this._orphaned = new Set(); 1.827 + 1.828 + // The client can tell the walker that it is interested in a node 1.829 + // even when it is orphaned with the `retainNode` method. This 1.830 + // list contains orphaned nodes that were so retained. 1.831 + this._retainedOrphans = new Set(); 1.832 + 1.833 + this.onMutations = this.onMutations.bind(this); 1.834 + this.onFrameLoad = this.onFrameLoad.bind(this); 1.835 + this.onFrameUnload = this.onFrameUnload.bind(this); 1.836 + 1.837 + events.on(tabActor, "will-navigate", this.onFrameUnload); 1.838 + events.on(tabActor, "navigate", this.onFrameLoad); 1.839 + 1.840 + // Ensure that the root document node actor is ready and 1.841 + // managed. 1.842 + this.rootNode = this.document(); 1.843 + }, 1.844 + 1.845 + // Returns the JSON representation of this object over the wire. 1.846 + form: function() { 1.847 + return { 1.848 + actor: this.actorID, 1.849 + root: this.rootNode.form() 1.850 + } 1.851 + }, 1.852 + 1.853 + toString: function() { 1.854 + return "[WalkerActor " + this.actorID + "]"; 1.855 + }, 1.856 + 1.857 + destroy: function() { 1.858 + this._hoveredNode = null; 1.859 + this.clearPseudoClassLocks(); 1.860 + this._activePseudoClassLocks = null; 1.861 + this.rootDoc = null; 1.862 + events.emit(this, "destroyed"); 1.863 + protocol.Actor.prototype.destroy.call(this); 1.864 + }, 1.865 + 1.866 + release: method(function() {}, { release: true }), 1.867 + 1.868 + unmanage: function(actor) { 1.869 + if (actor instanceof NodeActor) { 1.870 + if (this._activePseudoClassLocks && 1.871 + this._activePseudoClassLocks.has(actor)) { 1.872 + this.clearPsuedoClassLocks(actor); 1.873 + } 1.874 + this._refMap.delete(actor.rawNode); 1.875 + } 1.876 + protocol.Actor.prototype.unmanage.call(this, actor); 1.877 + }, 1.878 + 1.879 + _ref: function(node) { 1.880 + let actor = this._refMap.get(node); 1.881 + if (actor) return actor; 1.882 + 1.883 + actor = new NodeActor(this, node); 1.884 + 1.885 + // Add the node actor as a child of this walker actor, assigning 1.886 + // it an actorID. 1.887 + this.manage(actor); 1.888 + this._refMap.set(node, actor); 1.889 + 1.890 + if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { 1.891 + this._watchDocument(actor); 1.892 + } 1.893 + return actor; 1.894 + }, 1.895 + 1.896 + /** 1.897 + * This is kept for backward-compatibility reasons with older remote targets. 1.898 + * Targets prior to bug 916443. 1.899 + * 1.900 + * pick/cancelPick are used to pick a node on click on the content 1.901 + * document. But in their implementation prior to bug 916443, they don't allow 1.902 + * highlighting on hover. 1.903 + * The client-side now uses the highlighter actor's pick and cancelPick 1.904 + * methods instead. The client-side uses the the highlightable trait found in 1.905 + * the root actor to determine which version of pick to use. 1.906 + * 1.907 + * As for highlight, the new highlighter actor is used instead of the walker's 1.908 + * highlight method. Same here though, the client-side uses the highlightable 1.909 + * trait to dertermine which to use. 1.910 + * 1.911 + * Keeping these actor methods for now allows newer client-side debuggers to 1.912 + * inspect fxos 1.2 remote targets or older firefox desktop remote targets. 1.913 + */ 1.914 + pick: method(function() {}, {request: {}, response: RetVal("disconnectedNode")}), 1.915 + cancelPick: method(function() {}), 1.916 + highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}), 1.917 + 1.918 + attachElement: function(node) { 1.919 + node = this._ref(node); 1.920 + let newParents = this.ensurePathToRoot(node); 1.921 + return { 1.922 + node: node, 1.923 + newParents: [parent for (parent of newParents)] 1.924 + }; 1.925 + }, 1.926 + 1.927 + /** 1.928 + * Watch the given document node for mutations using the DOM observer 1.929 + * API. 1.930 + */ 1.931 + _watchDocument: function(actor) { 1.932 + let node = actor.rawNode; 1.933 + // Create the observer on the node's actor. The node will make sure 1.934 + // the observer is cleaned up when the actor is released. 1.935 + actor.observer = new actor.rawNode.defaultView.MutationObserver(this.onMutations); 1.936 + actor.observer.observe(node, { 1.937 + attributes: true, 1.938 + characterData: true, 1.939 + childList: true, 1.940 + subtree: true 1.941 + }); 1.942 + }, 1.943 + 1.944 + /** 1.945 + * Return the document node that contains the given node, 1.946 + * or the root node if no node is specified. 1.947 + * @param NodeActor node 1.948 + * The node whose document is needed, or null to 1.949 + * return the root. 1.950 + */ 1.951 + document: method(function(node) { 1.952 + let doc = node ? nodeDocument(node.rawNode) : this.rootDoc; 1.953 + return this._ref(doc); 1.954 + }, { 1.955 + request: { node: Arg(0, "nullable:domnode") }, 1.956 + response: { node: RetVal("domnode") }, 1.957 + }), 1.958 + 1.959 + /** 1.960 + * Return the documentElement for the document containing the 1.961 + * given node. 1.962 + * @param NodeActor node 1.963 + * The node whose documentElement is requested, or null 1.964 + * to use the root document. 1.965 + */ 1.966 + documentElement: method(function(node) { 1.967 + let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement; 1.968 + return this._ref(elt); 1.969 + }, { 1.970 + request: { node: Arg(0, "nullable:domnode") }, 1.971 + response: { node: RetVal("domnode") }, 1.972 + }), 1.973 + 1.974 + /** 1.975 + * Return all parents of the given node, ordered from immediate parent 1.976 + * to root. 1.977 + * @param NodeActor node 1.978 + * The node whose parents are requested. 1.979 + * @param object options 1.980 + * Named options, including: 1.981 + * `sameDocument`: If true, parents will be restricted to the same 1.982 + * document as the node. 1.983 + */ 1.984 + parents: method(function(node, options={}) { 1.985 + let walker = documentWalker(node.rawNode, this.rootWin); 1.986 + let parents = []; 1.987 + let cur; 1.988 + while((cur = walker.parentNode())) { 1.989 + if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) { 1.990 + break; 1.991 + } 1.992 + parents.push(this._ref(cur)); 1.993 + } 1.994 + return parents; 1.995 + }, { 1.996 + request: { 1.997 + node: Arg(0, "domnode"), 1.998 + sameDocument: Option(1) 1.999 + }, 1.1000 + response: { 1.1001 + nodes: RetVal("array:domnode") 1.1002 + }, 1.1003 + }), 1.1004 + 1.1005 + parentNode: function(node) { 1.1006 + let walker = documentWalker(node.rawNode, this.rootWin); 1.1007 + let parent = walker.parentNode(); 1.1008 + if (parent) { 1.1009 + return this._ref(parent); 1.1010 + } 1.1011 + return null; 1.1012 + }, 1.1013 + 1.1014 + /** 1.1015 + * Mark a node as 'retained'. 1.1016 + * 1.1017 + * A retained node is not released when `releaseNode` is called on its 1.1018 + * parent, or when a parent is released with the `cleanup` option to 1.1019 + * `getMutations`. 1.1020 + * 1.1021 + * When a retained node's parent is released, a retained mode is added to 1.1022 + * the walker's "retained orphans" list. 1.1023 + * 1.1024 + * Retained nodes can be deleted by providing the `force` option to 1.1025 + * `releaseNode`. They will also be released when their document 1.1026 + * has been destroyed. 1.1027 + * 1.1028 + * Retaining a node makes no promise about its children; They can 1.1029 + * still be removed by normal means. 1.1030 + */ 1.1031 + retainNode: method(function(node) { 1.1032 + node.retained = true; 1.1033 + }, { 1.1034 + request: { node: Arg(0, "domnode") }, 1.1035 + response: {} 1.1036 + }), 1.1037 + 1.1038 + /** 1.1039 + * Remove the 'retained' mark from a node. If the node was a 1.1040 + * retained orphan, release it. 1.1041 + */ 1.1042 + unretainNode: method(function(node) { 1.1043 + node.retained = false; 1.1044 + if (this._retainedOrphans.has(node)) { 1.1045 + this._retainedOrphans.delete(node); 1.1046 + this.releaseNode(node); 1.1047 + } 1.1048 + }, { 1.1049 + request: { node: Arg(0, "domnode") }, 1.1050 + response: {}, 1.1051 + }), 1.1052 + 1.1053 + /** 1.1054 + * Release actors for a node and all child nodes. 1.1055 + */ 1.1056 + releaseNode: method(function(node, options={}) { 1.1057 + if (node.retained && !options.force) { 1.1058 + this._retainedOrphans.add(node); 1.1059 + return; 1.1060 + } 1.1061 + 1.1062 + if (node.retained) { 1.1063 + // Forcing a retained node to go away. 1.1064 + this._retainedOrphans.delete(node); 1.1065 + } 1.1066 + 1.1067 + let walker = documentWalker(node.rawNode, this.rootWin); 1.1068 + 1.1069 + let child = walker.firstChild(); 1.1070 + while (child) { 1.1071 + let childActor = this._refMap.get(child); 1.1072 + if (childActor) { 1.1073 + this.releaseNode(childActor, options); 1.1074 + } 1.1075 + child = walker.nextSibling(); 1.1076 + } 1.1077 + 1.1078 + node.destroy(); 1.1079 + }, { 1.1080 + request: { 1.1081 + node: Arg(0, "domnode"), 1.1082 + force: Option(1) 1.1083 + } 1.1084 + }), 1.1085 + 1.1086 + /** 1.1087 + * Add any nodes between `node` and the walker's root node that have not 1.1088 + * yet been seen by the client. 1.1089 + */ 1.1090 + ensurePathToRoot: function(node, newParents=new Set()) { 1.1091 + if (!node) { 1.1092 + return newParents; 1.1093 + } 1.1094 + let walker = documentWalker(node.rawNode, this.rootWin); 1.1095 + let cur; 1.1096 + while ((cur = walker.parentNode())) { 1.1097 + let parent = this._refMap.get(cur); 1.1098 + if (!parent) { 1.1099 + // This parent didn't exist, so hasn't been seen by the client yet. 1.1100 + newParents.add(this._ref(cur)); 1.1101 + } else { 1.1102 + // This parent did exist, so the client knows about it. 1.1103 + return newParents; 1.1104 + } 1.1105 + } 1.1106 + return newParents; 1.1107 + }, 1.1108 + 1.1109 + /** 1.1110 + * Return children of the given node. By default this method will return 1.1111 + * all children of the node, but there are options that can restrict this 1.1112 + * to a more manageable subset. 1.1113 + * 1.1114 + * @param NodeActor node 1.1115 + * The node whose children you're curious about. 1.1116 + * @param object options 1.1117 + * Named options: 1.1118 + * `maxNodes`: The set of nodes returned by the method will be no longer 1.1119 + * than maxNodes. 1.1120 + * `start`: If a node is specified, the list of nodes will start 1.1121 + * with the given child. Mutally exclusive with `center`. 1.1122 + * `center`: If a node is specified, the given node will be as centered 1.1123 + * as possible in the list, given how close to the ends of the child 1.1124 + * list it is. Mutually exclusive with `start`. 1.1125 + * `whatToShow`: A bitmask of node types that should be included. See 1.1126 + * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. 1.1127 + * 1.1128 + * @returns an object with three items: 1.1129 + * hasFirst: true if the first child of the node is included in the list. 1.1130 + * hasLast: true if the last child of the node is included in the list. 1.1131 + * nodes: Child nodes returned by the request. 1.1132 + */ 1.1133 + children: method(function(node, options={}) { 1.1134 + if (options.center && options.start) { 1.1135 + throw Error("Can't specify both 'center' and 'start' options."); 1.1136 + } 1.1137 + let maxNodes = options.maxNodes || -1; 1.1138 + if (maxNodes == -1) { 1.1139 + maxNodes = Number.MAX_VALUE; 1.1140 + } 1.1141 + 1.1142 + // We're going to create a few document walkers with the same filter, 1.1143 + // make it easier. 1.1144 + let filteredWalker = (node) => { 1.1145 + return documentWalker(node, this.rootWin, options.whatToShow); 1.1146 + }; 1.1147 + 1.1148 + // Need to know the first and last child. 1.1149 + let rawNode = node.rawNode; 1.1150 + let firstChild = filteredWalker(rawNode).firstChild(); 1.1151 + let lastChild = filteredWalker(rawNode).lastChild(); 1.1152 + 1.1153 + if (!firstChild) { 1.1154 + // No children, we're done. 1.1155 + return { hasFirst: true, hasLast: true, nodes: [] }; 1.1156 + } 1.1157 + 1.1158 + let start; 1.1159 + if (options.center) { 1.1160 + start = options.center.rawNode; 1.1161 + } else if (options.start) { 1.1162 + start = options.start.rawNode; 1.1163 + } else { 1.1164 + start = firstChild; 1.1165 + } 1.1166 + 1.1167 + let nodes = []; 1.1168 + 1.1169 + // Start by reading backward from the starting point if we're centering... 1.1170 + let backwardWalker = filteredWalker(start); 1.1171 + if (start != firstChild && options.center) { 1.1172 + backwardWalker.previousSibling(); 1.1173 + let backwardCount = Math.floor(maxNodes / 2); 1.1174 + let backwardNodes = this._readBackward(backwardWalker, backwardCount); 1.1175 + nodes = backwardNodes; 1.1176 + } 1.1177 + 1.1178 + // Then read forward by any slack left in the max children... 1.1179 + let forwardWalker = filteredWalker(start); 1.1180 + let forwardCount = maxNodes - nodes.length; 1.1181 + nodes = nodes.concat(this._readForward(forwardWalker, forwardCount)); 1.1182 + 1.1183 + // If there's any room left, it means we've run all the way to the end. 1.1184 + // If we're centering, check if there are more items to read at the front. 1.1185 + let remaining = maxNodes - nodes.length; 1.1186 + if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) { 1.1187 + let firstNodes = this._readBackward(backwardWalker, remaining); 1.1188 + 1.1189 + // Then put it all back together. 1.1190 + nodes = firstNodes.concat(nodes); 1.1191 + } 1.1192 + 1.1193 + return { 1.1194 + hasFirst: nodes[0].rawNode == firstChild, 1.1195 + hasLast: nodes[nodes.length - 1].rawNode == lastChild, 1.1196 + nodes: nodes 1.1197 + }; 1.1198 + }, nodeArrayMethod), 1.1199 + 1.1200 + /** 1.1201 + * Return siblings of the given node. By default this method will return 1.1202 + * all siblings of the node, but there are options that can restrict this 1.1203 + * to a more manageable subset. 1.1204 + * 1.1205 + * If `start` or `center` are not specified, this method will center on the 1.1206 + * node whose siblings are requested. 1.1207 + * 1.1208 + * @param NodeActor node 1.1209 + * The node whose children you're curious about. 1.1210 + * @param object options 1.1211 + * Named options: 1.1212 + * `maxNodes`: The set of nodes returned by the method will be no longer 1.1213 + * than maxNodes. 1.1214 + * `start`: If a node is specified, the list of nodes will start 1.1215 + * with the given child. Mutally exclusive with `center`. 1.1216 + * `center`: If a node is specified, the given node will be as centered 1.1217 + * as possible in the list, given how close to the ends of the child 1.1218 + * list it is. Mutually exclusive with `start`. 1.1219 + * `whatToShow`: A bitmask of node types that should be included. See 1.1220 + * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. 1.1221 + * 1.1222 + * @returns an object with three items: 1.1223 + * hasFirst: true if the first child of the node is included in the list. 1.1224 + * hasLast: true if the last child of the node is included in the list. 1.1225 + * nodes: Child nodes returned by the request. 1.1226 + */ 1.1227 + siblings: method(function(node, options={}) { 1.1228 + let parentNode = documentWalker(node.rawNode, this.rootWin).parentNode(); 1.1229 + if (!parentNode) { 1.1230 + return { 1.1231 + hasFirst: true, 1.1232 + hasLast: true, 1.1233 + nodes: [node] 1.1234 + }; 1.1235 + } 1.1236 + 1.1237 + if (!(options.start || options.center)) { 1.1238 + options.center = node; 1.1239 + } 1.1240 + 1.1241 + return this.children(this._ref(parentNode), options); 1.1242 + }, nodeArrayMethod), 1.1243 + 1.1244 + /** 1.1245 + * Get the next sibling of a given node. Getting nodes one at a time 1.1246 + * might be inefficient, be careful. 1.1247 + * 1.1248 + * @param object options 1.1249 + * Named options: 1.1250 + * `whatToShow`: A bitmask of node types that should be included. See 1.1251 + * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. 1.1252 + */ 1.1253 + nextSibling: method(function(node, options={}) { 1.1254 + let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL); 1.1255 + let sibling = walker.nextSibling(); 1.1256 + return sibling ? this._ref(sibling) : null; 1.1257 + }, traversalMethod), 1.1258 + 1.1259 + /** 1.1260 + * Get the previous sibling of a given node. Getting nodes one at a time 1.1261 + * might be inefficient, be careful. 1.1262 + * 1.1263 + * @param object options 1.1264 + * Named options: 1.1265 + * `whatToShow`: A bitmask of node types that should be included. See 1.1266 + * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter. 1.1267 + */ 1.1268 + previousSibling: method(function(node, options={}) { 1.1269 + let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL); 1.1270 + let sibling = walker.previousSibling(); 1.1271 + return sibling ? this._ref(sibling) : null; 1.1272 + }, traversalMethod), 1.1273 + 1.1274 + /** 1.1275 + * Helper function for the `children` method: Read forward in the sibling 1.1276 + * list into an array with `count` items, including the current node. 1.1277 + */ 1.1278 + _readForward: function(walker, count) { 1.1279 + let ret = []; 1.1280 + let node = walker.currentNode; 1.1281 + do { 1.1282 + ret.push(this._ref(node)); 1.1283 + node = walker.nextSibling(); 1.1284 + } while (node && --count); 1.1285 + return ret; 1.1286 + }, 1.1287 + 1.1288 + /** 1.1289 + * Helper function for the `children` method: Read backward in the sibling 1.1290 + * list into an array with `count` items, including the current node. 1.1291 + */ 1.1292 + _readBackward: function(walker, count) { 1.1293 + let ret = []; 1.1294 + let node = walker.currentNode; 1.1295 + do { 1.1296 + ret.push(this._ref(node)); 1.1297 + node = walker.previousSibling(); 1.1298 + } while(node && --count); 1.1299 + ret.reverse(); 1.1300 + return ret; 1.1301 + }, 1.1302 + 1.1303 + /** 1.1304 + * Return the first node in the document that matches the given selector. 1.1305 + * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector 1.1306 + * 1.1307 + * @param NodeActor baseNode 1.1308 + * @param string selector 1.1309 + */ 1.1310 + querySelector: method(function(baseNode, selector) { 1.1311 + if (!baseNode) { 1.1312 + return {} 1.1313 + }; 1.1314 + let node = baseNode.rawNode.querySelector(selector); 1.1315 + 1.1316 + if (!node) { 1.1317 + return {} 1.1318 + }; 1.1319 + 1.1320 + return this.attachElement(node); 1.1321 + }, { 1.1322 + request: { 1.1323 + node: Arg(0, "domnode"), 1.1324 + selector: Arg(1) 1.1325 + }, 1.1326 + response: RetVal("disconnectedNode") 1.1327 + }), 1.1328 + 1.1329 + /** 1.1330 + * Return a NodeListActor with all nodes that match the given selector. 1.1331 + * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll 1.1332 + * 1.1333 + * @param NodeActor baseNode 1.1334 + * @param string selector 1.1335 + */ 1.1336 + querySelectorAll: method(function(baseNode, selector) { 1.1337 + let nodeList = null; 1.1338 + 1.1339 + try { 1.1340 + nodeList = baseNode.rawNode.querySelectorAll(selector); 1.1341 + } catch(e) { 1.1342 + // Bad selector. Do nothing as the selector can come from a searchbox. 1.1343 + } 1.1344 + 1.1345 + return new NodeListActor(this, nodeList); 1.1346 + }, { 1.1347 + request: { 1.1348 + node: Arg(0, "domnode"), 1.1349 + selector: Arg(1) 1.1350 + }, 1.1351 + response: { 1.1352 + list: RetVal("domnodelist") 1.1353 + } 1.1354 + }), 1.1355 + 1.1356 + /** 1.1357 + * Returns a list of matching results for CSS selector autocompletion. 1.1358 + * 1.1359 + * @param string query 1.1360 + * The selector query being completed 1.1361 + * @param string completing 1.1362 + * The exact token being completed out of the query 1.1363 + * @param string selectorState 1.1364 + * One of "pseudo", "id", "tag", "class", "null" 1.1365 + */ 1.1366 + getSuggestionsForQuery: method(function(query, completing, selectorState) { 1.1367 + let sugs = { 1.1368 + classes: new Map, 1.1369 + tags: new Map 1.1370 + }; 1.1371 + let result = []; 1.1372 + let nodes = null; 1.1373 + // Filtering and sorting the results so that protocol transfer is miminal. 1.1374 + switch (selectorState) { 1.1375 + case "pseudo": 1.1376 + result = PSEUDO_SELECTORS.filter(item => { 1.1377 + return item[0].startsWith(":" + completing); 1.1378 + }); 1.1379 + break; 1.1380 + 1.1381 + case "class": 1.1382 + if (!query) { 1.1383 + nodes = this.rootDoc.querySelectorAll("[class]"); 1.1384 + } 1.1385 + else { 1.1386 + nodes = this.rootDoc.querySelectorAll(query); 1.1387 + } 1.1388 + for (let node of nodes) { 1.1389 + for (let className of node.className.split(" ")) { 1.1390 + sugs.classes.set(className, (sugs.classes.get(className)|0) + 1); 1.1391 + } 1.1392 + } 1.1393 + sugs.classes.delete(""); 1.1394 + // Editing the style editor may make the stylesheet have errors and 1.1395 + // thus the page's elements' styles start changing with a transition. 1.1396 + // That transition comes from the `moz-styleeditor-transitioning` class. 1.1397 + sugs.classes.delete("moz-styleeditor-transitioning"); 1.1398 + sugs.classes.delete(HIDDEN_CLASS); 1.1399 + for (let [className, count] of sugs.classes) { 1.1400 + if (className.startsWith(completing)) { 1.1401 + result.push(["." + className, count]); 1.1402 + } 1.1403 + } 1.1404 + break; 1.1405 + 1.1406 + case "id": 1.1407 + if (!query) { 1.1408 + nodes = this.rootDoc.querySelectorAll("[id]"); 1.1409 + } 1.1410 + else { 1.1411 + nodes = this.rootDoc.querySelectorAll(query); 1.1412 + } 1.1413 + for (let node of nodes) { 1.1414 + if (node.id.startsWith(completing)) { 1.1415 + result.push(["#" + node.id, 1]); 1.1416 + } 1.1417 + } 1.1418 + break; 1.1419 + 1.1420 + case "tag": 1.1421 + if (!query) { 1.1422 + nodes = this.rootDoc.getElementsByTagName("*"); 1.1423 + } 1.1424 + else { 1.1425 + nodes = this.rootDoc.querySelectorAll(query); 1.1426 + } 1.1427 + for (let node of nodes) { 1.1428 + let tag = node.tagName.toLowerCase(); 1.1429 + sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1); 1.1430 + } 1.1431 + for (let [tag, count] of sugs.tags) { 1.1432 + if ((new RegExp("^" + completing + ".*", "i")).test(tag)) { 1.1433 + result.push([tag, count]); 1.1434 + } 1.1435 + } 1.1436 + break; 1.1437 + 1.1438 + case "null": 1.1439 + nodes = this.rootDoc.querySelectorAll(query); 1.1440 + for (let node of nodes) { 1.1441 + node.id && result.push(["#" + node.id, 1]); 1.1442 + let tag = node.tagName.toLowerCase(); 1.1443 + sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1); 1.1444 + for (let className of node.className.split(" ")) { 1.1445 + sugs.classes.set(className, (sugs.classes.get(className)|0) + 1); 1.1446 + } 1.1447 + } 1.1448 + for (let [tag, count] of sugs.tags) { 1.1449 + tag && result.push([tag, count]); 1.1450 + } 1.1451 + sugs.classes.delete(""); 1.1452 + // Editing the style editor may make the stylesheet have errors and 1.1453 + // thus the page's elements' styles start changing with a transition. 1.1454 + // That transition comes from the `moz-styleeditor-transitioning` class. 1.1455 + sugs.classes.delete("moz-styleeditor-transitioning"); 1.1456 + sugs.classes.delete(HIDDEN_CLASS); 1.1457 + for (let [className, count] of sugs.classes) { 1.1458 + className && result.push(["." + className, count]); 1.1459 + } 1.1460 + } 1.1461 + 1.1462 + // Sort alphabetically in increaseing order. 1.1463 + result = result.sort(); 1.1464 + // Sort based on count in decreasing order. 1.1465 + result = result.sort(function(a, b) { 1.1466 + return b[1] - a[1]; 1.1467 + }); 1.1468 + 1.1469 + result.slice(0, 25); 1.1470 + 1.1471 + return { 1.1472 + query: query, 1.1473 + suggestions: result 1.1474 + }; 1.1475 + }, { 1.1476 + request: { 1.1477 + query: Arg(0), 1.1478 + completing: Arg(1), 1.1479 + selectorState: Arg(2) 1.1480 + }, 1.1481 + response: { 1.1482 + list: RetVal("array:array:string") 1.1483 + } 1.1484 + }), 1.1485 + 1.1486 + /** 1.1487 + * Add a pseudo-class lock to a node. 1.1488 + * 1.1489 + * @param NodeActor node 1.1490 + * @param string pseudo 1.1491 + * A pseudoclass: ':hover', ':active', ':focus' 1.1492 + * @param options 1.1493 + * Options object: 1.1494 + * `parents`: True if the pseudo-class should be added 1.1495 + * to parent nodes. 1.1496 + * 1.1497 + * @returns An empty packet. A "pseudoClassLock" mutation will 1.1498 + * be queued for any changed nodes. 1.1499 + */ 1.1500 + addPseudoClassLock: method(function(node, pseudo, options={}) { 1.1501 + this._addPseudoClassLock(node, pseudo); 1.1502 + 1.1503 + if (!options.parents) { 1.1504 + return; 1.1505 + } 1.1506 + 1.1507 + let walker = documentWalker(node.rawNode, this.rootWin); 1.1508 + let cur; 1.1509 + while ((cur = walker.parentNode())) { 1.1510 + let curNode = this._ref(cur); 1.1511 + this._addPseudoClassLock(curNode, pseudo); 1.1512 + } 1.1513 + }, { 1.1514 + request: { 1.1515 + node: Arg(0, "domnode"), 1.1516 + pseudoClass: Arg(1), 1.1517 + parents: Option(2) 1.1518 + }, 1.1519 + response: {} 1.1520 + }), 1.1521 + 1.1522 + _queuePseudoClassMutation: function(node) { 1.1523 + this.queueMutation({ 1.1524 + target: node.actorID, 1.1525 + type: "pseudoClassLock", 1.1526 + pseudoClassLocks: node.writePseudoClassLocks() 1.1527 + }); 1.1528 + }, 1.1529 + 1.1530 + _addPseudoClassLock: function(node, pseudo) { 1.1531 + if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) { 1.1532 + return false; 1.1533 + } 1.1534 + DOMUtils.addPseudoClassLock(node.rawNode, pseudo); 1.1535 + this._activePseudoClassLocks.add(node); 1.1536 + this._queuePseudoClassMutation(node); 1.1537 + return true; 1.1538 + }, 1.1539 + 1.1540 + _installHelperSheet: function(node) { 1.1541 + if (!this.installedHelpers) { 1.1542 + this.installedHelpers = new WeakMap; 1.1543 + } 1.1544 + let win = node.rawNode.ownerDocument.defaultView; 1.1545 + if (!this.installedHelpers.has(win)) { 1.1546 + let { Style } = require("sdk/stylesheet/style"); 1.1547 + let { attach } = require("sdk/content/mod"); 1.1548 + let style = Style({source: HELPER_SHEET, type: "agent" }); 1.1549 + attach(style, win); 1.1550 + this.installedHelpers.set(win, style); 1.1551 + } 1.1552 + }, 1.1553 + 1.1554 + hideNode: method(function(node) { 1.1555 + this._installHelperSheet(node); 1.1556 + node.rawNode.classList.add(HIDDEN_CLASS); 1.1557 + }, { 1.1558 + request: { node: Arg(0, "domnode") } 1.1559 + }), 1.1560 + 1.1561 + unhideNode: method(function(node) { 1.1562 + node.rawNode.classList.remove(HIDDEN_CLASS); 1.1563 + }, { 1.1564 + request: { node: Arg(0, "domnode") } 1.1565 + }), 1.1566 + 1.1567 + /** 1.1568 + * Remove a pseudo-class lock from a node. 1.1569 + * 1.1570 + * @param NodeActor node 1.1571 + * @param string pseudo 1.1572 + * A pseudoclass: ':hover', ':active', ':focus' 1.1573 + * @param options 1.1574 + * Options object: 1.1575 + * `parents`: True if the pseudo-class should be removed 1.1576 + * from parent nodes. 1.1577 + * 1.1578 + * @returns An empty response. "pseudoClassLock" mutations 1.1579 + * will be emitted for any changed nodes. 1.1580 + */ 1.1581 + removePseudoClassLock: method(function(node, pseudo, options={}) { 1.1582 + this._removePseudoClassLock(node, pseudo); 1.1583 + 1.1584 + if (!options.parents) { 1.1585 + return; 1.1586 + } 1.1587 + 1.1588 + let walker = documentWalker(node.rawNode, this.rootWin); 1.1589 + let cur; 1.1590 + while ((cur = walker.parentNode())) { 1.1591 + let curNode = this._ref(cur); 1.1592 + this._removePseudoClassLock(curNode, pseudo); 1.1593 + } 1.1594 + }, { 1.1595 + request: { 1.1596 + node: Arg(0, "domnode"), 1.1597 + pseudoClass: Arg(1), 1.1598 + parents: Option(2) 1.1599 + }, 1.1600 + response: {} 1.1601 + }), 1.1602 + 1.1603 + _removePseudoClassLock: function(node, pseudo) { 1.1604 + if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) { 1.1605 + return false; 1.1606 + } 1.1607 + DOMUtils.removePseudoClassLock(node.rawNode, pseudo); 1.1608 + if (!node.writePseudoClassLocks()) { 1.1609 + this._activePseudoClassLocks.delete(node); 1.1610 + } 1.1611 + this._queuePseudoClassMutation(node); 1.1612 + return true; 1.1613 + }, 1.1614 + 1.1615 + /** 1.1616 + * Clear all the pseudo-classes on a given node 1.1617 + * or all nodes. 1.1618 + */ 1.1619 + clearPseudoClassLocks: method(function(node) { 1.1620 + if (node) { 1.1621 + DOMUtils.clearPseudoClassLocks(node.rawNode); 1.1622 + this._activePseudoClassLocks.delete(node); 1.1623 + this._queuePseudoClassMutation(node); 1.1624 + } else { 1.1625 + for (let locked of this._activePseudoClassLocks) { 1.1626 + DOMUtils.clearPseudoClassLocks(locked.rawNode); 1.1627 + this._activePseudoClassLocks.delete(locked); 1.1628 + this._queuePseudoClassMutation(locked); 1.1629 + } 1.1630 + } 1.1631 + }, { 1.1632 + request: { 1.1633 + node: Arg(0, "nullable:domnode") 1.1634 + }, 1.1635 + response: {} 1.1636 + }), 1.1637 + 1.1638 + /** 1.1639 + * Get a node's innerHTML property. 1.1640 + */ 1.1641 + innerHTML: method(function(node) { 1.1642 + return LongStringActor(this.conn, node.rawNode.innerHTML); 1.1643 + }, { 1.1644 + request: { 1.1645 + node: Arg(0, "domnode") 1.1646 + }, 1.1647 + response: { 1.1648 + value: RetVal("longstring") 1.1649 + } 1.1650 + }), 1.1651 + 1.1652 + /** 1.1653 + * Get a node's outerHTML property. 1.1654 + */ 1.1655 + outerHTML: method(function(node) { 1.1656 + return LongStringActor(this.conn, node.rawNode.outerHTML); 1.1657 + }, { 1.1658 + request: { 1.1659 + node: Arg(0, "domnode") 1.1660 + }, 1.1661 + response: { 1.1662 + value: RetVal("longstring") 1.1663 + } 1.1664 + }), 1.1665 + 1.1666 + /** 1.1667 + * Set a node's outerHTML property. 1.1668 + */ 1.1669 + setOuterHTML: method(function(node, value) { 1.1670 + let parsedDOM = DOMParser.parseFromString(value, "text/html"); 1.1671 + let rawNode = node.rawNode; 1.1672 + let parentNode = rawNode.parentNode; 1.1673 + 1.1674 + // Special case for head and body. Setting document.body.outerHTML 1.1675 + // creates an extra <head> tag, and document.head.outerHTML creates 1.1676 + // an extra <body>. So instead we will call replaceChild with the 1.1677 + // parsed DOM, assuming that they aren't trying to set both tags at once. 1.1678 + if (rawNode.tagName === "BODY") { 1.1679 + if (parsedDOM.head.innerHTML === "") { 1.1680 + parentNode.replaceChild(parsedDOM.body, rawNode); 1.1681 + } else { 1.1682 + rawNode.outerHTML = value; 1.1683 + } 1.1684 + } else if (rawNode.tagName === "HEAD") { 1.1685 + if (parsedDOM.body.innerHTML === "") { 1.1686 + parentNode.replaceChild(parsedDOM.head, rawNode); 1.1687 + } else { 1.1688 + rawNode.outerHTML = value; 1.1689 + } 1.1690 + } else if (node.isDocumentElement()) { 1.1691 + // Unable to set outerHTML on the document element. Fall back by 1.1692 + // setting attributes manually, then replace the body and head elements. 1.1693 + let finalAttributeModifications = []; 1.1694 + let attributeModifications = {}; 1.1695 + for (let attribute of rawNode.attributes) { 1.1696 + attributeModifications[attribute.name] = null; 1.1697 + } 1.1698 + for (let attribute of parsedDOM.documentElement.attributes) { 1.1699 + attributeModifications[attribute.name] = attribute.value; 1.1700 + } 1.1701 + for (let key in attributeModifications) { 1.1702 + finalAttributeModifications.push({ 1.1703 + attributeName: key, 1.1704 + newValue: attributeModifications[key] 1.1705 + }); 1.1706 + } 1.1707 + node.modifyAttributes(finalAttributeModifications); 1.1708 + rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head")); 1.1709 + rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body")); 1.1710 + } else { 1.1711 + rawNode.outerHTML = value; 1.1712 + } 1.1713 + }, { 1.1714 + request: { 1.1715 + node: Arg(0, "domnode"), 1.1716 + value: Arg(1), 1.1717 + }, 1.1718 + response: { 1.1719 + } 1.1720 + }), 1.1721 + 1.1722 + /** 1.1723 + * Removes a node from its parent node. 1.1724 + * 1.1725 + * @returns The node's nextSibling before it was removed. 1.1726 + */ 1.1727 + removeNode: method(function(node) { 1.1728 + if ((node.rawNode.ownerDocument && 1.1729 + node.rawNode.ownerDocument.documentElement === this.rawNode) || 1.1730 + node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { 1.1731 + throw Error("Cannot remove document or document elements."); 1.1732 + } 1.1733 + let nextSibling = this.nextSibling(node); 1.1734 + if (node.rawNode.parentNode) { 1.1735 + node.rawNode.parentNode.removeChild(node.rawNode); 1.1736 + // Mutation events will take care of the rest. 1.1737 + } 1.1738 + return nextSibling; 1.1739 + }, { 1.1740 + request: { 1.1741 + node: Arg(0, "domnode") 1.1742 + }, 1.1743 + response: { 1.1744 + nextSibling: RetVal("nullable:domnode") 1.1745 + } 1.1746 + }), 1.1747 + 1.1748 + /** 1.1749 + * Insert a node into the DOM. 1.1750 + */ 1.1751 + insertBefore: method(function(node, parent, sibling) { 1.1752 + parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null); 1.1753 + }, { 1.1754 + request: { 1.1755 + node: Arg(0, "domnode"), 1.1756 + parent: Arg(1, "domnode"), 1.1757 + sibling: Arg(2, "nullable:domnode") 1.1758 + }, 1.1759 + response: {} 1.1760 + }), 1.1761 + 1.1762 + /** 1.1763 + * Get any pending mutation records. Must be called by the client after 1.1764 + * the `new-mutations` notification is received. Returns an array of 1.1765 + * mutation records. 1.1766 + * 1.1767 + * Mutation records have a basic structure: 1.1768 + * 1.1769 + * { 1.1770 + * type: attributes|characterData|childList, 1.1771 + * target: <domnode actor ID>, 1.1772 + * } 1.1773 + * 1.1774 + * And additional attributes based on the mutation type: 1.1775 + * 1.1776 + * `attributes` type: 1.1777 + * attributeName: <string> - the attribute that changed 1.1778 + * attributeNamespace: <string> - the attribute's namespace URI, if any. 1.1779 + * newValue: <string> - The new value of the attribute, if any. 1.1780 + * 1.1781 + * `characterData` type: 1.1782 + * newValue: <string> - the new shortValue for the node 1.1783 + * [incompleteValue: true] - True if the shortValue was truncated. 1.1784 + * 1.1785 + * `childList` type is returned when the set of children for a node 1.1786 + * has changed. Includes extra data, which can be used by the client to 1.1787 + * maintain its ownership subtree. 1.1788 + * 1.1789 + * added: array of <domnode actor ID> - The list of actors *previously 1.1790 + * seen by the client* that were added to the target node. 1.1791 + * removed: array of <domnode actor ID> The list of actors *previously 1.1792 + * seen by the client* that were removed from the target node. 1.1793 + * 1.1794 + * Actors that are included in a MutationRecord's `removed` but 1.1795 + * not in an `added` have been removed from the client's ownership 1.1796 + * tree (either by being moved under a node the client has seen yet 1.1797 + * or by being removed from the tree entirely), and is considered 1.1798 + * 'orphaned'. 1.1799 + * 1.1800 + * Keep in mind that if a node that the client hasn't seen is moved 1.1801 + * into or out of the target node, it will not be included in the 1.1802 + * removedNodes and addedNodes list, so if the client is interested 1.1803 + * in the new set of children it needs to issue a `children` request. 1.1804 + */ 1.1805 + getMutations: method(function(options={}) { 1.1806 + let pending = this._pendingMutations || []; 1.1807 + this._pendingMutations = []; 1.1808 + 1.1809 + if (options.cleanup) { 1.1810 + for (let node of this._orphaned) { 1.1811 + // Release the orphaned node. Nodes or children that have been 1.1812 + // retained will be moved to this._retainedOrphans. 1.1813 + this.releaseNode(node); 1.1814 + } 1.1815 + this._orphaned = new Set(); 1.1816 + } 1.1817 + 1.1818 + return pending; 1.1819 + }, { 1.1820 + request: { 1.1821 + cleanup: Option(0) 1.1822 + }, 1.1823 + response: { 1.1824 + mutations: RetVal("array:dommutation") 1.1825 + } 1.1826 + }), 1.1827 + 1.1828 + queueMutation: function(mutation) { 1.1829 + if (!this.actorID) { 1.1830 + // We've been destroyed, don't bother queueing this mutation. 1.1831 + return; 1.1832 + } 1.1833 + // We only send the `new-mutations` notification once, until the client 1.1834 + // fetches mutations with the `getMutations` packet. 1.1835 + let needEvent = this._pendingMutations.length === 0; 1.1836 + 1.1837 + this._pendingMutations.push(mutation); 1.1838 + 1.1839 + if (needEvent) { 1.1840 + events.emit(this, "new-mutations"); 1.1841 + } 1.1842 + }, 1.1843 + 1.1844 + /** 1.1845 + * Handles mutations from the DOM mutation observer API. 1.1846 + * 1.1847 + * @param array[MutationRecord] mutations 1.1848 + * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord 1.1849 + */ 1.1850 + onMutations: function(mutations) { 1.1851 + for (let change of mutations) { 1.1852 + let targetActor = this._refMap.get(change.target); 1.1853 + if (!targetActor) { 1.1854 + continue; 1.1855 + } 1.1856 + let targetNode = change.target; 1.1857 + let mutation = { 1.1858 + type: change.type, 1.1859 + target: targetActor.actorID, 1.1860 + } 1.1861 + 1.1862 + if (mutation.type === "attributes") { 1.1863 + mutation.attributeName = change.attributeName; 1.1864 + mutation.attributeNamespace = change.attributeNamespace || undefined; 1.1865 + mutation.newValue = targetNode.getAttribute(mutation.attributeName); 1.1866 + } else if (mutation.type === "characterData") { 1.1867 + if (targetNode.nodeValue.length > gValueSummaryLength) { 1.1868 + mutation.newValue = targetNode.nodeValue.substring(0, gValueSummaryLength); 1.1869 + mutation.incompleteValue = true; 1.1870 + } else { 1.1871 + mutation.newValue = targetNode.nodeValue; 1.1872 + } 1.1873 + } else if (mutation.type === "childList") { 1.1874 + // Get the list of removed and added actors that the client has seen 1.1875 + // so that it can keep its ownership tree up to date. 1.1876 + let removedActors = []; 1.1877 + let addedActors = []; 1.1878 + for (let removed of change.removedNodes) { 1.1879 + let removedActor = this._refMap.get(removed); 1.1880 + if (!removedActor) { 1.1881 + // If the client never encountered this actor we don't need to 1.1882 + // mention that it was removed. 1.1883 + continue; 1.1884 + } 1.1885 + // While removed from the tree, nodes are saved as orphaned. 1.1886 + this._orphaned.add(removedActor); 1.1887 + removedActors.push(removedActor.actorID); 1.1888 + } 1.1889 + for (let added of change.addedNodes) { 1.1890 + let addedActor = this._refMap.get(added); 1.1891 + if (!addedActor) { 1.1892 + // If the client never encounted this actor we don't need to tell 1.1893 + // it about its addition for ownership tree purposes - if the 1.1894 + // client wants to see the new nodes it can ask for children. 1.1895 + continue; 1.1896 + } 1.1897 + // The actor is reconnected to the ownership tree, unorphan 1.1898 + // it and let the client know so that its ownership tree is up 1.1899 + // to date. 1.1900 + this._orphaned.delete(addedActor); 1.1901 + addedActors.push(addedActor.actorID); 1.1902 + } 1.1903 + mutation.numChildren = change.target.childNodes.length; 1.1904 + mutation.removed = removedActors; 1.1905 + mutation.added = addedActors; 1.1906 + } 1.1907 + this.queueMutation(mutation); 1.1908 + } 1.1909 + }, 1.1910 + 1.1911 + onFrameLoad: function({ window, isTopLevel }) { 1.1912 + if (!this.rootDoc && isTopLevel) { 1.1913 + this.rootDoc = window.document; 1.1914 + this.rootNode = this.document(); 1.1915 + this.queueMutation({ 1.1916 + type: "newRoot", 1.1917 + target: this.rootNode.form() 1.1918 + }); 1.1919 + } 1.1920 + let frame = this.layoutHelpers.getFrameElement(window); 1.1921 + let frameActor = this._refMap.get(frame); 1.1922 + if (!frameActor) { 1.1923 + return; 1.1924 + } 1.1925 + 1.1926 + this.queueMutation({ 1.1927 + type: "frameLoad", 1.1928 + target: frameActor.actorID, 1.1929 + }); 1.1930 + 1.1931 + // Send a childList mutation on the frame. 1.1932 + this.queueMutation({ 1.1933 + type: "childList", 1.1934 + target: frameActor.actorID, 1.1935 + added: [], 1.1936 + removed: [] 1.1937 + }) 1.1938 + }, 1.1939 + 1.1940 + // Returns true if domNode is in window or a subframe. 1.1941 + _childOfWindow: function(window, domNode) { 1.1942 + let win = nodeDocument(domNode).defaultView; 1.1943 + while (win) { 1.1944 + if (win === window) { 1.1945 + return true; 1.1946 + } 1.1947 + win = this.layoutHelpers.getFrameElement(win); 1.1948 + } 1.1949 + return false; 1.1950 + }, 1.1951 + 1.1952 + onFrameUnload: function({ window }) { 1.1953 + // Any retained orphans that belong to this document 1.1954 + // or its children need to be released, and a mutation sent 1.1955 + // to notify of that. 1.1956 + let releasedOrphans = []; 1.1957 + 1.1958 + for (let retained of this._retainedOrphans) { 1.1959 + if (Cu.isDeadWrapper(retained.rawNode) || 1.1960 + this._childOfWindow(window, retained.rawNode)) { 1.1961 + this._retainedOrphans.delete(retained); 1.1962 + releasedOrphans.push(retained.actorID); 1.1963 + this.releaseNode(retained, { force: true }); 1.1964 + } 1.1965 + } 1.1966 + 1.1967 + if (releasedOrphans.length > 0) { 1.1968 + this.queueMutation({ 1.1969 + target: this.rootNode.actorID, 1.1970 + type: "unretained", 1.1971 + nodes: releasedOrphans 1.1972 + }); 1.1973 + } 1.1974 + 1.1975 + let doc = window.document; 1.1976 + let documentActor = this._refMap.get(doc); 1.1977 + if (!documentActor) { 1.1978 + return; 1.1979 + } 1.1980 + 1.1981 + if (this.rootDoc === doc) { 1.1982 + this.rootDoc = null; 1.1983 + this.rootNode = null; 1.1984 + } 1.1985 + 1.1986 + this.queueMutation({ 1.1987 + type: "documentUnload", 1.1988 + target: documentActor.actorID 1.1989 + }); 1.1990 + 1.1991 + let walker = documentWalker(doc, this.rootWin); 1.1992 + let parentNode = walker.parentNode(); 1.1993 + if (parentNode) { 1.1994 + // Send a childList mutation on the frame so that clients know 1.1995 + // they should reread the children list. 1.1996 + this.queueMutation({ 1.1997 + type: "childList", 1.1998 + target: this._refMap.get(parentNode).actorID, 1.1999 + added: [], 1.2000 + removed: [] 1.2001 + }); 1.2002 + } 1.2003 + 1.2004 + // Need to force a release of this node, because those nodes can't 1.2005 + // be accessed anymore. 1.2006 + this.releaseNode(documentActor, { force: true }); 1.2007 + }, 1.2008 + 1.2009 + /** 1.2010 + * Check if a node is attached to the DOM tree of the current page. 1.2011 + * @param {nsIDomNode} rawNode 1.2012 + * @return {Boolean} false if the node is removed from the tree or within a 1.2013 + * document fragment 1.2014 + */ 1.2015 + _isInDOMTree: function(rawNode) { 1.2016 + let walker = documentWalker(rawNode, this.rootWin); 1.2017 + let current = walker.currentNode; 1.2018 + 1.2019 + // Reaching the top of tree 1.2020 + while (walker.parentNode()) { 1.2021 + current = walker.currentNode; 1.2022 + } 1.2023 + 1.2024 + // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't 1.2025 + // attached 1.2026 + if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE || 1.2027 + current !== this.rootDoc) { 1.2028 + return false; 1.2029 + } 1.2030 + 1.2031 + // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc 1.2032 + return true; 1.2033 + }, 1.2034 + 1.2035 + /** 1.2036 + * @see _isInDomTree 1.2037 + */ 1.2038 + isInDOMTree: method(function(node) { 1.2039 + return node ? this._isInDOMTree(node.rawNode) : false; 1.2040 + }, { 1.2041 + request: { node: Arg(0, "domnode") }, 1.2042 + response: { attached: RetVal("boolean") } 1.2043 + }), 1.2044 + 1.2045 + /** 1.2046 + * Given an ObjectActor (identified by its ID), commonly used in the debugger, 1.2047 + * webconsole and variablesView, return the corresponding inspector's NodeActor 1.2048 + */ 1.2049 + getNodeActorFromObjectActor: method(function(objectActorID) { 1.2050 + let debuggerObject = this.conn.getActor(objectActorID).obj; 1.2051 + let rawNode = debuggerObject.unsafeDereference(); 1.2052 + 1.2053 + if (!this._isInDOMTree(rawNode)) { 1.2054 + return null; 1.2055 + } 1.2056 + 1.2057 + // This is a special case for the document object whereby it is considered 1.2058 + // as document.documentElement (the <html> node) 1.2059 + if (rawNode.defaultView && rawNode === rawNode.defaultView.document) { 1.2060 + rawNode = rawNode.documentElement; 1.2061 + } 1.2062 + 1.2063 + return this.attachElement(rawNode); 1.2064 + }, { 1.2065 + request: { 1.2066 + objectActorID: Arg(0, "string") 1.2067 + }, 1.2068 + response: { 1.2069 + nodeFront: RetVal("nullable:disconnectedNode") 1.2070 + } 1.2071 + }), 1.2072 +}); 1.2073 + 1.2074 +/** 1.2075 + * Client side of the DOM walker. 1.2076 + */ 1.2077 +var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, { 1.2078 + // Set to true if cleanup should be requested after every mutation list. 1.2079 + autoCleanup: true, 1.2080 + 1.2081 + /** 1.2082 + * This is kept for backward-compatibility reasons with older remote target. 1.2083 + * Targets previous to bug 916443 1.2084 + */ 1.2085 + pick: protocol.custom(function() { 1.2086 + return this._pick().then(response => { 1.2087 + return response.node; 1.2088 + }); 1.2089 + }, {impl: "_pick"}), 1.2090 + 1.2091 + initialize: function(client, form) { 1.2092 + this._createRootNodePromise(); 1.2093 + protocol.Front.prototype.initialize.call(this, client, form); 1.2094 + this._orphaned = new Set(); 1.2095 + this._retainedOrphans = new Set(); 1.2096 + }, 1.2097 + 1.2098 + destroy: function() { 1.2099 + protocol.Front.prototype.destroy.call(this); 1.2100 + }, 1.2101 + 1.2102 + // Update the object given a form representation off the wire. 1.2103 + form: function(json) { 1.2104 + this.actorID = json.actor; 1.2105 + this.rootNode = types.getType("domnode").read(json.root, this); 1.2106 + this._rootNodeDeferred.resolve(this.rootNode); 1.2107 + }, 1.2108 + 1.2109 + /** 1.2110 + * Clients can use walker.rootNode to get the current root node of the 1.2111 + * walker, but during a reload the root node might be null. This 1.2112 + * method returns a promise that will resolve to the root node when it is 1.2113 + * set. 1.2114 + */ 1.2115 + getRootNode: function() { 1.2116 + return this._rootNodeDeferred.promise; 1.2117 + }, 1.2118 + 1.2119 + /** 1.2120 + * Create the root node promise, triggering the "new-root" notification 1.2121 + * on resolution. 1.2122 + */ 1.2123 + _createRootNodePromise: function() { 1.2124 + this._rootNodeDeferred = promise.defer(); 1.2125 + this._rootNodeDeferred.promise.then(() => { 1.2126 + events.emit(this, "new-root"); 1.2127 + }); 1.2128 + }, 1.2129 + 1.2130 + /** 1.2131 + * When reading an actor form off the wire, we want to hook it up to its 1.2132 + * parent front. The protocol guarantees that the parent will be seen 1.2133 + * by the client in either a previous or the current request. 1.2134 + * So if we've already seen this parent return it, otherwise create 1.2135 + * a bare-bones stand-in node. The stand-in node will be updated 1.2136 + * with a real form by the end of the deserialization. 1.2137 + */ 1.2138 + ensureParentFront: function(id) { 1.2139 + let front = this.get(id); 1.2140 + if (front) { 1.2141 + return front; 1.2142 + } 1.2143 + 1.2144 + return types.getType("domnode").read({ actor: id }, this, "standin"); 1.2145 + }, 1.2146 + 1.2147 + /** 1.2148 + * See the documentation for WalkerActor.prototype.retainNode for 1.2149 + * information on retained nodes. 1.2150 + * 1.2151 + * From the client's perspective, `retainNode` can fail if the node in 1.2152 + * question is removed from the ownership tree before the `retainNode` 1.2153 + * request reaches the server. This can only happen if the client has 1.2154 + * asked the server to release nodes but hasn't gotten a response 1.2155 + * yet: Either a `releaseNode` request or a `getMutations` with `cleanup` 1.2156 + * set is outstanding. 1.2157 + * 1.2158 + * If either of those requests is outstanding AND releases the retained 1.2159 + * node, this request will fail with noSuchActor, but the ownership tree 1.2160 + * will stay in a consistent state. 1.2161 + * 1.2162 + * Because the protocol guarantees that requests will be processed and 1.2163 + * responses received in the order they were sent, we get the right 1.2164 + * semantics by setting our local retained flag on the node only AFTER 1.2165 + * a SUCCESSFUL retainNode call. 1.2166 + */ 1.2167 + retainNode: protocol.custom(function(node) { 1.2168 + return this._retainNode(node).then(() => { 1.2169 + node.retained = true; 1.2170 + }); 1.2171 + }, { 1.2172 + impl: "_retainNode", 1.2173 + }), 1.2174 + 1.2175 + unretainNode: protocol.custom(function(node) { 1.2176 + return this._unretainNode(node).then(() => { 1.2177 + node.retained = false; 1.2178 + if (this._retainedOrphans.has(node)) { 1.2179 + this._retainedOrphans.delete(node); 1.2180 + this._releaseFront(node); 1.2181 + } 1.2182 + }); 1.2183 + }, { 1.2184 + impl: "_unretainNode" 1.2185 + }), 1.2186 + 1.2187 + releaseNode: protocol.custom(function(node, options={}) { 1.2188 + // NodeFront.destroy will destroy children in the ownership tree too, 1.2189 + // mimicking what the server will do here. 1.2190 + let actorID = node.actorID; 1.2191 + this._releaseFront(node, !!options.force); 1.2192 + return this._releaseNode({ actorID: actorID }); 1.2193 + }, { 1.2194 + impl: "_releaseNode" 1.2195 + }), 1.2196 + 1.2197 + querySelector: protocol.custom(function(queryNode, selector) { 1.2198 + return this._querySelector(queryNode, selector).then(response => { 1.2199 + return response.node; 1.2200 + }); 1.2201 + }, { 1.2202 + impl: "_querySelector" 1.2203 + }), 1.2204 + 1.2205 + getNodeActorFromObjectActor: protocol.custom(function(objectActorID) { 1.2206 + return this._getNodeActorFromObjectActor(objectActorID).then(response => { 1.2207 + return response ? response.node : null; 1.2208 + }); 1.2209 + }, { 1.2210 + impl: "_getNodeActorFromObjectActor" 1.2211 + }), 1.2212 + 1.2213 + _releaseFront: function(node, force) { 1.2214 + if (node.retained && !force) { 1.2215 + node.reparent(null); 1.2216 + this._retainedOrphans.add(node); 1.2217 + return; 1.2218 + } 1.2219 + 1.2220 + if (node.retained) { 1.2221 + // Forcing a removal. 1.2222 + this._retainedOrphans.delete(node); 1.2223 + } 1.2224 + 1.2225 + // Release any children 1.2226 + for (let child of node.treeChildren()) { 1.2227 + this._releaseFront(child, force); 1.2228 + } 1.2229 + 1.2230 + // All children will have been removed from the node by this point. 1.2231 + node.reparent(null); 1.2232 + node.destroy(); 1.2233 + }, 1.2234 + 1.2235 + /** 1.2236 + * Get any unprocessed mutation records and process them. 1.2237 + */ 1.2238 + getMutations: protocol.custom(function(options={}) { 1.2239 + return this._getMutations(options).then(mutations => { 1.2240 + let emitMutations = []; 1.2241 + for (let change of mutations) { 1.2242 + // The target is only an actorID, get the associated front. 1.2243 + let targetID; 1.2244 + let targetFront; 1.2245 + 1.2246 + if (change.type === "newRoot") { 1.2247 + this.rootNode = types.getType("domnode").read(change.target, this); 1.2248 + this._rootNodeDeferred.resolve(this.rootNode); 1.2249 + targetID = this.rootNode.actorID; 1.2250 + targetFront = this.rootNode; 1.2251 + } else { 1.2252 + targetID = change.target; 1.2253 + targetFront = this.get(targetID); 1.2254 + } 1.2255 + 1.2256 + if (!targetFront) { 1.2257 + console.trace("Got a mutation for an unexpected actor: " + targetID + ", please file a bug on bugzilla.mozilla.org!"); 1.2258 + continue; 1.2259 + } 1.2260 + 1.2261 + let emittedMutation = object.merge(change, { target: targetFront }); 1.2262 + 1.2263 + if (change.type === "childList") { 1.2264 + // Update the ownership tree according to the mutation record. 1.2265 + let addedFronts = []; 1.2266 + let removedFronts = []; 1.2267 + for (let removed of change.removed) { 1.2268 + let removedFront = this.get(removed); 1.2269 + if (!removedFront) { 1.2270 + console.error("Got a removal of an actor we didn't know about: " + removed); 1.2271 + continue; 1.2272 + } 1.2273 + // Remove from the ownership tree 1.2274 + removedFront.reparent(null); 1.2275 + 1.2276 + // This node is orphaned unless we get it in the 'added' list 1.2277 + // eventually. 1.2278 + this._orphaned.add(removedFront); 1.2279 + removedFronts.push(removedFront); 1.2280 + } 1.2281 + for (let added of change.added) { 1.2282 + let addedFront = this.get(added); 1.2283 + if (!addedFront) { 1.2284 + console.error("Got an addition of an actor we didn't know about: " + added); 1.2285 + continue; 1.2286 + } 1.2287 + addedFront.reparent(targetFront) 1.2288 + 1.2289 + // The actor is reconnected to the ownership tree, unorphan 1.2290 + // it. 1.2291 + this._orphaned.delete(addedFront); 1.2292 + addedFronts.push(addedFront); 1.2293 + } 1.2294 + // Before passing to users, replace the added and removed actor 1.2295 + // ids with front in the mutation record. 1.2296 + emittedMutation.added = addedFronts; 1.2297 + emittedMutation.removed = removedFronts; 1.2298 + targetFront._form.numChildren = change.numChildren; 1.2299 + } else if (change.type === "frameLoad") { 1.2300 + // Nothing we need to do here, except verify that we don't have any 1.2301 + // document children, because we should have gotten a documentUnload 1.2302 + // first. 1.2303 + for (let child of targetFront.treeChildren()) { 1.2304 + if (child.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { 1.2305 + console.trace("Got an unexpected frameLoad in the inspector, please file a bug on bugzilla.mozilla.org!"); 1.2306 + } 1.2307 + } 1.2308 + } else if (change.type === "documentUnload") { 1.2309 + if (targetFront === this.rootNode) { 1.2310 + this._createRootNodePromise(); 1.2311 + } 1.2312 + 1.2313 + // We try to give fronts instead of actorIDs, but these fronts need 1.2314 + // to be destroyed now. 1.2315 + emittedMutation.target = targetFront.actorID; 1.2316 + emittedMutation.targetParent = targetFront.parentNode(); 1.2317 + 1.2318 + // Release the document node and all of its children, even retained. 1.2319 + this._releaseFront(targetFront, true); 1.2320 + } else if (change.type === "unretained") { 1.2321 + // Retained orphans were force-released without the intervention of 1.2322 + // client (probably a navigated frame). 1.2323 + for (let released of change.nodes) { 1.2324 + let releasedFront = this.get(released); 1.2325 + this._retainedOrphans.delete(released); 1.2326 + this._releaseFront(releasedFront, true); 1.2327 + } 1.2328 + } else { 1.2329 + targetFront.updateMutation(change); 1.2330 + } 1.2331 + 1.2332 + emitMutations.push(emittedMutation); 1.2333 + } 1.2334 + 1.2335 + if (options.cleanup) { 1.2336 + for (let node of this._orphaned) { 1.2337 + // This will move retained nodes to this._retainedOrphans. 1.2338 + this._releaseFront(node); 1.2339 + } 1.2340 + this._orphaned = new Set(); 1.2341 + } 1.2342 + 1.2343 + events.emit(this, "mutations", emitMutations); 1.2344 + }); 1.2345 + }, { 1.2346 + impl: "_getMutations" 1.2347 + }), 1.2348 + 1.2349 + /** 1.2350 + * Handle the `new-mutations` notification by fetching the 1.2351 + * available mutation records. 1.2352 + */ 1.2353 + onMutations: protocol.preEvent("new-mutations", function() { 1.2354 + // Fetch and process the mutations. 1.2355 + this.getMutations({cleanup: this.autoCleanup}).then(null, console.error); 1.2356 + }), 1.2357 + 1.2358 + isLocal: function() { 1.2359 + return !!this.conn._transport._serverConnection; 1.2360 + }, 1.2361 + 1.2362 + // XXX hack during transition to remote inspector: get a proper NodeFront 1.2363 + // for a given local node. Only works locally. 1.2364 + frontForRawNode: function(rawNode) { 1.2365 + if (!this.isLocal()) { 1.2366 + console.warn("Tried to use frontForRawNode on a remote connection."); 1.2367 + return null; 1.2368 + } 1.2369 + let walkerActor = this.conn._transport._serverConnection.getActor(this.actorID); 1.2370 + if (!walkerActor) { 1.2371 + throw Error("Could not find client side for actor " + this.actorID); 1.2372 + } 1.2373 + let nodeActor = walkerActor._ref(rawNode); 1.2374 + 1.2375 + // Pass the node through a read/write pair to create the client side actor. 1.2376 + let nodeType = types.getType("domnode"); 1.2377 + let returnNode = nodeType.read(nodeType.write(nodeActor, walkerActor), this); 1.2378 + let top = returnNode; 1.2379 + let extras = walkerActor.parents(nodeActor); 1.2380 + for (let extraActor of extras) { 1.2381 + top = nodeType.read(nodeType.write(extraActor, walkerActor), this); 1.2382 + } 1.2383 + 1.2384 + if (top !== this.rootNode) { 1.2385 + // Imported an already-orphaned node. 1.2386 + this._orphaned.add(top); 1.2387 + walkerActor._orphaned.add(this.conn._transport._serverConnection.getActor(top.actorID)); 1.2388 + } 1.2389 + return returnNode; 1.2390 + } 1.2391 +}); 1.2392 + 1.2393 +/** 1.2394 + * Convenience API for building a list of attribute modifications 1.2395 + * for the `modifyAttributes` request. 1.2396 + */ 1.2397 +var AttributeModificationList = Class({ 1.2398 + initialize: function(node) { 1.2399 + this.node = node; 1.2400 + this.modifications = []; 1.2401 + }, 1.2402 + 1.2403 + apply: function() { 1.2404 + let ret = this.node.modifyAttributes(this.modifications); 1.2405 + return ret; 1.2406 + }, 1.2407 + 1.2408 + destroy: function() { 1.2409 + this.node = null; 1.2410 + this.modification = null; 1.2411 + }, 1.2412 + 1.2413 + setAttributeNS: function(ns, name, value) { 1.2414 + this.modifications.push({ 1.2415 + attributeNamespace: ns, 1.2416 + attributeName: name, 1.2417 + newValue: value 1.2418 + }); 1.2419 + }, 1.2420 + 1.2421 + setAttribute: function(name, value) { 1.2422 + this.setAttributeNS(undefined, name, value); 1.2423 + }, 1.2424 + 1.2425 + removeAttributeNS: function(ns, name) { 1.2426 + this.setAttributeNS(ns, name, undefined); 1.2427 + }, 1.2428 + 1.2429 + removeAttribute: function(name) { 1.2430 + this.setAttributeNS(undefined, name, undefined); 1.2431 + } 1.2432 +}) 1.2433 + 1.2434 +/** 1.2435 + * Server side of the inspector actor, which is used to create 1.2436 + * inspector-related actors, including the walker. 1.2437 + */ 1.2438 +var InspectorActor = protocol.ActorClass({ 1.2439 + typeName: "inspector", 1.2440 + initialize: function(conn, tabActor) { 1.2441 + protocol.Actor.prototype.initialize.call(this, conn); 1.2442 + this.tabActor = tabActor; 1.2443 + }, 1.2444 + 1.2445 + get window() this.tabActor.window, 1.2446 + 1.2447 + getWalker: method(function(options={}) { 1.2448 + if (this._walkerPromise) { 1.2449 + return this._walkerPromise; 1.2450 + } 1.2451 + 1.2452 + let deferred = promise.defer(); 1.2453 + this._walkerPromise = deferred.promise; 1.2454 + 1.2455 + let window = this.window; 1.2456 + var domReady = () => { 1.2457 + let tabActor = this.tabActor; 1.2458 + window.removeEventListener("DOMContentLoaded", domReady, true); 1.2459 + this.walker = WalkerActor(this.conn, tabActor, options); 1.2460 + events.once(this.walker, "destroyed", () => { 1.2461 + this._walkerPromise = null; 1.2462 + this._pageStylePromise = null; 1.2463 + }); 1.2464 + deferred.resolve(this.walker); 1.2465 + }; 1.2466 + 1.2467 + if (window.document.readyState === "loading") { 1.2468 + window.addEventListener("DOMContentLoaded", domReady, true); 1.2469 + } else { 1.2470 + domReady(); 1.2471 + } 1.2472 + 1.2473 + return this._walkerPromise; 1.2474 + }, { 1.2475 + request: {}, 1.2476 + response: { 1.2477 + walker: RetVal("domwalker") 1.2478 + } 1.2479 + }), 1.2480 + 1.2481 + getPageStyle: method(function() { 1.2482 + if (this._pageStylePromise) { 1.2483 + return this._pageStylePromise; 1.2484 + } 1.2485 + 1.2486 + this._pageStylePromise = this.getWalker().then(walker => { 1.2487 + return PageStyleActor(this); 1.2488 + }); 1.2489 + return this._pageStylePromise; 1.2490 + }, { 1.2491 + request: {}, 1.2492 + response: { 1.2493 + pageStyle: RetVal("pagestyle") 1.2494 + } 1.2495 + }), 1.2496 + 1.2497 + getHighlighter: method(function (autohide) { 1.2498 + if (this._highlighterPromise) { 1.2499 + return this._highlighterPromise; 1.2500 + } 1.2501 + 1.2502 + this._highlighterPromise = this.getWalker().then(walker => { 1.2503 + return HighlighterActor(this, autohide); 1.2504 + }); 1.2505 + return this._highlighterPromise; 1.2506 + }, { 1.2507 + request: { autohide: Arg(0, "boolean") }, 1.2508 + response: { 1.2509 + highligter: RetVal("highlighter") 1.2510 + } 1.2511 + }), 1.2512 + 1.2513 + /** 1.2514 + * Get the node's image data if any (for canvas and img nodes). 1.2515 + * Returns an imageData object with the actual data being a LongStringActor 1.2516 + * and a size json object. 1.2517 + * The image data is transmitted as a base64 encoded png data-uri. 1.2518 + * The method rejects if the node isn't an image or if the image is missing 1.2519 + * 1.2520 + * Accepts a maxDim request parameter to resize images that are larger. This 1.2521 + * is important as the resizing occurs server-side so that image-data being 1.2522 + * transfered in the longstring back to the client will be that much smaller 1.2523 + */ 1.2524 + getImageDataFromURL: method(function(url, maxDim) { 1.2525 + let deferred = promise.defer(); 1.2526 + let img = new this.window.Image(); 1.2527 + 1.2528 + // On load, get the image data and send the response 1.2529 + img.onload = () => { 1.2530 + // imageToImageData throws an error if the image is missing 1.2531 + try { 1.2532 + let imageData = imageToImageData(img, maxDim); 1.2533 + deferred.resolve({ 1.2534 + data: LongStringActor(this.conn, imageData.data), 1.2535 + size: imageData.size 1.2536 + }); 1.2537 + } catch (e) { 1.2538 + deferred.reject(new Error("Image " + url+ " not available")); 1.2539 + } 1.2540 + } 1.2541 + 1.2542 + // If the URL doesn't point to a resource, reject 1.2543 + img.onerror = () => { 1.2544 + deferred.reject(new Error("Image " + url+ " not available")); 1.2545 + } 1.2546 + 1.2547 + // If the request hangs for too long, kill it to avoid queuing up other requests 1.2548 + // to the same actor, except if we're running tests 1.2549 + if (!gDevTools.testing) { 1.2550 + this.window.setTimeout(() => { 1.2551 + deferred.reject(new Error("Image " + url + " could not be retrieved in time")); 1.2552 + }, IMAGE_FETCHING_TIMEOUT); 1.2553 + } 1.2554 + 1.2555 + img.src = url; 1.2556 + 1.2557 + return deferred.promise; 1.2558 + }, { 1.2559 + request: {url: Arg(0), maxDim: Arg(1, "nullable:number")}, 1.2560 + response: RetVal("imageData") 1.2561 + }) 1.2562 +}); 1.2563 + 1.2564 +/** 1.2565 + * Client side of the inspector actor, which is used to create 1.2566 + * inspector-related actors, including the walker. 1.2567 + */ 1.2568 +var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, { 1.2569 + initialize: function(client, tabForm) { 1.2570 + protocol.Front.prototype.initialize.call(this, client); 1.2571 + this.actorID = tabForm.inspectorActor; 1.2572 + 1.2573 + // XXX: This is the first actor type in its hierarchy to use the protocol 1.2574 + // library, so we're going to self-own on the client side for now. 1.2575 + client.addActorPool(this); 1.2576 + this.manage(this); 1.2577 + }, 1.2578 + 1.2579 + destroy: function() { 1.2580 + delete this.walker; 1.2581 + protocol.Front.prototype.destroy.call(this); 1.2582 + }, 1.2583 + 1.2584 + getWalker: protocol.custom(function() { 1.2585 + return this._getWalker().then(walker => { 1.2586 + this.walker = walker; 1.2587 + return walker; 1.2588 + }); 1.2589 + }, { 1.2590 + impl: "_getWalker" 1.2591 + }), 1.2592 + 1.2593 + getPageStyle: protocol.custom(function() { 1.2594 + return this._getPageStyle().then(pageStyle => { 1.2595 + // We need a walker to understand node references from the 1.2596 + // node style. 1.2597 + if (this.walker) { 1.2598 + return pageStyle; 1.2599 + } 1.2600 + return this.getWalker().then(() => { 1.2601 + return pageStyle; 1.2602 + }); 1.2603 + }); 1.2604 + }, { 1.2605 + impl: "_getPageStyle" 1.2606 + }) 1.2607 +}); 1.2608 + 1.2609 +function documentWalker(node, rootWin, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) { 1.2610 + return new DocumentWalker(node, rootWin, whatToShow, whitespaceTextFilter, false); 1.2611 +} 1.2612 + 1.2613 +// Exported for test purposes. 1.2614 +exports._documentWalker = documentWalker; 1.2615 + 1.2616 +function nodeDocument(node) { 1.2617 + return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null); 1.2618 +} 1.2619 + 1.2620 +/** 1.2621 + * Similar to a TreeWalker, except will dig in to iframes and it doesn't 1.2622 + * implement the good methods like previousNode and nextNode. 1.2623 + * 1.2624 + * See TreeWalker documentation for explanations of the methods. 1.2625 + */ 1.2626 +function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences) { 1.2627 + let doc = nodeDocument(aNode); 1.2628 + this.layoutHelpers = new LayoutHelpers(aRootWin); 1.2629 + this.walker = doc.createTreeWalker(doc, 1.2630 + aShow, aFilter, aExpandEntityReferences); 1.2631 + this.walker.currentNode = aNode; 1.2632 + this.filter = aFilter; 1.2633 +} 1.2634 + 1.2635 +DocumentWalker.prototype = { 1.2636 + get node() this.walker.node, 1.2637 + get whatToShow() this.walker.whatToShow, 1.2638 + get expandEntityReferences() this.walker.expandEntityReferences, 1.2639 + get currentNode() this.walker.currentNode, 1.2640 + set currentNode(aVal) this.walker.currentNode = aVal, 1.2641 + 1.2642 + /** 1.2643 + * Called when the new node is in a different document than 1.2644 + * the current node, creates a new treewalker for the document we've 1.2645 + * run in to. 1.2646 + */ 1.2647 + _reparentWalker: function(aNewNode) { 1.2648 + if (!aNewNode) { 1.2649 + return null; 1.2650 + } 1.2651 + let doc = nodeDocument(aNewNode); 1.2652 + let walker = doc.createTreeWalker(doc, 1.2653 + this.whatToShow, this.filter, this.expandEntityReferences); 1.2654 + walker.currentNode = aNewNode; 1.2655 + this.walker = walker; 1.2656 + return aNewNode; 1.2657 + }, 1.2658 + 1.2659 + parentNode: function() { 1.2660 + let currentNode = this.walker.currentNode; 1.2661 + let parentNode = this.walker.parentNode(); 1.2662 + 1.2663 + if (!parentNode) { 1.2664 + if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE 1.2665 + && currentNode.defaultView) { 1.2666 + 1.2667 + let window = currentNode.defaultView; 1.2668 + let frame = this.layoutHelpers.getFrameElement(window); 1.2669 + if (frame) { 1.2670 + return this._reparentWalker(frame); 1.2671 + } 1.2672 + } 1.2673 + return null; 1.2674 + } 1.2675 + 1.2676 + return parentNode; 1.2677 + }, 1.2678 + 1.2679 + firstChild: function() { 1.2680 + let node = this.walker.currentNode; 1.2681 + if (!node) 1.2682 + return null; 1.2683 + if (node.contentDocument) { 1.2684 + return this._reparentWalker(node.contentDocument); 1.2685 + } else if (node.getSVGDocument) { 1.2686 + return this._reparentWalker(node.getSVGDocument()); 1.2687 + } 1.2688 + return this.walker.firstChild(); 1.2689 + }, 1.2690 + 1.2691 + lastChild: function() { 1.2692 + let node = this.walker.currentNode; 1.2693 + if (!node) 1.2694 + return null; 1.2695 + if (node.contentDocument) { 1.2696 + return this._reparentWalker(node.contentDocument); 1.2697 + } else if (node.getSVGDocument) { 1.2698 + return this._reparentWalker(node.getSVGDocument()); 1.2699 + } 1.2700 + return this.walker.lastChild(); 1.2701 + }, 1.2702 + 1.2703 + previousSibling: function DW_previousSibling() this.walker.previousSibling(), 1.2704 + nextSibling: function DW_nextSibling() this.walker.nextSibling() 1.2705 +}; 1.2706 + 1.2707 +/** 1.2708 + * A tree walker filter for avoiding empty whitespace text nodes. 1.2709 + */ 1.2710 +function whitespaceTextFilter(aNode) { 1.2711 + if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE && 1.2712 + !/[^\s]/.exec(aNode.nodeValue)) { 1.2713 + return Ci.nsIDOMNodeFilter.FILTER_SKIP; 1.2714 + } else { 1.2715 + return Ci.nsIDOMNodeFilter.FILTER_ACCEPT; 1.2716 + } 1.2717 +} 1.2718 + 1.2719 +/** 1.2720 + * Given an image DOMNode, return the image data-uri. 1.2721 + * @param {DOMNode} node The image node 1.2722 + * @param {Number} maxDim Optionally pass a maximum size you want the longest 1.2723 + * side of the image to be resized to before getting the image data. 1.2724 + * @return {Object} An object containing the data-uri and size-related information 1.2725 + * {data: "...", size: {naturalWidth: 400, naturalHeight: 300, resized: true}} 1.2726 + * @throws an error if the node isn't an image or if the image is missing 1.2727 + */ 1.2728 +function imageToImageData(node, maxDim) { 1.2729 + let isImg = node.tagName.toLowerCase() === "img"; 1.2730 + let isCanvas = node.tagName.toLowerCase() === "canvas"; 1.2731 + 1.2732 + if (!isImg && !isCanvas) { 1.2733 + return null; 1.2734 + } 1.2735 + 1.2736 + // Get the image resize ratio if a maxDim was provided 1.2737 + let resizeRatio = 1; 1.2738 + let imgWidth = node.naturalWidth || node.width; 1.2739 + let imgHeight = node.naturalHeight || node.height; 1.2740 + let imgMax = Math.max(imgWidth, imgHeight); 1.2741 + if (maxDim && imgMax > maxDim) { 1.2742 + resizeRatio = maxDim / imgMax; 1.2743 + } 1.2744 + 1.2745 + // Extract the image data 1.2746 + let imageData; 1.2747 + // The image may already be a data-uri, in which case, save ourselves the 1.2748 + // trouble of converting via the canvas.drawImage.toDataURL method 1.2749 + if (isImg && node.src.startsWith("data:")) { 1.2750 + imageData = node.src; 1.2751 + } else { 1.2752 + // Create a canvas to copy the rawNode into and get the imageData from 1.2753 + let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas"); 1.2754 + canvas.width = imgWidth * resizeRatio; 1.2755 + canvas.height = imgHeight * resizeRatio; 1.2756 + let ctx = canvas.getContext("2d"); 1.2757 + 1.2758 + // Copy the rawNode image or canvas in the new canvas and extract data 1.2759 + ctx.drawImage(node, 0, 0, canvas.width, canvas.height); 1.2760 + imageData = canvas.toDataURL("image/png"); 1.2761 + } 1.2762 + 1.2763 + return { 1.2764 + data: imageData, 1.2765 + size: { 1.2766 + naturalWidth: imgWidth, 1.2767 + naturalHeight: imgHeight, 1.2768 + resized: resizeRatio !== 1 1.2769 + } 1.2770 + } 1.2771 +} 1.2772 + 1.2773 +loader.lazyGetter(this, "DOMUtils", function () { 1.2774 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.2775 +});