Wed, 31 Dec 2014 06:09:35 +0100
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 | }); |