The getter is potentially slow (it was picked up by DAMP), and in this case, it can be replaced with the `rawNode` arg. Differential Revision: https://phabricator.services.mozilla.com/D311470
1743 lines
56 KiB
JavaScript
1743 lines
56 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 {
|
|
pageStyleSpec,
|
|
} = require("resource://devtools/shared/specs/page-style.js");
|
|
|
|
const {
|
|
LongStringActor,
|
|
} = require("resource://devtools/server/actors/string.js");
|
|
|
|
const {
|
|
style: { ELEMENT_STYLE },
|
|
} = require("resource://devtools/shared/constants.js");
|
|
|
|
const {
|
|
toFixed,
|
|
} = require("resource://devtools/shared/inspector/font-utils.js");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"StyleRuleActor",
|
|
"resource://devtools/server/actors/style-rule.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"getFontPreviewData",
|
|
"resource://devtools/server/actors/stylesheets/style-utils.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"CssLogic",
|
|
"resource://devtools/server/actors/inspector/css-logic.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"SharedCssLogic",
|
|
"resource://devtools/shared/inspector/css-logic.js"
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"getDefinedGeometryProperties",
|
|
"resource://devtools/server/actors/highlighters/geometry-editor.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"UPDATE_GENERAL",
|
|
"resource://devtools/server/actors/stylesheets/stylesheets-manager.js",
|
|
true
|
|
);
|
|
|
|
loader.lazyGetter(this, "PSEUDO_ELEMENTS", () => {
|
|
return InspectorUtils.getCSSPseudoElementNames();
|
|
});
|
|
loader.lazyGetter(this, "FONT_VARIATIONS_ENABLED", () => {
|
|
return Services.prefs.getBoolPref("layout.css.font-variations.enabled");
|
|
});
|
|
|
|
const NORMAL_FONT_WEIGHT = 400;
|
|
const BOLD_FONT_WEIGHT = 700;
|
|
|
|
/**
|
|
* The PageStyle actor lets the client look at the styles on a page, as
|
|
* they are applied to a given node.
|
|
*/
|
|
class PageStyleActor extends Actor {
|
|
/**
|
|
* Create a PageStyleActor.
|
|
*
|
|
* @param inspector
|
|
* The InspectorActor that owns this PageStyleActor.
|
|
*
|
|
* @class
|
|
*/
|
|
constructor(inspector) {
|
|
super(inspector.conn, pageStyleSpec);
|
|
this.inspector = inspector;
|
|
if (!this.inspector.walker) {
|
|
throw Error(
|
|
"The inspector's WalkerActor must be created before " +
|
|
"creating a PageStyleActor."
|
|
);
|
|
}
|
|
this.walker = inspector.walker;
|
|
this.cssLogic = new CssLogic();
|
|
|
|
// Stores the association of DOM objects -> actors
|
|
this.refMap = new Map();
|
|
|
|
// Latest node queried for its applied styles.
|
|
this.selectedElement = null;
|
|
|
|
// Maps root node (document|ShadowRoot) to stylesheets, which are used to add new rules.
|
|
this.styleSheetsByRootNode = new WeakMap();
|
|
|
|
this.onFrameUnload = this.onFrameUnload.bind(this);
|
|
|
|
this.inspector.targetActor.on("will-navigate", this.onFrameUnload);
|
|
|
|
this.styleSheetsManager =
|
|
this.inspector.targetActor.getStyleSheetsManager();
|
|
|
|
this.styleSheetsManager.on("stylesheet-updated", this.#onStylesheetUpdated);
|
|
}
|
|
|
|
#observedRules = new Set();
|
|
|
|
destroy() {
|
|
if (!this.walker) {
|
|
return;
|
|
}
|
|
super.destroy();
|
|
this.inspector.targetActor.off("will-navigate", this.onFrameUnload);
|
|
this.inspector = null;
|
|
this.walker = null;
|
|
this.refMap = null;
|
|
this.selectedElement = null;
|
|
this.cssLogic = null;
|
|
this.styleSheetsByRootNode = null;
|
|
|
|
this.#observedRules = null;
|
|
}
|
|
|
|
get ownerWindow() {
|
|
return this.inspector.targetActor.window;
|
|
}
|
|
|
|
form() {
|
|
// We need to use CSS from the inspected window in order to use CSS.supports() and
|
|
// detect the right platform features from there.
|
|
const CSS = this.inspector.targetActor.window.CSS;
|
|
|
|
return {
|
|
actor: this.actorID,
|
|
traits: {
|
|
// Whether the page supports values of font-stretch from CSS Fonts Level 4.
|
|
fontStretchLevel4: CSS.supports("font-stretch: 100%"),
|
|
// Whether the page supports values of font-style from CSS Fonts Level 4.
|
|
fontStyleLevel4: CSS.supports("font-style: oblique 20deg"),
|
|
// Whether getAllUsedFontFaces/getUsedFontFaces accepts the includeVariations
|
|
// argument.
|
|
fontVariations: FONT_VARIATIONS_ENABLED,
|
|
// Whether the page supports values of font-weight from CSS Fonts Level 4.
|
|
// font-weight at CSS Fonts Level 4 accepts values in increments of 1 rather
|
|
// than 100. However, CSS.supports() returns false positives, so we guard with the
|
|
// expected support of font-stretch at CSS Fonts Level 4.
|
|
fontWeightLevel4:
|
|
CSS.supports("font-weight: 1") && CSS.supports("font-stretch: 100%"),
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Called when a style sheet is updated.
|
|
*/
|
|
#styleApplied = kind => {
|
|
// No matter what kind of update is done, we need to invalidate
|
|
// the keyframe cache.
|
|
this.cssLogic.reset();
|
|
if (kind === UPDATE_GENERAL) {
|
|
this.emit("stylesheet-updated");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Return or create a StyleRuleActor for the given item.
|
|
*
|
|
* @param {CSSStyleRule|Element} item
|
|
* @param {string} pseudoElement An optional pseudo-element type in cases when the CSS
|
|
* rule applies to a pseudo-element.
|
|
* @param {boolean} userAdded: Optional boolean to distinguish rules added by the user.
|
|
* @return {StyleRuleActor} The newly created, or cached, StyleRuleActor for this item.
|
|
*/
|
|
styleRef(item, pseudoElement, userAdded = false) {
|
|
if (this.refMap.has(item)) {
|
|
const styleRuleActor = this.refMap.get(item);
|
|
if (pseudoElement) {
|
|
styleRuleActor.addPseudo(pseudoElement);
|
|
}
|
|
return styleRuleActor;
|
|
}
|
|
const actor = new StyleRuleActor({
|
|
pageStyle: this,
|
|
item,
|
|
userAdded,
|
|
pseudoElement,
|
|
});
|
|
this.manage(actor);
|
|
this.refMap.set(item, actor);
|
|
|
|
return actor;
|
|
}
|
|
|
|
/**
|
|
* Update the association between a StyleRuleActor and its
|
|
* corresponding item. This is used when a StyleRuleActor updates
|
|
* as style sheet and starts using a new rule.
|
|
*
|
|
* @param oldItem The old association; either a CSSStyleRule or a
|
|
* DOM element.
|
|
* @param item Either a CSSStyleRule or a DOM element.
|
|
* @param actor a StyleRuleActor
|
|
*/
|
|
updateStyleRef(oldItem, item, actor) {
|
|
this.refMap.delete(oldItem);
|
|
this.refMap.set(item, actor);
|
|
}
|
|
|
|
/**
|
|
* Get the StyleRuleActor matching the given rule id or null if no match is found.
|
|
*
|
|
* @param {string} ruleId
|
|
* Actor ID of the StyleRuleActor
|
|
* @return {StyleRuleActor|null}
|
|
*/
|
|
getRule(ruleId) {
|
|
let match = null;
|
|
|
|
for (const actor of this.refMap.values()) {
|
|
if (actor.actorID === ruleId) {
|
|
match = actor;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return match;
|
|
}
|
|
|
|
/**
|
|
* Get the computed style for a node.
|
|
*
|
|
* @param {NodeActor} node
|
|
* @param {object} options
|
|
* @param {string} options.filter: A string filter that affects the "matched" handling.
|
|
* @param {Array<string>} options.filterProperties: An array of properties names that
|
|
* you would like returned.
|
|
* @param {boolean} options.markMatched: true if you want the 'matched' property to be
|
|
* added when a computed property has been modified by a style included by `filter`.
|
|
* @param {boolean} options.onlyMatched: true if unmatched properties shouldn't be included.
|
|
* @param {boolean} options.clearCache: true if the cssLogic cache should be cleared.
|
|
*
|
|
* @returns a JSON blob with the following form:
|
|
* {
|
|
* "property-name": {
|
|
* value: "property-value",
|
|
* priority: "!important" <optional>
|
|
* matched: <true if there are matched selectors for this value>
|
|
* },
|
|
* ...
|
|
* }
|
|
*/
|
|
getComputed(node, options) {
|
|
const ret = Object.create(null);
|
|
|
|
if (options.clearCache) {
|
|
this.cssLogic.reset();
|
|
}
|
|
const filterProperties = Array.isArray(options.filterProperties)
|
|
? options.filterProperties
|
|
: null;
|
|
this.cssLogic.sourceFilter = options.filter || SharedCssLogic.FILTER.UA;
|
|
this.cssLogic.highlight(node.rawNode);
|
|
const computed = this.cssLogic.computedStyle || [];
|
|
const targetDocument = this.inspector.targetActor.window.document;
|
|
|
|
for (const name of computed) {
|
|
if (filterProperties && !filterProperties.includes(name)) {
|
|
continue;
|
|
}
|
|
ret[name] = {
|
|
value: computed.getPropertyValue(name),
|
|
priority: computed.getPropertyPriority(name) || undefined,
|
|
};
|
|
|
|
if (name.startsWith("--")) {
|
|
const registeredProperty = InspectorUtils.getCSSRegisteredProperty(
|
|
targetDocument,
|
|
name
|
|
);
|
|
if (registeredProperty) {
|
|
ret[name].registeredPropertyInitialValue =
|
|
registeredProperty.initialValue;
|
|
if (
|
|
!InspectorUtils.valueMatchesSyntax(
|
|
targetDocument,
|
|
ret[name].value,
|
|
registeredProperty.syntax
|
|
)
|
|
) {
|
|
ret[name].invalidAtComputedValueTime = true;
|
|
ret[name].registeredPropertySyntax = registeredProperty.syntax;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.markMatched || options.onlyMatched) {
|
|
const matched = this.cssLogic.hasMatchedSelectors(Object.keys(ret));
|
|
for (const key in ret) {
|
|
if (matched.has(key)) {
|
|
ret[key].matched = options.markMatched ? true : undefined;
|
|
} else if (options.onlyMatched) {
|
|
delete ret[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Get all the fonts from a page.
|
|
*
|
|
* @param object options
|
|
* `includePreviews`: Whether to also return image previews of the fonts.
|
|
* `previewText`: The text to display in the previews.
|
|
* `previewFontSize`: The font size of the text in the previews.
|
|
*
|
|
* @returns object
|
|
* object with 'fontFaces', a list of fonts that apply to this node.
|
|
*/
|
|
getAllUsedFontFaces(options) {
|
|
const windows = this.inspector.targetActor.windows;
|
|
let fontsList = [];
|
|
for (const win of windows) {
|
|
// Fall back to the documentElement for XUL documents.
|
|
const node = win.document.body
|
|
? win.document.body
|
|
: win.document.documentElement;
|
|
fontsList = [...fontsList, ...this.getUsedFontFaces(node, options)];
|
|
}
|
|
|
|
return fontsList;
|
|
}
|
|
|
|
/**
|
|
* Get the font faces used in an element.
|
|
*
|
|
* @param NodeActor node / actual DOM node
|
|
* The node to get fonts from.
|
|
* @param object options
|
|
* `includePreviews`: Whether to also return image previews of the fonts.
|
|
* `previewText`: The text to display in the previews.
|
|
* `previewFontSize`: The font size of the text in the previews.
|
|
*
|
|
* @returns object
|
|
* object with 'fontFaces', a list of fonts that apply to this node.
|
|
*/
|
|
getUsedFontFaces(node, options) {
|
|
// node.rawNode is defined for NodeActor objects
|
|
const actualNode = node.rawNode || node;
|
|
const contentDocument = actualNode.ownerDocument;
|
|
// We don't get fonts for a node, but for a range
|
|
const rng = contentDocument.createRange();
|
|
const isPseudoElement = Boolean(
|
|
CssLogic.getBindingElementAndPseudo(actualNode).pseudo
|
|
);
|
|
if (isPseudoElement) {
|
|
rng.selectNodeContents(actualNode);
|
|
} else {
|
|
rng.selectNode(actualNode);
|
|
}
|
|
const fonts = InspectorUtils.getUsedFontFaces(rng);
|
|
const fontsArray = [];
|
|
|
|
for (let i = 0; i < fonts.length; i++) {
|
|
const font = fonts[i];
|
|
const fontFace = {
|
|
name: font.name,
|
|
CSSFamilyName: font.CSSFamilyName,
|
|
CSSGeneric: font.CSSGeneric || null,
|
|
srcIndex: font.srcIndex,
|
|
URI: font.URI,
|
|
format: font.format,
|
|
localName: font.localName,
|
|
metadata: font.metadata,
|
|
version: font.getNameString(InspectorFontFace.NAME_ID_VERSION),
|
|
description: font.getNameString(InspectorFontFace.NAME_ID_DESCRIPTION),
|
|
manufacturer: font.getNameString(
|
|
InspectorFontFace.NAME_ID_MANUFACTURER
|
|
),
|
|
vendorUrl: font.getNameString(InspectorFontFace.NAME_ID_VENDOR_URL),
|
|
designer: font.getNameString(InspectorFontFace.NAME_ID_DESIGNER),
|
|
designerUrl: font.getNameString(InspectorFontFace.NAME_ID_DESIGNER_URL),
|
|
license: font.getNameString(InspectorFontFace.NAME_ID_LICENSE),
|
|
licenseUrl: font.getNameString(InspectorFontFace.NAME_ID_LICENSE_URL),
|
|
sampleText: font.getNameString(InspectorFontFace.NAME_ID_SAMPLE_TEXT),
|
|
};
|
|
|
|
// If this font comes from a @font-face rule
|
|
if (font.rule) {
|
|
const styleActor = new StyleRuleActor({
|
|
pageStyle: this,
|
|
item: font.rule,
|
|
});
|
|
this.manage(styleActor);
|
|
fontFace.rule = styleActor;
|
|
fontFace.ruleText = font.rule.cssText;
|
|
}
|
|
|
|
// Get the weight and style of this font for the preview and sort order
|
|
let weight = NORMAL_FONT_WEIGHT,
|
|
style = "";
|
|
if (font.rule) {
|
|
weight =
|
|
font.rule.style.getPropertyValue("font-weight") || NORMAL_FONT_WEIGHT;
|
|
if (weight == "bold") {
|
|
weight = BOLD_FONT_WEIGHT;
|
|
} else if (weight == "normal") {
|
|
weight = NORMAL_FONT_WEIGHT;
|
|
}
|
|
style = font.rule.style.getPropertyValue("font-style") || "";
|
|
}
|
|
fontFace.weight = weight;
|
|
fontFace.style = style;
|
|
|
|
if (options.includePreviews) {
|
|
const opts = {
|
|
previewText: options.previewText,
|
|
previewFontSize: options.previewFontSize,
|
|
fontStyle: style,
|
|
fontWeight: weight,
|
|
fillStyle: options.previewFillStyle,
|
|
};
|
|
const { dataURL, size } = getFontPreviewData(
|
|
font.CSSFamilyName,
|
|
contentDocument,
|
|
opts
|
|
);
|
|
fontFace.preview = {
|
|
data: new LongStringActor(this.conn, dataURL),
|
|
size,
|
|
};
|
|
}
|
|
|
|
if (options.includeVariations && FONT_VARIATIONS_ENABLED) {
|
|
// Round font variation axes values
|
|
fontFace.variationAxes = font.getVariationAxes().map(axis => ({
|
|
...axis,
|
|
minValue: toFixed(axis.minValue, 3),
|
|
maxValue: toFixed(axis.maxValue, 3),
|
|
defaultValue: toFixed(axis.defaultValue, 3),
|
|
}));
|
|
fontFace.variationInstances = font
|
|
.getVariationInstances()
|
|
.map(instance => ({
|
|
...instance,
|
|
values: instance.values.map(variationValue => ({
|
|
...variationValue,
|
|
value: toFixed(variationValue.value, 3),
|
|
})),
|
|
}));
|
|
}
|
|
|
|
fontsArray.push(fontFace);
|
|
}
|
|
|
|
// @font-face fonts at the top, then alphabetically, then by weight
|
|
fontsArray.sort(function (a, b) {
|
|
return a.weight > b.weight ? 1 : -1;
|
|
});
|
|
fontsArray.sort(function (a, b) {
|
|
if (a.CSSFamilyName == b.CSSFamilyName) {
|
|
return 0;
|
|
}
|
|
return a.CSSFamilyName > b.CSSFamilyName ? 1 : -1;
|
|
});
|
|
fontsArray.sort(function (a, b) {
|
|
if ((a.rule && b.rule) || (!a.rule && !b.rule)) {
|
|
return 0;
|
|
}
|
|
return !a.rule && b.rule ? 1 : -1;
|
|
});
|
|
|
|
return fontsArray;
|
|
}
|
|
|
|
/**
|
|
* Get a list of selectors that match a given property for a node.
|
|
*
|
|
* @param NodeActor node
|
|
* @param string property
|
|
* @param object options
|
|
* `filter`: A string filter that affects the "matched" handling.
|
|
* 'user': Include properties from user style sheets.
|
|
* 'ua': Include properties from user and user-agent sheets.
|
|
* Default value is 'ua'
|
|
*
|
|
* @returns a JSON object with the following form:
|
|
* {
|
|
* // An ordered list of rules that apply
|
|
* matched: [{
|
|
* rule: <rule actorid>,
|
|
* sourceText: <string>, // The source of the selector, relative
|
|
* // to the node in question.
|
|
* selector: <string>, // the selector ID that matched
|
|
* value: <string>, // the value of the property
|
|
* status: <int>,
|
|
* // The status of the match - high numbers are better placed
|
|
* // to provide styling information:
|
|
* // 3: Best match, was used.
|
|
* // 2: Matched, but was overridden.
|
|
* // 1: Rule from a parent matched.
|
|
* // 0: Unmatched (never returned in this API)
|
|
* }, ...],
|
|
*
|
|
* // The full form of any domrule referenced.
|
|
* rules: [ <domrule>, ... ], // The full form of any domrule referenced
|
|
*
|
|
* // The full form of any sheets referenced.
|
|
* sheets: [ <domsheet>, ... ]
|
|
* }
|
|
*/
|
|
getMatchedSelectors(node, property, options) {
|
|
this.cssLogic.sourceFilter = options.filter || SharedCssLogic.FILTER.UA;
|
|
this.cssLogic.highlight(node.rawNode);
|
|
|
|
const rules = new Set();
|
|
const matched = [];
|
|
|
|
const targetDocument = this.inspector.targetActor.window.document;
|
|
let registeredProperty;
|
|
if (property.startsWith("--")) {
|
|
registeredProperty = InspectorUtils.getCSSRegisteredProperty(
|
|
targetDocument,
|
|
property
|
|
);
|
|
}
|
|
|
|
const propInfo = this.cssLogic.getPropertyInfo(property);
|
|
for (const selectorInfo of propInfo.matchedSelectors) {
|
|
const cssRule = selectorInfo.selector.cssRule;
|
|
const domRule = cssRule.sourceElement || cssRule.domRule;
|
|
|
|
const rule = this.styleRef(domRule);
|
|
rules.add(rule);
|
|
|
|
const match = {
|
|
rule,
|
|
sourceText: this.getSelectorSource(selectorInfo, node.rawNode),
|
|
selector: selectorInfo.selector.text,
|
|
name: selectorInfo.property,
|
|
value: selectorInfo.value,
|
|
status: selectorInfo.status,
|
|
};
|
|
if (
|
|
registeredProperty &&
|
|
!InspectorUtils.valueMatchesSyntax(
|
|
targetDocument,
|
|
match.value,
|
|
registeredProperty.syntax
|
|
)
|
|
) {
|
|
match.invalidAtComputedValueTime = true;
|
|
match.registeredPropertySyntax = registeredProperty.syntax;
|
|
}
|
|
matched.push(match);
|
|
}
|
|
|
|
return {
|
|
matched,
|
|
rules: [...rules],
|
|
};
|
|
}
|
|
|
|
// Get a selector source for a CssSelectorInfo relative to a given
|
|
// node.
|
|
getSelectorSource(selectorInfo, relativeTo) {
|
|
let result = selectorInfo.selector.text;
|
|
const ruleDeclarationOrigin =
|
|
selectorInfo.selector.cssRule.domRule.declarationOrigin;
|
|
if (
|
|
ruleDeclarationOrigin === "style-attribute" ||
|
|
ruleDeclarationOrigin === "pres-hints"
|
|
) {
|
|
const source = selectorInfo.sourceElement;
|
|
if (source === relativeTo) {
|
|
result = "element";
|
|
} else {
|
|
result = CssLogic.getShortName(source);
|
|
}
|
|
|
|
if (ruleDeclarationOrigin === "pres-hints") {
|
|
result += " attributes style";
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @typedef {"user" | "ua" } GetAppliedFilterOption
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} GetAppliedOptions
|
|
*
|
|
* @property {GetAppliedFilterOption} filter - A string filter that affects the "matched" handling.
|
|
* Possible values are:
|
|
* - 'user': Include properties from user style sheets.
|
|
* - 'ua': Include properties from user and user-agent sheets.
|
|
* Default value is 'ua'
|
|
* @property {boolean} inherited - Include styles inherited from parent nodes.
|
|
* @property {boolean} matchedSelectors - Include an array of specific selectors that
|
|
* caused this rule to match its node.
|
|
* @property {boolean} skipPseudo - Exclude styles applied to pseudo elements of the
|
|
* provided node.
|
|
*/
|
|
|
|
/**
|
|
* Get the set of styles that apply to a given node.
|
|
*
|
|
* @param {NodeActor} node
|
|
* @param {GetAppliedOptions} options
|
|
*/
|
|
async getApplied(node, options) {
|
|
// Clear any previous references to StyleRuleActor instances for CSS rules.
|
|
// Assume the consumer has switched context to a new node and no longer
|
|
// interested in state changes of previous rules.
|
|
this.#observedRules.clear();
|
|
this.selectedElement = node?.rawNode || null;
|
|
|
|
if (!node) {
|
|
return { entries: [] };
|
|
}
|
|
|
|
this.cssLogic.highlight(node.rawNode);
|
|
|
|
const entries = this.getAppliedProps(
|
|
node,
|
|
this.#getAllElementRules(node, {
|
|
skipPseudo: options.skipPseudo,
|
|
filter: options.filter,
|
|
}),
|
|
options
|
|
);
|
|
|
|
const promises = [];
|
|
for (const entry of entries) {
|
|
// Reference to instances of StyleRuleActor for CSS rules matching the node.
|
|
// Assume these are used by a consumer which wants to be notified when their
|
|
// state or declarations change either directly or indirectly.
|
|
this.#observedRules.add(entry.rule);
|
|
// We need to be sure that authoredText has been set before StyleRule#form is called.
|
|
// This has to be treated specially, for now, because we cannot synchronously compute
|
|
// the authored text and |form| can't return a promise.
|
|
// See bug 1205868.
|
|
promises.push(entry.rule.getAuthoredCssText());
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
return { entries };
|
|
}
|
|
|
|
#hasInheritedProps(style) {
|
|
const doc = this.inspector.targetActor.window.document;
|
|
return Array.prototype.some.call(style, prop =>
|
|
InspectorUtils.isInheritedProperty(doc, prop)
|
|
);
|
|
}
|
|
|
|
async isPositionEditable(node) {
|
|
if (!node || node.rawNode.nodeType !== node.rawNode.ELEMENT_NODE) {
|
|
return false;
|
|
}
|
|
|
|
const props = getDefinedGeometryProperties(node.rawNode);
|
|
|
|
// Elements with only `width` and `height` are currently not considered
|
|
// editable.
|
|
return (
|
|
props.has("top") ||
|
|
props.has("right") ||
|
|
props.has("left") ||
|
|
props.has("bottom")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function for getApplied, gets all the rules from a given
|
|
* element. See getApplied for documentation on parameters.
|
|
*
|
|
* @param {NodeActor} node
|
|
* @param {object} options
|
|
* @param {boolean} options.isInherited - Set to true if we want to retrieve inherited rules,
|
|
* i.e. the passed node actor is an ancestor of the node we want to retrieved the
|
|
* applied rules for originally.
|
|
* @param {boolean} options.skipPseudo - Exclude styles applied to pseudo elements of the
|
|
* provided node
|
|
* @param {GetAppliedFilterOption} options.filter - will be passed to #getElementRules
|
|
*
|
|
* @return {Array{AppliedStyle}} The rules for a given element.
|
|
* See #getRuleItem for definition.
|
|
*/
|
|
#getAllElementRules(node, { isInherited, skipPseudo, filter }) {
|
|
const { rawNode } = node;
|
|
const rules = [];
|
|
|
|
// First add rule actors for element style defined via the DOM "style" attribute,
|
|
// and/or via the JS "style" attribute (CSSOM API).
|
|
if (rawNode.style) {
|
|
const isPseudoElement = !!rawNode.implementedPseudoElement;
|
|
// We only show element styles if:
|
|
// - we aren't processing a parent element for inherited rules, and that's not a pseudo element
|
|
// --or--
|
|
// - we are processing parent elements for inherited rules and we have at least one inherited rule
|
|
const showElementStyles = !isInherited && !isPseudoElement;
|
|
const showInheritedStyles =
|
|
isInherited && this.#hasInheritedProps(rawNode.style);
|
|
|
|
if (showElementStyles || showInheritedStyles) {
|
|
const elementStyleActor = this.styleRef(
|
|
rawNode,
|
|
// We never try to fetch element styles for pseudo elements.
|
|
null
|
|
);
|
|
|
|
if (showElementStyles) {
|
|
rules.push(
|
|
this.#getRuleItem(elementStyleActor, rawNode, {
|
|
pseudoElement: null,
|
|
isSystem: false,
|
|
inherited: null,
|
|
})
|
|
);
|
|
} else if (showInheritedStyles) {
|
|
// at this point `isInherited` is true, so we want to put the NodeActor in the
|
|
// `inherited` property so the client can show this information (for example in
|
|
// the "Inherited from X" section in the Rules view).
|
|
rules.push(
|
|
this.#getRuleItem(elementStyleActor, rawNode, {
|
|
pseudoElement: null,
|
|
isSystem: false,
|
|
inherited: node,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add normal rules matching exactly the DOM Element passed in (node).
|
|
//
|
|
// Note that when we are processing the special DOM Elements for pseudo elements
|
|
// like ::before, ::marker, ::details-content,...
|
|
// we pass an empty pseudo string as we want to build the Rule for that DOM Element
|
|
// and not a pseudo element Rule.
|
|
// Pseudo element rule are build just after in this method and are for pseudo elements
|
|
// created as children of the currently processed DOM Element.
|
|
for (const oneRule of this.#getElementRules(
|
|
rawNode,
|
|
"",
|
|
isInherited ? node : null,
|
|
filter
|
|
)) {
|
|
rules.push(oneRule);
|
|
}
|
|
|
|
// If we don't want to check pseudo elements rules, we can stop here.
|
|
if (skipPseudo) {
|
|
return rules;
|
|
}
|
|
|
|
// Now retrieve any pseudo element rules.
|
|
// We can have pseudo element that are children of other pseudo elements (e.g. with
|
|
// ::before::marker , ::marker is a child of ::before).
|
|
// In such case, we want to call #getElementRules with the actual pseudo element node,
|
|
// not its binding element.
|
|
|
|
const relevantPseudoElements = [];
|
|
for (const readPseudo of PSEUDO_ELEMENTS) {
|
|
if (!this.#pseudoIsRelevant(rawNode, readPseudo, isInherited)) {
|
|
continue;
|
|
}
|
|
|
|
// FIXME: Bug 1909173. Need to handle view transitions peudo-elements.
|
|
if (readPseudo === "::highlight") {
|
|
InspectorUtils.getRegisteredCssHighlights(
|
|
this.inspector.targetActor.window.document,
|
|
// only active
|
|
true
|
|
).forEach(name => {
|
|
relevantPseudoElements.push(`::highlight(${name})`);
|
|
});
|
|
} else {
|
|
relevantPseudoElements.push(readPseudo);
|
|
}
|
|
}
|
|
|
|
for (const readPseudo of relevantPseudoElements) {
|
|
const pseudoRules = this.#getElementRules(
|
|
rawNode,
|
|
readPseudo,
|
|
isInherited ? node : null,
|
|
filter
|
|
);
|
|
// inherited element backed pseudo element rules (e.g. `::details-content`) should
|
|
// not be at the same "level" as rules inherited from the binding element (e.g. `<details>`),
|
|
// so we need to put them before the "regular" rules.
|
|
if (
|
|
SharedCssLogic.ELEMENT_BACKED_PSEUDO_ELEMENTS.has(readPseudo) &&
|
|
isInherited
|
|
) {
|
|
rules.unshift(...pseudoRules);
|
|
} else {
|
|
rules.push(...pseudoRules);
|
|
}
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Create a new rule description ultimately returned by PageStyle.getApplied method.
|
|
*
|
|
* @param {StyleRuleActor} rule
|
|
* @param {DOMNode | null} rawNode
|
|
* @param {object} params
|
|
* @param {NodeActor} params.inherited
|
|
* @param {boolean} params.isSystem
|
|
* @param {string | null} params.pseudoElement
|
|
* @param {StyleRuleActor} params.keyframes
|
|
* @returns {appliedstyle}
|
|
*/
|
|
#getRuleItem(
|
|
rule,
|
|
rawNode = null,
|
|
{ inherited, isSystem, pseudoElement, keyframes } = {}
|
|
) {
|
|
let siblingCount;
|
|
let siblingIndex;
|
|
if (rawNode) {
|
|
let element = rawNode;
|
|
// if we have a pseudoElement, the sibling-count() and sibling-index() are computed
|
|
// based on its binding element
|
|
if (element.implementedPseudoElement) {
|
|
element = CssLogic.getBindingElementAndPseudo(element).bindingElement;
|
|
}
|
|
|
|
const parentNode = element?.parentNode;
|
|
siblingCount = parentNode?.childElementCount;
|
|
if (parentNode) {
|
|
for (let i = 0; i < siblingCount; i++) {
|
|
if (parentNode.children[i] === element) {
|
|
// sibling-index() is 1-based
|
|
siblingIndex = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
// /!\ Keep "appliedstyle" protocol.js type definition in sync with this object
|
|
rule,
|
|
pseudoElement,
|
|
isSystem,
|
|
inherited,
|
|
// We can't compute the value for the whole document as the color scheme
|
|
// can be set at the node level (e.g. with `color-scheme`)
|
|
darkColorScheme: rawNode
|
|
? InspectorUtils.isUsedColorSchemeDark(rawNode)
|
|
: undefined,
|
|
keyframes,
|
|
// May be later set from getAppliedProps.
|
|
matchedSelectorIndexes: undefined,
|
|
siblingCount,
|
|
siblingIndex,
|
|
};
|
|
}
|
|
|
|
#nodeIsTextfieldLike(node) {
|
|
if (node.nodeName == "TEXTAREA") {
|
|
return true;
|
|
}
|
|
return (
|
|
node.mozIsTextField &&
|
|
(node.mozIsTextField(false) || node.type == "number")
|
|
);
|
|
}
|
|
|
|
#nodeIsListItem(node) {
|
|
const computed = CssLogic.getComputedStyle(node);
|
|
if (!computed) {
|
|
return false;
|
|
}
|
|
|
|
const display = computed.getPropertyValue("display");
|
|
// This is written this way to handle `inline list-item` and such.
|
|
return display.split(" ").includes("list-item");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or node the pseudo element is relevant for the passed node
|
|
*
|
|
* @param {DOMNode} node
|
|
* @param {string} pseudo
|
|
* @param {boolean} isInherited
|
|
* @returns {boolean}
|
|
*/
|
|
// eslint-disable-next-line complexity
|
|
#pseudoIsRelevant(node, pseudo, isInherited = false) {
|
|
switch (pseudo) {
|
|
case "::after":
|
|
case "::before":
|
|
case "::first-letter":
|
|
case "::first-line":
|
|
case "::selection":
|
|
case "::highlight":
|
|
case "::target-text":
|
|
return !isInherited;
|
|
case "::marker":
|
|
return !isInherited && this.#nodeIsListItem(node);
|
|
case "::backdrop":
|
|
return !isInherited && node.matches(":modal, :popover-open");
|
|
case "::cue":
|
|
return !isInherited && node.nodeName == "VIDEO";
|
|
case "::file-selector-button":
|
|
return !isInherited && node.nodeName == "INPUT" && node.type == "file";
|
|
case "::details-content": {
|
|
const isDetailsNode = node.nodeName == "DETAILS";
|
|
if (!isDetailsNode) {
|
|
return false;
|
|
}
|
|
|
|
if (!isInherited) {
|
|
return true;
|
|
}
|
|
|
|
// If we're getting rules on a parent element, we need to check if the selected
|
|
// element is inside the ::details-content of node
|
|
// We traverse the flattened parent tree until we find the <slot> that implements
|
|
// the pseudo element, as it's easier to handle edge cases like nested <details>,
|
|
// multiple <summary>, etc …
|
|
let traversedNode = this.selectedElement;
|
|
while (traversedNode) {
|
|
if (
|
|
// if we found the <slot> implementing the pseudo element
|
|
traversedNode.implementedPseudoElement === "::details-content" &&
|
|
// and its parent <details> element is the element we're evaluating
|
|
traversedNode.flattenedTreeParentNode === node
|
|
) {
|
|
// then include the ::details-content rules from that element
|
|
return true;
|
|
}
|
|
// otherwise keep looking up the tree
|
|
traversedNode = traversedNode.flattenedTreeParentNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
case "::placeholder":
|
|
case "::-moz-placeholder":
|
|
return !isInherited && this.#nodeIsTextfieldLike(node);
|
|
case "::-moz-meter-bar":
|
|
return !isInherited && node.nodeName == "METER";
|
|
case "::-moz-progress-bar":
|
|
return !isInherited && node.nodeName == "PROGRESS";
|
|
case "::-moz-color-swatch":
|
|
return !isInherited && node.nodeName == "INPUT" && node.type == "color";
|
|
case "::-moz-range-progress":
|
|
case "::-moz-range-thumb":
|
|
case "::-moz-range-track":
|
|
case "::slider-fill":
|
|
case "::slider-thumb":
|
|
case "::slider-track":
|
|
return !isInherited && node.nodeName == "INPUT" && node.type == "range";
|
|
case "::view-transition":
|
|
case "::view-transition-group":
|
|
case "::view-transition-image-pair":
|
|
case "::view-transition-old":
|
|
case "::view-transition-new":
|
|
// FIXME: Bug 1909173. Need to handle view transitions peudo-elements
|
|
// for DevTools. For now we skip them.
|
|
return false;
|
|
case "::picker-icon":
|
|
case "::picker":
|
|
// FIXME: Bug 2042839. Need to handle in DevTools.
|
|
return !isInherited && node.nodeName == "SELECT";
|
|
case "::checkmark":
|
|
return !isInherited && node.nodeName == "OPTION";
|
|
case "::-webkit-scrollbar":
|
|
return false;
|
|
default:
|
|
console.error("Unhandled pseudo-element " + pseudo);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function for #getAllElementRules, returns the rules from a given
|
|
* element. See getApplied for documentation on parameters.
|
|
*
|
|
* @param {DOMNode} node
|
|
* @param {string} pseudo
|
|
* @param {NodeActor} inherited
|
|
* @param {GetAppliedFilterOption} filter
|
|
*
|
|
* @returns {Array<appliedstyle>}
|
|
*/
|
|
#getElementRules(node, pseudo, inherited, filter) {
|
|
if (!Element.isInstance(node)) {
|
|
return [];
|
|
}
|
|
|
|
// we don't need to retrieve inherited starting style rules
|
|
const includeStartingStyleRules = !inherited;
|
|
const domRules = InspectorUtils.getMatchingCSSRules(
|
|
node,
|
|
pseudo,
|
|
CssLogic.hasVisitedState(node),
|
|
includeStartingStyleRules
|
|
);
|
|
|
|
if (!domRules) {
|
|
return [];
|
|
}
|
|
|
|
const rules = [];
|
|
|
|
const doc = this.inspector.targetActor.window.document;
|
|
|
|
let hasClosestAppearanceBaseNode = null;
|
|
|
|
// getMatchingCSSRules returns ordered from least-specific to
|
|
// most-specific.
|
|
for (let i = domRules.length - 1; i >= 0; i--) {
|
|
const domRule = domRules[i];
|
|
const isSystem =
|
|
domRule.parentStyleSheet &&
|
|
SharedCssLogic.isAgentStylesheet(domRule.parentStyleSheet);
|
|
|
|
// For now, when dealing with InspectorDeclaration, we only care about presentational
|
|
// hints style (e.g. <img height=100>).
|
|
if (
|
|
domRule.declarationOrigin &&
|
|
domRule.declarationOrigin !== "pres-hints"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (isSystem && filter != SharedCssLogic.FILTER.UA) {
|
|
continue;
|
|
}
|
|
|
|
if (inherited) {
|
|
// Don't include inherited rules if none of its properties
|
|
// are inheritable.
|
|
let hasInherited = false;
|
|
// This can be on a hot path, so let's use a simple for rule instead of turning
|
|
// domRule.style into an Array to use some on it.
|
|
for (let j = 0, len = domRule.style.length; j < len; j++) {
|
|
if (InspectorUtils.isInheritedProperty(doc, domRule.style[j])) {
|
|
hasInherited = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasInherited) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (isSystem) {
|
|
// Rules inside @appearance-base only apply if the effective appearance value is
|
|
// base / base-select in the element or any of its ancestors.
|
|
// For now, if we don't match those condition, we're not going to show those rules
|
|
// in the UI so it doesn't interfere with how we're handling overridden declarations.
|
|
// Ideally, this should be done in C++ for better performance (see Bug 2028761).
|
|
let currentRule = domRule;
|
|
let hasClosestAppearanceBaseRule = false;
|
|
while (currentRule) {
|
|
if (
|
|
ChromeUtils.getClassName(currentRule) === "CSSAppearanceBaseRule"
|
|
) {
|
|
hasClosestAppearanceBaseRule = true;
|
|
break;
|
|
}
|
|
currentRule = currentRule.parentRule;
|
|
}
|
|
|
|
if (hasClosestAppearanceBaseRule) {
|
|
// lazyily compute this as this can be costly
|
|
if (hasClosestAppearanceBaseNode === null) {
|
|
hasClosestAppearanceBaseNode = false;
|
|
let currentNode = node;
|
|
while (currentNode) {
|
|
const computed = CssLogic.getComputedStyle(currentNode);
|
|
const appearance = computed
|
|
? computed.getPropertyValue("appearance")
|
|
: null;
|
|
if (appearance === "base" || appearance === "base-select") {
|
|
hasClosestAppearanceBaseNode = true;
|
|
break;
|
|
}
|
|
currentNode = currentNode.parentElement;
|
|
}
|
|
}
|
|
|
|
if (!hasClosestAppearanceBaseNode) {
|
|
// We don't want to display @appearance-base rules if they will be inactive
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ruleActor = this.styleRef(domRule, pseudo);
|
|
|
|
rules.push(
|
|
this.#getRuleItem(ruleActor, node, {
|
|
inherited,
|
|
isSystem,
|
|
pseudoElement: pseudo,
|
|
})
|
|
);
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Given a node and a CSS rule, walk up the DOM looking for a matching element rule.
|
|
*
|
|
* @param {NodeActor} nodeActor the node
|
|
* @param {CSSStyleRule} matchingRule the rule to find the entry for
|
|
* @return {object | null} An entry as returned by #getAllElementRules, or null if no entry
|
|
* matching the passed rule was find
|
|
*/
|
|
findEntryMatchingRule(nodeActor, matchingRule) {
|
|
let currentNodeActor = nodeActor;
|
|
while (
|
|
currentNodeActor &&
|
|
currentNodeActor.rawNode.nodeType != Node.DOCUMENT_NODE
|
|
) {
|
|
for (const entry of this.#getAllElementRules(currentNodeActor, {
|
|
isInherited: nodeActor !== currentNodeActor,
|
|
})) {
|
|
if (entry.rule.rawRule === matchingRule) {
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
currentNodeActor = this.walker.parentNode(currentNodeActor);
|
|
}
|
|
|
|
// If we reached the document node without finding the rule, return null
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Helper function for getApplied that fetches a set of style properties that
|
|
* apply to the given node and associated rules
|
|
*
|
|
* @param {NodeActor} node
|
|
* @param {Array<appliedstyle>} entries
|
|
* List of appliedstyle objects that lists the rules that apply to the
|
|
* node. If adding a new rule to the stylesheet, only the new rule entry
|
|
* is provided and only the style properties that apply to the new
|
|
* rule is fetched.
|
|
* @param {GetAppliedOptions} options
|
|
* @returns {Array<appliedstyle>} of rule entries that applies to the given node and its associated rules.
|
|
*/
|
|
getAppliedProps(node, entries, options) {
|
|
if (options.inherited) {
|
|
let parent = this.walker.parentNode(node);
|
|
while (parent && parent.rawNode.nodeType != Node.DOCUMENT_NODE) {
|
|
entries = entries.concat(
|
|
this.#getAllElementRules(parent, {
|
|
isInherited: true,
|
|
skipPseudo: options.skipPseudo,
|
|
filter: options.filter,
|
|
})
|
|
);
|
|
parent = this.walker.parentNode(parent);
|
|
}
|
|
}
|
|
|
|
if (options.matchedSelectors) {
|
|
this.#computeSelectorIndexes(entries, node);
|
|
}
|
|
|
|
const computedStyle = this.cssLogic.computedStyle;
|
|
if (computedStyle) {
|
|
this.#getKeyFrameRules(entries, computedStyle);
|
|
this.#getPositionTryRules(entries, computedStyle);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
/**
|
|
* If requested by the client, populate the `matchedSelectorIndex` array in all `entries`.
|
|
* This array contains the indexes of all selectors that are matching the selected DOM element.
|
|
*
|
|
* @param {Array<appliedstyle>} entries
|
|
* @param {NodeActor} node
|
|
*/
|
|
#computeSelectorIndexes(entries, node) {
|
|
for (const entry of entries) {
|
|
if (entry.rule.type === ELEMENT_STYLE) {
|
|
continue;
|
|
}
|
|
entry.matchedSelectorIndexes = [];
|
|
|
|
const domRule = entry.rule.rawRule;
|
|
const element = entry.inherited ? entry.inherited.rawNode : node.rawNode;
|
|
|
|
const pseudos = [];
|
|
const { bindingElement, pseudo } =
|
|
CssLogic.getBindingElementAndPseudo(element);
|
|
|
|
// if we couldn't find a binding element, we can't call domRule.selectorMatchesElement,
|
|
// so bail out
|
|
if (!bindingElement) {
|
|
continue;
|
|
}
|
|
|
|
if (pseudo) {
|
|
pseudos.push(pseudo);
|
|
} else if (entry.rule.pseudoElements.size) {
|
|
// if `node` is not a pseudo element but the rule applies to some pseudo elements,
|
|
// we need to pass those to CSSStyleRule#selectorMatchesElement
|
|
pseudos.push(...entry.rule.pseudoElements);
|
|
} else {
|
|
// If the rule doesn't apply to any pseudo, set a null item so we'll still do
|
|
// the proper check below
|
|
pseudos.push(null);
|
|
}
|
|
|
|
const relevantLinkVisited = CssLogic.hasVisitedState(bindingElement);
|
|
const len = domRule.selectorCount;
|
|
for (let i = 0; i < len; i++) {
|
|
for (const pseudoElementName of pseudos) {
|
|
if (
|
|
domRule.selectorMatchesElement(
|
|
i,
|
|
bindingElement,
|
|
pseudoElementName,
|
|
relevantLinkVisited
|
|
)
|
|
) {
|
|
entry.matchedSelectorIndexes.push(i);
|
|
// if we matched the selector for one pseudo, no need to check the other ones
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add all the keyframes rules associated with the element into `entries`
|
|
* based on the applied declarations defined in `computedStyle`.
|
|
*
|
|
* @param {Array<appliedstyle>} entries
|
|
* @param {CSSStyleDeclaration} computedStyle
|
|
*/
|
|
#getKeyFrameRules(entries, computedStyle) {
|
|
let animationNames = computedStyle.animationName.split(",");
|
|
if (!animationNames.length) {
|
|
return;
|
|
}
|
|
animationNames = animationNames.map(name => name.trim());
|
|
|
|
// Traverse through all the available keyframes rule and add
|
|
// the keyframes rule that matches the computed animation name
|
|
for (const keyframesRule of this.cssLogic.keyframesRules) {
|
|
if (!animationNames.includes(keyframesRule.name)) {
|
|
continue;
|
|
}
|
|
|
|
for (const rule of keyframesRule.cssRules) {
|
|
entries.push(
|
|
this.#getRuleItem(this.styleRef(rule), null, {
|
|
keyframes: this.styleRef(keyframesRule),
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add all the @position-try associated with the element into `entries`
|
|
* based on the applied declarations defined in `computedStyle`.
|
|
*
|
|
* @param {Array<appliedstyle>} entries
|
|
* @param {CSSStyleDeclaration} computedStyle
|
|
*/
|
|
#getPositionTryRules(entries, computedStyle) {
|
|
const positionTryIdents = new Set();
|
|
for (const part of computedStyle.positionTryFallbacks.split(",")) {
|
|
const name = part.trim();
|
|
if (name.startsWith("--")) {
|
|
positionTryIdents.add(name);
|
|
}
|
|
}
|
|
|
|
if (!positionTryIdents.size) {
|
|
return;
|
|
}
|
|
|
|
for (const positionTryRule of this.cssLogic.positionTryRules) {
|
|
if (!positionTryIdents.has(positionTryRule.name)) {
|
|
continue;
|
|
}
|
|
entries.push(this.#getRuleItem(this.styleRef(positionTryRule)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get layout-related information about a node.
|
|
* This method returns an object with properties giving information about
|
|
* the node's margin, border, padding and content region sizes, as well
|
|
* as information about the type of box, its position, z-index, etc...
|
|
*
|
|
* @param {NodeActor} node
|
|
* @param {object} options The only available option is autoMargins.
|
|
* If set to true, the element's margins will receive an extra check to see
|
|
* whether they are set to "auto" (knowing that the computed-style in this
|
|
* case would return "0px").
|
|
* The returned object will contain an extra property (autoMargins) listing
|
|
* all margins that are set to auto, e.g. {top: "auto", left: "auto"}.
|
|
* @return {object}
|
|
*/
|
|
getLayout(node, options) {
|
|
this.cssLogic.highlight(node.rawNode);
|
|
|
|
const layout = {};
|
|
|
|
// First, we update the first part of the box model view, with
|
|
// the size of the element.
|
|
|
|
const clientRect = node.rawNode.getBoundingClientRect();
|
|
layout.width = parseFloat(clientRect.width.toPrecision(6));
|
|
layout.height = parseFloat(clientRect.height.toPrecision(6));
|
|
|
|
// We compute and update the values of margins & co.
|
|
const style = CssLogic.getComputedStyle(node.rawNode);
|
|
for (const prop of [
|
|
"position",
|
|
"top",
|
|
"right",
|
|
"bottom",
|
|
"left",
|
|
"margin-top",
|
|
"margin-right",
|
|
"margin-bottom",
|
|
"margin-left",
|
|
"padding-top",
|
|
"padding-right",
|
|
"padding-bottom",
|
|
"padding-left",
|
|
"border-top-width",
|
|
"border-right-width",
|
|
"border-bottom-width",
|
|
"border-left-width",
|
|
"z-index",
|
|
"box-sizing",
|
|
"display",
|
|
"float",
|
|
"line-height",
|
|
]) {
|
|
layout[prop] = style.getPropertyValue(prop);
|
|
}
|
|
|
|
if (options.autoMargins) {
|
|
layout.autoMargins = this.processMargins(this.cssLogic);
|
|
}
|
|
|
|
for (const i in this.map) {
|
|
const property = this.map[i].property;
|
|
this.map[i].value = parseFloat(style.getPropertyValue(property));
|
|
}
|
|
|
|
return layout;
|
|
}
|
|
|
|
/**
|
|
* Find 'auto' margin properties.
|
|
*/
|
|
processMargins(cssLogic) {
|
|
const margins = {};
|
|
|
|
for (const prop of ["top", "bottom", "left", "right"]) {
|
|
const info = cssLogic.getPropertyInfo("margin-" + prop);
|
|
const selectors = info.matchedSelectors;
|
|
if (selectors && !!selectors.length && selectors[0].value == "auto") {
|
|
margins[prop] = "auto";
|
|
}
|
|
}
|
|
|
|
return margins;
|
|
}
|
|
|
|
/**
|
|
* On page navigation, tidy up remaining objects.
|
|
*/
|
|
onFrameUnload() {
|
|
this.styleSheetsByRootNode = new WeakMap();
|
|
}
|
|
|
|
#onStylesheetUpdated = ({ resourceId, updateKind, updates = {} }) => {
|
|
if (updateKind != "style-applied") {
|
|
return;
|
|
}
|
|
const kind = updates.event.kind;
|
|
// Duplicate refMap content before looping as onStyleApplied may mutate it
|
|
for (const styleActor of [...this.refMap.values()]) {
|
|
// Ignore StyleRuleActor that don't have a parent stylesheet.
|
|
// i.e. actor whose type is ELEMENT_STYLE.
|
|
if (!styleActor._parentSheet) {
|
|
continue;
|
|
}
|
|
const resId = this.styleSheetsManager.getStyleSheetResourceId(
|
|
styleActor._parentSheet
|
|
);
|
|
if (resId === resourceId) {
|
|
styleActor.onStyleApplied(kind);
|
|
}
|
|
}
|
|
this.#styleApplied(kind);
|
|
};
|
|
|
|
/**
|
|
* Helper function for adding a new rule and getting its applied style
|
|
* properties
|
|
*
|
|
* @param NodeActor node
|
|
* @param CSSStyleRule rule
|
|
* @returns Array containing its applied style properties
|
|
*/
|
|
getNewAppliedProps(node, rule) {
|
|
const ruleActor = this.styleRef(rule);
|
|
return this.getAppliedProps(node, [this.#getRuleItem(ruleActor)], {
|
|
matchedSelectors: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds a new rule, and returns the new StyleRuleActor.
|
|
*
|
|
* @param {NodeActor} node
|
|
* @param {string} pseudoClasses The list of pseudo classes to append to the
|
|
* new selector.
|
|
* @returns {StyleRuleActor} the new rule
|
|
*/
|
|
async addNewRule(node, pseudoClasses) {
|
|
let sheet = null;
|
|
const doc = node.rawNode.ownerDocument;
|
|
const rootNode = node.rawNode.getRootNode();
|
|
|
|
if (
|
|
this.styleSheetsByRootNode.has(rootNode) &&
|
|
this.styleSheetsByRootNode.get(rootNode).ownerNode?.isConnected
|
|
) {
|
|
sheet = this.styleSheetsByRootNode.get(rootNode);
|
|
} else {
|
|
sheet = await this.styleSheetsManager.addStyleSheet(
|
|
doc,
|
|
node.rawNode.containingShadowRoot || doc.documentElement
|
|
);
|
|
this.styleSheetsByRootNode.set(rootNode, sheet);
|
|
}
|
|
|
|
const cssRules = sheet.cssRules;
|
|
|
|
// Get the binding element in case node is a pseudo element, so we can properly
|
|
// build the selector
|
|
const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(
|
|
node.rawNode
|
|
);
|
|
const classes = [...bindingElement.classList];
|
|
|
|
let selector;
|
|
if (bindingElement.id) {
|
|
selector = "#" + CSS.escape(bindingElement.id);
|
|
} else if (classes.length) {
|
|
selector = "." + classes.map(c => CSS.escape(c)).join(".");
|
|
} else {
|
|
selector = bindingElement.localName;
|
|
}
|
|
|
|
if (pseudo && pseudoClasses?.length) {
|
|
throw new Error(
|
|
`Can't set pseudo classes (${JSON.stringify(pseudoClasses)}) onto a pseudo element (${pseudo})`
|
|
);
|
|
}
|
|
|
|
if (pseudo) {
|
|
selector += pseudo;
|
|
}
|
|
if (pseudoClasses && pseudoClasses.length) {
|
|
selector += pseudoClasses.join("");
|
|
}
|
|
|
|
const index = sheet.insertRule(selector + " {}", cssRules.length);
|
|
|
|
const resourceId = this.styleSheetsManager.getStyleSheetResourceId(sheet);
|
|
let authoredText = await this.styleSheetsManager.getText(resourceId);
|
|
authoredText += "\n" + selector + " {\n" + "}";
|
|
await this.styleSheetsManager.setStyleSheetText(resourceId, authoredText);
|
|
|
|
const cssRule = sheet.cssRules.item(index);
|
|
const ruleActor = this.styleRef(cssRule, null, true);
|
|
|
|
this.inspector.targetActor.emit("track-css-change", {
|
|
...ruleActor.metadata,
|
|
type: "rule-add",
|
|
add: null,
|
|
remove: null,
|
|
selector,
|
|
});
|
|
|
|
return { entries: this.getNewAppliedProps(node, cssRule) };
|
|
}
|
|
|
|
/**
|
|
* Cause all StyleRuleActor instances of observed CSS rules to check whether the
|
|
* states of their declarations have changed.
|
|
*
|
|
* Observed rules are the latest rules returned by a call to PageStyleActor.getApplied()
|
|
*
|
|
* This is necessary because changes in one rule can cause the declarations in another
|
|
* to not be applicable (inactive CSS). The observers of those rules should be notified.
|
|
* Rules will fire a "rule-updated" event if any of their declarations changed state.
|
|
*
|
|
* Call this method whenever a CSS rule is mutated:
|
|
* - a CSS declaration is added/changed/disabled/removed
|
|
* - a selector is added/changed/removed
|
|
*
|
|
* @param {Array<StyleRuleActor>} rulesToForceRefresh: An array of rules that,
|
|
* if observed, should be refreshed even if the state of their declaration
|
|
* didn't change.
|
|
*/
|
|
refreshObservedRules(rulesToForceRefresh) {
|
|
for (const rule of this.#observedRules) {
|
|
const force = rulesToForceRefresh && rulesToForceRefresh.includes(rule);
|
|
rule.maybeRefresh(force);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an array of existing attribute values in a node document.
|
|
*
|
|
* @param {string} search: A string to filter attribute value on.
|
|
* @param {string} attributeType: The type of attribute we want to retrieve the values.
|
|
* @param {Element} node: The element we want to get possible attributes for. This will
|
|
* be used to get the document where the search is happening.
|
|
* @returns {Array<string>} An array of strings
|
|
*/
|
|
getAttributesInOwnerDocument(search, attributeType, node) {
|
|
if (!search) {
|
|
throw new Error("search is mandatory");
|
|
}
|
|
|
|
// In a non-fission world, a node from an iframe shares the same `rootNode` as a node
|
|
// in the top-level document. So here we need to retrieve the document from the node
|
|
// in parameter in order to retrieve the right document.
|
|
// This may change once we have a dedicated walker for every target in a tab, as we'll
|
|
// be able to directly talk to the "right" walker actor.
|
|
const targetDocument = node.rawNode.ownerDocument;
|
|
|
|
// We store the result in a Set which will contain the attribute value
|
|
const result = new Set();
|
|
const lcSearch = search.toLowerCase();
|
|
this.#collectAttributesFromDocumentDOM(
|
|
result,
|
|
lcSearch,
|
|
attributeType,
|
|
targetDocument,
|
|
node.rawNode
|
|
);
|
|
this.#collectAttributesFromDocumentStyleSheets(
|
|
result,
|
|
lcSearch,
|
|
attributeType,
|
|
targetDocument
|
|
);
|
|
|
|
return Array.from(result).sort();
|
|
}
|
|
|
|
/**
|
|
* Collect attribute values from the document DOM tree, matching the passed filter and
|
|
* type, to the result Set.
|
|
*
|
|
* @param {Set<string>} result: A Set to which the results will be added.
|
|
* @param {string} search: A string to filter attribute value on.
|
|
* @param {string} attributeType: The type of attribute we want to retrieve the values.
|
|
* @param {Document} targetDocument: The document the search occurs in.
|
|
* @param {Node} currentNode: The current element rawNode
|
|
*/
|
|
#collectAttributesFromDocumentDOM(
|
|
result,
|
|
search,
|
|
attributeType,
|
|
targetDocument,
|
|
nodeRawNode
|
|
) {
|
|
// In order to retrieve attributes from DOM elements in the document, we're going to
|
|
// do a query on the root node using attributes selector, to directly get the elements
|
|
// matching the attributes we're looking for.
|
|
|
|
// For classes, we need something a bit different as the className we're looking
|
|
// for might not be the first in the attribute value, meaning we can't use the
|
|
// "attribute starts with X" selector.
|
|
const attributeSelectorPositionChar = attributeType === "class" ? "*" : "^";
|
|
const selector = `[${attributeType}${attributeSelectorPositionChar}=${search} i]`;
|
|
|
|
const matchingElements = targetDocument.querySelectorAll(selector);
|
|
|
|
for (const element of matchingElements) {
|
|
if (element === nodeRawNode) {
|
|
return;
|
|
}
|
|
// For class attribute, we need to add the elements of the classList that match
|
|
// the filter string.
|
|
if (attributeType === "class") {
|
|
for (const cls of element.classList) {
|
|
if (!result.has(cls) && cls.toLowerCase().startsWith(search)) {
|
|
result.add(cls);
|
|
}
|
|
}
|
|
} else {
|
|
const { value } = element.attributes[attributeType];
|
|
// For other attributes, we can directly use the attribute value.
|
|
result.add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect attribute values from the document stylesheets, matching the passed filter
|
|
* and type, to the result Set.
|
|
*
|
|
* @param {Set<string>} result: A Set to which the results will be added.
|
|
* @param {string} search: A string to filter attribute value on.
|
|
* @param {string} attributeType: The type of attribute we want to retrieve the values.
|
|
* It only supports "class" and "id" at the moment.
|
|
* @param {Document} targetDocument: The document the search occurs in.
|
|
*/
|
|
#collectAttributesFromDocumentStyleSheets(
|
|
result,
|
|
search,
|
|
attributeType,
|
|
targetDocument
|
|
) {
|
|
if (attributeType !== "class" && attributeType !== "id") {
|
|
return;
|
|
}
|
|
|
|
// We loop through all the stylesheets and their rules, recursively so we can go through
|
|
// nested rules, and then use the lexer to only get the attributes we're looking for.
|
|
const traverseRules = ruleList => {
|
|
for (const rule of ruleList) {
|
|
this.#collectAttributesFromRule(result, rule, search, attributeType);
|
|
if (rule.cssRules) {
|
|
traverseRules(rule.cssRules);
|
|
}
|
|
}
|
|
};
|
|
for (const styleSheet of targetDocument.styleSheets) {
|
|
traverseRules(styleSheet.rules);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect attribute values from the rule, matching the passed filter and type, to the
|
|
* result Set.
|
|
*
|
|
* @param {Set<string>} result: A Set to which the results will be added.
|
|
* @param {Rule} rule: The rule the search occurs in.
|
|
* @param {string} search: A string to filter attribute value on.
|
|
* @param {string} attributeType: The type of attribute we want to retrieve the values.
|
|
* It only supports "class" and "id" at the moment.
|
|
*/
|
|
#collectAttributesFromRule(result, rule, search, attributeType) {
|
|
const shouldRetrieveClasses = attributeType === "class";
|
|
const shouldRetrieveIds = attributeType === "id";
|
|
|
|
const { selectorText } = rule;
|
|
// If there's no selectorText, or if the selectorText does not include the
|
|
// filter, we can bail out.
|
|
if (!selectorText || !selectorText.toLowerCase().includes(search)) {
|
|
return;
|
|
}
|
|
|
|
// Check if we should parse the selectorText (do we need to check for class/id and
|
|
// if so, does the selector contains class/id related chars).
|
|
const parseForClasses =
|
|
shouldRetrieveClasses &&
|
|
selectorText.toLowerCase().includes(`.${search}`);
|
|
const parseForIds =
|
|
shouldRetrieveIds && selectorText.toLowerCase().includes(`#${search}`);
|
|
|
|
if (!parseForClasses && !parseForIds) {
|
|
return;
|
|
}
|
|
|
|
const lexer = new InspectorCSSParser(selectorText);
|
|
let token;
|
|
while ((token = lexer.nextToken())) {
|
|
if (
|
|
token.tokenType === "Delim" &&
|
|
shouldRetrieveClasses &&
|
|
token.text === "."
|
|
) {
|
|
token = lexer.nextToken();
|
|
if (
|
|
token.tokenType === "Ident" &&
|
|
token.text.toLowerCase().startsWith(search)
|
|
) {
|
|
result.add(token.text);
|
|
}
|
|
}
|
|
if (token.tokenType === "IDHash" && shouldRetrieveIds) {
|
|
const idWithoutHash = token.value;
|
|
if (idWithoutHash.startsWith(search)) {
|
|
result.add(idWithoutHash);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an array of valid anchor names for the selected node
|
|
*
|
|
* @param {NodeActor} node: The node for which we want anchor names
|
|
* @return {Array<string>}
|
|
*/
|
|
getAnchorNames(node) {
|
|
return InspectorUtils.getAnchorNamesFor(node.rawNode);
|
|
}
|
|
}
|
|
exports.PageStyleActor = PageStyleActor;
|