tor-browser

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

element-style.js (37880B)


      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 Rule = require("resource://devtools/client/inspector/rules/models/rule.js");
      8 const UserProperties = require("resource://devtools/client/inspector/rules/models/user-properties.js");
      9 const {
     10  style: { ELEMENT_STYLE, PRES_HINTS },
     11 } = require("resource://devtools/shared/constants.js");
     12 
     13 loader.lazyRequireGetter(
     14  this,
     15  "promiseWarn",
     16  "resource://devtools/client/inspector/shared/utils.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  ["parseDeclarations", "parseNamedDeclarations", "parseSingleValue"],
     22  "resource://devtools/shared/css/parsing-utils.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "isCssVariable",
     28  "resource://devtools/shared/inspector/css-logic.js",
     29  true
     30 );
     31 
     32 /**
     33 * ElementStyle is responsible for the following:
     34 *   Keeps track of which properties are overridden.
     35 *   Maintains a list of Rule objects for a given element.
     36 */
     37 class ElementStyle {
     38  /**
     39   * @param  {Element} element
     40   *         The element whose style we are viewing.
     41   * @param  {CssRuleView} ruleView
     42   *         The instance of the rule-view panel.
     43   * @param  {object} store
     44   *         The ElementStyle can use this object to store metadata
     45   *         that might outlast the rule view, particularly the current
     46   *         set of disabled properties.
     47   * @param  {PageStyleFront} pageStyle
     48   *         Front for the page style actor that will be providing
     49   *         the style information.
     50   * @param  {boolean} showUserAgentStyles
     51   *         Should user agent styles be inspected?
     52   */
     53  constructor(element, ruleView, store, pageStyle, showUserAgentStyles) {
     54    this.element = element;
     55    this.ruleView = ruleView;
     56    this.store = store || {};
     57    this.pageStyle = pageStyle;
     58    this.pseudoElementTypes = new Set();
     59    this.showUserAgentStyles = showUserAgentStyles;
     60    this.rules = [];
     61    this.cssProperties = this.ruleView.cssProperties;
     62    this.variablesMap = new Map();
     63    this.startingStyleVariablesMap = new Map();
     64 
     65    // We don't want to overwrite this.store.userProperties so we only create it
     66    // if it doesn't already exist.
     67    if (!("userProperties" in this.store)) {
     68      this.store.userProperties = new UserProperties();
     69    }
     70 
     71    if (!("disabled" in this.store)) {
     72      this.store.disabled = new WeakMap();
     73    }
     74  }
     75 
     76  destroy() {
     77    if (this.destroyed) {
     78      return;
     79    }
     80 
     81    this.destroyed = true;
     82    this.pseudoElementTypes.clear();
     83 
     84    for (const rule of this.rules) {
     85      if (rule.editor) {
     86        rule.editor.destroy();
     87      }
     88 
     89      rule.destroy();
     90    }
     91  }
     92 
     93  /**
     94   * Called by the Rule object when it has been changed through the
     95   * setProperty* methods.
     96   */
     97  _changed() {
     98    if (this.onChanged) {
     99      this.onChanged();
    100    }
    101  }
    102 
    103  /**
    104   * Refresh the list of rules to be displayed for the active element.
    105   * Upon completion, this.rules[] will hold a list of Rule objects.
    106   *
    107   * Returns a promise that will be resolved when the elementStyle is
    108   * ready.
    109   */
    110  populate() {
    111    const populated = this.pageStyle
    112      .getApplied(this.element, {
    113        inherited: true,
    114        matchedSelectors: true,
    115        filter: this.showUserAgentStyles ? "ua" : undefined,
    116      })
    117      .then(entries => {
    118        if (this.destroyed || this.populated !== populated) {
    119          return Promise.resolve(undefined);
    120        }
    121 
    122        // Store the current list of rules (if any) during the population
    123        // process. They will be reused if possible.
    124        const existingRules = this.rules;
    125 
    126        this.rules = [];
    127 
    128        for (const entry of entries) {
    129          this._maybeAddRule(entry, existingRules);
    130        }
    131 
    132        // Store a list of all (non-inherited) pseudo-element types found in the matching rules.
    133        this.pseudoElementTypes = new Set();
    134        for (const rule of this.rules) {
    135          if (rule.pseudoElement && !rule.inherited) {
    136            this.pseudoElementTypes.add(rule.pseudoElement);
    137          }
    138        }
    139 
    140        // Mark overridden computed styles.
    141        this.onRuleUpdated();
    142 
    143        this._sortRulesForPseudoElement();
    144 
    145        // We're done with the previous list of rules.
    146        for (const r of existingRules) {
    147          if (r?.editor) {
    148            r.editor.destroy();
    149          }
    150 
    151          r.destroy();
    152        }
    153 
    154        return undefined;
    155      })
    156      .catch(e => {
    157        // populate is often called after a setTimeout,
    158        // the connection may already be closed.
    159        if (this.destroyed) {
    160          return Promise.resolve(undefined);
    161        }
    162        return promiseWarn(e);
    163      });
    164    this.populated = populated;
    165    return this.populated;
    166  }
    167 
    168  /**
    169   * Returns the Rule object of the given rule id.
    170   *
    171   * @param  {string | null} id
    172   *         The id of the Rule object.
    173   * @return {Rule|undefined} of the given rule id or undefined if it cannot be found.
    174   */
    175  getRule(id) {
    176    return id
    177      ? this.rules.find(rule => rule.domRule.actorID === id)
    178      : undefined;
    179  }
    180 
    181  /**
    182   * Get the font families in use by the element.
    183   *
    184   * Returns a promise that will be resolved to a Set of lowercased CSS family names.
    185   */
    186  getUsedFontFamilies() {
    187    return new Promise((resolve, reject) => {
    188      this.ruleView.styleWindow.requestIdleCallback(async () => {
    189        if (this.element.isDestroyed()) {
    190          resolve([]);
    191          return;
    192        }
    193        try {
    194          const fonts = await this.pageStyle.getUsedFontFaces(this.element, {
    195            includePreviews: false,
    196          });
    197          const familyNames = new Set();
    198          for (const font of fonts) {
    199            if (font.CSSFamilyName) {
    200              familyNames.add(font.CSSFamilyName.toLowerCase());
    201            }
    202 
    203            // CSSGeneric is the font generic name (e.g. system-ui), which is different
    204            // from the CSSFamilyName but can also be used as a font-family (e.g. for
    205            // system-ui, the actual font name is ".SF NS" on OSX 14.6).
    206            if (font.CSSGeneric) {
    207              familyNames.add(font.CSSGeneric.toLowerCase());
    208            }
    209          }
    210          resolve(familyNames);
    211        } catch (e) {
    212          reject(e);
    213        }
    214      });
    215    });
    216  }
    217 
    218  /**
    219   * Put non inherited pseudo elements in front of others rules.
    220   */
    221  _sortRulesForPseudoElement() {
    222    this.rules = this.rules.sort((a, b) => {
    223      if (
    224        !a.inherited === !b.inherited &&
    225        !!a.pseudoElement !== !!b.pseudoElement
    226      ) {
    227        return (a.pseudoElement || "z") > (b.pseudoElement || "z") ? 1 : -1;
    228      }
    229      return 0;
    230    });
    231  }
    232 
    233  /**
    234   * Add a rule if it's one we care about. Filters out duplicates and
    235   * inherited styles with no inherited properties.
    236   *
    237   * @param  {object} options
    238   *         Options for creating the Rule, see the Rule constructor.
    239   * @param  {Array} existingRules
    240   *         Rules to reuse if possible. If a rule is reused, then it
    241   *         it will be deleted from this array.
    242   * @return {boolean} true if we added the rule.
    243   */
    244  _maybeAddRule(options, existingRules) {
    245    // If we've already included this domRule (for example, when a
    246    // common selector is inherited), ignore it.
    247    if (
    248      options.system ||
    249      (options.rule && this.rules.some(rule => rule.domRule === options.rule))
    250    ) {
    251      return false;
    252    }
    253 
    254    let rule = null;
    255 
    256    // If we're refreshing and the rule previously existed, reuse the
    257    // Rule object.
    258    if (existingRules) {
    259      const ruleIndex = existingRules.findIndex(r => r.matches(options));
    260      if (ruleIndex >= 0) {
    261        rule = existingRules[ruleIndex];
    262        rule.refresh(options);
    263        existingRules.splice(ruleIndex, 1);
    264      }
    265    }
    266 
    267    // If this is a new rule, create its Rule object.
    268    if (!rule) {
    269      rule = new Rule(this, options);
    270    }
    271 
    272    // Ignore inherited rules with no visible properties.
    273    if (options.inherited && !rule.hasAnyVisibleProperties()) {
    274      return false;
    275    }
    276 
    277    this.rules.push(rule);
    278    return true;
    279  }
    280 
    281  /**
    282   * Calls updateDeclarations with all supported pseudo elements
    283   */
    284  onRuleUpdated() {
    285    this.updateDeclarations();
    286 
    287    // Update declarations for matching rules for pseudo-elements.
    288    for (const pseudo of this.pseudoElementTypes) {
    289      this.updateDeclarations(pseudo);
    290    }
    291  }
    292 
    293  /**
    294   * Go over all CSS rules matching the selected element and mark the CSS declarations
    295   * (aka TextProperty instances) with an `overridden` Boolean flag if an earlier or
    296   * higher priority declaration overrides it. Rules are already ordered by specificity.
    297   *
    298   * If a pseudo-element type is passed (ex: ::before, ::first-line, etc),
    299   * restrict the operation only to declarations in rules matching that pseudo-element.
    300   *
    301   * At the end, update the declaration's view (TextPropertyEditor instance) so it relects
    302   * the latest state. Use this opportunity to also trigger checks for the "inactive"
    303   * state of the declaration (whether it has effect or not).
    304   *
    305   * @param  {string} pseudo
    306   *         Optional pseudo-element for which to restrict marking CSS declarations as
    307   *         overridden.
    308   */
    309  // eslint-disable-next-line complexity
    310  updateDeclarations(pseudo = "") {
    311    // Gather all text properties applicable to the selected element or pseudo-element.
    312    const textProps = this._getDeclarations(pseudo);
    313 
    314    // CSS Variables inherits from the normal element in case of pseudo element.
    315    const variables = new Map(pseudo ? this.variablesMap.get("") : null);
    316    const startingStyleVariables = new Map(
    317      pseudo ? this.startingStyleVariablesMap.get("") : null
    318    );
    319 
    320    // Walk over the computed properties. As we see a property name
    321    // for the first time, mark that property's name as taken by this
    322    // property.
    323    //
    324    // If we come across a property whose name is already taken, check
    325    // its priority against the property that was found first:
    326    //
    327    //   If the new property is a higher priority, mark the old
    328    //   property overridden and mark the property name as taken by
    329    //   the new property.
    330    //
    331    //   If the new property is a lower or equal priority, mark it as
    332    //   overridden.
    333    //
    334    //   Note that this is different if layers are involved: if both
    335    //   old and new properties have a high priority, and if the new
    336    //   property is in a rule belonging to a layer that is different
    337    //   from the the one the old property rule might be in,
    338    //   mark the old property overridden and mark the property name as
    339    //   taken by the new property.
    340    //
    341    // _overriddenDirty will be set on each prop, indicating whether its
    342    // dirty status changed during this pass.
    343    const taken = new Map();
    344    const takenInStartingStyle = new Map();
    345    for (const textProp of textProps) {
    346      for (const computedProp of textProp.computed) {
    347        const earlier = taken.get(computedProp.name);
    348        const earlierInStartingStyle = takenInStartingStyle.get(
    349          computedProp.name
    350        );
    351 
    352        // Prevent -webkit-gradient from being selected after unchecking
    353        // linear-gradient in this case:
    354        //  -moz-linear-gradient: ...;
    355        //  -webkit-linear-gradient: ...;
    356        //  linear-gradient: ...;
    357        if (!computedProp.textProp.isValid()) {
    358          continue;
    359        }
    360 
    361        const isPropInStartingStyle =
    362          computedProp.textProp.rule?.isInStartingStyle();
    363 
    364        const hasHigherPriority = this._hasHigherPriorityThanEarlierProp(
    365          computedProp,
    366          earlier
    367        );
    368        const startingStyleHasHigherPriority =
    369          this._hasHigherPriorityThanEarlierProp(
    370            computedProp,
    371            earlierInStartingStyle
    372          );
    373 
    374        // earlier prop is overridden if the new property has higher priority and is not
    375        // in a starting style rule.
    376        if (hasHigherPriority && !isPropInStartingStyle) {
    377          // New property is higher priority. Mark the earlier property
    378          // overridden (which will reverse its dirty state).
    379          earlier._overriddenDirty = !earlier._overriddenDirty;
    380          earlier.overridden = true;
    381        }
    382 
    383        // earlier starting-style prop are always going to be overriden if the new property
    384        // has higher priority
    385        if (startingStyleHasHigherPriority) {
    386          earlierInStartingStyle._overriddenDirty =
    387            !earlierInStartingStyle._overriddenDirty;
    388          earlierInStartingStyle.overridden = true;
    389          // which means we also need to remove the variable from startingStyleVariables
    390          if (isCssVariable(computedProp.name)) {
    391            startingStyleVariables.delete(computedProp.name);
    392          }
    393        }
    394 
    395        // This computed property is overridden if:
    396        // - there was an earlier prop and this one does not have higher priority
    397        // - or if this is a starting-style prop, and there was an earlier starting-style
    398        //   prop, and this one hasn't higher priority.
    399        const overridden =
    400          (!!earlier && !hasHigherPriority) ||
    401          (isPropInStartingStyle &&
    402            !!earlierInStartingStyle &&
    403            !startingStyleHasHigherPriority);
    404 
    405        computedProp._overriddenDirty =
    406          !!computedProp.overridden !== overridden;
    407        computedProp.overridden = overridden;
    408 
    409        if (!computedProp.overridden && computedProp.textProp.enabled) {
    410          if (isPropInStartingStyle) {
    411            takenInStartingStyle.set(computedProp.name, computedProp);
    412          } else {
    413            taken.set(computedProp.name, computedProp);
    414          }
    415 
    416          // At this point, we can get CSS variable from "inherited" rules.
    417          // When this is a registered custom property with `inherits` set to false,
    418          // the text prop is "invisible" (i.e. not shown in the rule view).
    419          // In such case, we don't want to get the value in the Map, and we'll rather
    420          // get the initial value from the registered property definition.
    421          if (
    422            isCssVariable(computedProp.name) &&
    423            !computedProp.textProp.invisible
    424          ) {
    425            if (!isPropInStartingStyle) {
    426              variables.set(computedProp.name, {
    427                declarationValue: computedProp.value,
    428                computedValue: computedProp.textProp.getVariableComputedValue(),
    429              });
    430            } else {
    431              startingStyleVariables.set(computedProp.name, computedProp.value);
    432            }
    433          }
    434        }
    435      }
    436    }
    437 
    438    // Find the CSS variables that have been updated.
    439    const previousVariablesMap = new Map(this.variablesMap.get(pseudo));
    440    const changedVariableNamesSet = new Set(
    441      [...variables.keys(), ...previousVariablesMap.keys()].filter(
    442        k => variables.get(k) !== previousVariablesMap.get(k)
    443      )
    444    );
    445    const previousStartingStyleVariablesMap = new Map(
    446      this.startingStyleVariablesMap.get(pseudo)
    447    );
    448    const changedStartingStyleVariableNamesSet = new Set(
    449      [...variables.keys(), ...previousStartingStyleVariablesMap.keys()].filter(
    450        k => variables.get(k) !== previousStartingStyleVariablesMap.get(k)
    451      )
    452    );
    453 
    454    this.variablesMap.set(pseudo, variables);
    455    this.startingStyleVariablesMap.set(pseudo, startingStyleVariables);
    456 
    457    const rulesEditors = new Set();
    458    const variableTree = new Map();
    459 
    460    if (!this.usedVariables) {
    461      this.usedVariables = new Set();
    462    } else {
    463      this.usedVariables.clear();
    464    }
    465 
    466    // For each TextProperty, mark it overridden if all of its computed
    467    // properties are marked overridden. Update the text property's associated
    468    // editor, if any. This will clear the _overriddenDirty state on all
    469    // computed properties. For each editor we also show or hide the inactive
    470    // CSS icon as needed.
    471    for (const textProp of textProps) {
    472      // _updatePropertyOverridden will return true if the
    473      // overridden state has changed for the text property.
    474      // _hasUpdatedCSSVariable will return true if the declaration contains any
    475      // of the updated CSS variable names.
    476      if (
    477        this._updatePropertyOverridden(textProp) ||
    478        this._hasUpdatedCSSVariable(textProp, changedVariableNamesSet) ||
    479        this._hasUpdatedCSSVariable(
    480          textProp,
    481          changedStartingStyleVariableNamesSet
    482        )
    483      ) {
    484        textProp.updateEditor();
    485      }
    486 
    487      // For each editor show or hide the inactive CSS icon as needed.
    488      if (textProp.editor) {
    489        textProp.editor.updateUI();
    490      }
    491 
    492      // First we need to update used variables from all declarations
    493      textProp.updateUsedVariables();
    494      const isCustomProperty = textProp.name.startsWith("--");
    495      const isNewCustomProperty =
    496        isCustomProperty && textProp.isPropertyChanged;
    497      if (isNewCustomProperty) {
    498        this.usedVariables.add(textProp.name);
    499      }
    500      if (textProp.usedVariables) {
    501        if (!isCustomProperty) {
    502          for (const variable of textProp.usedVariables) {
    503            this.usedVariables.add(variable);
    504          }
    505        } else {
    506          variableTree.set(textProp.name, textProp.usedVariables);
    507        }
    508      }
    509 
    510      if (textProp.rule.editor) {
    511        rulesEditors.add(textProp.rule.editor);
    512      }
    513    }
    514 
    515    const collectVariableDependencies = variable => {
    516      if (!variableTree.has(variable)) {
    517        return;
    518      }
    519 
    520      for (const dep of variableTree.get(variable)) {
    521        if (!this.usedVariables.has(dep)) {
    522          this.usedVariables.add(dep);
    523          collectVariableDependencies(dep);
    524        }
    525      }
    526    };
    527 
    528    for (const variable of this.usedVariables) {
    529      collectVariableDependencies(variable);
    530    }
    531 
    532    for (const textProp of textProps) {
    533      // Then we need to update the isUnusedCssVariable
    534      textProp.updateIsUnusedVariable();
    535    }
    536 
    537    // Then update the UI
    538    for (const ruleEditor of rulesEditors) {
    539      ruleEditor.updateUnusedCssVariables();
    540    }
    541  }
    542 
    543  /**
    544   * Return whether or not the passed computed property has a higher priority than
    545   * a computed property seen "earlier" (e.g. whose rule had higher priority, or that
    546   * was declared in the same rule, but earlier).
    547   *
    548   * @param {object} computedProp: A computed prop object, as stored in TextProp#computed
    549   * @param {object} earlierProp: The computed prop to compare against
    550   * @returns Boolean
    551   */
    552  _hasHigherPriorityThanEarlierProp(computedProp, earlierProp) {
    553    if (!earlierProp) {
    554      return false;
    555    }
    556 
    557    if (computedProp.priority !== "important") {
    558      return false;
    559    }
    560 
    561    const rule = computedProp.textProp.rule;
    562    const earlierRule = earlierProp.textProp.rule;
    563 
    564    // for only consider rules applying to the same node.
    565    if (rule.inherited !== earlierRule.inherited) {
    566      return false;
    567    }
    568 
    569    // only consider rules applying on the same (inherited) pseudo element (e.g. ::details-content),
    570    // or rules both not applying to pseudo elements
    571    if (rule.pseudoElement !== earlierRule.pseudoElement) {
    572      return false;
    573    }
    574 
    575    // At this point, the computed prop is important, and it applies to the same element
    576    // (or pseudo element) than the earlier prop.
    577    return (
    578      earlierProp.priority !== "important" ||
    579      // Even if the earlier property was important, if the current rule is in a layer
    580      // it will take precedence, unless the earlier property rule was in the same layer…
    581      (rule?.isInLayer() &&
    582        rule.isInDifferentLayer(earlierRule) &&
    583        // … or if the earlier declaration is in the style attribute (https://www.w3.org/TR/css-cascade-5/#style-attr).
    584        earlierRule.domRule.type !== ELEMENT_STYLE)
    585    );
    586  }
    587 
    588  /**
    589   * Update CSS variable tooltip information on textProp editor when registered property
    590   * are added/modified/removed.
    591   *
    592   * @param {Set<string>} registeredPropertyNamesSet: A Set containing the name of the
    593   *                      registered properties which were added/modified/removed.
    594   */
    595  onRegisteredPropertiesChange(registeredPropertyNamesSet) {
    596    for (const rule of this.rules) {
    597      for (const textProp of rule.textProps) {
    598        if (this._hasUpdatedCSSVariable(textProp, registeredPropertyNamesSet)) {
    599          textProp.updateEditor();
    600        }
    601      }
    602    }
    603  }
    604 
    605  /**
    606   * Returns true if the given declaration's property value contains a CSS variable
    607   * matching any of the updated CSS variable names.
    608   *
    609   * @param {TextProperty} declaration
    610   *        A TextProperty of a rule.
    611   * @param {Set<string>} variableNamesSet
    612   *        A Set of CSS variable names that have been updated.
    613   */
    614  _hasUpdatedCSSVariable(declaration, variableNamesSet) {
    615    if (variableNamesSet.size === 0) {
    616      return false;
    617    }
    618 
    619    return !variableNamesSet.isDisjointFrom(declaration.usedVariables);
    620  }
    621 
    622  /**
    623   * Helper for |this.updateDeclarations()| to mark CSS declarations as overridden.
    624   *
    625   * Returns an array of CSS declarations (aka TextProperty instances) from all rules
    626   * applicable to the selected element ordered from more- to less-specific.
    627   *
    628   * If a pseudo-element type is given, restrict the result only to declarations
    629   * applicable to that pseudo-element.
    630   *
    631   * NOTE: this method skips CSS declarations in @keyframes rules because a number of
    632   * criteria such as time and animation delay need to be checked in order to determine
    633   * if the property is overridden at runtime.
    634   *
    635   * @param  {string} pseudo
    636   *         Optional pseudo-element for which to restrict marking CSS declarations as
    637   *         overridden. If omitted, only declarations for regular style rules are
    638   *         returned (no pseudo-element style rules).
    639   *
    640   * @return {Array}
    641   *         Array of TextProperty instances.
    642   */
    643  _getDeclarations(pseudo = "") {
    644    const textProps = [];
    645 
    646    for (const rule of this.rules) {
    647      // Skip @keyframes rules
    648      if (rule.keyframes) {
    649        continue;
    650      }
    651 
    652      const isNestedDeclarations = rule.domRule.isNestedDeclarations;
    653      const isInherited = !!rule.inherited;
    654 
    655      // Style rules must be considered only when they have selectors that match the node.
    656      // When renaming a selector, the unmatched rule lingers in the Rule view, but it no
    657      // longer matches the node. This strict check avoids accidentally causing
    658      // declarations to be overridden in the remaining matching rules.
    659      const isStyleRule =
    660        rule.pseudoElement === "" && rule.matchedSelectorIndexes.length;
    661 
    662      // Style rules for pseudo-elements must always be considered, regardless if their
    663      // selector matches the node. As a convenience, declarations in rules for
    664      // pseudo-elements show up in a separate Pseudo-elements accordion when selecting
    665      // the host node (instead of the pseudo-element node directly, which is sometimes
    666      // impossible, for example with ::selection or ::first-line).
    667      // Loosening the strict check on matched selectors ensures these declarations
    668      // participate in the algorithm below to mark them as overridden.
    669      const isMatchingPseudoElementRule =
    670        rule.pseudoElement !== "" &&
    671        rule.pseudoElement === pseudo &&
    672        // Inherited pseudo element rules don't appear in the "Pseudo elements" section,
    673        // so they should be considered style rules.
    674        !isInherited;
    675      const isInheritedPseudoElementRule =
    676        rule.pseudoElement !== "" && isInherited;
    677 
    678      const isElementStyle = rule.domRule.type === ELEMENT_STYLE;
    679      const isElementAttributesStyle = rule.domRule.type === PRES_HINTS;
    680 
    681      const filterCondition =
    682        isNestedDeclarations ||
    683        (pseudo && isMatchingPseudoElementRule) ||
    684        (pseudo === "" &&
    685          (isStyleRule ||
    686            isElementStyle ||
    687            isElementAttributesStyle ||
    688            isInheritedPseudoElementRule));
    689 
    690      // Collect all relevant CSS declarations (aka TextProperty instances).
    691      if (filterCondition) {
    692        for (const textProp of rule.textProps.toReversed()) {
    693          if (textProp.enabled) {
    694            textProps.push(textProp);
    695          }
    696        }
    697      }
    698    }
    699 
    700    return textProps;
    701  }
    702 
    703  /**
    704   * Adds a new declaration to the rule.
    705   *
    706   * @param  {string} ruleId
    707   *         The id of the Rule to be modified.
    708   * @param  {string} value
    709   *         The new declaration value.
    710   */
    711  addNewDeclaration(ruleId, value) {
    712    const rule = this.getRule(ruleId);
    713    if (!rule) {
    714      return;
    715    }
    716 
    717    const declarationsToAdd = parseNamedDeclarations(
    718      this.cssProperties.isKnown,
    719      value,
    720      true
    721    );
    722    if (!declarationsToAdd.length) {
    723      return;
    724    }
    725 
    726    this._addMultipleDeclarations(rule, declarationsToAdd);
    727  }
    728 
    729  /**
    730   * Adds a new rule. The rules view is updated from a "stylesheet-updated" event
    731   * emitted the PageStyleActor as a result of the rule being inserted into the
    732   * the stylesheet.
    733   */
    734  async addNewRule() {
    735    await this.pageStyle.addNewRule(
    736      this.element,
    737      this.element.pseudoClassLocks
    738    );
    739  }
    740 
    741  /**
    742   * Given the id of the rule and the new declaration name, modifies the existing
    743   * declaration name to the new given value.
    744   *
    745   * @param  {string} ruleId
    746   *         The Rule id of the given CSS declaration.
    747   * @param  {string} declarationId
    748   *         The TextProperty id for the CSS declaration.
    749   * @param  {string} name
    750   *         The new declaration name.
    751   */
    752  async modifyDeclarationName(ruleId, declarationId, name) {
    753    const rule = this.getRule(ruleId);
    754    if (!rule) {
    755      return;
    756    }
    757 
    758    const declaration = rule.getDeclaration(declarationId);
    759    if (!declaration || declaration.name === name) {
    760      return;
    761    }
    762 
    763    // Adding multiple rules inside of name field overwrites the current
    764    // property with the first, then adds any more onto the property list.
    765    const declarations = parseDeclarations(this.cssProperties.isKnown, name);
    766    if (!declarations.length) {
    767      return;
    768    }
    769 
    770    await declaration.setName(declarations[0].name);
    771 
    772    if (!declaration.enabled) {
    773      await declaration.setEnabled(true);
    774    }
    775  }
    776 
    777  /**
    778   * Helper function to addNewDeclaration() and modifyDeclarationValue() for
    779   * adding multiple declarations to a rule.
    780   *
    781   * @param  {Rule} rule
    782   *         The Rule object to write new declarations to.
    783   * @param  {Array<object>} declarationsToAdd
    784   *         An array of object containg the parsed declaration data to be added.
    785   * @param  {TextProperty|null} siblingDeclaration
    786   *         Optional declaration next to which the new declaration will be added.
    787   */
    788  _addMultipleDeclarations(rule, declarationsToAdd, siblingDeclaration = null) {
    789    for (const { commentOffsets, name, value, priority } of declarationsToAdd) {
    790      const isCommented = Boolean(commentOffsets);
    791      const enabled = !isCommented;
    792      siblingDeclaration = rule.createProperty(
    793        name,
    794        value,
    795        priority,
    796        enabled,
    797        siblingDeclaration
    798      );
    799    }
    800  }
    801 
    802  /**
    803   * Parse a value string and break it into pieces, starting with the
    804   * first value, and into an array of additional declarations (if any).
    805   *
    806   * Example: Calling with "red; width: 100px" would return
    807   * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
    808   *
    809   * @param  {string} value
    810   *         The string to parse.
    811   * @return {object} An object with the following properties:
    812   *         firstValue: A string containing a simple value, like
    813   *                     "red" or "100px!important"
    814   *         declarationsToAdd: An array with additional declarations, following the
    815   *                            parseDeclarations format of { name, value, priority }
    816   */
    817  _getValueAndExtraProperties(value) {
    818    // The inplace editor will prevent manual typing of multiple declarations,
    819    // but we need to deal with the case during a paste event.
    820    // Adding multiple declarations inside of value editor sets value with the
    821    // first, then adds any more onto the declaration list (below this declarations).
    822    let firstValue = value;
    823    let declarationsToAdd = [];
    824 
    825    const declarations = parseDeclarations(this.cssProperties.isKnown, value);
    826 
    827    // Check to see if the input string can be parsed as multiple declarations
    828    if (declarations.length) {
    829      // Get the first property value (if any), and any remaining
    830      // declarations (if any)
    831      if (!declarations[0].name && declarations[0].value) {
    832        firstValue = declarations[0].value;
    833        declarationsToAdd = declarations.slice(1);
    834      } else if (declarations[0].name && declarations[0].value) {
    835        // In some cases, the value could be a property:value pair
    836        // itself.  Join them as one value string and append
    837        // potentially following declarations
    838        firstValue = declarations[0].name + ": " + declarations[0].value;
    839        declarationsToAdd = declarations.slice(1);
    840      }
    841    }
    842 
    843    return {
    844      declarationsToAdd,
    845      firstValue,
    846    };
    847  }
    848 
    849  /**
    850   * Given the id of the rule and the new declaration value, modifies the existing
    851   * declaration value to the new given value.
    852   *
    853   * @param  {string} ruleId
    854   *         The Rule id of the given CSS declaration.
    855   * @param  {string} declarationId
    856   *         The TextProperty id for the CSS declaration.
    857   * @param  {string} value
    858   *         The new declaration value.
    859   */
    860  async modifyDeclarationValue(ruleId, declarationId, value) {
    861    const rule = this.getRule(ruleId);
    862    if (!rule) {
    863      return;
    864    }
    865 
    866    const declaration = rule.getDeclaration(declarationId);
    867    if (!declaration) {
    868      return;
    869    }
    870 
    871    const { declarationsToAdd, firstValue } =
    872      this._getValueAndExtraProperties(value);
    873    const parsedValue = parseSingleValue(
    874      this.cssProperties.isKnown,
    875      firstValue
    876    );
    877 
    878    if (
    879      !declarationsToAdd.length &&
    880      declaration.value === parsedValue.value &&
    881      declaration.priority === parsedValue.priority
    882    ) {
    883      return;
    884    }
    885 
    886    // First, set this declaration value (common case, only modified a property)
    887    await declaration.setValue(parsedValue.value, parsedValue.priority);
    888 
    889    if (!declaration.enabled) {
    890      await declaration.setEnabled(true);
    891    }
    892 
    893    this._addMultipleDeclarations(rule, declarationsToAdd, declaration);
    894  }
    895 
    896  /**
    897   * Modifies the existing rule's selector to the new given value.
    898   *
    899   * @param  {string} ruleId
    900   *         The id of the Rule to be modified.
    901   * @param  {string} selector
    902   *         The new selector value.
    903   */
    904  async modifySelector(ruleId, selector) {
    905    try {
    906      const rule = this.getRule(ruleId);
    907      if (!rule) {
    908        return;
    909      }
    910 
    911      const response = await rule.domRule.modifySelector(
    912        this.element,
    913        selector
    914      );
    915      const { ruleProps, isMatching } = response;
    916 
    917      if (!ruleProps) {
    918        // Notify for changes, even when nothing changes, just to allow tests
    919        // being able to track end of this request.
    920        this.ruleView.emit("ruleview-invalid-selector");
    921        return;
    922      }
    923 
    924      const newRule = new Rule(this, {
    925        ...ruleProps,
    926        isUnmatched: !isMatching,
    927      });
    928 
    929      // Recompute the list of applied styles because editing a
    930      // selector might cause this rule's position to change.
    931      const appliedStyles = await this.pageStyle.getApplied(this.element, {
    932        inherited: true,
    933        matchedSelectors: true,
    934        filter: this.showUserAgentStyles ? "ua" : undefined,
    935      });
    936      const newIndex = appliedStyles.findIndex(r => r.rule == ruleProps.rule);
    937      const oldIndex = this.rules.indexOf(rule);
    938 
    939      // Remove the old rule and insert the new rule according to where it appears
    940      // in the list of applied styles.
    941      this.rules.splice(oldIndex, 1);
    942      // If the selector no longer matches, then we leave the rule in
    943      // the same relative position.
    944      this.rules.splice(newIndex === -1 ? oldIndex : newIndex, 0, newRule);
    945 
    946      // Recompute, mark and update the UI for any properties that are
    947      // overridden or contain inactive CSS according to the new list of rules.
    948      this.onRuleUpdated();
    949 
    950      // In order to keep the new rule in place of the old in the rules view, we need
    951      // to remove the rule again if the rule was inserted to its new index according
    952      // to the list of applied styles.
    953      // Note: you might think we would replicate the list-modification logic above,
    954      // but that is complicated due to the way the UI installs pseudo-element rules
    955      // and the like.
    956      if (newIndex !== -1) {
    957        this.rules.splice(newIndex, 1);
    958        this.rules.splice(oldIndex, 0, newRule);
    959      }
    960      this._changed();
    961    } catch (e) {
    962      console.error(e);
    963    }
    964  }
    965 
    966  /**
    967   * Toggles the enabled state of the given CSS declaration.
    968   *
    969   * @param  {string} ruleId
    970   *         The Rule id of the given CSS declaration.
    971   * @param  {string} declarationId
    972   *         The TextProperty id for the CSS declaration.
    973   */
    974  toggleDeclaration(ruleId, declarationId) {
    975    const rule = this.getRule(ruleId);
    976    if (!rule) {
    977      return;
    978    }
    979 
    980    const declaration = rule.getDeclaration(declarationId);
    981    if (!declaration) {
    982      return;
    983    }
    984 
    985    declaration.setEnabled(!declaration.enabled);
    986  }
    987 
    988  /**
    989   * Mark a given TextProperty as overridden or not depending on the
    990   * state of its computed properties. Clears the _overriddenDirty state
    991   * on all computed properties.
    992   *
    993   * @param  {TextProperty} prop
    994   *         The text property to update.
    995   * @return {boolean} true if the TextProperty's overridden state (or any of
    996   *         its computed properties overridden state) changed.
    997   */
    998  _updatePropertyOverridden(prop) {
    999    if (!prop.isValid() && !prop.computed.length) {
   1000      prop.overridden = false;
   1001      return false;
   1002    }
   1003 
   1004    let overridden = true;
   1005    let dirty = false;
   1006 
   1007    for (const computedProp of prop.computed) {
   1008      if (!computedProp.overridden) {
   1009        overridden = false;
   1010      }
   1011 
   1012      dirty = computedProp._overriddenDirty || dirty;
   1013      delete computedProp._overriddenDirty;
   1014    }
   1015 
   1016    dirty = !!prop.overridden !== overridden || dirty;
   1017    prop.overridden = overridden;
   1018    return dirty;
   1019  }
   1020 
   1021  /**
   1022   * Returns data about a CSS variable.
   1023   *
   1024   * @param  {string} name
   1025   *         The name of the variable.
   1026   * @param  {string} pseudo
   1027   *         The pseudo-element name of the rule.
   1028   * @return {object} An object with the following properties:
   1029   *         - {String|undefined} value: The variable's value. Undefined if variable is not set.
   1030   *         - {RegisteredPropertyResource|undefined} registeredProperty: The registered
   1031   *           property data (syntax, initial value, inherits). Undefined if the variable
   1032   *           is not a registered property.
   1033   */
   1034  getVariableData(name, pseudo = "") {
   1035    const variables = this.variablesMap.get(pseudo);
   1036    const startingStyleVariables = this.startingStyleVariablesMap.get(pseudo);
   1037    const registeredPropertiesMap =
   1038      this.ruleView.getRegisteredPropertiesForSelectedNodeTarget();
   1039 
   1040    const data = {};
   1041    if (variables?.has(name)) {
   1042      // XXX Check what to do in case the value doesn't match the registered property syntax.
   1043      // Will be handled in Bug 1866712
   1044      const { declarationValue, computedValue } = variables.get(name);
   1045      data.value = declarationValue;
   1046      data.computedValue = computedValue;
   1047    }
   1048    if (startingStyleVariables?.has(name)) {
   1049      data.startingStyle = startingStyleVariables.get(name);
   1050    }
   1051    if (registeredPropertiesMap?.has(name)) {
   1052      data.registeredProperty = registeredPropertiesMap.get(name);
   1053    }
   1054 
   1055    return data;
   1056  }
   1057 
   1058  /**
   1059   * Get all custom properties.
   1060   *
   1061   * @param  {string} pseudo
   1062   *         The pseudo-element name of the rule.
   1063   * @returns Map<String, String> A map whose key is the custom property name and value is
   1064   *                              the custom property value (or registered property initial
   1065   *                              value if the property is not defined)
   1066   */
   1067  getAllCustomProperties(pseudo = "") {
   1068    const customProperties = new Map();
   1069    for (const [
   1070      key,
   1071      { computedValue, declarationValue },
   1072    ] of this.variablesMap.get(pseudo)) {
   1073      customProperties.set(key, computedValue ?? declarationValue);
   1074    }
   1075 
   1076    const startingStyleCustomProperties =
   1077      this.startingStyleVariablesMap.get(pseudo);
   1078 
   1079    const registeredPropertiesMap =
   1080      this.ruleView.getRegisteredPropertiesForSelectedNodeTarget();
   1081 
   1082    // If there's no registered properties nor starting style ones, we can return the Map as is
   1083    if (
   1084      (!registeredPropertiesMap || registeredPropertiesMap.size === 0) &&
   1085      (!startingStyleCustomProperties ||
   1086        startingStyleCustomProperties.size === 0)
   1087    ) {
   1088      return customProperties;
   1089    }
   1090 
   1091    if (startingStyleCustomProperties) {
   1092      for (const [name, value] of startingStyleCustomProperties) {
   1093        // Only set the starting style property if it's not defined (i.e. not in the "main"
   1094        // variable map)
   1095        if (!customProperties.has(name)) {
   1096          customProperties.set(name, value);
   1097        }
   1098      }
   1099    }
   1100 
   1101    if (registeredPropertiesMap) {
   1102      for (const [name, propertyDefinition] of registeredPropertiesMap) {
   1103        // Only set the registered property if it's not defined (i.e. not in the variable map)
   1104        if (!customProperties.has(name)) {
   1105          customProperties.set(name, propertyDefinition.initialValue);
   1106        }
   1107      }
   1108    }
   1109 
   1110    return customProperties;
   1111  }
   1112 }
   1113 
   1114 module.exports = ElementStyle;