Bug 1587789 - Remove isXBLAnonymous functions defined and used in the inspector....
[gecko.git] / devtools / server / actors / inspector / node.js
blobc002c0a974c8d1caeb78de1ed8db31feac5e20e1
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/. */
5 "use strict";
7 const { Cu } = require("chrome");
8 const Services = require("Services");
9 const InspectorUtils = require("InspectorUtils");
10 const protocol = require("devtools/shared/protocol");
11 const { PSEUDO_CLASSES } = require("devtools/shared/css/constants");
12 const { nodeSpec, nodeListSpec } = require("devtools/shared/specs/node");
13 loader.lazyRequireGetter(
14   this,
15   "getCssPath",
16   "devtools/shared/inspector/css-logic",
17   true
19 loader.lazyRequireGetter(
20   this,
21   "getXPath",
22   "devtools/shared/inspector/css-logic",
23   true
25 loader.lazyRequireGetter(
26   this,
27   "findCssSelector",
28   "devtools/shared/inspector/css-logic",
29   true
32 loader.lazyRequireGetter(
33   this,
34   "isAfterPseudoElement",
35   "devtools/shared/layout/utils",
36   true
38 loader.lazyRequireGetter(
39   this,
40   "isAnonymous",
41   "devtools/shared/layout/utils",
42   true
44 loader.lazyRequireGetter(
45   this,
46   "isBeforePseudoElement",
47   "devtools/shared/layout/utils",
48   true
50 loader.lazyRequireGetter(
51   this,
52   "isDirectShadowHostChild",
53   "devtools/shared/layout/utils",
54   true
56 loader.lazyRequireGetter(
57   this,
58   "isMarkerPseudoElement",
59   "devtools/shared/layout/utils",
60   true
62 loader.lazyRequireGetter(
63   this,
64   "isNativeAnonymous",
65   "devtools/shared/layout/utils",
66   true
68 loader.lazyRequireGetter(
69   this,
70   "isShadowAnonymous",
71   "devtools/shared/layout/utils",
72   true
74 loader.lazyRequireGetter(
75   this,
76   "isShadowHost",
77   "devtools/shared/layout/utils",
78   true
80 loader.lazyRequireGetter(
81   this,
82   "isShadowRoot",
83   "devtools/shared/layout/utils",
84   true
86 loader.lazyRequireGetter(
87   this,
88   "getShadowRootMode",
89   "devtools/shared/layout/utils",
90   true
92 loader.lazyRequireGetter(
93   this,
94   "isRemoteFrame",
95   "devtools/shared/layout/utils",
96   true
99 loader.lazyRequireGetter(
100   this,
101   "InspectorActorUtils",
102   "devtools/server/actors/inspector/utils"
104 loader.lazyRequireGetter(
105   this,
106   "LongStringActor",
107   "devtools/server/actors/string",
108   true
110 loader.lazyRequireGetter(
111   this,
112   "getFontPreviewData",
113   "devtools/server/actors/styles",
114   true
116 loader.lazyRequireGetter(
117   this,
118   "CssLogic",
119   "devtools/server/actors/inspector/css-logic",
120   true
122 loader.lazyRequireGetter(
123   this,
124   "EventCollector",
125   "devtools/server/actors/inspector/event-collector",
126   true
128 loader.lazyRequireGetter(
129   this,
130   "DocumentWalker",
131   "devtools/server/actors/inspector/document-walker",
132   true
134 loader.lazyRequireGetter(
135   this,
136   "scrollbarTreeWalkerFilter",
137   "devtools/server/actors/inspector/utils",
138   true
141 const SUBGRID_ENABLED = Services.prefs.getBoolPref(
142   "layout.css.grid-template-subgrid-value.enabled"
145 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
146 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
149  * Server side of the node actor.
150  */
151 const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
152   initialize: function(walker, node) {
153     protocol.Actor.prototype.initialize.call(this, null);
154     this.walker = walker;
155     this.rawNode = node;
156     this._eventCollector = new EventCollector(this.walker.targetActor);
158     // Store the original display type and scrollable state and whether or not the node is
159     // displayed to track changes when reflows occur.
160     this.currentDisplayType = this.displayType;
161     this.wasDisplayed = this.isDisplayed;
162     this.wasScrollable = this.isScrollable;
163   },
165   toString: function() {
166     return (
167       "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"
168     );
169   },
171   /**
172    * Instead of storing a connection object, the NodeActor gets its connection
173    * from its associated walker.
174    */
175   get conn() {
176     return this.walker.conn;
177   },
179   isDocumentElement: function() {
180     return (
181       this.rawNode.ownerDocument &&
182       this.rawNode.ownerDocument.documentElement === this.rawNode
183     );
184   },
186   destroy: function() {
187     protocol.Actor.prototype.destroy.call(this);
189     if (this.mutationObserver) {
190       if (!Cu.isDeadWrapper(this.mutationObserver)) {
191         this.mutationObserver.disconnect();
192       }
193       this.mutationObserver = null;
194     }
196     if (this.slotchangeListener) {
197       if (!InspectorActorUtils.isNodeDead(this)) {
198         this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
199       }
200       this.slotchangeListener = null;
201     }
203     this._eventCollector.destroy();
204     this._eventCollector = null;
205     this.rawNode = null;
206     this.walker = null;
207   },
209   // Returns the JSON representation of this object over the wire.
210   form: function() {
211     const parentNode = this.walker.parentNode(this);
212     const inlineTextChild = this.walker.inlineTextChild(this);
213     const shadowRoot = isShadowRoot(this.rawNode);
214     const hostActor = shadowRoot
215       ? this.walker.getNode(this.rawNode.host)
216       : null;
218     const form = {
219       actor: this.actorID,
220       host: hostActor ? hostActor.actorID : undefined,
221       baseURI: this.rawNode.baseURI,
222       parent: parentNode ? parentNode.actorID : undefined,
223       nodeType: this.rawNode.nodeType,
224       namespaceURI: this.rawNode.namespaceURI,
225       nodeName: this.rawNode.nodeName,
226       nodeValue: this.rawNode.nodeValue,
227       displayName: InspectorActorUtils.getNodeDisplayName(this.rawNode),
228       numChildren: this.numChildren,
229       inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
230       displayType: this.displayType,
231       isScrollable: this.isScrollable,
233       // doctype attributes
234       name: this.rawNode.name,
235       publicId: this.rawNode.publicId,
236       systemId: this.rawNode.systemId,
238       attrs: this.writeAttrs(),
239       customElementLocation: this.getCustomElementLocation(),
240       isMarkerPseudoElement: isMarkerPseudoElement(this.rawNode),
241       isBeforePseudoElement: isBeforePseudoElement(this.rawNode),
242       isAfterPseudoElement: isAfterPseudoElement(this.rawNode),
243       isAnonymous: isAnonymous(this.rawNode),
244       isNativeAnonymous: isNativeAnonymous(this.rawNode),
245       isShadowAnonymous: isShadowAnonymous(this.rawNode),
246       isShadowRoot: shadowRoot,
247       shadowRootMode: getShadowRootMode(this.rawNode),
248       isShadowHost: isShadowHost(this.rawNode),
249       isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode),
250       pseudoClassLocks: this.writePseudoClassLocks(),
251       mutationBreakpoints: this.walker.getMutationBreakpoints(this),
253       isDisplayed: this.isDisplayed,
254       isInHTMLDocument:
255         this.rawNode.ownerDocument &&
256         this.rawNode.ownerDocument.contentType === "text/html",
257       hasEventListeners: this._hasEventListeners,
258     };
260     if (this.isDocumentElement()) {
261       form.isDocumentElement = true;
262     }
264     // Flag the remote frame and declare at least one child (the #document element) so
265     // that they can be expanded.
266     if (this.isRemoteFrame) {
267       form.remoteFrame = true;
268       form.numChildren = 1;
269       form.browsingContextID = this.rawNode.browsingContext.id;
270     }
272     return form;
273   },
275   /**
276    * Watch the given document node for mutations using the DOM observer
277    * API.
278    */
279   watchDocument: function(doc, callback) {
280     const node = this.rawNode;
281     // Create the observer on the node's actor.  The node will make sure
282     // the observer is cleaned up when the actor is released.
283     const observer = new doc.defaultView.MutationObserver(callback);
284     observer.mergeAttributeRecords = true;
285     observer.observe(node, {
286       nativeAnonymousChildList: true,
287       attributes: true,
288       characterData: true,
289       characterDataOldValue: true,
290       childList: true,
291       subtree: true,
292     });
293     this.mutationObserver = observer;
294   },
296   /**
297    * Watch for all "slotchange" events on the node.
298    */
299   watchSlotchange: function(callback) {
300     this.slotchangeListener = callback;
301     this.rawNode.addEventListener("slotchange", this.slotchangeListener);
302   },
304   /**
305    * Check if the current node is representing a remote frame.
306    * In the context of the browser toolbox, a remote frame can be the <browser remote>
307    * element found inside each tab.
308    * In the context of the content toolbox, a remote frame can be a <iframe> that contains
309    * a different origin document.
310    */
311   get isRemoteFrame() {
312     return isRemoteFrame(this.rawNode);
313   },
315   // Estimate the number of children that the walker will return without making
316   // a call to children() if possible.
317   get numChildren() {
318     // For pseudo elements, childNodes.length returns 1, but the walker
319     // will return 0.
320     if (
321       isMarkerPseudoElement(this.rawNode) ||
322       isBeforePseudoElement(this.rawNode) ||
323       isAfterPseudoElement(this.rawNode)
324     ) {
325       return 0;
326     }
328     const rawNode = this.rawNode;
329     let numChildren = rawNode.childNodes.length;
330     const hasAnonChildren =
331       rawNode.nodeType === Node.ELEMENT_NODE &&
332       rawNode.ownerDocument.getAnonymousNodes(rawNode);
334     const hasContentDocument = rawNode.contentDocument;
335     const hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
336     if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
337       // This might be an iframe with virtual children.
338       numChildren = 1;
339     }
341     // Normal counting misses ::before/::after.  Also, some anonymous children
342     // may ultimately be skipped, so we have to consult with the walker.
343     if (
344       numChildren === 0 ||
345       hasAnonChildren ||
346       isShadowHost(this.rawNode) ||
347       isShadowAnonymous(this.rawNode)
348     ) {
349       numChildren = this.walker.countChildren(this);
350     }
352     return numChildren;
353   },
355   get computedStyle() {
356     if (!this._computedStyle) {
357       this._computedStyle = CssLogic.getComputedStyle(this.rawNode);
358     }
359     return this._computedStyle;
360   },
362   /**
363    * Returns the computed display style property value of the node.
364    */
365   get displayType() {
366     // Consider all non-element nodes as displayed.
367     if (
368       InspectorActorUtils.isNodeDead(this) ||
369       this.rawNode.nodeType !== Node.ELEMENT_NODE
370     ) {
371       return null;
372     }
374     const style = this.computedStyle;
375     if (!style) {
376       return null;
377     }
379     let display = null;
380     try {
381       display = style.display;
382     } catch (e) {
383       // Fails for <scrollbar> elements.
384     }
386     if (
387       SUBGRID_ENABLED &&
388       (display === "grid" || display === "inline-grid") &&
389       (style.gridTemplateRows.startsWith("subgrid") ||
390         style.gridTemplateColumns.startsWith("subgrid"))
391     ) {
392       display = "subgrid";
393     }
395     return display;
396   },
398   /**
399    * Check whether the node currently has scrollbars and is scrollable.
400    */
401   get isScrollable() {
402     // Check first if the element has an overflow area, bail out if not.
403     if (
404       this.rawNode.clientHeight === this.rawNode.scrollHeight &&
405       this.rawNode.clientWidth === this.rawNode.scrollWidth
406     ) {
407       return false;
408     }
410     // If it does, then check it also has scrollbars.
411     try {
412       const walker = new DocumentWalker(
413         this.rawNode,
414         this.rawNode.ownerGlobal,
415         { filter: scrollbarTreeWalkerFilter }
416       );
417       return !!walker.firstChild();
418     } catch (e) {
419       // We have no access to a DOM object. This is probably due to a CORS
420       // violation. Using try / catch is the only way to avoid this error.
421       return false;
422     }
423   },
425   /**
426    * Is the node currently displayed?
427    */
428   get isDisplayed() {
429     const type = this.displayType;
431     // Consider all non-elements or elements with no display-types to be displayed.
432     if (!type) {
433       return true;
434     }
436     // Otherwise consider elements to be displayed only if their display-types is other
437     // than "none"".
438     return type !== "none";
439   },
441   /**
442    * Are there event listeners that are listening on this node? This method
443    * uses all parsers registered via event-parsers.js.registerEventParser() to
444    * check if there are any event listeners.
445    */
446   get _hasEventListeners() {
447     // We need to pass a debugger instance from this compartment because
448     // otherwise we can't make use of it inside the event-collector module.
449     const dbg = this.parent().targetActor.makeDebugger();
450     return this._eventCollector.hasEventListeners(this.rawNode, dbg);
451   },
453   writeAttrs: function() {
454     // If the node has no attributes or this.rawNode is the document node and a
455     // node with `name="attributes"` exists in the DOM we need to bail.
456     if (
457       !this.rawNode.attributes ||
458       !(this.rawNode.attributes instanceof NamedNodeMap)
459     ) {
460       return undefined;
461     }
463     return [...this.rawNode.attributes].map(attr => {
464       return { namespace: attr.namespace, name: attr.name, value: attr.value };
465     });
466   },
468   writePseudoClassLocks: function() {
469     if (this.rawNode.nodeType !== Node.ELEMENT_NODE) {
470       return undefined;
471     }
472     let ret = undefined;
473     for (const pseudo of PSEUDO_CLASSES) {
474       if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
475         ret = ret || [];
476         ret.push(pseudo);
477       }
478     }
479     return ret;
480   },
482   /**
483    * Gets event listeners and adds their information to the events array.
484    *
485    * @param  {Node} node
486    *         Node for which we are to get listeners.
487    */
488   getEventListeners: function(node) {
489     return this._eventCollector.getEventListeners(node);
490   },
492   /**
493    * Retrieve the script location of the custom element definition for this node, when
494    * relevant. To be linked to a custom element definition
495    */
496   getCustomElementLocation: function() {
497     // Get a reference to the custom element definition function.
498     const name = this.rawNode.localName;
500     if (!this.rawNode.ownerGlobal) {
501       return undefined;
502     }
504     const customElementsRegistry = this.rawNode.ownerGlobal.customElements;
505     const customElement =
506       customElementsRegistry && customElementsRegistry.get(name);
507     if (!customElement) {
508       return undefined;
509     }
510     // Create debugger object for the customElement function.
511     const global = Cu.getGlobalForObject(customElement);
512     const dbg = this.parent().targetActor.makeDebugger();
513     const globalDO = dbg.addDebuggee(global);
514     const customElementDO = globalDO.makeDebuggeeValue(customElement);
516     // Return undefined if we can't find a script for the custom element definition.
517     if (!customElementDO.script) {
518       return undefined;
519     }
521     return {
522       url: customElementDO.script.url,
523       line: customElementDO.script.startLine,
524     };
525   },
527   /**
528    * Returns a LongStringActor with the node's value.
529    */
530   getNodeValue: function() {
531     return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
532   },
534   /**
535    * Set the node's value to a given string.
536    */
537   setNodeValue: function(value) {
538     this.rawNode.nodeValue = value;
539   },
541   /**
542    * Get a unique selector string for this node.
543    */
544   getUniqueSelector: function() {
545     if (Cu.isDeadWrapper(this.rawNode)) {
546       return "";
547     }
548     return findCssSelector(this.rawNode);
549   },
551   /**
552    * Get the full CSS path for this node.
553    *
554    * @return {String} A CSS selector with a part for the node and each of its ancestors.
555    */
556   getCssPath: function() {
557     if (Cu.isDeadWrapper(this.rawNode)) {
558       return "";
559     }
560     return getCssPath(this.rawNode);
561   },
563   /**
564    * Get the XPath for this node.
565    *
566    * @return {String} The XPath for finding this node on the page.
567    */
568   getXPath: function() {
569     if (Cu.isDeadWrapper(this.rawNode)) {
570       return "";
571     }
572     return getXPath(this.rawNode);
573   },
575   /**
576    * Scroll the selected node into view.
577    */
578   scrollIntoView: function() {
579     this.rawNode.scrollIntoView(true);
580   },
582   /**
583    * Get the node's image data if any (for canvas and img nodes).
584    * Returns an imageData object with the actual data being a LongStringActor
585    * and a size json object.
586    * The image data is transmitted as a base64 encoded png data-uri.
587    * The method rejects if the node isn't an image or if the image is missing
588    *
589    * Accepts a maxDim request parameter to resize images that are larger. This
590    * is important as the resizing occurs server-side so that image-data being
591    * transfered in the longstring back to the client will be that much smaller
592    */
593   getImageData: function(maxDim) {
594     return InspectorActorUtils.imageToImageData(this.rawNode, maxDim).then(
595       imageData => {
596         return {
597           data: LongStringActor(this.conn, imageData.data),
598           size: imageData.size,
599         };
600       }
601     );
602   },
604   /**
605    * Get all event listeners that are listening on this node.
606    */
607   getEventListenerInfo: function() {
608     return this.getEventListeners(this.rawNode);
609   },
611   /**
612    * Modify a node's attributes.  Passed an array of modifications
613    * similar in format to "attributes" mutations.
614    * {
615    *   attributeName: <string>
616    *   attributeNamespace: <optional string>
617    *   newValue: <optional string> - If null or undefined, the attribute
618    *     will be removed.
619    * }
620    *
621    * Returns when the modifications have been made.  Mutations will
622    * be queued for any changes made.
623    */
624   modifyAttributes: function(modifications) {
625     const rawNode = this.rawNode;
626     for (const change of modifications) {
627       if (change.newValue == null) {
628         if (change.attributeNamespace) {
629           rawNode.removeAttributeNS(
630             change.attributeNamespace,
631             change.attributeName
632           );
633         } else {
634           rawNode.removeAttribute(change.attributeName);
635         }
636       } else if (change.attributeNamespace) {
637         rawNode.setAttributeNS(
638           change.attributeNamespace,
639           change.attributeName,
640           change.newValue
641         );
642       } else {
643         rawNode.setAttribute(change.attributeName, change.newValue);
644       }
645     }
646   },
648   /**
649    * Given the font and fill style, get the image data of a canvas with the
650    * preview text and font.
651    * Returns an imageData object with the actual data being a LongStringActor
652    * and the width of the text as a string.
653    * The image data is transmitted as a base64 encoded png data-uri.
654    */
655   getFontFamilyDataURL: function(font, fillStyle = "black") {
656     const doc = this.rawNode.ownerDocument;
657     const options = {
658       previewText: FONT_FAMILY_PREVIEW_TEXT,
659       previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
660       fillStyle: fillStyle,
661     };
662     const { dataURL, size } = getFontPreviewData(font, doc, options);
664     return { data: LongStringActor(this.conn, dataURL), size: size };
665   },
667   /**
668    * Finds the computed background color of the closest parent with a set background
669    * color.
670    *
671    * @return {String}
672    *         String with the background color of the form rgba(r, g, b, a). Defaults to
673    *         rgba(255, 255, 255, 1) if no background color is found.
674    */
675   getClosestBackgroundColor: function() {
676     return InspectorActorUtils.getClosestBackgroundColor(this.rawNode);
677   },
679   /**
680    * Finds the background color range for the parent of a single text node
681    * (i.e. for multi-colored backgrounds with gradients, images) or a single
682    * background color for single-colored backgrounds. Defaults to the closest
683    * background color if an error is encountered.
684    *
685    * @return {Object}
686    *         Object with one or more of the following properties: value, min, max
687    */
688   getBackgroundColor: function() {
689     return InspectorActorUtils.getBackgroundColor(this);
690   },
692   /**
693    * Returns an object with the width and height of the node's owner window.
694    *
695    * @return {Object}
696    */
697   getOwnerGlobalDimensions: function() {
698     const win = this.rawNode.ownerGlobal;
699     return {
700       innerWidth: win.innerWidth,
701       innerHeight: win.innerHeight,
702     };
703   },
707  * Server side of a node list as returned by querySelectorAll()
708  */
709 const NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
710   typeName: "domnodelist",
712   initialize: function(walker, nodeList) {
713     protocol.Actor.prototype.initialize.call(this);
714     this.walker = walker;
715     this.nodeList = nodeList || [];
716   },
718   destroy: function() {
719     protocol.Actor.prototype.destroy.call(this);
720   },
722   /**
723    * Instead of storing a connection object, the NodeActor gets its connection
724    * from its associated walker.
725    */
726   get conn() {
727     return this.walker.conn;
728   },
730   /**
731    * Items returned by this actor should belong to the parent walker.
732    */
733   marshallPool: function() {
734     return this.walker;
735   },
737   // Returns the JSON representation of this object over the wire.
738   form: function() {
739     return {
740       actor: this.actorID,
741       length: this.nodeList ? this.nodeList.length : 0,
742     };
743   },
745   /**
746    * Get a single node from the node list.
747    */
748   item: function(index) {
749     return this.walker.attachElement(this.nodeList[index]);
750   },
752   /**
753    * Get a range of the items from the node list.
754    */
755   items: function(start = 0, end = this.nodeList.length) {
756     const items = Array.prototype.slice
757       .call(this.nodeList, start, end)
758       .map(item => this.walker._ref(item));
759     return this.walker.attachElements(items);
760   },
762   release: function() {},
765 exports.NodeActor = NodeActor;
766 exports.NodeListActor = NodeListActor;