885 lines
26 KiB
JavaScript
885 lines
26 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { Actor } = require("resource://devtools/shared/protocol.js");
|
|
const {
|
|
nodeSpec,
|
|
nodeListSpec,
|
|
} = require("resource://devtools/shared/specs/node.js");
|
|
|
|
const {
|
|
PSEUDO_CLASSES,
|
|
} = require("resource://devtools/shared/css/constants.js");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
["getCssPath", "getXPath", "findCssSelector"],
|
|
"resource://devtools/shared/inspector/css-logic.js",
|
|
true
|
|
);
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
[
|
|
"getShadowRootMode",
|
|
"isAfterPseudoElement",
|
|
"isAnonymous",
|
|
"isBeforePseudoElement",
|
|
"isDirectShadowHostChild",
|
|
"isFrameBlockedByCSP",
|
|
"isFrameWithChildTarget",
|
|
"isMarkerPseudoElement",
|
|
"isNativeAnonymous",
|
|
"isShadowHost",
|
|
"isShadowRoot",
|
|
],
|
|
"resource://devtools/shared/layout/utils.js",
|
|
true
|
|
);
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
[
|
|
"getBackgroundColor",
|
|
"getClosestBackgroundColor",
|
|
"getNodeDisplayName",
|
|
"imageToImageData",
|
|
"isNodeDead",
|
|
],
|
|
"resource://devtools/server/actors/inspector/utils.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"LongStringActor",
|
|
"resource://devtools/server/actors/string.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"getFontPreviewData",
|
|
"resource://devtools/server/actors/utils/style-utils.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"CssLogic",
|
|
"resource://devtools/server/actors/inspector/css-logic.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"EventCollector",
|
|
"resource://devtools/server/actors/inspector/event-collector.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"DOMHelpers",
|
|
"resource://devtools/shared/dom-helpers.js",
|
|
true
|
|
);
|
|
|
|
const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
|
|
const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
|
|
|
|
/**
|
|
* Server side of the node actor.
|
|
*/
|
|
class NodeActor extends Actor {
|
|
constructor(walker, node) {
|
|
super(walker.conn, nodeSpec);
|
|
this.walker = walker;
|
|
this.rawNode = node;
|
|
this._eventCollector = new EventCollector(this.walker.targetActor);
|
|
// Map<id -> nsIEventListenerInfo> that we maintain to be able to disable/re-enable event listeners
|
|
// The id is generated from getEventListenerInfo
|
|
this._nsIEventListenersInfo = new Map();
|
|
|
|
// Store the original display type and scrollable state and whether or not the node is
|
|
// displayed to track changes when reflows occur.
|
|
const wasScrollable = this.isScrollable;
|
|
|
|
this.currentDisplayType = this.displayType;
|
|
this.wasDisplayed = this.isDisplayed;
|
|
this.wasScrollable = wasScrollable;
|
|
this.currentContainerType = this.containerType;
|
|
|
|
if (wasScrollable) {
|
|
this.walker.updateOverflowCausingElements(
|
|
this,
|
|
this.walker.overflowCausingElementsMap
|
|
);
|
|
}
|
|
}
|
|
|
|
toString() {
|
|
return (
|
|
"[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"
|
|
);
|
|
}
|
|
|
|
isDocumentElement() {
|
|
return (
|
|
this.rawNode.ownerDocument &&
|
|
this.rawNode.ownerDocument.documentElement === this.rawNode
|
|
);
|
|
}
|
|
|
|
destroy() {
|
|
super.destroy();
|
|
|
|
if (this.mutationObserver) {
|
|
if (!Cu.isDeadWrapper(this.mutationObserver)) {
|
|
this.mutationObserver.disconnect();
|
|
}
|
|
this.mutationObserver = null;
|
|
}
|
|
|
|
if (this.slotchangeListener) {
|
|
if (!isNodeDead(this)) {
|
|
this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
|
|
}
|
|
this.slotchangeListener = null;
|
|
}
|
|
|
|
if (this._waitForFrameLoadAbortController) {
|
|
this._waitForFrameLoadAbortController.abort();
|
|
this._waitForFrameLoadAbortController = null;
|
|
}
|
|
if (this._waitForFrameLoadIntervalId) {
|
|
clearInterval(this._waitForFrameLoadIntervalId);
|
|
this._waitForFrameLoadIntervalId = null;
|
|
}
|
|
|
|
if (this._nsIEventListenersInfo) {
|
|
// Re-enable all event listeners that we might have disabled
|
|
for (const nsIEventListenerInfo of this._nsIEventListenersInfo.values()) {
|
|
// If event listeners/node don't exist anymore, accessing nsIEventListenerInfo.enabled
|
|
// will throw.
|
|
try {
|
|
if (!nsIEventListenerInfo.enabled) {
|
|
nsIEventListenerInfo.enabled = true;
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
}
|
|
this._nsIEventListenersInfo = null;
|
|
}
|
|
|
|
this._eventCollector.destroy();
|
|
this._eventCollector = null;
|
|
this.rawNode = null;
|
|
this.walker = null;
|
|
}
|
|
|
|
// Returns the JSON representation of this object over the wire.
|
|
form() {
|
|
const parentNode = this.walker.parentNode(this);
|
|
const inlineTextChild = this.walker.inlineTextChild(this);
|
|
const shadowRoot = isShadowRoot(this.rawNode);
|
|
const hostActor = shadowRoot
|
|
? this.walker.getNode(this.rawNode.host)
|
|
: null;
|
|
const nodeType = this.rawNode.nodeType;
|
|
|
|
const form = {
|
|
actor: this.actorID,
|
|
host: hostActor ? hostActor.actorID : undefined,
|
|
baseURI: this.rawNode.baseURI,
|
|
parent: parentNode ? parentNode.actorID : undefined,
|
|
nodeType,
|
|
namespaceURI: this.rawNode.namespaceURI,
|
|
nodeName: this.rawNode.nodeName,
|
|
nodeValue: this.rawNode.nodeValue,
|
|
displayName: getNodeDisplayName(this.rawNode),
|
|
numChildren: this.numChildren,
|
|
inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
|
|
displayType: this.displayType,
|
|
isScrollable: this.isScrollable,
|
|
isTopLevelDocument: this.isTopLevelDocument,
|
|
causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode),
|
|
containerType: this.containerType,
|
|
|
|
// doctype attributes
|
|
name: this.rawNode.name,
|
|
publicId: this.rawNode.publicId,
|
|
systemId: this.rawNode.systemId,
|
|
|
|
attrs: this.writeAttrs(),
|
|
customElementLocation: this.getCustomElementLocation(),
|
|
isMarkerPseudoElement: isMarkerPseudoElement(this.rawNode),
|
|
isBeforePseudoElement: isBeforePseudoElement(this.rawNode),
|
|
isAfterPseudoElement: isAfterPseudoElement(this.rawNode),
|
|
isAnonymous: isAnonymous(this.rawNode),
|
|
isNativeAnonymous: isNativeAnonymous(this.rawNode),
|
|
isShadowRoot: shadowRoot,
|
|
shadowRootMode: getShadowRootMode(this.rawNode),
|
|
isShadowHost: isShadowHost(this.rawNode),
|
|
isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode),
|
|
pseudoClassLocks: this.writePseudoClassLocks(),
|
|
mutationBreakpoints: this.walker.getMutationBreakpoints(this),
|
|
|
|
isDisplayed: this.isDisplayed,
|
|
isInHTMLDocument:
|
|
this.rawNode.ownerDocument &&
|
|
this.rawNode.ownerDocument.contentType === "text/html",
|
|
traits: {},
|
|
};
|
|
|
|
// The event collector can be expensive, so only check for events on nodes that
|
|
// can display the `event` badge.
|
|
if (
|
|
nodeType !== Node.COMMENT_NODE &&
|
|
nodeType !== Node.TEXT_NODE &&
|
|
nodeType !== Node.CDATA_SECTION_NODE &&
|
|
nodeType !== Node.DOCUMENT_NODE &&
|
|
nodeType !== Node.DOCUMENT_TYPE_NODE &&
|
|
!form.isMarkerPseudoElement &&
|
|
!form.isBeforePseudoElement &&
|
|
!form.isAfterPseudoElement
|
|
) {
|
|
form.hasEventListeners = this.hasEventListeners();
|
|
}
|
|
|
|
if (this.isDocumentElement()) {
|
|
form.isDocumentElement = true;
|
|
}
|
|
|
|
if (isFrameBlockedByCSP(this.rawNode)) {
|
|
form.numChildren = 0;
|
|
}
|
|
|
|
// Flag the node if a different walker is needed to retrieve its children (i.e. if
|
|
// this is a remote frame, or if it's an iframe and we're creating targets for every iframes)
|
|
if (this.useChildTargetToFetchChildren) {
|
|
form.useChildTargetToFetchChildren = true;
|
|
// Declare at least one child (the #document element) so
|
|
// that they can be expanded.
|
|
form.numChildren = 1;
|
|
}
|
|
form.browsingContextID = this.rawNode.browsingContext?.id;
|
|
|
|
return form;
|
|
}
|
|
|
|
/**
|
|
* Watch the given document node for mutations using the DOM observer
|
|
* API.
|
|
*/
|
|
watchDocument(doc, callback) {
|
|
if (!doc.defaultView) {
|
|
return;
|
|
}
|
|
|
|
const node = this.rawNode;
|
|
// Create the observer on the node's actor. The node will make sure
|
|
// the observer is cleaned up when the actor is released.
|
|
const observer = new doc.defaultView.MutationObserver(callback);
|
|
observer.mergeAttributeRecords = true;
|
|
observer.observe(node, {
|
|
attributes: true,
|
|
characterData: true,
|
|
characterDataOldValue: true,
|
|
childList: true,
|
|
subtree: true,
|
|
// Track addition/removal of pseudo-elements too
|
|
chromeOnlyNodes: true,
|
|
});
|
|
this.mutationObserver = observer;
|
|
}
|
|
|
|
/**
|
|
* Watch for all "slotchange" events on the node.
|
|
*/
|
|
watchSlotchange(callback) {
|
|
this.slotchangeListener = callback;
|
|
this.rawNode.addEventListener("slotchange", this.slotchangeListener);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node represents an element (e.g. an iframe) which has a dedicated
|
|
* target for its underlying document that we would need to use to fetch the child nodes.
|
|
* This will be the case for iframes if EFT is enabled, or if this is a remote iframe and
|
|
* fission is enabled.
|
|
*/
|
|
get useChildTargetToFetchChildren() {
|
|
return isFrameWithChildTarget(this.walker.targetActor, this.rawNode);
|
|
}
|
|
|
|
get isTopLevelDocument() {
|
|
return this.rawNode === this.walker.rootDoc;
|
|
}
|
|
|
|
// Estimate the number of children that the walker will return without making
|
|
// a call to children() if possible.
|
|
get numChildren() {
|
|
// For pseudo elements, childNodes.length returns 1, but the walker
|
|
// will return 0.
|
|
if (
|
|
isMarkerPseudoElement(this.rawNode) ||
|
|
isBeforePseudoElement(this.rawNode) ||
|
|
isAfterPseudoElement(this.rawNode)
|
|
) {
|
|
return 0;
|
|
}
|
|
|
|
const rawNode = this.rawNode;
|
|
let numChildren = rawNode.childNodes.length;
|
|
const hasContentDocument = rawNode.contentDocument;
|
|
const hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
|
|
if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
|
|
// This might be an iframe with virtual children.
|
|
numChildren = 1;
|
|
}
|
|
|
|
// Normal counting misses ::before/::after. Also, some anonymous children
|
|
// may ultimately be skipped, so we have to consult with the walker.
|
|
//
|
|
// FIXME: We should be able to just check <slot> rather than
|
|
// containingShadowRoot.
|
|
if (
|
|
numChildren === 0 ||
|
|
isShadowHost(this.rawNode) ||
|
|
this.rawNode.containingShadowRoot
|
|
) {
|
|
numChildren = this.walker.countChildren(this);
|
|
}
|
|
|
|
return numChildren;
|
|
}
|
|
|
|
get computedStyle() {
|
|
if (!this._computedStyle) {
|
|
this._computedStyle = CssLogic.getComputedStyle(this.rawNode);
|
|
}
|
|
return this._computedStyle;
|
|
}
|
|
|
|
/**
|
|
* Returns the computed display style property value of the node.
|
|
*/
|
|
get displayType() {
|
|
// Consider all non-element nodes as displayed.
|
|
if (isNodeDead(this) || this.rawNode.nodeType !== Node.ELEMENT_NODE) {
|
|
return null;
|
|
}
|
|
|
|
const style = this.computedStyle;
|
|
if (!style) {
|
|
return null;
|
|
}
|
|
|
|
let display = null;
|
|
try {
|
|
display = style.display;
|
|
} catch (e) {
|
|
// Fails for <scrollbar> elements.
|
|
}
|
|
|
|
if (
|
|
(display === "grid" || display === "inline-grid") &&
|
|
(style.gridTemplateRows.startsWith("subgrid") ||
|
|
style.gridTemplateColumns.startsWith("subgrid"))
|
|
) {
|
|
display = "subgrid";
|
|
}
|
|
|
|
return display;
|
|
}
|
|
|
|
/**
|
|
* Returns the computed containerType style property value of the node.
|
|
*/
|
|
get containerType() {
|
|
// non-element nodes can't be containers
|
|
if (
|
|
isNodeDead(this) ||
|
|
this.rawNode.nodeType !== Node.ELEMENT_NODE ||
|
|
!this.computedStyle
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return this.computedStyle.containerType;
|
|
}
|
|
|
|
/**
|
|
* Check whether the node currently has scrollbars and is scrollable.
|
|
*/
|
|
get isScrollable() {
|
|
return (
|
|
this.rawNode.nodeType === Node.ELEMENT_NODE &&
|
|
this.rawNode.hasVisibleScrollbars
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Is the node currently displayed?
|
|
*/
|
|
get isDisplayed() {
|
|
const type = this.displayType;
|
|
|
|
// Consider all non-elements or elements with no display-types to be displayed.
|
|
if (!type) {
|
|
return true;
|
|
}
|
|
|
|
// Otherwise consider elements to be displayed only if their display-types is other
|
|
// than "none"".
|
|
return type !== "none";
|
|
}
|
|
|
|
/**
|
|
* Are there event listeners that are listening on this node? This method
|
|
* uses all parsers registered via event-parsers.js.registerEventParser() to
|
|
* check if there are any event listeners.
|
|
*
|
|
* @returns {Boolean}
|
|
*/
|
|
hasEventListeners(refreshCache = false) {
|
|
if (this._hasEventListenersCached === undefined || refreshCache) {
|
|
const result = this._eventCollector.hasEventListeners(this.rawNode);
|
|
this._hasEventListenersCached = result;
|
|
}
|
|
return this._hasEventListenersCached;
|
|
}
|
|
|
|
writeAttrs() {
|
|
// If the node has no attributes or this.rawNode is the document node and a
|
|
// node with `name="attributes"` exists in the DOM we need to bail.
|
|
if (
|
|
!this.rawNode.attributes ||
|
|
!NamedNodeMap.isInstance(this.rawNode.attributes)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return [...this.rawNode.attributes].map(attr => {
|
|
return { namespace: attr.namespace, name: attr.name, value: attr.value };
|
|
});
|
|
}
|
|
|
|
writePseudoClassLocks() {
|
|
if (this.rawNode.nodeType !== Node.ELEMENT_NODE) {
|
|
return undefined;
|
|
}
|
|
let ret = undefined;
|
|
for (const pseudo of PSEUDO_CLASSES) {
|
|
if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
|
|
ret = ret || [];
|
|
ret.push(pseudo);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the script location of the custom element definition for this node, when
|
|
* relevant. To be linked to a custom element definition
|
|
*/
|
|
getCustomElementLocation() {
|
|
// Get a reference to the custom element definition function.
|
|
const name = this.rawNode.localName;
|
|
|
|
if (!this.rawNode.ownerGlobal) {
|
|
return undefined;
|
|
}
|
|
|
|
const customElementsRegistry = this.rawNode.ownerGlobal.customElements;
|
|
const customElement =
|
|
customElementsRegistry && customElementsRegistry.get(name);
|
|
if (!customElement) {
|
|
return undefined;
|
|
}
|
|
// Create debugger object for the customElement function.
|
|
const global = Cu.getGlobalForObject(customElement);
|
|
|
|
const dbg = this.getParent().targetActor.makeDebugger();
|
|
|
|
// If we hit a <browser> element of Firefox, its global will be the chrome window
|
|
// which is system principal and will be in the same compartment as the debuggee.
|
|
// For some reason, this happens when we run the content toolbox. As for the content
|
|
// toolboxes, the modules are loaded in the same compartment as the <browser> element,
|
|
// this throws as the debugger can _not_ be in the same compartment as the debugger.
|
|
// This happens when we toggle fission for content toolbox because we try to reparent
|
|
// the Walker of the tab. This happens because we do not detect in Walker.reparentRemoteFrame
|
|
// that the target of the tab is the top level. That's because the target is a WindowGlobalTargetActor
|
|
// which is retrieved via Node.getEmbedderElement and doesn't return the LocalTabTargetActor.
|
|
// We should probably work on TabDescriptor so that the LocalTabTargetActor has a descriptor,
|
|
// and see if we can possibly move the local tab specific out of the TargetActor and have
|
|
// the TabDescriptor expose a pure WindowGlobalTargetActor?? (See bug 1579042)
|
|
if (Cu.getObjectPrincipal(global) == Cu.getObjectPrincipal(dbg)) {
|
|
return undefined;
|
|
}
|
|
|
|
const globalDO = dbg.addDebuggee(global);
|
|
const customElementDO = globalDO.makeDebuggeeValue(customElement);
|
|
|
|
// Return undefined if we can't find a script for the custom element definition.
|
|
if (!customElementDO.script) {
|
|
return undefined;
|
|
}
|
|
|
|
// NOTE: Debugger.Script.prototype.startColumn is 1-based.
|
|
// Convert to 0-based, while keeping the wasm's column (1) as is.
|
|
// (bug 1863878)
|
|
const columnBase = customElementDO.script.format === "wasm" ? 0 : 1;
|
|
|
|
return {
|
|
url: customElementDO.script.url,
|
|
line: customElementDO.script.startLine,
|
|
column: customElementDO.script.startColumn - columnBase,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a LongStringActor with the node's value.
|
|
*/
|
|
getNodeValue() {
|
|
return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
|
|
}
|
|
|
|
/**
|
|
* Set the node's value to a given string.
|
|
*/
|
|
setNodeValue(value) {
|
|
this.rawNode.nodeValue = value;
|
|
}
|
|
|
|
/**
|
|
* Get a unique selector string for this node.
|
|
*/
|
|
getUniqueSelector() {
|
|
if (Cu.isDeadWrapper(this.rawNode)) {
|
|
return "";
|
|
}
|
|
return findCssSelector(this.rawNode);
|
|
}
|
|
|
|
/**
|
|
* Get the full CSS path for this node.
|
|
*
|
|
* @return {String} A CSS selector with a part for the node and each of its ancestors.
|
|
*/
|
|
getCssPath() {
|
|
if (Cu.isDeadWrapper(this.rawNode)) {
|
|
return "";
|
|
}
|
|
return getCssPath(this.rawNode);
|
|
}
|
|
|
|
/**
|
|
* Get the XPath for this node.
|
|
*
|
|
* @return {String} The XPath for finding this node on the page.
|
|
*/
|
|
getXPath() {
|
|
if (Cu.isDeadWrapper(this.rawNode)) {
|
|
return "";
|
|
}
|
|
return getXPath(this.rawNode);
|
|
}
|
|
|
|
/**
|
|
* Scroll the selected node into view.
|
|
*/
|
|
scrollIntoView() {
|
|
// this.rawNode can be an element without `scrollIntoView` (e.g. a `Text` or a `Comment`)
|
|
// In such case, bail out.
|
|
if (typeof this.rawNode.scrollIntoView !== "function") {
|
|
return;
|
|
}
|
|
this.rawNode.scrollIntoView(true);
|
|
}
|
|
|
|
/**
|
|
* Get the node's image data if any (for canvas and img nodes).
|
|
* Returns an imageData object with the actual data being a LongStringActor
|
|
* and a size json object.
|
|
* The image data is transmitted as a base64 encoded png data-uri.
|
|
* The method rejects if the node isn't an image or if the image is missing
|
|
*
|
|
* Accepts a maxDim request parameter to resize images that are larger. This
|
|
* is important as the resizing occurs server-side so that image-data being
|
|
* transfered in the longstring back to the client will be that much smaller
|
|
*/
|
|
getImageData(maxDim) {
|
|
return imageToImageData(this.rawNode, maxDim).then(imageData => {
|
|
return {
|
|
data: new LongStringActor(this.conn, imageData.data),
|
|
size: imageData.size,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all event listeners that are listening on this node.
|
|
*/
|
|
getEventListenerInfo() {
|
|
this._nsIEventListenersInfo.clear();
|
|
|
|
const eventListenersData = this._eventCollector.getEventListeners(
|
|
this.rawNode
|
|
);
|
|
let counter = 0;
|
|
for (const eventListenerData of eventListenersData) {
|
|
if (eventListenerData.nsIEventListenerInfo) {
|
|
const id = `event-listener-info-${++counter}`;
|
|
this._nsIEventListenersInfo.set(
|
|
id,
|
|
eventListenerData.nsIEventListenerInfo
|
|
);
|
|
|
|
eventListenerData.eventListenerInfoId = id;
|
|
// remove the nsIEventListenerInfo since we don't want to send it to the client.
|
|
delete eventListenerData.nsIEventListenerInfo;
|
|
}
|
|
}
|
|
return eventListenersData;
|
|
}
|
|
|
|
/**
|
|
* Disable a specific event listener given its associated id
|
|
*
|
|
* @param {String} eventListenerInfoId
|
|
*/
|
|
disableEventListener(eventListenerInfoId) {
|
|
const nsEventListenerInfo =
|
|
this._nsIEventListenersInfo.get(eventListenerInfoId);
|
|
if (!nsEventListenerInfo) {
|
|
throw new Error("Unkown nsEventListenerInfo");
|
|
}
|
|
nsEventListenerInfo.enabled = false;
|
|
}
|
|
|
|
/**
|
|
* (Re-)enable a specific event listener given its associated id
|
|
*
|
|
* @param {String} eventListenerInfoId
|
|
*/
|
|
enableEventListener(eventListenerInfoId) {
|
|
const nsEventListenerInfo =
|
|
this._nsIEventListenersInfo.get(eventListenerInfoId);
|
|
if (!nsEventListenerInfo) {
|
|
throw new Error("Unkown nsEventListenerInfo");
|
|
}
|
|
nsEventListenerInfo.enabled = true;
|
|
}
|
|
|
|
/**
|
|
* Modify a node's attributes. Passed an array of modifications
|
|
* similar in format to "attributes" mutations.
|
|
* {
|
|
* attributeName: <string>
|
|
* attributeNamespace: <optional string>
|
|
* newValue: <optional string> - If null or undefined, the attribute
|
|
* will be removed.
|
|
* }
|
|
*
|
|
* Returns when the modifications have been made. Mutations will
|
|
* be queued for any changes made.
|
|
*/
|
|
modifyAttributes(modifications) {
|
|
const rawNode = this.rawNode;
|
|
for (const change of modifications) {
|
|
if (change.newValue == null) {
|
|
if (change.attributeNamespace) {
|
|
rawNode.removeAttributeNS(
|
|
change.attributeNamespace,
|
|
change.attributeName
|
|
);
|
|
} else {
|
|
rawNode.removeAttribute(change.attributeName);
|
|
}
|
|
} else if (change.attributeNamespace) {
|
|
rawNode.setAttributeDevtoolsNS(
|
|
change.attributeNamespace,
|
|
change.attributeName,
|
|
change.newValue
|
|
);
|
|
} else {
|
|
rawNode.setAttributeDevtools(change.attributeName, change.newValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given the font and fill style, get the image data of a canvas with the
|
|
* preview text and font.
|
|
* Returns an imageData object with the actual data being a LongStringActor
|
|
* and the width of the text as a string.
|
|
* The image data is transmitted as a base64 encoded png data-uri.
|
|
*/
|
|
getFontFamilyDataURL(font, fillStyle = "black") {
|
|
const doc = this.rawNode.ownerDocument;
|
|
const options = {
|
|
previewText: FONT_FAMILY_PREVIEW_TEXT,
|
|
previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
|
|
fillStyle,
|
|
};
|
|
const { dataURL, size } = getFontPreviewData(font, doc, options);
|
|
|
|
return { data: new LongStringActor(this.conn, dataURL), size };
|
|
}
|
|
|
|
/**
|
|
* Finds the computed background color of the closest parent with a set background
|
|
* color.
|
|
*
|
|
* @return {String}
|
|
* String with the background color of the form rgba(r, g, b, a). Defaults to
|
|
* rgba(255, 255, 255, 1) if no background color is found.
|
|
*/
|
|
getClosestBackgroundColor() {
|
|
return getClosestBackgroundColor(this.rawNode);
|
|
}
|
|
|
|
/**
|
|
* Finds the background color range for the parent of a single text node
|
|
* (i.e. for multi-colored backgrounds with gradients, images) or a single
|
|
* background color for single-colored backgrounds. Defaults to the closest
|
|
* background color if an error is encountered.
|
|
*
|
|
* @return {Object}
|
|
* Object with one or more of the following properties: value, min, max
|
|
*/
|
|
getBackgroundColor() {
|
|
return getBackgroundColor(this);
|
|
}
|
|
|
|
/**
|
|
* Returns an object with the width and height of the node's owner window.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
getOwnerGlobalDimensions() {
|
|
const win = this.rawNode.ownerGlobal;
|
|
return {
|
|
innerWidth: win.innerWidth,
|
|
innerHeight: win.innerHeight,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* If the current node is an iframe, wait for the content window to be loaded.
|
|
*/
|
|
async waitForFrameLoad() {
|
|
if (this.useChildTargetToFetchChildren) {
|
|
// If the document is handled by a dedicated target, we'll wait for a DOCUMENT_EVENT
|
|
// on the created target.
|
|
throw new Error(
|
|
"iframe content document has its own target, use that one instead"
|
|
);
|
|
}
|
|
|
|
if (Cu.isDeadWrapper(this.rawNode)) {
|
|
throw new Error("Node is dead");
|
|
}
|
|
|
|
const { contentDocument } = this.rawNode;
|
|
if (!contentDocument) {
|
|
throw new Error("Can't access contentDocument");
|
|
}
|
|
|
|
if (contentDocument.readyState === "uninitialized") {
|
|
// If the readyState is "uninitialized", the document is probably an about:blank
|
|
// transient document. In such case, we want to wait until the "final" document
|
|
// is inserted.
|
|
|
|
const { chromeEventHandler } = this.rawNode.ownerGlobal.docShell;
|
|
const browsingContextID = this.rawNode.browsingContext.id;
|
|
await new Promise((resolve, reject) => {
|
|
this._waitForFrameLoadAbortController = new AbortController();
|
|
|
|
chromeEventHandler.addEventListener(
|
|
"DOMDocElementInserted",
|
|
e => {
|
|
const { browsingContext } = e.target.defaultView;
|
|
// Check that the document we're notified about is the iframe one.
|
|
if (browsingContext.id == browsingContextID) {
|
|
resolve();
|
|
this._waitForFrameLoadAbortController.abort();
|
|
}
|
|
},
|
|
{ signal: this._waitForFrameLoadAbortController.signal }
|
|
);
|
|
|
|
// It might happen that the "final" document will be a remote one, living in a
|
|
// different process, which means we won't get the DOMDocElementInserted event
|
|
// here, and will wait forever. To prevent this Promise to hang forever, we use
|
|
// a setInterval to check if the final document can be reached, so we can reject
|
|
// if it's not.
|
|
// This is definitely not a perfect solution, but I wasn't able to find something
|
|
// better for this feature. I think it's _fine_ as this method will be removed
|
|
// when EFT is enabled everywhere in release.
|
|
this._waitForFrameLoadIntervalId = setInterval(() => {
|
|
if (Cu.isDeadWrapper(this.rawNode) || !this.rawNode.contentDocument) {
|
|
reject("Can't access the iframe content document");
|
|
clearInterval(this._waitForFrameLoadIntervalId);
|
|
this._waitForFrameLoadIntervalId = null;
|
|
this._waitForFrameLoadAbortController.abort();
|
|
}
|
|
}, 50);
|
|
});
|
|
}
|
|
|
|
if (this.rawNode.contentDocument.readyState === "loading") {
|
|
await new Promise(resolve => {
|
|
DOMHelpers.onceDOMReady(this.rawNode.contentWindow, resolve);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Server side of a node list as returned by querySelectorAll()
|
|
*/
|
|
class NodeListActor extends Actor {
|
|
constructor(walker, nodeList) {
|
|
super(walker.conn, nodeListSpec);
|
|
this.walker = walker;
|
|
this.nodeList = nodeList || [];
|
|
}
|
|
|
|
/**
|
|
* Items returned by this actor should belong to the parent walker.
|
|
*/
|
|
marshallPool() {
|
|
return this.walker;
|
|
}
|
|
|
|
// Returns the JSON representation of this object over the wire.
|
|
form() {
|
|
return {
|
|
actor: this.actorID,
|
|
length: this.nodeList ? this.nodeList.length : 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a single node from the node list.
|
|
*/
|
|
item(index) {
|
|
return this.walker.attachElement(this.nodeList[index]);
|
|
}
|
|
|
|
/**
|
|
* Get a range of the items from the node list.
|
|
*/
|
|
items(start = 0, end = this.nodeList.length) {
|
|
const items = Array.prototype.slice
|
|
.call(this.nodeList, start, end)
|
|
.map(item => this.walker._getOrCreateNodeActor(item));
|
|
return this.walker.attachElements(items);
|
|
}
|
|
|
|
release() {}
|
|
}
|
|
|
|
exports.NodeActor = NodeActor;
|
|
exports.NodeListActor = NodeListActor;
|