tor-browser

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

touch-simulator.js (6890B)


      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 loader.lazyRequireGetter(
      8  this,
      9  "PICKER_TYPES",
     10  "resource://devtools/shared/picker-constants.js"
     11 );
     12 
     13 var isClickHoldEnabled = Services.prefs.getBoolPref(
     14  "ui.click_hold_context_menus"
     15 );
     16 var clickHoldDelay = Services.prefs.getIntPref(
     17  "ui.click_hold_context_menus.delay",
     18  500
     19 );
     20 
     21 const EVENTS_TO_HANDLE = [
     22  "mousedown",
     23  "mousemove",
     24  "mouseup",
     25  "mouseenter",
     26  "mouseover",
     27  "mouseout",
     28  "mouseleave",
     29 ];
     30 
     31 const kStateHover = 0x00000004; // ElementState::HOVER
     32 
     33 /**
     34 * Simulate touch events for platforms where they aren't generally available.
     35 */
     36 class TouchSimulator {
     37  /**
     38   * @param {WindowGlobalTargetActor} windowTarget: The window object we'll use
     39   *                                  to listen for click and touch events to handle.
     40   */
     41  constructor(windowTarget) {
     42    this.windowTarget = windowTarget;
     43    this.simulatorTarget = windowTarget.chromeEventHandler;
     44    this._currentPickerMap = new Map();
     45    this.previousScreenY = 0;
     46  }
     47 
     48  enabled = false;
     49 
     50  start() {
     51    if (this.enabled) {
     52      // Simulator is already started
     53      return;
     54    }
     55 
     56    EVENTS_TO_HANDLE.forEach(evt => {
     57      // Only listen trusted events to prevent messing with
     58      // event dispatched manually within content documents
     59      this.simulatorTarget.addEventListener(evt, this, true, false);
     60    });
     61 
     62    this.enabled = true;
     63  }
     64 
     65  stop() {
     66    if (!this.enabled) {
     67      // Simulator isn't running
     68      return;
     69    }
     70    EVENTS_TO_HANDLE.forEach(evt => {
     71      this.simulatorTarget.removeEventListener(evt, this, true);
     72    });
     73    this.enabled = false;
     74  }
     75 
     76  _isPicking() {
     77    const types = Object.values(PICKER_TYPES);
     78    return types.some(type => this._currentPickerMap.get(type));
     79  }
     80 
     81  /**
     82   * Set the state value for one of DevTools pickers (either eyedropper or
     83   * element picker).
     84   * If any content picker is currently active, we should not be emulating
     85   * touch events. Otherwise it is ok to emulate touch events.
     86   * In theory only one picker can ever be active at a time, but tracking the
     87   * different pickers independantly avoids race issues in the client code.
     88   *
     89   * @param {boolean} state
     90   *        True if the picker is currently active, false otherwise.
     91   * @param {string} pickerType
     92   *        One of PICKER_TYPES.
     93   */
     94  setElementPickerState(state, pickerType) {
     95    if (!Object.values(PICKER_TYPES).includes(pickerType)) {
     96      throw new Error(
     97        "Unsupported type in setElementPickerState: " + pickerType
     98      );
     99    }
    100    this._currentPickerMap.set(pickerType, state);
    101  }
    102 
    103  // eslint-disable-next-line complexity
    104  handleEvent(evt) {
    105    // Bail out if devtools is in pick mode in the same tab.
    106    if (this._isPicking()) {
    107      return;
    108    }
    109 
    110    const content = this.getContent(evt.target);
    111    if (!content) {
    112      return;
    113    }
    114 
    115    // Ignore all but real mouse event coming from physical mouse
    116    // (especially ignore mouse event being dispatched from a touch event)
    117    if (
    118      evt.button ||
    119      evt.inputSource != evt.MOZ_SOURCE_MOUSE ||
    120      evt.isSynthesized
    121    ) {
    122      return;
    123    }
    124 
    125    const eventTarget = this.target;
    126    let type = "";
    127    switch (evt.type) {
    128      case "mouseenter":
    129      case "mouseover":
    130      case "mouseout":
    131      case "mouseleave":
    132        // Don't propagate events which are not related to touch events
    133        evt.stopPropagation();
    134        evt.preventDefault();
    135 
    136        // We don't want to trigger any visual changes to elements whose content can
    137        // be modified via hover states. We can avoid this by removing the element's
    138        // content state.
    139        InspectorUtils.removeContentState(evt.target, kStateHover);
    140        break;
    141 
    142      case "mousedown":
    143        this.target = evt.target;
    144 
    145        // If the click-hold feature is enabled, start a timeout to convert long clicks
    146        // into contextmenu events.
    147        // Just don't do it if the event occurred on a scrollbar.
    148        if (isClickHoldEnabled && !evt.originalTarget.closest("scrollbar")) {
    149          this._contextMenuTimeout = this.sendContextMenu(evt);
    150        }
    151 
    152        this.previousScreenY = evt.screenY;
    153 
    154        type = "touchstart";
    155        break;
    156 
    157      case "mousemove": {
    158        if (!eventTarget) {
    159          // Don't propagate mousemove event when touchstart event isn't fired
    160          evt.stopPropagation();
    161          return;
    162        }
    163 
    164        type = "touchmove";
    165        const deltaY = evt.screenY - this.previousScreenY;
    166        this.previousScreenY = evt.screenY;
    167        this.windowTarget.emit("contentScrolled", deltaY);
    168        break;
    169      }
    170 
    171      case "mouseup":
    172        if (!eventTarget) {
    173          return;
    174        }
    175        this.target = null;
    176 
    177        content.clearTimeout(this._contextMenuTimeout);
    178        type = "touchend";
    179 
    180        // Only register click listener after mouseup to ensure
    181        // catching only real user click. (Especially ignore click
    182        // being dispatched on form submit)
    183        if (evt.detail == 1) {
    184          this.simulatorTarget.addEventListener("click", this, {
    185            capture: true,
    186            once: true,
    187          });
    188        }
    189        break;
    190    }
    191 
    192    const target = eventTarget || this.target;
    193    if (target && type) {
    194      this.sendTouchEvent(content, evt.clientX, evt.clientY, type);
    195    }
    196 
    197    evt.preventDefault();
    198    evt.stopImmediatePropagation();
    199  }
    200 
    201  sendContextMenu({ target, clientX, clientY, screenX, screenY }) {
    202    const view = target.ownerGlobal;
    203    const evt = new view.PointerEvent("contextmenu", {
    204      bubbles: true,
    205      cancelable: true,
    206      view,
    207      screenX,
    208      screenY,
    209      clientX,
    210      clientY,
    211    });
    212    const content = this.getContent(target);
    213    const timeout = content.setTimeout(() => {
    214      target.dispatchEvent(evt);
    215    }, clickHoldDelay);
    216 
    217    return timeout;
    218  }
    219 
    220  /**
    221   * Sends a touch action on a given target element.
    222   *
    223   * @param {Window} win
    224   *        The target window.
    225   * @param {number} clientX
    226   *        The `x` screen coordinate relative to the viewport origin.
    227   * @param {number} clientY
    228   *        The `y` screen coordinate relative to the viewport origin.
    229   * @param {string} type
    230   *        The type of the touch event.
    231   */
    232  sendTouchEvent(win, clientX, clientY, type) {
    233    const utils = win.windowUtils;
    234    utils.sendTouchEvent(
    235      type,
    236      [0],
    237      [clientX],
    238      [clientY],
    239      [0],
    240      [0],
    241      [0],
    242      [0],
    243      [0],
    244      [0],
    245      [0],
    246      0,
    247      utils.ASYNC_ENABLED
    248    );
    249    return true;
    250  }
    251 
    252  getContent(target) {
    253    const win = target?.ownerDocument ? target.ownerGlobal : null;
    254    return win;
    255  }
    256 }
    257 
    258 exports.TouchSimulator = TouchSimulator;