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/. */
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(
16 "devtools/shared/inspector/css-logic",
19 loader.lazyRequireGetter(
22 "devtools/shared/inspector/css-logic",
25 loader.lazyRequireGetter(
28 "devtools/shared/inspector/css-logic",
32 loader.lazyRequireGetter(
34 "isAfterPseudoElement",
35 "devtools/shared/layout/utils",
38 loader.lazyRequireGetter(
41 "devtools/shared/layout/utils",
44 loader.lazyRequireGetter(
46 "isBeforePseudoElement",
47 "devtools/shared/layout/utils",
50 loader.lazyRequireGetter(
52 "isDirectShadowHostChild",
53 "devtools/shared/layout/utils",
56 loader.lazyRequireGetter(
58 "isMarkerPseudoElement",
59 "devtools/shared/layout/utils",
62 loader.lazyRequireGetter(
65 "devtools/shared/layout/utils",
68 loader.lazyRequireGetter(
71 "devtools/shared/layout/utils",
74 loader.lazyRequireGetter(
77 "devtools/shared/layout/utils",
80 loader.lazyRequireGetter(
83 "devtools/shared/layout/utils",
86 loader.lazyRequireGetter(
89 "devtools/shared/layout/utils",
92 loader.lazyRequireGetter(
95 "devtools/shared/layout/utils",
99 loader.lazyRequireGetter(
101 "InspectorActorUtils",
102 "devtools/server/actors/inspector/utils"
104 loader.lazyRequireGetter(
107 "devtools/server/actors/string",
110 loader.lazyRequireGetter(
112 "getFontPreviewData",
113 "devtools/server/actors/styles",
116 loader.lazyRequireGetter(
119 "devtools/server/actors/inspector/css-logic",
122 loader.lazyRequireGetter(
125 "devtools/server/actors/inspector/event-collector",
128 loader.lazyRequireGetter(
131 "devtools/server/actors/inspector/document-walker",
134 loader.lazyRequireGetter(
136 "scrollbarTreeWalkerFilter",
137 "devtools/server/actors/inspector/utils",
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.
151 const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
152 initialize: function(walker, node) {
153 protocol.Actor.prototype.initialize.call(this, null);
154 this.walker = walker;
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;
165 toString: function() {
167 "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"
172 * Instead of storing a connection object, the NodeActor gets its connection
173 * from its associated walker.
176 return this.walker.conn;
179 isDocumentElement: function() {
181 this.rawNode.ownerDocument &&
182 this.rawNode.ownerDocument.documentElement === this.rawNode
186 destroy: function() {
187 protocol.Actor.prototype.destroy.call(this);
189 if (this.mutationObserver) {
190 if (!Cu.isDeadWrapper(this.mutationObserver)) {
191 this.mutationObserver.disconnect();
193 this.mutationObserver = null;
196 if (this.slotchangeListener) {
197 if (!InspectorActorUtils.isNodeDead(this)) {
198 this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
200 this.slotchangeListener = null;
203 this._eventCollector.destroy();
204 this._eventCollector = null;
209 // Returns the JSON representation of this object over the wire.
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)
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,
255 this.rawNode.ownerDocument &&
256 this.rawNode.ownerDocument.contentType === "text/html",
257 hasEventListeners: this._hasEventListeners,
260 if (this.isDocumentElement()) {
261 form.isDocumentElement = true;
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;
276 * Watch the given document node for mutations using the DOM observer
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,
289 characterDataOldValue: true,
293 this.mutationObserver = observer;
297 * Watch for all "slotchange" events on the node.
299 watchSlotchange: function(callback) {
300 this.slotchangeListener = callback;
301 this.rawNode.addEventListener("slotchange", this.slotchangeListener);
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.
311 get isRemoteFrame() {
312 return isRemoteFrame(this.rawNode);
315 // Estimate the number of children that the walker will return without making
316 // a call to children() if possible.
318 // For pseudo elements, childNodes.length returns 1, but the walker
321 isMarkerPseudoElement(this.rawNode) ||
322 isBeforePseudoElement(this.rawNode) ||
323 isAfterPseudoElement(this.rawNode)
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.
341 // Normal counting misses ::before/::after. Also, some anonymous children
342 // may ultimately be skipped, so we have to consult with the walker.
346 isShadowHost(this.rawNode) ||
347 isShadowAnonymous(this.rawNode)
349 numChildren = this.walker.countChildren(this);
355 get computedStyle() {
356 if (!this._computedStyle) {
357 this._computedStyle = CssLogic.getComputedStyle(this.rawNode);
359 return this._computedStyle;
363 * Returns the computed display style property value of the node.
366 // Consider all non-element nodes as displayed.
368 InspectorActorUtils.isNodeDead(this) ||
369 this.rawNode.nodeType !== Node.ELEMENT_NODE
374 const style = this.computedStyle;
381 display = style.display;
383 // Fails for <scrollbar> elements.
388 (display === "grid" || display === "inline-grid") &&
389 (style.gridTemplateRows.startsWith("subgrid") ||
390 style.gridTemplateColumns.startsWith("subgrid"))
399 * Check whether the node currently has scrollbars and is scrollable.
402 // Check first if the element has an overflow area, bail out if not.
404 this.rawNode.clientHeight === this.rawNode.scrollHeight &&
405 this.rawNode.clientWidth === this.rawNode.scrollWidth
410 // If it does, then check it also has scrollbars.
412 const walker = new DocumentWalker(
414 this.rawNode.ownerGlobal,
415 { filter: scrollbarTreeWalkerFilter }
417 return !!walker.firstChild();
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.
426 * Is the node currently displayed?
429 const type = this.displayType;
431 // Consider all non-elements or elements with no display-types to be displayed.
436 // Otherwise consider elements to be displayed only if their display-types is other
438 return type !== "none";
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.
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);
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.
457 !this.rawNode.attributes ||
458 !(this.rawNode.attributes instanceof NamedNodeMap)
463 return [...this.rawNode.attributes].map(attr => {
464 return { namespace: attr.namespace, name: attr.name, value: attr.value };
468 writePseudoClassLocks: function() {
469 if (this.rawNode.nodeType !== Node.ELEMENT_NODE) {
473 for (const pseudo of PSEUDO_CLASSES) {
474 if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
483 * Gets event listeners and adds their information to the events array.
486 * Node for which we are to get listeners.
488 getEventListeners: function(node) {
489 return this._eventCollector.getEventListeners(node);
493 * Retrieve the script location of the custom element definition for this node, when
494 * relevant. To be linked to a custom element definition
496 getCustomElementLocation: function() {
497 // Get a reference to the custom element definition function.
498 const name = this.rawNode.localName;
500 if (!this.rawNode.ownerGlobal) {
504 const customElementsRegistry = this.rawNode.ownerGlobal.customElements;
505 const customElement =
506 customElementsRegistry && customElementsRegistry.get(name);
507 if (!customElement) {
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) {
522 url: customElementDO.script.url,
523 line: customElementDO.script.startLine,
528 * Returns a LongStringActor with the node's value.
530 getNodeValue: function() {
531 return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
535 * Set the node's value to a given string.
537 setNodeValue: function(value) {
538 this.rawNode.nodeValue = value;
542 * Get a unique selector string for this node.
544 getUniqueSelector: function() {
545 if (Cu.isDeadWrapper(this.rawNode)) {
548 return findCssSelector(this.rawNode);
552 * Get the full CSS path for this node.
554 * @return {String} A CSS selector with a part for the node and each of its ancestors.
556 getCssPath: function() {
557 if (Cu.isDeadWrapper(this.rawNode)) {
560 return getCssPath(this.rawNode);
564 * Get the XPath for this node.
566 * @return {String} The XPath for finding this node on the page.
568 getXPath: function() {
569 if (Cu.isDeadWrapper(this.rawNode)) {
572 return getXPath(this.rawNode);
576 * Scroll the selected node into view.
578 scrollIntoView: function() {
579 this.rawNode.scrollIntoView(true);
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
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
593 getImageData: function(maxDim) {
594 return InspectorActorUtils.imageToImageData(this.rawNode, maxDim).then(
597 data: LongStringActor(this.conn, imageData.data),
598 size: imageData.size,
605 * Get all event listeners that are listening on this node.
607 getEventListenerInfo: function() {
608 return this.getEventListeners(this.rawNode);
612 * Modify a node's attributes. Passed an array of modifications
613 * similar in format to "attributes" mutations.
615 * attributeName: <string>
616 * attributeNamespace: <optional string>
617 * newValue: <optional string> - If null or undefined, the attribute
621 * Returns when the modifications have been made. Mutations will
622 * be queued for any changes made.
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,
634 rawNode.removeAttribute(change.attributeName);
636 } else if (change.attributeNamespace) {
637 rawNode.setAttributeNS(
638 change.attributeNamespace,
639 change.attributeName,
643 rawNode.setAttribute(change.attributeName, change.newValue);
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.
655 getFontFamilyDataURL: function(font, fillStyle = "black") {
656 const doc = this.rawNode.ownerDocument;
658 previewText: FONT_FAMILY_PREVIEW_TEXT,
659 previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
660 fillStyle: fillStyle,
662 const { dataURL, size } = getFontPreviewData(font, doc, options);
664 return { data: LongStringActor(this.conn, dataURL), size: size };
668 * Finds the computed background color of the closest parent with a set background
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.
675 getClosestBackgroundColor: function() {
676 return InspectorActorUtils.getClosestBackgroundColor(this.rawNode);
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.
686 * Object with one or more of the following properties: value, min, max
688 getBackgroundColor: function() {
689 return InspectorActorUtils.getBackgroundColor(this);
693 * Returns an object with the width and height of the node's owner window.
697 getOwnerGlobalDimensions: function() {
698 const win = this.rawNode.ownerGlobal;
700 innerWidth: win.innerWidth,
701 innerHeight: win.innerHeight,
707 * Server side of a node list as returned by querySelectorAll()
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 || [];
718 destroy: function() {
719 protocol.Actor.prototype.destroy.call(this);
723 * Instead of storing a connection object, the NodeActor gets its connection
724 * from its associated walker.
727 return this.walker.conn;
731 * Items returned by this actor should belong to the parent walker.
733 marshallPool: function() {
737 // Returns the JSON representation of this object over the wire.
741 length: this.nodeList ? this.nodeList.length : 0,
746 * Get a single node from the node list.
748 item: function(index) {
749 return this.walker.attachElement(this.nodeList[index]);
753 * Get a range of the items from the node list.
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);
762 release: function() {},
765 exports.NodeActor = NodeActor;
766 exports.NodeListActor = NodeListActor;