/* 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";
loader.lazyRequireGetter(
this,
"colorUtils",
"resource://devtools/shared/css/color.js",
true
);
loader.lazyRequireGetter(
this,
"AsyncUtils",
"resource://devtools/shared/async-utils.js"
);
loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
loader.lazyRequireGetter(
this,
"DevToolsUtils",
"resource://devtools/shared/DevToolsUtils.js"
);
loader.lazyRequireGetter(
this,
"nodeFilterConstants",
"resource://devtools/shared/dom-node-filter-constants.js"
);
loader.lazyRequireGetter(
this,
["isNativeAnonymous", "getAdjustedQuads"],
"resource://devtools/shared/layout/utils.js",
true
);
loader.lazyRequireGetter(
this,
"CssLogic",
"resource://devtools/server/actors/inspector/css-logic.js",
true
);
loader.lazyRequireGetter(
this,
"getBackgroundFor",
"resource://devtools/server/actors/accessibility/audit/contrast.js",
true
);
loader.lazyRequireGetter(
this,
["loadSheetForBackgroundCalculation", "removeSheetForBackgroundCalculation"],
"resource://devtools/server/actors/utils/accessibility.js",
true
);
loader.lazyRequireGetter(
this,
"getTextProperties",
"resource://devtools/shared/accessibility.js",
true
);
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const IMAGE_FETCHING_TIMEOUT = 500;
/**
* Returns the properly cased version of the node's tag name, which can be
* used when displaying said name in the UI.
*
* @param {Node} rawNode
* Node for which we want the display name
* @return {String}
* Properly cased version of the node tag name
*/
const getNodeDisplayName = function (rawNode) {
if (rawNode.nodeName && !rawNode.localName) {
// The localName & prefix APIs have been moved from the Node interface to the Element
// interface. Use Node.nodeName as a fallback.
return rawNode.nodeName;
}
return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
};
/**
* Returns flex and grid information about a DOM node.
* In particular is it a grid flex/container and/or item?
*
* @param {DOMNode} node
* The node for which then information is required
* @return {Object}
* An object like { grid: { isContainer, isItem }, flex: { isContainer, isItem } }
*/
function getNodeGridFlexType(node) {
return {
grid: getNodeGridType(node),
flex: getNodeFlexType(node),
};
}
function getNodeFlexType(node) {
return {
isContainer: node.getAsFlexContainer && !!node.getAsFlexContainer(),
isItem: !!node.parentFlexElement,
};
}
function getNodeGridType(node) {
return {
isContainer: node.hasGridFragments && node.hasGridFragments(),
isItem: !!findGridParentContainerForNode(node),
};
}
function nodeDocument(node) {
if (Cu.isDeadWrapper(node)) {
return null;
}
return (
node.ownerDocument || (node.nodeType == Node.DOCUMENT_NODE ? node : null)
);
}
function isNodeDead(node) {
return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
}
function isInXULDocument(el) {
const doc = nodeDocument(el);
return doc?.documentElement && doc.documentElement.namespaceURI === XUL_NS;
}
/**
* This DeepTreeWalker filter skips whitespace text nodes and anonymous
* content with the exception of ::marker, ::before, and ::after, plus anonymous
* content in XUL document (needed to show all elements in the browser toolbox).
*/
function standardTreeWalkerFilter(node) {
// ::marker, ::before, and ::after are native anonymous content, but we always
// want to show them
if (
node.nodeName === "_moz_generated_content_marker" ||
node.nodeName === "_moz_generated_content_before" ||
node.nodeName === "_moz_generated_content_after"
) {
return nodeFilterConstants.FILTER_ACCEPT;
}
// Ignore empty whitespace text nodes that do not impact the layout.
if (isWhitespaceTextNode(node)) {
return nodeHasSize(node)
? nodeFilterConstants.FILTER_ACCEPT
: nodeFilterConstants.FILTER_SKIP;
}
// Ignore all native anonymous roots inside a non-XUL document.
// We need to do this to skip things like form controls, scrollbars,
// video controls, etc (see bug 1187482).
if (isNativeAnonymous(node) && !isInXULDocument(node)) {
return nodeFilterConstants.FILTER_SKIP;
}
return nodeFilterConstants.FILTER_ACCEPT;
}
/**
* This DeepTreeWalker filter ignores anonymous content.
*/
function noAnonymousContentTreeWalkerFilter(node) {
// Ignore all native anonymous content inside a non-XUL document.
// We need to do this to skip things like form controls, scrollbars,
// video controls, etc (see bug 1187482).
if (!isInXULDocument(node) && isNativeAnonymous(node)) {
return nodeFilterConstants.FILTER_SKIP;
}
return nodeFilterConstants.FILTER_ACCEPT;
}
/**
* This DeepTreeWalker filter is like standardTreeWalkerFilter except that
* it also includes all anonymous content (like internal form controls).
*/
function allAnonymousContentTreeWalkerFilter(node) {
// Ignore empty whitespace text nodes that do not impact the layout.
if (isWhitespaceTextNode(node)) {
return nodeHasSize(node)
? nodeFilterConstants.FILTER_ACCEPT
: nodeFilterConstants.FILTER_SKIP;
}
return nodeFilterConstants.FILTER_ACCEPT;
}
/**
* Is the given node a text node composed of whitespace only?
* @param {DOMNode} node
* @return {Boolean}
*/
function isWhitespaceTextNode(node) {
return node.nodeType == Node.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
}
/**
* Does the given node have non-0 width and height?
* @param {DOMNode} node
* @return {Boolean}
*/
function nodeHasSize(node) {
if (!node.getBoxQuads) {
return false;
}
const quads = node.getBoxQuads({
createFramesForSuppressedWhitespace: false,
});
return quads.some(quad => {
const bounds = quad.getBounds();
return bounds.width && bounds.height;
});
}
/**
* Returns a promise that is settled once the given HTMLImageElement has
* finished loading.
*
* @param {HTMLImageElement} image - The image element.
* @param {Number} timeout - Maximum amount of time the image is allowed to load
* before the waiting is aborted. Ignored if flags.testing is set.
*
* @return {Promise} that is fulfilled once the image has loaded. If the image
* fails to load or the load takes too long, the promise is rejected.
*/
function ensureImageLoaded(image, timeout) {
const { HTMLImageElement } = image.ownerGlobal;
if (!(image instanceof HTMLImageElement)) {
return Promise.reject("image must be an HTMLImageELement");
}
if (image.complete) {
// The image has already finished loading.
return Promise.resolve();
}
// This image is still loading.
const onLoad = AsyncUtils.listenOnce(image, "load");
// Reject if loading fails.
const onError = AsyncUtils.listenOnce(image, "error").then(() => {
return Promise.reject("Image '" + image.src + "' failed to load.");
});
// Don't timeout when testing. This is never settled.
let onAbort = new Promise(() => {});
if (!flags.testing) {
// Tests are not running. Reject the promise after given timeout.
onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
return Promise.reject("Image '" + image.src + "' took too long to load.");
});
}
// See which happens first.
return Promise.race([onLoad, onError, onAbort]);
}
/**
* Given an or