tor-browser

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

view-helpers.js (8351B)


      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 "use strict";
      5 
      6 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
      7 
      8 const PANE_APPEARANCE_DELAY = 50;
      9 
     10 var namedTimeoutsStore = new Map();
     11 
     12 /**
     13 * Helper for draining a rapid succession of events and invoking a callback
     14 * once everything settles down.
     15 *
     16 * @param string id
     17 *        A string identifier for the named timeout.
     18 * @param number wait
     19 *        The amount of milliseconds to wait after no more events are fired.
     20 * @param function callback
     21 *        Invoked when no more events are fired after the specified time.
     22 */
     23 const setNamedTimeout = function setNamedTimeout(id, wait, callback) {
     24  clearNamedTimeout(id);
     25 
     26  namedTimeoutsStore.set(
     27    id,
     28    setTimeout(() => namedTimeoutsStore.delete(id) && callback(), wait)
     29  );
     30 };
     31 exports.setNamedTimeout = setNamedTimeout;
     32 
     33 /**
     34 * Clears a named timeout.
     35 *
     36 * @see setNamedTimeout
     37 *
     38 * @param string id
     39 *        A string identifier for the named timeout.
     40 */
     41 const clearNamedTimeout = function clearNamedTimeout(id) {
     42  if (!namedTimeoutsStore) {
     43    return;
     44  }
     45  clearTimeout(namedTimeoutsStore.get(id));
     46  namedTimeoutsStore.delete(id);
     47 };
     48 exports.clearNamedTimeout = clearNamedTimeout;
     49 
     50 /**
     51 * Helpers for creating and messaging between UI components.
     52 */
     53 exports.ViewHelpers = {
     54  /**
     55   * Convenience method, dispatching a custom event.
     56   *
     57   * @param Node target
     58   *        A custom target element to dispatch the event from.
     59   * @param string type
     60   *        The name of the event.
     61   * @param any detail
     62   *        The data passed when initializing the event.
     63   * @return boolean
     64   *         True if the event was cancelled or a registered handler
     65   *         called preventDefault.
     66   */
     67  dispatchEvent(target, type, detail) {
     68    if (!(target instanceof Node)) {
     69      // Event cancelled.
     70      return true;
     71    }
     72    const document = target.ownerDocument || target;
     73    const dispatcher = target.ownerDocument ? target : document.documentElement;
     74 
     75    const event = document.createEvent("CustomEvent");
     76    event.initCustomEvent(type, true, true, detail);
     77    return dispatcher.dispatchEvent(event);
     78  },
     79 
     80  /**
     81   * Helper delegating some of the DOM attribute methods of a node to a widget.
     82   *
     83   * @param object widget
     84   *        The widget to assign the methods to.
     85   * @param Node node
     86   *        A node to delegate the methods to.
     87   */
     88  delegateWidgetAttributeMethods(widget, node) {
     89    widget.getAttribute = widget.getAttribute || node.getAttribute.bind(node);
     90    widget.setAttribute = widget.setAttribute || node.setAttribute.bind(node);
     91    widget.removeAttribute =
     92      widget.removeAttribute || node.removeAttribute.bind(node);
     93  },
     94 
     95  /**
     96   * Helper delegating some of the DOM event methods of a node to a widget.
     97   *
     98   * @param object widget
     99   *        The widget to assign the methods to.
    100   * @param Node node
    101   *        A node to delegate the methods to.
    102   */
    103  delegateWidgetEventMethods(widget, node) {
    104    widget.addEventListener =
    105      widget.addEventListener || node.addEventListener.bind(node);
    106    widget.removeEventListener =
    107      widget.removeEventListener || node.removeEventListener.bind(node);
    108  },
    109 
    110  /**
    111   * Checks if the specified object looks like it's been decorated by an
    112   * event emitter.
    113   *
    114   * @return boolean
    115   *         True if it looks, walks and quacks like an event emitter.
    116   */
    117  isEventEmitter(object) {
    118    return object?.on && object?.off && object?.once && object?.emit;
    119  },
    120 
    121  /**
    122   * Checks if the specified object is an instance of a DOM node.
    123   *
    124   * @return boolean
    125   *         True if it's a node, false otherwise.
    126   */
    127  isNode(object) {
    128    return (
    129      object instanceof Node ||
    130      object instanceof Element ||
    131      Cu.getClassName(object) == "DocumentFragment"
    132    );
    133  },
    134 
    135  /**
    136   * Prevents event propagation when navigation keys are pressed.
    137   *
    138   * @param Event e
    139   *        The event to be prevented.
    140   */
    141  preventScrolling(e) {
    142    switch (e.keyCode) {
    143      case KeyCodes.DOM_VK_UP:
    144      case KeyCodes.DOM_VK_DOWN:
    145      case KeyCodes.DOM_VK_LEFT:
    146      case KeyCodes.DOM_VK_RIGHT:
    147      case KeyCodes.DOM_VK_PAGE_UP:
    148      case KeyCodes.DOM_VK_PAGE_DOWN:
    149      case KeyCodes.DOM_VK_HOME:
    150      case KeyCodes.DOM_VK_END:
    151        e.preventDefault();
    152        e.stopPropagation();
    153    }
    154  },
    155 
    156  /**
    157   * Check if the enter key or space was pressed
    158   *
    159   * @param event event
    160   *        The event triggered by a keydown or keypress on an element
    161   */
    162  isSpaceOrReturn(event) {
    163    return (
    164      event.keyCode === KeyCodes.DOM_VK_SPACE ||
    165      event.keyCode === KeyCodes.DOM_VK_RETURN
    166    );
    167  },
    168 
    169  /**
    170   * Sets a toggled pane hidden or visible. The pane can either be displayed on
    171   * the side (right or left depending on the locale) or at the bottom.
    172   *
    173   * @param object flags
    174   *        An object containing some of the following properties:
    175   *        - visible: true if the pane should be shown, false to hide
    176   *        - animated: true to display an animation on toggle
    177   *        - delayed: true to wait a few cycles before toggle
    178   *        - callback: a function to invoke when the toggle finishes
    179   * @param Node pane
    180   *        The element representing the pane to toggle.
    181   */
    182  togglePane(flags, pane) {
    183    // Make sure a pane is actually available first.
    184    if (!pane) {
    185      return;
    186    }
    187 
    188    // Hiding is always handled via margins, not the hidden attribute.
    189    pane.removeAttribute("hidden");
    190 
    191    // Add a class to the pane to handle min-widths, margins and animations.
    192    pane.classList.add("generic-toggled-pane");
    193 
    194    // Avoid toggles in the middle of animation.
    195    if (pane.hasAttribute("animated")) {
    196      return;
    197    }
    198 
    199    // Avoid useless toggles.
    200    if (flags.visible == !pane.classList.contains("pane-collapsed")) {
    201      if (flags.callback) {
    202        flags.callback();
    203      }
    204      return;
    205    }
    206 
    207    // The "animated" attributes enables animated toggles (slide in-out).
    208    if (flags.animated) {
    209      pane.setAttribute("animated", "");
    210    } else {
    211      pane.removeAttribute("animated");
    212    }
    213 
    214    // Computes and sets the pane margins in order to hide or show it.
    215    const doToggle = () => {
    216      // Negative margins are applied to "right" and "left" to support RTL and
    217      // LTR directions, as well as to "bottom" to support vertical layouts.
    218      // Unnecessary negative margins are forced to 0 via CSS in widgets.css.
    219      if (flags.visible) {
    220        pane.style.marginLeft = "0";
    221        pane.style.marginRight = "0";
    222        pane.style.marginBottom = "0";
    223        pane.classList.remove("pane-collapsed");
    224      } else {
    225        const width = Math.floor(pane.getAttribute("width")) + 1;
    226        const height = Math.floor(pane.getAttribute("height")) + 1;
    227        pane.style.marginLeft = -width + "px";
    228        pane.style.marginRight = -width + "px";
    229        pane.style.marginBottom = -height + "px";
    230      }
    231 
    232      // Wait for the animation to end before calling afterToggle()
    233      if (flags.animated) {
    234        const options = {
    235          useCapture: false,
    236          once: true,
    237        };
    238 
    239        pane.addEventListener(
    240          "transitionend",
    241          () => {
    242            // Prevent unwanted transitions: if the panel is hidden and the layout
    243            // changes margins will be updated and the panel will pop out.
    244            pane.removeAttribute("animated");
    245 
    246            if (!flags.visible) {
    247              pane.classList.add("pane-collapsed");
    248            }
    249            if (flags.callback) {
    250              flags.callback();
    251            }
    252          },
    253          options
    254        );
    255      } else {
    256        if (!flags.visible) {
    257          pane.classList.add("pane-collapsed");
    258        }
    259 
    260        // Invoke the callback immediately since there's no transition.
    261        if (flags.callback) {
    262          flags.callback();
    263        }
    264      }
    265    };
    266 
    267    // Sometimes it's useful delaying the toggle a few ticks to ensure
    268    // a smoother slide in-out animation.
    269    if (flags.delayed) {
    270      pane.ownerDocument.defaultView.setTimeout(
    271        doToggle,
    272        PANE_APPEARANCE_DELAY
    273      );
    274    } else {
    275      doToggle();
    276    }
    277  },
    278 };