tor-browser

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

text-property.js (14443B)


      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 { generateUUID } = require("resource://devtools/shared/generate-uuid.js");
      8 const {
      9  COMPATIBILITY_TOOLTIP_MESSAGE,
     10 } = require("resource://devtools/client/inspector/rules/constants.js");
     11 
     12 loader.lazyRequireGetter(
     13  this,
     14  "escapeCSSComment",
     15  "resource://devtools/shared/css/parsing-utils.js",
     16  true
     17 );
     18 
     19 loader.lazyRequireGetter(
     20  this,
     21  "getCSSVariables",
     22  "resource://devtools/client/inspector/rules/utils/utils.js",
     23  true
     24 );
     25 
     26 /**
     27 * TextProperty is responsible for the following:
     28 *   Manages a single property from the authoredText attribute of the
     29 *     relevant declaration.
     30 *   Maintains a list of computed properties that come from this
     31 *     property declaration.
     32 *   Changes to the TextProperty are sent to its related Rule for
     33 *     application.
     34 */
     35 class TextProperty {
     36  /**
     37   * @param {object} options
     38   * @param {Rule} options.rule
     39   *        The rule this TextProperty came from.
     40   * @param {string} options.name
     41   *        The text property name (such as "background" or "border-top").
     42   * @param {string} options.value
     43   *        The property's value (not including priority).
     44   * @param {string} options.priority
     45   *        The property's priority (either "important" or an empty string).
     46   * @param {boolean} options.enabled
     47   *        Whether the property is enabled.
     48   * @param {boolean} options.invisible
     49   *        Whether the property is invisible. In an inherited rule, only show
     50   *        the inherited declarations. The other declarations are considered
     51   *        invisible and does not show up in the UI. These are needed so that
     52   *        the index of a property in Rule.textProps is the same as the index
     53   *        coming from parseDeclarations.
     54   */
     55  constructor({
     56    rule,
     57    name,
     58    value,
     59    priority,
     60    enabled = true,
     61    invisible = false,
     62  }) {
     63    this.id = name + "_" + generateUUID().toString();
     64    this.rule = rule;
     65    this.name = name;
     66    this.value = value;
     67    this.priority = priority;
     68    this.enabled = !!enabled;
     69    this.invisible = invisible;
     70    this.elementStyle = this.rule.elementStyle;
     71    this.cssProperties = this.elementStyle.ruleView.cssProperties;
     72    this.panelDoc = this.elementStyle.ruleView.inspector.panelDoc;
     73    this.userProperties = this.elementStyle.store.userProperties;
     74    // Names of CSS variables used in the value of this declaration.
     75    this.usedVariables = new Set();
     76 
     77    this.updateComputed();
     78    this.updateUsedVariables();
     79    this.updateIsUnusedVariable();
     80  }
     81 
     82  get computedProperties() {
     83    return this.computed
     84      .filter(computed => computed.name !== this.name)
     85      .map(computed => {
     86        return {
     87          isOverridden: computed.overridden,
     88          name: computed.name,
     89          priority: computed.priority,
     90          value: computed.value,
     91        };
     92      });
     93  }
     94 
     95  /**
     96   * Returns whether or not the declaration's name is known.
     97   *
     98   * @return {boolean} true if the declaration name is known, false otherwise.
     99   */
    100  get isKnownProperty() {
    101    return this.cssProperties.isKnown(this.name);
    102  }
    103 
    104  /**
    105   * Returns whether or not the declaration is changed by the user.
    106   *
    107   * @return {boolean} true if the declaration is changed by the user, false
    108   * otherwise.
    109   */
    110  get isPropertyChanged() {
    111    return this.userProperties.contains(this.rule.domRule, this.name);
    112  }
    113 
    114  /**
    115   * Update the editor associated with this text property,
    116   * if any.
    117   */
    118  updateEditor() {
    119    // When the editor updates, reset the saved
    120    // compatibility issues list as any updates
    121    // may alter the compatibility status of declarations
    122    this.rule.compatibilityIssues = null;
    123    if (this.editor) {
    124      this.editor.update();
    125    }
    126  }
    127 
    128  /**
    129   * Update the list of computed properties for this text property.
    130   */
    131  updateComputed() {
    132    if (!this.name) {
    133      return;
    134    }
    135 
    136    // This is a bit funky.  To get the list of computed properties
    137    // for this text property, we'll set the property on a dummy element
    138    // and see what the computed style looks like.
    139    const dummyElement = this.elementStyle.ruleView.dummyElement;
    140    const dummyStyle = dummyElement.style;
    141    dummyStyle.cssText = "";
    142    dummyStyle.setProperty(this.name, this.value, this.priority);
    143 
    144    this.computed = [];
    145 
    146    // Manually get all the properties that are set when setting a value on
    147    // this.name and check the computed style on dummyElement for each one.
    148    // If we just read dummyStyle, it would skip properties when value === "".
    149    const subProps = this.cssProperties.getSubproperties(this.name);
    150 
    151    for (const prop of subProps) {
    152      this.computed.push({
    153        textProp: this,
    154        name: prop,
    155        value: dummyStyle.getPropertyValue(prop),
    156        priority: dummyStyle.getPropertyPriority(prop),
    157      });
    158    }
    159  }
    160 
    161  /**
    162   * Extract all CSS variable names used in this declaration's value into a Set for
    163   * easy querying. Call this method any time the declaration's value changes.
    164   */
    165  updateUsedVariables() {
    166    this.usedVariables.clear();
    167 
    168    for (const variable of getCSSVariables(this.value)) {
    169      this.usedVariables.add(variable);
    170    }
    171  }
    172 
    173  /**
    174   * Sets this.isUnusedVariable
    175   */
    176  updateIsUnusedVariable() {
    177    this.isUnusedVariable =
    178      this.name.startsWith("--") &&
    179      // If an editor was created for the declaration, never hide it back
    180      !this.editor &&
    181      // Don't consider user-added variables, custom properties whose name is the same as
    182      // user-added variables, to be unused (we do want to display those to avoid confusion
    183      // for the user.
    184      !this.userProperties.containsName(this.name) &&
    185      this.elementStyle.usedVariables &&
    186      !this.elementStyle.usedVariables.has(this.name);
    187  }
    188 
    189  /**
    190   * Set all the values from another TextProperty instance into
    191   * this TextProperty instance.
    192   *
    193   * @param {TextProperty} prop
    194   *        The other TextProperty instance.
    195   */
    196  set(prop) {
    197    let changed = false;
    198    for (const item of ["name", "value", "priority", "enabled"]) {
    199      if (this[item] !== prop[item]) {
    200        this[item] = prop[item];
    201        changed = true;
    202      }
    203    }
    204 
    205    if (changed) {
    206      this.updateUsedVariables();
    207      this.updateEditor();
    208    }
    209  }
    210 
    211  setValue(value, priority, force = false) {
    212    if (value !== this.value || force) {
    213      this.userProperties.setProperty(this.rule.domRule, this.name, value);
    214    }
    215    return this.rule.setPropertyValue(this, value, priority).then(() => {
    216      this.updateUsedVariables();
    217      this.updateEditor();
    218    });
    219  }
    220 
    221  /**
    222   * Called when the property's value has been updated externally, and
    223   * the property and editor should update to reflect that value.
    224   *
    225   * @param {string} value
    226   *        Property value
    227   */
    228  updateValue(value) {
    229    if (value !== this.value) {
    230      this.value = value;
    231      this.updateUsedVariables();
    232      this.updateEditor();
    233    }
    234  }
    235 
    236  async setName(name) {
    237    if (name !== this.name) {
    238      this.userProperties.setProperty(this.rule.domRule, name, this.value);
    239    }
    240 
    241    await this.rule.setPropertyName(this, name);
    242    this.updateEditor();
    243  }
    244 
    245  setEnabled(value) {
    246    this.rule.setPropertyEnabled(this, value);
    247    this.updateEditor();
    248  }
    249 
    250  remove() {
    251    this.rule.removeProperty(this);
    252  }
    253 
    254  /**
    255   * Return a string representation of the rule property.
    256   */
    257  stringifyProperty() {
    258    // Get the displayed property value
    259    let declaration = this.name + ": " + this.value;
    260 
    261    if (this.priority) {
    262      declaration += " !" + this.priority;
    263    }
    264 
    265    declaration += ";";
    266 
    267    // Comment out property declarations that are not enabled
    268    if (!this.enabled) {
    269      declaration = "/* " + escapeCSSComment(declaration) + " */";
    270    }
    271 
    272    return declaration;
    273  }
    274 
    275  /**
    276   * Returns the associated StyleRule declaration if it exists
    277   *
    278   * @returns {object | undefined}
    279   */
    280  #getDomRuleDeclaration() {
    281    const selfIndex = this.rule.textProps.indexOf(this);
    282    return this.rule.domRule.declarations?.[selfIndex];
    283  }
    284 
    285  /**
    286   * Validate this property. Does it make sense for this value to be assigned
    287   * to this property name?
    288   *
    289   * @return {boolean} true if the whole CSS declaration is valid, false otherwise.
    290   */
    291  isValid() {
    292    const declaration = this.#getDomRuleDeclaration();
    293 
    294    // When adding a new property in the rule-view, the TextProperty object is
    295    // created right away before the rule gets updated on the server, so we're
    296    // not going to find the corresponding declaration object yet. Default to
    297    // true.
    298    if (!declaration) {
    299      return true;
    300    }
    301 
    302    return declaration.isValid;
    303  }
    304 
    305  /**
    306   * Returns an object with properties explaining why the property is inactive, if it is.
    307   * If it's not inactive, this returns undefined.
    308   *
    309   * @returns {object | undefined}
    310   */
    311  getInactiveCssData() {
    312    const declaration = this.#getDomRuleDeclaration();
    313 
    314    if (!declaration) {
    315      return undefined;
    316    }
    317 
    318    return declaration.inactiveCssData;
    319  }
    320 
    321  /**
    322   * Get compatibility issue linked with the textProp.
    323   *
    324   * @returns  A JSON objects with compatibility information in following form:
    325   *    {
    326   *      // A boolean to denote the compatibility status
    327   *      isCompatible: <boolean>,
    328   *      // The CSS declaration that has compatibility issues
    329   *      property: <string>,
    330   *      // The un-aliased root CSS declaration for the given property
    331   *      rootProperty: <string>,
    332   *      // The l10n message id for the tooltip message
    333   *      msgId: <string>,
    334   *      // Link to MDN documentation for the rootProperty
    335   *      url: <string>,
    336   *      // An array of all the browsers that don't support the given CSS rule
    337   *      unsupportedBrowsers: <Array>,
    338   *    }
    339   */
    340  async isCompatible() {
    341    // This is a workaround for Bug 1648339
    342    // https://bugzilla.mozilla.org/show_bug.cgi?id=1648339
    343    // that makes the tooltip icon inconsistent with the
    344    // position of the rule it is associated with. Once solved,
    345    // the compatibility data can be directly accessed from the
    346    // declaration and this logic can be used to set isCompatible
    347    // property directly to domRule in StyleRuleActor's form() method.
    348    if (!this.enabled) {
    349      return { isCompatible: true };
    350    }
    351 
    352    const compatibilityIssues = await this.rule.getCompatibilityIssues();
    353    if (!compatibilityIssues.length) {
    354      return { isCompatible: true };
    355    }
    356 
    357    const property = this.name;
    358    const indexOfProperty = compatibilityIssues.findIndex(
    359      issue => issue.property === property || issue.aliases?.includes(property)
    360    );
    361 
    362    if (indexOfProperty < 0) {
    363      return { isCompatible: true };
    364    }
    365 
    366    const {
    367      property: rootProperty,
    368      deprecated,
    369      experimental,
    370      specUrl,
    371      url,
    372      unsupportedBrowsers,
    373    } = compatibilityIssues[indexOfProperty];
    374 
    375    let msgId = COMPATIBILITY_TOOLTIP_MESSAGE.default;
    376    if (deprecated && experimental && !unsupportedBrowsers.length) {
    377      msgId =
    378        COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental-supported"];
    379    } else if (deprecated && experimental) {
    380      msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental"];
    381    } else if (deprecated && !unsupportedBrowsers.length) {
    382      msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"];
    383    } else if (deprecated) {
    384      msgId = COMPATIBILITY_TOOLTIP_MESSAGE.deprecated;
    385    } else if (experimental && !unsupportedBrowsers.length) {
    386      msgId = COMPATIBILITY_TOOLTIP_MESSAGE["experimental-supported"];
    387    } else if (experimental) {
    388      msgId = COMPATIBILITY_TOOLTIP_MESSAGE.experimental;
    389    }
    390 
    391    return {
    392      isCompatible: false,
    393      property,
    394      rootProperty,
    395      msgId,
    396      specUrl,
    397      url,
    398      unsupportedBrowsers,
    399    };
    400  }
    401 
    402  /**
    403   * Validate the name of this property.
    404   *
    405   * @return {boolean} true if the property name is valid, false otherwise.
    406   */
    407  isNameValid() {
    408    const declaration = this.#getDomRuleDeclaration();
    409 
    410    // When adding a new property in the rule-view, the TextProperty object is
    411    // created right away before the rule gets updated on the server, so we're
    412    // not going to find the corresponding declaration object yet. Default to
    413    // true.
    414    if (!declaration) {
    415      return true;
    416    }
    417 
    418    return declaration.isNameValid;
    419  }
    420 
    421  /**
    422   * Returns whether the property is invalid at computed-value time.
    423   * For now, it's only computed on the server for declarations of
    424   * registered properties.
    425   *
    426   * @return {boolean}
    427   */
    428  isInvalidAtComputedValueTime() {
    429    const declaration = this.#getDomRuleDeclaration();
    430    // When adding a new property in the rule-view, the TextProperty object is
    431    // created right away before the rule gets updated on the server, so we're
    432    // not going to find the corresponding declaration object yet. Default to
    433    // false.
    434    if (!declaration) {
    435      return false;
    436    }
    437 
    438    return declaration.invalidAtComputedValueTime;
    439  }
    440 
    441  /**
    442   * Get the associated CSS variable computed value.
    443   *
    444   * @return {string}
    445   */
    446  getVariableComputedValue() {
    447    const declaration = this.#getDomRuleDeclaration();
    448    // When adding a new property in the rule-view, the TextProperty object is
    449    // created right away before the rule gets updated on the server, so we're
    450    // not going to find the corresponding declaration object yet. Default to null.
    451    if (!declaration || !declaration.isCustomProperty) {
    452      return null;
    453    }
    454 
    455    return declaration.computedValue;
    456  }
    457 
    458  /**
    459   * Returns the expected syntax for this property.
    460   * For now, it's only sent from the server for invalid at computed-value time declarations.
    461   *
    462   * @return {string | null} The expected syntax, or null.
    463   */
    464  getExpectedSyntax() {
    465    const declaration = this.#getDomRuleDeclaration();
    466    // When adding a new property in the rule-view, the TextProperty object is
    467    // created right away before the rule gets updated on the server, so we're
    468    // not going to find the corresponding declaration object yet. Default to
    469    // null.
    470    if (!declaration) {
    471      return null;
    472    }
    473 
    474    return declaration.syntax;
    475  }
    476 }
    477 
    478 module.exports = TextProperty;