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