tor-browser

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

TooltipToggle.js (6437B)


      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 DEFAULT_TOGGLE_DELAY = 50;
      8 
      9 /**
     10 * Tooltip helper designed to show/hide the tooltip when the mouse hovers over
     11 * particular nodes.
     12 *
     13 * This works by tracking mouse movements on a base container node (baseNode)
     14 * and showing the tooltip when the mouse stops moving. A callback can be
     15 * provided to the start() method to know whether or not the node being
     16 * hovered over should indeed receive the tooltip.
     17 */
     18 class TooltipToggle {
     19  constructor(tooltip) {
     20    this.tooltip = tooltip;
     21    this.win = tooltip.doc.defaultView;
     22 
     23    this._onMouseMove = this._onMouseMove.bind(this);
     24    this._onMouseOut = this._onMouseOut.bind(this);
     25 
     26    this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this);
     27    this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this);
     28  }
     29  /**
     30   * Start tracking mouse movements on the provided baseNode to show the
     31   * tooltip.
     32   *
     33   * 2 Ways to make this work:
     34   * - Provide a single node to attach the tooltip to, as the baseNode, and
     35   *   omit the second targetNodeCb argument
     36   * - Provide a baseNode that is the container of possibly numerous children
     37   *   elements that may receive a tooltip. In this case, provide the second
     38   *   targetNodeCb argument to decide wether or not a child should receive
     39   *   a tooltip.
     40   *
     41   * Note that if you call this function a second time, it will itself call
     42   * stop() before adding mouse tracking listeners again.
     43   *
     44   * @param {node} baseNode
     45   *        The container for all target nodes
     46   * @param {Function} targetNodeCb
     47   *        A function that accepts a node argument and that checks if a tooltip
     48   *        should be displayed. Possible return values are:
     49   *        - false (or a falsy value) if the tooltip should not be displayed
     50   *        - true if the tooltip should be displayed
     51   *        - a DOM node to display the tooltip on the returned anchor
     52   *        The function can also return a promise that will resolve to one of
     53   *        the values listed above.
     54   *        If omitted, the tooltip will be shown everytime.
     55   * @param {object} options
     56            Set of optional arguments:
     57   *        - {Number} toggleDelay
     58   *          An optional delay (in ms) that will be observed before showing
     59   *          and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
     60   *        - {Boolean} interactive
     61   *          If enabled, the tooltip is not hidden when mouse leaves the
     62   *          target element and enters the tooltip. Allows the tooltip
     63   *          content to be interactive.
     64   */
     65  start(
     66    baseNode,
     67    targetNodeCb,
     68    { toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false } = {}
     69  ) {
     70    this.stop();
     71 
     72    if (!baseNode) {
     73      // Calling tool is in the process of being destroyed.
     74      return;
     75    }
     76 
     77    this._baseNode = baseNode;
     78    this._targetNodeCb = targetNodeCb || (() => true);
     79    this._toggleDelay = toggleDelay;
     80    this._interactive = interactive;
     81 
     82    baseNode.addEventListener("mousemove", this._onMouseMove);
     83    baseNode.addEventListener("mouseout", this._onMouseOut);
     84 
     85    const target = this.tooltip.xulPanelWrapper || this.tooltip.container;
     86    if (this._interactive) {
     87      target.addEventListener("mouseover", this._onTooltipMouseOver);
     88      target.addEventListener("mouseout", this._onTooltipMouseOut);
     89    } else {
     90      target.classList.add("non-interactive-toggle");
     91    }
     92  }
     93 
     94  /**
     95   * If the start() function has been used previously, and you want to get rid
     96   * of this behavior, then call this function to remove the mouse movement
     97   * tracking
     98   */
     99  stop() {
    100    this.win.clearTimeout(this.toggleTimer);
    101 
    102    if (!this._baseNode) {
    103      return;
    104    }
    105 
    106    this._baseNode.removeEventListener("mousemove", this._onMouseMove);
    107    this._baseNode.removeEventListener("mouseout", this._onMouseOut);
    108 
    109    const target = this.tooltip.xulPanelWrapper || this.tooltip.container;
    110    if (this._interactive) {
    111      target.removeEventListener("mouseover", this._onTooltipMouseOver);
    112      target.removeEventListener("mouseout", this._onTooltipMouseOut);
    113    } else {
    114      target.classList.remove("non-interactive-toggle");
    115    }
    116 
    117    this._baseNode = null;
    118    this._targetNodeCb = null;
    119    this._lastHovered = null;
    120  }
    121 
    122  _onMouseMove(event) {
    123    if (event.target !== this._lastHovered) {
    124      this._lastHovered = event.target;
    125 
    126      this.win.clearTimeout(this.toggleTimer);
    127      this.toggleTimer = this.win.setTimeout(() => {
    128        this.tooltip.hide();
    129        this.isValidHoverTarget(event.target).then(
    130          target => {
    131            if (target === null || !this._baseNode) {
    132              // bail out if no target or if the toggle has been destroyed.
    133              return;
    134            }
    135            this.tooltip.show(target);
    136          },
    137          reason => {
    138            console.error(
    139              "isValidHoverTarget rejected with unexpected reason:"
    140            );
    141            console.error(reason);
    142          }
    143        );
    144      }, this._toggleDelay);
    145    }
    146  }
    147 
    148  /**
    149   * Is the given target DOMNode a valid node for toggling the tooltip on hover.
    150   * This delegates to the user-defined _targetNodeCb callback.
    151   *
    152   * @return {Promise} a promise that will resolve the anchor to use for the
    153   *         tooltip or null if no valid target was found.
    154   */
    155  async isValidHoverTarget(target) {
    156    const res = await this._targetNodeCb(target, this.tooltip);
    157    if (res) {
    158      return res.nodeName ? res : target;
    159    }
    160 
    161    return null;
    162  }
    163 
    164  _onMouseOut(event) {
    165    // Only hide the tooltip if the mouse leaves baseNode.
    166    if (
    167      event &&
    168      this._baseNode &&
    169      this._baseNode.contains(event.relatedTarget)
    170    ) {
    171      return;
    172    }
    173 
    174    this._lastHovered = null;
    175    this.win.clearTimeout(this.toggleTimer);
    176    this.toggleTimer = this.win.setTimeout(() => {
    177      this.tooltip.hide();
    178    }, this._toggleDelay);
    179  }
    180 
    181  _onTooltipMouseOver() {
    182    this.win.clearTimeout(this.toggleTimer);
    183  }
    184 
    185  _onTooltipMouseOut() {
    186    this.win.clearTimeout(this.toggleTimer);
    187    this.toggleTimer = this.win.setTimeout(() => {
    188      this.tooltip.hide();
    189    }, this._toggleDelay);
    190  }
    191 
    192  destroy() {
    193    this.stop();
    194  }
    195 }
    196 
    197 module.exports.TooltipToggle = TooltipToggle;