tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

rules.js (90009B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const flags = require("resource://devtools/shared/flags.js");
      8 const { l10n } = require("resource://devtools/shared/inspector/css-logic.js");
      9 const {
     10  style: { ELEMENT_STYLE },
     11 } = require("resource://devtools/shared/constants.js");
     12 const {
     13  PSEUDO_CLASSES,
     14 } = require("resource://devtools/shared/css/constants.js");
     15 const OutputParser = require("resource://devtools/client/shared/output-parser.js");
     16 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
     17 const ElementStyle = require("resource://devtools/client/inspector/rules/models/element-style.js");
     18 const RuleEditor = require("resource://devtools/client/inspector/rules/views/rule-editor.js");
     19 const RegisteredPropertyEditor = require("resource://devtools/client/inspector/rules/views/registered-property-editor.js");
     20 const TooltipsOverlay = require("resource://devtools/client/inspector/shared/tooltips-overlay.js");
     21 const {
     22  createChild,
     23  promiseWarn,
     24 } = require("resource://devtools/client/inspector/shared/utils.js");
     25 const { debounce } = require("resource://devtools/shared/debounce.js");
     26 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     27 
     28 loader.lazyRequireGetter(
     29  this,
     30  ["flashElementOn", "flashElementOff"],
     31  "resource://devtools/client/inspector/markup/utils.js",
     32  true
     33 );
     34 loader.lazyRequireGetter(
     35  this,
     36  "ClassListPreviewer",
     37  "resource://devtools/client/inspector/rules/views/class-list-previewer.js"
     38 );
     39 loader.lazyRequireGetter(
     40  this,
     41  ["getNodeInfo", "getNodeCompatibilityInfo", "getRuleFromNode"],
     42  "resource://devtools/client/inspector/rules/utils/utils.js",
     43  true
     44 );
     45 loader.lazyRequireGetter(
     46  this,
     47  "StyleInspectorMenu",
     48  "resource://devtools/client/inspector/shared/style-inspector-menu.js"
     49 );
     50 loader.lazyRequireGetter(
     51  this,
     52  "AutocompletePopup",
     53  "resource://devtools/client/shared/autocomplete-popup.js"
     54 );
     55 loader.lazyRequireGetter(
     56  this,
     57  "KeyShortcuts",
     58  "resource://devtools/client/shared/key-shortcuts.js"
     59 );
     60 loader.lazyRequireGetter(
     61  this,
     62  "clipboardHelper",
     63  "resource://devtools/shared/platform/clipboard.js"
     64 );
     65 loader.lazyRequireGetter(
     66  this,
     67  "openContentLink",
     68  "resource://devtools/client/shared/link.js",
     69  true
     70 );
     71 
     72 const lazy = {};
     73 ChromeUtils.defineESModuleGetters(lazy, {
     74  AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
     75 });
     76 
     77 const HTML_NS = "http://www.w3.org/1999/xhtml";
     78 const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
     79 const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
     80 const PREF_DRAGGABLE = "devtools.inspector.draggable_properties";
     81 const PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER =
     82  "devtools.inspector.rule-view.focusNextOnEnter";
     83 const FILTER_CHANGED_TIMEOUT = 150;
     84 // Removes the flash-out class from an element after 1 second (100ms in tests so they
     85 // don't take too long to run).
     86 const PROPERTY_FLASHING_DURATION = flags.testing ? 100 : 1000;
     87 
     88 // This is used to parse user input when filtering.
     89 const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
     90 // This is used to parse the filter search value to see if the filter
     91 // should be strict or not
     92 const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
     93 
     94 const RULE_VIEW_HEADER_CLASSNAME = "ruleview-header";
     95 const PSEUDO_ELEMENTS_CONTAINER_ID = "pseudo-elements-container";
     96 const REGISTERED_PROPERTIES_CONTAINER_ID = "registered-properties-container";
     97 const POSITION_TRY_CONTAINER_ID = "position-try-container";
     98 
     99 /**
    100 * Our model looks like this:
    101 *
    102 * ElementStyle:
    103 *   Responsible for keeping track of which properties are overridden.
    104 *   Maintains a list of Rule objects that apply to the element.
    105 * Rule:
    106 *   Manages a single style declaration or rule.
    107 *   Responsible for applying changes to the properties in a rule.
    108 *   Maintains a list of TextProperty objects.
    109 * TextProperty:
    110 *   Manages a single property from the authoredText attribute of the
    111 *     relevant declaration.
    112 *   Maintains a list of computed properties that come from this
    113 *     property declaration.
    114 *   Changes to the TextProperty are sent to its related Rule for
    115 *     application.
    116 *
    117 * View hierarchy mostly follows the model hierarchy.
    118 *
    119 * CssRuleView:
    120 *   Owns an ElementStyle and creates a list of RuleEditors for its
    121 *    Rules.
    122 * RuleEditor:
    123 *   Owns a Rule object and creates a list of TextPropertyEditors
    124 *     for its TextProperties.
    125 *   Manages creation of new text properties.
    126 * TextPropertyEditor:
    127 *   Owns a TextProperty object.
    128 *   Manages changes to the TextProperty.
    129 *   Can be expanded to display computed properties.
    130 *   Can mark a property disabled or enabled.
    131 */
    132 
    133 /**
    134 * CssRuleView is a view of the style rules and declarations that
    135 * apply to a given element.  After construction, the 'element'
    136 * property will be available with the user interface.
    137 */
    138 class CssRuleView extends EventEmitter {
    139  /**
    140   * @param {Inspector} inspector
    141   *        Inspector toolbox panel
    142   * @param {Document} document
    143   *        The document that will contain the rule view.
    144   * @param {object} store
    145   *        The CSS rule view can use this object to store metadata
    146   *        that might outlast the rule view, particularly the current
    147   *        set of disabled properties.
    148   */
    149  constructor(inspector, document, store) {
    150    super();
    151 
    152    this.inspector = inspector;
    153    this.cssProperties = inspector.cssProperties;
    154    this.styleDocument = document;
    155    this.styleWindow = this.styleDocument.defaultView;
    156    this.store = store || {
    157      expandedUnusedCustomCssPropertiesRuleActorIds: new Set(),
    158    };
    159 
    160    // Allow tests to override debouncing behavior, as this can cause intermittents.
    161    this.debounce = debounce;
    162 
    163    // Variable used to stop the propagation of mouse events to children
    164    // when we are updating a value by dragging the mouse and we then release it
    165    this.childHasDragged = false;
    166 
    167    this._outputParser = new OutputParser(document, this.cssProperties);
    168    this._abortController = new this.styleWindow.AbortController();
    169 
    170    this.addNewRule = this.addNewRule.bind(this);
    171    this._onContextMenu = this._onContextMenu.bind(this);
    172    this._onCopy = this._onCopy.bind(this);
    173    this._onFilterStyles = this._onFilterStyles.bind(this);
    174    this._onClearSearch = this._onClearSearch.bind(this);
    175    this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
    176    this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
    177    this._onToggleClassPanel = this._onToggleClassPanel.bind(this);
    178    this._onToggleLightColorSchemeSimulation =
    179      this._onToggleLightColorSchemeSimulation.bind(this);
    180    this._onToggleDarkColorSchemeSimulation =
    181      this._onToggleDarkColorSchemeSimulation.bind(this);
    182    this._onTogglePrintSimulation = this._onTogglePrintSimulation.bind(this);
    183    this.highlightProperty = this.highlightProperty.bind(this);
    184    this.refreshPanel = this.refreshPanel.bind(this);
    185 
    186    const doc = this.styleDocument;
    187    // Delegate bulk handling of events happening within the DOM tree of the Rules view
    188    // to this.handleEvent(). Listening on the capture phase of the event bubbling to be
    189    // able to stop event propagation on a case-by-case basis and prevent event target
    190    // ancestor nodes from handling them.
    191    this.styleDocument.addEventListener("click", this, { capture: true });
    192    this.element = doc.getElementById("ruleview-container-focusable");
    193    this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
    194    this.searchField = doc.getElementById("ruleview-searchbox");
    195    this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
    196    this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
    197    this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
    198    this.classPanel = doc.getElementById("ruleview-class-panel");
    199    this.classToggle = doc.getElementById("class-panel-toggle");
    200    this.colorSchemeLightSimulationButton = doc.getElementById(
    201      "color-scheme-simulation-light-toggle"
    202    );
    203    this.colorSchemeDarkSimulationButton = doc.getElementById(
    204      "color-scheme-simulation-dark-toggle"
    205    );
    206    this.printSimulationButton = doc.getElementById("print-simulation-toggle");
    207 
    208    this._initSimulationFeatures();
    209 
    210    this.searchClearButton.hidden = true;
    211 
    212    this.onHighlighterShown = data =>
    213      this.handleHighlighterEvent("highlighter-shown", data);
    214    this.onHighlighterHidden = data =>
    215      this.handleHighlighterEvent("highlighter-hidden", data);
    216    this.inspector.highlighters.on(
    217      "highlighter-shown",
    218      this.onHighlighterShown
    219    );
    220    this.inspector.highlighters.on(
    221      "highlighter-hidden",
    222      this.onHighlighterHidden
    223    );
    224 
    225    this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
    226    this._onShortcut = this._onShortcut.bind(this);
    227    this.shortcuts.on("Escape", event => this._onShortcut("Escape", event));
    228    this.shortcuts.on("Return", event => this._onShortcut("Return", event));
    229    this.shortcuts.on("Space", event => this._onShortcut("Space", event));
    230    this.shortcuts.on("CmdOrCtrl+F", event =>
    231      this._onShortcut("CmdOrCtrl+F", event)
    232    );
    233    this.element.addEventListener("copy", this._onCopy);
    234    this.element.addEventListener("contextmenu", this._onContextMenu);
    235    this.addRuleButton.addEventListener("click", this.addNewRule);
    236    this.searchField.addEventListener("input", this._onFilterStyles);
    237    this.searchClearButton.addEventListener("click", this._onClearSearch);
    238    this.pseudoClassToggle.addEventListener(
    239      "click",
    240      this._onTogglePseudoClassPanel
    241    );
    242    this.classToggle.addEventListener("click", this._onToggleClassPanel);
    243    // The "change" event bubbles up from checkbox inputs nested within the panel container.
    244    this.pseudoClassPanel.addEventListener("change", this._onTogglePseudoClass);
    245 
    246    if (flags.testing) {
    247      // In tests, we start listening immediately to avoid having to simulate a mousemove.
    248      this.highlighters.addToView(this);
    249    } else {
    250      this.element.addEventListener(
    251        "mousemove",
    252        () => {
    253          this.highlighters.addToView(this);
    254        },
    255        { once: true }
    256      );
    257    }
    258 
    259    this._handlePrefChange = this._handlePrefChange.bind(this);
    260    this._handleUAStylePrefChange = this._handleUAStylePrefChange.bind(this);
    261    this._handleDefaultColorUnitPrefChange =
    262      this._handleDefaultColorUnitPrefChange.bind(this);
    263    this._handleDraggablePrefChange =
    264      this._handleDraggablePrefChange.bind(this);
    265    this._handleInplaceEditorFocusNextOnEnterPrefChange =
    266      this._handleInplaceEditorFocusNextOnEnterPrefChange.bind(this);
    267 
    268    this._prefObserver = new PrefObserver("devtools.");
    269    this._prefObserver.on(PREF_UA_STYLES, this._handleUAStylePrefChange);
    270    this._prefObserver.on(
    271      PREF_DEFAULT_COLOR_UNIT,
    272      this._handleDefaultColorUnitPrefChange
    273    );
    274    this._prefObserver.on(PREF_DRAGGABLE, this._handleDraggablePrefChange);
    275    // Initialize value of this.draggablePropertiesEnabled
    276    this._handleDraggablePrefChange();
    277 
    278    this._prefObserver.on(
    279      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
    280      this._handleInplaceEditorFocusNextOnEnterPrefChange
    281    );
    282    // Initialize value of this.inplaceEditorFocusNextOnEnter
    283    this._handleInplaceEditorFocusNextOnEnterPrefChange();
    284 
    285    this.pseudoClassCheckboxes = this._createPseudoClassCheckboxes();
    286    this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
    287 
    288    // Add the tooltips and highlighters to the view
    289    this.tooltips = new TooltipsOverlay(this);
    290 
    291    this.cssRegisteredPropertiesByTarget = new Map();
    292    this._elementsWithPendingClicks = new this.styleWindow.WeakSet();
    293  }
    294 
    295  // The element that we're inspecting.
    296  _viewedElement = null;
    297 
    298  // Used for cancelling timeouts in the style filter.
    299  _filterChangedTimeout = null;
    300 
    301  // Empty, unconnected element of the same type as this node, used
    302  // to figure out how shorthand properties will be parsed.
    303  _dummyElement = null;
    304 
    305  get popup() {
    306    if (!this._popup) {
    307      // The popup will be attached to the toolbox document.
    308      this._popup = new AutocompletePopup(this.inspector.toolbox.doc, {
    309        autoSelect: true,
    310      });
    311    }
    312 
    313    return this._popup;
    314  }
    315 
    316  get classListPreviewer() {
    317    if (!this._classListPreviewer) {
    318      this._classListPreviewer = new ClassListPreviewer(
    319        this.inspector,
    320        this.classPanel
    321      );
    322    }
    323 
    324    return this._classListPreviewer;
    325  }
    326 
    327  get contextMenu() {
    328    if (!this._contextMenu) {
    329      this._contextMenu = new StyleInspectorMenu(this, { isRuleView: true });
    330    }
    331 
    332    return this._contextMenu;
    333  }
    334 
    335  // Get the dummy elemenet.
    336  get dummyElement() {
    337    return this._dummyElement;
    338  }
    339 
    340  // Get the highlighters overlay from the Inspector.
    341  get highlighters() {
    342    if (!this._highlighters) {
    343      // highlighters is a lazy getter in the inspector.
    344      this._highlighters = this.inspector.highlighters;
    345    }
    346 
    347    return this._highlighters;
    348  }
    349 
    350  // Get the filter search value.
    351  get searchValue() {
    352    return this.searchField.value.toLowerCase();
    353  }
    354 
    355  get rules() {
    356    return this._elementStyle ? this._elementStyle.rules : [];
    357  }
    358 
    359  get currentTarget() {
    360    return this.inspector.toolbox.target;
    361  }
    362 
    363  /**
    364   * Highlight/unhighlight all the nodes that match a given rule's selector
    365   * inside the document of the current selected node.
    366   * Only one selector can be highlighted at a time, so calling the method a
    367   * second time with a different rule will first unhighlight the previously
    368   * highlighted nodes.
    369   * Calling the method a second time with the same rule will just
    370   * unhighlight the highlighted nodes.
    371   *
    372   * @param {Rule} rule
    373   * @param {string} selector
    374   *        Elements matching this selector will be highlighted on the page.
    375   * @param {boolean} highlightFromRulesSelector
    376   */
    377  async toggleSelectorHighlighter(
    378    rule,
    379    selector,
    380    highlightFromRulesSelector = true
    381  ) {
    382    if (this.isSelectorHighlighted(selector)) {
    383      await this.inspector.highlighters.hideHighlighterType(
    384        this.inspector.highlighters.TYPES.SELECTOR
    385      );
    386    } else {
    387      const options = {
    388        hideInfoBar: true,
    389        hideGuides: true,
    390        // we still pass the selector (which can be the StyleRuleFront#computedSelector)
    391        // even if highlightFromRulesSelector is set to true, as it's how we keep track
    392        // of which selector is highlighted.
    393        selector,
    394      };
    395      if (highlightFromRulesSelector) {
    396        options.ruleActorID = rule.domRule.actorID;
    397      }
    398      await this.inspector.highlighters.showHighlighterTypeForNode(
    399        this.inspector.highlighters.TYPES.SELECTOR,
    400        this.inspector.selection.nodeFront,
    401        options
    402      );
    403    }
    404  }
    405 
    406  isPanelVisible() {
    407    return (
    408      this.inspector.toolbox &&
    409      this.inspector.sidebar &&
    410      this.inspector.toolbox.currentToolId === "inspector" &&
    411      (this.inspector.sidebar.getCurrentTabID() == "ruleview" ||
    412        this.inspector.isThreePaneModeEnabled)
    413    );
    414  }
    415 
    416  /**
    417   * Check whether a SelectorHighlighter is active for the given selector text.
    418   *
    419   * @param {string} selector
    420   * @return {boolean}
    421   */
    422  isSelectorHighlighted(selector) {
    423    const options = this.inspector.highlighters.getOptionsForActiveHighlighter(
    424      this.inspector.highlighters.TYPES.SELECTOR
    425    );
    426 
    427    return options?.selector === selector;
    428  }
    429 
    430  /**
    431   * Delegate handler for events happening within the DOM tree of the Rules view.
    432   * Itself delegates to specific handlers by event type.
    433   *
    434   * Use this instead of attaching specific event handlers when:
    435   * - there are many elements with the same event handler (eases memory pressure)
    436   * - you want to avoid having to remove event handlers manually
    437   * - elements are added/removed from the DOM tree arbitrarily over time
    438   *
    439   * @param {MouseEvent|UIEvent} event
    440   */
    441  handleEvent(event) {
    442    if (this.childHasDragged) {
    443      this.childHasDragged = false;
    444      event.stopPropagation();
    445      return;
    446    }
    447    switch (event.type) {
    448      case "click":
    449        this.handleClickEvent(event);
    450        break;
    451      default:
    452    }
    453  }
    454 
    455  /**
    456   * Delegate handler for click events happening within the DOM tree of the Rules view.
    457   * Stop propagation of click event wrapping a CSS rule or CSS declaration to avoid
    458   * triggering the prompt to add a new CSS declaration or to edit the existing one.
    459   *
    460   * @param {MouseEvent} event
    461   */
    462  async handleClickEvent(event) {
    463    const target = event.target;
    464 
    465    // Handle click on the icon next to a CSS selector.
    466    if (target.classList.contains("js-toggle-selector-highlighter")) {
    467      event.stopPropagation();
    468      let selector = target.dataset.computedSelector;
    469      const highlightFromRulesSelector =
    470        !!selector && !target.dataset.isUniqueSelector;
    471      // dataset.computedSelector will be initially empty for inline styles (inherited or not)
    472      // Rules associated with a regular selector should have this data-attribute
    473      // set in devtools/client/inspector/rules/views/rule-editor.js
    474      const rule = getRuleFromNode(target, this._elementStyle);
    475      if (selector === "") {
    476        try {
    477          if (rule.inherited) {
    478            // This is an inline style from an inherited rule. Need to resolve the
    479            // unique selector from the node which this rule is inherited from.
    480            selector = await rule.inherited.getUniqueSelector();
    481          } else {
    482            // This is an inline style from the current node.
    483            selector =
    484              await this.inspector.selection.nodeFront.getUniqueSelector();
    485          }
    486 
    487          // Now that the selector was computed, we can store it for subsequent usage.
    488          target.dataset.computedSelector = selector;
    489          target.dataset.isUniqueSelector = true;
    490        } finally {
    491          // Could not resolve a unique selector for the inline style.
    492        }
    493      }
    494 
    495      this.toggleSelectorHighlighter(
    496        rule,
    497        selector,
    498        highlightFromRulesSelector
    499      );
    500    }
    501 
    502    // Handle click on swatches next to flex and inline-flex CSS properties
    503    if (target.classList.contains("js-toggle-flexbox-highlighter")) {
    504      event.stopPropagation();
    505      this.inspector.highlighters.toggleFlexboxHighlighter(
    506        this.inspector.selection.nodeFront,
    507        "rule"
    508      );
    509    }
    510 
    511    // Handle click on swatches next to grid CSS properties
    512    if (target.classList.contains("js-toggle-grid-highlighter")) {
    513      event.stopPropagation();
    514      this.inspector.highlighters.toggleGridHighlighter(
    515        this.inspector.selection.nodeFront,
    516        "rule"
    517      );
    518    }
    519 
    520    const valueSpan = target.closest(".ruleview-propertyvalue");
    521    if (valueSpan) {
    522      if (this._elementsWithPendingClicks.has(valueSpan)) {
    523        // When we start handling a drag in the TextPropertyEditor valueSpan,
    524        // we make the valueSpan capture the pointer. Then, `click` event target is always
    525        // the valueSpan with the latest spec of Pointer Events.
    526        // Therefore, we should stop immediate propagation of the `click` event
    527        // if we've handled a drag to prevent moving focus to the inplace editor.
    528        event.stopImmediatePropagation();
    529        return;
    530      }
    531 
    532      // Handle link click in RuleEditor property value
    533      if (target.nodeName === "a") {
    534        event.stopPropagation();
    535        event.preventDefault();
    536        openContentLink(target.href, {
    537          relatedToCurrent: true,
    538          inBackground:
    539            event.button === 1 ||
    540            (lazy.AppConstants.platform === "macosx"
    541              ? event.metaKey
    542              : event.ctrlKey),
    543        });
    544      }
    545    }
    546  }
    547 
    548  /**
    549   * Delegate handler for highlighter events.
    550   *
    551   * This is the place to observe for highlighter events, check the highlighter type and
    552   * event name, then react to specific events, for example by modifying the DOM.
    553   *
    554   * @param {string} eventName
    555   *        Highlighter event name. One of: "highlighter-hidden", "highlighter-shown"
    556   * @param {object} data
    557   *        Object with data associated with the highlighter event.
    558   */
    559  handleHighlighterEvent(eventName, data) {
    560    switch (data.type) {
    561      // Toggle the "highlighted" class on selector icons in the Rules view when
    562      // the SelectorHighlighter is shown/hidden for a certain CSS selector.
    563      case this.inspector.highlighters.TYPES.SELECTOR:
    564        {
    565          const selector = data?.options?.selector;
    566          if (!selector) {
    567            return;
    568          }
    569 
    570          const query = `.js-toggle-selector-highlighter[data-computed-selector='${selector}']`;
    571          for (const node of this.styleDocument.querySelectorAll(query)) {
    572            const isHighlighterDisplayed = eventName == "highlighter-shown";
    573            node.classList.toggle("highlighted", isHighlighterDisplayed);
    574            node.setAttribute("aria-pressed", isHighlighterDisplayed);
    575          }
    576        }
    577        break;
    578 
    579      // Toggle the "aria-pressed" attribute on swatches next to flex and inline-flex CSS properties
    580      // when the FlexboxHighlighter is shown/hidden for the currently selected node.
    581      case this.inspector.highlighters.TYPES.FLEXBOX:
    582        {
    583          const query = ".js-toggle-flexbox-highlighter";
    584          for (const node of this.styleDocument.querySelectorAll(query)) {
    585            node.setAttribute("aria-pressed", eventName == "highlighter-shown");
    586          }
    587        }
    588        break;
    589 
    590      // Toggle the "aria-pressed" class on swatches next to grid CSS properties
    591      // when the GridHighlighter is shown/hidden for the currently selected node.
    592      case this.inspector.highlighters.TYPES.GRID:
    593        {
    594          const query = ".js-toggle-grid-highlighter";
    595          for (const node of this.styleDocument.querySelectorAll(query)) {
    596            // From the Layout panel, we can toggle grid highlighters for nodes which are
    597            // not currently selected. The Rules view shows `display: grid` declarations
    598            // only for the selected node. Avoid mistakenly marking them as "active".
    599            if (data.nodeFront === this.inspector.selection.nodeFront) {
    600              node.setAttribute(
    601                "aria-pressed",
    602                eventName == "highlighter-shown"
    603              );
    604            }
    605 
    606            // When the max limit of grid highlighters is reached (default 3),
    607            // mark inactive grid swatches as disabled.
    608            node.toggleAttribute(
    609              "disabled",
    610              !this.inspector.highlighters.canGridHighlighterToggle(
    611                this.inspector.selection.nodeFront
    612              )
    613            );
    614          }
    615        }
    616        break;
    617    }
    618  }
    619 
    620  /**
    621   * Enables the print and color scheme simulation only for local and remote tab debugging.
    622   */
    623  async _initSimulationFeatures() {
    624    if (!this.inspector.commands.descriptorFront.isTabDescriptor) {
    625      return;
    626    }
    627    this.colorSchemeLightSimulationButton.removeAttribute("hidden");
    628    this.colorSchemeDarkSimulationButton.removeAttribute("hidden");
    629    this.printSimulationButton.removeAttribute("hidden");
    630    this.printSimulationButton.addEventListener(
    631      "click",
    632      this._onTogglePrintSimulation
    633    );
    634    this.colorSchemeLightSimulationButton.addEventListener(
    635      "click",
    636      this._onToggleLightColorSchemeSimulation
    637    );
    638    this.colorSchemeDarkSimulationButton.addEventListener(
    639      "click",
    640      this._onToggleDarkColorSchemeSimulation
    641    );
    642    const { rfpCSSColorScheme } = this.inspector.walker;
    643    if (rfpCSSColorScheme) {
    644      this.colorSchemeLightSimulationButton.setAttribute("disabled", true);
    645      this.colorSchemeDarkSimulationButton.setAttribute("disabled", true);
    646      console.warn("Color scheme simulation is disabled in RFP mode.");
    647    }
    648  }
    649 
    650  /**
    651   * Get the type of a given node in the rule-view
    652   *
    653   * @param {DOMNode} node
    654   *        The node which we want information about
    655   * @return {object | null} containing the following props:
    656   * - type {String} One of the VIEW_NODE_XXX_TYPE const in
    657   *   client/inspector/shared/node-types.
    658   * - rule {Rule} The Rule object.
    659   * - value {Object} Depends on the type of the node.
    660   * Otherwise, returns null if the node isn't anything we care about.
    661   */
    662  getNodeInfo(node) {
    663    return getNodeInfo(node, this._elementStyle);
    664  }
    665 
    666  /**
    667   * Get the node's compatibility issues
    668   *
    669   * @param {DOMNode} node
    670   *        The node which we want information about
    671   * @return {object | null} containing the following props:
    672   * - type {String} Compatibility issue type.
    673   * - property {string} The incompatible rule
    674   * - alias {Array} The browser specific alias of rule
    675   * - url {string} Link to MDN documentation
    676   * - deprecated {bool} True if the rule is deprecated
    677   * - experimental {bool} True if rule is experimental
    678   * - unsupportedBrowsers {Array} Array of unsupported browser
    679   * Otherwise, returns null if the node has cross-browser compatible CSS
    680   */
    681  async getNodeCompatibilityInfo(node) {
    682    const compatibilityInfo = await getNodeCompatibilityInfo(
    683      node,
    684      this._elementStyle
    685    );
    686 
    687    return compatibilityInfo;
    688  }
    689 
    690  /**
    691   * Context menu handler.
    692   */
    693  _onContextMenu(event) {
    694    if (
    695      event.originalTarget.closest("input[type=text]") ||
    696      event.originalTarget.closest("input:not([type])") ||
    697      event.originalTarget.closest("textarea")
    698    ) {
    699      return;
    700    }
    701 
    702    event.stopPropagation();
    703    event.preventDefault();
    704 
    705    this.contextMenu.show(event);
    706  }
    707 
    708  /**
    709   * Callback for copy event. Copy the selected text.
    710   *
    711   * @param {Event} event
    712   *        copy event object.
    713   */
    714  _onCopy(event) {
    715    if (event) {
    716      this.copySelection(event.target);
    717      event.preventDefault();
    718      event.stopPropagation();
    719    }
    720  }
    721 
    722  /**
    723   * Copy the current selection. The current target is necessary
    724   * if the selection is inside an input or a textarea
    725   *
    726   * @param {DOMNode} target
    727   *        DOMNode target of the copy action
    728   */
    729  copySelection(target) {
    730    try {
    731      let text = "";
    732 
    733      const nodeName = target?.nodeName;
    734      const targetType = target?.type;
    735 
    736      if (
    737        // The target can be the enable/disable rule checkbox here (See Bug 1680893).
    738        (nodeName === "input" && targetType !== "checkbox") ||
    739        nodeName == "textarea"
    740      ) {
    741        const start = Math.min(target.selectionStart, target.selectionEnd);
    742        const end = Math.max(target.selectionStart, target.selectionEnd);
    743        const count = end - start;
    744        text = target.value.substr(start, count);
    745      } else {
    746        text = this.styleWindow.getSelection().toString();
    747 
    748        // Remove any double newlines.
    749        text = text.replace(/(\r?\n)\r?\n/g, "$1");
    750      }
    751 
    752      clipboardHelper.copyString(text);
    753    } catch (e) {
    754      console.error(e);
    755    }
    756  }
    757 
    758  /**
    759   * Add a new rule to the current element.
    760   */
    761  addNewRule() {
    762    const elementStyle = this._elementStyle;
    763    const element = elementStyle.element;
    764    const pseudoClasses = element.pseudoClassLocks;
    765 
    766    // Clear the search input so the new rule is visible
    767    this._onClearSearch();
    768 
    769    this._focusNextUserAddedRule = true;
    770    this.pageStyle.addNewRule(element, pseudoClasses);
    771  }
    772 
    773  /**
    774   * Returns true if the "Add Rule" action (either via the addRuleButton or the context
    775   * menu entry) can be performed for the currently selected node.
    776   *
    777   * @returns {boolean}
    778   */
    779  canAddNewRuleForSelectedNode() {
    780    return this._viewedElement && this.inspector.selection.isElementNode();
    781  }
    782 
    783  /**
    784   * Disables add rule button when needed
    785   */
    786  refreshAddRuleButtonState() {
    787    this.addRuleButton.disabled = !this.canAddNewRuleForSelectedNode();
    788  }
    789 
    790  /**
    791   * Return {Boolean} true if the rule view currently has an input
    792   * editor visible.
    793   */
    794  get isEditing() {
    795    return (
    796      this.tooltips.isEditing ||
    797      !!this.element.querySelectorAll(".styleinspector-propertyeditor").length
    798    );
    799  }
    800 
    801  _handleUAStylePrefChange() {
    802    this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
    803    this._handlePrefChange(PREF_UA_STYLES);
    804  }
    805 
    806  _handleDefaultColorUnitPrefChange() {
    807    this._handlePrefChange(PREF_DEFAULT_COLOR_UNIT);
    808  }
    809 
    810  _handleDraggablePrefChange() {
    811    this.draggablePropertiesEnabled = Services.prefs.getBoolPref(
    812      PREF_DRAGGABLE,
    813      false
    814    );
    815    // This event is consumed by text-property-editor instances in order to
    816    // update their draggable behavior. Preferences observer are costly, so
    817    // we are forwarding the preference update via the EventEmitter.
    818    this.emit("draggable-preference-updated");
    819  }
    820 
    821  _handleInplaceEditorFocusNextOnEnterPrefChange() {
    822    this.inplaceEditorFocusNextOnEnter = Services.prefs.getBoolPref(
    823      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
    824      false
    825    );
    826    this._handlePrefChange(PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER);
    827  }
    828 
    829  _handlePrefChange(pref) {
    830    // Reselect the currently selected element
    831    const refreshOnPrefs = [
    832      PREF_UA_STYLES,
    833      PREF_DEFAULT_COLOR_UNIT,
    834      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
    835    ];
    836    if (this._viewedElement && refreshOnPrefs.includes(pref)) {
    837      this.selectElement(this._viewedElement, true);
    838    }
    839  }
    840 
    841  /**
    842   * Set the filter style search value.
    843   *
    844   * @param {string} value
    845   *        The search value.
    846   */
    847  setFilterStyles(value = "") {
    848    this.searchField.value = value;
    849    this.searchField.focus();
    850    this._onFilterStyles();
    851  }
    852 
    853  /**
    854   * Called when the user enters a search term in the filter style search box.
    855   * The actual filtering (done in _doFilterStyles) will be throttled if the search input
    856   * isn't empty, but will happen immediately when the search gets cleared.
    857   */
    858  _onFilterStyles() {
    859    if (this._filterChangedTimeout) {
    860      clearTimeout(this._filterChangedTimeout);
    861    }
    862 
    863    const isSearchEmpty = this.searchValue.length === 0;
    864    this.searchClearButton.hidden = isSearchEmpty;
    865 
    866    // If the search is cleared update the UI directly so calls to this function (or any
    867    // callsite of it) can assume the UI is up to date directly after the call.
    868    if (isSearchEmpty) {
    869      this._doFilterStyles();
    870    } else {
    871      this._filterChangedTimeout = setTimeout(
    872        () => this._doFilterStyles(),
    873        FILTER_CHANGED_TIMEOUT
    874      );
    875    }
    876  }
    877 
    878  /**
    879   * Actually update search data and update the UI to reflect the current search.
    880   *
    881   * @fires ruleview-filtered
    882   */
    883  _doFilterStyles() {
    884    this.searchData = {
    885      searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue),
    886      searchPropertyName: this.searchValue,
    887      searchPropertyValue: this.searchValue,
    888      strictSearchValue: "",
    889      strictSearchPropertyName: false,
    890      strictSearchPropertyValue: false,
    891      strictSearchAllValues: false,
    892    };
    893 
    894    if (this.searchData.searchPropertyMatch) {
    895      // Parse search value as a single property line and extract the
    896      // property name and value. If the parsed property name or value is
    897      // contained in backquotes (`), extract the value within the backquotes
    898      // and set the corresponding strict search for the property to true.
    899      if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) {
    900        this.searchData.strictSearchPropertyName = true;
    901        this.searchData.searchPropertyName = FILTER_STRICT_RE.exec(
    902          this.searchData.searchPropertyMatch[1]
    903        )[1];
    904      } else {
    905        this.searchData.searchPropertyName =
    906          this.searchData.searchPropertyMatch[1];
    907      }
    908 
    909      if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) {
    910        this.searchData.strictSearchPropertyValue = true;
    911        this.searchData.searchPropertyValue = FILTER_STRICT_RE.exec(
    912          this.searchData.searchPropertyMatch[2]
    913        )[1];
    914      } else {
    915        this.searchData.searchPropertyValue =
    916          this.searchData.searchPropertyMatch[2];
    917      }
    918 
    919      // Strict search for stylesheets will match the property line regex.
    920      // Extract the search value within the backquotes to be used
    921      // in the strict search for stylesheets in _highlightStyleSheet.
    922      if (FILTER_STRICT_RE.test(this.searchValue)) {
    923        this.searchData.strictSearchValue = FILTER_STRICT_RE.exec(
    924          this.searchValue
    925        )[1];
    926      }
    927    } else if (FILTER_STRICT_RE.test(this.searchValue)) {
    928      // If the search value does not correspond to a property line and
    929      // is contained in backquotes, extract the search value within the
    930      // backquotes and set the flag to perform a strict search for all
    931      // the values (selector, stylesheet, property and computed values).
    932      const searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1];
    933      this.searchData.strictSearchAllValues = true;
    934      this.searchData.searchPropertyName = searchValue;
    935      this.searchData.searchPropertyValue = searchValue;
    936      this.searchData.strictSearchValue = searchValue;
    937    }
    938 
    939    this._clearHighlight(this.element);
    940    this._clearRules();
    941    this._createEditors();
    942 
    943    this.inspector.emit("ruleview-filtered");
    944 
    945    this._filterChangeTimeout = null;
    946  }
    947 
    948  /**
    949   * Called when the user clicks on the clear button in the filter style search
    950   * box. Returns true if the search box is cleared and false otherwise.
    951   */
    952  _onClearSearch() {
    953    if (this.searchField.value) {
    954      this.setFilterStyles("");
    955      return true;
    956    }
    957 
    958    return false;
    959  }
    960 
    961  destroy() {
    962    this.isDestroyed = true;
    963    this.clear();
    964 
    965    this._dummyElement = null;
    966    // off handlers must have the same reference as their on handlers
    967    this._prefObserver.off(PREF_UA_STYLES, this._handleUAStylePrefChange);
    968    this._prefObserver.off(
    969      PREF_DEFAULT_COLOR_UNIT,
    970      this._handleDefaultColorUnitPrefChange
    971    );
    972    this._prefObserver.off(PREF_DRAGGABLE, this._handleDraggablePrefChange);
    973    this._prefObserver.off(
    974      PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
    975      this._handleInplaceEditorFocusNextOnEnterPrefChange
    976    );
    977    this._prefObserver.destroy();
    978 
    979    this._outputParser = null;
    980 
    981    if (this._classListPreviewer) {
    982      this._classListPreviewer.destroy();
    983      this._classListPreviewer = null;
    984    }
    985 
    986    if (this._contextMenu) {
    987      this._contextMenu.destroy();
    988      this._contextMenu = null;
    989    }
    990 
    991    if (this._highlighters) {
    992      this._highlighters.removeFromView(this);
    993      this._highlighters = null;
    994    }
    995 
    996    // Clean-up for simulations.
    997    this.colorSchemeLightSimulationButton.removeEventListener(
    998      "click",
    999      this._onToggleLightColorSchemeSimulation
   1000    );
   1001    this.colorSchemeDarkSimulationButton.removeEventListener(
   1002      "click",
   1003      this._onToggleDarkColorSchemeSimulation
   1004    );
   1005    this.printSimulationButton.removeEventListener(
   1006      "click",
   1007      this._onTogglePrintSimulation
   1008    );
   1009 
   1010    this.colorSchemeLightSimulationButton = null;
   1011    this.colorSchemeDarkSimulationButton = null;
   1012    this.printSimulationButton = null;
   1013 
   1014    this.tooltips.destroy();
   1015 
   1016    // Remove bound listeners
   1017    this._abortController.abort();
   1018    this._abortController = null;
   1019    this.shortcuts.destroy();
   1020    this.styleDocument.removeEventListener("click", this, { capture: true });
   1021    this.element.removeEventListener("copy", this._onCopy);
   1022    this.element.removeEventListener("contextmenu", this._onContextMenu);
   1023    this.addRuleButton.removeEventListener("click", this.addNewRule);
   1024    this.searchField.removeEventListener("input", this._onFilterStyles);
   1025    this.searchClearButton.removeEventListener("click", this._onClearSearch);
   1026    this.pseudoClassPanel.removeEventListener(
   1027      "change",
   1028      this._onTogglePseudoClass
   1029    );
   1030    this.pseudoClassToggle.removeEventListener(
   1031      "click",
   1032      this._onTogglePseudoClassPanel
   1033    );
   1034    this.classToggle.removeEventListener("click", this._onToggleClassPanel);
   1035    this.inspector.highlighters.off(
   1036      "highlighter-shown",
   1037      this.onHighlighterShown
   1038    );
   1039    this.inspector.highlighters.off(
   1040      "highlighter-hidden",
   1041      this.onHighlighterHidden
   1042    );
   1043 
   1044    this.searchField = null;
   1045    this.searchClearButton = null;
   1046    this.pseudoClassPanel = null;
   1047    this.pseudoClassToggle = null;
   1048    this.pseudoClassCheckboxes = null;
   1049    this.classPanel = null;
   1050    this.classToggle = null;
   1051 
   1052    this.inspector = null;
   1053    this.styleDocument = null;
   1054    this.styleWindow = null;
   1055 
   1056    if (this.element.parentNode) {
   1057      this.element.remove();
   1058    }
   1059 
   1060    if (this._elementStyle) {
   1061      this._elementStyle.destroy();
   1062    }
   1063 
   1064    if (this._popup) {
   1065      this._popup.destroy();
   1066      this._popup = null;
   1067    }
   1068  }
   1069 
   1070  /**
   1071   * Mark the view as selecting an element, disabling all interaction, and
   1072   * visually clearing the view after a few milliseconds to avoid confusion
   1073   * about which element's styles the rule view shows.
   1074   */
   1075  _startSelectingElement() {
   1076    this.element.classList.add("non-interactive");
   1077  }
   1078 
   1079  /**
   1080   * Mark the view as no longer selecting an element, re-enabling interaction.
   1081   */
   1082  _stopSelectingElement() {
   1083    this.element.classList.remove("non-interactive");
   1084  }
   1085 
   1086  /**
   1087   * Update the view with a new selected element.
   1088   *
   1089   * @param {NodeActor} element
   1090   *        The node whose style rules we'll inspect.
   1091   * @param {boolean} allowRefresh
   1092   *        Update the view even if the element is the same as last time.
   1093   */
   1094  selectElement(element, allowRefresh = false) {
   1095    const refresh = this._viewedElement === element;
   1096    if (refresh && !allowRefresh) {
   1097      return Promise.resolve(undefined);
   1098    }
   1099 
   1100    if (this._popup && this.popup.isOpen) {
   1101      this.popup.hidePopup();
   1102    }
   1103 
   1104    this.clear(false);
   1105    this._viewedElement = element;
   1106 
   1107    this.clearPseudoClassPanel();
   1108    this.refreshAddRuleButtonState();
   1109 
   1110    if (!this._viewedElement) {
   1111      this._stopSelectingElement();
   1112      this._clearRules();
   1113      this._showEmpty();
   1114      this.refreshPseudoClassPanel();
   1115      if (this.pageStyle) {
   1116        this.pageStyle.off("stylesheet-updated", this.refreshPanel);
   1117        this.pageStyle = null;
   1118      }
   1119      return Promise.resolve(undefined);
   1120    }
   1121 
   1122    const isProfilerActive = Services.profiler?.IsActive();
   1123    const startTime = isProfilerActive ? ChromeUtils.now() : null;
   1124 
   1125    this.pageStyle = element.inspectorFront.pageStyle;
   1126    this.pageStyle.on("stylesheet-updated", this.refreshPanel);
   1127 
   1128    // To figure out how shorthand properties are interpreted by the
   1129    // engine, we will set properties on a dummy element and observe
   1130    // how their .style attribute reflects them as computed values.
   1131    const dummyElementPromise = Promise.resolve(this.styleDocument)
   1132      .then(document => {
   1133        // ::before and ::after do not have a namespaceURI
   1134        const namespaceURI =
   1135          this.element.namespaceURI || document.documentElement.namespaceURI;
   1136        this._dummyElement = document.createElementNS(
   1137          namespaceURI,
   1138          this.element.tagName
   1139        );
   1140      })
   1141      .catch(promiseWarn);
   1142 
   1143    const elementStyle = new ElementStyle(
   1144      element,
   1145      this,
   1146      this.store,
   1147      this.pageStyle,
   1148      this.showUserAgentStyles
   1149    );
   1150    this._elementStyle = elementStyle;
   1151 
   1152    this._startSelectingElement();
   1153 
   1154    return dummyElementPromise
   1155      .then(() => {
   1156        if (this._elementStyle === elementStyle) {
   1157          return this._populate();
   1158        }
   1159        return undefined;
   1160      })
   1161      .then(() => {
   1162        if (this._elementStyle === elementStyle) {
   1163          if (!refresh) {
   1164            this.element.scrollTop = 0;
   1165          }
   1166          this._stopSelectingElement();
   1167          this._elementStyle.onChanged = () => {
   1168            this._changed();
   1169          };
   1170        }
   1171        if (isProfilerActive && this._elementStyle.rules) {
   1172          let declarations = 0;
   1173          for (const rule of this._elementStyle.rules) {
   1174            declarations += rule.textProps.length;
   1175          }
   1176          ChromeUtils.addProfilerMarker(
   1177            "DevTools:CssRuleView.selectElement",
   1178            startTime,
   1179            `${declarations} CSS declarations in ${this._elementStyle.rules.length} rules`
   1180          );
   1181        }
   1182      })
   1183      .catch(e => {
   1184        if (this._elementStyle === elementStyle) {
   1185          this._stopSelectingElement();
   1186          this._clearRules();
   1187        }
   1188        console.error(e);
   1189      });
   1190  }
   1191 
   1192  /**
   1193   * Update the rules for the currently highlighted element.
   1194   */
   1195  refreshPanel() {
   1196    // Ignore refreshes when the panel is hidden, or during editing or when no element is selected.
   1197    if (!this.isPanelVisible() || this.isEditing || !this._elementStyle) {
   1198      return Promise.resolve(undefined);
   1199    }
   1200 
   1201    // Repopulate the element style once the current modifications are done.
   1202    const promises = [];
   1203    for (const rule of this._elementStyle.rules) {
   1204      if (rule._applyingModifications) {
   1205        promises.push(rule._applyingModifications);
   1206      }
   1207    }
   1208 
   1209    return Promise.all(promises).then(() => {
   1210      return this._populate();
   1211    });
   1212  }
   1213 
   1214  /**
   1215   * Clear the pseudo class options panel by removing the checked and disabled
   1216   * attributes for each checkbox.
   1217   */
   1218  clearPseudoClassPanel() {
   1219    this.pseudoClassCheckboxes.forEach(checkbox => {
   1220      checkbox.checked = false;
   1221      checkbox.disabled = false;
   1222    });
   1223  }
   1224 
   1225  /**
   1226   * For each item in PSEUDO_CLASSES, create a checkbox input element for toggling a
   1227   * pseudo-class on the selected element and append it to the pseudo-class panel.
   1228   *
   1229   * Returns an array with the checkbox input elements for pseudo-classes.
   1230   *
   1231   * @return {Array}
   1232   */
   1233  _createPseudoClassCheckboxes() {
   1234    const doc = this.styleDocument;
   1235    const fragment = doc.createDocumentFragment();
   1236 
   1237    for (const pseudo of PSEUDO_CLASSES) {
   1238      const label = doc.createElement("label");
   1239      const checkbox = doc.createElement("input");
   1240      checkbox.setAttribute("tabindex", "-1");
   1241      checkbox.setAttribute("type", "checkbox");
   1242      checkbox.setAttribute("value", pseudo);
   1243 
   1244      label.append(checkbox, pseudo);
   1245      fragment.append(label);
   1246    }
   1247 
   1248    this.pseudoClassPanel.append(fragment);
   1249    return Array.from(
   1250      this.pseudoClassPanel.querySelectorAll("input[type=checkbox]")
   1251    );
   1252  }
   1253 
   1254  /**
   1255   * Update the pseudo class options for the currently highlighted element.
   1256   */
   1257  refreshPseudoClassPanel() {
   1258    if (
   1259      !this._elementStyle ||
   1260      !this.inspector.canTogglePseudoClassForSelectedNode()
   1261    ) {
   1262      this.pseudoClassCheckboxes.forEach(checkbox => {
   1263        checkbox.disabled = true;
   1264      });
   1265      return;
   1266    }
   1267 
   1268    const pseudoClassLocks = this._elementStyle.element.pseudoClassLocks;
   1269    this.pseudoClassCheckboxes.forEach(checkbox => {
   1270      checkbox.disabled = false;
   1271      checkbox.checked = pseudoClassLocks.includes(checkbox.value);
   1272    });
   1273  }
   1274 
   1275  _populate() {
   1276    const elementStyle = this._elementStyle;
   1277    return this._elementStyle
   1278      .populate()
   1279      .then(() => {
   1280        if (this._elementStyle !== elementStyle || this.isDestroyed) {
   1281          return null;
   1282        }
   1283 
   1284        this._clearRules();
   1285        const onEditorsReady = this._createEditors();
   1286        this.refreshPseudoClassPanel();
   1287 
   1288        // Notify anyone that cares that we refreshed.
   1289        return onEditorsReady.then(() => {
   1290          this.emit("ruleview-refreshed");
   1291        }, console.error);
   1292      })
   1293      .catch(promiseWarn);
   1294  }
   1295 
   1296  /**
   1297   * Show the user that the rule view has no node selected.
   1298   */
   1299  _showEmpty() {
   1300    if (this.styleDocument.getElementById("ruleview-no-results")) {
   1301      return;
   1302    }
   1303 
   1304    createChild(this.element, "div", {
   1305      id: "ruleview-no-results",
   1306      class: "devtools-sidepanel-no-result",
   1307      textContent: l10n("rule.empty"),
   1308    });
   1309  }
   1310 
   1311  /**
   1312   * Clear the rules.
   1313   */
   1314  _clearRules() {
   1315    this.element.innerHTML = "";
   1316  }
   1317 
   1318  /**
   1319   * Clear the rule view.
   1320   */
   1321  clear(clearDom = true) {
   1322    if (clearDom) {
   1323      this._clearRules();
   1324    }
   1325    this._viewedElement = null;
   1326 
   1327    if (this._elementStyle) {
   1328      this._elementStyle.destroy();
   1329      this._elementStyle = null;
   1330    }
   1331 
   1332    if (this.pageStyle) {
   1333      this.pageStyle.off("stylesheet-updated", this.refreshPanel);
   1334      this.pageStyle = null;
   1335    }
   1336  }
   1337 
   1338  /**
   1339   * Called when the user has made changes to the ElementStyle.
   1340   * Emits an event that clients can listen to.
   1341   */
   1342  _changed() {
   1343    this.emit("ruleview-changed");
   1344  }
   1345 
   1346  /**
   1347   * Text for header that shows above rules for this element
   1348   */
   1349  get selectedElementLabel() {
   1350    if (this._selectedElementLabel) {
   1351      return this._selectedElementLabel;
   1352    }
   1353    this._selectedElementLabel = l10n("rule.selectedElement");
   1354    return this._selectedElementLabel;
   1355  }
   1356 
   1357  /**
   1358   * Text for header that shows above rules for pseudo elements
   1359   */
   1360  get pseudoElementLabel() {
   1361    if (this._pseudoElementLabel) {
   1362      return this._pseudoElementLabel;
   1363    }
   1364    this._pseudoElementLabel = l10n("rule.pseudoElement");
   1365    return this._pseudoElementLabel;
   1366  }
   1367 
   1368  get showPseudoElements() {
   1369    if (this._showPseudoElements === undefined) {
   1370      this._showPseudoElements = Services.prefs.getBoolPref(
   1371        "devtools.inspector.show_pseudo_elements"
   1372      );
   1373    }
   1374    return this._showPseudoElements;
   1375  }
   1376 
   1377  /**
   1378   * Creates an expandable container in the rule view
   1379   *
   1380   * @param  {string} label
   1381   *         The label for the container header
   1382   * @param  {string} containerId
   1383   *         The id that will be set on the container
   1384   * @param  {boolean} isPseudo
   1385   *         Whether or not the container will hold pseudo element rules
   1386   * @return {DOMNode} The container element
   1387   */
   1388  createExpandableContainer(label, containerId, isPseudo = false) {
   1389    const header = this.styleDocument.createElementNS(HTML_NS, "div");
   1390    header.classList.add(
   1391      RULE_VIEW_HEADER_CLASSNAME,
   1392      "ruleview-expandable-header"
   1393    );
   1394    header.setAttribute("role", "heading");
   1395 
   1396    const toggleButton = this.styleDocument.createElementNS(HTML_NS, "button");
   1397    toggleButton.setAttribute(
   1398      "title",
   1399      l10n("rule.expandableContainerToggleButton.title")
   1400    );
   1401    toggleButton.setAttribute("aria-expanded", "true");
   1402    toggleButton.setAttribute("aria-controls", containerId);
   1403 
   1404    const twisty = this.styleDocument.createElementNS(HTML_NS, "span");
   1405    twisty.className = "ruleview-expander theme-twisty";
   1406 
   1407    toggleButton.append(twisty, this.styleDocument.createTextNode(label));
   1408    header.append(toggleButton);
   1409 
   1410    const container = this.styleDocument.createElementNS(HTML_NS, "div");
   1411    container.id = containerId;
   1412    container.classList.add("ruleview-expandable-container");
   1413    container.hidden = false;
   1414 
   1415    this.element.append(header, container);
   1416 
   1417    toggleButton.addEventListener("click", () => {
   1418      this._toggleContainerVisibility(
   1419        toggleButton,
   1420        container,
   1421        isPseudo,
   1422        !this.showPseudoElements
   1423      );
   1424    });
   1425 
   1426    if (isPseudo) {
   1427      this._toggleContainerVisibility(
   1428        toggleButton,
   1429        container,
   1430        isPseudo,
   1431        this.showPseudoElements
   1432      );
   1433    }
   1434 
   1435    return container;
   1436  }
   1437 
   1438  /**
   1439   * Create the `@property` expandable container
   1440   *
   1441   * @returns {Element}
   1442   */
   1443  createRegisteredPropertiesExpandableContainer() {
   1444    const el = this.createExpandableContainer(
   1445      "@property",
   1446      REGISTERED_PROPERTIES_CONTAINER_ID
   1447    );
   1448    el.classList.add("registered-properties");
   1449    return el;
   1450  }
   1451 
   1452  /**
   1453   * Return the RegisteredPropertyEditor element for a given property name
   1454   *
   1455   * @param {string} registeredPropertyName
   1456   * @returns {Element|null}
   1457   */
   1458  getRegisteredPropertyElement(registeredPropertyName) {
   1459    return this.styleDocument.querySelector(
   1460      `#${REGISTERED_PROPERTIES_CONTAINER_ID} [data-name="${registeredPropertyName}"]`
   1461    );
   1462  }
   1463 
   1464  /**
   1465   * Toggle the visibility of an expandable container
   1466   *
   1467   * @param  {DOMNode}  twisty
   1468   *         Clickable toggle DOM Node
   1469   * @param  {DOMNode}  container
   1470   *         Expandable container DOM Node
   1471   * @param  {boolean}  isPseudo
   1472   *         Whether or not the container will hold pseudo element rules
   1473   * @param  {boolean}  showPseudo
   1474   *         Whether or not pseudo element rules should be displayed
   1475   */
   1476  _toggleContainerVisibility(toggleButton, container, isPseudo, showPseudo) {
   1477    let isOpen = toggleButton.getAttribute("aria-expanded") === "true";
   1478 
   1479    if (isPseudo) {
   1480      this._showPseudoElements = !!showPseudo;
   1481 
   1482      Services.prefs.setBoolPref(
   1483        "devtools.inspector.show_pseudo_elements",
   1484        this.showPseudoElements
   1485      );
   1486 
   1487      container.hidden = !this.showPseudoElements;
   1488      isOpen = !this.showPseudoElements;
   1489    } else {
   1490      container.hidden = !container.hidden;
   1491    }
   1492 
   1493    toggleButton.setAttribute("aria-expanded", !isOpen);
   1494  }
   1495 
   1496  /**
   1497   * Creates editor UI for each of the rules in _elementStyle.
   1498   */
   1499  // eslint-disable-next-line complexity
   1500  _createEditors() {
   1501    // Run through the current list of rules, attaching
   1502    // their editors in order.  Create editors if needed.
   1503    let lastInherited = null;
   1504    let lastinheritedSectionLabel = "";
   1505    let seenNormalElement = false;
   1506    let seenSearchTerm = false;
   1507    const containers = new Map();
   1508 
   1509    if (!this._elementStyle.rules) {
   1510      return Promise.resolve();
   1511    }
   1512 
   1513    const editorReadyPromises = [];
   1514    for (const rule of this._elementStyle.rules) {
   1515      if (rule.domRule.system) {
   1516        continue;
   1517      }
   1518 
   1519      // Initialize rule editor
   1520      if (!rule.editor) {
   1521        const ruleActorID = rule.domRule.actorID;
   1522        rule.editor = new RuleEditor(this, rule, {
   1523          elementsWithPendingClicks: this._elementsWithPendingClicks,
   1524          onShowUnusedCustomCssProperties: () => {
   1525            this.store.expandedUnusedCustomCssPropertiesRuleActorIds.add(
   1526              ruleActorID
   1527            );
   1528          },
   1529          shouldHideUnusedCustomCssProperties:
   1530            !this.store.expandedUnusedCustomCssPropertiesRuleActorIds.has(
   1531              ruleActorID
   1532            ),
   1533        });
   1534        editorReadyPromises.push(rule.editor.once("source-link-updated"));
   1535      }
   1536 
   1537      // Filter the rules and highlight any matches if there is a search input
   1538      if (this.searchValue && this.searchData) {
   1539        if (this.highlightRule(rule)) {
   1540          seenSearchTerm = true;
   1541        } else if (rule.domRule.type !== ELEMENT_STYLE) {
   1542          continue;
   1543        }
   1544      }
   1545 
   1546      const isNonInheritedPseudo = !!rule.pseudoElement && !rule.inherited;
   1547 
   1548      // Only print header for this element if there are pseudo elements
   1549      if (
   1550        containers.has(PSEUDO_ELEMENTS_CONTAINER_ID) &&
   1551        !seenNormalElement &&
   1552        !rule.pseudoElement
   1553      ) {
   1554        seenNormalElement = true;
   1555        const div = this.styleDocument.createElementNS(HTML_NS, "div");
   1556        div.className = RULE_VIEW_HEADER_CLASSNAME;
   1557        div.setAttribute("role", "heading");
   1558        div.textContent = this.selectedElementLabel;
   1559        this.element.appendChild(div);
   1560      }
   1561 
   1562      const { inherited, inheritedSectionLabel } = rule;
   1563      // We need to check both `inherited` (a NodeFront) and `inheritedSectionLabel` (string),
   1564      // as element-backed pseudo element rules (e.g. `::details-content`) can have the same
   1565      // `inherited` property as a regular rule (e.g. on `<details>`), but the element is
   1566      // to be considered as a child of the binding element.
   1567      // e.g. we want to have
   1568      // This element
   1569      // Inherited by details::details-content
   1570      // Inherited by details
   1571      if (
   1572        inherited &&
   1573        (inherited !== lastInherited ||
   1574          inheritedSectionLabel !== lastinheritedSectionLabel)
   1575      ) {
   1576        const div = this.styleDocument.createElementNS(HTML_NS, "div");
   1577        div.classList.add(RULE_VIEW_HEADER_CLASSNAME);
   1578        div.setAttribute("role", "heading");
   1579        div.setAttribute("aria-level", "3");
   1580        div.textContent = rule.inheritedSectionLabel;
   1581        lastInherited = inherited;
   1582        lastinheritedSectionLabel = inheritedSectionLabel;
   1583        this.element.appendChild(div);
   1584      }
   1585 
   1586      const keyframes = rule.keyframes;
   1587 
   1588      let containerKey = null;
   1589 
   1590      // Don't display inherited pseudo element rules (e.g. ::details-content) inside
   1591      // the pseudo element container
   1592      if (isNonInheritedPseudo) {
   1593        containerKey = PSEUDO_ELEMENTS_CONTAINER_ID;
   1594        if (!containers.has(containerKey)) {
   1595          containers.set(
   1596            containerKey,
   1597            this.createExpandableContainer(
   1598              this.pseudoElementLabel,
   1599              containerKey,
   1600              true
   1601            )
   1602          );
   1603        }
   1604      } else if (keyframes) {
   1605        containerKey = keyframes;
   1606        if (!containers.has(containerKey)) {
   1607          containers.set(
   1608            containerKey,
   1609            this.createExpandableContainer(
   1610              rule.keyframesName,
   1611              `keyframes-container-${keyframes.name}`
   1612            )
   1613          );
   1614        }
   1615      } else if (rule.domRule.className === "CSSPositionTryRule") {
   1616        containerKey = POSITION_TRY_CONTAINER_ID;
   1617        if (!containers.has(containerKey)) {
   1618          containers.set(
   1619            containerKey,
   1620            this.createExpandableContainer(
   1621              `@position-try`,
   1622              `position-try-container`
   1623            )
   1624          );
   1625        }
   1626      }
   1627 
   1628      rule.editor.element.setAttribute("role", "article");
   1629      const container = containers.get(containerKey);
   1630      if (container) {
   1631        container.appendChild(rule.editor.element);
   1632      } else {
   1633        this.element.appendChild(rule.editor.element);
   1634      }
   1635 
   1636      // Automatically select the selector input when we are adding a user-added rule
   1637      if (this._focusNextUserAddedRule && rule.domRule.userAdded) {
   1638        this._focusNextUserAddedRule = null;
   1639        rule.editor.selectorText.click();
   1640        this.emitForTests("new-rule-added", rule);
   1641      }
   1642    }
   1643 
   1644    const targetRegisteredProperties =
   1645      this.getRegisteredPropertiesForSelectedNodeTarget();
   1646    if (targetRegisteredProperties?.size) {
   1647      const registeredPropertiesContainer =
   1648        this.createRegisteredPropertiesExpandableContainer();
   1649 
   1650      // Sort properties by their name, as we want to display them in alphabetical order
   1651      const propertyDefinitions = Array.from(
   1652        targetRegisteredProperties.values()
   1653      ).sort((a, b) => (a.name < b.name ? -1 : 1));
   1654      for (const propertyDefinition of propertyDefinitions) {
   1655        const registeredPropertyEditor = new RegisteredPropertyEditor(
   1656          this,
   1657          propertyDefinition
   1658        );
   1659 
   1660        registeredPropertiesContainer.appendChild(
   1661          registeredPropertyEditor.element
   1662        );
   1663      }
   1664    }
   1665 
   1666    const searchBox = this.searchField.parentNode;
   1667    searchBox.classList.toggle(
   1668      "devtools-searchbox-no-match",
   1669      this.searchValue && !seenSearchTerm
   1670    );
   1671 
   1672    return Promise.all(editorReadyPromises);
   1673  }
   1674 
   1675  /**
   1676   * Highlight rules that matches the filter search value and returns a
   1677   * boolean indicating whether or not rules were highlighted.
   1678   *
   1679   * @param  {Rule} rule
   1680   *         The rule object we're highlighting if its rule selectors or
   1681   *         property values match the search value.
   1682   * @return {boolean} true if the rule was highlighted, false otherwise.
   1683   */
   1684  highlightRule(rule) {
   1685    const isRuleSelectorHighlighted = this._highlightRuleSelector(rule);
   1686    const isStyleSheetHighlighted = this._highlightStyleSheet(rule);
   1687    const isAncestorRulesHighlighted = this._highlightAncestorRules(rule);
   1688    let isHighlighted =
   1689      isRuleSelectorHighlighted ||
   1690      isStyleSheetHighlighted ||
   1691      isAncestorRulesHighlighted;
   1692 
   1693    // Highlight search matches in the rule properties
   1694    for (const textProp of rule.textProps) {
   1695      if (!textProp.invisible && this._highlightProperty(textProp)) {
   1696        isHighlighted = true;
   1697      }
   1698    }
   1699 
   1700    return isHighlighted;
   1701  }
   1702 
   1703  /**
   1704   * Highlights the rule selector that matches the filter search value and
   1705   * returns a boolean indicating whether or not the selector was highlighted.
   1706   *
   1707   * @param  {Rule} rule
   1708   *         The Rule object.
   1709   * @return {boolean} true if the rule selector was highlighted,
   1710   *         false otherwise.
   1711   */
   1712  _highlightRuleSelector(rule) {
   1713    let isSelectorHighlighted = false;
   1714 
   1715    let selectorNodes = [...rule.editor.selectorText.childNodes];
   1716    if (
   1717      rule.domRule.type === CSSRule.KEYFRAME_RULE ||
   1718      rule.domRule.className === "CSSPositionTryRule"
   1719    ) {
   1720      selectorNodes = [rule.editor.selectorText];
   1721    } else if (rule.domRule.type === ELEMENT_STYLE) {
   1722      selectorNodes = [];
   1723    }
   1724 
   1725    // Highlight search matches in the rule selectors
   1726    for (const selectorNode of selectorNodes) {
   1727      const selector = selectorNode.textContent.toLowerCase();
   1728      if (
   1729        (this.searchData.strictSearchAllValues &&
   1730          selector === this.searchData.strictSearchValue) ||
   1731        (!this.searchData.strictSearchAllValues &&
   1732          selector.includes(this.searchValue))
   1733      ) {
   1734        selectorNode.classList.add("ruleview-highlight");
   1735        isSelectorHighlighted = true;
   1736      }
   1737    }
   1738 
   1739    return isSelectorHighlighted;
   1740  }
   1741 
   1742  /**
   1743   * Highlights the ancestor rules data (@media / @layer) that matches the filter search
   1744   * value and returns a boolean indicating whether or not element was highlighted.
   1745   *
   1746   * @return {boolean} true if the element was highlighted, false otherwise.
   1747   */
   1748  _highlightAncestorRules(rule) {
   1749    const element = rule.editor.ancestorDataEl;
   1750    if (!element) {
   1751      return false;
   1752    }
   1753 
   1754    const ancestorSelectors = element.querySelectorAll(
   1755      ".ruleview-rule-ancestor-selectorcontainer"
   1756    );
   1757 
   1758    let isHighlighted = false;
   1759    for (const child of ancestorSelectors) {
   1760      const dataText = child.innerText.toLowerCase();
   1761      const matches = this.searchData.strictSearchValue
   1762        ? dataText === this.searchData.strictSearchValue
   1763        : dataText.includes(this.searchValue);
   1764      if (matches) {
   1765        isHighlighted = true;
   1766        child.classList.add("ruleview-highlight");
   1767      }
   1768    }
   1769 
   1770    return isHighlighted;
   1771  }
   1772 
   1773  /**
   1774   * Highlights the stylesheet source that matches the filter search value and
   1775   * returns a boolean indicating whether or not the stylesheet source was
   1776   * highlighted.
   1777   *
   1778   * @return {boolean} true if the stylesheet source was highlighted, false
   1779   *         otherwise.
   1780   */
   1781  _highlightStyleSheet(rule) {
   1782    const styleSheetSource = rule.title.toLowerCase();
   1783    const isStyleSheetHighlighted = this.searchData.strictSearchValue
   1784      ? styleSheetSource === this.searchData.strictSearchValue
   1785      : styleSheetSource.includes(this.searchValue);
   1786 
   1787    if (isStyleSheetHighlighted && rule.editor.source) {
   1788      rule.editor.source.classList.add("ruleview-highlight");
   1789    }
   1790 
   1791    return isStyleSheetHighlighted;
   1792  }
   1793 
   1794  /**
   1795   * Highlights the rule properties and computed properties that match the
   1796   * filter search value and returns a boolean indicating whether or not the
   1797   * property or computed property was highlighted.
   1798   *
   1799   * @param  {TextProperty} textProperty
   1800   *         The rule property.
   1801   * @returns {boolean} true if the property or computed property was
   1802   *         highlighted, false otherwise.
   1803   */
   1804  _highlightProperty(textProperty) {
   1805    const isPropertyHighlighted = this._highlightRuleProperty(textProperty);
   1806    const isComputedHighlighted = this._highlightComputedProperty(textProperty);
   1807 
   1808    // Expand the computed list if a computed property is highlighted and the
   1809    // property rule is not highlighted
   1810    if (
   1811      !isPropertyHighlighted &&
   1812      isComputedHighlighted &&
   1813      !textProperty.editor.computed.hasAttribute("user-open")
   1814    ) {
   1815      textProperty.editor.expandForFilter();
   1816    }
   1817 
   1818    return isPropertyHighlighted || isComputedHighlighted;
   1819  }
   1820 
   1821  /**
   1822   * Called when TextPropertyEditor is updated and updates the rule property
   1823   * highlight.
   1824   *
   1825   * @param  {TextPropertyEditor} editor
   1826   *         The rule property TextPropertyEditor object.
   1827   */
   1828  _updatePropertyHighlight(editor) {
   1829    if (!this.searchValue || !this.searchData) {
   1830      return;
   1831    }
   1832 
   1833    this._clearHighlight(editor.element);
   1834 
   1835    if (this._highlightProperty(editor.prop)) {
   1836      this.searchField.classList.remove("devtools-style-searchbox-no-match");
   1837    }
   1838  }
   1839 
   1840  /**
   1841   * Highlights the rule property that matches the filter search value
   1842   * and returns a boolean indicating whether or not the property was
   1843   * highlighted.
   1844   *
   1845   * @param  {TextProperty} textProperty
   1846   *         The rule property object.
   1847   * @returns {boolean} true if the rule property was highlighted,
   1848   *         false otherwise.
   1849   */
   1850  _highlightRuleProperty(textProperty) {
   1851    const propertyName = textProperty.name.toLowerCase();
   1852    // Get the actual property value displayed in the rule view if we have an editor for
   1853    // it (that might not be the case for unused CSS custom properties).
   1854    const propertyValue = textProperty.editor
   1855      ? textProperty.editor.valueSpan.textContent.toLowerCase()
   1856      : textProperty.value.toLowerCase();
   1857 
   1858    return this._highlightMatches({
   1859      element: textProperty.editor?.container,
   1860      propertyName,
   1861      propertyValue,
   1862      textProperty,
   1863    });
   1864  }
   1865 
   1866  /**
   1867   * Highlights the computed property that matches the filter search value and
   1868   * returns a boolean indicating whether or not the computed property was
   1869   * highlighted.
   1870   *
   1871   * @param  {TextProperty} textProperty
   1872   *         The rule property object.
   1873   * @returns {boolean} true if the computed property was highlighted, false
   1874   *         otherwise.
   1875   */
   1876  _highlightComputedProperty(textProperty) {
   1877    if (!textProperty.editor) {
   1878      return false;
   1879    }
   1880 
   1881    let isComputedHighlighted = false;
   1882 
   1883    // Highlight search matches in the computed list of properties
   1884    textProperty.editor.populateComputed();
   1885    for (const computed of textProperty.computed) {
   1886      if (computed.element) {
   1887        // Get the actual property value displayed in the computed list
   1888        const computedName = computed.name.toLowerCase();
   1889        const computedValue = computed.parsedValue.toLowerCase();
   1890 
   1891        isComputedHighlighted = this._highlightMatches({
   1892          element: computed.element,
   1893          propertyName: computedName,
   1894          propertyValue: computedValue,
   1895          textProperty,
   1896        })
   1897          ? true
   1898          : isComputedHighlighted;
   1899      }
   1900    }
   1901 
   1902    return isComputedHighlighted;
   1903  }
   1904 
   1905  /**
   1906   * Helper function for highlightRules that carries out highlighting the given
   1907   * element if the search terms match the property, and returns a boolean
   1908   * indicating whether or not the search terms match.
   1909   *
   1910   * @param  {object} options
   1911   * @param  {DOMNode} options.element
   1912   *         The node to highlight if search terms match
   1913   * @param  {string} options.propertyName
   1914   *         The property name of a rule
   1915   * @param  {string} options.propertyValue
   1916   *         The property value of a rule
   1917   * @param  {TextProperty} options.textProperty
   1918   *         The text property that we may highlight. It's helpful in cases we don't have
   1919   *         an element yet (e.g. if the property is a hidden unused variable)
   1920   * @return {boolean} true if the given search terms match the property, false
   1921   *         otherwise.
   1922   */
   1923  _highlightMatches({ element, propertyName, propertyValue, textProperty }) {
   1924    const {
   1925      searchPropertyName,
   1926      searchPropertyValue,
   1927      searchPropertyMatch,
   1928      strictSearchPropertyName,
   1929      strictSearchPropertyValue,
   1930      strictSearchAllValues,
   1931    } = this.searchData;
   1932    let matches = false;
   1933 
   1934    // If the inputted search value matches a property line like
   1935    // `font-family: arial`, then check to make sure the name and value match.
   1936    // Otherwise, just compare the inputted search string directly against the
   1937    // name and value of the rule property.
   1938    const hasNameAndValue =
   1939      searchPropertyMatch && searchPropertyName && searchPropertyValue;
   1940    const isMatch = (value, query, isStrict) => {
   1941      return isStrict ? value === query : query && value.includes(query);
   1942    };
   1943 
   1944    if (hasNameAndValue) {
   1945      matches =
   1946        isMatch(propertyName, searchPropertyName, strictSearchPropertyName) &&
   1947        isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue);
   1948    } else {
   1949      matches =
   1950        isMatch(
   1951          propertyName,
   1952          searchPropertyName,
   1953          strictSearchPropertyName || strictSearchAllValues
   1954        ) ||
   1955        isMatch(
   1956          propertyValue,
   1957          searchPropertyValue,
   1958          strictSearchPropertyValue || strictSearchAllValues
   1959        );
   1960    }
   1961 
   1962    if (!matches) {
   1963      return false;
   1964    }
   1965 
   1966    // We might not have an element when the prop is an unused custom css property.
   1967    if (!element && textProperty?.isUnusedVariable) {
   1968      const editor =
   1969        textProperty.rule.editor.showUnusedCssVariable(textProperty);
   1970 
   1971      // The editor couldn't be created, bail (shouldn't happen)
   1972      if (!editor) {
   1973        return false;
   1974      }
   1975 
   1976      element = editor.container;
   1977    }
   1978 
   1979    element.classList.add("ruleview-highlight");
   1980 
   1981    return true;
   1982  }
   1983 
   1984  /**
   1985   * Clear all search filter highlights in the panel, and close the computed
   1986   * list if toggled opened
   1987   */
   1988  _clearHighlight(element) {
   1989    for (const el of element.querySelectorAll(".ruleview-highlight")) {
   1990      el.classList.remove("ruleview-highlight");
   1991    }
   1992 
   1993    for (const computed of element.querySelectorAll(
   1994      ".ruleview-computedlist[filter-open]"
   1995    )) {
   1996      computed.parentNode._textPropertyEditor.collapseForFilter();
   1997    }
   1998  }
   1999 
   2000  /**
   2001   * Called when the pseudo class panel button is clicked and toggles
   2002   * the display of the pseudo class panel.
   2003   */
   2004  _onTogglePseudoClassPanel() {
   2005    if (this.pseudoClassPanel.hidden) {
   2006      this.showPseudoClassPanel();
   2007    } else {
   2008      this.hidePseudoClassPanel();
   2009    }
   2010  }
   2011 
   2012  showPseudoClassPanel() {
   2013    this.hideClassPanel();
   2014 
   2015    this.pseudoClassToggle.setAttribute("aria-pressed", "true");
   2016    this.pseudoClassCheckboxes.forEach(checkbox => {
   2017      checkbox.setAttribute("tabindex", "0");
   2018    });
   2019    this.pseudoClassPanel.hidden = false;
   2020  }
   2021 
   2022  hidePseudoClassPanel() {
   2023    this.pseudoClassToggle.setAttribute("aria-pressed", "false");
   2024    this.pseudoClassCheckboxes.forEach(checkbox => {
   2025      checkbox.setAttribute("tabindex", "-1");
   2026    });
   2027    this.pseudoClassPanel.hidden = true;
   2028  }
   2029 
   2030  /**
   2031   * Called when a pseudo class checkbox is clicked and toggles
   2032   * the pseudo class for the current selected element.
   2033   */
   2034  _onTogglePseudoClass(event) {
   2035    const target = event.target;
   2036    this.inspector.togglePseudoClass(target.value);
   2037  }
   2038 
   2039  /**
   2040   * Called when the class panel button is clicked and toggles the display of the class
   2041   * panel.
   2042   */
   2043  _onToggleClassPanel() {
   2044    if (this.classPanel.hidden) {
   2045      this.showClassPanel();
   2046    } else {
   2047      this.hideClassPanel();
   2048    }
   2049  }
   2050 
   2051  showClassPanel() {
   2052    this.hidePseudoClassPanel();
   2053 
   2054    this.classToggle.setAttribute("aria-pressed", "true");
   2055    this.classPanel.hidden = false;
   2056 
   2057    this.classListPreviewer.focusAddClassField();
   2058  }
   2059 
   2060  hideClassPanel() {
   2061    this.classToggle.setAttribute("aria-pressed", "false");
   2062    this.classPanel.hidden = true;
   2063  }
   2064 
   2065  /**
   2066   * Handle the keypress event in the rule view.
   2067   */
   2068  _onShortcut(name, event) {
   2069    if (!event.target.closest("#sidebar-panel-ruleview")) {
   2070      return;
   2071    }
   2072 
   2073    if (name === "CmdOrCtrl+F") {
   2074      this.searchField.focus();
   2075      event.preventDefault();
   2076    } else if (
   2077      (name === "Return" || name === "Space") &&
   2078      this.element.classList.contains("non-interactive")
   2079    ) {
   2080      event.preventDefault();
   2081    } else if (
   2082      name === "Escape" &&
   2083      event.target === this.searchField &&
   2084      this._onClearSearch()
   2085    ) {
   2086      // Handle the search box's keypress event. If the escape key is pressed,
   2087      // clear the search box field.
   2088      event.preventDefault();
   2089      event.stopPropagation();
   2090    }
   2091  }
   2092 
   2093  async _onToggleLightColorSchemeSimulation() {
   2094    const shouldSimulateLightScheme =
   2095      this.colorSchemeLightSimulationButton.getAttribute("aria-pressed") !==
   2096      "true";
   2097 
   2098    this.colorSchemeLightSimulationButton.setAttribute(
   2099      "aria-pressed",
   2100      shouldSimulateLightScheme
   2101    );
   2102 
   2103    this.colorSchemeDarkSimulationButton.setAttribute("aria-pressed", "false");
   2104 
   2105    await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
   2106      {
   2107        colorSchemeSimulation: shouldSimulateLightScheme ? "light" : null,
   2108      }
   2109    );
   2110    // Refresh the current element's rules in the panel.
   2111    this.refreshPanel();
   2112  }
   2113 
   2114  async _onToggleDarkColorSchemeSimulation() {
   2115    const shouldSimulateDarkScheme =
   2116      this.colorSchemeDarkSimulationButton.getAttribute("aria-pressed") !==
   2117      "true";
   2118 
   2119    this.colorSchemeDarkSimulationButton.setAttribute(
   2120      "aria-pressed",
   2121      shouldSimulateDarkScheme
   2122    );
   2123 
   2124    this.colorSchemeLightSimulationButton.setAttribute("aria-pressed", "false");
   2125 
   2126    await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
   2127      {
   2128        colorSchemeSimulation: shouldSimulateDarkScheme ? "dark" : null,
   2129      }
   2130    );
   2131    // Refresh the current element's rules in the panel.
   2132    this.refreshPanel();
   2133  }
   2134 
   2135  async _onTogglePrintSimulation() {
   2136    const enabled =
   2137      this.printSimulationButton.getAttribute("aria-pressed") !== "true";
   2138    this.printSimulationButton.setAttribute("aria-pressed", enabled);
   2139    await this.inspector.commands.targetConfigurationCommand.updateConfiguration(
   2140      {
   2141        printSimulationEnabled: enabled,
   2142      }
   2143    );
   2144    // Refresh the current element's rules in the panel.
   2145    this.refreshPanel();
   2146  }
   2147 
   2148  /**
   2149   * Temporarily flash the given element.
   2150   *
   2151   * @param  {Element} element
   2152   *         The element.
   2153   * @returns {Promise} Promise that resolves after the element was flashed-out
   2154   */
   2155  _flashElement(element) {
   2156    flashElementOn(element, {
   2157      backgroundClass: "theme-bg-contrast",
   2158    });
   2159 
   2160    if (this._flashMutationCallback) {
   2161      this._flashMutationCallback();
   2162    }
   2163 
   2164    return new Promise(resolve => {
   2165      this._flashMutationCallback = () => {
   2166        flashElementOff(element, {
   2167          backgroundClass: "theme-bg-contrast",
   2168        });
   2169        this._flashMutationCallback = null;
   2170        resolve();
   2171      };
   2172 
   2173      setTimeout(this._flashMutationCallback, PROPERTY_FLASHING_DURATION);
   2174    });
   2175  }
   2176 
   2177  /**
   2178   * Scrolls to the top of either the rule or declaration. The view will try to scroll to
   2179   * the rule if both can fit in the viewport. If not, then scroll to the declaration.
   2180   *
   2181   * @param  {Element} rule
   2182   *         The rule to scroll to.
   2183   * @param  {Element|null} declaration
   2184   *         Optional. The declaration to scroll to.
   2185   * @param  {string} scrollBehavior
   2186   *         Optional. The transition animation when scrolling. If prefers-reduced-motion
   2187   *         system pref is set, then the scroll behavior will be overridden to "auto".
   2188   */
   2189  _scrollToElement(rule, declaration, scrollBehavior = "smooth") {
   2190    let elementToScrollTo = rule;
   2191 
   2192    if (declaration) {
   2193      const { offsetTop, offsetHeight } = declaration;
   2194      // Get the distance between both the rule and declaration. If the distance is
   2195      // greater than the height of the rule view, then only scroll to the declaration.
   2196      const distance = offsetTop + offsetHeight - rule.offsetTop;
   2197 
   2198      if (this.element.parentNode.offsetHeight <= distance) {
   2199        elementToScrollTo = declaration;
   2200      }
   2201    }
   2202 
   2203    // Ensure that smooth scrolling is disabled when the user prefers reduced motion.
   2204    const win = elementToScrollTo.ownerGlobal;
   2205    const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
   2206    scrollBehavior = reducedMotion ? "auto" : scrollBehavior;
   2207 
   2208    elementToScrollTo.scrollIntoView({ behavior: scrollBehavior });
   2209  }
   2210 
   2211  /**
   2212   * Toggles the visibility of the pseudo element rule's container.
   2213   */
   2214  _togglePseudoElementRuleContainer() {
   2215    const container = this.styleDocument.getElementById(
   2216      PSEUDO_ELEMENTS_CONTAINER_ID
   2217    );
   2218    const toggle = this.styleDocument.querySelector(
   2219      `[aria-controls="${PSEUDO_ELEMENTS_CONTAINER_ID}"]`
   2220    );
   2221    this._toggleContainerVisibility(toggle, container, true, true);
   2222  }
   2223 
   2224  /**
   2225   * Finds the specified TextProperty name in the rule view. If found, scroll to and
   2226   * flash the TextProperty.
   2227   *
   2228   * @param  {string} name
   2229   *         The property name to scroll to and highlight.
   2230   * @param  {object} options
   2231   * @param  {Function|undefined} options.ruleValidator
   2232   *         An optional function that can be used to filter out rules we shouldn't look
   2233   *         into to find the property name. The function is called with a Rule object,
   2234   *         and the rule will be skipped if the function returns a falsy value.
   2235   * @return {boolean} true if the TextProperty name is found, and false otherwise.
   2236   */
   2237  highlightProperty(name, { ruleValidator } = {}) {
   2238    // First, let's clear any search we might have, as the property could be hidden
   2239    this._onClearSearch();
   2240 
   2241    let scrollBehavior = "auto";
   2242    const hasRuleValidator = typeof ruleValidator === "function";
   2243    for (const rule of this.rules) {
   2244      if (hasRuleValidator && !ruleValidator(rule)) {
   2245        continue;
   2246      }
   2247      for (const textProp of rule.textProps) {
   2248        if (textProp.overridden || textProp.invisible || !textProp.enabled) {
   2249          continue;
   2250        }
   2251 
   2252        // First, search for a matching authored property.
   2253        if (textProp.name === name) {
   2254          // If using 2-Pane mode, then switch to the Rules tab first.
   2255          if (!this.inspector.isThreePaneModeEnabled) {
   2256            this.inspector.sidebar.select("ruleview");
   2257          }
   2258 
   2259          // If the property is being applied by a pseudo element rule, expand the pseudo
   2260          // element list container.
   2261          if (rule.pseudoElement.length && !this.showPseudoElements) {
   2262            // Set the scroll behavior to "auto" to avoid timing issues between toggling
   2263            // the pseudo element container and scrolling smoothly to the rule.
   2264            scrollBehavior = "auto";
   2265            this._togglePseudoElementRuleContainer();
   2266          }
   2267 
   2268          // If we're jumping to an unused CSS variable, it might not be visible, so show
   2269          // it here.
   2270          if (!textProp.editor && textProp.isUnusedVariable) {
   2271            textProp.rule.editor.showUnusedCssVariable(textProp);
   2272          }
   2273 
   2274          this._highlightElementInRule(
   2275            rule,
   2276            textProp.editor.element,
   2277            scrollBehavior
   2278          );
   2279          return true;
   2280        }
   2281 
   2282        // If there is no matching property, then look in computed properties.
   2283        for (const computed of textProp.computed) {
   2284          if (computed.overridden) {
   2285            continue;
   2286          }
   2287 
   2288          if (computed.name === name) {
   2289            if (!this.inspector.isThreePaneModeEnabled) {
   2290              this.inspector.sidebar.select("ruleview");
   2291            }
   2292 
   2293            if (
   2294              textProp.rule.pseudoElement.length &&
   2295              !this.showPseudoElements
   2296            ) {
   2297              scrollBehavior = "auto";
   2298              this._togglePseudoElementRuleContainer();
   2299            }
   2300 
   2301            // Expand the computed list.
   2302            textProp.editor.expandForFilter();
   2303 
   2304            this._highlightElementInRule(
   2305              rule,
   2306              computed.element,
   2307              scrollBehavior
   2308            );
   2309 
   2310            return true;
   2311          }
   2312        }
   2313      }
   2314    }
   2315    // If the property is a CSS variable and we didn't find its declaration, it might
   2316    // be a registered property
   2317    if (this._maybeHighlightCssRegisteredProperty(name)) {
   2318      return true;
   2319    }
   2320 
   2321    return false;
   2322  }
   2323 
   2324  /**
   2325   * If the passed name matches a registered CSS property highlight it
   2326   *
   2327   * @param {string} name - The name of the registered property to highlight
   2328   * @param {string} scrollBehavior
   2329   * @returns {boolean} Returns true if `name` matched a registered property
   2330   */
   2331  _maybeHighlightCssRegisteredProperty(name, scrollBehavior) {
   2332    if (!name.startsWith("--")) {
   2333      return false;
   2334    }
   2335 
   2336    // Get a potential @property section
   2337    const propertyContainer = this.styleDocument.getElementById(
   2338      REGISTERED_PROPERTIES_CONTAINER_ID
   2339    );
   2340    if (!propertyContainer) {
   2341      return false;
   2342    }
   2343 
   2344    const propertyEl = propertyContainer.querySelector(`[data-name="${name}"]`);
   2345    if (!propertyEl) {
   2346      return false;
   2347    }
   2348 
   2349    const toggle = this.styleDocument.querySelector(
   2350      `[aria-controls="${REGISTERED_PROPERTIES_CONTAINER_ID}"]`
   2351    );
   2352    if (toggle.ariaExpanded === "false") {
   2353      this._toggleContainerVisibility(toggle, propertyContainer);
   2354    }
   2355 
   2356    this._highlightElementInRule(null, propertyEl, scrollBehavior);
   2357    return true;
   2358  }
   2359 
   2360  /**
   2361   * Highlight a given element in a rule editor
   2362   *
   2363   * @param {Rule} rule
   2364   * @param {Element} element
   2365   * @param {string} scrollBehavior
   2366   */
   2367  _highlightElementInRule(rule, element, scrollBehavior) {
   2368    if (rule) {
   2369      this._scrollToElement(rule.editor.selectorText, element, scrollBehavior);
   2370    } else {
   2371      this._scrollToElement(element, null, scrollBehavior);
   2372    }
   2373    this._flashElement(element).then(() =>
   2374      this.emitForTests("element-highlighted", element)
   2375    );
   2376  }
   2377 
   2378  /**
   2379   * Returns a Map (keyed by name) of the registered
   2380   * properties for the currently selected node document.
   2381   *
   2382   * @returns Map<String, Object>|null
   2383   */
   2384  getRegisteredPropertiesForSelectedNodeTarget() {
   2385    return this.cssRegisteredPropertiesByTarget.get(
   2386      this.inspector.selection.nodeFront.targetFront
   2387    );
   2388  }
   2389 }
   2390 
   2391 class RuleViewTool {
   2392  constructor(inspector, window) {
   2393    this.inspector = inspector;
   2394    this.document = window.document;
   2395 
   2396    this.view = new CssRuleView(this.inspector, this.document);
   2397 
   2398    this.refresh = this.refresh.bind(this);
   2399    this.onDetachedFront = this.onDetachedFront.bind(this);
   2400    this.onPanelSelected = this.onPanelSelected.bind(this);
   2401    this.onDetachedFront = this.onDetachedFront.bind(this);
   2402    this.onSelected = this.onSelected.bind(this);
   2403    this.onViewRefreshed = this.onViewRefreshed.bind(this);
   2404 
   2405    this.#abortController = new window.AbortController();
   2406    const { signal } = this.#abortController;
   2407    const baseEventConfig = { signal };
   2408 
   2409    this.view.on("ruleview-refreshed", this.onViewRefreshed, baseEventConfig);
   2410    this.inspector.selection.on(
   2411      "detached-front",
   2412      this.onDetachedFront,
   2413      baseEventConfig
   2414    );
   2415    this.inspector.selection.on(
   2416      "new-node-front",
   2417      this.onSelected,
   2418      baseEventConfig
   2419    );
   2420    this.inspector.selection.on("pseudoclass", this.refresh, baseEventConfig);
   2421    this.inspector.ruleViewSideBar.on(
   2422      "ruleview-selected",
   2423      this.onPanelSelected,
   2424      baseEventConfig
   2425    );
   2426    this.inspector.sidebar.on(
   2427      "ruleview-selected",
   2428      this.onPanelSelected,
   2429      baseEventConfig
   2430    );
   2431    this.inspector.toolbox.on(
   2432      "inspector-selected",
   2433      this.onPanelSelected,
   2434      baseEventConfig
   2435    );
   2436    this.inspector.styleChangeTracker.on(
   2437      "style-changed",
   2438      this.refresh,
   2439      baseEventConfig
   2440    );
   2441 
   2442    this.inspector.commands.resourceCommand.watchResources(
   2443      [
   2444        this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
   2445        this.inspector.commands.resourceCommand.TYPES.STYLESHEET,
   2446      ],
   2447      {
   2448        onAvailable: this.#onResourceAvailable,
   2449        ignoreExistingResources: true,
   2450      }
   2451    );
   2452 
   2453    // We do want to get already existing registered properties, so we need to watch
   2454    // them separately
   2455    this.inspector.commands.resourceCommand
   2456      .watchResources(
   2457        [
   2458          this.inspector.commands.resourceCommand.TYPES
   2459            .CSS_REGISTERED_PROPERTIES,
   2460        ],
   2461        {
   2462          onAvailable: this.#onResourceAvailable,
   2463          onUpdated: this.#onResourceUpdated,
   2464          onDestroyed: this.#onResourceDestroyed,
   2465          ignoreExistingResources: false,
   2466        }
   2467      )
   2468      .catch(e => {
   2469        // watchResources is async and even making it's resulting promise part of
   2470        // this.readyPromise still causes test failures, so simply ignore the rejection
   2471        // if the view was already destroyed.
   2472        if (!this.view) {
   2473          return;
   2474        }
   2475        throw e;
   2476      });
   2477 
   2478    // At the moment `readyPromise` is only consumed in tests (see `openRuleView`) to be
   2479    // notified when the ruleview was first populated to match the initial selected node.
   2480    this.readyPromise = this.onSelected();
   2481  }
   2482 
   2483  #abortController;
   2484 
   2485  isPanelVisible() {
   2486    if (!this.view) {
   2487      return false;
   2488    }
   2489    return this.view.isPanelVisible();
   2490  }
   2491 
   2492  onDetachedFront() {
   2493    this.onSelected(false);
   2494  }
   2495 
   2496  onSelected(selectElement = true) {
   2497    // Ignore the event if the view has been destroyed, or if it's inactive.
   2498    // But only if the current selection isn't null. If it's been set to null,
   2499    // let the update go through as this is needed to empty the view on
   2500    // navigation.
   2501    if (!this.view) {
   2502      return null;
   2503    }
   2504 
   2505    const isInactive =
   2506      !this.isPanelVisible() && this.inspector.selection.nodeFront;
   2507    if (isInactive) {
   2508      return null;
   2509    }
   2510 
   2511    if (
   2512      !this.inspector.selection.isConnected() ||
   2513      !this.inspector.selection.isElementNode()
   2514    ) {
   2515      return this.view.selectElement(null);
   2516    }
   2517 
   2518    if (!selectElement) {
   2519      return null;
   2520    }
   2521 
   2522    const done = this.inspector.updating("rule-view");
   2523    return this.view
   2524      .selectElement(this.inspector.selection.nodeFront)
   2525      .then(done, done);
   2526  }
   2527 
   2528  refresh() {
   2529    if (this.isPanelVisible()) {
   2530      this.view.refreshPanel();
   2531    }
   2532  }
   2533 
   2534  #onResourceAvailable = resources => {
   2535    if (!this.inspector) {
   2536      return;
   2537    }
   2538 
   2539    let hasNewStylesheet = false;
   2540    const addedRegisteredProperties = [];
   2541    for (const resource of resources) {
   2542      if (
   2543        resource.resourceType ===
   2544          this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
   2545        resource.name === "will-navigate"
   2546      ) {
   2547        this.view.cssRegisteredPropertiesByTarget.delete(resource.targetFront);
   2548        if (resource.targetFront.isTopLevel) {
   2549          this.clearUserProperties();
   2550        }
   2551        continue;
   2552      }
   2553 
   2554      if (
   2555        resource.resourceType ===
   2556          this.inspector.commands.resourceCommand.TYPES.STYLESHEET &&
   2557        // resource.isNew is only true when the stylesheet was added from DevTools,
   2558        // for example when adding a rule in the rule view. In such cases, we're already
   2559        // updating the rule view, so ignore those.
   2560        !resource.isNew
   2561      ) {
   2562        hasNewStylesheet = true;
   2563      }
   2564 
   2565      if (
   2566        resource.resourceType ===
   2567        this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
   2568      ) {
   2569        if (
   2570          !this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
   2571        ) {
   2572          this.view.cssRegisteredPropertiesByTarget.set(
   2573            resource.targetFront,
   2574            new Map()
   2575          );
   2576        }
   2577        this.view.cssRegisteredPropertiesByTarget
   2578          .get(resource.targetFront)
   2579          .set(resource.name, resource);
   2580        // Only add properties from the same target as the selected node
   2581        if (
   2582          this.view.inspector.selection?.nodeFront?.targetFront ===
   2583          resource.targetFront
   2584        ) {
   2585          addedRegisteredProperties.push(resource);
   2586        }
   2587      }
   2588    }
   2589 
   2590    if (addedRegisteredProperties.length) {
   2591      // Retrieve @property container
   2592      let registeredPropertiesContainer =
   2593        this.view.styleDocument.getElementById(
   2594          REGISTERED_PROPERTIES_CONTAINER_ID
   2595        );
   2596      // create it if it didn't exist before
   2597      if (!registeredPropertiesContainer) {
   2598        registeredPropertiesContainer =
   2599          this.view.createRegisteredPropertiesExpandableContainer();
   2600      }
   2601 
   2602      // Then add all new registered properties
   2603      const names = new Set();
   2604      for (const propertyDefinition of addedRegisteredProperties) {
   2605        const editor = new RegisteredPropertyEditor(
   2606          this.view,
   2607          propertyDefinition
   2608        );
   2609        names.add(propertyDefinition.name);
   2610 
   2611        // We need to insert the element at the right position so we keep the list of
   2612        // properties alphabetically sorted.
   2613        let referenceNode = null;
   2614        for (const child of registeredPropertiesContainer.children) {
   2615          if (child.getAttribute("data-name") > propertyDefinition.name) {
   2616            referenceNode = child;
   2617            break;
   2618          }
   2619        }
   2620        registeredPropertiesContainer.insertBefore(
   2621          editor.element,
   2622          referenceNode
   2623        );
   2624      }
   2625 
   2626      // Finally, update textProps that might rely on those new properties
   2627      this._updateElementStyleRegisteredProperties(names);
   2628    }
   2629 
   2630    if (hasNewStylesheet) {
   2631      this.refresh();
   2632    }
   2633  };
   2634 
   2635  #onResourceUpdated = updates => {
   2636    const updatedProperties = [];
   2637    for (const update of updates) {
   2638      if (
   2639        update.resource.resourceType ===
   2640        this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
   2641      ) {
   2642        const { resource } = update;
   2643        if (
   2644          !this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
   2645        ) {
   2646          continue;
   2647        }
   2648 
   2649        this.view.cssRegisteredPropertiesByTarget
   2650          .get(resource.targetFront)
   2651          .set(resource.name, resource);
   2652 
   2653        // Only consider properties from the same target as the selected node
   2654        if (
   2655          this.view.inspector.selection?.nodeFront?.targetFront ===
   2656          resource.targetFront
   2657        ) {
   2658          updatedProperties.push(resource);
   2659        }
   2660      }
   2661    }
   2662 
   2663    const names = new Set();
   2664    if (updatedProperties.length) {
   2665      const registeredPropertiesContainer =
   2666        this.view.styleDocument.getElementById(
   2667          REGISTERED_PROPERTIES_CONTAINER_ID
   2668        );
   2669      for (const resource of updatedProperties) {
   2670        // Replace the existing registered property editor element with a new one,
   2671        // so we don't have to compute which elements should be updated.
   2672        const name = resource.name;
   2673        const el = this.view.getRegisteredPropertyElement(name);
   2674        const editor = new RegisteredPropertyEditor(this.view, resource);
   2675        registeredPropertiesContainer.replaceChild(editor.element, el);
   2676 
   2677        names.add(resource.name);
   2678      }
   2679      // Finally, update textProps that might rely on those new properties
   2680      this._updateElementStyleRegisteredProperties(names);
   2681    }
   2682  };
   2683 
   2684  #onResourceDestroyed = resources => {
   2685    const destroyedPropertiesNames = new Set();
   2686    for (const resource of resources) {
   2687      if (
   2688        resource.resourceType ===
   2689        this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
   2690      ) {
   2691        if (
   2692          !this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
   2693        ) {
   2694          continue;
   2695        }
   2696 
   2697        const targetRegisteredProperties =
   2698          this.view.cssRegisteredPropertiesByTarget.get(resource.targetFront);
   2699        const resourceName = Array.from(
   2700          targetRegisteredProperties.entries()
   2701        ).find(
   2702          ([_, propDef]) => propDef.resourceId === resource.resourceId
   2703        )?.[0];
   2704        if (!resourceName) {
   2705          continue;
   2706        }
   2707 
   2708        targetRegisteredProperties.delete(resourceName);
   2709 
   2710        // Only consider properties from the same target as the selected node
   2711        if (
   2712          this.view.inspector.selection?.nodeFront?.targetFront ===
   2713          resource.targetFront
   2714        ) {
   2715          destroyedPropertiesNames.add(resourceName);
   2716        }
   2717      }
   2718    }
   2719    if (destroyedPropertiesNames.size > 0) {
   2720      for (const name of destroyedPropertiesNames) {
   2721        this.view.getRegisteredPropertyElement(name)?.remove();
   2722      }
   2723      // Finally, update textProps that were relying on those removed properties
   2724      this._updateElementStyleRegisteredProperties(destroyedPropertiesNames);
   2725    }
   2726  };
   2727 
   2728  /**
   2729   * Update rules that reference registered properties whose name is in the passed Set,
   2730   * so the `var()` tooltip has up-to-date information.
   2731   *
   2732   * @param {Set<string>} registeredPropertyNames
   2733   */
   2734  _updateElementStyleRegisteredProperties(registeredPropertyNames) {
   2735    if (!this.view._elementStyle) {
   2736      return;
   2737    }
   2738    this.view._elementStyle.onRegisteredPropertiesChange(
   2739      registeredPropertyNames
   2740    );
   2741  }
   2742 
   2743  clearUserProperties() {
   2744    if (this.view && this.view.store && this.view.store.userProperties) {
   2745      this.view.store.userProperties.clear();
   2746    }
   2747  }
   2748 
   2749  onPanelSelected() {
   2750    if (this.inspector.selection.nodeFront === this.view._viewedElement) {
   2751      this.refresh();
   2752    } else {
   2753      this.onSelected();
   2754    }
   2755  }
   2756 
   2757  onViewRefreshed() {
   2758    this.inspector.emit("rule-view-refreshed");
   2759  }
   2760 
   2761  destroy() {
   2762    if (this.#abortController) {
   2763      this.#abortController.abort();
   2764    }
   2765 
   2766    this.inspector.commands.resourceCommand.unwatchResources(
   2767      [
   2768        this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT,
   2769        this.inspector.commands.resourceCommand.TYPES.STYLESHEET,
   2770      ],
   2771      {
   2772        onAvailable: this.#onResourceAvailable,
   2773      }
   2774    );
   2775 
   2776    this.inspector.commands.resourceCommand.unwatchResources(
   2777      [this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES],
   2778      {
   2779        onAvailable: this.#onResourceAvailable,
   2780        onUpdated: this.#onResourceUpdated,
   2781        onDestroyed: this.#onResourceDestroyed,
   2782      }
   2783    );
   2784 
   2785    this.view.destroy();
   2786 
   2787    this.view =
   2788      this.document =
   2789      this.inspector =
   2790      this.readyPromise =
   2791      this.store =
   2792      this.#abortController =
   2793        null;
   2794  }
   2795 }
   2796 
   2797 exports.CssRuleView = CssRuleView;
   2798 exports.RuleViewTool = RuleViewTool;