1673 lines
52 KiB
JavaScript
1673 lines
52 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";
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"CssLogic",
|
|
"resource://devtools/server/actors/inspector/css-logic.js",
|
|
true
|
|
);
|
|
|
|
const INACTIVE_CSS_ENABLED = Services.prefs.getBoolPref(
|
|
"devtools.inspector.inactive.css.enabled",
|
|
false
|
|
);
|
|
|
|
const TEXT_WRAP_BALANCE_LIMIT = Services.prefs.getIntPref(
|
|
"layout.css.text-wrap-balance.limit",
|
|
10
|
|
);
|
|
|
|
const ALIGN_CONTENT_BLOCKS = Services.prefs.getBoolPref(
|
|
"layout.css.align-content.blocks.enabled",
|
|
false
|
|
);
|
|
|
|
const VISITED_MDN_LINK = "https://developer.mozilla.org/docs/Web/CSS/:visited";
|
|
const VISITED_INVALID_PROPERTIES = allCssPropertiesExcept([
|
|
"all",
|
|
"color",
|
|
"background",
|
|
"background-color",
|
|
"border",
|
|
"border-color",
|
|
"border-bottom-color",
|
|
"border-left-color",
|
|
"border-right-color",
|
|
"border-top-color",
|
|
"border-block",
|
|
"border-block-color",
|
|
"border-block-start-color",
|
|
"border-block-end-color",
|
|
"border-inline",
|
|
"border-inline-color",
|
|
"border-inline-start-color",
|
|
"border-inline-end-color",
|
|
"column-rule",
|
|
"column-rule-color",
|
|
"outline",
|
|
"outline-color",
|
|
"text-decoration-color",
|
|
"text-emphasis-color",
|
|
]);
|
|
|
|
// Set of node names which are always treated as replaced elements:
|
|
const REPLACED_ELEMENTS_NAMES = new Set([
|
|
"audio",
|
|
"br",
|
|
"button",
|
|
"canvas",
|
|
"embed",
|
|
"hr",
|
|
"iframe",
|
|
// Inputs are generally replaced elements. E.g. checkboxes and radios are replaced
|
|
// unless they have `appearance: none`. However unconditionally treating them
|
|
// as replaced is enough for our purpose here, and avoids extra complexity that
|
|
// will likely not be necessary in most cases.
|
|
"input",
|
|
"math",
|
|
"object",
|
|
"picture",
|
|
// Select is a replaced element if it has `size<=1` or no size specified, but
|
|
// unconditionally treating it as replaced is enough for our purpose here, and
|
|
// avoids extra complexity that will likely not be necessary in most cases.
|
|
"select",
|
|
"svg",
|
|
"textarea",
|
|
"video",
|
|
]);
|
|
|
|
const CUE_PSEUDO_ELEMENT_STYLING_SPEC_URL =
|
|
"https://developer.mozilla.org/docs/Web/CSS/::cue";
|
|
|
|
const HIGHLIGHT_PSEUDO_ELEMENTS_STYLING_SPEC_URL =
|
|
"https://www.w3.org/TR/css-pseudo-4/#highlight-styling";
|
|
const HIGHLIGHT_PSEUDO_ELEMENTS = [
|
|
"::highlight",
|
|
"::selection",
|
|
"::target-text",
|
|
// Below are properties not yet implemented in Firefox (Bug 1694053)
|
|
"::grammar-error",
|
|
"::spelling-error",
|
|
];
|
|
const REGEXP_HIGHLIGHT_PSEUDO_ELEMENTS = new RegExp(
|
|
`${HIGHLIGHT_PSEUDO_ELEMENTS.join("|")}`
|
|
);
|
|
|
|
const FIRST_LINE_PSEUDO_ELEMENT_STYLING_SPEC_URL =
|
|
"https://www.w3.org/TR/css-pseudo-4/#first-line-styling";
|
|
|
|
const FIRST_LETTER_PSEUDO_ELEMENT_STYLING_SPEC_URL =
|
|
"https://www.w3.org/TR/css-pseudo-4/#first-letter-styling";
|
|
|
|
const PLACEHOLDER_PSEUDO_ELEMENT_STYLING_SPEC_URL =
|
|
"https://www.w3.org/TR/css-pseudo-4/#placeholder-pseudo";
|
|
|
|
class InactivePropertyHelper {
|
|
/**
|
|
* A list of rules for when CSS properties have no effect.
|
|
*
|
|
* In certain situations, CSS properties do not have any effect. A common
|
|
* example is trying to set a width on an inline element like a <span>.
|
|
*
|
|
* There are so many properties in CSS that it's difficult to remember which
|
|
* ones do and don't apply in certain situations. Some are straight-forward
|
|
* like `flex-wrap` only applying to an element that has `display:flex`.
|
|
* Others are less trivial like setting something other than a color on a
|
|
* `:visited` pseudo-class.
|
|
*
|
|
* This file contains "rules" in the form of objects with the following
|
|
* properties:
|
|
* {
|
|
* invalidProperties:
|
|
* Set of CSS property names that are inactive if the rule matches.
|
|
* when:
|
|
* The rule itself, a JS function used to identify the conditions
|
|
* indicating whether a property is valid or not.
|
|
* fixId:
|
|
* A Fluent id containing a suggested solution to the problem that is
|
|
* causing a property to be inactive.
|
|
* msgId:
|
|
* A Fluent id containing an error message explaining why a property is
|
|
* inactive in this situation.
|
|
* }
|
|
*
|
|
* If you add a new rule, also add a test for it in:
|
|
* server/tests/chrome/test_inspector-inactive-property-helper.html
|
|
*
|
|
* The main export is `isPropertyUsed()`, which can be used to check if a
|
|
* property is used or not, and why.
|
|
*
|
|
* NOTE: We should generally *not* add rules here for any CSS properties that
|
|
* inherit by default, because it's hard for us to know whether such
|
|
* properties are truly "inactive". Web developers might legitimately set
|
|
* such a property on any arbitrary element, in order to concisely establish
|
|
* the default property-value throughout that element's subtree. For example,
|
|
* consider the "list-style-*" properties, which inherit by default and which
|
|
* only have a rendering effect on elements with "display:list-item"
|
|
* (e.g. <li>). It might superficially seem like we could add a rule here to
|
|
* warn about usages of these properties on non-"list-item" elements, but we
|
|
* shouldn't actually warn about that. A web developer may legitimately
|
|
* prefer to set these properties on an arbitrary container element (e.g. an
|
|
* <ol> element, or even the <html> element) in order to concisely adjust the
|
|
* rendering of a whole list (or all the lists in a document).
|
|
*/
|
|
get INVALID_PROPERTIES_VALIDATORS() {
|
|
return [
|
|
// Flex container property used on non-flex container.
|
|
{
|
|
invalidProperties: ["flex-direction", "flex-flow", "flex-wrap"],
|
|
when: () => !this.flexContainer,
|
|
fixId: "inactive-css-not-flex-container-fix",
|
|
msgId: "inactive-css-not-flex-container",
|
|
},
|
|
// Flex item property used on non-flex item.
|
|
{
|
|
invalidProperties: ["flex", "flex-basis", "flex-grow", "flex-shrink"],
|
|
when: () => !this.flexItem,
|
|
fixId: "inactive-css-not-flex-item-fix-2",
|
|
msgId: "inactive-css-not-flex-item",
|
|
},
|
|
// Grid container property used on non-grid container.
|
|
{
|
|
invalidProperties: [
|
|
"grid-auto-columns",
|
|
"grid-auto-flow",
|
|
"grid-auto-rows",
|
|
"grid-template",
|
|
"grid-template-areas",
|
|
"grid-template-columns",
|
|
"grid-template-rows",
|
|
"justify-items",
|
|
],
|
|
when: () => !this.gridContainer,
|
|
fixId: "inactive-css-not-grid-container-fix",
|
|
msgId: "inactive-css-not-grid-container",
|
|
},
|
|
// Grid item property used on non-grid item.
|
|
{
|
|
invalidProperties: [
|
|
"grid-area",
|
|
"grid-column",
|
|
"grid-column-end",
|
|
"grid-column-start",
|
|
"grid-row",
|
|
"grid-row-end",
|
|
"grid-row-start",
|
|
"justify-self",
|
|
],
|
|
when: () => !this.gridItem && !this.isAbsPosGridElement(),
|
|
fixId: "inactive-css-not-grid-item-fix-2",
|
|
msgId: "inactive-css-not-grid-item",
|
|
},
|
|
// Grid and flex item properties used on non-grid or non-flex item.
|
|
{
|
|
invalidProperties: ["align-self", "place-self", "order"],
|
|
when: () =>
|
|
!this.gridItem && !this.flexItem && !this.isAbsPosGridElement(),
|
|
fixId: "inactive-css-not-grid-or-flex-item-fix-3",
|
|
msgId: "inactive-css-not-grid-or-flex-item",
|
|
},
|
|
// Grid and flex container properties used on non-grid or non-flex container.
|
|
{
|
|
invalidProperties: [
|
|
"align-items",
|
|
"justify-content",
|
|
"place-content",
|
|
"place-items",
|
|
"row-gap",
|
|
// grid-*-gap are supported legacy shorthands for the corresponding *-gap properties.
|
|
// See https://drafts.csswg.org/css-align-3/#gap-legacy for more information.
|
|
"grid-row-gap",
|
|
],
|
|
when: () => !this.gridContainer && !this.flexContainer,
|
|
fixId: "inactive-css-not-grid-or-flex-container-fix",
|
|
msgId: "inactive-css-not-grid-or-flex-container",
|
|
},
|
|
// align-content is special as align-content:baseline does have an effect on all
|
|
// grid items, flex items and table cells, regardless of what type of box they are.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1598730
|
|
{
|
|
invalidProperties: ["align-content"],
|
|
when: () => {
|
|
if (this.style["align-content"].includes("baseline")) {
|
|
return false;
|
|
}
|
|
const supportedDisplay = [
|
|
"flex",
|
|
"inline-flex",
|
|
"grid",
|
|
"inline-grid",
|
|
// Uncomment table-cell when Bug 1883357 is fixed.
|
|
// "table-cell"
|
|
];
|
|
if (ALIGN_CONTENT_BLOCKS) {
|
|
supportedDisplay.push("block", "inline-block");
|
|
}
|
|
return !this.checkComputedStyle("display", supportedDisplay);
|
|
},
|
|
fixId: ALIGN_CONTENT_BLOCKS
|
|
? "inactive-css-not-grid-or-flex-or-block-container-fix"
|
|
: "inactive-css-not-grid-or-flex-container-fix",
|
|
msgId: ALIGN_CONTENT_BLOCKS
|
|
? "inactive-css-property-because-of-display"
|
|
: "inactive-css-not-grid-or-flex-container",
|
|
},
|
|
// column-gap and shorthands used on non-grid or non-flex or non-multi-col container.
|
|
{
|
|
invalidProperties: [
|
|
"column-gap",
|
|
"gap",
|
|
"grid-gap",
|
|
// grid-*-gap are supported legacy shorthands for the corresponding *-gap properties.
|
|
// See https://drafts.csswg.org/css-align-3/#gap-legacy for more information.
|
|
"grid-column-gap",
|
|
],
|
|
when: () =>
|
|
!this.gridContainer && !this.flexContainer && !this.multiColContainer,
|
|
fixId:
|
|
"inactive-css-not-grid-or-flex-container-or-multicol-container-fix",
|
|
msgId: "inactive-css-not-grid-or-flex-container-or-multicol-container",
|
|
},
|
|
// Multi-column related properties used on non-multi-column container.
|
|
{
|
|
invalidProperties: [
|
|
"column-fill",
|
|
"column-rule",
|
|
"column-rule-color",
|
|
"column-rule-style",
|
|
"column-rule-width",
|
|
],
|
|
when: () => !this.multiColContainer,
|
|
fixId: "inactive-css-not-multicol-container-fix",
|
|
msgId: "inactive-css-not-multicol-container",
|
|
},
|
|
// column-span used within non-multi-column container.
|
|
{
|
|
invalidProperties: ["column-span"],
|
|
when: () => !this.inMultiColContainer,
|
|
fixId: "inactive-css-column-span-fix",
|
|
msgId: "inactive-css-column-span",
|
|
},
|
|
// Inline properties used on non-inline-level elements.
|
|
{
|
|
invalidProperties: ["vertical-align"],
|
|
when: () =>
|
|
!this.isInlineLevel() && !this.isFirstLetter && !this.isFirstLine,
|
|
fixId: "inactive-css-not-inline-or-tablecell-fix",
|
|
msgId: "inactive-css-not-inline-or-tablecell",
|
|
},
|
|
// Writing mode properties used on ::first-line pseudo-element.
|
|
{
|
|
invalidProperties: ["direction", "text-orientation", "writing-mode"],
|
|
when: () => this.isFirstLine,
|
|
fixId: "learn-more",
|
|
msgId: "inactive-css-first-line-pseudo-element-not-supported",
|
|
learnMoreURL: FIRST_LINE_PSEUDO_ELEMENT_STYLING_SPEC_URL,
|
|
},
|
|
// Content modifying properties used on ::first-letter pseudo-element.
|
|
{
|
|
invalidProperties: ["content"],
|
|
when: () => this.isFirstLetter,
|
|
fixId: "learn-more",
|
|
msgId: "inactive-css-first-letter-pseudo-element-not-supported",
|
|
learnMoreURL: FIRST_LETTER_PSEUDO_ELEMENT_STYLING_SPEC_URL,
|
|
},
|
|
// Writing mode or inline properties used on ::placeholder pseudo-element.
|
|
{
|
|
invalidProperties: [
|
|
"baseline-source",
|
|
"direction",
|
|
"dominant-baseline",
|
|
"line-height",
|
|
"text-orientation",
|
|
"vertical-align",
|
|
"writing-mode",
|
|
// Below are properties not yet implemented in Firefox (Bug 1312611)
|
|
"alignment-baseline",
|
|
"baseline-shift",
|
|
"initial-letter",
|
|
"text-box-trim",
|
|
],
|
|
when: () => {
|
|
const { selectorText } = this.cssRule;
|
|
return selectorText && selectorText.includes("::placeholder");
|
|
},
|
|
fixId: "learn-more",
|
|
msgId: "inactive-css-placeholder-pseudo-element-not-supported",
|
|
learnMoreURL: PLACEHOLDER_PSEUDO_ELEMENT_STYLING_SPEC_URL,
|
|
},
|
|
// (max-|min-)width used on inline elements, table rows, or row groups.
|
|
{
|
|
invalidProperties: ["max-width", "min-width", "width"],
|
|
when: () =>
|
|
this.nonReplacedInlineBox ||
|
|
this.horizontalTableTrack ||
|
|
this.horizontalTableTrackGroup,
|
|
fixId: "inactive-css-non-replaced-inline-or-table-row-or-row-group-fix",
|
|
msgId: "inactive-css-property-because-of-display",
|
|
},
|
|
// (max-|min-)height used on inline elements, table columns, or column groups.
|
|
{
|
|
invalidProperties: ["max-height", "min-height", "height"],
|
|
when: () =>
|
|
this.nonReplacedInlineBox ||
|
|
this.verticalTableTrack ||
|
|
this.verticalTableTrackGroup,
|
|
fixId:
|
|
"inactive-css-non-replaced-inline-or-table-column-or-column-group-fix",
|
|
msgId: "inactive-css-property-because-of-display",
|
|
},
|
|
{
|
|
invalidProperties: ["display"],
|
|
when: () =>
|
|
this.isFloated &&
|
|
this.checkResolvedStyle("display", [
|
|
"inline",
|
|
"inline-block",
|
|
"inline-table",
|
|
"inline-flex",
|
|
"inline-grid",
|
|
"table-cell",
|
|
"table-row",
|
|
"table-row-group",
|
|
"table-header-group",
|
|
"table-footer-group",
|
|
"table-column",
|
|
"table-column-group",
|
|
"table-caption",
|
|
]),
|
|
fixId: "inactive-css-not-display-block-on-floated-fix",
|
|
msgId: "inactive-css-not-display-block-on-floated-2",
|
|
},
|
|
// float property used on non-floating elements.
|
|
{
|
|
invalidProperties: ["float"],
|
|
when: () => this.gridItem || this.flexItem,
|
|
fixId: "inactive-css-only-non-grid-or-flex-item-fix",
|
|
msgId: "inactive-css-only-non-grid-or-flex-item",
|
|
},
|
|
// clear property used on non-floating elements.
|
|
{
|
|
invalidProperties: ["clear"],
|
|
when: () => !this.isBlockLevel(),
|
|
fixId: "inactive-css-not-block-fix",
|
|
msgId: "inactive-css-not-block",
|
|
},
|
|
// shape-image-threshold, shape-margin, shape-outside properties used on non-floated elements.
|
|
{
|
|
invalidProperties: [
|
|
"shape-image-threshold",
|
|
"shape-margin",
|
|
"shape-outside",
|
|
],
|
|
when: () => !this.isFloated,
|
|
fixId: "inactive-css-not-floated-fix",
|
|
msgId: "inactive-css-not-floated",
|
|
},
|
|
// The property is impossible to override due to :visited restriction.
|
|
{
|
|
invalidProperties: VISITED_INVALID_PROPERTIES,
|
|
when: () => this.isVisitedRule(),
|
|
fixId: "learn-more",
|
|
msgId: "inactive-css-property-is-impossible-to-override-in-visited",
|
|
learnMoreURL: VISITED_MDN_LINK,
|
|
},
|
|
// top, right, bottom, left properties used on non positioned boxes.
|
|
{
|
|
invalidProperties: ["top", "right", "bottom", "left"],
|
|
when: () => !this.isPositioned,
|
|
fixId: "inactive-css-position-property-on-unpositioned-box-fix",
|
|
msgId: "inactive-css-position-property-on-unpositioned-box",
|
|
},
|
|
// z-index property used on non positioned boxes that are not grid/flex items.
|
|
{
|
|
invalidProperties: ["z-index"],
|
|
when: () => !this.isPositioned && !this.gridItem && !this.flexItem,
|
|
fixId: "inactive-css-position-property-on-unpositioned-box-fix",
|
|
msgId: "inactive-css-position-property-on-unpositioned-box",
|
|
},
|
|
// object-fit or object-position property used on non-replaced elements.
|
|
{
|
|
invalidProperties: ["object-fit", "object-position"],
|
|
when: () => !this.replaced,
|
|
fixId: "inactive-css-only-replaced-elements-fix",
|
|
msgId: "inactive-css-only-replaced-elements",
|
|
},
|
|
// text-overflow property used on elements for which 'overflow' is set to 'visible'
|
|
// (the initial value) in the inline axis. Note that this validator only checks if
|
|
// 'overflow-inline' computes to 'visible' on the element.
|
|
// In theory, we should also be checking if the element is a block as this doesn't
|
|
// normally work on inline element. However there are many edge cases that made it
|
|
// impossible for the JS code to determine whether the type of box would support
|
|
// text-overflow. So, rather than risking to show invalid warnings, we decided to
|
|
// only warn when 'overflow-inline: visible' was set. There is more information
|
|
// about this in this discussion https://phabricator.services.mozilla.com/D62407 and
|
|
// on the bug https://bugzilla.mozilla.org/show_bug.cgi?id=1551578
|
|
{
|
|
invalidProperties: ["text-overflow"],
|
|
when: () => this.checkComputedStyle("overflow-inline", ["visible"]),
|
|
fixId: "inactive-text-overflow-when-no-overflow-fix",
|
|
msgId: "inactive-text-overflow-when-no-overflow",
|
|
},
|
|
// content-visibility used on elements for which size containment doesn't apply.
|
|
{
|
|
invalidProperties: ["content-visibility"],
|
|
when: () =>
|
|
!this.hasPrincipalBox ||
|
|
this.table ||
|
|
this.internalTableElement ||
|
|
this.rubyContainer ||
|
|
this.internalRubyElement ||
|
|
this.nonAtomicInlineBox,
|
|
fixId: "inactive-css-no-size-containment-fix",
|
|
msgId: "inactive-css-no-size-containment",
|
|
},
|
|
// margin properties used on table internal elements.
|
|
{
|
|
invalidProperties: [
|
|
"margin",
|
|
"margin-block",
|
|
"margin-block-end",
|
|
"margin-block-start",
|
|
"margin-bottom",
|
|
"margin-inline",
|
|
"margin-inline-end",
|
|
"margin-inline-start",
|
|
"margin-left",
|
|
"margin-right",
|
|
"margin-top",
|
|
],
|
|
when: () => this.internalTableElement,
|
|
fixId: "inactive-css-not-for-internal-table-elements-fix",
|
|
msgId: "inactive-css-not-for-internal-table-elements",
|
|
},
|
|
// padding properties used on table internal elements except table cells.
|
|
{
|
|
invalidProperties: [
|
|
"padding",
|
|
"padding-block",
|
|
"padding-block-end",
|
|
"padding-block-start",
|
|
"padding-bottom",
|
|
"padding-inline",
|
|
"padding-inline-end",
|
|
"padding-inline-start",
|
|
"padding-left",
|
|
"padding-right",
|
|
"padding-top",
|
|
],
|
|
when: () =>
|
|
this.internalTableElement &&
|
|
!this.checkComputedStyle("display", ["table-cell"]),
|
|
fixId:
|
|
"inactive-css-not-for-internal-table-elements-except-table-cells-fix",
|
|
msgId:
|
|
"inactive-css-not-for-internal-table-elements-except-table-cells",
|
|
},
|
|
// table-related properties used on non-table elements.
|
|
{
|
|
invalidProperties: [
|
|
"border-collapse",
|
|
"border-spacing",
|
|
"table-layout",
|
|
],
|
|
when: () =>
|
|
!this.checkComputedStyle("display", ["table", "inline-table"]),
|
|
fixId: "inactive-css-not-table-fix",
|
|
msgId: "inactive-css-not-table",
|
|
},
|
|
// border-spacing property used on collapsed table borders.
|
|
{
|
|
invalidProperties: ["border-spacing"],
|
|
when: () => this.checkComputedStyle("border-collapse", ["collapse"]),
|
|
fixId: "inactive-css-collapsed-table-borders-fix",
|
|
msgId: "inactive-css-collapsed-table-borders",
|
|
},
|
|
// empty-cells property used on non-table-cell elements.
|
|
{
|
|
invalidProperties: ["empty-cells"],
|
|
when: () => !this.checkComputedStyle("display", ["table-cell"]),
|
|
fixId: "inactive-css-not-table-cell-fix",
|
|
msgId: "inactive-css-not-table-cell",
|
|
},
|
|
// scroll-padding-* properties used on non-scrollable elements.
|
|
{
|
|
invalidProperties: [
|
|
"scroll-padding",
|
|
"scroll-padding-top",
|
|
"scroll-padding-right",
|
|
"scroll-padding-bottom",
|
|
"scroll-padding-left",
|
|
"scroll-padding-block",
|
|
"scroll-padding-block-end",
|
|
"scroll-padding-block-start",
|
|
"scroll-padding-inline",
|
|
"scroll-padding-inline-end",
|
|
"scroll-padding-inline-start",
|
|
],
|
|
when: () => !this.isScrollContainer,
|
|
fixId: "inactive-scroll-padding-when-not-scroll-container-fix",
|
|
msgId: "inactive-scroll-padding-when-not-scroll-container",
|
|
},
|
|
// border-image properties used on internal table with border collapse.
|
|
{
|
|
invalidProperties: [
|
|
"border-image",
|
|
"border-image-outset",
|
|
"border-image-repeat",
|
|
"border-image-slice",
|
|
"border-image-source",
|
|
"border-image-width",
|
|
],
|
|
when: () =>
|
|
this.internalTableElement &&
|
|
this.checkTableParentHasBorderCollapsed(),
|
|
fixId: "inactive-css-border-image-fix",
|
|
msgId: "inactive-css-border-image",
|
|
},
|
|
// width & height properties used on ruby elements.
|
|
{
|
|
invalidProperties: [
|
|
"height",
|
|
"min-height",
|
|
"max-height",
|
|
"width",
|
|
"min-width",
|
|
"max-width",
|
|
],
|
|
when: () => this.checkComputedStyle("display", ["ruby", "ruby-text"]),
|
|
fixId: "inactive-css-ruby-element-fix",
|
|
msgId: "inactive-css-ruby-element",
|
|
},
|
|
// resize property used on non-overflowing elements or replaced elements other than textarea.
|
|
{
|
|
invalidProperties: ["resize"],
|
|
when: () => !this.isScrollContainer && !this.isResizableReplacedElement,
|
|
fixId: "inactive-css-resize-fix",
|
|
msgId: "inactive-css-resize",
|
|
},
|
|
// text-wrap: balance; used on elements exceeding the threshold line number
|
|
{
|
|
invalidProperties: ["text-wrap"],
|
|
when: () => {
|
|
if (!this.checkComputedStyle("text-wrap", ["balance"])) {
|
|
return false;
|
|
}
|
|
const blockLineCounts = InspectorUtils.getBlockLineCounts(this.node);
|
|
// We only check the number of lines within the first block
|
|
// because the text-wrap: balance; property only applies to
|
|
// the first block. And fragmented elements (with multiple
|
|
// blocks) are excluded from line balancing for the time being.
|
|
return (
|
|
blockLineCounts && blockLineCounts[0] > TEXT_WRAP_BALANCE_LIMIT
|
|
);
|
|
},
|
|
fixId: "inactive-css-text-wrap-balance-lines-exceeded-fix",
|
|
msgId: "inactive-css-text-wrap-balance-lines-exceeded",
|
|
lineCount: TEXT_WRAP_BALANCE_LIMIT,
|
|
},
|
|
// text-wrap: balance; used on fragmented elements
|
|
{
|
|
invalidProperties: ["text-wrap"],
|
|
when: () => {
|
|
if (!this.checkComputedStyle("text-wrap", ["balance"])) {
|
|
return false;
|
|
}
|
|
const blockLineCounts = InspectorUtils.getBlockLineCounts(this.node);
|
|
const isFragmented = blockLineCounts && blockLineCounts.length > 1;
|
|
return isFragmented;
|
|
},
|
|
fixId: "inactive-css-text-wrap-balance-fragmented-fix",
|
|
msgId: "inactive-css-text-wrap-balance-fragmented",
|
|
},
|
|
// box-sizing used on element ignoring width and height.
|
|
{
|
|
invalidProperties: ["box-sizing"],
|
|
when: () => this.nonReplacedInlineBox,
|
|
fixId: "learn-more",
|
|
msgId: "inactive-css-no-width-height",
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* A list of rules for when CSS properties have no effect,
|
|
* based on an allow list of properties.
|
|
* We're setting this as a different array than INVALID_PROPERTIES_VALIDATORS as we
|
|
* need to check every properties, which we don't do for invalid properties ( see check
|
|
* on this.invalidProperties).
|
|
*
|
|
* This file contains "rules" in the form of objects with the following
|
|
* properties:
|
|
* {
|
|
* acceptedProperties:
|
|
* Array of CSS property names that are the only one accepted if the rule matches.
|
|
* when:
|
|
* The rule itself, a JS function used to identify the conditions
|
|
* indicating whether a property is valid or not.
|
|
* fixId:
|
|
* A Fluent id containing a suggested solution to the problem that is
|
|
* causing a property to be inactive.
|
|
* msgId:
|
|
* A Fluent id containing an error message explaining why a property is
|
|
* inactive in this situation.
|
|
* }
|
|
*
|
|
* If you add a new rule, also add a test for it in:
|
|
* server/tests/chrome/test_inspector-inactive-property-helper.html
|
|
*
|
|
* The main export is `isPropertyUsed()`, which can be used to check if a
|
|
* property is used or not, and why.
|
|
*/
|
|
ACCEPTED_PROPERTIES_VALIDATORS = [
|
|
// Constrained set of properties on highlight pseudo-elements
|
|
{
|
|
acceptedProperties: new Set([
|
|
// At the moment, for shorthand we don't look into each properties it covers,
|
|
// and so, although `background` might hold inactive values (e.g. background-image)
|
|
// we don't want to mark it as inactive if it sets a background-color (e.g. background: red).
|
|
"background",
|
|
"background-color",
|
|
"color",
|
|
"text-decoration",
|
|
"text-decoration-color",
|
|
"text-decoration-line",
|
|
"text-decoration-style",
|
|
"text-decoration-thickness",
|
|
"text-shadow",
|
|
"text-underline-offset",
|
|
"text-underline-position",
|
|
"-webkit-text-fill-color",
|
|
"-webkit-text-stroke-color",
|
|
"-webkit-text-stroke-width",
|
|
"-webkit-text-stroke",
|
|
]),
|
|
when: () => {
|
|
const { selectorText } = this.cssRule;
|
|
return (
|
|
selectorText && REGEXP_HIGHLIGHT_PSEUDO_ELEMENTS.test(selectorText)
|
|
);
|
|
},
|
|
msgId: "inactive-css-highlight-pseudo-elements-not-supported",
|
|
fixId: "learn-more",
|
|
learnMoreURL: HIGHLIGHT_PSEUDO_ELEMENTS_STYLING_SPEC_URL,
|
|
},
|
|
// Constrained set of properties on ::cue pseudo-element
|
|
//
|
|
// Note that Gecko doesn't yet support the ::cue() pseudo-element
|
|
// taking a selector as argument. The properties accecpted by that
|
|
// partly differ from the ones accepted by the ::cue pseudo-element.
|
|
// See https://w3c.github.io/webvtt/#ref-for-selectordef-cue-selector⑧.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=865395 and its
|
|
// dependencies for the implementation status.
|
|
{
|
|
acceptedProperties: new Set([
|
|
"background",
|
|
"background-attachment",
|
|
// The WebVTT spec. currently only allows all properties covered by
|
|
// the `background` shorthand and `background-blend-mode` is not
|
|
// part of that, though Gecko does support it, anyway.
|
|
// Therefore, there's also an issue pending to add it (and others)
|
|
// to the spec. See https://github.com/w3c/webvtt/issues/518.
|
|
"background-blend-mode",
|
|
"background-clip",
|
|
"background-color",
|
|
"background-image",
|
|
"background-origin",
|
|
"background-position",
|
|
"background-position-x",
|
|
"background-position-y",
|
|
"background-repeat",
|
|
"background-size",
|
|
"color",
|
|
"font",
|
|
"font-family",
|
|
"font-size",
|
|
"font-stretch",
|
|
"font-style",
|
|
"font-variant",
|
|
"font-variant-alternates",
|
|
"font-variant-caps",
|
|
"font-variant-east-asian",
|
|
"font-variant-ligatures",
|
|
"font-variant-numeric",
|
|
"font-variant-position",
|
|
"font-weight",
|
|
"line-height",
|
|
"opacity",
|
|
"outline",
|
|
"outline-color",
|
|
"outline-offset",
|
|
"outline-style",
|
|
"outline-width",
|
|
"ruby-position",
|
|
"text-combine-upright",
|
|
"text-decoration",
|
|
"text-decoration-color",
|
|
"text-decoration-line",
|
|
"text-decoration-style",
|
|
"text-decoration-thickness",
|
|
"text-shadow",
|
|
"visibility",
|
|
"white-space",
|
|
]),
|
|
when: () => {
|
|
const { selectorText } = this.cssRule;
|
|
return selectorText && selectorText.includes("::cue");
|
|
},
|
|
msgId: "inactive-css-cue-pseudo-element-not-supported",
|
|
fixId: "learn-more",
|
|
learnMoreURL: CUE_PSEUDO_ELEMENT_STYLING_SPEC_URL,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Get a list of unique CSS property names for which there are checks
|
|
* for used/unused state.
|
|
*
|
|
* @return {Set}
|
|
* List of CSS properties
|
|
*/
|
|
get invalidProperties() {
|
|
if (!this._invalidProperties) {
|
|
const allProps = this.INVALID_PROPERTIES_VALIDATORS.map(
|
|
v => v.invalidProperties
|
|
).flat();
|
|
this._invalidProperties = new Set(allProps);
|
|
}
|
|
|
|
return this._invalidProperties;
|
|
}
|
|
|
|
/**
|
|
* Is this CSS property having any effect on this element?
|
|
*
|
|
* @param {DOMNode} el
|
|
* The DOM element.
|
|
* @param {Style} elStyle
|
|
* The computed style for this DOMNode.
|
|
* @param {DOMRule} cssRule
|
|
* The CSS rule the property is defined in.
|
|
* @param {String} property
|
|
* The CSS property name.
|
|
*
|
|
* @return {Object} object
|
|
* @return {String} object.display
|
|
* The element computed display value.
|
|
* @return {String} object.fixId
|
|
* A Fluent id containing a suggested solution to the problem that is
|
|
* causing a property to be inactive.
|
|
* @return {String} object.msgId
|
|
* A Fluent id containing an error message explaining why a property
|
|
* is inactive in this situation.
|
|
* @return {String} object.property
|
|
* The inactive property name.
|
|
* @return {String} object.learnMoreURL
|
|
* An optional link if we need to open an other link than
|
|
* the default MDN property one.
|
|
* @return {Boolean} object.used
|
|
* true if the property is used.
|
|
*/
|
|
isPropertyUsed(el, elStyle, cssRule, property) {
|
|
// Assume the property is used when the Inactive CSS pref is not enabled
|
|
if (!INACTIVE_CSS_ENABLED) {
|
|
return { used: true };
|
|
}
|
|
|
|
let fixId = "";
|
|
let msgId = "";
|
|
let learnMoreURL = null;
|
|
let lineCount = null;
|
|
let used = true;
|
|
|
|
const someFn = validator => {
|
|
// First check if this rule cares about this property.
|
|
let isRuleConcerned = false;
|
|
|
|
if (validator.invalidProperties) {
|
|
isRuleConcerned = validator.invalidProperties.includes(property);
|
|
} else if (validator.acceptedProperties) {
|
|
isRuleConcerned =
|
|
!validator.acceptedProperties.has(property) &&
|
|
// custom properties can always be set
|
|
!property.startsWith("--");
|
|
}
|
|
|
|
if (!isRuleConcerned) {
|
|
return false;
|
|
}
|
|
|
|
this.select(el, elStyle, cssRule, property);
|
|
|
|
// And then run the validator, gathering the error message if the
|
|
// validator passes.
|
|
if (validator.when()) {
|
|
fixId = validator.fixId;
|
|
msgId = validator.msgId;
|
|
learnMoreURL = validator.learnMoreURL;
|
|
lineCount = validator.lineCount;
|
|
used = false;
|
|
|
|
// We can bail out as soon as a validator reported an issue.
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// First run the accepted properties validators
|
|
const isNotAccepted = this.ACCEPTED_PROPERTIES_VALIDATORS.some(someFn);
|
|
|
|
// If the property is not in the list of properties to check and there was no issues
|
|
// in the accepted properties validators, assume the property is used.
|
|
if (!isNotAccepted && !this.invalidProperties.has(property)) {
|
|
this.unselect();
|
|
return { used: true };
|
|
}
|
|
|
|
// Otherwise, if there was no issue from the accepted properties validators,
|
|
// run the invalid properties validators.
|
|
if (!isNotAccepted) {
|
|
this.INVALID_PROPERTIES_VALIDATORS.some(someFn);
|
|
}
|
|
|
|
this.unselect();
|
|
|
|
// Accessing elStyle might throws, we wrap it in a try/catch block to avoid test
|
|
// failures.
|
|
let display;
|
|
try {
|
|
display = elStyle ? elStyle.display : null;
|
|
} catch (e) {}
|
|
|
|
return {
|
|
display,
|
|
fixId,
|
|
msgId,
|
|
property,
|
|
learnMoreURL,
|
|
lineCount,
|
|
used,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Focus on a node.
|
|
*
|
|
* @param {DOMNode} node
|
|
* Node to focus on.
|
|
*/
|
|
select(node, style, cssRule, property) {
|
|
this._node = node;
|
|
this._cssRule = cssRule;
|
|
this._property = property;
|
|
this._style = style;
|
|
}
|
|
|
|
/**
|
|
* Clear references to avoid leaks.
|
|
*/
|
|
unselect() {
|
|
this._node = null;
|
|
this._cssRule = null;
|
|
this._property = null;
|
|
this._style = null;
|
|
}
|
|
|
|
/**
|
|
* Provide a public reference to node.
|
|
*/
|
|
get node() {
|
|
return this._node;
|
|
}
|
|
|
|
/**
|
|
* Cache and provide node's computed style.
|
|
*/
|
|
get style() {
|
|
return this._style;
|
|
}
|
|
|
|
/**
|
|
* Provide a public reference to the css rule.
|
|
*/
|
|
get cssRule() {
|
|
return this._cssRule;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node's propName is set to one of the values passed in
|
|
* the values array.
|
|
*
|
|
* @param {String} propName
|
|
* Property name to check.
|
|
* @param {Array} values
|
|
* Values to compare against.
|
|
*/
|
|
checkComputedStyle(propName, values) {
|
|
if (!this.style) {
|
|
return false;
|
|
}
|
|
return values.some(value => this.style[propName] === value);
|
|
}
|
|
|
|
/**
|
|
* Check if a rule's propName is set to one of the values passed in the values
|
|
* array.
|
|
*
|
|
* @param {String} propName
|
|
* Property name to check.
|
|
* @param {Array} values
|
|
* Values to compare against.
|
|
*/
|
|
checkResolvedStyle(propName, values) {
|
|
if (!(this.cssRule && this.cssRule.style)) {
|
|
return false;
|
|
}
|
|
const { style } = this.cssRule;
|
|
|
|
return values.some(value => style[propName] === value);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is an block-level box.
|
|
*/
|
|
isBlockLevel() {
|
|
return this.checkComputedStyle("display", [
|
|
"block",
|
|
"flow-root",
|
|
"flex",
|
|
"grid",
|
|
"table",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is an inline-level box.
|
|
*/
|
|
isInlineLevel() {
|
|
return this.checkComputedStyle("display", [
|
|
"inline",
|
|
"inline-block",
|
|
"inline-table",
|
|
"inline-flex",
|
|
"inline-grid",
|
|
"table-cell",
|
|
"table-row",
|
|
"table-row-group",
|
|
"table-header-group",
|
|
"table-footer-group",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a flex container i.e. a node that has a style
|
|
* of `display:flex` or `display:inline-flex`.
|
|
*/
|
|
get flexContainer() {
|
|
return this.checkComputedStyle("display", ["flex", "inline-flex"]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a flex item.
|
|
*/
|
|
get flexItem() {
|
|
return this.isFlexItem(this.node);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a grid container i.e. a node that has a style
|
|
* of `display:grid` or `display:inline-grid`.
|
|
*/
|
|
get gridContainer() {
|
|
return this.checkComputedStyle("display", ["grid", "inline-grid"]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a grid item.
|
|
*/
|
|
get gridItem() {
|
|
return this.isGridItem(this.node);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a multi-column container, i.e. a node element whose
|
|
* `column-width` or `column-count` property is not `auto`.
|
|
*/
|
|
get multiColContainer() {
|
|
const autoColumnWidth = this.checkComputedStyle("column-width", ["auto"]);
|
|
const autoColumnCount = this.checkComputedStyle("column-count", ["auto"]);
|
|
|
|
return !autoColumnWidth || !autoColumnCount;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is in a multi-column container, i.e. a node element
|
|
* that has an ancestor with `column-width` or `column-count` property set to a value.
|
|
*/
|
|
get inMultiColContainer() {
|
|
return !!this.getParentMultiColElement(this.node);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a table.
|
|
*/
|
|
get table() {
|
|
return this.checkComputedStyle("display", ["table", "inline-table"]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a table row.
|
|
*/
|
|
get tableRow() {
|
|
return this.style && this.style.display === "table-row";
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a table column.
|
|
*/
|
|
get tableColumn() {
|
|
return this.style && this.style.display === "table-column";
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is an internal table element.
|
|
*/
|
|
get internalTableElement() {
|
|
return this.checkComputedStyle("display", [
|
|
"table-cell",
|
|
"table-row",
|
|
"table-row-group",
|
|
"table-header-group",
|
|
"table-footer-group",
|
|
"table-column",
|
|
"table-column-group",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a horizontal table track. That is: either a table row
|
|
* displayed in horizontal writing mode, or a table column displayed in vertical writing
|
|
* mode.
|
|
*/
|
|
get horizontalTableTrack() {
|
|
if (!this.tableRow && !this.tableColumn) {
|
|
return false;
|
|
}
|
|
|
|
const tableTrackParent = this.getTableTrackParent();
|
|
|
|
return this.hasVerticalWritingMode(tableTrackParent)
|
|
? this.tableColumn
|
|
: this.tableRow;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a vertical table track. That is: either a table row
|
|
* displayed in vertical writing mode, or a table column displayed in horizontal writing
|
|
* mode.
|
|
*/
|
|
get verticalTableTrack() {
|
|
if (!this.tableRow && !this.tableColumn) {
|
|
return false;
|
|
}
|
|
|
|
const tableTrackParent = this.getTableTrackParent();
|
|
|
|
return this.hasVerticalWritingMode(tableTrackParent)
|
|
? this.tableRow
|
|
: this.tableColumn;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a row group.
|
|
*/
|
|
get rowGroup() {
|
|
return this.isRowGroup(this.node);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a table column group.
|
|
*/
|
|
get columnGroup() {
|
|
return this.isColumnGroup(this.node);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a horizontal table track group. That is: either a table
|
|
* row group displayed in horizontal writing mode, or a table column group displayed in
|
|
* vertical writing mode.
|
|
*/
|
|
get horizontalTableTrackGroup() {
|
|
if (!this.rowGroup && !this.columnGroup) {
|
|
return false;
|
|
}
|
|
|
|
const tableTrackParent = this.getTableTrackParent(true);
|
|
const isVertical = this.hasVerticalWritingMode(tableTrackParent);
|
|
|
|
const isHorizontalRowGroup = this.rowGroup && !isVertical;
|
|
const isHorizontalColumnGroup = this.columnGroup && isVertical;
|
|
|
|
return isHorizontalRowGroup || isHorizontalColumnGroup;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a vertical table track group. That is: either a table row
|
|
* group displayed in vertical writing mode, or a table column group displayed in
|
|
* horizontal writing mode.
|
|
*/
|
|
get verticalTableTrackGroup() {
|
|
if (!this.rowGroup && !this.columnGroup) {
|
|
return false;
|
|
}
|
|
|
|
const tableTrackParent = this.getTableTrackParent(true);
|
|
const isVertical = this.hasVerticalWritingMode(tableTrackParent);
|
|
|
|
const isVerticalRowGroup = this.rowGroup && isVertical;
|
|
const isVerticalColumnGroup = this.columnGroup && !isVertical;
|
|
|
|
return isVerticalRowGroup || isVerticalColumnGroup;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a ruby container.
|
|
*/
|
|
get rubyContainer() {
|
|
return this.checkComputedStyle("display", ["ruby"]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is an internal ruby element.
|
|
*/
|
|
get internalRubyElement() {
|
|
return this.checkComputedStyle("display", [
|
|
"ruby-base",
|
|
"ruby-text",
|
|
"ruby-base-container",
|
|
"ruby-text-container",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Returns whether this element uses CSS layout.
|
|
*/
|
|
get hasCssLayout() {
|
|
return !this.isSvg && !this.isMathMl;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a non-replaced CSS inline box.
|
|
*/
|
|
get nonReplacedInlineBox() {
|
|
return (
|
|
this.hasCssLayout &&
|
|
this.nonReplaced &&
|
|
this.style &&
|
|
this.style.display === "inline"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a non-atomic CSS inline box.
|
|
*/
|
|
get nonAtomicInlineBox() {
|
|
return (
|
|
this.hasCssLayout &&
|
|
this.nonReplaced &&
|
|
this.style &&
|
|
this.checkComputedStyle("display", ["inline", "inline list-item"])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node generates a principal box.
|
|
*/
|
|
get hasPrincipalBox() {
|
|
return (
|
|
this.hasCssLayout &&
|
|
this.style &&
|
|
this.style.display !== "none" &&
|
|
this.style.display !== "contents"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the current selector refers to a ::first-letter pseudo-element
|
|
*/
|
|
get isFirstLetter() {
|
|
const { selectorText } = this.cssRule;
|
|
return selectorText && selectorText.includes("::first-letter");
|
|
}
|
|
|
|
/**
|
|
* Check if the current selector refers to a ::first-line pseudo-element
|
|
*/
|
|
get isFirstLine() {
|
|
const { selectorText } = this.cssRule;
|
|
return selectorText && selectorText.includes("::first-line");
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a non-replaced element. See `replaced()` for
|
|
* a description of what a replaced element is.
|
|
*/
|
|
get nonReplaced() {
|
|
return !this.replaced;
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is an absolutely-positioned element.
|
|
*/
|
|
get isAbsolutelyPositioned() {
|
|
return this.checkComputedStyle("position", ["absolute", "fixed"]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is positioned (i.e. its position property has a value other
|
|
* than static).
|
|
*/
|
|
get isPositioned() {
|
|
return this.checkComputedStyle("position", [
|
|
"relative",
|
|
"absolute",
|
|
"fixed",
|
|
"sticky",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is floated
|
|
*/
|
|
get isFloated() {
|
|
return this.style && this.style.cssFloat !== "none";
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is scrollable
|
|
*/
|
|
get isScrollContainer() {
|
|
// If `overflow` doesn't contain the values `visible` or `clip`, it is a scroll container.
|
|
// While `hidden` doesn't allow scrolling via a user interaction, the element can
|
|
// still be scrolled programmatically.
|
|
// See https://www.w3.org/TR/css-overflow-3/#overflow-properties.
|
|
const overflow = computedStyle(this.node).overflow;
|
|
// `overflow` is a shorthand for `overflow-x` and `overflow-y`
|
|
// (and with that also for `overflow-inline` and `overflow-block`),
|
|
// so may hold two values.
|
|
const overflowValues = overflow.split(" ");
|
|
return !(
|
|
overflowValues.includes("visible") || overflowValues.includes("clip")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a replaced element that can be resized.
|
|
*/
|
|
get isResizableReplacedElement() {
|
|
// There might be more replaced elements that can be resized in the future.
|
|
// (See bug 1280920 and its dependencies.)
|
|
return this.localName === "textarea";
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is a replaced element i.e. an element with
|
|
* content that will be replaced e.g. <img>, <audio>, <video> or <object>
|
|
* elements.
|
|
*/
|
|
get replaced() {
|
|
if (REPLACED_ELEMENTS_NAMES.has(this.localName)) {
|
|
return true;
|
|
}
|
|
|
|
// img tags are replaced elements only when the image has finished loading.
|
|
if (this.localName === "img" && this.node.complete) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the current node's localName.
|
|
*
|
|
* @returns {String}
|
|
*/
|
|
get localName() {
|
|
return this.node.localName;
|
|
}
|
|
|
|
/**
|
|
* Return whether the node is a MathML element.
|
|
*/
|
|
get isMathMl() {
|
|
return this.node.namespaceURI === "http://www.w3.org/1998/Math/MathML";
|
|
}
|
|
|
|
/**
|
|
* Return whether the node is an SVG element.
|
|
*/
|
|
get isSvg() {
|
|
return this.node.namespaceURI === "http://www.w3.org/2000/svg";
|
|
}
|
|
|
|
/**
|
|
* Check if the current node is an absolutely-positioned grid element.
|
|
* See: https://drafts.csswg.org/css-grid/#abspos-items
|
|
*
|
|
* @return {Boolean} whether or not the current node is absolutely-positioned by a
|
|
* grid container.
|
|
*/
|
|
isAbsPosGridElement() {
|
|
if (!this.isAbsolutelyPositioned) {
|
|
return false;
|
|
}
|
|
|
|
const containingBlock = this.getContainingBlock();
|
|
|
|
return containingBlock !== null && this.isGridContainer(containingBlock);
|
|
}
|
|
|
|
/**
|
|
* Check if a node is a flex item.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node to check.
|
|
*/
|
|
isFlexItem(node) {
|
|
return !!node.parentFlexElement;
|
|
}
|
|
|
|
/**
|
|
* Check if a node is a flex container.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node to check.
|
|
*/
|
|
isFlexContainer(node) {
|
|
return !!node.getAsFlexContainer();
|
|
}
|
|
|
|
/**
|
|
* Check if a node is a grid container.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node to check.
|
|
*/
|
|
isGridContainer(node) {
|
|
return node.hasGridFragments();
|
|
}
|
|
|
|
/**
|
|
* Check if a node is a grid item.
|
|
*/
|
|
isGridItem() {
|
|
return !!this.getParentGridElement(this.node);
|
|
}
|
|
|
|
isVisitedRule() {
|
|
if (!CssLogic.hasVisitedState(this.node)) {
|
|
return false;
|
|
}
|
|
|
|
const selectors = CssLogic.getSelectors(this.cssRule);
|
|
if (!selectors.some(s => s.endsWith(":visited"))) {
|
|
return false;
|
|
}
|
|
|
|
const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(
|
|
this.node
|
|
);
|
|
|
|
for (let i = 0; i < selectors.length; i++) {
|
|
if (
|
|
!selectors[i].endsWith(":visited") &&
|
|
this.cssRule.selectorMatchesElement(i, bindingElement, pseudo, true)
|
|
) {
|
|
// Match non :visited selector.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Return the current node's ancestor that generates its containing block.
|
|
*/
|
|
getContainingBlock() {
|
|
return this.node ? InspectorUtils.containingBlockOf(this.node) : null;
|
|
}
|
|
|
|
getParentGridElement(node) {
|
|
// The documentElement can't be a grid item, only a container, so bail out.
|
|
if (node.flattenedTreeParentNode === node.ownerDocument) {
|
|
return null;
|
|
}
|
|
|
|
if (node.nodeType === node.ELEMENT_NODE) {
|
|
const display = this.style ? this.style.display : null;
|
|
|
|
if (!display || display === "none" || display === "contents") {
|
|
// Doesn't generate a box, not a grid item.
|
|
return null;
|
|
}
|
|
if (this.isAbsolutelyPositioned) {
|
|
// Out of flow, not a grid item.
|
|
return null;
|
|
}
|
|
} else if (node.nodeType !== node.TEXT_NODE) {
|
|
return null;
|
|
}
|
|
|
|
for (
|
|
let p = node.flattenedTreeParentNode;
|
|
p;
|
|
p = p.flattenedTreeParentNode
|
|
) {
|
|
if (this.isGridContainer(p)) {
|
|
// It's a grid item!
|
|
return p;
|
|
}
|
|
|
|
const style = computedStyle(p, node.ownerGlobal);
|
|
const display = style.display;
|
|
|
|
if (display !== "contents") {
|
|
return null; // Not a grid item, for sure.
|
|
}
|
|
// display: contents, walk to the parent
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return the multi-column container for a node if it exists.
|
|
*
|
|
* @param {DOMNode} The node we want the container for
|
|
* @param {DOMNode|null} The container element, or null if there is none.
|
|
*/
|
|
getParentMultiColElement(node) {
|
|
// The documentElement can't be an element in a multi-column container,
|
|
// only a container, so bail out.
|
|
if (node.flattenedTreeParentNode === node.ownerDocument) {
|
|
return null;
|
|
}
|
|
|
|
// Ignore nodes that are not elements nor text nodes
|
|
if (
|
|
node.nodeType !== node.ELEMENT_NODE &&
|
|
node.nodeType !== node.TEXT_NODE
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (node.nodeType === node.ELEMENT_NODE) {
|
|
const display = this.style ? this.style.display : null;
|
|
|
|
if (!display || display === "none" || display === "contents") {
|
|
// Doesn't generate a box, not an element in a multi-column container.
|
|
return null;
|
|
}
|
|
if (this.isAbsolutelyPositioned) {
|
|
// Out of flow, not an element in a multi-column container.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Walk up the tree to find the nearest multi-column container.
|
|
// Loop over flattenedTreeParentNode instead of parentNode to reach the
|
|
// shadow host from the shadow DOM.
|
|
for (
|
|
let p = node.flattenedTreeParentNode;
|
|
p && p !== node.ownerDocument;
|
|
p = p.flattenedTreeParentNode
|
|
) {
|
|
const style = computedStyle(p, node.ownerGlobal);
|
|
if (style.columnWidth !== "auto" || style.columnCount !== "auto") {
|
|
// It's a multi-column container!
|
|
return p;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
isRowGroup(node) {
|
|
const style = node === this.node ? this.style : computedStyle(node);
|
|
|
|
return (
|
|
style &&
|
|
(style.display === "table-row-group" ||
|
|
style.display === "table-header-group" ||
|
|
style.display === "table-footer-group")
|
|
);
|
|
}
|
|
|
|
isColumnGroup(node) {
|
|
const style = node === this.node ? this.style : computedStyle(node);
|
|
|
|
return style && style.display === "table-column-group";
|
|
}
|
|
|
|
/**
|
|
* Check if the given node's writing mode is vertical
|
|
*/
|
|
hasVerticalWritingMode(node) {
|
|
// Only 'horizontal-tb' has a horizontal writing mode.
|
|
// See https://drafts.csswg.org/css-writing-modes-4/#propdef-writing-mode
|
|
return computedStyle(node).writingMode !== "horizontal-tb";
|
|
}
|
|
|
|
/**
|
|
* Assuming the current element is a table track (row or column) or table track group,
|
|
* get the parent table.
|
|
* This is either going to be the table element if there is one, or the parent element.
|
|
* If the current element is not a table track, this returns the current element.
|
|
*
|
|
* @param {Boolean} isGroup
|
|
* Whether the element is a table track group, instead of a table track.
|
|
* @return {DOMNode}
|
|
* The parent table, the parent element, or the element itself.
|
|
*/
|
|
getTableTrackParent(isGroup) {
|
|
let current = this.node.parentNode;
|
|
|
|
// Skip over unrendered elements.
|
|
while (computedStyle(current).display === "contents") {
|
|
current = current.parentNode;
|
|
}
|
|
|
|
// Skip over groups if the initial element wasn't already one.
|
|
if (!isGroup && (this.isRowGroup(current) || this.isColumnGroup(current))) {
|
|
current = current.parentNode;
|
|
}
|
|
|
|
// Once more over unrendered elements above the group.
|
|
while (computedStyle(current).display === "contents") {
|
|
current = current.parentNode;
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Get the parent table element of the current element.
|
|
*
|
|
* @return {DOMNode|null}
|
|
* The closest table element or null if there are none.
|
|
*/
|
|
getTableParent() {
|
|
let current = this.node.parentNode;
|
|
|
|
// Find the table parent
|
|
while (current && computedStyle(current).display !== "table") {
|
|
current = current.parentNode;
|
|
|
|
// If we reached the document element, stop.
|
|
if (current == this.node.ownerDocument.documentElement) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Assuming the current element is an internal table element,
|
|
* check wether its parent table element has `border-collapse` set to `collapse`.
|
|
*
|
|
* @returns {Boolean}
|
|
*/
|
|
checkTableParentHasBorderCollapsed() {
|
|
const parent = this.getTableParent();
|
|
if (!parent) {
|
|
return false;
|
|
}
|
|
return computedStyle(parent).borderCollapse === "collapse";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns all CSS property names except given properties.
|
|
*
|
|
* @param {Array} - propertiesToIgnore
|
|
* Array of property ignored.
|
|
* @return {Array}
|
|
* Array of all CSS property name except propertiesToIgnore.
|
|
*/
|
|
function allCssPropertiesExcept(propertiesToIgnore) {
|
|
const properties = new Set(
|
|
InspectorUtils.getCSSPropertyNames({ includeAliases: true })
|
|
);
|
|
|
|
for (const name of propertiesToIgnore) {
|
|
properties.delete(name);
|
|
}
|
|
|
|
return [...properties];
|
|
}
|
|
|
|
/**
|
|
* Helper for getting an element's computed styles.
|
|
*
|
|
* @param {DOMNode} node
|
|
* The node to get the styles for.
|
|
* @param {Window} window
|
|
* Optional window object. If omitted, will get the node's window.
|
|
* @return {Object}
|
|
*/
|
|
function computedStyle(node, window = node.ownerGlobal) {
|
|
return window.getComputedStyle(node);
|
|
}
|
|
|
|
const inactivePropertyHelper = new InactivePropertyHelper();
|
|
|
|
// The only public method from this module is `isPropertyUsed`.
|
|
exports.isPropertyUsed = inactivePropertyHelper.isPropertyUsed.bind(
|
|
inactivePropertyHelper
|
|
);
|