tor-browser

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

SwatchBasedEditorTooltip.js (7340B)


      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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
      9 const {
     10  HTMLTooltip,
     11 } = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
     12 
     13 loader.lazyRequireGetter(
     14  this,
     15  "KeyCodes",
     16  "resource://devtools/client/shared/keycodes.js",
     17  true
     18 );
     19 
     20 /**
     21 * Base class for all (color, gradient, ...)-swatch based value editors inside
     22 * tooltips
     23 *
     24 * @param {Document} document
     25 *        The document to attach the SwatchBasedEditorTooltip. This should be the
     26 *        toolbox document
     27 */
     28 
     29 class SwatchBasedEditorTooltip {
     30  constructor(document) {
     31    EventEmitter.decorate(this);
     32 
     33    // This one will consume outside clicks as it makes more sense to let the user
     34    // close the tooltip by clicking out
     35    // It will also close on <escape> and <enter>
     36    this.tooltip = new HTMLTooltip(document, {
     37      type: "arrow",
     38      consumeOutsideClicks: true,
     39      useXulWrapper: true,
     40    });
     41 
     42    // By default, swatch-based editor tooltips revert value change on <esc> and
     43    // commit value change on <enter>
     44    this.shortcuts = new KeyShortcuts({
     45      window: this.tooltip.doc.defaultView,
     46    });
     47    this.shortcuts.on("Escape", event => {
     48      if (!this.tooltip.isVisible()) {
     49        return;
     50      }
     51      this.revert();
     52      this.hide();
     53      event.stopPropagation();
     54      event.preventDefault();
     55    });
     56    this.shortcuts.on("Return", event => {
     57      if (!this.tooltip.isVisible()) {
     58        return;
     59      }
     60      this.commit();
     61      this.hide();
     62      event.stopPropagation();
     63      event.preventDefault();
     64    });
     65 
     66    // All target swatches are kept in a WeakMap, indexed by swatch DOM elements
     67    this.swatches = new WeakMap();
     68 
     69    // When a swatch is clicked, and for as long as the tooltip is shown, the
     70    // activeSwatch property will hold the reference to the swatch DOM element
     71    // that was clicked
     72    this.activeSwatch = null;
     73 
     74    this._onSwatchClick = this._onSwatchClick.bind(this);
     75    this._onSwatchKeyDown = this._onSwatchKeyDown.bind(this);
     76  }
     77 
     78  /**
     79   * Reports if the tooltip is currently shown
     80   *
     81   * @return {boolean} True if the tooltip is displayed.
     82   */
     83  isVisible() {
     84    return this.tooltip.isVisible();
     85  }
     86 
     87  /**
     88   * Reports if the tooltip is currently editing the targeted value
     89   *
     90   * @return {boolean} True if the tooltip is editing.
     91   */
     92  isEditing() {
     93    return this.isVisible();
     94  }
     95 
     96  /**
     97   * Show the editor tooltip for the currently active swatch.
     98   *
     99   * @return {Promise} a promise that resolves once the editor tooltip is displayed, or
    100   *         immediately if there is no currently active swatch.
    101   */
    102  show() {
    103    if (this.tooltipAnchor) {
    104      const onShown = this.tooltip.once("shown");
    105 
    106      this.tooltip.show(this.tooltipAnchor);
    107      this.tooltip.once("hidden", () => this.onTooltipHidden());
    108 
    109      return onShown;
    110    }
    111 
    112    return Promise.resolve();
    113  }
    114 
    115  /**
    116   * Can be overridden by subclasses if implementation specific behavior is needed on
    117   * tooltip hidden.
    118   */
    119  onTooltipHidden() {
    120    // When the tooltip is closed by clicking outside the panel we want to commit any
    121    // changes.
    122    if (!this._reverted) {
    123      this.commit();
    124    }
    125    this._reverted = false;
    126 
    127    // Once the tooltip is hidden we need to clean up any remaining objects.
    128    this.activeSwatch = null;
    129  }
    130 
    131  hide() {
    132    if (this.swatchActivatedWithKeyboard) {
    133      this.activeSwatch.focus();
    134      this.swatchActivatedWithKeyboard = null;
    135    }
    136 
    137    this.tooltip.hide();
    138  }
    139 
    140  /**
    141   * Add a new swatch DOM element to the list of swatch elements this editor
    142   * tooltip knows about. That means from now on, clicking on that swatch will
    143   * toggle the editor.
    144   *
    145   * @param {node} swatchEl
    146   *        The element to add
    147   * @param {object} callbacks
    148   *        Callbacks that will be executed when the editor wants to preview a
    149   *        value change, or revert a change, or commit a change.
    150   *        - onShow: will be called when one of the swatch tooltip is shown
    151   *        - onPreview: will be called when one of the sub-classes calls
    152   *        preview
    153   *        - onRevert: will be called when the user ESCapes out of the tooltip
    154   *        - onCommit: will be called when the user presses ENTER or clicks
    155   *        outside the tooltip.
    156   */
    157  addSwatch(swatchEl, callbacks = {}) {
    158    if (!callbacks.onShow) {
    159      callbacks.onShow = function () {};
    160    }
    161    if (!callbacks.onPreview) {
    162      callbacks.onPreview = function () {};
    163    }
    164    if (!callbacks.onRevert) {
    165      callbacks.onRevert = function () {};
    166    }
    167    if (!callbacks.onCommit) {
    168      callbacks.onCommit = function () {};
    169    }
    170 
    171    this.swatches.set(swatchEl, {
    172      callbacks,
    173    });
    174    swatchEl.addEventListener("click", this._onSwatchClick);
    175    swatchEl.addEventListener("keydown", this._onSwatchKeyDown);
    176  }
    177 
    178  removeSwatch(swatchEl) {
    179    if (this.swatches.has(swatchEl)) {
    180      if (this.activeSwatch === swatchEl) {
    181        this.hide();
    182        this.activeSwatch = null;
    183      }
    184      swatchEl.removeEventListener("click", this._onSwatchClick);
    185      swatchEl.removeEventListener("keydown", this._onSwatchKeyDown);
    186      this.swatches.delete(swatchEl);
    187    }
    188  }
    189 
    190  _onSwatchKeyDown(event) {
    191    if (
    192      event.keyCode === KeyCodes.DOM_VK_RETURN ||
    193      event.keyCode === KeyCodes.DOM_VK_SPACE
    194    ) {
    195      event.preventDefault();
    196      event.stopPropagation();
    197      this._onSwatchClick(event);
    198    }
    199  }
    200 
    201  _onSwatchClick(event) {
    202    const { shiftKey, clientX, clientY, target } = event;
    203 
    204    // If mouse coordinates are 0, the event listener could have been triggered
    205    // by a keybaord
    206    this.swatchActivatedWithKeyboard =
    207      event.key && clientX === 0 && clientY === 0;
    208 
    209    if (shiftKey) {
    210      event.stopPropagation();
    211      return;
    212    }
    213 
    214    const swatch = this.swatches.get(target);
    215 
    216    if (swatch) {
    217      this.activeSwatch = target;
    218      this.show();
    219      swatch.callbacks.onShow();
    220      event.stopPropagation();
    221    }
    222  }
    223 
    224  /**
    225   * Not called by this parent class, needs to be taken care of by sub-classes
    226   */
    227  preview(value) {
    228    if (this.activeSwatch) {
    229      const swatch = this.swatches.get(this.activeSwatch);
    230      swatch.callbacks.onPreview(value);
    231    }
    232  }
    233 
    234  /**
    235   * This parent class only calls this on <esc> keydown
    236   */
    237  revert() {
    238    if (this.activeSwatch) {
    239      this._reverted = true;
    240      const swatch = this.swatches.get(this.activeSwatch);
    241      this.tooltip.once("hidden", () => {
    242        swatch.callbacks.onRevert();
    243      });
    244    }
    245  }
    246 
    247  /**
    248   * This parent class only calls this on <enter> keydown
    249   */
    250  commit() {
    251    if (this.activeSwatch) {
    252      const swatch = this.swatches.get(this.activeSwatch);
    253      swatch.callbacks.onCommit();
    254    }
    255  }
    256 
    257  get tooltipAnchor() {
    258    return this.activeSwatch;
    259  }
    260 
    261  destroy() {
    262    this.activeSwatch = null;
    263    this.tooltip.off("keydown", this._onTooltipKeydown);
    264    this.tooltip.destroy();
    265    this.shortcuts.destroy();
    266  }
    267 }
    268 
    269 module.exports = SwatchBasedEditorTooltip;