tor-browser

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

Spectrum.js (11671B)


      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 { ColorPickerCommon } = ChromeUtils.importESModule(
      8  "chrome://global/content/bindings/colorpicker-common.mjs"
      9 );
     10 
     11 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     12 const {
     13  MultiLocalizationHelper,
     14 } = require("resource://devtools/shared/l10n.js");
     15 
     16 loader.lazyRequireGetter(
     17  this,
     18  ["getTextProperties", "getContrastRatioAgainstBackground"],
     19  "resource://devtools/shared/accessibility.js",
     20  true
     21 );
     22 loader.lazyGetter(this, "ColorPickerBundle", () => {
     23  return new Localization(["devtools/client/inspector.ftl"], true);
     24 });
     25 
     26 const L10N = new MultiLocalizationHelper(
     27  "devtools/client/locales/accessibility.properties",
     28  "devtools/client/locales/inspector.properties"
     29 );
     30 const XHTML_NS = "http://www.w3.org/1999/xhtml";
     31 
     32 /**
     33 * Spectrum creates a color picker widget in any container you give it.
     34 *
     35 * Simple usage example:
     36 *
     37 * const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
     38 * let s = new Spectrum(containerElement, [255, 126, 255, 1]);
     39 * s.on("changed", (rgba, color) => {
     40 *   console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
     41 *     rgba[3] + ")");
     42 * });
     43 * s.show();
     44 * s.destroy();
     45 *
     46 * Note that the color picker is hidden by default and you need to call show to
     47 * make it appear. This 2 stages initialization helps in cases you are creating
     48 * the color picker in a parent element that hasn't been appended anywhere yet
     49 * or that is hidden. Calling show() when the parent element is appended and
     50 * visible will allow spectrum to correctly initialize its various parts.
     51 *
     52 * Fires the following events:
     53 * - changed : When the user changes the current color
     54 */
     55 class Spectrum extends ColorPickerCommon {
     56  constructor(parentEl, rgb) {
     57    const element = parentEl.ownerDocument.createElement("div");
     58    // eslint-disable-next-line no-unsanitized/property
     59    element.innerHTML = `
     60    <section class="spectrum-color-picker">
     61      <div class="spectrum-color spectrum-box"
     62           tabindex="0"
     63           role="slider"
     64           aria-describedby="spectrum-dragger">
     65        <div class="spectrum-sat">
     66          <div class="spectrum-val">
     67            <div class="spectrum-dragger" id="spectrum-dragger"></div>
     68          </div>
     69        </div>
     70      </div>
     71    </section>
     72    <section class="spectrum-controls">
     73      <div class="spectrum-color-preview"></div>
     74      <div class="spectrum-slider-container">
     75        <div class="spectrum-hue spectrum-box"></div>
     76        <div class="spectrum-alpha spectrum-checker spectrum-box"></div>
     77      </div>
     78    </section>
     79    <section class="spectrum-color-contrast accessibility-color-contrast">
     80      <div class="contrast-ratio-header-and-single-ratio">
     81        <span class="contrast-ratio-label" role="presentation"></span>
     82        <span class="contrast-value-and-swatch contrast-ratio-single" role="presentation">
     83          <span class="accessibility-contrast-value"></span>
     84        </span>
     85      </div>
     86      <div class="contrast-ratio-range">
     87        <span class="contrast-value-and-swatch contrast-ratio-min" role="presentation">
     88          <span class="accessibility-contrast-value"></span>
     89        </span>
     90        <span class="accessibility-color-contrast-separator"></span>
     91        <span class="contrast-value-and-swatch contrast-ratio-max" role="presentation">
     92          <span class="accessibility-contrast-value"></span>
     93        </span>
     94      </div>
     95    </section>
     96  `;
     97    super(element);
     98    EventEmitter.decorate(this);
     99 
    100    parentEl.appendChild(this.element);
    101 
    102    // Create the eyedropper.
    103    const eyedropper = this.document.createElementNS(XHTML_NS, "button");
    104    eyedropper.id = "eyedropper-button";
    105    eyedropper.className = "devtools-button";
    106    eyedropper.style.pointerEvents = "auto";
    107    eyedropper.setAttribute(
    108      "aria-label",
    109      ColorPickerBundle.formatValueSync("colorpicker-tooltip-eyedropper-title")
    110    );
    111    this.controls.insertBefore(eyedropper, this.colorPreview);
    112 
    113    // Color contrast
    114    this.spectrumContrast = this.element.querySelector(
    115      ".spectrum-color-contrast"
    116    );
    117    this.contrastLabel = this.element.querySelector(".contrast-ratio-label");
    118    [this.contrastValue, this.contrastValueMin, this.contrastValueMax] =
    119      this.element.querySelectorAll(".accessibility-contrast-value");
    120 
    121    // Create the learn more info button
    122    const learnMore = this.document.createElementNS(XHTML_NS, "button");
    123    learnMore.id = "learn-more-button";
    124    learnMore.className = "learn-more";
    125    learnMore.title = L10N.getStr("accessibility.learnMore");
    126    this.element
    127      .querySelector(".contrast-ratio-header-and-single-ratio")
    128      .appendChild(learnMore);
    129 
    130    if (rgb) {
    131      this.rgb = rgb;
    132      this.updateUI();
    133    }
    134  }
    135 
    136  set textProps(style) {
    137    this._textProps = style
    138      ? {
    139          fontSize: style["font-size"].value,
    140          fontWeight: style["font-weight"].value,
    141          opacity: style.opacity.value,
    142        }
    143      : null;
    144  }
    145 
    146  set backgroundColorData(colorData) {
    147    this._backgroundColorData = colorData;
    148  }
    149 
    150  get backgroundColorData() {
    151    return this._backgroundColorData;
    152  }
    153 
    154  get textProps() {
    155    return this._textProps;
    156  }
    157 
    158  onChange() {
    159    this.emit("changed", this.rgb, this.rgbCssString);
    160  }
    161 
    162  /**
    163   * Updates the contrast label with appropriate content (i.e. large text indicator
    164   * if the contrast is calculated for large text, or a base label otherwise)
    165   *
    166   * @param  {boolean} isLargeText
    167   *         True if contrast is calculated for large text.
    168   */
    169  updateContrastLabel(isLargeText) {
    170    if (!isLargeText) {
    171      this.contrastLabel.textContent = L10N.getStr(
    172        "accessibility.contrast.ratio.label"
    173      );
    174      return;
    175    }
    176 
    177    const largeTextStr = L10N.getStr("accessibility.contrast.large.text");
    178    const contrastLabelStr = L10N.getFormatStr(
    179      "colorPickerTooltip.contrast.large.title",
    180      largeTextStr
    181    );
    182 
    183    // Build an array of children nodes for the contrast label element
    184    const contents = contrastLabelStr
    185      .split(new RegExp(largeTextStr), 2)
    186      .map(content => this.document.createTextNode(content));
    187    const largeTextIndicator = this.document.createElementNS(XHTML_NS, "span");
    188    largeTextIndicator.className = "accessibility-color-contrast-large-text";
    189    largeTextIndicator.textContent = largeTextStr;
    190    largeTextIndicator.title = L10N.getStr(
    191      "accessibility.contrast.large.title"
    192    );
    193    contents.splice(1, 0, largeTextIndicator);
    194 
    195    // Update contrast label
    196    this.contrastLabel.replaceChildren(...contents);
    197  }
    198 
    199  /**
    200   * Updates a contrast value element with the given score, value and swatches.
    201   *
    202   * @param  {DOMNode} el
    203   *         Contrast value element to update.
    204   * @param  {string} score
    205   *         Contrast ratio score.
    206   * @param  {number} value
    207   *         Contrast ratio value.
    208   * @param  {Array} backgroundColor
    209   *         RGBA color array for the background color to show in the swatch.
    210   */
    211  updateContrastValueEl(el, score, value, backgroundColor) {
    212    el.classList.toggle(score, true);
    213    el.textContent = value.toFixed(2);
    214    el.title = L10N.getFormatStr(
    215      `accessibility.contrast.annotation.${score}`,
    216      L10N.getFormatStr(
    217        "colorPickerTooltip.contrastAgainstBgTitle",
    218        `rgba(${backgroundColor})`
    219      )
    220    );
    221    el.parentElement.style.setProperty(
    222      "--accessibility-contrast-color",
    223      this.rgbCssString
    224    );
    225    el.parentElement.style.setProperty(
    226      "--accessibility-contrast-bg",
    227      `rgba(${backgroundColor})`
    228    );
    229  }
    230 
    231  /* Calculates the contrast ratio for the currently selected
    232   * color against a single or range of background colors and displays contrast ratio section
    233   * components depending on the contrast ratio calculated.
    234   *
    235   * Contrast ratio components include:
    236   *    - contrastLargeTextIndicator: Hidden by default, shown when text has large font
    237   *                                  size if there is no error in calculation.
    238   *    - contrastValue(s):           Set to calculated value(s), score(s) and text color on
    239   *                                  background swatches. Set to error text
    240   *                                  if there is an error in calculation.
    241   */
    242  updateContrast() {
    243    // Remove additional classes on spectrum contrast, leaving behind only base classes
    244    this.spectrumContrast.classList.toggle("visible", false);
    245    this.spectrumContrast.classList.toggle("range", false);
    246    this.spectrumContrast.classList.toggle("error", false);
    247    // Assign only base class to all contrastValues, removing any score class
    248    this.contrastValue.className =
    249      this.contrastValueMin.className =
    250      this.contrastValueMax.className =
    251        "accessibility-contrast-value";
    252 
    253    if (!this.contrastEnabled) {
    254      return;
    255    }
    256 
    257    const isRange = this.backgroundColorData.min !== undefined;
    258    this.spectrumContrast.classList.toggle("visible", true);
    259    this.spectrumContrast.classList.toggle("range", isRange);
    260 
    261    const colorContrast = getContrastRatio(
    262      {
    263        ...this.textProps,
    264        color: this.rgbCssString,
    265      },
    266      this.backgroundColorData
    267    );
    268 
    269    const {
    270      value,
    271      min,
    272      max,
    273      score,
    274      scoreMin,
    275      scoreMax,
    276      backgroundColor,
    277      backgroundColorMin,
    278      backgroundColorMax,
    279      isLargeText,
    280      error,
    281    } = colorContrast;
    282 
    283    if (error) {
    284      this.updateContrastLabel(false);
    285      this.spectrumContrast.classList.toggle("error", true);
    286 
    287      // If current background color is a range, show the error text in the contrast range
    288      // span. Otherwise, show it in the single contrast span.
    289      const contrastValEl = isRange
    290        ? this.contrastValueMin
    291        : this.contrastValue;
    292      contrastValEl.textContent = L10N.getStr("accessibility.contrast.error");
    293      contrastValEl.title = L10N.getStr(
    294        "accessibility.contrast.annotation.transparent.error"
    295      );
    296 
    297      return;
    298    }
    299 
    300    this.updateContrastLabel(isLargeText);
    301    if (!isRange) {
    302      this.updateContrastValueEl(
    303        this.contrastValue,
    304        score,
    305        value,
    306        backgroundColor
    307      );
    308 
    309      return;
    310    }
    311 
    312    this.updateContrastValueEl(
    313      this.contrastValueMin,
    314      scoreMin,
    315      min,
    316      backgroundColorMin
    317    );
    318    this.updateContrastValueEl(
    319      this.contrastValueMax,
    320      scoreMax,
    321      max,
    322      backgroundColorMax
    323    );
    324  }
    325 
    326  updateUI() {
    327    super.updateUI();
    328    this.updateContrast();
    329  }
    330 
    331  destroy() {
    332    super.destroy();
    333    this.spectrumContrast = null;
    334    this.contrastValue = this.contrastValueMin = this.contrastValueMax = null;
    335    this.contrastLabel = null;
    336  }
    337 }
    338 
    339 /**
    340 * Calculates the contrast ratio for a DOM node's computed style against
    341 * a given background.
    342 *
    343 * @param  {object} computedStyle
    344 *         The computed style for which we want to calculate the contrast ratio.
    345 * @param  {object} backgroundColor
    346 *         Object with one or more of the following properties: value, min, max
    347 * @return {object}
    348 *         An object that may contain one or more of the following fields: error,
    349 *         isLargeText, value, score for contrast.
    350 */
    351 function getContrastRatio(computedStyle, backgroundColor) {
    352  const props = getTextProperties(computedStyle);
    353 
    354  if (!props) {
    355    return {
    356      error: true,
    357    };
    358  }
    359 
    360  return getContrastRatioAgainstBackground(backgroundColor, props);
    361 }
    362 
    363 module.exports = Spectrum;