Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Impressum rules.js

  Interaktion und
PortierbarkeitJAVA
 

/* 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 flags = require("resource://devtools/shared/flags.js");
const { l10n } = require("resource://devtools/shared/inspector/css-logic.js");
const {
  style: { ELEMENT_STYLE },
} = require("resource://devtools/shared/constants.js");
const {
  PSEUDO_CLASSES,
  ELEMENT_SPECIFIC_PSEUDO_CLASSES,
} = require("resource://devtools/shared/css/constants.js");
const OutputParser = require("resource://devtools/client/shared/output-parser.js");
const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
const ElementStyle = require("resource://devtools/client/inspector/rules/models/element-style.js");
const RuleEditor = require("resource://devtools/client/inspector/rules/views/rule-editor.js");
const RegisteredPropertyEditor = require("resource://devtools/client/inspector/rules/views/registered-property-editor.js");
const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js");
const {
  createChild,
} = require("resource://devtools/client/inspector/shared/utils.js");
const { debounce } = require("resource://devtools/shared/debounce.js");
const EventEmitter = require("resource://devtools/shared/event-emitter.js");

loader.lazyRequireGetter(
  this,
  ["flashElementOn""flashElementOff"],
  "resource://devtools/client/inspector/markup/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "ClassListPreviewer",
  "resource://devtools/client/inspector/rules/views/class-list-previewer.js"
);
loader.lazyRequireGetter(
  this,
  ["getNodeInfo""getNodeCompatibilityInfo""getRuleFromNode"],
  "resource://devtools/client/inspector/rules/utils/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "StyleInspectorMenu",
  "resource://devtools/client/inspector/shared/style-inspector-menu.js"
);
loader.lazyRequireGetter(
  this,
  "AutocompletePopup",
  "resource://devtools/client/shared/autocomplete-popup.js"
);
loader.lazyRequireGetter(
  this,
  "KeyShortcuts",
  "resource://devtools/client/shared/key-shortcuts.js"
);
loader.lazyRequireGetter(
  this,
  "clipboardHelper",
  "resource://devtools/shared/platform/clipboard.js"
);
loader.lazyRequireGetter(
  this,
  "openContentLink",
  "resource://devtools/client/shared/link.js",
  true
);

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
});

const HTML_NS = "http://www.w3.org/1999/xhtml";
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
const PREF_DRAGGABLE = "devtools.inspector.draggable_properties";
const PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER =
  "devtools.inspector.rule-view.focusNextOnEnter";
const PREF_CSS_EXPLAINERS = "devtools.inspector.css-explainers";
const FILTER_CHANGED_TIMEOUT = 150;
// Removes the flash-out class from an element after 1 second (100ms in tests so they
// don't take too long to run).
const PROPERTY_FLASHING_DURATION = flags.testing ? 100 : 1000;

// This is used to parse user input when filtering.
const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
// This is used to parse the filter search value to see if the filter
// should be strict or not
const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;

const RULE_VIEW_HEADER_CLASSNAME = "ruleview-header";

// List of all container IDs, order by typical order of display
const PSEUDO_ELEMENTS_CONTAINER_ID = "pseudo-elements-container";
const ELEMENT_CONTAINER_ID = "element-container";
const REGISTERED_PROPERTIES_CONTAINER_ID = "registered-properties-container";
const POSITION_TRY_CONTAINER_ID = "position-try-container";

/**
 * Our model looks like this:
 *
 * ElementStyle:
 *   Responsible for keeping track of which properties are overridden.
 *   Maintains a list of Rule objects that apply to the element.
 * Rule:
 *   Manages a single style declaration or rule.
 *   Responsible for applying changes to the properties in a rule.
 *   Maintains a list of TextProperty objects.
 * TextProperty:
 *   Manages a single property from the authoredText attribute of the
 *     relevant declaration.
 *   Maintains a list of computed properties that come from this
 *     property declaration.
 *   Changes to the TextProperty are sent to its related Rule for
 *     application.
 *
 * View hierarchy mostly follows the model hierarchy.
 *
 * CssRuleView:
 *   Owns an ElementStyle and creates a list of RuleEditors for its
 *    Rules.
 * RuleEditor:
 *   Owns a Rule object and creates a list of TextPropertyEditors
 *     for its TextProperties.
 *   Manages creation of new text properties.
 * TextPropertyEditor:
 *   Owns a TextProperty object.
 *   Manages changes to the TextProperty.
 *   Can be expanded to display computed properties.
 *   Can mark a property disabled or enabled.
 */


/**
 * CssRuleView is a view of the style rules and declarations that
 * apply to a given element.  After construction, the 'element'
 * property will be available with the user interface.
 */

class CssRuleView extends EventEmitter {
  /**
   * @param {Inspector} inspector
   *        Inspector toolbox panel
   * @param {Document} document
   *        The document that will contain the rule view.
   * @param {object} store
   *        The CSS rule view can use this object to store metadata
   *        that might outlast the rule view, particularly the current
   *        set of disabled properties.
   */

  constructor(inspector, document, store) {
    super();

    this.inspector = inspector;
    this.cssProperties = inspector.cssProperties;
    this.styleDocument = document;
    this.styleWindow = this.styleDocument.defaultView;
    this.store = store || {
      expandedUnusedCustomCssPropertiesRuleActorIds: new Set(),
    };

    // Allow tests to override debouncing behavior, as this can cause intermittents.
    this.debounce = debounce;

    // Used by TextPropertyEditor
    this.outputParser = new OutputParser(document, this.cssProperties);

    this.addNewRule = this.addNewRule.bind(this);
    this.refreshPanel = this.refreshPanel.bind(this);

    this.#abortController = new this.styleWindow.AbortController();
    const { signal } = this.#abortController;
    const baseEventConfig = { signal };

    const doc = this.styleDocument;
    // Delegate bulk handling of events happening within the DOM tree of the Rules view
    // to this.handleEvent(). Listening on the capture phase of the event bubbling to be
    // able to stop event propagation on a case-by-case basis and prevent event target
    // ancestor nodes from handling them.
    this.styleDocument.addEventListener("click"this, {
      capture: true,
      signal,
    });
    this.element = doc.getElementById("ruleview-container-focusable");
    this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
    this.searchField = doc.getElementById("ruleview-searchbox");
    this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
    this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
    this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
    this.pseudoClassesStandardPanel = doc.getElementById(
      "pseudo-classes-standard"
    );
    this.pseudoClassesElementSpecificPanel = doc.getElementById(
      "pseudo-classes-element-specific"
    );
    this.pseudoClassesElementSpecificHeading = doc.getElementById(
      "pseudo-classes-element-specific-heading"
    );
    this.classPanel = doc.getElementById("ruleview-class-panel");
    this.classToggle = doc.getElementById("class-panel-toggle");
    this.colorSchemeLightSimulationButton = doc.getElementById(
      "color-scheme-simulation-light-toggle"
    );
    this.colorSchemeDarkSimulationButton = doc.getElementById(
      "color-scheme-simulation-dark-toggle"
    );
    this.printSimulationButton = doc.getElementById("print-simulation-toggle");

    this.#initSimulationFeatures();

    this.searchClearButton.hidden = true;

    this.onHighlighterShown = data =>
      this.handleHighlighterEvent("highlighter-shown", data);
    this.onHighlighterHidden = data =>
      this.handleHighlighterEvent("highlighter-hidden", data);
    this.inspector.highlighters.on(
      "highlighter-shown",
      this.onHighlighterShown
    );
    this.inspector.highlighters.on(
      "highlighter-hidden",
      this.onHighlighterHidden
    );

    this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
    this.shortcuts.on("Escape", event => this.#onShortcut("Escape", event));
    this.shortcuts.on("Return", event => this.#onShortcut("Return", event));
    this.shortcuts.on("Space", event => this.#onShortcut("Space", event));
    this.shortcuts.on("CmdOrCtrl+F", event =>
      this.#onShortcut("CmdOrCtrl+F", event)
    );
    this.element.addEventListener("copy"this.#onCopy, baseEventConfig);
    this.element.addEventListener(
      "contextmenu",
      this.#onContextMenu,
      baseEventConfig
    );
    this.addRuleButton.addEventListener(
      "click",
      this.addNewRule,
      baseEventConfig
    );
    this.searchField.addEventListener(
      "input",
      this.#onFilterStyles,
      baseEventConfig
    );
    this.searchClearButton.addEventListener(
      "click",
      this.#onClearSearch,
      baseEventConfig
    );
    this.pseudoClassToggle.addEventListener(
      "click",
      this.#onTogglePseudoClassPanel,
      baseEventConfig
    );
    this.classToggle.addEventListener(
      "click",
      this.#onToggleClassPanel,
      baseEventConfig
    );
    // The "change" event bubbles up from checkbox inputs nested within the panel container.
    this.pseudoClassPanel.addEventListener(
      "change",
      this.#onTogglePseudoClass,
      baseEventConfig
    );

    if (flags.testing) {
      // In tests, we start listening immediately to avoid having to simulate a mousemove.
      this.highlighters.addToView(this);
    } else {
      this.element.addEventListener(
        "mousemove",
        () => {
          this.highlighters.addToView(this);
        },
        { once: true, signal }
      );
    }

    this.#prefObserver = new PrefObserver("devtools.");
    this.#prefObserver.on(PREF_UA_STYLES, this.#handleUAStylePrefChange);
    this.#prefObserver.on(
      PREF_DEFAULT_COLOR_UNIT,
      this.#handleDefaultColorUnitPrefChange
    );
    this.#prefObserver.on(PREF_DRAGGABLE, this.#handleDraggablePrefChange);
    // Initialize value of this.draggablePropertiesEnabled
    this.#handleDraggablePrefChange();

    this.#prefObserver.on(
      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
      this.#handleInplaceEditorFocusNextOnEnterPrefChange
    );
    // Initialize value of this.inplaceEditorFocusNextOnEnter
    this.#handleInplaceEditorFocusNextOnEnterPrefChange();

    // Used from tests
    this.pseudoClassCheckboxes = this.#createCheckboxes(
      PSEUDO_CLASSES,
      this.pseudoClassesStandardPanel
    );
    this.elementSpecificPseudoClassCheckboxes = this.#createCheckboxes(
      Object.keys(ELEMENT_SPECIFIC_PSEUDO_CLASSES),
      this.pseudoClassesElementSpecificPanel
    );
    this.#showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
    this.cssExplainersEnabled = Services.prefs.getBoolPref(
      PREF_CSS_EXPLAINERS,
      false
    );

    // Add the tooltips and highlighters to the view
    this.tooltips = new TooltipsOverlay(this);

    // Used from RuleViewTool class
    this.cssRegisteredPropertiesByTarget = new Map();

    this.#elementsWithPendingClicks = new this.styleWindow.WeakSet();
  }

  #abortController;

  #prefObserver;
  #elementsWithPendingClicks;
  #showUserAgentStyles;
  #focusNextUserAddedRule;

  // References to all active rule containers DOM Elements.
  // Containers can be: "pseudo element", "inherited by", "keyframes",...
  // Map(String => Object { header: DOM Element, container: DOM Element} )
  // Map(Container ID => Header and container DOM Elements)
  #containers = new Map();

  // Variable used to stop the propagation of mouse events to children
  // when we are updating a value by dragging the mouse and we then release it
  #childHasDragged = false;

  // The element that we're inspecting.
  // (Used from RuleViewTool class)
  selectedNodeFront = null;

  // Used for cancelling timeouts in the style filter.
  #filterChangedTimeout = null;

  // Empty, unconnected element of the same type as this selected node,
  // used to figure out how shorthand properties will be parsed.
  #dummyElement = null;

  #popup;

  get popup() {
    if (!this.#popup) {
      // The popup will be attached to the toolbox document.
      this.#popup = new AutocompletePopup(this.inspector.toolbox.doc, {
        autoSelect: true,
      });
    }

    return this.#popup;
  }

  #classListPreviewer;
  get classListPreviewer() {
    if (!this.#classListPreviewer) {
      this.#classListPreviewer = new ClassListPreviewer(
        this.inspector,
        this.classPanel
      );
    }

    return this.#classListPreviewer;
  }

  #contextMenu;
  get contextMenu() {
    if (!this.#contextMenu) {
      this.#contextMenu = new StyleInspectorMenu(this, { isRuleView: true });
    }

    return this.#contextMenu;
  }

  // Get the dummy element.
  get dummyElement() {
    return this.#dummyElement;
  }

  #refreshDummyElement() {
    // Only update the dummy element if the selected element's tag is different.
    if (
      !this.selectedNodeFront ||
      this.#dummyElement?.tagName === this.selectedNodeFront.tagName
    ) {
      return;
    }

    // To figure out how shorthand properties are interpreted by the
    // engine, we will set properties on a dummy element and observe
    // how their .style attribute reflects them as computed values.
    try {
      // ::before and ::after do not have a namespaceURI
      const namespaceURI =
        this.selectedNodeFront.namespaceURI ||
        this.styleDocument.documentElement.namespaceURI;
      this.#dummyElement = this.styleDocument.createElementNS(
        namespaceURI,
        this.selectedNodeFront.tagName
      );
    } catch (e) {
      console.error("Error while creating dummy element", e);
    }
  }

  // Get the highlighters overlay from the Inspector.
  #highlighters;
  get highlighters() {
    if (!this.#highlighters) {
      // highlighters is a lazy getter in the inspector.
      this.#highlighters = this.inspector.highlighters;
    }

    return this.#highlighters;
  }

  // Get the filter search value.
  get searchValue() {
    return this.searchField.value.toLowerCase();
  }

  get rules() {
    return this.elementStyle ? this.elementStyle.rules : [];
  }

  get currentTarget() {
    return this.inspector.toolbox.target;
  }

  /**
   * Highlight/unhighlight all the nodes that match a given rule's selector
   * inside the document of the current selected node.
   * Only one selector can be highlighted at a time, so calling the method a
   * second time with a different rule will first unhighlight the previously
   * highlighted nodes.
   * Calling the method a second time with the same rule will just
   * unhighlight the highlighted nodes.
   *
   * @param {Rule} rule
   * @param {string} selector
   *        Elements matching this selector will be highlighted on the page.
   * @param {boolean} highlightFromRulesSelector
   */

  async toggleSelectorHighlighter(
    rule,
    selector,
    highlightFromRulesSelector = true
  ) {
    if (this.isSelectorHighlighted(selector)) {
      await this.inspector.highlighters.hideHighlighterType(
        this.inspector.highlighters.TYPES.SELECTOR
      );
    } else {
      const options = {
        hideInfoBar: true,
        hideGuides: true,
        // we still pass the selector (which can be the StyleRuleFront#computedSelector)
        // even if highlightFromRulesSelector is set to true, as it's how we keep track
        // of which selector is highlighted.
        selector,
      };
      if (highlightFromRulesSelector) {
        options.ruleActorID = rule.domRule.actorID;
      }
      await this.inspector.highlighters.showHighlighterTypeForNode(
        this.inspector.highlighters.TYPES.SELECTOR,
        this.inspector.selection.nodeFront,
        options
      );
    }
  }

  isPanelVisible() {
    return (
      this.inspector.toolbox &&
      this.inspector.sidebar &&
      this.inspector.toolbox.currentToolId === "inspector" &&
      (this.inspector.sidebar.getCurrentTabID() == "ruleview" ||
        this.inspector.isThreePaneModeEnabled)
    );
  }

  /**
   * Check whether a SelectorHighlighter is active for the given selector text.
   *
   * @param {string} selector
   * @return {boolean}
   */

  isSelectorHighlighted(selector) {
    const options = this.inspector.highlighters.getOptionsForActiveHighlighter(
      this.inspector.highlighters.TYPES.SELECTOR
    );

    return options?.selector === selector;
  }

  /**
   * Delegate handler for events happening within the DOM tree of the Rules view.
   * Itself delegates to specific handlers by event type.
   *
   * Use this instead of attaching specific event handlers when:
   * - there are many elements with the same event handler (eases memory pressure)
   * - you want to avoid having to remove event handlers manually
   * - elements are added/removed from the DOM tree arbitrarily over time
   *
   * @param {MouseEvent|UIEvent} event
   */

  handleEvent(event) {
    if (this.#childHasDragged) {
      this.#childHasDragged = false;
      event.stopPropagation();
      return;
    }
    switch (event.type) {
      case "click":
        this.handleClickEvent(event);
        break;
      default:
    }
  }

  /**
   * Delegate handler for click events happening within the DOM tree of the Rules view.
   * Stop propagation of click event wrapping a CSS rule or CSS declaration to avoid
   * triggering the prompt to add a new CSS declaration or to edit the existing one.
   *
   * @param {MouseEvent} event
   */

  async handleClickEvent(event) {
    const target = event.target;

    // Handle click on the icon next to a CSS selector.
    if (target.classList.contains("js-toggle-selector-highlighter")) {
      event.stopPropagation();
      let selector = target.dataset.computedSelector;
      const highlightFromRulesSelector =
        !!selector && !target.dataset.isUniqueSelector;
      // dataset.computedSelector will be initially empty for inline styles (inherited or not)
      // Rules associated with a regular selector should have this data-attribute
      // set in devtools/client/inspector/rules/views/rule-editor.js
      const rule = getRuleFromNode(target, this.elementStyle);
      if (selector === "") {
        try {
          if (rule.inherited) {
            // This is an inline style from an inherited rule. Need to resolve the
            // unique selector from the node which this rule is inherited from.
            selector = await rule.inherited.getUniqueSelector();
          } else {
            // This is an inline style from the current node.
            selector =
              await this.inspector.selection.nodeFront.getUniqueSelector();
          }

          // Now that the selector was computed, we can store it for subsequent usage.
          target.dataset.computedSelector = selector;
          target.dataset.isUniqueSelector = true;
        } finally {
          // Could not resolve a unique selector for the inline style.
        }
      }

      this.toggleSelectorHighlighter(
        rule,
        selector,
        highlightFromRulesSelector
      );
    }

    // Handle click on swatches next to flex and inline-flex CSS properties
    if (target.classList.contains("js-toggle-flexbox-highlighter")) {
      event.stopPropagation();
      this.inspector.highlighters.toggleFlexboxHighlighter(
        this.inspector.selection.nodeFront,
        "rule"
      );
    }

    // Handle click on swatches next to grid CSS properties
    if (target.classList.contains("js-toggle-grid-highlighter")) {
      event.stopPropagation();
      this.inspector.highlighters.toggleGridHighlighter(
        this.inspector.selection.nodeFront,
        "rule"
      );
    }

    const valueSpan = target.closest(".ruleview-propertyvalue");
    if (valueSpan) {
      if (this.#elementsWithPendingClicks.has(valueSpan)) {
        // When we start handling a drag in the TextPropertyEditor valueSpan,
        // we make the valueSpan capture the pointer. Then, `click` event target is always
        // the valueSpan with the latest spec of Pointer Events.
        // Therefore, we should stop immediate propagation of the `click` event
        // if we've handled a drag to prevent moving focus to the inplace editor.
        event.stopImmediatePropagation();
        return;
      }

      // Handle link click in RuleEditor property value
      if (target.nodeName === "a") {
        event.stopPropagation();
        event.preventDefault();
        openContentLink(target.href, {
          relatedToCurrent: true,
          inBackground:
            event.button === 1 ||
            (lazy.AppConstants.platform === "macosx"
              ? event.metaKey
              : event.ctrlKey),
        });
      }
    }
  }

  /**
   * Delegate handler for highlighter events.
   *
   * This is the place to observe for highlighter events, check the highlighter type and
   * event name, then react to specific events, for example by modifying the DOM.
   *
   * @param {string} eventName
   *        Highlighter event name. One of: "highlighter-hidden", "highlighter-shown"
   * @param {object} data
   *        Object with data associated with the highlighter event.
   */

  handleHighlighterEvent(eventName, data) {
    switch (data.type) {
      // Toggle the "highlighted" class on selector icons in the Rules view when
      // the SelectorHighlighter is shown/hidden for a certain CSS selector.
      case this.inspector.highlighters.TYPES.SELECTOR:
        {
          const selector = data?.options?.selector;
          if (!selector) {
            return;
          }

          const query = `.js-toggle-selector-highlighter[data-computed-selector='${selector}']`;
          for (const node of this.styleDocument.querySelectorAll(query)) {
            const isHighlighterDisplayed = eventName == "highlighter-shown";
            node.classList.toggle("highlighted", isHighlighterDisplayed);
            node.setAttribute("aria-pressed", isHighlighterDisplayed);
          }
        }
        break;

      // Toggle the "aria-pressed" attribute on swatches next to flex and inline-flex CSS properties
      // when the FlexboxHighlighter is shown/hidden for the currently selected node.
      case this.inspector.highlighters.TYPES.FLEXBOX:
        {
          const query = ".js-toggle-flexbox-highlighter";
          for (const node of this.styleDocument.querySelectorAll(query)) {
            node.setAttribute("aria-pressed", eventName == "highlighter-shown");
          }
        }
        break;

      // Toggle the "aria-pressed" class on swatches next to grid CSS properties
      // when the GridHighlighter is shown/hidden for the currently selected node.
      case this.inspector.highlighters.TYPES.GRID:
        {
          const query = ".js-toggle-grid-highlighter";
          for (const node of this.styleDocument.querySelectorAll(query)) {
            // From the Layout panel, we can toggle grid highlighters for nodes which are
            // not currently selected. The Rules view shows `display: grid` declarations
            // only for the selected node. Avoid mistakenly marking them as "active".
            if (data.nodeFront === this.inspector.selection.nodeFront) {
              node.setAttribute(
                "aria-pressed",
                eventName == "highlighter-shown"
              );
            }

            // When the max limit of grid highlighters is reached (default 3),
            // mark inactive grid swatches as disabled.
            node.toggleAttribute(
              "disabled",
              !this.inspector.highlighters.canGridHighlighterToggle(
                this.inspector.selection.nodeFront
              )
            );
          }
        }
        break;
    }
  }

  /**
   * Enables the print and color scheme simulation only for local and remote tab debugging.
   */

  async #initSimulationFeatures() {
    if (!this.inspector.commands.descriptorFront.isTabDescriptor) {
      return;
    }
    this.colorSchemeLightSimulationButton.removeAttribute("hidden");
    this.colorSchemeDarkSimulationButton.removeAttribute("hidden");
    this.printSimulationButton.removeAttribute("hidden");
    const { signal } = this.#abortController;
    const baseEventConfig = { signal };
    this.printSimulationButton.addEventListener(
      "click",
      this.#onTogglePrintSimulation,
      baseEventConfig
    );
    this.colorSchemeLightSimulationButton.addEventListener(
      "click",
      this.#onToggleLightColorSchemeSimulation,
      baseEventConfig
    );
    this.colorSchemeDarkSimulationButton.addEventListener(
      "click",
      this.#onToggleDarkColorSchemeSimulation,
      baseEventConfig
    );
    const { rfpCSSColorScheme } = this.inspector.walker;
    if (rfpCSSColorScheme) {
      this.colorSchemeLightSimulationButton.setAttribute("disabled"true);
      this.colorSchemeDarkSimulationButton.setAttribute("disabled"true);
      console.warn("Color scheme simulation is disabled in RFP mode.");
    }
  }

  /**
   * Get the type of a given node in the rule-view
   *
   * @param {DOMNode} node
   *        The node which we want information about
   * @return {object | null} containing the following props:
   * - type {String} One of the VIEW_NODE_XXX_TYPE const in
   *   client/inspector/shared/node-types.
   * - rule {Rule} The Rule object.
   * - value {Object} Depends on the type of the node.
   * Otherwise, returns null if the node isn't anything we care about.
   */

  getNodeInfo(node) {
    return getNodeInfo(node, this.elementStyle);
  }

  /**
   * Get the node's compatibility issues
   *
   * @param {DOMNode} node
   *        The node which we want information about
   * @return {object | null} containing the following props:
   * - type {String} Compatibility issue type.
   * - property {string} The incompatible rule
   * - alias {Array} The browser specific alias of rule
   * - url {string} Link to MDN documentation
   * - deprecated {bool} True if the rule is deprecated
   * - experimental {bool} True if rule is experimental
   * - unsupportedBrowsers {Array} Array of unsupported browser
   * Otherwise, returns null if the node has cross-browser compatible CSS
   */

  async getNodeCompatibilityInfo(node) {
    const compatibilityInfo = await getNodeCompatibilityInfo(
      node,
      this.elementStyle
    );

    return compatibilityInfo;
  }

  /**
   * Context menu handler.
   */

  #onContextMenu = event => {
    const inInput = event.composedTarget.matches(
      "input:is([type=text], [type=search], :not([type])), textarea"
    );
    if (inInput) {
      return;
    }

    event.stopPropagation();
    event.preventDefault();

    this.contextMenu.show(event);
  };

  /**
   * Callback for copy event. Copy the selected text.
   *
   * @param {Event} event
   *        copy event object.
   */

  #onCopy = event => {
    if (event) {
      this.copySelection(event.target);
      event.preventDefault();
      event.stopPropagation();
    }
  };

  /**
   * Copy the current selection. The current target is necessary
   * if the selection is inside an input or a textarea
   *
   * @param {DOMNode} target
   *        DOMNode target of the copy action
   */

  copySelection(target) {
    try {
      let text = "";

      const nodeName = target?.nodeName;
      const targetType = target?.type;

      if (
        // The target can be the enable/disable rule checkbox here (See Bug 1680893).
        (nodeName === "input" && targetType !== "checkbox") ||
        nodeName == "textarea"
      ) {
        const start = Math.min(target.selectionStart, target.selectionEnd);
        const end = Math.max(target.selectionStart, target.selectionEnd);
        const count = end - start;
        text = target.value.substr(start, count);
      } else {
        text = this.styleWindow.getSelection().toString();

        // Remove any double newlines.
        text = text.replace(/(\r?\n)\r?\n/g, "$1");
      }

      clipboardHelper.copyString(text);
    } catch (e) {
      console.error(e);
    }
  }

  /**
   * Add a new rule to the current element.
   */

  addNewRule() {
    // Clear the search input so the new rule is visible
    this.#onClearSearch({ focusSearchField: false });

    this.#focusNextUserAddedRule = true;
    this.pageStyle.addNewRule(
      this.selectedNodeFront,
      this.selectedNodeFront.pseudoClassLocks
    );
  }

  /**
   * Returns true if the "Add Rule" action (either via the addRuleButton or the context
   * menu entry) can be performed for the currently selected node.
   *
   * @returns {boolean}
   */

  canAddNewRuleForSelectedNode() {
    return this.selectedNodeFront && this.inspector.selection.isElementNode();
  }

  /**
   * Return {Boolean} true if the rule view currently has an input
   * editor visible.
   */

  get isEditing() {
    return (
      this.tooltips.isEditing ||
      !!this.element.querySelectorAll(".styleinspector-propertyeditor").length
    );
  }

  #handleUAStylePrefChange = () => {
    this.#showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
    this.#handlePrefChange(PREF_UA_STYLES);
  };

  #handleDefaultColorUnitPrefChange = () => {
    this.#handlePrefChange(PREF_DEFAULT_COLOR_UNIT);
  };

  #handleDraggablePrefChange = () => {
    this.draggablePropertiesEnabled = Services.prefs.getBoolPref(
      PREF_DRAGGABLE,
      false
    );
    // This event is consumed by text-property-editor instances in order to
    // update their draggable behavior. Preferences observer are costly, so
    // we are forwarding the preference update via the EventEmitter.
    this.emit("draggable-preference-updated");
  };

  #handleInplaceEditorFocusNextOnEnterPrefChange = () => {
    this.inplaceEditorFocusNextOnEnter = Services.prefs.getBoolPref(
      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
      false
    );
    this.#handlePrefChange(PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER);
  };

  #handlePrefChange(pref) {
    // Reselect the currently selected element
    const refreshOnPrefs = [
      PREF_UA_STYLES,
      PREF_DEFAULT_COLOR_UNIT,
      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
    ];
    if (this.selectedNodeFront && refreshOnPrefs.includes(pref)) {
      this.selectElement(this.selectedNodeFront, true);
    }
  }

  /**
   * Set the filter style search value.
   *
   * @param {string} value
   *        The search value.
   * @param {object} options
   * @param {boolean} options.focusSearchField
   *        Whether or not to focus the search input. Defaults to true.
   */

  setFilterStyles(value = "", { focusSearchField = true } = {}) {
    this.searchField.value = value;
    if (focusSearchField) {
      this.searchField.focus();
    }
    this.#onFilterStyles();
  }

  /**
   * Called when the user enters a search term in the filter style search box.
   * The actual filtering (done in #doFilterStyles) will be throttled if the search input
   * isn't empty, but will happen immediately when the search gets cleared.
   */

  #onFilterStyles = () => {
    if (this.#filterChangedTimeout) {
      clearTimeout(this.#filterChangedTimeout);
    }

    const isSearchEmpty = this.searchValue.length === 0;
    this.searchClearButton.hidden = isSearchEmpty;

    // If the search is cleared update the UI directly so calls to this function (or any
    // callsite of it) can assume the UI is up to date directly after the call.
    if (isSearchEmpty) {
      this.#doFilterStyles();
    } else {
      this.#filterChangedTimeout = setTimeout(
        () => this.#doFilterStyles(),
        FILTER_CHANGED_TIMEOUT
      );
    }
  };

  /**
   * Actually update search data and update the UI to reflect the current search.
   *
   * @fires ruleview-filtered
   */

  #doFilterStyles() {
    this.searchData = {
      searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue),
      searchPropertyName: this.searchValue,
      searchPropertyValue: this.searchValue,
      strictSearchValue: "",
      strictSearchPropertyName: false,
      strictSearchPropertyValue: false,
      strictSearchAllValues: false,
    };

    if (this.searchData.searchPropertyMatch) {
      // Parse search value as a single property line and extract the
      // property name and value. If the parsed property name or value is
      // contained in backquotes (`), extract the value within the backquotes
      // and set the corresponding strict search for the property to true.
      if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) {
        this.searchData.strictSearchPropertyName = true;
        this.searchData.searchPropertyName = FILTER_STRICT_RE.exec(
          this.searchData.searchPropertyMatch[1]
        )[1];
      } else {
        this.searchData.searchPropertyName =
          this.searchData.searchPropertyMatch[1];
      }

      if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) {
        this.searchData.strictSearchPropertyValue = true;
        this.searchData.searchPropertyValue = FILTER_STRICT_RE.exec(
          this.searchData.searchPropertyMatch[2]
        )[1];
      } else {
        this.searchData.searchPropertyValue =
          this.searchData.searchPropertyMatch[2];
      }

      // Strict search for stylesheets will match the property line regex.
      // Extract the search value within the backquotes to be used
      // in the strict search for stylesheets in #highlightStyleSheet.
      if (FILTER_STRICT_RE.test(this.searchValue)) {
        this.searchData.strictSearchValue = FILTER_STRICT_RE.exec(
          this.searchValue
        )[1];
      }
    } else if (FILTER_STRICT_RE.test(this.searchValue)) {
      // If the search value does not correspond to a property line and
      // is contained in backquotes, extract the search value within the
      // backquotes and set the flag to perform a strict search for all
      // the values (selector, stylesheet, property and computed values).
      const searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1];
      this.searchData.strictSearchAllValues = true;
      this.searchData.searchPropertyName = searchValue;
      this.searchData.searchPropertyValue = searchValue;
      this.searchData.strictSearchValue = searchValue;
    }

    this.#clearHighlight(this.element);
    this.#clearRules();
    this.#createEditors();

    this.inspector.emit("ruleview-filtered");
  }

  /**
   * Called when the user clicks on the clear button in the filter style search
   * box. Returns true if the search box is cleared and false otherwise.
   *
   * @param {object} options
   *        Options that will be passed to `setFilterStyles`
   */

  #onClearSearch = options => {
    if (this.searchField.value) {
      this.setFilterStyles("", options);
      return true;
    }

    return false;
  };

  destroy() {
    this.isDestroyed = true;

    this.selectedNodeFront = null;

    if (this.elementStyle) {
      this.elementStyle.destroy();
      this.elementStyle = null;
    }

    if (this.pageStyle) {
      this.pageStyle.off("stylesheet-updated"this.refreshPanel);
      this.pageStyle = null;
    }

    this.#dummyElement = null;
    this.#prefObserver.destroy();

    this.outputParser = null;

    if (this.#classListPreviewer) {
      this.#classListPreviewer.destroy();
      this.#classListPreviewer = null;
    }

    if (this.#contextMenu) {
      this.#contextMenu.destroy();
      this.#contextMenu = null;
    }

    if (this.#highlighters) {
      this.#highlighters.removeFromView(this);
      this.#highlighters = null;
    }

    this.colorSchemeLightSimulationButton = null;
    this.colorSchemeDarkSimulationButton = null;
    this.printSimulationButton = null;

    this.tooltips.destroy();

    this.#abortController.abort();
    this.#abortController = null;
    this.#containers.clear();

    this.shortcuts.destroy();

    this.inspector.highlighters.off(
      "highlighter-shown",
      this.onHighlighterShown
    );
    this.inspector.highlighters.off(
      "highlighter-hidden",
      this.onHighlighterHidden
    );

    this.searchField = null;
    this.searchClearButton = null;
    this.pseudoClassPanel = null;
    this.pseudoClassToggle = null;
    this.pseudoClassCheckboxes = null;
    this.pseudoClassesStandardPanel = null;
    this.pseudoClassesElementSpecificPanel = null;
    this.pseudoClassesElementSpecificHeading = null;
    this.elementSpecificPseudoClassCheckboxes = null;
    this.classPanel = null;
    this.classToggle = null;

    this.inspector = null;
    this.styleDocument = null;
    this.styleWindow = null;

    if (this.element.parentNode) {
      this.element.remove();
    }

    if (this.#popup) {
      this.#popup.destroy();
      this.#popup = null;
    }
  }

  /**
   * Mark the view as selecting an element, disabling all interaction, and
   * visually clearing the view after a few milliseconds to avoid confusion
   * about which element's styles the rule view shows.
   */

  #startSelectingElement() {
    this.element.classList.add("non-interactive");
  }

  /**
   * Mark the view as no longer selecting an element, re-enabling interaction.
   */

  #stopSelectingElement() {
    this.element.classList.remove("non-interactive");
  }

  /**
   * Disables add rule button when needed
   */

  #refreshAddRuleButtonState() {
    this.addRuleButton.disabled = !this.canAddNewRuleForSelectedNode();
  }

  /**
   * Update pageStyle reference and listen for stylesheet updates.
   */

  #refreshPageStyle() {
    const newPageStyle = this.selectedNodeFront?.inspectorFront.pageStyle;
    if (this.pageStyle == newPageStyle) {
      return;
    }
    // If we were already selecting an element from a different process,
    // we should unregister the PageStyle Actor event listener
    if (this.pageStyle) {
      this.pageStyle.off("stylesheet-updated"this.refreshPanel);
      this.pageStyle = null;
    }
    // If we are selecting a new element, we should also start listening
    // for event from its process and its related Page Style Actor.
    if (newPageStyle) {
      this.pageStyle = newPageStyle;
      this.pageStyle.on("stylesheet-updated"this.refreshPanel);
    }
  }

  /**
   * Update the view with a new selected element.
   *
   * @param {NodeActor} element
   *        The node whose style rules we'll inspect.
   * @param {boolean} forceRefresh
   *        Update the view even if the element is the same as last time.
   */

  async selectElement(element, forceRefresh = false) {
    const sameElementSelected = this.selectedNodeFront === element;
    if (sameElementSelected && !forceRefresh) {
      return;
    }

    // 1/3 All cleanups that do not depend on former/new selected element
    if (this.#popup && this.#popup.isOpen) {
      this.#popup.hidePopup();
    }

    // 2/3 Important step: actually switch to a new or empty element
    this.selectedNodeFront = element;

    // 3/3 Now update based on the newly selected element

    // Wipe the whole rule view content when deselecting or selecting another element
    // as there is little chance we would display the same rules. It is faster
    // to render everything from scratch than trying to do incremental updates.
    if (!sameElementSelected) {
      this.#clearRules();
    }

    this.#refreshEmptyNotice();
    this.#refreshAddRuleButtonState();
    this.#refreshDummyElement();
    this.#refreshPageStyle();
    this.#refreshPseudoClassPanel();

    if (!element) {
      this.#stopSelectingElement();

      // Destroy the ElementStyle *after* having cleared the rule view
      // (earlier call to `#clearRules`), as it may destroy each DOM element
      // for each rule individually and cause unecessary reflows
      if (this.elementStyle) {
        this.elementStyle.destroy();
        this.elementStyle = null;
      }
      return;
    }

    const isProfilerActive = Services.profiler.IsActive();
    const startTime = isProfilerActive ? ChromeUtils.now() : null;
    const done = this.inspector.updating("rule-view");

    const elementStyle = new ElementStyle(
      element,
      this,
      this.store,
      this.pageStyle,
      this.#showUserAgentStyles
    );
    let previousElementStyle = this.elementStyle;
    this.elementStyle = elementStyle;

    this.#startSelectingElement();

    try {
      // Bug 2016127: This is historical, but unfortunately breaks some tests if removed
      await Promise.resolve(null);

      await this.#populate();
      if (this.elementStyle !== elementStyle) {
        done();
        return;
      }
      this.elementStyle.onChanged = () => {
        this.#onElementStyleChanged();
      };
      // Cleanup the previous ElementStyle model only after the refresh
      // as it will destroy each rule individually and may cause uncessary reflows
      // if entire containers are removed.
      if (previousElementStyle) {
        previousElementStyle.destroy();
        previousElementStyle = null;

        // We need to update containers as some may now be empty
        this.#updateContainers();
      }
      this.#stopSelectingElement();
      if (isProfilerActive && this.elementStyle.rules) {
        let declarations = 0;
        for (const rule of this.elementStyle.rules) {
          declarations += rule.textProps.length;
        }
        ChromeUtils.addProfilerMarker(
          "DevTools:CssRuleView.selectElement",
          startTime,
          `${declarations} CSS declarations in ${this.elementStyle.rules.length} rules`
        );
      }
    } catch (e) {
      if (this.elementStyle === elementStyle) {
        this.#stopSelectingElement();
        this.#clearRules();
      }
      console.error("Error while updating the rule view", e);
    }
    done();
  }

  /**
   * Update the rules for the currently highlighted element.
   */

  async refreshPanel() {
    // Ignore refreshes when the panel is hidden, or during editing or when no element is selected.
    if (!this.isPanelVisible() || this.isEditing || !this.elementStyle) {
      return;
    }

    // Repopulate the element style once the current modifications are done.
    const promises = [];
    for (const rule of this.elementStyle.rules) {
      if (rule.applyingModifications) {
        promises.push(rule.applyingModifications);
      }
    }

    await Promise.all(promises);
    await this.#populate();
  }

  /**
   * For each pseudo-class item, create a checkbox input element for toggling a
   * pseudo-class on the selected element and append it to passed container.
   *
   * @param {Array} items
   *        An array of pseudo-class items to create checkboxes for.
   * @param {HTMLElement} container
   *        The container element to which the checkboxes will be appended.
   *
   * @returns {Array<HTMLInputElement>} An array with the checkbox input elements for pseudo-classes.
   */

  #createCheckboxes(items, container) {
    const doc = this.styleDocument;
    const fragment = doc.createDocumentFragment();
    const checkboxes = [];
    for (const item of items) {
      const label = doc.createElement("label");
      const checkbox = doc.createElement("input");
      checkbox.setAttribute("type""checkbox");
      checkbox.setAttribute("value", item);

      label.append(checkbox, item);
      fragment.append(label);
      checkboxes.push(checkbox);
    }

    container.append(fragment);
    return checkboxes;
  }

  /**
   * Get the element-specific pseudo-classes that apply to the currently selected element.
   *
   * @returns {Array} Array of pseudo-classes that apply to the current element
   */

  #getApplicableElementSpecificPseudoClasses() {
    if (!this.selectedNodeFront) {
      return [];
    }

    const tagName = this.selectedNodeFront.tagName?.toLowerCase();
    const applicablePseudoClasses = [];

    for (const [pseudo, elementTypes] of Object.entries(
      ELEMENT_SPECIFIC_PSEUDO_CLASSES
    )) {
      if (elementTypes.has(tagName)) {
        applicablePseudoClasses.push(pseudo);
      }
    }

    return applicablePseudoClasses;
  }

  /**
   * Update the pseudo class options for the currently highlighted element.
   */

  #refreshPseudoClassPanel() {
    if (
      !this.selectedNodeFront ||
      !this.inspector.canTogglePseudoClassForSelectedNode()
    ) {
      for (const checkbox of [
        ...this.pseudoClassCheckboxes,
        ...this.elementSpecificPseudoClassCheckboxes,
      ]) {
        checkbox.disabled = true;
      }
      this.#updateElementSpecificPseudoClassPanel();
      return;
    }

    const pseudoClassLocks = this.selectedNodeFront.pseudoClassLocks;
    for (const checkbox of this.pseudoClassCheckboxes) {
      checkbox.disabled = false;
      checkbox.checked = pseudoClassLocks.includes(checkbox.value);
    }

    const applicablePseudoClasses =
      this.#getApplicableElementSpecificPseudoClasses();
    for (const checkbox of this.elementSpecificPseudoClassCheckboxes) {
      const isApplicable = applicablePseudoClasses.includes(checkbox.value);
      checkbox.disabled = !isApplicable;
      if (isApplicable) {
        checkbox.checked = pseudoClassLocks.includes(checkbox.value);
      }
    }

    this.#updateElementSpecificPseudoClassPanel();
  }

  /**
   * Update the visibility of the element-specific pseudo-class panel.
   * Show the panel if there are applicable pseudo-classes, otherwise hide it.
   */

  #updateElementSpecificPseudoClassPanel() {
    const applicablePseudoClasses =
      this.#getApplicableElementSpecificPseudoClasses();
    const hasApplicablePseudoClasses = !!applicablePseudoClasses.length;

    this.pseudoClassesElementSpecificHeading.hidden =
      !hasApplicablePseudoClasses;
  }

  async #populate() {
    try {
      const elementStyle = this.elementStyle;

      await this.elementStyle.populate();

      if (this.elementStyle !== elementStyle || this.isDestroyed) {
        return;
      }

      await this.#createEditors();

      // Notify anyone that cares that we refreshed.
      this.inspector.emit("rule-view-refreshed");
    } catch (e) {
      console.error("Exception while populating the rule view", e);
      throw e;
    }
  }

  /**
   * Show or hide the empty notice depending on selected node.
   */

  #refreshEmptyNotice() {
    const emptyNotice = this.styleDocument.getElementById(
      "ruleview-no-results"
    );
    if (this.selectedNodeFront && emptyNotice) {
      emptyNotice.remove();
    } else if (!this.selectedNodeFront && !emptyNotice) {
      createChild(this.element, "div", {
        id: "ruleview-no-results",
        class"devtools-sidepanel-no-result",
        textContent: l10n("rule.empty"),
      });
    }
  }

  /**
   * Clear the rules.
   */

  #clearRules() {
    this.#containers.clear();
    this.element.innerHTML = "";
  }

  /**
   * Called when the user has made changes to the ElementStyle.
   * Emits an event that clients can listen to.
   */

  #onElementStyleChanged() {
    this.emit("ruleview-changed");
  }

  /**
   * Text for header that shows above rules for this element
   */

  #selectedElementLabel;
  get selectedElementLabel() {
    if (this.#selectedElementLabel) {
      return this.#selectedElementLabel;
    }
    this.#selectedElementLabel = l10n("rule.selectedElement");
    return this.#selectedElementLabel;
  }

  /**
   * Text for header that shows above rules for pseudo elements
   */

  #pseudoElementLabel;
  get pseudoElementLabel() {
    if (this.#pseudoElementLabel) {
      return this.#pseudoElementLabel;
    }
    this.#pseudoElementLabel = l10n("rule.pseudoElement");
    return this.#pseudoElementLabel;
  }

  #showPseudoElements;
  get showPseudoElements() {
    if (this.#showPseudoElements === undefined) {
      this.#showPseudoElements = Services.prefs.getBoolPref(
        "devtools.inspector.show_pseudo_elements"
      );
    }
    return this.#showPseudoElements;
  }

  /**
   * Creates a simple, non-expandable container in the rule view
   *
   * @param  {string} label
   *         The label for the container header
   * @param  {string} containerId
   *         The id that will be set on the container
   * @return {Object{ header:DOMElement, container: DOMElement }}
   *         Object containing both the container element and its related header.
   */

  createSimpleContainer(label, containerId) {
    const header = this.styleDocument.createElementNS(HTML_NS, "div");
    header.className = RULE_VIEW_HEADER_CLASSNAME;
    header.setAttribute("role""heading");
    // Element container is only shown when pseudo element container exists
    // which can only be computed later after having processed all rules.
    if (containerId == ELEMENT_CONTAINER_ID) {
      header.hidden = true;
    }
    header.append(label);

    const container = this.styleDocument.createElementNS(HTML_NS, "div");
    container.classList.add("ruleview-container");
    container.id = containerId;

    this.#containers.set(containerId, { header, container });

    return { header, container };
  }

  /**
   * Creates an expandable container in the rule view
   *
   * @param  {string} label
   *         The label for the container header
   * @param  {string} containerId
   *         The id that will be set on the container
   * @return {Object{ header:DOMElement, container: DOMElement }}
   *         Object containing both the container element and its related header.
   */

  createExpandableContainer(label, containerId) {
    const header = this.styleDocument.createElementNS(HTML_NS, "div");
    header.classList.add(
      RULE_VIEW_HEADER_CLASSNAME,
      "ruleview-expandable-header"
    );
    header.setAttribute("role""heading");
    header.setAttribute("aria-level""3");
    header.hidden = false;

    const toggleButton = this.styleDocument.createElementNS(HTML_NS, "button");
    toggleButton.setAttribute(
      "title",
      l10n("rule.expandableContainerToggleButton.title")
    );
    toggleButton.setAttribute("aria-expanded""true");
    toggleButton.setAttribute("aria-controls", containerId);

    const twisty = this.styleDocument.createElementNS(HTML_NS, "span");
    twisty.className = "ruleview-expander theme-twisty";

    toggleButton.append(twisty, label);
    header.append(toggleButton);

    const container = this.styleDocument.createElementNS(HTML_NS, "div");
    container.id = containerId;
    container.classList.add(
      "ruleview-container",
      "ruleview-expandable-container"
    );
    container.hidden = false;

    this.#containers.set(containerId, { header, container });

    const { signal } = this.#abortController;
    toggleButton.addEventListener(
      "click",
      this.#toggleContainerVisibility.bind(this, containerId),
      { signal }
    );

    // All containers are expanded by default, but pseudo elements
    // are only expanded if the related preference is true.
    // So manually collapse the pseudo container if this pref is false.
    const isPseudo = containerId == PSEUDO_ELEMENTS_CONTAINER_ID;
    if (isPseudo && !this.showPseudoElements) {
      this.#toggleContainerVisibility(containerId);
    }

    return { header, container };
  }

  /**
   * Create the `@property` expandable container
   *
   * @returns {Element}
   */

  getOrCreateRegisteredPropertiesExpandableContainer() {
    const existingEntry = this.#containers.get(
      REGISTERED_PROPERTIES_CONTAINER_ID
    );
    if (existingEntry) {
      return existingEntry.container;
    }
    const { header, container } = this.createExpandableContainer(
      "@property",
      REGISTERED_PROPERTIES_CONTAINER_ID
    );
    this.#containers.set(REGISTERED_PROPERTIES_CONTAINER_ID, {
      header,
      container,
    });
    this.element.append(header, container);
    return container;
  }

  /**
   * Return the RegisteredPropertyEditor element for a given property name
   *
   * @param {string} registeredPropertyName
   * @returns {Element|null}
   */

  getRegisteredPropertyElement(registeredPropertyName) {
    return this.styleDocument.querySelector(
      `#${REGISTERED_PROPERTIES_CONTAINER_ID} [data-name="${registeredPropertyName}"]`
    );
  }

  /**
   * Toggle the visibility of an expandable container
   *
   * @param  {string}  containerId
   *         Container ID.
   */

  #toggleContainerVisibility(containerId) {
    const { header, container } = this.#containers.get(containerId);
    const toggleButton = header.querySelector("button");
    const shouldExpand = toggleButton.getAttribute("aria-expanded") !== "true";

    // Memoize the state in the pref for pseudo elements
    const isPseudo = containerId == PSEUDO_ELEMENTS_CONTAINER_ID;
    if (isPseudo) {
      this.#showPseudoElements = shouldExpand;
      Services.prefs.setBoolPref(
        "devtools.inspector.show_pseudo_elements",
        this.#showPseudoElements
      );
    }

    container.hidden = !shouldExpand;

    toggleButton.setAttribute("aria-expanded", shouldExpand);
  }

  /**
   * Creates editor UI for each of the rules in elementStyle.
   */

  #createEditors() {
    // Run through the current list of rules, attaching
    // their editors in order.  Create editors if needed.
    let seenSearchTerm = false;

    if (!this.elementStyle.rules) {
      return Promise.resolve();
    }

    // Transient list of containers added to the DOM
    // while processing this method.
    const currentContainers = [];

    const editorReadyPromises = [];
    const lastElementPerContainer = new Map();
    for (const rule of this.elementStyle.rules) {
      // Initialize rule editor if this is a new rule
      if (!rule.editor) {
        editorReadyPromises.push(this.#createEditorForRule(rule));
      }

      // Filter the rules and highlight any matches if there is a search input
      if (this.searchValue && this.searchData) {
        if (this.highlightRule(rule)) {
          seenSearchTerm = true;
        } else if (rule.domRule.type !== ELEMENT_STYLE) {
          continue;
        }
      }

      const container = this.#getContainerForRule(rule, currentContainers);

      // Ensure adding the rule editor at the right location
      const lastElement = lastElementPerContainer.get(container);
      // Also avoid any DOM mutation if the rule already exists and wasn't moved
      if (
        lastElement &&
        lastElement.nextElementSibling != rule.editor.element
      ) {
        lastElement.insertAdjacentElement("afterend", rule.editor.element);
      } else if (
        !lastElement &&
        container.firstElementChild != rule.editor.element
      ) {
        container.insertAdjacentElement("afterbegin", rule.editor.element);
      }
      lastElementPerContainer.set(container, rule.editor.element);
    }

    this.#createRegisteredPropertyEditors();

    this.#updateContainers();

    // Automatically select the selector input when we are adding a user-added rule.
    // (Focus after having updated all the rules to prevent the focus from being
    // lost because of possible following DOM updates)
    if (this.#focusNextUserAddedRule) {
      const rule = this.elementStyle.rules.find(r => r.domRule.userAdded);
      if (rule) {
        rule.editor.selectorText.click();
        this.emitForTests("new-rule-added", rule);
      }
      this.#focusNextUserAddedRule = null;
    }

    const searchBox = this.searchField.parentNode;
    searchBox.classList.toggle(
      "devtools-searchbox-no-match",
      this.searchValue && !seenSearchTerm
    );

    return Promise.all(editorReadyPromises);
  }

  /**
   * Instantiate a new RuleEditor for a given rule
   * currently applying to the selected element.
   *
   * @param  {Rule} rule
   */

  #createEditorForRule(rule) {
    const ruleActorID = rule.domRule.actorID;
    rule.editor = new RuleEditor(this, rule, {
      elementsWithPendingClicks: this.#elementsWithPendingClicks,
      onShowUnusedCustomCssProperties: () => {
        this.store.expandedUnusedCustomCssPropertiesRuleActorIds.add(
          ruleActorID
        );
      },
      shouldHideUnusedCustomCssProperties:
        !this.store.expandedUnusedCustomCssPropertiesRuleActorIds.has(
          ruleActorID
        ),
    });

    return rule.editor.once("source-link-updated");
  }

  /**
   * Create or get existing container for a given rule.
   *
   * @param {Rule} rule
   * @param {Array<DOMElement>} currentContainers
   */

  #getContainerForRule(rule, currentContainers) {
    let id,
      label,
      expandable = false;

    // Don't display inherited pseudo element rules (e.g. ::details-content) inside
    // the pseudo element container
    const isNonInheritedPseudo = !!rule.pseudoElement && !rule.inherited;
    const keyframes = rule.keyframes;

    if (isNonInheritedPseudo) {
      id = PSEUDO_ELEMENTS_CONTAINER_ID;
      label = this.pseudoElementLabel;
      expandable = true;
    } else if (keyframes) {
      // See bug 1042036 and Bug 1894873 we are showing all keyframes with the same name.
      // We may use ${keyframes.name}, but all rule's content would be merged
      // into a unique rule/container.
      // (only use part of the actorID to have a valid DOM id)
      id = `keyframes-container-${rule.keyframes.actorID.match(/\w+$/)[0]}`;
      label = rule.keyframesName;
      expandable = true;
    } else if (rule.domRule.className === "CSSPositionTryRule") {
      id = POSITION_TRY_CONTAINER_ID;
      label = "@position-try";
      expandable = true;
    } else if (rule.inherited) {
      // We need to check both `inherited` (a NodeFront) and `pseudoElement` (string),
      // as element-backed pseudo element rules (e.g. `::details-content`) can have the same
      // `inherited` property as a regular rule (e.g. on `<details>`), but the element is
      // to be considered as a child of the binding element.
      //
      // e.g. we want to have:
      //   This element
      //   Inherited by details::details-content
      //   Inherited by details
      id = `inherited-${rule.inherited.actorID}-${rule.pseudoElement}`;
      label = rule.inheritedSectionLabel;
    } else {
      id = ELEMENT_CONTAINER_ID;
      label = this.selectedElementLabel;
    }

    let entry = this.#containers.get(id);
    // Create the DOM for the container if it doesn't exist yet
    if (!entry) {
      if (expandable) {
        entry = this.createExpandableContainer(label, id);
      } else {
        entry = this.createSimpleContainer(label, id);
      }
    }

    const { header, container } = entry;

    // Ensure displaying the containers in the new order processed in #createEditors.
    if (!currentContainers.includes(container)) {
      const lastContainer = currentContainers.at(-1);
      // Pseudo element rules are sorted **after** element matching rules and inherited rules
      // in ElementStyle.rules, but we want the container to always be shown at the top.
      //
      // Also avoid any DOM mutation if the rule already exists and wasn't moved
      if (id == PSEUDO_ELEMENTS_CONTAINER_ID || !lastContainer) {
        if (this.element.firstElementChild != header) {
          this.element.insertAdjacentElement("afterbegin", header);
          header.insertAdjacentElement("afterend", container);
        }
      } else if (lastContainer.nextElementSibling != header) {
        lastContainer.insertAdjacentElement("afterend", header);
        header.insertAdjacentElement("afterend", container);
      }
      currentContainers.push(container);
    }

    return container;
  }

  /**
   * Whenever we may add or remove rules in the list,
   * we have to eventually update containers by removing the empty ones
   * and update the visibility of "Element" header.
   */

  #updateContainers() {
    // Clear containers which no longer contain any rule
    for (const [
      containerId,
      { header, container },
    ] of this.#containers.entries()) {
      if (!container.children.length) {
        header.remove();
        container.remove();
        this.#containers.delete(containerId);
      }
    }

    // Only print header for "This element" if there are pseudo elements displayed before
    const elementEntry = this.#containers.get(ELEMENT_CONTAINER_ID);
    if (elementEntry) {
      const hasPseudoElementRules = this.#containers.has(
        PSEUDO_ELEMENTS_CONTAINER_ID
      );
      // Show the header element, which is before the container element
      elementEntry.header.hidden = !hasPseudoElementRules;
    }
  }

  #createRegisteredPropertyEditors() {
    const targetRegisteredProperties =
      this.getRegisteredPropertiesForSelectedNodeTarget();
    if (!targetRegisteredProperties?.size) {
      // Wipe the list, but only if the properties container was populated
      const entry = this.#containers.get(REGISTERED_PROPERTIES_CONTAINER_ID);
      if (entry) {
        entry.container.replaceChildren();
      }
      return;
    }

    const registeredPropertiesContainer =
      this.getOrCreateRegisteredPropertiesExpandableContainer();
    // Always wipe and rebuild the list of properties from scratch
    registeredPropertiesContainer.replaceChildren();

    // Sort properties by their name, as we want to display them in alphabetical order
    const propertyDefinitions = Array.from(
      targetRegisteredProperties.values()
    ).sort((a, b) => (a.name < b.name ? -1 : 1));
    for (const propertyDefinition of propertyDefinitions) {
      const registeredPropertyEditor = new RegisteredPropertyEditor(
        this,
        propertyDefinition
      );

      registeredPropertiesContainer.appendChild(
        registeredPropertyEditor.element
      );
    }
  }

  /**
   * Highlight rules that matches the filter search value and returns a
   * boolean indicating whether or not rules were highlighted.
   *
   * @param  {Rule} rule
   *         The rule object we're highlighting if its rule selectors or
   *         property values match the search value.
   * @return {boolean} true if the rule was highlighted, false otherwise.
   */

  highlightRule(rule) {
    const isRuleSelectorHighlighted = this.#highlightRuleSelector(rule);
    const isStyleSheetHighlighted = this.#highlightStyleSheet(rule);
    const isAncestorRulesHighlighted = this.#highlightAncestorRules(rule);
    let isHighlighted =
      isRuleSelectorHighlighted ||
      isStyleSheetHighlighted ||
      isAncestorRulesHighlighted;

    // Highlight search matches in the rule properties
    for (const textProp of rule.textProps) {
      if (!textProp.invisible && this.#highlightProperty(textProp)) {
        isHighlighted = true;
      }
    }

    return isHighlighted;
  }

  /**
   * Highlights the rule selector that matches the filter search value and
   * returns a boolean indicating whether or not the selector was highlighted.
   *
   * @param  {Rule} rule
   *         The Rule object.
   * @return {boolean} true if the rule selector was highlighted,
   *         false otherwise.
   */

  #highlightRuleSelector(rule) {
    let isSelectorHighlighted = false;

    let selectorNodes = [...rule.editor.selectorText.childNodes];
    if (
      rule.domRule.type === CSSRule.KEYFRAME_RULE ||
      rule.domRule.className === "CSSPositionTryRule"
    ) {
      selectorNodes = [rule.editor.selectorText];
    } else if (rule.domRule.type === ELEMENT_STYLE) {
      selectorNodes = [];
    }

    // Highlight search matches in the rule selectors
    for (const selectorNode of selectorNodes) {
      const selector = selectorNode.textContent.toLowerCase();
      if (
        (this.searchData.strictSearchAllValues &&
          selector === this.searchData.strictSearchValue) ||
        (!this.searchData.strictSearchAllValues &&
          selector.includes(this.searchValue))
      ) {
        selectorNode.classList.add("ruleview-highlight");
        isSelectorHighlighted = true;
      }
    }

    return isSelectorHighlighted;
  }

  /**
   * Highlights the ancestor rules data (@media / @layer) that matches the filter search
   * value and returns a boolean indicating whether or not element was highlighted.
   *
   * @return {boolean} true if the element was highlighted, false otherwise.
   */

  #highlightAncestorRules(rule) {
    const element = rule.editor.ancestorDataEl;
    if (!element) {
      return false;
    }

    const ancestorSelectors = element.querySelectorAll(
      ".ruleview-rule-ancestor-selectorcontainer"
    );

    let isHighlighted = false;
    for (const child of ancestorSelectors) {
      const dataText = child.innerText.toLowerCase();
      const matches = this.searchData.strictSearchValue
        ? dataText === this.searchData.strictSearchValue
        : dataText.includes(this.searchValue);
      if (matches) {
        isHighlighted = true;
        child.classList.add("ruleview-highlight");
      }
    }

    return isHighlighted;
  }

  /**
   * Highlights the stylesheet source that matches the filter search value and
   * returns a boolean indicating whether or not the stylesheet source was
   * highlighted.
   *
   * @return {boolean} true if the stylesheet source was highlighted, false
   *         otherwise.
   */

  #highlightStyleSheet(rule) {
    const styleSheetSource = rule.title.toLowerCase();
    const isStyleSheetHighlighted = this.searchData.strictSearchValue
      ? styleSheetSource === this.searchData.strictSearchValue
      : styleSheetSource.includes(this.searchValue);

    if (isStyleSheetHighlighted && rule.editor.source) {
      rule.editor.source.classList.add("ruleview-highlight");
    }

    return isStyleSheetHighlighted;
  }

  /**
   * Highlights the rule properties and computed properties that match the
   * filter search value and returns a boolean indicating whether or not the
   * property or computed property was highlighted.
   *
   * @param  {TextProperty} textProperty
   *         The rule property.
   * @returns {boolean} true if the property or computed property was
   *         highlighted, false otherwise.
   */

  #highlightProperty(textProperty) {
    const isPropertyHighlighted = this.#highlightRuleProperty(textProperty);
    const isComputedHighlighted = this.#highlightComputedProperty(textProperty);

    // Expand the computed list if a computed property is highlighted and the
    // property rule is not highlighted
    if (
      !isPropertyHighlighted &&
      isComputedHighlighted &&
      !textProperty.editor.computed.hasAttribute("user-open")
    ) {
      textProperty.editor.expandForFilter();
    }

    return isPropertyHighlighted || isComputedHighlighted;
  }

  /**
   * Called when TextPropertyEditor is updated and updates the rule property
   * highlight.
   *
   * @param  {TextPropertyEditor} editor
   *         The rule property TextPropertyEditor object.
   */
  updatePropertyHighlight(editor) {
    if (!this.searchValue || !this.searchData) {
      return;
    }

    this.#clearHighlight(editor.element);

    if (this.#highlightProperty(editor.prop)) {
      this.searchField.classList.remove("devtools-style-searchbox-no-match");
    }
  }

  /**
   * Highlights the rule property that matches the filter search value
   * and returns a boolean indicating whether or not the property was
   * highlighted.
   *
   * @param  {TextProperty} textProperty
   *         The rule property object.
   * @returns {boolean} true if the rule property was highlighted,
   *         false otherwise.
   */

  #highlightRuleProperty(textProperty) {
    const propertyName = textProperty.name.toLowerCase();
    // Get the actual property value displayed in the rule view if we have an editor for
    // it (that might not be the case for unused CSS custom properties).
    const propertyValue = textProperty.editor
      ? textProperty.editor.valueSpan.textContent.toLowerCase()
      : textProperty.value.toLowerCase();

    return this.#highlightMatches({
      element: textProperty.editor?.container,
      propertyName,
      propertyValue,
      textProperty,
    });
  }

  /**
   * Highlights the computed property that matches the filter search value and
   * returns a boolean indicating whether or not the computed property was
   * highlighted.
   *
   * @param  {TextProperty} textProperty
   *         The rule property object.
   * @returns {boolean} true if the computed property was highlighted, false
   *         otherwise.
   */

  #highlightComputedProperty(textProperty) {
    if (!textProperty.editor) {
      return false;
    }

    let isComputedHighlighted = false;

    // Highlight search matches in the computed list of properties
    textProperty.editor.populateComputed();
    for (const computed of textProperty.computed) {
      if (computed.element) {
        // Get the actual property value displayed in the computed list
        const computedName = computed.name.toLowerCase();
        const computedValue = computed.parsedValue.toLowerCase();

        isComputedHighlighted = this.#highlightMatches({
          element: computed.element,
          propertyName: computedName,
          propertyValue: computedValue,
          textProperty,
        })
          ? true
          : isComputedHighlighted;
      }
    }

    return isComputedHighlighted;
  }

  /**
   * Helper function for highlightRules that carries out highlighting the given
   * element if the search terms match the property, and returns a boolean
   * indicating whether or not the search terms match.
   *
   * @param  {object} options
   * @param  {DOMNode} options.element
   *         The node to highlight if search terms match
   * @param  {string} options.propertyName
   *         The property name of a rule
   * @param  {string} options.propertyValue
   *         The property value of a rule
   * @param  {TextProperty} options.textProperty
   *         The text property that we may highlight. It's helpful in cases we don't have
   *         an element yet (e.g. if the property is a hidden unused variable)
   * @return {boolean} true if the given search terms match the property, false
   *         otherwise.
   */

  #highlightMatches({ element, propertyName, propertyValue, textProperty }) {
    const {
      searchPropertyName,
      searchPropertyValue,
      searchPropertyMatch,
      strictSearchPropertyName,
      strictSearchPropertyValue,
      strictSearchAllValues,
    } = this.searchData;
    let matches = false;

    // If the inputted search value matches a property line like
    // `font-family: arial`, then check to make sure the name and value match.
    // Otherwise, just compare the inputted search string directly against the
    // name and value of the rule property.
    const hasNameAndValue =
      searchPropertyMatch && searchPropertyName && searchPropertyValue;
    const isMatch = (value, query, isStrict) => {
      return isStrict ? value === query : query && value.includes(query);
    };

    if (hasNameAndValue) {
      matches =
        isMatch(propertyName, searchPropertyName, strictSearchPropertyName) &&
        isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue);
    } else {
      matches =
        isMatch(
          propertyName,
          searchPropertyName,
          strictSearchPropertyName || strictSearchAllValues
        ) ||
        isMatch(
          propertyValue,
          searchPropertyValue,
          strictSearchPropertyValue || strictSearchAllValues
        );
    }

    if (!matches) {
      return false;
    }

    // We might not have an element when the prop is an unused custom css property.
    if (!element && textProperty?.isUnusedVariable) {
      const editor =
        textProperty.rule.editor.showUnusedCssVariable(textProperty);

      // The editor couldn't be created, bail (shouldn't happen)
      if (!editor) {
        return false;
      }

      element = editor.container;
    }

    element.classList.add("ruleview-highlight");

    return true;
  }

  /**
   * Clear all search filter highlights in the panel, and close the computed
   * list if toggled opened
   */

  #clearHighlight(element) {
    for (const el of element.querySelectorAll(".ruleview-highlight")) {
      el.classList.remove("ruleview-highlight");
    }

    for (const computed of element.querySelectorAll(
      ".ruleview-computedlist[filter-open]"
    )) {
      computed.parentNode._textPropertyEditor.collapseForFilter();
    }
  }

  /**
   * Called when the pseudo class panel button is clicked and toggles
   * the display of the pseudo class panel.
   */

  #onTogglePseudoClassPanel = () => {
    if (this.pseudoClassPanel.hidden) {
      this.showPseudoClassPanel();
    } else {
      this.hidePseudoClassPanel();
    }
  };

  showPseudoClassPanel() {
    this.hideClassPanel();

    this.pseudoClassToggle.setAttribute("aria-pressed""true");
    this.pseudoClassPanel.inert = false;
    this.pseudoClassPanel.hidden = false;
  }

  hidePseudoClassPanel() {
    this.pseudoClassToggle.setAttribute("aria-pressed""false");
    this.pseudoClassPanel.inert = true;
    this.pseudoClassPanel.hidden = true;
  }

  /**
   * Called when a pseudo class checkbox is clicked and toggles
   * the pseudo class for the current selected element.
   */

  #onTogglePseudoClass = event => {
    const target = event.target;
    this.inspector.togglePseudoClass(target.value);
  };

  /**
   * Called when the class panel button is clicked and toggles the display of the class
   * panel.
   */

  #onToggleClassPanel = () => {
    if (this.classPanel.hidden) {
      this.showClassPanel();
    } else {
      this.hideClassPanel();
    }
  };

  showClassPanel() {
    this.hidePseudoClassPanel();

    this.classToggle.setAttribute("aria-pressed""true");
    this.classPanel.hidden = false;

    this.classListPreviewer.focusAddClassField();
  }

  hideClassPanel() {
    this.classToggle.setAttribute("aria-pressed""false");
    this.classPanel.hidden = true;
  }

  /**
   * Handle the keypress event in the rule view.
   */

  #onShortcut(name, event) {
    if (!event.target.closest("#sidebar-panel-ruleview")) {
      return;
    }

    if (name === "CmdOrCtrl+F") {
      this.searchField.focus();
      event.preventDefault();
    } else if (
      (name === "Return" || name === "Space") &&
      this.element.classList.contains("non-interactive")
    ) {
      event.preventDefault();
    } else if (
      name === "Escape" &&
      event.target === this.searchField &&
      this.#onClearSearch()
    ) {
      // Handle the search box's keypress event. If the escape key is pressed,
      // clear the search box field.
      event.preventDefault();
      event.stopPropagation();
    }
  }

  #onToggleLightColorSchemeSimulation = async () => {
    const shouldSimulateLightScheme =
      this.colorSchemeLightSimulationButton.getAttribute("aria-pressed") !==
      "true";

    this.colorSchemeLightSimulationButton.setAttribute(
      "aria-pressed",
      shouldSimulateLightScheme
    );

    this.colorSchemeDarkSimulationButton.setAttribute("aria-pressed""false");

    await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
      {
        colorSchemeSimulation: shouldSimulateLightScheme ? "light" : null,
      }
    );
    // Refresh the current element's rules in the panel.
    this.refreshPanel();
  };

  #onToggleDarkColorSchemeSimulation = async () => {
    const shouldSimulateDarkScheme =
      this.colorSchemeDarkSimulationButton.getAttribute("aria-pressed") !==
      "true";

    this.colorSchemeDarkSimulationButton.setAttribute(
      "aria-pressed",
      shouldSimulateDarkScheme
    );

    this.colorSchemeLightSimulationButton.setAttribute("aria-pressed""false");

    await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
      {
        colorSchemeSimulation: shouldSimulateDarkScheme ? "dark" : null,
      }
    );
    // Refresh the current element's rules in the panel.
    this.refreshPanel();
  };

  #onTogglePrintSimulation = async () => {
    const enabled =
      this.printSimulationButton.getAttribute("aria-pressed") !== "true";
    this.printSimulationButton.setAttribute("aria-pressed", enabled);
    await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
      {
        printSimulationEnabled: enabled,
      }
    );
    // Refresh the current element's rules in the panel.
    this.refreshPanel();
  };

  /**
   * Temporarily flash the given element.
   *
   * @param  {Element} element
   *         The element.
   * @returns {Promise} Promise that resolves after the element was flashed-out
   */

  #flashMutationCallback;
  #flashElement(element) {
    flashElementOn(element, {
      backgroundClass: "theme-bg-contrast",
    });

    if (this.#flashMutationCallback) {
      this.#flashMutationCallback();
    }

    return new Promise(resolve => {
      this.#flashMutationCallback = () => {
        flashElementOff(element, {
          backgroundClass: "theme-bg-contrast",
        });
        this.#flashMutationCallback = null;
        resolve();
      };

      setTimeout(this.#flashMutationCallback, PROPERTY_FLASHING_DURATION);
    });
  }

  /**
   * Scrolls to the top of either the rule or declaration. The view will try to scroll to
   * the rule if both can fit in the viewport. If not, then scroll to the declaration.
   *
   * @param  {Element} rule
   *         The rule to scroll to.
   * @param  {Element|null} declaration
   *         Optional. The declaration to scroll to.
   * @param  {string} scrollBehavior
   *         Optional. The transition animation when scrolling. If prefers-reduced-motion
   *         system pref is set, then the scroll behavior will be overridden to "instant".
   */

  #scrollToElement(rule, declaration, scrollBehavior = "smooth") {
    let elementToScrollTo = rule;

    if (declaration) {
      const { offsetTop, offsetHeight } = declaration;
      // Get the distance between both the rule and declaration. If the distance is
      // greater than the height of the rule view, then only scroll to the declaration.
      const distance = offsetTop + offsetHeight - rule.offsetTop;

      if (this.element.parentNode.offsetHeight <= distance) {
        elementToScrollTo = declaration;
      }
    }

    // Ensure that smooth scrolling is disabled when the user prefers reduced motion.
    const win = elementToScrollTo.documentGlobal;
    const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
    scrollBehavior = reducedMotion ? "instant" : scrollBehavior;
    elementToScrollTo.scrollIntoView({
      behavior: scrollBehavior,
    });
  }

  /**
   * Returns whether or not the rule is in the view
   *
   * @param  {StyleRuleFront} ruleFront
   * @returns {boolean}
   */

  hasRule(ruleFront) {
    return this.rules.some(r => r.domRule === ruleFront);
  }

  /**
   * Finds the specified TextProperty name in the rule view. If found, scroll to and
   * flash the TextProperty.
   *
   * @param  {string} name
   *         The property name to scroll to and highlight.
   * @param  {object} options
   * @param  {StyleRuleFront|undefined} options.ruleFront
   *         An optional StyleRuleFront. When this is set, we will only look for the property
   *         in this exact rule.
   * @param  {Function|undefined} options.ruleValidator
   *         An optional function that can be used to filter out rules we shouldn't look
   *         into to find the property name. The function is called with a Rule object,
   *         and the rule will be skipped if the function returns a falsy value.
   * @param  {true|undefined} options.focusValue
   *         An optional boolean that indicate that the declaration value should be focused.
   *         If false (the default value), the declaration name will be focused.
   * @returns {boolean} true if the TextProperty name is found, and false otherwise.
   */

  highlightProperty = async (
    name,
    { ruleValidator, ruleFront, focusValue = false } = {}
  ) => {
    // First, let's clear any search we might have, as the property could be hidden
    this.#onClearSearch({ focusSearchField: false });

    if (ruleFront) {
      const rule = this.rules.find(r => r.domRule === ruleFront);
      if (!rule) {
        console.error("Unable to find a rule for actor", ruleFront);
        return false;
      }

      const highlighted = await this.#maybeHighlightPropertyInRule({
        name,
        rule,
        ruleValidator,
        // If ruleFront is passed, we might need to highlight a declaration that is actually
        // overridden  (e.g. when calling this from a  "matched selector" item in the
        // computed panel).
        matchOverridden: true,
        focusValue,
      });
      if (!highlighted) {
        console.error("Unable to highlight rule", name, rule);
        return false;
      }

      return true;
    }

    for (const rule of this.rules) {
      if (
        await this.#maybeHighlightPropertyInRule({
          name,
          rule,
          ruleValidator,
          focusValue,
        })
      ) {
        return true;
      }
    }
    // If the property is a CSS variable and we didn't find its declaration, it might
    // be a registered property
    if (this.#maybeHighlightCssRegisteredProperty(name)) {
      return true;
    }

    return false;
  };

  /**
   * Finds the specified TextProperty name in a specific rule. If found, scroll to it and
   * flash/focus the TextProperty.
   *
   * @param  {object} options
   * @param  {string} options.name
   *         The property name to scroll to and highlight.
   * @param  {Rule} options.rule
   *         The rule to find the property into
   * @param  {Function|undefined} options.ruleValidator
   *         An optional function that can be used to filter out rules we shouldn't look
   *         into to find the property name. The function is called with a Rule object,
   *         and the rule will be skipped if the function returns a falsy value.
   * @param  {boolean|undefined} options.matchOverridden
   *         If true, the declaration will be considered even if it is overridden.
   * @param {string} options.scrollBehavior
   *        An optional string that will be used as `scrollIntoView` `behavior` option.
   *        Note that this will be overridden to "instant" if the user prefers reduced motion.
   * @param  {true|undefined} options.focusValue
   *         An optional boolean that indicate that the declaration value should be focused.
   *         If false (the default value), the declaration name will be focused.
   * @returns {boolean} true if the TextProperty name is found, and false otherwise.
   */

  async #maybeHighlightPropertyInRule({
    name,
    rule,
    ruleValidator,
    matchOverridden = false,
    focusValue = false,
  }) {
    const hasRuleValidator = typeof ruleValidator === "function";
    if (hasRuleValidator && !ruleValidator(rule)) {
      return false;
    }

    let matchingTextPropComputed;
    // hasHigherPriorityThanEarlierProp (that we use in the loop), is expecting
    // the props to be iterated through in reverse.
    const textProps = rule.textProps.toReversed();
    for (const textProp of textProps) {
      for (const computed of textProp.computed) {
        if (
          computed.name === name &&
          !textProp.invisible &&
          textProp.enabled &&
          (!computed.overridden || matchOverridden) &&
          (!matchingTextPropComputed ||
            this.elementStyle.hasHigherPriorityThanEarlierProp(
              computed,
              matchingTextPropComputed
            ))
        ) {
          matchingTextPropComputed = computed;
        }
      }
    }

    if (!matchingTextPropComputed) {
      return false;
    }

    await this.#selectRuleViewIfNeeded();

    let scrollBehavior;

    // If the property is being applied by a pseudo element rule, expand the pseudo
    // element list container.
    if (rule.pseudoElement.length && !this.showPseudoElements) {
      // Set the scroll behavior to "instant" to avoid timing issues between toggling
      // the pseudo element container and scrolling smoothly to the rule.
      scrollBehavior = "instant";
      this.#toggleContainerVisibility(PSEUDO_ELEMENTS_CONTAINER_ID);
    }

    const textProp = matchingTextPropComputed.textProp;

    // If we're jumping to an unused CSS variable, it might not be visible, so show
    // it here.
    if (!textProp.editor && textProp.isUnusedVariable) {
      textProp.rule.editor.showUnusedCssVariable(textProp);
    }

    // If the textProp is a shorthand, we need to expand the computed list so we can
    // highlight the proper element.
    const isTextPropAShorthand =
      textProp.name !== matchingTextPropComputed.name;
    if (isTextPropAShorthand) {
      textProp.editor.expandForFilter();
    }

    this.#highlightElementInRule({
      rule,
      elementToHighlight: isTextPropAShorthand
        ? // if the prop is a shorthand, we hand to highlight the exact longhand property
          matchingTextPropComputed.element
        : textProp.editor.element,
      elementToFocus: focusValue
        ? textProp.editor.valueSpan
        : textProp.editor.nameSpan,
      scrollBehavior,
    });
    return true;
  }

  /**
   * If the passed name matches a registered CSS property highlight it
   *
   * @param {string} name - The name of the registered property to highlight
   * @param {string} scrollBehavior - An optional string that will be used as
   *        `scrollIntoView` `behavior` option.
   *        Note that this will be overridden to "instant" if the user prefers reduced motion.
   * @returns {boolean} Returns true if `name` matched a registered property
   */
  #maybeHighlightCssRegisteredProperty(name, scrollBehavior) {
    if (!name.startsWith("--")) {
      return false;
    }

    // Get a potential @property section
    const entry = this.#containers.get(REGISTERED_PROPERTIES_CONTAINER_ID);
    if (!entry) {
      return false;
    }
    const { header, container } = entry;

    const propertyEl = container.querySelector(`[data-name="${name}"]`);
    if (!propertyEl) {
      return false;
    }

    const toggleButton = header.querySelector("button");
    if (toggleButton.ariaExpanded === "false") {
      this.#toggleContainerVisibility(REGISTERED_PROPERTIES_CONTAINER_ID);
    }

    this.#highlightElementInRule({
      elementToHighlight: propertyEl,
      scrollBehavior,
    });
    return true;
  }

  /**
   * Highlight a given element in a rule editor
   *
   * @param {object} options
   * @param {Rule} options.rule
   *        The rule the element to highlight is in.
   * @param {Element} options.elementToHighlight
   *        The element to highlight.
   * @param {Element} options.elementToFocus
   *        An optional element to focus.
   * @param {string} options.scrollBehavior
   *        An optional string that will be used as `scrollIntoView` `behavior` option.
   *        Note that this will be overridden to "instant" if the user prefers reduced motion.
   */

  async #highlightElementInRule({
    rule,
    elementToHighlight,
    elementToFocus,
    scrollBehavior,
  }) {
    if (rule) {
      this.#scrollToElement(
        rule.editor.selectorText,
        elementToHighlight,
        scrollBehavior
      );
    } else {
      this.#scrollToElement(elementToHighlight, null, scrollBehavior);
    }

    if (elementToFocus) {
      elementToFocus.focus({
        focusVisible: true,
        // Don't scroll when focusing, this is taken care of by the call to #scrollToElement
        preventScroll: true,
      });
    }

    await this.#flashElement(elementToHighlight);
    this.emitForTests("element-highlighted", elementToHighlight);
  }

  /**
   * Returns a Map (keyed by name) of the registered
   * properties for the currently selected node document.
   *
   * @returns Map<String, Object>|null
   */

  getRegisteredPropertiesForSelectedNodeTarget() {
    return this.cssRegisteredPropertiesByTarget.get(
      this.inspector.selection.nodeFront.targetFront
    );
  }

  async #selectRuleViewIfNeeded() {
    // If three-pane mode is enable, or if the rule view is already the currently selected
    // tab, there's no need to do anything.
    if (
      this.inspector.isThreePaneModeEnabled ||
      this.inspector.sidebar.getCurrentTabID() === "ruleview"
    ) {
      return;
    }

    // Otherwise, we need to select the ruleview from the sidebar. This will update the
    // panel, so we need to wait for it be populatedr
    const onRefreshed = this.inspector.once("rule-view-refreshed");
    await this.inspector.sidebar.select("ruleview");
    await onRefreshed;
  }
}

class RuleViewTool {
  constructor(inspector, window) {
    this.inspector = inspector;
    this.document = window.document;

    this.view = new CssRuleView(this.inspector, this.document);

    this.refresh = this.refresh.bind(this);
    this.onDetachedFront = this.onDetachedFront.bind(this);
    this.onPanelSelected = this.onPanelSelected.bind(this);
    this.onDetachedFront = this.onDetachedFront.bind(this);
    this.onSelected = this.onSelected.bind(this);

    this.#abortController = new window.AbortController();
    const { signal } = this.#abortController;
    const baseEventConfig = { signal };

    this.inspector.selection.on(
      "detached-front",
      this.onDetachedFront,
      baseEventConfig
    );
    this.inspector.selection.on(
      "new-node-front",
      this.onSelected,
      baseEventConfig
    );
    this.inspector.selection.on("pseudoclass", this.refresh, baseEventConfig);
    this.inspector.ruleViewSideBar.on(
      "ruleview-selected",
      this.onPanelSelected,
      baseEventConfig
    );
    this.inspector.sidebar.on(
      "ruleview-selected",
      this.onPanelSelected,
      baseEventConfig
    );
    this.inspector.toolbox.on(
      "inspector-selected",
      this.onPanelSelected,
      baseEventConfig
    );
    this.inspector.styleChangeTracker.on(
      "style-changed",
      this.refresh,
      baseEventConfig
    );

    this.inspector.commands.resourceCommand.watchResources(
      [
        this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
        this.inspector.commands.resourceCommand.TYPES.STYLESHEET,
      ],
      {
        onAvailable: this.#onResourceAvailable,
        ignoreExistingResources: true,
      }
    );

    // We do want to get already existing registered properties, so we need to watch
    // them separately
    this.inspector.commands.resourceCommand
      .watchResources(
        [
          this.inspector.commands.resourceCommand.TYPES
            .CSS_REGISTERED_PROPERTIES,
        ],
        {
          onAvailable: this.#onResourceAvailable,
          onUpdated: this.#onResourceUpdated,
          onDestroyed: this.#onResourceDestroyed,
          ignoreExistingResources: false,
        }
      )
      .catch(e => {
        // watchResources is async and even making it's resulting promise part of
        // this.readyPromise still causes test failures, so simply ignore the rejection
        // if the view was already destroyed.
        if (!this.view) {
          return;
        }
        throw e;
      });

    // At the moment `readyPromise` is only consumed in tests (see `openRuleView`) to be
    // notified when the ruleview was first populated to match the initial selected node.
    this.readyPromise = this.onSelected();
  }

  #abortController;

  isPanelVisible() {
    if (!this.view) {
      return false;
    }
    return this.view.isPanelVisible();
  }

  onDetachedFront() {
    this.onSelected(false);
  }

  async onSelected(selectElement = true) {
    // Ignore the event if the view has been destroyed, or if it's inactive.
    // But only if the current selection isn't null. If it's been set to null,
    // let the update go through as this is needed to empty the view on
    // navigation.
    if (!this.view) {
      return;
    }

    const isInactive =
      !this.isPanelVisible() && this.inspector.selection.nodeFront;
    if (isInactive) {
      return;
    }

    if (
      !this.inspector.selection.isConnected() ||
      !this.inspector.selection.isElementNode()
    ) {
      this.view.selectElement(null);
      return;
    }

    if (!selectElement) {
      return;
    }

    await this.view.selectElement(this.inspector.selection.nodeFront);
  }

  refresh() {
    if (this.isPanelVisible()) {
      this.view.refreshPanel();
    }
  }

  #onResourceAvailable = resources => {
    if (!this.inspector) {
      return;
    }

    let hasNewStylesheet = false;
    const addedRegisteredProperties = [];
    for (const resource of resources) {
      if (
        resource.resourceType ===
          this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
        resource.name === "will-navigate"
      ) {
        this.view.cssRegisteredPropertiesByTarget.delete(resource.targetFront);
        if (resource.targetFront.isTopLevel) {
          this.clearUserProperties();
        }
        continue;
      }

      if (
        resource.resourceType ===
          this.inspector.commands.resourceCommand.TYPES.STYLESHEET &&
        // resource.isNew is only true when the stylesheet was added from DevTools,
        // for example when adding a rule in the rule view. In such cases, we're already
        // updating the rule view, so ignore those.
        !resource.isNew
      ) {
        hasNewStylesheet = true;
      }

      if (
        resource.resourceType ===
        this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
      ) {
        if (
          !this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
        ) {
          this.view.cssRegisteredPropertiesByTarget.set(
            resource.targetFront,
            new Map()
          );
        }
        this.view.cssRegisteredPropertiesByTarget
          .get(resource.targetFront)
          .set(resource.name, resource);
        // Only add properties from the same target as the selected node
        if (
          this.view.inspector.selection?.nodeFront?.targetFront ===
          resource.targetFront
        ) {
          addedRegisteredProperties.push(resource);
        }
      }
    }

    if (addedRegisteredProperties.length) {
      // Retrieve @property container
      const registeredPropertiesContainer =
        this.view.getOrCreateRegisteredPropertiesExpandableContainer();

      // Then add all new registered properties
      const names = new Set();
      for (const propertyDefinition of addedRegisteredProperties) {
        const editor = new RegisteredPropertyEditor(
          this.view,
          propertyDefinition
        );
        names.add(propertyDefinition.name);

        // We need to insert the element at the right position so we keep the list of
        // properties alphabetically sorted.
        let referenceNode = null;
        for (const child of registeredPropertiesContainer.children) {
          if (child.getAttribute("data-name") > propertyDefinition.name) {
            referenceNode = child;
            break;
          }
        }
        registeredPropertiesContainer.insertBefore(
          editor.element,
          referenceNode
        );
      }

      // Finally, update textProps that might rely on those new properties
      this.#updateElementStyleRegisteredProperties(names);
    }

    if (hasNewStylesheet) {
      this.refresh();
    }
  };

  #onResourceUpdated = updates => {
    const updatedProperties = [];
    for (const update of updates) {
      if (
        update.resource.resourceType ===
        this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
      ) {
        const { resource } = update;
        if (
          !this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
        ) {
          continue;
        }

        this.view.cssRegisteredPropertiesByTarget
          .get(resource.targetFront)
          .set(resource.name, resource);

        // Only consider properties from the same target as the selected node
        if (
          this.view.inspector.selection?.nodeFront?.targetFront ===
          resource.targetFront
        ) {
          updatedProperties.push(resource);
        }
      }
    }

    const names = new Set();
    if (updatedProperties.length) {
      const registeredPropertiesContainer =
        this.view.styleDocument.getElementById(
          REGISTERED_PROPERTIES_CONTAINER_ID
        );
      for (const resource of updatedProperties) {
        // Replace the existing registered property editor element with a new one,
        // so we don't have to compute which elements should be updated.
        const name = resource.name;
        const el = this.view.getRegisteredPropertyElement(name);
        const editor = new RegisteredPropertyEditor(this.view, resource);
        registeredPropertiesContainer.replaceChild(editor.element, el);

        names.add(resource.name);
      }
      // Finally, update textProps that might rely on those new properties
      this.#updateElementStyleRegisteredProperties(names);
    }
  };

  #onResourceDestroyed = resources => {
    const destroyedPropertiesNames = new Set();
    for (const resource of resources) {
      if (
        resource.resourceType ===
        this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
      ) {
        if (
          !this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
        ) {
          continue;
        }

        const targetRegisteredProperties =
          this.view.cssRegisteredPropertiesByTarget.get(resource.targetFront);
        const resourceName = Array.from(
          targetRegisteredProperties.entries()
        ).find(
          ([_, propDef]) => propDef.resourceId === resource.resourceId
        )?.[0];
        if (!resourceName) {
          continue;
        }

        targetRegisteredProperties.delete(resourceName);

        // Only consider properties from the same target as the selected node
        if (
          this.view.inspector.selection?.nodeFront?.targetFront ===
          resource.targetFront
        ) {
          destroyedPropertiesNames.add(resourceName);
        }
      }
    }
    if (destroyedPropertiesNames.size > 0) {
      for (const name of destroyedPropertiesNames) {
        this.view.getRegisteredPropertyElement(name)?.remove();
      }
      // Finally, update textProps that were relying on those removed properties
      this.#updateElementStyleRegisteredProperties(destroyedPropertiesNames);
    }
  };

  /**
   * Update rules that reference registered properties whose name is in the passed Set,
   * so the `var()` tooltip has up-to-date information.
   *
   * @param {Set<string>} registeredPropertyNames
   */

  #updateElementStyleRegisteredProperties(registeredPropertyNames) {
    if (!this.view.elementStyle) {
      return;
    }
    this.view.elementStyle.onRegisteredPropertiesChange(
      registeredPropertyNames
    );
  }

  clearUserProperties() {
    if (this.view && this.view.store && this.view.store.userProperties) {
      this.view.store.userProperties.clear();
    }
  }

  onPanelSelected() {
    if (this.inspector.selection.nodeFront === this.view.selectedNodeFront) {
      this.refresh();
    } else {
      this.onSelected();
    }
  }

  destroy() {
    if (this.#abortController) {
      this.#abortController.abort();
    }

    this.inspector.commands.resourceCommand.unwatchResources(
      [
        this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
        this.inspector.commands.resourceCommand.TYPES.STYLESHEET,
      ],
      {
        onAvailable: this.#onResourceAvailable,
      }
    );

    this.inspector.commands.resourceCommand.unwatchResources(
      [this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES],
      {
        onAvailable: this.#onResourceAvailable,
        onUpdated: this.#onResourceUpdated,
        onDestroyed: this.#onResourceDestroyed,
      }
    );

    this.view.destroy();

    this.view =
      this.document =
      this.inspector =
      this.readyPromise =
      this.store =
      this.#abortController =
        null;
  }
}

exports.CssRuleView = CssRuleView;
exports.RuleViewTool = RuleViewTool;

Messung V0.5 in Prozent
C=88 H=95 G=91

¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.82Angebot  (Wie Sie bei der Firma Beratungs- und Dienstleistungen beauftragen können 2026-08-26) ¤

*Eine klare Vorstellung vom Zielzustand






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=752002