toolkit/devtools/server/actors/inspector.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 /**
michael@0 8 * Here's the server side of the remote inspector.
michael@0 9 *
michael@0 10 * The WalkerActor is the client's view of the debuggee's DOM. It's gives
michael@0 11 * the client a tree of NodeActor objects.
michael@0 12 *
michael@0 13 * The walker presents the DOM tree mostly unmodified from the source DOM
michael@0 14 * tree, but with a few key differences:
michael@0 15 *
michael@0 16 * - Empty text nodes are ignored. This is pretty typical of developer
michael@0 17 * tools, but maybe we should reconsider that on the server side.
michael@0 18 * - iframes with documents loaded have the loaded document as the child,
michael@0 19 * the walker provides one big tree for the whole document tree.
michael@0 20 *
michael@0 21 * There are a few ways to get references to NodeActors:
michael@0 22 *
michael@0 23 * - When you first get a WalkerActor reference, it comes with a free
michael@0 24 * reference to the root document's node.
michael@0 25 * - Given a node, you can ask for children, siblings, and parents.
michael@0 26 * - You can issue querySelector and querySelectorAll requests to find
michael@0 27 * other elements.
michael@0 28 * - Requests that return arbitrary nodes from the tree (like querySelector
michael@0 29 * and querySelectorAll) will also return any nodes the client hasn't
michael@0 30 * seen in order to have a complete set of parents.
michael@0 31 *
michael@0 32 * Once you have a NodeFront, you should be able to answer a few questions
michael@0 33 * without further round trips, like the node's name, namespace/tagName,
michael@0 34 * attributes, etc. Other questions (like a text node's full nodeValue)
michael@0 35 * might require another round trip.
michael@0 36 *
michael@0 37 * The protocol guarantees that the client will always know the parent of
michael@0 38 * any node that is returned by the server. This means that some requests
michael@0 39 * (like querySelector) will include the extra nodes needed to satisfy this
michael@0 40 * requirement. The client keeps track of this parent relationship, so the
michael@0 41 * node fronts form a tree that is a subset of the actual DOM tree.
michael@0 42 *
michael@0 43 *
michael@0 44 * We maintain this guarantee to support the ability to release subtrees on
michael@0 45 * the client - when a node is disconnected from the DOM tree we want to be
michael@0 46 * able to free the client objects for all the children nodes.
michael@0 47 *
michael@0 48 * So to be able to answer "all the children of a given node that we have
michael@0 49 * seen on the client side", we guarantee that every time we've seen a node,
michael@0 50 * we connect it up through its parents.
michael@0 51 */
michael@0 52
michael@0 53 const {Cc, Ci, Cu, Cr} = require("chrome");
michael@0 54 const Services = require("Services");
michael@0 55 const protocol = require("devtools/server/protocol");
michael@0 56 const {Arg, Option, method, RetVal, types} = protocol;
michael@0 57 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
michael@0 58 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 59 const object = require("sdk/util/object");
michael@0 60 const events = require("sdk/event/core");
michael@0 61 const {Unknown} = require("sdk/platform/xpcom");
michael@0 62 const {Class} = require("sdk/core/heritage");
michael@0 63 const {PageStyleActor} = require("devtools/server/actors/styles");
michael@0 64 const {HighlighterActor} = require("devtools/server/actors/highlighter");
michael@0 65
michael@0 66 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
michael@0 67 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
michael@0 68 const XHTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 69 const IMAGE_FETCHING_TIMEOUT = 500;
michael@0 70 // The possible completions to a ':' with added score to give certain values
michael@0 71 // some preference.
michael@0 72 const PSEUDO_SELECTORS = [
michael@0 73 [":active", 1],
michael@0 74 [":hover", 1],
michael@0 75 [":focus", 1],
michael@0 76 [":visited", 0],
michael@0 77 [":link", 0],
michael@0 78 [":first-letter", 0],
michael@0 79 [":first-child", 2],
michael@0 80 [":before", 2],
michael@0 81 [":after", 2],
michael@0 82 [":lang(", 0],
michael@0 83 [":not(", 3],
michael@0 84 [":first-of-type", 0],
michael@0 85 [":last-of-type", 0],
michael@0 86 [":only-of-type", 0],
michael@0 87 [":only-child", 2],
michael@0 88 [":nth-child(", 3],
michael@0 89 [":nth-last-child(", 0],
michael@0 90 [":nth-of-type(", 0],
michael@0 91 [":nth-last-of-type(", 0],
michael@0 92 [":last-child", 2],
michael@0 93 [":root", 0],
michael@0 94 [":empty", 0],
michael@0 95 [":target", 0],
michael@0 96 [":enabled", 0],
michael@0 97 [":disabled", 0],
michael@0 98 [":checked", 1],
michael@0 99 ["::selection", 0]
michael@0 100 ];
michael@0 101
michael@0 102
michael@0 103 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
michael@0 104 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
michael@0 105
michael@0 106 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
michael@0 107
michael@0 108 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
michael@0 109
michael@0 110 loader.lazyGetter(this, "DOMParser", function() {
michael@0 111 return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
michael@0 112 });
michael@0 113
michael@0 114 exports.register = function(handle) {
michael@0 115 handle.addGlobalActor(InspectorActor, "inspectorActor");
michael@0 116 handle.addTabActor(InspectorActor, "inspectorActor");
michael@0 117 };
michael@0 118
michael@0 119 exports.unregister = function(handle) {
michael@0 120 handle.removeGlobalActor(InspectorActor);
michael@0 121 handle.removeTabActor(InspectorActor);
michael@0 122 };
michael@0 123
michael@0 124 // XXX: A poor man's makeInfallible until we move it out of transport.js
michael@0 125 // Which should be very soon.
michael@0 126 function makeInfallible(handler) {
michael@0 127 return function(...args) {
michael@0 128 try {
michael@0 129 return handler.apply(this, args);
michael@0 130 } catch(ex) {
michael@0 131 console.error(ex);
michael@0 132 }
michael@0 133 return undefined;
michael@0 134 }
michael@0 135 }
michael@0 136
michael@0 137 // A resolve that hits the main loop first.
michael@0 138 function delayedResolve(value) {
michael@0 139 let deferred = promise.defer();
michael@0 140 Services.tm.mainThread.dispatch(makeInfallible(function delayedResolveHandler() {
michael@0 141 deferred.resolve(value);
michael@0 142 }), 0);
michael@0 143 return deferred.promise;
michael@0 144 }
michael@0 145
michael@0 146 types.addDictType("imageData", {
michael@0 147 // The image data
michael@0 148 data: "nullable:longstring",
michael@0 149 // The original image dimensions
michael@0 150 size: "json"
michael@0 151 });
michael@0 152
michael@0 153 /**
michael@0 154 * We only send nodeValue up to a certain size by default. This stuff
michael@0 155 * controls that size.
michael@0 156 */
michael@0 157 exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
michael@0 158 var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
michael@0 159
michael@0 160 exports.getValueSummaryLength = function() {
michael@0 161 return gValueSummaryLength;
michael@0 162 };
michael@0 163
michael@0 164 exports.setValueSummaryLength = function(val) {
michael@0 165 gValueSummaryLength = val;
michael@0 166 };
michael@0 167
michael@0 168 /**
michael@0 169 * Server side of the node actor.
michael@0 170 */
michael@0 171 var NodeActor = exports.NodeActor = protocol.ActorClass({
michael@0 172 typeName: "domnode",
michael@0 173
michael@0 174 initialize: function(walker, node) {
michael@0 175 protocol.Actor.prototype.initialize.call(this, null);
michael@0 176 this.walker = walker;
michael@0 177 this.rawNode = node;
michael@0 178 },
michael@0 179
michael@0 180 toString: function() {
michael@0 181 return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]";
michael@0 182 },
michael@0 183
michael@0 184 /**
michael@0 185 * Instead of storing a connection object, the NodeActor gets its connection
michael@0 186 * from its associated walker.
michael@0 187 */
michael@0 188 get conn() this.walker.conn,
michael@0 189
michael@0 190 isDocumentElement: function() {
michael@0 191 return this.rawNode.ownerDocument &&
michael@0 192 this.rawNode.ownerDocument.documentElement === this.rawNode;
michael@0 193 },
michael@0 194
michael@0 195 // Returns the JSON representation of this object over the wire.
michael@0 196 form: function(detail) {
michael@0 197 if (detail === "actorid") {
michael@0 198 return this.actorID;
michael@0 199 }
michael@0 200
michael@0 201 let parentNode = this.walker.parentNode(this);
michael@0 202
michael@0 203 // Estimate the number of children.
michael@0 204 let numChildren = this.rawNode.childNodes.length;
michael@0 205 if (numChildren === 0 &&
michael@0 206 (this.rawNode.contentDocument || this.rawNode.getSVGDocument)) {
michael@0 207 // This might be an iframe with virtual children.
michael@0 208 numChildren = 1;
michael@0 209 }
michael@0 210
michael@0 211 let form = {
michael@0 212 actor: this.actorID,
michael@0 213 baseURI: this.rawNode.baseURI,
michael@0 214 parent: parentNode ? parentNode.actorID : undefined,
michael@0 215 nodeType: this.rawNode.nodeType,
michael@0 216 namespaceURI: this.rawNode.namespaceURI,
michael@0 217 nodeName: this.rawNode.nodeName,
michael@0 218 numChildren: numChildren,
michael@0 219
michael@0 220 // doctype attributes
michael@0 221 name: this.rawNode.name,
michael@0 222 publicId: this.rawNode.publicId,
michael@0 223 systemId: this.rawNode.systemId,
michael@0 224
michael@0 225 attrs: this.writeAttrs(),
michael@0 226
michael@0 227 pseudoClassLocks: this.writePseudoClassLocks(),
michael@0 228 };
michael@0 229
michael@0 230 if (this.isDocumentElement()) {
michael@0 231 form.isDocumentElement = true;
michael@0 232 }
michael@0 233
michael@0 234 if (this.rawNode.nodeValue) {
michael@0 235 // We only include a short version of the value if it's longer than
michael@0 236 // gValueSummaryLength
michael@0 237 if (this.rawNode.nodeValue.length > gValueSummaryLength) {
michael@0 238 form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength);
michael@0 239 form.incompleteValue = true;
michael@0 240 } else {
michael@0 241 form.shortValue = this.rawNode.nodeValue;
michael@0 242 }
michael@0 243 }
michael@0 244
michael@0 245 return form;
michael@0 246 },
michael@0 247
michael@0 248 writeAttrs: function() {
michael@0 249 if (!this.rawNode.attributes) {
michael@0 250 return undefined;
michael@0 251 }
michael@0 252 return [{namespace: attr.namespace, name: attr.name, value: attr.value }
michael@0 253 for (attr of this.rawNode.attributes)];
michael@0 254 },
michael@0 255
michael@0 256 writePseudoClassLocks: function() {
michael@0 257 if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 258 return undefined;
michael@0 259 }
michael@0 260 let ret = undefined;
michael@0 261 for (let pseudo of PSEUDO_CLASSES) {
michael@0 262 if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
michael@0 263 ret = ret || [];
michael@0 264 ret.push(pseudo);
michael@0 265 }
michael@0 266 }
michael@0 267 return ret;
michael@0 268 },
michael@0 269
michael@0 270 /**
michael@0 271 * Returns a LongStringActor with the node's value.
michael@0 272 */
michael@0 273 getNodeValue: method(function() {
michael@0 274 return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
michael@0 275 }, {
michael@0 276 request: {},
michael@0 277 response: {
michael@0 278 value: RetVal("longstring")
michael@0 279 }
michael@0 280 }),
michael@0 281
michael@0 282 /**
michael@0 283 * Set the node's value to a given string.
michael@0 284 */
michael@0 285 setNodeValue: method(function(value) {
michael@0 286 this.rawNode.nodeValue = value;
michael@0 287 }, {
michael@0 288 request: { value: Arg(0) },
michael@0 289 response: {}
michael@0 290 }),
michael@0 291
michael@0 292 /**
michael@0 293 * Get the node's image data if any (for canvas and img nodes).
michael@0 294 * Returns an imageData object with the actual data being a LongStringActor
michael@0 295 * and a size json object.
michael@0 296 * The image data is transmitted as a base64 encoded png data-uri.
michael@0 297 * The method rejects if the node isn't an image or if the image is missing
michael@0 298 *
michael@0 299 * Accepts a maxDim request parameter to resize images that are larger. This
michael@0 300 * is important as the resizing occurs server-side so that image-data being
michael@0 301 * transfered in the longstring back to the client will be that much smaller
michael@0 302 */
michael@0 303 getImageData: method(function(maxDim) {
michael@0 304 // imageToImageData may fail if the node isn't an image
michael@0 305 try {
michael@0 306 let imageData = imageToImageData(this.rawNode, maxDim);
michael@0 307 return promise.resolve({
michael@0 308 data: LongStringActor(this.conn, imageData.data),
michael@0 309 size: imageData.size
michael@0 310 });
michael@0 311 } catch(e) {
michael@0 312 return promise.reject(new Error("Image not available"));
michael@0 313 }
michael@0 314 }, {
michael@0 315 request: {maxDim: Arg(0, "nullable:number")},
michael@0 316 response: RetVal("imageData")
michael@0 317 }),
michael@0 318
michael@0 319 /**
michael@0 320 * Modify a node's attributes. Passed an array of modifications
michael@0 321 * similar in format to "attributes" mutations.
michael@0 322 * {
michael@0 323 * attributeName: <string>
michael@0 324 * attributeNamespace: <optional string>
michael@0 325 * newValue: <optional string> - If null or undefined, the attribute
michael@0 326 * will be removed.
michael@0 327 * }
michael@0 328 *
michael@0 329 * Returns when the modifications have been made. Mutations will
michael@0 330 * be queued for any changes made.
michael@0 331 */
michael@0 332 modifyAttributes: method(function(modifications) {
michael@0 333 let rawNode = this.rawNode;
michael@0 334 for (let change of modifications) {
michael@0 335 if (change.newValue == null) {
michael@0 336 if (change.attributeNamespace) {
michael@0 337 rawNode.removeAttributeNS(change.attributeNamespace, change.attributeName);
michael@0 338 } else {
michael@0 339 rawNode.removeAttribute(change.attributeName);
michael@0 340 }
michael@0 341 } else {
michael@0 342 if (change.attributeNamespace) {
michael@0 343 rawNode.setAttributeNS(change.attributeNamespace, change.attributeName, change.newValue);
michael@0 344 } else {
michael@0 345 rawNode.setAttribute(change.attributeName, change.newValue);
michael@0 346 }
michael@0 347 }
michael@0 348 }
michael@0 349 }, {
michael@0 350 request: {
michael@0 351 modifications: Arg(0, "array:json")
michael@0 352 },
michael@0 353 response: {}
michael@0 354 })
michael@0 355 });
michael@0 356
michael@0 357 /**
michael@0 358 * Client side of the node actor.
michael@0 359 *
michael@0 360 * Node fronts are strored in a tree that mirrors the DOM tree on the
michael@0 361 * server, but with a few key differences:
michael@0 362 * - Not all children will be necessary loaded for each node.
michael@0 363 * - The order of children isn't guaranteed to be the same as the DOM.
michael@0 364 * Children are stored in a doubly-linked list, to make addition/removal
michael@0 365 * and traversal quick.
michael@0 366 *
michael@0 367 * Due to the order/incompleteness of the child list, it is safe to use
michael@0 368 * the parent node from clients, but the `children` request should be used
michael@0 369 * to traverse children.
michael@0 370 */
michael@0 371 let NodeFront = protocol.FrontClass(NodeActor, {
michael@0 372 initialize: function(conn, form, detail, ctx) {
michael@0 373 this._parent = null; // The parent node
michael@0 374 this._child = null; // The first child of this node.
michael@0 375 this._next = null; // The next sibling of this node.
michael@0 376 this._prev = null; // The previous sibling of this node.
michael@0 377 protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx);
michael@0 378 },
michael@0 379
michael@0 380 /**
michael@0 381 * Destroy a node front. The node must have been removed from the
michael@0 382 * ownership tree before this is called, unless the whole walker front
michael@0 383 * is being destroyed.
michael@0 384 */
michael@0 385 destroy: function() {
michael@0 386 // If an observer was added on this node, shut it down.
michael@0 387 if (this.observer) {
michael@0 388 this.observer.disconnect();
michael@0 389 this.observer = null;
michael@0 390 }
michael@0 391
michael@0 392 protocol.Front.prototype.destroy.call(this);
michael@0 393 },
michael@0 394
michael@0 395 // Update the object given a form representation off the wire.
michael@0 396 form: function(form, detail, ctx) {
michael@0 397 if (detail === "actorid") {
michael@0 398 this.actorID = form;
michael@0 399 return;
michael@0 400 }
michael@0 401 // Shallow copy of the form. We could just store a reference, but
michael@0 402 // eventually we'll want to update some of the data.
michael@0 403 this._form = object.merge(form);
michael@0 404 this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
michael@0 405
michael@0 406 if (form.parent) {
michael@0 407 // Get the owner actor for this actor (the walker), and find the
michael@0 408 // parent node of this actor from it, creating a standin node if
michael@0 409 // necessary.
michael@0 410 let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
michael@0 411 this.reparent(parentNodeFront);
michael@0 412 }
michael@0 413 },
michael@0 414
michael@0 415 /**
michael@0 416 * Returns the parent NodeFront for this NodeFront.
michael@0 417 */
michael@0 418 parentNode: function() {
michael@0 419 return this._parent;
michael@0 420 },
michael@0 421
michael@0 422 /**
michael@0 423 * Process a mutation entry as returned from the walker's `getMutations`
michael@0 424 * request. Only tries to handle changes of the node's contents
michael@0 425 * themselves (character data and attribute changes), the walker itself
michael@0 426 * will keep the ownership tree up to date.
michael@0 427 */
michael@0 428 updateMutation: function(change) {
michael@0 429 if (change.type === "attributes") {
michael@0 430 // We'll need to lazily reparse the attributes after this change.
michael@0 431 this._attrMap = undefined;
michael@0 432
michael@0 433 // Update any already-existing attributes.
michael@0 434 let found = false;
michael@0 435 for (let i = 0; i < this.attributes.length; i++) {
michael@0 436 let attr = this.attributes[i];
michael@0 437 if (attr.name == change.attributeName &&
michael@0 438 attr.namespace == change.attributeNamespace) {
michael@0 439 if (change.newValue !== null) {
michael@0 440 attr.value = change.newValue;
michael@0 441 } else {
michael@0 442 this.attributes.splice(i, 1);
michael@0 443 }
michael@0 444 found = true;
michael@0 445 break;
michael@0 446 }
michael@0 447 }
michael@0 448 // This is a new attribute.
michael@0 449 if (!found) {
michael@0 450 this.attributes.push({
michael@0 451 name: change.attributeName,
michael@0 452 namespace: change.attributeNamespace,
michael@0 453 value: change.newValue
michael@0 454 });
michael@0 455 }
michael@0 456 } else if (change.type === "characterData") {
michael@0 457 this._form.shortValue = change.newValue;
michael@0 458 this._form.incompleteValue = change.incompleteValue;
michael@0 459 } else if (change.type === "pseudoClassLock") {
michael@0 460 this._form.pseudoClassLocks = change.pseudoClassLocks;
michael@0 461 }
michael@0 462 },
michael@0 463
michael@0 464 // Some accessors to make NodeFront feel more like an nsIDOMNode
michael@0 465
michael@0 466 get id() this.getAttribute("id"),
michael@0 467
michael@0 468 get nodeType() this._form.nodeType,
michael@0 469 get namespaceURI() this._form.namespaceURI,
michael@0 470 get nodeName() this._form.nodeName,
michael@0 471
michael@0 472 get baseURI() this._form.baseURI,
michael@0 473
michael@0 474 get className() {
michael@0 475 return this.getAttribute("class") || '';
michael@0 476 },
michael@0 477
michael@0 478 get hasChildren() this._form.numChildren > 0,
michael@0 479 get numChildren() this._form.numChildren,
michael@0 480
michael@0 481 get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null,
michael@0 482 get shortValue() this._form.shortValue,
michael@0 483 get incompleteValue() !!this._form.incompleteValue,
michael@0 484
michael@0 485 get isDocumentElement() !!this._form.isDocumentElement,
michael@0 486
michael@0 487 // doctype properties
michael@0 488 get name() this._form.name,
michael@0 489 get publicId() this._form.publicId,
michael@0 490 get systemId() this._form.systemId,
michael@0 491
michael@0 492 getAttribute: function(name) {
michael@0 493 let attr = this._getAttribute(name);
michael@0 494 return attr ? attr.value : null;
michael@0 495 },
michael@0 496 hasAttribute: function(name) {
michael@0 497 this._cacheAttributes();
michael@0 498 return (name in this._attrMap);
michael@0 499 },
michael@0 500
michael@0 501 get hidden() {
michael@0 502 let cls = this.getAttribute("class");
michael@0 503 return cls && cls.indexOf(HIDDEN_CLASS) > -1;
michael@0 504 },
michael@0 505
michael@0 506 get attributes() this._form.attrs,
michael@0 507
michael@0 508 get pseudoClassLocks() this._form.pseudoClassLocks || [],
michael@0 509 hasPseudoClassLock: function(pseudo) {
michael@0 510 return this.pseudoClassLocks.some(locked => locked === pseudo);
michael@0 511 },
michael@0 512
michael@0 513 getNodeValue: protocol.custom(function() {
michael@0 514 if (!this.incompleteValue) {
michael@0 515 return delayedResolve(new ShortLongString(this.shortValue));
michael@0 516 } else {
michael@0 517 return this._getNodeValue();
michael@0 518 }
michael@0 519 }, {
michael@0 520 impl: "_getNodeValue"
michael@0 521 }),
michael@0 522
michael@0 523 /**
michael@0 524 * Return a new AttributeModificationList for this node.
michael@0 525 */
michael@0 526 startModifyingAttributes: function() {
michael@0 527 return AttributeModificationList(this);
michael@0 528 },
michael@0 529
michael@0 530 _cacheAttributes: function() {
michael@0 531 if (typeof(this._attrMap) != "undefined") {
michael@0 532 return;
michael@0 533 }
michael@0 534 this._attrMap = {};
michael@0 535 for (let attr of this.attributes) {
michael@0 536 this._attrMap[attr.name] = attr;
michael@0 537 }
michael@0 538 },
michael@0 539
michael@0 540 _getAttribute: function(name) {
michael@0 541 this._cacheAttributes();
michael@0 542 return this._attrMap[name] || undefined;
michael@0 543 },
michael@0 544
michael@0 545 /**
michael@0 546 * Set this node's parent. Note that the children saved in
michael@0 547 * this tree are unordered and incomplete, so shouldn't be used
michael@0 548 * instead of a `children` request.
michael@0 549 */
michael@0 550 reparent: function(parent) {
michael@0 551 if (this._parent === parent) {
michael@0 552 return;
michael@0 553 }
michael@0 554
michael@0 555 if (this._parent && this._parent._child === this) {
michael@0 556 this._parent._child = this._next;
michael@0 557 }
michael@0 558 if (this._prev) {
michael@0 559 this._prev._next = this._next;
michael@0 560 }
michael@0 561 if (this._next) {
michael@0 562 this._next._prev = this._prev;
michael@0 563 }
michael@0 564 this._next = null;
michael@0 565 this._prev = null;
michael@0 566 this._parent = parent;
michael@0 567 if (!parent) {
michael@0 568 // Subtree is disconnected, we're done
michael@0 569 return;
michael@0 570 }
michael@0 571 this._next = parent._child;
michael@0 572 if (this._next) {
michael@0 573 this._next._prev = this;
michael@0 574 }
michael@0 575 parent._child = this;
michael@0 576 },
michael@0 577
michael@0 578 /**
michael@0 579 * Return all the known children of this node.
michael@0 580 */
michael@0 581 treeChildren: function() {
michael@0 582 let ret = [];
michael@0 583 for (let child = this._child; child != null; child = child._next) {
michael@0 584 ret.push(child);
michael@0 585 }
michael@0 586 return ret;
michael@0 587 },
michael@0 588
michael@0 589 /**
michael@0 590 * Do we use a local target?
michael@0 591 * Useful to know if a rawNode is available or not.
michael@0 592 *
michael@0 593 * This will, one day, be removed. External code should
michael@0 594 * not need to know if the target is remote or not.
michael@0 595 */
michael@0 596 isLocal_toBeDeprecated: function() {
michael@0 597 return !!this.conn._transport._serverConnection;
michael@0 598 },
michael@0 599
michael@0 600 /**
michael@0 601 * Get an nsIDOMNode for the given node front. This only works locally,
michael@0 602 * and is only intended as a stopgap during the transition to the remote
michael@0 603 * protocol. If you depend on this you're likely to break soon.
michael@0 604 */
michael@0 605 rawNode: function(rawNode) {
michael@0 606 if (!this.conn._transport._serverConnection) {
michael@0 607 console.warn("Tried to use rawNode on a remote connection.");
michael@0 608 return null;
michael@0 609 }
michael@0 610 let actor = this.conn._transport._serverConnection.getActor(this.actorID);
michael@0 611 if (!actor) {
michael@0 612 // Can happen if we try to get the raw node for an already-expired
michael@0 613 // actor.
michael@0 614 return null;
michael@0 615 }
michael@0 616 return actor.rawNode;
michael@0 617 }
michael@0 618 });
michael@0 619
michael@0 620 /**
michael@0 621 * Returned from any call that might return a node that isn't connected to root by
michael@0 622 * nodes the child has seen, such as querySelector.
michael@0 623 */
michael@0 624 types.addDictType("disconnectedNode", {
michael@0 625 // The actual node to return
michael@0 626 node: "domnode",
michael@0 627
michael@0 628 // Nodes that are needed to connect the node to a node the client has already seen
michael@0 629 newParents: "array:domnode"
michael@0 630 });
michael@0 631
michael@0 632 types.addDictType("disconnectedNodeArray", {
michael@0 633 // The actual node list to return
michael@0 634 nodes: "array:domnode",
michael@0 635
michael@0 636 // Nodes that are needed to connect those nodes to the root.
michael@0 637 newParents: "array:domnode"
michael@0 638 });
michael@0 639
michael@0 640 types.addDictType("dommutation", {});
michael@0 641
michael@0 642 /**
michael@0 643 * Server side of a node list as returned by querySelectorAll()
michael@0 644 */
michael@0 645 var NodeListActor = exports.NodeListActor = protocol.ActorClass({
michael@0 646 typeName: "domnodelist",
michael@0 647
michael@0 648 initialize: function(walker, nodeList) {
michael@0 649 protocol.Actor.prototype.initialize.call(this);
michael@0 650 this.walker = walker;
michael@0 651 this.nodeList = nodeList;
michael@0 652 },
michael@0 653
michael@0 654 destroy: function() {
michael@0 655 protocol.Actor.prototype.destroy.call(this);
michael@0 656 },
michael@0 657
michael@0 658 /**
michael@0 659 * Instead of storing a connection object, the NodeActor gets its connection
michael@0 660 * from its associated walker.
michael@0 661 */
michael@0 662 get conn() {
michael@0 663 return this.walker.conn;
michael@0 664 },
michael@0 665
michael@0 666 /**
michael@0 667 * Items returned by this actor should belong to the parent walker.
michael@0 668 */
michael@0 669 marshallPool: function() {
michael@0 670 return this.walker;
michael@0 671 },
michael@0 672
michael@0 673 // Returns the JSON representation of this object over the wire.
michael@0 674 form: function() {
michael@0 675 return {
michael@0 676 actor: this.actorID,
michael@0 677 length: this.nodeList.length
michael@0 678 }
michael@0 679 },
michael@0 680
michael@0 681 /**
michael@0 682 * Get a single node from the node list.
michael@0 683 */
michael@0 684 item: method(function(index) {
michael@0 685 return this.walker.attachElement(this.nodeList[index]);
michael@0 686 }, {
michael@0 687 request: { item: Arg(0) },
michael@0 688 response: RetVal("disconnectedNode")
michael@0 689 }),
michael@0 690
michael@0 691 /**
michael@0 692 * Get a range of the items from the node list.
michael@0 693 */
michael@0 694 items: method(function(start=0, end=this.nodeList.length) {
michael@0 695 let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))];
michael@0 696 let newParents = new Set();
michael@0 697 for (let item of items) {
michael@0 698 this.walker.ensurePathToRoot(item, newParents);
michael@0 699 }
michael@0 700 return {
michael@0 701 nodes: items,
michael@0 702 newParents: [node for (node of newParents)]
michael@0 703 }
michael@0 704 }, {
michael@0 705 request: {
michael@0 706 start: Arg(0, "nullable:number"),
michael@0 707 end: Arg(1, "nullable:number")
michael@0 708 },
michael@0 709 response: RetVal("disconnectedNodeArray")
michael@0 710 }),
michael@0 711
michael@0 712 release: method(function() {}, { release: true })
michael@0 713 });
michael@0 714
michael@0 715 /**
michael@0 716 * Client side of a node list as returned by querySelectorAll()
michael@0 717 */
michael@0 718 var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, {
michael@0 719 initialize: function(client, form) {
michael@0 720 protocol.Front.prototype.initialize.call(this, client, form);
michael@0 721 },
michael@0 722
michael@0 723 destroy: function() {
michael@0 724 protocol.Front.prototype.destroy.call(this);
michael@0 725 },
michael@0 726
michael@0 727 marshallPool: function() {
michael@0 728 return this.parent();
michael@0 729 },
michael@0 730
michael@0 731 // Update the object given a form representation off the wire.
michael@0 732 form: function(json) {
michael@0 733 this.length = json.length;
michael@0 734 },
michael@0 735
michael@0 736 item: protocol.custom(function(index) {
michael@0 737 return this._item(index).then(response => {
michael@0 738 return response.node;
michael@0 739 });
michael@0 740 }, {
michael@0 741 impl: "_item"
michael@0 742 }),
michael@0 743
michael@0 744 items: protocol.custom(function(start, end) {
michael@0 745 return this._items(start, end).then(response => {
michael@0 746 return response.nodes;
michael@0 747 });
michael@0 748 }, {
michael@0 749 impl: "_items"
michael@0 750 })
michael@0 751 });
michael@0 752
michael@0 753 // Some common request/response templates for the dom walker
michael@0 754
michael@0 755 let nodeArrayMethod = {
michael@0 756 request: {
michael@0 757 node: Arg(0, "domnode"),
michael@0 758 maxNodes: Option(1),
michael@0 759 center: Option(1, "domnode"),
michael@0 760 start: Option(1, "domnode"),
michael@0 761 whatToShow: Option(1)
michael@0 762 },
michael@0 763 response: RetVal(types.addDictType("domtraversalarray", {
michael@0 764 nodes: "array:domnode"
michael@0 765 }))
michael@0 766 };
michael@0 767
michael@0 768 let traversalMethod = {
michael@0 769 request: {
michael@0 770 node: Arg(0, "domnode"),
michael@0 771 whatToShow: Option(1)
michael@0 772 },
michael@0 773 response: {
michael@0 774 node: RetVal("nullable:domnode")
michael@0 775 }
michael@0 776 }
michael@0 777
michael@0 778 /**
michael@0 779 * Server side of the DOM walker.
michael@0 780 */
michael@0 781 var WalkerActor = protocol.ActorClass({
michael@0 782 typeName: "domwalker",
michael@0 783
michael@0 784 events: {
michael@0 785 "new-mutations" : {
michael@0 786 type: "newMutations"
michael@0 787 },
michael@0 788 "picker-node-picked" : {
michael@0 789 type: "pickerNodePicked",
michael@0 790 node: Arg(0, "disconnectedNode")
michael@0 791 },
michael@0 792 "picker-node-hovered" : {
michael@0 793 type: "pickerNodeHovered",
michael@0 794 node: Arg(0, "disconnectedNode")
michael@0 795 },
michael@0 796 "highlighter-ready" : {
michael@0 797 type: "highlighter-ready"
michael@0 798 },
michael@0 799 "highlighter-hide" : {
michael@0 800 type: "highlighter-hide"
michael@0 801 }
michael@0 802 },
michael@0 803
michael@0 804 /**
michael@0 805 * Create the WalkerActor
michael@0 806 * @param DebuggerServerConnection conn
michael@0 807 * The server connection.
michael@0 808 */
michael@0 809 initialize: function(conn, tabActor, options) {
michael@0 810 protocol.Actor.prototype.initialize.call(this, conn);
michael@0 811 this.tabActor = tabActor;
michael@0 812 this.rootWin = tabActor.window;
michael@0 813 this.rootDoc = this.rootWin.document;
michael@0 814 this._refMap = new Map();
michael@0 815 this._pendingMutations = [];
michael@0 816 this._activePseudoClassLocks = new Set();
michael@0 817
michael@0 818 this.layoutHelpers = new LayoutHelpers(this.rootWin);
michael@0 819
michael@0 820 // Nodes which have been removed from the client's known
michael@0 821 // ownership tree are considered "orphaned", and stored in
michael@0 822 // this set.
michael@0 823 this._orphaned = new Set();
michael@0 824
michael@0 825 // The client can tell the walker that it is interested in a node
michael@0 826 // even when it is orphaned with the `retainNode` method. This
michael@0 827 // list contains orphaned nodes that were so retained.
michael@0 828 this._retainedOrphans = new Set();
michael@0 829
michael@0 830 this.onMutations = this.onMutations.bind(this);
michael@0 831 this.onFrameLoad = this.onFrameLoad.bind(this);
michael@0 832 this.onFrameUnload = this.onFrameUnload.bind(this);
michael@0 833
michael@0 834 events.on(tabActor, "will-navigate", this.onFrameUnload);
michael@0 835 events.on(tabActor, "navigate", this.onFrameLoad);
michael@0 836
michael@0 837 // Ensure that the root document node actor is ready and
michael@0 838 // managed.
michael@0 839 this.rootNode = this.document();
michael@0 840 },
michael@0 841
michael@0 842 // Returns the JSON representation of this object over the wire.
michael@0 843 form: function() {
michael@0 844 return {
michael@0 845 actor: this.actorID,
michael@0 846 root: this.rootNode.form()
michael@0 847 }
michael@0 848 },
michael@0 849
michael@0 850 toString: function() {
michael@0 851 return "[WalkerActor " + this.actorID + "]";
michael@0 852 },
michael@0 853
michael@0 854 destroy: function() {
michael@0 855 this._hoveredNode = null;
michael@0 856 this.clearPseudoClassLocks();
michael@0 857 this._activePseudoClassLocks = null;
michael@0 858 this.rootDoc = null;
michael@0 859 events.emit(this, "destroyed");
michael@0 860 protocol.Actor.prototype.destroy.call(this);
michael@0 861 },
michael@0 862
michael@0 863 release: method(function() {}, { release: true }),
michael@0 864
michael@0 865 unmanage: function(actor) {
michael@0 866 if (actor instanceof NodeActor) {
michael@0 867 if (this._activePseudoClassLocks &&
michael@0 868 this._activePseudoClassLocks.has(actor)) {
michael@0 869 this.clearPsuedoClassLocks(actor);
michael@0 870 }
michael@0 871 this._refMap.delete(actor.rawNode);
michael@0 872 }
michael@0 873 protocol.Actor.prototype.unmanage.call(this, actor);
michael@0 874 },
michael@0 875
michael@0 876 _ref: function(node) {
michael@0 877 let actor = this._refMap.get(node);
michael@0 878 if (actor) return actor;
michael@0 879
michael@0 880 actor = new NodeActor(this, node);
michael@0 881
michael@0 882 // Add the node actor as a child of this walker actor, assigning
michael@0 883 // it an actorID.
michael@0 884 this.manage(actor);
michael@0 885 this._refMap.set(node, actor);
michael@0 886
michael@0 887 if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
michael@0 888 this._watchDocument(actor);
michael@0 889 }
michael@0 890 return actor;
michael@0 891 },
michael@0 892
michael@0 893 /**
michael@0 894 * This is kept for backward-compatibility reasons with older remote targets.
michael@0 895 * Targets prior to bug 916443.
michael@0 896 *
michael@0 897 * pick/cancelPick are used to pick a node on click on the content
michael@0 898 * document. But in their implementation prior to bug 916443, they don't allow
michael@0 899 * highlighting on hover.
michael@0 900 * The client-side now uses the highlighter actor's pick and cancelPick
michael@0 901 * methods instead. The client-side uses the the highlightable trait found in
michael@0 902 * the root actor to determine which version of pick to use.
michael@0 903 *
michael@0 904 * As for highlight, the new highlighter actor is used instead of the walker's
michael@0 905 * highlight method. Same here though, the client-side uses the highlightable
michael@0 906 * trait to dertermine which to use.
michael@0 907 *
michael@0 908 * Keeping these actor methods for now allows newer client-side debuggers to
michael@0 909 * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
michael@0 910 */
michael@0 911 pick: method(function() {}, {request: {}, response: RetVal("disconnectedNode")}),
michael@0 912 cancelPick: method(function() {}),
michael@0 913 highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}),
michael@0 914
michael@0 915 attachElement: function(node) {
michael@0 916 node = this._ref(node);
michael@0 917 let newParents = this.ensurePathToRoot(node);
michael@0 918 return {
michael@0 919 node: node,
michael@0 920 newParents: [parent for (parent of newParents)]
michael@0 921 };
michael@0 922 },
michael@0 923
michael@0 924 /**
michael@0 925 * Watch the given document node for mutations using the DOM observer
michael@0 926 * API.
michael@0 927 */
michael@0 928 _watchDocument: function(actor) {
michael@0 929 let node = actor.rawNode;
michael@0 930 // Create the observer on the node's actor. The node will make sure
michael@0 931 // the observer is cleaned up when the actor is released.
michael@0 932 actor.observer = new actor.rawNode.defaultView.MutationObserver(this.onMutations);
michael@0 933 actor.observer.observe(node, {
michael@0 934 attributes: true,
michael@0 935 characterData: true,
michael@0 936 childList: true,
michael@0 937 subtree: true
michael@0 938 });
michael@0 939 },
michael@0 940
michael@0 941 /**
michael@0 942 * Return the document node that contains the given node,
michael@0 943 * or the root node if no node is specified.
michael@0 944 * @param NodeActor node
michael@0 945 * The node whose document is needed, or null to
michael@0 946 * return the root.
michael@0 947 */
michael@0 948 document: method(function(node) {
michael@0 949 let doc = node ? nodeDocument(node.rawNode) : this.rootDoc;
michael@0 950 return this._ref(doc);
michael@0 951 }, {
michael@0 952 request: { node: Arg(0, "nullable:domnode") },
michael@0 953 response: { node: RetVal("domnode") },
michael@0 954 }),
michael@0 955
michael@0 956 /**
michael@0 957 * Return the documentElement for the document containing the
michael@0 958 * given node.
michael@0 959 * @param NodeActor node
michael@0 960 * The node whose documentElement is requested, or null
michael@0 961 * to use the root document.
michael@0 962 */
michael@0 963 documentElement: method(function(node) {
michael@0 964 let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement;
michael@0 965 return this._ref(elt);
michael@0 966 }, {
michael@0 967 request: { node: Arg(0, "nullable:domnode") },
michael@0 968 response: { node: RetVal("domnode") },
michael@0 969 }),
michael@0 970
michael@0 971 /**
michael@0 972 * Return all parents of the given node, ordered from immediate parent
michael@0 973 * to root.
michael@0 974 * @param NodeActor node
michael@0 975 * The node whose parents are requested.
michael@0 976 * @param object options
michael@0 977 * Named options, including:
michael@0 978 * `sameDocument`: If true, parents will be restricted to the same
michael@0 979 * document as the node.
michael@0 980 */
michael@0 981 parents: method(function(node, options={}) {
michael@0 982 let walker = documentWalker(node.rawNode, this.rootWin);
michael@0 983 let parents = [];
michael@0 984 let cur;
michael@0 985 while((cur = walker.parentNode())) {
michael@0 986 if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) {
michael@0 987 break;
michael@0 988 }
michael@0 989 parents.push(this._ref(cur));
michael@0 990 }
michael@0 991 return parents;
michael@0 992 }, {
michael@0 993 request: {
michael@0 994 node: Arg(0, "domnode"),
michael@0 995 sameDocument: Option(1)
michael@0 996 },
michael@0 997 response: {
michael@0 998 nodes: RetVal("array:domnode")
michael@0 999 },
michael@0 1000 }),
michael@0 1001
michael@0 1002 parentNode: function(node) {
michael@0 1003 let walker = documentWalker(node.rawNode, this.rootWin);
michael@0 1004 let parent = walker.parentNode();
michael@0 1005 if (parent) {
michael@0 1006 return this._ref(parent);
michael@0 1007 }
michael@0 1008 return null;
michael@0 1009 },
michael@0 1010
michael@0 1011 /**
michael@0 1012 * Mark a node as 'retained'.
michael@0 1013 *
michael@0 1014 * A retained node is not released when `releaseNode` is called on its
michael@0 1015 * parent, or when a parent is released with the `cleanup` option to
michael@0 1016 * `getMutations`.
michael@0 1017 *
michael@0 1018 * When a retained node's parent is released, a retained mode is added to
michael@0 1019 * the walker's "retained orphans" list.
michael@0 1020 *
michael@0 1021 * Retained nodes can be deleted by providing the `force` option to
michael@0 1022 * `releaseNode`. They will also be released when their document
michael@0 1023 * has been destroyed.
michael@0 1024 *
michael@0 1025 * Retaining a node makes no promise about its children; They can
michael@0 1026 * still be removed by normal means.
michael@0 1027 */
michael@0 1028 retainNode: method(function(node) {
michael@0 1029 node.retained = true;
michael@0 1030 }, {
michael@0 1031 request: { node: Arg(0, "domnode") },
michael@0 1032 response: {}
michael@0 1033 }),
michael@0 1034
michael@0 1035 /**
michael@0 1036 * Remove the 'retained' mark from a node. If the node was a
michael@0 1037 * retained orphan, release it.
michael@0 1038 */
michael@0 1039 unretainNode: method(function(node) {
michael@0 1040 node.retained = false;
michael@0 1041 if (this._retainedOrphans.has(node)) {
michael@0 1042 this._retainedOrphans.delete(node);
michael@0 1043 this.releaseNode(node);
michael@0 1044 }
michael@0 1045 }, {
michael@0 1046 request: { node: Arg(0, "domnode") },
michael@0 1047 response: {},
michael@0 1048 }),
michael@0 1049
michael@0 1050 /**
michael@0 1051 * Release actors for a node and all child nodes.
michael@0 1052 */
michael@0 1053 releaseNode: method(function(node, options={}) {
michael@0 1054 if (node.retained && !options.force) {
michael@0 1055 this._retainedOrphans.add(node);
michael@0 1056 return;
michael@0 1057 }
michael@0 1058
michael@0 1059 if (node.retained) {
michael@0 1060 // Forcing a retained node to go away.
michael@0 1061 this._retainedOrphans.delete(node);
michael@0 1062 }
michael@0 1063
michael@0 1064 let walker = documentWalker(node.rawNode, this.rootWin);
michael@0 1065
michael@0 1066 let child = walker.firstChild();
michael@0 1067 while (child) {
michael@0 1068 let childActor = this._refMap.get(child);
michael@0 1069 if (childActor) {
michael@0 1070 this.releaseNode(childActor, options);
michael@0 1071 }
michael@0 1072 child = walker.nextSibling();
michael@0 1073 }
michael@0 1074
michael@0 1075 node.destroy();
michael@0 1076 }, {
michael@0 1077 request: {
michael@0 1078 node: Arg(0, "domnode"),
michael@0 1079 force: Option(1)
michael@0 1080 }
michael@0 1081 }),
michael@0 1082
michael@0 1083 /**
michael@0 1084 * Add any nodes between `node` and the walker's root node that have not
michael@0 1085 * yet been seen by the client.
michael@0 1086 */
michael@0 1087 ensurePathToRoot: function(node, newParents=new Set()) {
michael@0 1088 if (!node) {
michael@0 1089 return newParents;
michael@0 1090 }
michael@0 1091 let walker = documentWalker(node.rawNode, this.rootWin);
michael@0 1092 let cur;
michael@0 1093 while ((cur = walker.parentNode())) {
michael@0 1094 let parent = this._refMap.get(cur);
michael@0 1095 if (!parent) {
michael@0 1096 // This parent didn't exist, so hasn't been seen by the client yet.
michael@0 1097 newParents.add(this._ref(cur));
michael@0 1098 } else {
michael@0 1099 // This parent did exist, so the client knows about it.
michael@0 1100 return newParents;
michael@0 1101 }
michael@0 1102 }
michael@0 1103 return newParents;
michael@0 1104 },
michael@0 1105
michael@0 1106 /**
michael@0 1107 * Return children of the given node. By default this method will return
michael@0 1108 * all children of the node, but there are options that can restrict this
michael@0 1109 * to a more manageable subset.
michael@0 1110 *
michael@0 1111 * @param NodeActor node
michael@0 1112 * The node whose children you're curious about.
michael@0 1113 * @param object options
michael@0 1114 * Named options:
michael@0 1115 * `maxNodes`: The set of nodes returned by the method will be no longer
michael@0 1116 * than maxNodes.
michael@0 1117 * `start`: If a node is specified, the list of nodes will start
michael@0 1118 * with the given child. Mutally exclusive with `center`.
michael@0 1119 * `center`: If a node is specified, the given node will be as centered
michael@0 1120 * as possible in the list, given how close to the ends of the child
michael@0 1121 * list it is. Mutually exclusive with `start`.
michael@0 1122 * `whatToShow`: A bitmask of node types that should be included. See
michael@0 1123 * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
michael@0 1124 *
michael@0 1125 * @returns an object with three items:
michael@0 1126 * hasFirst: true if the first child of the node is included in the list.
michael@0 1127 * hasLast: true if the last child of the node is included in the list.
michael@0 1128 * nodes: Child nodes returned by the request.
michael@0 1129 */
michael@0 1130 children: method(function(node, options={}) {
michael@0 1131 if (options.center && options.start) {
michael@0 1132 throw Error("Can't specify both 'center' and 'start' options.");
michael@0 1133 }
michael@0 1134 let maxNodes = options.maxNodes || -1;
michael@0 1135 if (maxNodes == -1) {
michael@0 1136 maxNodes = Number.MAX_VALUE;
michael@0 1137 }
michael@0 1138
michael@0 1139 // We're going to create a few document walkers with the same filter,
michael@0 1140 // make it easier.
michael@0 1141 let filteredWalker = (node) => {
michael@0 1142 return documentWalker(node, this.rootWin, options.whatToShow);
michael@0 1143 };
michael@0 1144
michael@0 1145 // Need to know the first and last child.
michael@0 1146 let rawNode = node.rawNode;
michael@0 1147 let firstChild = filteredWalker(rawNode).firstChild();
michael@0 1148 let lastChild = filteredWalker(rawNode).lastChild();
michael@0 1149
michael@0 1150 if (!firstChild) {
michael@0 1151 // No children, we're done.
michael@0 1152 return { hasFirst: true, hasLast: true, nodes: [] };
michael@0 1153 }
michael@0 1154
michael@0 1155 let start;
michael@0 1156 if (options.center) {
michael@0 1157 start = options.center.rawNode;
michael@0 1158 } else if (options.start) {
michael@0 1159 start = options.start.rawNode;
michael@0 1160 } else {
michael@0 1161 start = firstChild;
michael@0 1162 }
michael@0 1163
michael@0 1164 let nodes = [];
michael@0 1165
michael@0 1166 // Start by reading backward from the starting point if we're centering...
michael@0 1167 let backwardWalker = filteredWalker(start);
michael@0 1168 if (start != firstChild && options.center) {
michael@0 1169 backwardWalker.previousSibling();
michael@0 1170 let backwardCount = Math.floor(maxNodes / 2);
michael@0 1171 let backwardNodes = this._readBackward(backwardWalker, backwardCount);
michael@0 1172 nodes = backwardNodes;
michael@0 1173 }
michael@0 1174
michael@0 1175 // Then read forward by any slack left in the max children...
michael@0 1176 let forwardWalker = filteredWalker(start);
michael@0 1177 let forwardCount = maxNodes - nodes.length;
michael@0 1178 nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
michael@0 1179
michael@0 1180 // If there's any room left, it means we've run all the way to the end.
michael@0 1181 // If we're centering, check if there are more items to read at the front.
michael@0 1182 let remaining = maxNodes - nodes.length;
michael@0 1183 if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
michael@0 1184 let firstNodes = this._readBackward(backwardWalker, remaining);
michael@0 1185
michael@0 1186 // Then put it all back together.
michael@0 1187 nodes = firstNodes.concat(nodes);
michael@0 1188 }
michael@0 1189
michael@0 1190 return {
michael@0 1191 hasFirst: nodes[0].rawNode == firstChild,
michael@0 1192 hasLast: nodes[nodes.length - 1].rawNode == lastChild,
michael@0 1193 nodes: nodes
michael@0 1194 };
michael@0 1195 }, nodeArrayMethod),
michael@0 1196
michael@0 1197 /**
michael@0 1198 * Return siblings of the given node. By default this method will return
michael@0 1199 * all siblings of the node, but there are options that can restrict this
michael@0 1200 * to a more manageable subset.
michael@0 1201 *
michael@0 1202 * If `start` or `center` are not specified, this method will center on the
michael@0 1203 * node whose siblings are requested.
michael@0 1204 *
michael@0 1205 * @param NodeActor node
michael@0 1206 * The node whose children you're curious about.
michael@0 1207 * @param object options
michael@0 1208 * Named options:
michael@0 1209 * `maxNodes`: The set of nodes returned by the method will be no longer
michael@0 1210 * than maxNodes.
michael@0 1211 * `start`: If a node is specified, the list of nodes will start
michael@0 1212 * with the given child. Mutally exclusive with `center`.
michael@0 1213 * `center`: If a node is specified, the given node will be as centered
michael@0 1214 * as possible in the list, given how close to the ends of the child
michael@0 1215 * list it is. Mutually exclusive with `start`.
michael@0 1216 * `whatToShow`: A bitmask of node types that should be included. See
michael@0 1217 * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
michael@0 1218 *
michael@0 1219 * @returns an object with three items:
michael@0 1220 * hasFirst: true if the first child of the node is included in the list.
michael@0 1221 * hasLast: true if the last child of the node is included in the list.
michael@0 1222 * nodes: Child nodes returned by the request.
michael@0 1223 */
michael@0 1224 siblings: method(function(node, options={}) {
michael@0 1225 let parentNode = documentWalker(node.rawNode, this.rootWin).parentNode();
michael@0 1226 if (!parentNode) {
michael@0 1227 return {
michael@0 1228 hasFirst: true,
michael@0 1229 hasLast: true,
michael@0 1230 nodes: [node]
michael@0 1231 };
michael@0 1232 }
michael@0 1233
michael@0 1234 if (!(options.start || options.center)) {
michael@0 1235 options.center = node;
michael@0 1236 }
michael@0 1237
michael@0 1238 return this.children(this._ref(parentNode), options);
michael@0 1239 }, nodeArrayMethod),
michael@0 1240
michael@0 1241 /**
michael@0 1242 * Get the next sibling of a given node. Getting nodes one at a time
michael@0 1243 * might be inefficient, be careful.
michael@0 1244 *
michael@0 1245 * @param object options
michael@0 1246 * Named options:
michael@0 1247 * `whatToShow`: A bitmask of node types that should be included. See
michael@0 1248 * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
michael@0 1249 */
michael@0 1250 nextSibling: method(function(node, options={}) {
michael@0 1251 let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
michael@0 1252 let sibling = walker.nextSibling();
michael@0 1253 return sibling ? this._ref(sibling) : null;
michael@0 1254 }, traversalMethod),
michael@0 1255
michael@0 1256 /**
michael@0 1257 * Get the previous sibling of a given node. Getting nodes one at a time
michael@0 1258 * might be inefficient, be careful.
michael@0 1259 *
michael@0 1260 * @param object options
michael@0 1261 * Named options:
michael@0 1262 * `whatToShow`: A bitmask of node types that should be included. See
michael@0 1263 * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
michael@0 1264 */
michael@0 1265 previousSibling: method(function(node, options={}) {
michael@0 1266 let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
michael@0 1267 let sibling = walker.previousSibling();
michael@0 1268 return sibling ? this._ref(sibling) : null;
michael@0 1269 }, traversalMethod),
michael@0 1270
michael@0 1271 /**
michael@0 1272 * Helper function for the `children` method: Read forward in the sibling
michael@0 1273 * list into an array with `count` items, including the current node.
michael@0 1274 */
michael@0 1275 _readForward: function(walker, count) {
michael@0 1276 let ret = [];
michael@0 1277 let node = walker.currentNode;
michael@0 1278 do {
michael@0 1279 ret.push(this._ref(node));
michael@0 1280 node = walker.nextSibling();
michael@0 1281 } while (node && --count);
michael@0 1282 return ret;
michael@0 1283 },
michael@0 1284
michael@0 1285 /**
michael@0 1286 * Helper function for the `children` method: Read backward in the sibling
michael@0 1287 * list into an array with `count` items, including the current node.
michael@0 1288 */
michael@0 1289 _readBackward: function(walker, count) {
michael@0 1290 let ret = [];
michael@0 1291 let node = walker.currentNode;
michael@0 1292 do {
michael@0 1293 ret.push(this._ref(node));
michael@0 1294 node = walker.previousSibling();
michael@0 1295 } while(node && --count);
michael@0 1296 ret.reverse();
michael@0 1297 return ret;
michael@0 1298 },
michael@0 1299
michael@0 1300 /**
michael@0 1301 * Return the first node in the document that matches the given selector.
michael@0 1302 * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
michael@0 1303 *
michael@0 1304 * @param NodeActor baseNode
michael@0 1305 * @param string selector
michael@0 1306 */
michael@0 1307 querySelector: method(function(baseNode, selector) {
michael@0 1308 if (!baseNode) {
michael@0 1309 return {}
michael@0 1310 };
michael@0 1311 let node = baseNode.rawNode.querySelector(selector);
michael@0 1312
michael@0 1313 if (!node) {
michael@0 1314 return {}
michael@0 1315 };
michael@0 1316
michael@0 1317 return this.attachElement(node);
michael@0 1318 }, {
michael@0 1319 request: {
michael@0 1320 node: Arg(0, "domnode"),
michael@0 1321 selector: Arg(1)
michael@0 1322 },
michael@0 1323 response: RetVal("disconnectedNode")
michael@0 1324 }),
michael@0 1325
michael@0 1326 /**
michael@0 1327 * Return a NodeListActor with all nodes that match the given selector.
michael@0 1328 * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
michael@0 1329 *
michael@0 1330 * @param NodeActor baseNode
michael@0 1331 * @param string selector
michael@0 1332 */
michael@0 1333 querySelectorAll: method(function(baseNode, selector) {
michael@0 1334 let nodeList = null;
michael@0 1335
michael@0 1336 try {
michael@0 1337 nodeList = baseNode.rawNode.querySelectorAll(selector);
michael@0 1338 } catch(e) {
michael@0 1339 // Bad selector. Do nothing as the selector can come from a searchbox.
michael@0 1340 }
michael@0 1341
michael@0 1342 return new NodeListActor(this, nodeList);
michael@0 1343 }, {
michael@0 1344 request: {
michael@0 1345 node: Arg(0, "domnode"),
michael@0 1346 selector: Arg(1)
michael@0 1347 },
michael@0 1348 response: {
michael@0 1349 list: RetVal("domnodelist")
michael@0 1350 }
michael@0 1351 }),
michael@0 1352
michael@0 1353 /**
michael@0 1354 * Returns a list of matching results for CSS selector autocompletion.
michael@0 1355 *
michael@0 1356 * @param string query
michael@0 1357 * The selector query being completed
michael@0 1358 * @param string completing
michael@0 1359 * The exact token being completed out of the query
michael@0 1360 * @param string selectorState
michael@0 1361 * One of "pseudo", "id", "tag", "class", "null"
michael@0 1362 */
michael@0 1363 getSuggestionsForQuery: method(function(query, completing, selectorState) {
michael@0 1364 let sugs = {
michael@0 1365 classes: new Map,
michael@0 1366 tags: new Map
michael@0 1367 };
michael@0 1368 let result = [];
michael@0 1369 let nodes = null;
michael@0 1370 // Filtering and sorting the results so that protocol transfer is miminal.
michael@0 1371 switch (selectorState) {
michael@0 1372 case "pseudo":
michael@0 1373 result = PSEUDO_SELECTORS.filter(item => {
michael@0 1374 return item[0].startsWith(":" + completing);
michael@0 1375 });
michael@0 1376 break;
michael@0 1377
michael@0 1378 case "class":
michael@0 1379 if (!query) {
michael@0 1380 nodes = this.rootDoc.querySelectorAll("[class]");
michael@0 1381 }
michael@0 1382 else {
michael@0 1383 nodes = this.rootDoc.querySelectorAll(query);
michael@0 1384 }
michael@0 1385 for (let node of nodes) {
michael@0 1386 for (let className of node.className.split(" ")) {
michael@0 1387 sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
michael@0 1388 }
michael@0 1389 }
michael@0 1390 sugs.classes.delete("");
michael@0 1391 // Editing the style editor may make the stylesheet have errors and
michael@0 1392 // thus the page's elements' styles start changing with a transition.
michael@0 1393 // That transition comes from the `moz-styleeditor-transitioning` class.
michael@0 1394 sugs.classes.delete("moz-styleeditor-transitioning");
michael@0 1395 sugs.classes.delete(HIDDEN_CLASS);
michael@0 1396 for (let [className, count] of sugs.classes) {
michael@0 1397 if (className.startsWith(completing)) {
michael@0 1398 result.push(["." + className, count]);
michael@0 1399 }
michael@0 1400 }
michael@0 1401 break;
michael@0 1402
michael@0 1403 case "id":
michael@0 1404 if (!query) {
michael@0 1405 nodes = this.rootDoc.querySelectorAll("[id]");
michael@0 1406 }
michael@0 1407 else {
michael@0 1408 nodes = this.rootDoc.querySelectorAll(query);
michael@0 1409 }
michael@0 1410 for (let node of nodes) {
michael@0 1411 if (node.id.startsWith(completing)) {
michael@0 1412 result.push(["#" + node.id, 1]);
michael@0 1413 }
michael@0 1414 }
michael@0 1415 break;
michael@0 1416
michael@0 1417 case "tag":
michael@0 1418 if (!query) {
michael@0 1419 nodes = this.rootDoc.getElementsByTagName("*");
michael@0 1420 }
michael@0 1421 else {
michael@0 1422 nodes = this.rootDoc.querySelectorAll(query);
michael@0 1423 }
michael@0 1424 for (let node of nodes) {
michael@0 1425 let tag = node.tagName.toLowerCase();
michael@0 1426 sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
michael@0 1427 }
michael@0 1428 for (let [tag, count] of sugs.tags) {
michael@0 1429 if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
michael@0 1430 result.push([tag, count]);
michael@0 1431 }
michael@0 1432 }
michael@0 1433 break;
michael@0 1434
michael@0 1435 case "null":
michael@0 1436 nodes = this.rootDoc.querySelectorAll(query);
michael@0 1437 for (let node of nodes) {
michael@0 1438 node.id && result.push(["#" + node.id, 1]);
michael@0 1439 let tag = node.tagName.toLowerCase();
michael@0 1440 sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
michael@0 1441 for (let className of node.className.split(" ")) {
michael@0 1442 sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
michael@0 1443 }
michael@0 1444 }
michael@0 1445 for (let [tag, count] of sugs.tags) {
michael@0 1446 tag && result.push([tag, count]);
michael@0 1447 }
michael@0 1448 sugs.classes.delete("");
michael@0 1449 // Editing the style editor may make the stylesheet have errors and
michael@0 1450 // thus the page's elements' styles start changing with a transition.
michael@0 1451 // That transition comes from the `moz-styleeditor-transitioning` class.
michael@0 1452 sugs.classes.delete("moz-styleeditor-transitioning");
michael@0 1453 sugs.classes.delete(HIDDEN_CLASS);
michael@0 1454 for (let [className, count] of sugs.classes) {
michael@0 1455 className && result.push(["." + className, count]);
michael@0 1456 }
michael@0 1457 }
michael@0 1458
michael@0 1459 // Sort alphabetically in increaseing order.
michael@0 1460 result = result.sort();
michael@0 1461 // Sort based on count in decreasing order.
michael@0 1462 result = result.sort(function(a, b) {
michael@0 1463 return b[1] - a[1];
michael@0 1464 });
michael@0 1465
michael@0 1466 result.slice(0, 25);
michael@0 1467
michael@0 1468 return {
michael@0 1469 query: query,
michael@0 1470 suggestions: result
michael@0 1471 };
michael@0 1472 }, {
michael@0 1473 request: {
michael@0 1474 query: Arg(0),
michael@0 1475 completing: Arg(1),
michael@0 1476 selectorState: Arg(2)
michael@0 1477 },
michael@0 1478 response: {
michael@0 1479 list: RetVal("array:array:string")
michael@0 1480 }
michael@0 1481 }),
michael@0 1482
michael@0 1483 /**
michael@0 1484 * Add a pseudo-class lock to a node.
michael@0 1485 *
michael@0 1486 * @param NodeActor node
michael@0 1487 * @param string pseudo
michael@0 1488 * A pseudoclass: ':hover', ':active', ':focus'
michael@0 1489 * @param options
michael@0 1490 * Options object:
michael@0 1491 * `parents`: True if the pseudo-class should be added
michael@0 1492 * to parent nodes.
michael@0 1493 *
michael@0 1494 * @returns An empty packet. A "pseudoClassLock" mutation will
michael@0 1495 * be queued for any changed nodes.
michael@0 1496 */
michael@0 1497 addPseudoClassLock: method(function(node, pseudo, options={}) {
michael@0 1498 this._addPseudoClassLock(node, pseudo);
michael@0 1499
michael@0 1500 if (!options.parents) {
michael@0 1501 return;
michael@0 1502 }
michael@0 1503
michael@0 1504 let walker = documentWalker(node.rawNode, this.rootWin);
michael@0 1505 let cur;
michael@0 1506 while ((cur = walker.parentNode())) {
michael@0 1507 let curNode = this._ref(cur);
michael@0 1508 this._addPseudoClassLock(curNode, pseudo);
michael@0 1509 }
michael@0 1510 }, {
michael@0 1511 request: {
michael@0 1512 node: Arg(0, "domnode"),
michael@0 1513 pseudoClass: Arg(1),
michael@0 1514 parents: Option(2)
michael@0 1515 },
michael@0 1516 response: {}
michael@0 1517 }),
michael@0 1518
michael@0 1519 _queuePseudoClassMutation: function(node) {
michael@0 1520 this.queueMutation({
michael@0 1521 target: node.actorID,
michael@0 1522 type: "pseudoClassLock",
michael@0 1523 pseudoClassLocks: node.writePseudoClassLocks()
michael@0 1524 });
michael@0 1525 },
michael@0 1526
michael@0 1527 _addPseudoClassLock: function(node, pseudo) {
michael@0 1528 if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 1529 return false;
michael@0 1530 }
michael@0 1531 DOMUtils.addPseudoClassLock(node.rawNode, pseudo);
michael@0 1532 this._activePseudoClassLocks.add(node);
michael@0 1533 this._queuePseudoClassMutation(node);
michael@0 1534 return true;
michael@0 1535 },
michael@0 1536
michael@0 1537 _installHelperSheet: function(node) {
michael@0 1538 if (!this.installedHelpers) {
michael@0 1539 this.installedHelpers = new WeakMap;
michael@0 1540 }
michael@0 1541 let win = node.rawNode.ownerDocument.defaultView;
michael@0 1542 if (!this.installedHelpers.has(win)) {
michael@0 1543 let { Style } = require("sdk/stylesheet/style");
michael@0 1544 let { attach } = require("sdk/content/mod");
michael@0 1545 let style = Style({source: HELPER_SHEET, type: "agent" });
michael@0 1546 attach(style, win);
michael@0 1547 this.installedHelpers.set(win, style);
michael@0 1548 }
michael@0 1549 },
michael@0 1550
michael@0 1551 hideNode: method(function(node) {
michael@0 1552 this._installHelperSheet(node);
michael@0 1553 node.rawNode.classList.add(HIDDEN_CLASS);
michael@0 1554 }, {
michael@0 1555 request: { node: Arg(0, "domnode") }
michael@0 1556 }),
michael@0 1557
michael@0 1558 unhideNode: method(function(node) {
michael@0 1559 node.rawNode.classList.remove(HIDDEN_CLASS);
michael@0 1560 }, {
michael@0 1561 request: { node: Arg(0, "domnode") }
michael@0 1562 }),
michael@0 1563
michael@0 1564 /**
michael@0 1565 * Remove a pseudo-class lock from a node.
michael@0 1566 *
michael@0 1567 * @param NodeActor node
michael@0 1568 * @param string pseudo
michael@0 1569 * A pseudoclass: ':hover', ':active', ':focus'
michael@0 1570 * @param options
michael@0 1571 * Options object:
michael@0 1572 * `parents`: True if the pseudo-class should be removed
michael@0 1573 * from parent nodes.
michael@0 1574 *
michael@0 1575 * @returns An empty response. "pseudoClassLock" mutations
michael@0 1576 * will be emitted for any changed nodes.
michael@0 1577 */
michael@0 1578 removePseudoClassLock: method(function(node, pseudo, options={}) {
michael@0 1579 this._removePseudoClassLock(node, pseudo);
michael@0 1580
michael@0 1581 if (!options.parents) {
michael@0 1582 return;
michael@0 1583 }
michael@0 1584
michael@0 1585 let walker = documentWalker(node.rawNode, this.rootWin);
michael@0 1586 let cur;
michael@0 1587 while ((cur = walker.parentNode())) {
michael@0 1588 let curNode = this._ref(cur);
michael@0 1589 this._removePseudoClassLock(curNode, pseudo);
michael@0 1590 }
michael@0 1591 }, {
michael@0 1592 request: {
michael@0 1593 node: Arg(0, "domnode"),
michael@0 1594 pseudoClass: Arg(1),
michael@0 1595 parents: Option(2)
michael@0 1596 },
michael@0 1597 response: {}
michael@0 1598 }),
michael@0 1599
michael@0 1600 _removePseudoClassLock: function(node, pseudo) {
michael@0 1601 if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 1602 return false;
michael@0 1603 }
michael@0 1604 DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
michael@0 1605 if (!node.writePseudoClassLocks()) {
michael@0 1606 this._activePseudoClassLocks.delete(node);
michael@0 1607 }
michael@0 1608 this._queuePseudoClassMutation(node);
michael@0 1609 return true;
michael@0 1610 },
michael@0 1611
michael@0 1612 /**
michael@0 1613 * Clear all the pseudo-classes on a given node
michael@0 1614 * or all nodes.
michael@0 1615 */
michael@0 1616 clearPseudoClassLocks: method(function(node) {
michael@0 1617 if (node) {
michael@0 1618 DOMUtils.clearPseudoClassLocks(node.rawNode);
michael@0 1619 this._activePseudoClassLocks.delete(node);
michael@0 1620 this._queuePseudoClassMutation(node);
michael@0 1621 } else {
michael@0 1622 for (let locked of this._activePseudoClassLocks) {
michael@0 1623 DOMUtils.clearPseudoClassLocks(locked.rawNode);
michael@0 1624 this._activePseudoClassLocks.delete(locked);
michael@0 1625 this._queuePseudoClassMutation(locked);
michael@0 1626 }
michael@0 1627 }
michael@0 1628 }, {
michael@0 1629 request: {
michael@0 1630 node: Arg(0, "nullable:domnode")
michael@0 1631 },
michael@0 1632 response: {}
michael@0 1633 }),
michael@0 1634
michael@0 1635 /**
michael@0 1636 * Get a node's innerHTML property.
michael@0 1637 */
michael@0 1638 innerHTML: method(function(node) {
michael@0 1639 return LongStringActor(this.conn, node.rawNode.innerHTML);
michael@0 1640 }, {
michael@0 1641 request: {
michael@0 1642 node: Arg(0, "domnode")
michael@0 1643 },
michael@0 1644 response: {
michael@0 1645 value: RetVal("longstring")
michael@0 1646 }
michael@0 1647 }),
michael@0 1648
michael@0 1649 /**
michael@0 1650 * Get a node's outerHTML property.
michael@0 1651 */
michael@0 1652 outerHTML: method(function(node) {
michael@0 1653 return LongStringActor(this.conn, node.rawNode.outerHTML);
michael@0 1654 }, {
michael@0 1655 request: {
michael@0 1656 node: Arg(0, "domnode")
michael@0 1657 },
michael@0 1658 response: {
michael@0 1659 value: RetVal("longstring")
michael@0 1660 }
michael@0 1661 }),
michael@0 1662
michael@0 1663 /**
michael@0 1664 * Set a node's outerHTML property.
michael@0 1665 */
michael@0 1666 setOuterHTML: method(function(node, value) {
michael@0 1667 let parsedDOM = DOMParser.parseFromString(value, "text/html");
michael@0 1668 let rawNode = node.rawNode;
michael@0 1669 let parentNode = rawNode.parentNode;
michael@0 1670
michael@0 1671 // Special case for head and body. Setting document.body.outerHTML
michael@0 1672 // creates an extra <head> tag, and document.head.outerHTML creates
michael@0 1673 // an extra <body>. So instead we will call replaceChild with the
michael@0 1674 // parsed DOM, assuming that they aren't trying to set both tags at once.
michael@0 1675 if (rawNode.tagName === "BODY") {
michael@0 1676 if (parsedDOM.head.innerHTML === "") {
michael@0 1677 parentNode.replaceChild(parsedDOM.body, rawNode);
michael@0 1678 } else {
michael@0 1679 rawNode.outerHTML = value;
michael@0 1680 }
michael@0 1681 } else if (rawNode.tagName === "HEAD") {
michael@0 1682 if (parsedDOM.body.innerHTML === "") {
michael@0 1683 parentNode.replaceChild(parsedDOM.head, rawNode);
michael@0 1684 } else {
michael@0 1685 rawNode.outerHTML = value;
michael@0 1686 }
michael@0 1687 } else if (node.isDocumentElement()) {
michael@0 1688 // Unable to set outerHTML on the document element. Fall back by
michael@0 1689 // setting attributes manually, then replace the body and head elements.
michael@0 1690 let finalAttributeModifications = [];
michael@0 1691 let attributeModifications = {};
michael@0 1692 for (let attribute of rawNode.attributes) {
michael@0 1693 attributeModifications[attribute.name] = null;
michael@0 1694 }
michael@0 1695 for (let attribute of parsedDOM.documentElement.attributes) {
michael@0 1696 attributeModifications[attribute.name] = attribute.value;
michael@0 1697 }
michael@0 1698 for (let key in attributeModifications) {
michael@0 1699 finalAttributeModifications.push({
michael@0 1700 attributeName: key,
michael@0 1701 newValue: attributeModifications[key]
michael@0 1702 });
michael@0 1703 }
michael@0 1704 node.modifyAttributes(finalAttributeModifications);
michael@0 1705 rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
michael@0 1706 rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
michael@0 1707 } else {
michael@0 1708 rawNode.outerHTML = value;
michael@0 1709 }
michael@0 1710 }, {
michael@0 1711 request: {
michael@0 1712 node: Arg(0, "domnode"),
michael@0 1713 value: Arg(1),
michael@0 1714 },
michael@0 1715 response: {
michael@0 1716 }
michael@0 1717 }),
michael@0 1718
michael@0 1719 /**
michael@0 1720 * Removes a node from its parent node.
michael@0 1721 *
michael@0 1722 * @returns The node's nextSibling before it was removed.
michael@0 1723 */
michael@0 1724 removeNode: method(function(node) {
michael@0 1725 if ((node.rawNode.ownerDocument &&
michael@0 1726 node.rawNode.ownerDocument.documentElement === this.rawNode) ||
michael@0 1727 node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
michael@0 1728 throw Error("Cannot remove document or document elements.");
michael@0 1729 }
michael@0 1730 let nextSibling = this.nextSibling(node);
michael@0 1731 if (node.rawNode.parentNode) {
michael@0 1732 node.rawNode.parentNode.removeChild(node.rawNode);
michael@0 1733 // Mutation events will take care of the rest.
michael@0 1734 }
michael@0 1735 return nextSibling;
michael@0 1736 }, {
michael@0 1737 request: {
michael@0 1738 node: Arg(0, "domnode")
michael@0 1739 },
michael@0 1740 response: {
michael@0 1741 nextSibling: RetVal("nullable:domnode")
michael@0 1742 }
michael@0 1743 }),
michael@0 1744
michael@0 1745 /**
michael@0 1746 * Insert a node into the DOM.
michael@0 1747 */
michael@0 1748 insertBefore: method(function(node, parent, sibling) {
michael@0 1749 parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null);
michael@0 1750 }, {
michael@0 1751 request: {
michael@0 1752 node: Arg(0, "domnode"),
michael@0 1753 parent: Arg(1, "domnode"),
michael@0 1754 sibling: Arg(2, "nullable:domnode")
michael@0 1755 },
michael@0 1756 response: {}
michael@0 1757 }),
michael@0 1758
michael@0 1759 /**
michael@0 1760 * Get any pending mutation records. Must be called by the client after
michael@0 1761 * the `new-mutations` notification is received. Returns an array of
michael@0 1762 * mutation records.
michael@0 1763 *
michael@0 1764 * Mutation records have a basic structure:
michael@0 1765 *
michael@0 1766 * {
michael@0 1767 * type: attributes|characterData|childList,
michael@0 1768 * target: <domnode actor ID>,
michael@0 1769 * }
michael@0 1770 *
michael@0 1771 * And additional attributes based on the mutation type:
michael@0 1772 *
michael@0 1773 * `attributes` type:
michael@0 1774 * attributeName: <string> - the attribute that changed
michael@0 1775 * attributeNamespace: <string> - the attribute's namespace URI, if any.
michael@0 1776 * newValue: <string> - The new value of the attribute, if any.
michael@0 1777 *
michael@0 1778 * `characterData` type:
michael@0 1779 * newValue: <string> - the new shortValue for the node
michael@0 1780 * [incompleteValue: true] - True if the shortValue was truncated.
michael@0 1781 *
michael@0 1782 * `childList` type is returned when the set of children for a node
michael@0 1783 * has changed. Includes extra data, which can be used by the client to
michael@0 1784 * maintain its ownership subtree.
michael@0 1785 *
michael@0 1786 * added: array of <domnode actor ID> - The list of actors *previously
michael@0 1787 * seen by the client* that were added to the target node.
michael@0 1788 * removed: array of <domnode actor ID> The list of actors *previously
michael@0 1789 * seen by the client* that were removed from the target node.
michael@0 1790 *
michael@0 1791 * Actors that are included in a MutationRecord's `removed` but
michael@0 1792 * not in an `added` have been removed from the client's ownership
michael@0 1793 * tree (either by being moved under a node the client has seen yet
michael@0 1794 * or by being removed from the tree entirely), and is considered
michael@0 1795 * 'orphaned'.
michael@0 1796 *
michael@0 1797 * Keep in mind that if a node that the client hasn't seen is moved
michael@0 1798 * into or out of the target node, it will not be included in the
michael@0 1799 * removedNodes and addedNodes list, so if the client is interested
michael@0 1800 * in the new set of children it needs to issue a `children` request.
michael@0 1801 */
michael@0 1802 getMutations: method(function(options={}) {
michael@0 1803 let pending = this._pendingMutations || [];
michael@0 1804 this._pendingMutations = [];
michael@0 1805
michael@0 1806 if (options.cleanup) {
michael@0 1807 for (let node of this._orphaned) {
michael@0 1808 // Release the orphaned node. Nodes or children that have been
michael@0 1809 // retained will be moved to this._retainedOrphans.
michael@0 1810 this.releaseNode(node);
michael@0 1811 }
michael@0 1812 this._orphaned = new Set();
michael@0 1813 }
michael@0 1814
michael@0 1815 return pending;
michael@0 1816 }, {
michael@0 1817 request: {
michael@0 1818 cleanup: Option(0)
michael@0 1819 },
michael@0 1820 response: {
michael@0 1821 mutations: RetVal("array:dommutation")
michael@0 1822 }
michael@0 1823 }),
michael@0 1824
michael@0 1825 queueMutation: function(mutation) {
michael@0 1826 if (!this.actorID) {
michael@0 1827 // We've been destroyed, don't bother queueing this mutation.
michael@0 1828 return;
michael@0 1829 }
michael@0 1830 // We only send the `new-mutations` notification once, until the client
michael@0 1831 // fetches mutations with the `getMutations` packet.
michael@0 1832 let needEvent = this._pendingMutations.length === 0;
michael@0 1833
michael@0 1834 this._pendingMutations.push(mutation);
michael@0 1835
michael@0 1836 if (needEvent) {
michael@0 1837 events.emit(this, "new-mutations");
michael@0 1838 }
michael@0 1839 },
michael@0 1840
michael@0 1841 /**
michael@0 1842 * Handles mutations from the DOM mutation observer API.
michael@0 1843 *
michael@0 1844 * @param array[MutationRecord] mutations
michael@0 1845 * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
michael@0 1846 */
michael@0 1847 onMutations: function(mutations) {
michael@0 1848 for (let change of mutations) {
michael@0 1849 let targetActor = this._refMap.get(change.target);
michael@0 1850 if (!targetActor) {
michael@0 1851 continue;
michael@0 1852 }
michael@0 1853 let targetNode = change.target;
michael@0 1854 let mutation = {
michael@0 1855 type: change.type,
michael@0 1856 target: targetActor.actorID,
michael@0 1857 }
michael@0 1858
michael@0 1859 if (mutation.type === "attributes") {
michael@0 1860 mutation.attributeName = change.attributeName;
michael@0 1861 mutation.attributeNamespace = change.attributeNamespace || undefined;
michael@0 1862 mutation.newValue = targetNode.getAttribute(mutation.attributeName);
michael@0 1863 } else if (mutation.type === "characterData") {
michael@0 1864 if (targetNode.nodeValue.length > gValueSummaryLength) {
michael@0 1865 mutation.newValue = targetNode.nodeValue.substring(0, gValueSummaryLength);
michael@0 1866 mutation.incompleteValue = true;
michael@0 1867 } else {
michael@0 1868 mutation.newValue = targetNode.nodeValue;
michael@0 1869 }
michael@0 1870 } else if (mutation.type === "childList") {
michael@0 1871 // Get the list of removed and added actors that the client has seen
michael@0 1872 // so that it can keep its ownership tree up to date.
michael@0 1873 let removedActors = [];
michael@0 1874 let addedActors = [];
michael@0 1875 for (let removed of change.removedNodes) {
michael@0 1876 let removedActor = this._refMap.get(removed);
michael@0 1877 if (!removedActor) {
michael@0 1878 // If the client never encountered this actor we don't need to
michael@0 1879 // mention that it was removed.
michael@0 1880 continue;
michael@0 1881 }
michael@0 1882 // While removed from the tree, nodes are saved as orphaned.
michael@0 1883 this._orphaned.add(removedActor);
michael@0 1884 removedActors.push(removedActor.actorID);
michael@0 1885 }
michael@0 1886 for (let added of change.addedNodes) {
michael@0 1887 let addedActor = this._refMap.get(added);
michael@0 1888 if (!addedActor) {
michael@0 1889 // If the client never encounted this actor we don't need to tell
michael@0 1890 // it about its addition for ownership tree purposes - if the
michael@0 1891 // client wants to see the new nodes it can ask for children.
michael@0 1892 continue;
michael@0 1893 }
michael@0 1894 // The actor is reconnected to the ownership tree, unorphan
michael@0 1895 // it and let the client know so that its ownership tree is up
michael@0 1896 // to date.
michael@0 1897 this._orphaned.delete(addedActor);
michael@0 1898 addedActors.push(addedActor.actorID);
michael@0 1899 }
michael@0 1900 mutation.numChildren = change.target.childNodes.length;
michael@0 1901 mutation.removed = removedActors;
michael@0 1902 mutation.added = addedActors;
michael@0 1903 }
michael@0 1904 this.queueMutation(mutation);
michael@0 1905 }
michael@0 1906 },
michael@0 1907
michael@0 1908 onFrameLoad: function({ window, isTopLevel }) {
michael@0 1909 if (!this.rootDoc && isTopLevel) {
michael@0 1910 this.rootDoc = window.document;
michael@0 1911 this.rootNode = this.document();
michael@0 1912 this.queueMutation({
michael@0 1913 type: "newRoot",
michael@0 1914 target: this.rootNode.form()
michael@0 1915 });
michael@0 1916 }
michael@0 1917 let frame = this.layoutHelpers.getFrameElement(window);
michael@0 1918 let frameActor = this._refMap.get(frame);
michael@0 1919 if (!frameActor) {
michael@0 1920 return;
michael@0 1921 }
michael@0 1922
michael@0 1923 this.queueMutation({
michael@0 1924 type: "frameLoad",
michael@0 1925 target: frameActor.actorID,
michael@0 1926 });
michael@0 1927
michael@0 1928 // Send a childList mutation on the frame.
michael@0 1929 this.queueMutation({
michael@0 1930 type: "childList",
michael@0 1931 target: frameActor.actorID,
michael@0 1932 added: [],
michael@0 1933 removed: []
michael@0 1934 })
michael@0 1935 },
michael@0 1936
michael@0 1937 // Returns true if domNode is in window or a subframe.
michael@0 1938 _childOfWindow: function(window, domNode) {
michael@0 1939 let win = nodeDocument(domNode).defaultView;
michael@0 1940 while (win) {
michael@0 1941 if (win === window) {
michael@0 1942 return true;
michael@0 1943 }
michael@0 1944 win = this.layoutHelpers.getFrameElement(win);
michael@0 1945 }
michael@0 1946 return false;
michael@0 1947 },
michael@0 1948
michael@0 1949 onFrameUnload: function({ window }) {
michael@0 1950 // Any retained orphans that belong to this document
michael@0 1951 // or its children need to be released, and a mutation sent
michael@0 1952 // to notify of that.
michael@0 1953 let releasedOrphans = [];
michael@0 1954
michael@0 1955 for (let retained of this._retainedOrphans) {
michael@0 1956 if (Cu.isDeadWrapper(retained.rawNode) ||
michael@0 1957 this._childOfWindow(window, retained.rawNode)) {
michael@0 1958 this._retainedOrphans.delete(retained);
michael@0 1959 releasedOrphans.push(retained.actorID);
michael@0 1960 this.releaseNode(retained, { force: true });
michael@0 1961 }
michael@0 1962 }
michael@0 1963
michael@0 1964 if (releasedOrphans.length > 0) {
michael@0 1965 this.queueMutation({
michael@0 1966 target: this.rootNode.actorID,
michael@0 1967 type: "unretained",
michael@0 1968 nodes: releasedOrphans
michael@0 1969 });
michael@0 1970 }
michael@0 1971
michael@0 1972 let doc = window.document;
michael@0 1973 let documentActor = this._refMap.get(doc);
michael@0 1974 if (!documentActor) {
michael@0 1975 return;
michael@0 1976 }
michael@0 1977
michael@0 1978 if (this.rootDoc === doc) {
michael@0 1979 this.rootDoc = null;
michael@0 1980 this.rootNode = null;
michael@0 1981 }
michael@0 1982
michael@0 1983 this.queueMutation({
michael@0 1984 type: "documentUnload",
michael@0 1985 target: documentActor.actorID
michael@0 1986 });
michael@0 1987
michael@0 1988 let walker = documentWalker(doc, this.rootWin);
michael@0 1989 let parentNode = walker.parentNode();
michael@0 1990 if (parentNode) {
michael@0 1991 // Send a childList mutation on the frame so that clients know
michael@0 1992 // they should reread the children list.
michael@0 1993 this.queueMutation({
michael@0 1994 type: "childList",
michael@0 1995 target: this._refMap.get(parentNode).actorID,
michael@0 1996 added: [],
michael@0 1997 removed: []
michael@0 1998 });
michael@0 1999 }
michael@0 2000
michael@0 2001 // Need to force a release of this node, because those nodes can't
michael@0 2002 // be accessed anymore.
michael@0 2003 this.releaseNode(documentActor, { force: true });
michael@0 2004 },
michael@0 2005
michael@0 2006 /**
michael@0 2007 * Check if a node is attached to the DOM tree of the current page.
michael@0 2008 * @param {nsIDomNode} rawNode
michael@0 2009 * @return {Boolean} false if the node is removed from the tree or within a
michael@0 2010 * document fragment
michael@0 2011 */
michael@0 2012 _isInDOMTree: function(rawNode) {
michael@0 2013 let walker = documentWalker(rawNode, this.rootWin);
michael@0 2014 let current = walker.currentNode;
michael@0 2015
michael@0 2016 // Reaching the top of tree
michael@0 2017 while (walker.parentNode()) {
michael@0 2018 current = walker.currentNode;
michael@0 2019 }
michael@0 2020
michael@0 2021 // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
michael@0 2022 // attached
michael@0 2023 if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
michael@0 2024 current !== this.rootDoc) {
michael@0 2025 return false;
michael@0 2026 }
michael@0 2027
michael@0 2028 // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
michael@0 2029 return true;
michael@0 2030 },
michael@0 2031
michael@0 2032 /**
michael@0 2033 * @see _isInDomTree
michael@0 2034 */
michael@0 2035 isInDOMTree: method(function(node) {
michael@0 2036 return node ? this._isInDOMTree(node.rawNode) : false;
michael@0 2037 }, {
michael@0 2038 request: { node: Arg(0, "domnode") },
michael@0 2039 response: { attached: RetVal("boolean") }
michael@0 2040 }),
michael@0 2041
michael@0 2042 /**
michael@0 2043 * Given an ObjectActor (identified by its ID), commonly used in the debugger,
michael@0 2044 * webconsole and variablesView, return the corresponding inspector's NodeActor
michael@0 2045 */
michael@0 2046 getNodeActorFromObjectActor: method(function(objectActorID) {
michael@0 2047 let debuggerObject = this.conn.getActor(objectActorID).obj;
michael@0 2048 let rawNode = debuggerObject.unsafeDereference();
michael@0 2049
michael@0 2050 if (!this._isInDOMTree(rawNode)) {
michael@0 2051 return null;
michael@0 2052 }
michael@0 2053
michael@0 2054 // This is a special case for the document object whereby it is considered
michael@0 2055 // as document.documentElement (the <html> node)
michael@0 2056 if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
michael@0 2057 rawNode = rawNode.documentElement;
michael@0 2058 }
michael@0 2059
michael@0 2060 return this.attachElement(rawNode);
michael@0 2061 }, {
michael@0 2062 request: {
michael@0 2063 objectActorID: Arg(0, "string")
michael@0 2064 },
michael@0 2065 response: {
michael@0 2066 nodeFront: RetVal("nullable:disconnectedNode")
michael@0 2067 }
michael@0 2068 }),
michael@0 2069 });
michael@0 2070
michael@0 2071 /**
michael@0 2072 * Client side of the DOM walker.
michael@0 2073 */
michael@0 2074 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
michael@0 2075 // Set to true if cleanup should be requested after every mutation list.
michael@0 2076 autoCleanup: true,
michael@0 2077
michael@0 2078 /**
michael@0 2079 * This is kept for backward-compatibility reasons with older remote target.
michael@0 2080 * Targets previous to bug 916443
michael@0 2081 */
michael@0 2082 pick: protocol.custom(function() {
michael@0 2083 return this._pick().then(response => {
michael@0 2084 return response.node;
michael@0 2085 });
michael@0 2086 }, {impl: "_pick"}),
michael@0 2087
michael@0 2088 initialize: function(client, form) {
michael@0 2089 this._createRootNodePromise();
michael@0 2090 protocol.Front.prototype.initialize.call(this, client, form);
michael@0 2091 this._orphaned = new Set();
michael@0 2092 this._retainedOrphans = new Set();
michael@0 2093 },
michael@0 2094
michael@0 2095 destroy: function() {
michael@0 2096 protocol.Front.prototype.destroy.call(this);
michael@0 2097 },
michael@0 2098
michael@0 2099 // Update the object given a form representation off the wire.
michael@0 2100 form: function(json) {
michael@0 2101 this.actorID = json.actor;
michael@0 2102 this.rootNode = types.getType("domnode").read(json.root, this);
michael@0 2103 this._rootNodeDeferred.resolve(this.rootNode);
michael@0 2104 },
michael@0 2105
michael@0 2106 /**
michael@0 2107 * Clients can use walker.rootNode to get the current root node of the
michael@0 2108 * walker, but during a reload the root node might be null. This
michael@0 2109 * method returns a promise that will resolve to the root node when it is
michael@0 2110 * set.
michael@0 2111 */
michael@0 2112 getRootNode: function() {
michael@0 2113 return this._rootNodeDeferred.promise;
michael@0 2114 },
michael@0 2115
michael@0 2116 /**
michael@0 2117 * Create the root node promise, triggering the "new-root" notification
michael@0 2118 * on resolution.
michael@0 2119 */
michael@0 2120 _createRootNodePromise: function() {
michael@0 2121 this._rootNodeDeferred = promise.defer();
michael@0 2122 this._rootNodeDeferred.promise.then(() => {
michael@0 2123 events.emit(this, "new-root");
michael@0 2124 });
michael@0 2125 },
michael@0 2126
michael@0 2127 /**
michael@0 2128 * When reading an actor form off the wire, we want to hook it up to its
michael@0 2129 * parent front. The protocol guarantees that the parent will be seen
michael@0 2130 * by the client in either a previous or the current request.
michael@0 2131 * So if we've already seen this parent return it, otherwise create
michael@0 2132 * a bare-bones stand-in node. The stand-in node will be updated
michael@0 2133 * with a real form by the end of the deserialization.
michael@0 2134 */
michael@0 2135 ensureParentFront: function(id) {
michael@0 2136 let front = this.get(id);
michael@0 2137 if (front) {
michael@0 2138 return front;
michael@0 2139 }
michael@0 2140
michael@0 2141 return types.getType("domnode").read({ actor: id }, this, "standin");
michael@0 2142 },
michael@0 2143
michael@0 2144 /**
michael@0 2145 * See the documentation for WalkerActor.prototype.retainNode for
michael@0 2146 * information on retained nodes.
michael@0 2147 *
michael@0 2148 * From the client's perspective, `retainNode` can fail if the node in
michael@0 2149 * question is removed from the ownership tree before the `retainNode`
michael@0 2150 * request reaches the server. This can only happen if the client has
michael@0 2151 * asked the server to release nodes but hasn't gotten a response
michael@0 2152 * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
michael@0 2153 * set is outstanding.
michael@0 2154 *
michael@0 2155 * If either of those requests is outstanding AND releases the retained
michael@0 2156 * node, this request will fail with noSuchActor, but the ownership tree
michael@0 2157 * will stay in a consistent state.
michael@0 2158 *
michael@0 2159 * Because the protocol guarantees that requests will be processed and
michael@0 2160 * responses received in the order they were sent, we get the right
michael@0 2161 * semantics by setting our local retained flag on the node only AFTER
michael@0 2162 * a SUCCESSFUL retainNode call.
michael@0 2163 */
michael@0 2164 retainNode: protocol.custom(function(node) {
michael@0 2165 return this._retainNode(node).then(() => {
michael@0 2166 node.retained = true;
michael@0 2167 });
michael@0 2168 }, {
michael@0 2169 impl: "_retainNode",
michael@0 2170 }),
michael@0 2171
michael@0 2172 unretainNode: protocol.custom(function(node) {
michael@0 2173 return this._unretainNode(node).then(() => {
michael@0 2174 node.retained = false;
michael@0 2175 if (this._retainedOrphans.has(node)) {
michael@0 2176 this._retainedOrphans.delete(node);
michael@0 2177 this._releaseFront(node);
michael@0 2178 }
michael@0 2179 });
michael@0 2180 }, {
michael@0 2181 impl: "_unretainNode"
michael@0 2182 }),
michael@0 2183
michael@0 2184 releaseNode: protocol.custom(function(node, options={}) {
michael@0 2185 // NodeFront.destroy will destroy children in the ownership tree too,
michael@0 2186 // mimicking what the server will do here.
michael@0 2187 let actorID = node.actorID;
michael@0 2188 this._releaseFront(node, !!options.force);
michael@0 2189 return this._releaseNode({ actorID: actorID });
michael@0 2190 }, {
michael@0 2191 impl: "_releaseNode"
michael@0 2192 }),
michael@0 2193
michael@0 2194 querySelector: protocol.custom(function(queryNode, selector) {
michael@0 2195 return this._querySelector(queryNode, selector).then(response => {
michael@0 2196 return response.node;
michael@0 2197 });
michael@0 2198 }, {
michael@0 2199 impl: "_querySelector"
michael@0 2200 }),
michael@0 2201
michael@0 2202 getNodeActorFromObjectActor: protocol.custom(function(objectActorID) {
michael@0 2203 return this._getNodeActorFromObjectActor(objectActorID).then(response => {
michael@0 2204 return response ? response.node : null;
michael@0 2205 });
michael@0 2206 }, {
michael@0 2207 impl: "_getNodeActorFromObjectActor"
michael@0 2208 }),
michael@0 2209
michael@0 2210 _releaseFront: function(node, force) {
michael@0 2211 if (node.retained && !force) {
michael@0 2212 node.reparent(null);
michael@0 2213 this._retainedOrphans.add(node);
michael@0 2214 return;
michael@0 2215 }
michael@0 2216
michael@0 2217 if (node.retained) {
michael@0 2218 // Forcing a removal.
michael@0 2219 this._retainedOrphans.delete(node);
michael@0 2220 }
michael@0 2221
michael@0 2222 // Release any children
michael@0 2223 for (let child of node.treeChildren()) {
michael@0 2224 this._releaseFront(child, force);
michael@0 2225 }
michael@0 2226
michael@0 2227 // All children will have been removed from the node by this point.
michael@0 2228 node.reparent(null);
michael@0 2229 node.destroy();
michael@0 2230 },
michael@0 2231
michael@0 2232 /**
michael@0 2233 * Get any unprocessed mutation records and process them.
michael@0 2234 */
michael@0 2235 getMutations: protocol.custom(function(options={}) {
michael@0 2236 return this._getMutations(options).then(mutations => {
michael@0 2237 let emitMutations = [];
michael@0 2238 for (let change of mutations) {
michael@0 2239 // The target is only an actorID, get the associated front.
michael@0 2240 let targetID;
michael@0 2241 let targetFront;
michael@0 2242
michael@0 2243 if (change.type === "newRoot") {
michael@0 2244 this.rootNode = types.getType("domnode").read(change.target, this);
michael@0 2245 this._rootNodeDeferred.resolve(this.rootNode);
michael@0 2246 targetID = this.rootNode.actorID;
michael@0 2247 targetFront = this.rootNode;
michael@0 2248 } else {
michael@0 2249 targetID = change.target;
michael@0 2250 targetFront = this.get(targetID);
michael@0 2251 }
michael@0 2252
michael@0 2253 if (!targetFront) {
michael@0 2254 console.trace("Got a mutation for an unexpected actor: " + targetID + ", please file a bug on bugzilla.mozilla.org!");
michael@0 2255 continue;
michael@0 2256 }
michael@0 2257
michael@0 2258 let emittedMutation = object.merge(change, { target: targetFront });
michael@0 2259
michael@0 2260 if (change.type === "childList") {
michael@0 2261 // Update the ownership tree according to the mutation record.
michael@0 2262 let addedFronts = [];
michael@0 2263 let removedFronts = [];
michael@0 2264 for (let removed of change.removed) {
michael@0 2265 let removedFront = this.get(removed);
michael@0 2266 if (!removedFront) {
michael@0 2267 console.error("Got a removal of an actor we didn't know about: " + removed);
michael@0 2268 continue;
michael@0 2269 }
michael@0 2270 // Remove from the ownership tree
michael@0 2271 removedFront.reparent(null);
michael@0 2272
michael@0 2273 // This node is orphaned unless we get it in the 'added' list
michael@0 2274 // eventually.
michael@0 2275 this._orphaned.add(removedFront);
michael@0 2276 removedFronts.push(removedFront);
michael@0 2277 }
michael@0 2278 for (let added of change.added) {
michael@0 2279 let addedFront = this.get(added);
michael@0 2280 if (!addedFront) {
michael@0 2281 console.error("Got an addition of an actor we didn't know about: " + added);
michael@0 2282 continue;
michael@0 2283 }
michael@0 2284 addedFront.reparent(targetFront)
michael@0 2285
michael@0 2286 // The actor is reconnected to the ownership tree, unorphan
michael@0 2287 // it.
michael@0 2288 this._orphaned.delete(addedFront);
michael@0 2289 addedFronts.push(addedFront);
michael@0 2290 }
michael@0 2291 // Before passing to users, replace the added and removed actor
michael@0 2292 // ids with front in the mutation record.
michael@0 2293 emittedMutation.added = addedFronts;
michael@0 2294 emittedMutation.removed = removedFronts;
michael@0 2295 targetFront._form.numChildren = change.numChildren;
michael@0 2296 } else if (change.type === "frameLoad") {
michael@0 2297 // Nothing we need to do here, except verify that we don't have any
michael@0 2298 // document children, because we should have gotten a documentUnload
michael@0 2299 // first.
michael@0 2300 for (let child of targetFront.treeChildren()) {
michael@0 2301 if (child.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
michael@0 2302 console.trace("Got an unexpected frameLoad in the inspector, please file a bug on bugzilla.mozilla.org!");
michael@0 2303 }
michael@0 2304 }
michael@0 2305 } else if (change.type === "documentUnload") {
michael@0 2306 if (targetFront === this.rootNode) {
michael@0 2307 this._createRootNodePromise();
michael@0 2308 }
michael@0 2309
michael@0 2310 // We try to give fronts instead of actorIDs, but these fronts need
michael@0 2311 // to be destroyed now.
michael@0 2312 emittedMutation.target = targetFront.actorID;
michael@0 2313 emittedMutation.targetParent = targetFront.parentNode();
michael@0 2314
michael@0 2315 // Release the document node and all of its children, even retained.
michael@0 2316 this._releaseFront(targetFront, true);
michael@0 2317 } else if (change.type === "unretained") {
michael@0 2318 // Retained orphans were force-released without the intervention of
michael@0 2319 // client (probably a navigated frame).
michael@0 2320 for (let released of change.nodes) {
michael@0 2321 let releasedFront = this.get(released);
michael@0 2322 this._retainedOrphans.delete(released);
michael@0 2323 this._releaseFront(releasedFront, true);
michael@0 2324 }
michael@0 2325 } else {
michael@0 2326 targetFront.updateMutation(change);
michael@0 2327 }
michael@0 2328
michael@0 2329 emitMutations.push(emittedMutation);
michael@0 2330 }
michael@0 2331
michael@0 2332 if (options.cleanup) {
michael@0 2333 for (let node of this._orphaned) {
michael@0 2334 // This will move retained nodes to this._retainedOrphans.
michael@0 2335 this._releaseFront(node);
michael@0 2336 }
michael@0 2337 this._orphaned = new Set();
michael@0 2338 }
michael@0 2339
michael@0 2340 events.emit(this, "mutations", emitMutations);
michael@0 2341 });
michael@0 2342 }, {
michael@0 2343 impl: "_getMutations"
michael@0 2344 }),
michael@0 2345
michael@0 2346 /**
michael@0 2347 * Handle the `new-mutations` notification by fetching the
michael@0 2348 * available mutation records.
michael@0 2349 */
michael@0 2350 onMutations: protocol.preEvent("new-mutations", function() {
michael@0 2351 // Fetch and process the mutations.
michael@0 2352 this.getMutations({cleanup: this.autoCleanup}).then(null, console.error);
michael@0 2353 }),
michael@0 2354
michael@0 2355 isLocal: function() {
michael@0 2356 return !!this.conn._transport._serverConnection;
michael@0 2357 },
michael@0 2358
michael@0 2359 // XXX hack during transition to remote inspector: get a proper NodeFront
michael@0 2360 // for a given local node. Only works locally.
michael@0 2361 frontForRawNode: function(rawNode) {
michael@0 2362 if (!this.isLocal()) {
michael@0 2363 console.warn("Tried to use frontForRawNode on a remote connection.");
michael@0 2364 return null;
michael@0 2365 }
michael@0 2366 let walkerActor = this.conn._transport._serverConnection.getActor(this.actorID);
michael@0 2367 if (!walkerActor) {
michael@0 2368 throw Error("Could not find client side for actor " + this.actorID);
michael@0 2369 }
michael@0 2370 let nodeActor = walkerActor._ref(rawNode);
michael@0 2371
michael@0 2372 // Pass the node through a read/write pair to create the client side actor.
michael@0 2373 let nodeType = types.getType("domnode");
michael@0 2374 let returnNode = nodeType.read(nodeType.write(nodeActor, walkerActor), this);
michael@0 2375 let top = returnNode;
michael@0 2376 let extras = walkerActor.parents(nodeActor);
michael@0 2377 for (let extraActor of extras) {
michael@0 2378 top = nodeType.read(nodeType.write(extraActor, walkerActor), this);
michael@0 2379 }
michael@0 2380
michael@0 2381 if (top !== this.rootNode) {
michael@0 2382 // Imported an already-orphaned node.
michael@0 2383 this._orphaned.add(top);
michael@0 2384 walkerActor._orphaned.add(this.conn._transport._serverConnection.getActor(top.actorID));
michael@0 2385 }
michael@0 2386 return returnNode;
michael@0 2387 }
michael@0 2388 });
michael@0 2389
michael@0 2390 /**
michael@0 2391 * Convenience API for building a list of attribute modifications
michael@0 2392 * for the `modifyAttributes` request.
michael@0 2393 */
michael@0 2394 var AttributeModificationList = Class({
michael@0 2395 initialize: function(node) {
michael@0 2396 this.node = node;
michael@0 2397 this.modifications = [];
michael@0 2398 },
michael@0 2399
michael@0 2400 apply: function() {
michael@0 2401 let ret = this.node.modifyAttributes(this.modifications);
michael@0 2402 return ret;
michael@0 2403 },
michael@0 2404
michael@0 2405 destroy: function() {
michael@0 2406 this.node = null;
michael@0 2407 this.modification = null;
michael@0 2408 },
michael@0 2409
michael@0 2410 setAttributeNS: function(ns, name, value) {
michael@0 2411 this.modifications.push({
michael@0 2412 attributeNamespace: ns,
michael@0 2413 attributeName: name,
michael@0 2414 newValue: value
michael@0 2415 });
michael@0 2416 },
michael@0 2417
michael@0 2418 setAttribute: function(name, value) {
michael@0 2419 this.setAttributeNS(undefined, name, value);
michael@0 2420 },
michael@0 2421
michael@0 2422 removeAttributeNS: function(ns, name) {
michael@0 2423 this.setAttributeNS(ns, name, undefined);
michael@0 2424 },
michael@0 2425
michael@0 2426 removeAttribute: function(name) {
michael@0 2427 this.setAttributeNS(undefined, name, undefined);
michael@0 2428 }
michael@0 2429 })
michael@0 2430
michael@0 2431 /**
michael@0 2432 * Server side of the inspector actor, which is used to create
michael@0 2433 * inspector-related actors, including the walker.
michael@0 2434 */
michael@0 2435 var InspectorActor = protocol.ActorClass({
michael@0 2436 typeName: "inspector",
michael@0 2437 initialize: function(conn, tabActor) {
michael@0 2438 protocol.Actor.prototype.initialize.call(this, conn);
michael@0 2439 this.tabActor = tabActor;
michael@0 2440 },
michael@0 2441
michael@0 2442 get window() this.tabActor.window,
michael@0 2443
michael@0 2444 getWalker: method(function(options={}) {
michael@0 2445 if (this._walkerPromise) {
michael@0 2446 return this._walkerPromise;
michael@0 2447 }
michael@0 2448
michael@0 2449 let deferred = promise.defer();
michael@0 2450 this._walkerPromise = deferred.promise;
michael@0 2451
michael@0 2452 let window = this.window;
michael@0 2453 var domReady = () => {
michael@0 2454 let tabActor = this.tabActor;
michael@0 2455 window.removeEventListener("DOMContentLoaded", domReady, true);
michael@0 2456 this.walker = WalkerActor(this.conn, tabActor, options);
michael@0 2457 events.once(this.walker, "destroyed", () => {
michael@0 2458 this._walkerPromise = null;
michael@0 2459 this._pageStylePromise = null;
michael@0 2460 });
michael@0 2461 deferred.resolve(this.walker);
michael@0 2462 };
michael@0 2463
michael@0 2464 if (window.document.readyState === "loading") {
michael@0 2465 window.addEventListener("DOMContentLoaded", domReady, true);
michael@0 2466 } else {
michael@0 2467 domReady();
michael@0 2468 }
michael@0 2469
michael@0 2470 return this._walkerPromise;
michael@0 2471 }, {
michael@0 2472 request: {},
michael@0 2473 response: {
michael@0 2474 walker: RetVal("domwalker")
michael@0 2475 }
michael@0 2476 }),
michael@0 2477
michael@0 2478 getPageStyle: method(function() {
michael@0 2479 if (this._pageStylePromise) {
michael@0 2480 return this._pageStylePromise;
michael@0 2481 }
michael@0 2482
michael@0 2483 this._pageStylePromise = this.getWalker().then(walker => {
michael@0 2484 return PageStyleActor(this);
michael@0 2485 });
michael@0 2486 return this._pageStylePromise;
michael@0 2487 }, {
michael@0 2488 request: {},
michael@0 2489 response: {
michael@0 2490 pageStyle: RetVal("pagestyle")
michael@0 2491 }
michael@0 2492 }),
michael@0 2493
michael@0 2494 getHighlighter: method(function (autohide) {
michael@0 2495 if (this._highlighterPromise) {
michael@0 2496 return this._highlighterPromise;
michael@0 2497 }
michael@0 2498
michael@0 2499 this._highlighterPromise = this.getWalker().then(walker => {
michael@0 2500 return HighlighterActor(this, autohide);
michael@0 2501 });
michael@0 2502 return this._highlighterPromise;
michael@0 2503 }, {
michael@0 2504 request: { autohide: Arg(0, "boolean") },
michael@0 2505 response: {
michael@0 2506 highligter: RetVal("highlighter")
michael@0 2507 }
michael@0 2508 }),
michael@0 2509
michael@0 2510 /**
michael@0 2511 * Get the node's image data if any (for canvas and img nodes).
michael@0 2512 * Returns an imageData object with the actual data being a LongStringActor
michael@0 2513 * and a size json object.
michael@0 2514 * The image data is transmitted as a base64 encoded png data-uri.
michael@0 2515 * The method rejects if the node isn't an image or if the image is missing
michael@0 2516 *
michael@0 2517 * Accepts a maxDim request parameter to resize images that are larger. This
michael@0 2518 * is important as the resizing occurs server-side so that image-data being
michael@0 2519 * transfered in the longstring back to the client will be that much smaller
michael@0 2520 */
michael@0 2521 getImageDataFromURL: method(function(url, maxDim) {
michael@0 2522 let deferred = promise.defer();
michael@0 2523 let img = new this.window.Image();
michael@0 2524
michael@0 2525 // On load, get the image data and send the response
michael@0 2526 img.onload = () => {
michael@0 2527 // imageToImageData throws an error if the image is missing
michael@0 2528 try {
michael@0 2529 let imageData = imageToImageData(img, maxDim);
michael@0 2530 deferred.resolve({
michael@0 2531 data: LongStringActor(this.conn, imageData.data),
michael@0 2532 size: imageData.size
michael@0 2533 });
michael@0 2534 } catch (e) {
michael@0 2535 deferred.reject(new Error("Image " + url+ " not available"));
michael@0 2536 }
michael@0 2537 }
michael@0 2538
michael@0 2539 // If the URL doesn't point to a resource, reject
michael@0 2540 img.onerror = () => {
michael@0 2541 deferred.reject(new Error("Image " + url+ " not available"));
michael@0 2542 }
michael@0 2543
michael@0 2544 // If the request hangs for too long, kill it to avoid queuing up other requests
michael@0 2545 // to the same actor, except if we're running tests
michael@0 2546 if (!gDevTools.testing) {
michael@0 2547 this.window.setTimeout(() => {
michael@0 2548 deferred.reject(new Error("Image " + url + " could not be retrieved in time"));
michael@0 2549 }, IMAGE_FETCHING_TIMEOUT);
michael@0 2550 }
michael@0 2551
michael@0 2552 img.src = url;
michael@0 2553
michael@0 2554 return deferred.promise;
michael@0 2555 }, {
michael@0 2556 request: {url: Arg(0), maxDim: Arg(1, "nullable:number")},
michael@0 2557 response: RetVal("imageData")
michael@0 2558 })
michael@0 2559 });
michael@0 2560
michael@0 2561 /**
michael@0 2562 * Client side of the inspector actor, which is used to create
michael@0 2563 * inspector-related actors, including the walker.
michael@0 2564 */
michael@0 2565 var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, {
michael@0 2566 initialize: function(client, tabForm) {
michael@0 2567 protocol.Front.prototype.initialize.call(this, client);
michael@0 2568 this.actorID = tabForm.inspectorActor;
michael@0 2569
michael@0 2570 // XXX: This is the first actor type in its hierarchy to use the protocol
michael@0 2571 // library, so we're going to self-own on the client side for now.
michael@0 2572 client.addActorPool(this);
michael@0 2573 this.manage(this);
michael@0 2574 },
michael@0 2575
michael@0 2576 destroy: function() {
michael@0 2577 delete this.walker;
michael@0 2578 protocol.Front.prototype.destroy.call(this);
michael@0 2579 },
michael@0 2580
michael@0 2581 getWalker: protocol.custom(function() {
michael@0 2582 return this._getWalker().then(walker => {
michael@0 2583 this.walker = walker;
michael@0 2584 return walker;
michael@0 2585 });
michael@0 2586 }, {
michael@0 2587 impl: "_getWalker"
michael@0 2588 }),
michael@0 2589
michael@0 2590 getPageStyle: protocol.custom(function() {
michael@0 2591 return this._getPageStyle().then(pageStyle => {
michael@0 2592 // We need a walker to understand node references from the
michael@0 2593 // node style.
michael@0 2594 if (this.walker) {
michael@0 2595 return pageStyle;
michael@0 2596 }
michael@0 2597 return this.getWalker().then(() => {
michael@0 2598 return pageStyle;
michael@0 2599 });
michael@0 2600 });
michael@0 2601 }, {
michael@0 2602 impl: "_getPageStyle"
michael@0 2603 })
michael@0 2604 });
michael@0 2605
michael@0 2606 function documentWalker(node, rootWin, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
michael@0 2607 return new DocumentWalker(node, rootWin, whatToShow, whitespaceTextFilter, false);
michael@0 2608 }
michael@0 2609
michael@0 2610 // Exported for test purposes.
michael@0 2611 exports._documentWalker = documentWalker;
michael@0 2612
michael@0 2613 function nodeDocument(node) {
michael@0 2614 return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
michael@0 2615 }
michael@0 2616
michael@0 2617 /**
michael@0 2618 * Similar to a TreeWalker, except will dig in to iframes and it doesn't
michael@0 2619 * implement the good methods like previousNode and nextNode.
michael@0 2620 *
michael@0 2621 * See TreeWalker documentation for explanations of the methods.
michael@0 2622 */
michael@0 2623 function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences) {
michael@0 2624 let doc = nodeDocument(aNode);
michael@0 2625 this.layoutHelpers = new LayoutHelpers(aRootWin);
michael@0 2626 this.walker = doc.createTreeWalker(doc,
michael@0 2627 aShow, aFilter, aExpandEntityReferences);
michael@0 2628 this.walker.currentNode = aNode;
michael@0 2629 this.filter = aFilter;
michael@0 2630 }
michael@0 2631
michael@0 2632 DocumentWalker.prototype = {
michael@0 2633 get node() this.walker.node,
michael@0 2634 get whatToShow() this.walker.whatToShow,
michael@0 2635 get expandEntityReferences() this.walker.expandEntityReferences,
michael@0 2636 get currentNode() this.walker.currentNode,
michael@0 2637 set currentNode(aVal) this.walker.currentNode = aVal,
michael@0 2638
michael@0 2639 /**
michael@0 2640 * Called when the new node is in a different document than
michael@0 2641 * the current node, creates a new treewalker for the document we've
michael@0 2642 * run in to.
michael@0 2643 */
michael@0 2644 _reparentWalker: function(aNewNode) {
michael@0 2645 if (!aNewNode) {
michael@0 2646 return null;
michael@0 2647 }
michael@0 2648 let doc = nodeDocument(aNewNode);
michael@0 2649 let walker = doc.createTreeWalker(doc,
michael@0 2650 this.whatToShow, this.filter, this.expandEntityReferences);
michael@0 2651 walker.currentNode = aNewNode;
michael@0 2652 this.walker = walker;
michael@0 2653 return aNewNode;
michael@0 2654 },
michael@0 2655
michael@0 2656 parentNode: function() {
michael@0 2657 let currentNode = this.walker.currentNode;
michael@0 2658 let parentNode = this.walker.parentNode();
michael@0 2659
michael@0 2660 if (!parentNode) {
michael@0 2661 if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
michael@0 2662 && currentNode.defaultView) {
michael@0 2663
michael@0 2664 let window = currentNode.defaultView;
michael@0 2665 let frame = this.layoutHelpers.getFrameElement(window);
michael@0 2666 if (frame) {
michael@0 2667 return this._reparentWalker(frame);
michael@0 2668 }
michael@0 2669 }
michael@0 2670 return null;
michael@0 2671 }
michael@0 2672
michael@0 2673 return parentNode;
michael@0 2674 },
michael@0 2675
michael@0 2676 firstChild: function() {
michael@0 2677 let node = this.walker.currentNode;
michael@0 2678 if (!node)
michael@0 2679 return null;
michael@0 2680 if (node.contentDocument) {
michael@0 2681 return this._reparentWalker(node.contentDocument);
michael@0 2682 } else if (node.getSVGDocument) {
michael@0 2683 return this._reparentWalker(node.getSVGDocument());
michael@0 2684 }
michael@0 2685 return this.walker.firstChild();
michael@0 2686 },
michael@0 2687
michael@0 2688 lastChild: function() {
michael@0 2689 let node = this.walker.currentNode;
michael@0 2690 if (!node)
michael@0 2691 return null;
michael@0 2692 if (node.contentDocument) {
michael@0 2693 return this._reparentWalker(node.contentDocument);
michael@0 2694 } else if (node.getSVGDocument) {
michael@0 2695 return this._reparentWalker(node.getSVGDocument());
michael@0 2696 }
michael@0 2697 return this.walker.lastChild();
michael@0 2698 },
michael@0 2699
michael@0 2700 previousSibling: function DW_previousSibling() this.walker.previousSibling(),
michael@0 2701 nextSibling: function DW_nextSibling() this.walker.nextSibling()
michael@0 2702 };
michael@0 2703
michael@0 2704 /**
michael@0 2705 * A tree walker filter for avoiding empty whitespace text nodes.
michael@0 2706 */
michael@0 2707 function whitespaceTextFilter(aNode) {
michael@0 2708 if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
michael@0 2709 !/[^\s]/.exec(aNode.nodeValue)) {
michael@0 2710 return Ci.nsIDOMNodeFilter.FILTER_SKIP;
michael@0 2711 } else {
michael@0 2712 return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
michael@0 2713 }
michael@0 2714 }
michael@0 2715
michael@0 2716 /**
michael@0 2717 * Given an image DOMNode, return the image data-uri.
michael@0 2718 * @param {DOMNode} node The image node
michael@0 2719 * @param {Number} maxDim Optionally pass a maximum size you want the longest
michael@0 2720 * side of the image to be resized to before getting the image data.
michael@0 2721 * @return {Object} An object containing the data-uri and size-related information
michael@0 2722 * {data: "...", size: {naturalWidth: 400, naturalHeight: 300, resized: true}}
michael@0 2723 * @throws an error if the node isn't an image or if the image is missing
michael@0 2724 */
michael@0 2725 function imageToImageData(node, maxDim) {
michael@0 2726 let isImg = node.tagName.toLowerCase() === "img";
michael@0 2727 let isCanvas = node.tagName.toLowerCase() === "canvas";
michael@0 2728
michael@0 2729 if (!isImg && !isCanvas) {
michael@0 2730 return null;
michael@0 2731 }
michael@0 2732
michael@0 2733 // Get the image resize ratio if a maxDim was provided
michael@0 2734 let resizeRatio = 1;
michael@0 2735 let imgWidth = node.naturalWidth || node.width;
michael@0 2736 let imgHeight = node.naturalHeight || node.height;
michael@0 2737 let imgMax = Math.max(imgWidth, imgHeight);
michael@0 2738 if (maxDim && imgMax > maxDim) {
michael@0 2739 resizeRatio = maxDim / imgMax;
michael@0 2740 }
michael@0 2741
michael@0 2742 // Extract the image data
michael@0 2743 let imageData;
michael@0 2744 // The image may already be a data-uri, in which case, save ourselves the
michael@0 2745 // trouble of converting via the canvas.drawImage.toDataURL method
michael@0 2746 if (isImg && node.src.startsWith("data:")) {
michael@0 2747 imageData = node.src;
michael@0 2748 } else {
michael@0 2749 // Create a canvas to copy the rawNode into and get the imageData from
michael@0 2750 let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
michael@0 2751 canvas.width = imgWidth * resizeRatio;
michael@0 2752 canvas.height = imgHeight * resizeRatio;
michael@0 2753 let ctx = canvas.getContext("2d");
michael@0 2754
michael@0 2755 // Copy the rawNode image or canvas in the new canvas and extract data
michael@0 2756 ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
michael@0 2757 imageData = canvas.toDataURL("image/png");
michael@0 2758 }
michael@0 2759
michael@0 2760 return {
michael@0 2761 data: imageData,
michael@0 2762 size: {
michael@0 2763 naturalWidth: imgWidth,
michael@0 2764 naturalHeight: imgHeight,
michael@0 2765 resized: resizeRatio !== 1
michael@0 2766 }
michael@0 2767 }
michael@0 2768 }
michael@0 2769
michael@0 2770 loader.lazyGetter(this, "DOMUtils", function () {
michael@0 2771 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 2772 });

mercurial