tor-browser

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

shim.js (9990B)


      1 /**
      2 * mouse_event_shim.js: generate mouse events from touch events.
      3 *
      4 * This library listens for touch events and generates mousedown, mousemove
      5 * mouseup, and click events to match them. It captures and dicards any
      6 * real mouse events (non-synthetic events with isTrusted true) that are
      7 * send by gecko so that there are not duplicates.
      8 *
      9 * This library does emit mouseover/mouseout and mouseenter/mouseleave
     10 * events. You can turn them off by setting MouseEventShim.trackMouseMoves to
     11 * false. This means that mousemove events will always have the same target
     12 * as the mousedown even that began the series. You can also call
     13 * MouseEventShim.setCapture() from a mousedown event handler to prevent
     14 * mouse tracking until the next mouseup event.
     15 *
     16 * This library does not support multi-touch but should be sufficient
     17 * to do drags based on mousedown/mousemove/mouseup events.
     18 *
     19 * This library does not emit dblclick events or contextmenu events
     20 */
     21 
     22 "use strict";
     23 
     24 (function () {
     25  // Make sure we don't run more than once
     26  if (MouseEventShim) {
     27    return;
     28  }
     29 
     30  // Bail if we're not on running on a platform that sends touch
     31  // events. We don't need the shim code for mouse events.
     32  try {
     33    document.createEvent("TouchEvent");
     34  } catch (e) {
     35    return;
     36  }
     37 
     38  let starttouch; // The Touch object that we started with
     39  let target; // The element the touch is currently over
     40  let emitclick; // Will we be sending a click event after mouseup?
     41 
     42  // Use capturing listeners to discard all mouse events from gecko
     43  window.addEventListener("mousedown", discardEvent, true);
     44  window.addEventListener("mouseup", discardEvent, true);
     45  window.addEventListener("mousemove", discardEvent, true);
     46  window.addEventListener("click", discardEvent, true);
     47 
     48  function discardEvent(e) {
     49    if (e.isTrusted) {
     50      e.stopImmediatePropagation(); // so it goes no further
     51      if (e.type === "click") {
     52        e.preventDefault();
     53      } // so it doesn't trigger a change event
     54    }
     55  }
     56 
     57  // Listen for touch events that bubble up to the window.
     58  // If other code has called stopPropagation on the touch events
     59  // then we'll never see them. Also, we'll honor the defaultPrevented
     60  // state of the event and will not generate synthetic mouse events
     61  window.addEventListener("touchstart", handleTouchStart);
     62  window.addEventListener("touchmove", handleTouchMove);
     63  window.addEventListener("touchend", handleTouchEnd);
     64  window.addEventListener("touchcancel", handleTouchEnd); // Same as touchend
     65 
     66  function handleTouchStart(e) {
     67    // If we're already handling a touch, ignore this one
     68    if (starttouch) {
     69      return;
     70    }
     71 
     72    // Ignore any event that has already been prevented
     73    if (e.defaultPrevented) {
     74      return;
     75    }
     76 
     77    // Sometimes an unknown gecko bug causes us to get a touchstart event
     78    // for an iframe target that we can't use because it is cross origin.
     79    // Don't start handling a touch in that case
     80    try {
     81      e.changedTouches[0].target.ownerDocument;
     82    } catch (e) {
     83      // Ignore the event if we can't see the properties of the target
     84      return;
     85    }
     86 
     87    // If there is more than one simultaneous touch, ignore all but the first
     88    starttouch = e.changedTouches[0];
     89    target = starttouch.target;
     90    emitclick = true;
     91 
     92    // Move to the position of the touch
     93    emitEvent("mousemove", target, starttouch);
     94 
     95    // Now send a synthetic mousedown
     96    let result = emitEvent("mousedown", target, starttouch);
     97 
     98    // If the mousedown was prevented, pass that on to the touch event.
     99    // And remember not to send a click event
    100    if (!result) {
    101      e.preventDefault();
    102      emitclick = false;
    103    }
    104  }
    105 
    106  function handleTouchEnd(e) {
    107    if (!starttouch) {
    108      return;
    109    }
    110 
    111    // End a MouseEventShim.setCapture() call
    112    if (MouseEventShim.capturing) {
    113      MouseEventShim.capturing = false;
    114      MouseEventShim.captureTarget = null;
    115    }
    116 
    117    for (let i = 0; i < e.changedTouches.length; i++) {
    118      let touch = e.changedTouches[i];
    119      // If the ended touch does not have the same id, skip it
    120      if (touch.identifier !== starttouch.identifier) {
    121        continue;
    122      }
    123 
    124      emitEvent("mouseup", target, touch);
    125 
    126      // If target is still the same element we started and the touch did not
    127      // move more than the threshold and if the user did not prevent
    128      // the mousedown, then send a click event, too.
    129      if (emitclick) {
    130        emitEvent("click", starttouch.target, touch);
    131      }
    132 
    133      starttouch = null;
    134      return;
    135    }
    136  }
    137 
    138  function handleTouchMove(e) {
    139    if (!starttouch) {
    140      return;
    141    }
    142 
    143    for (let i = 0; i < e.changedTouches.length; i++) {
    144      let touch = e.changedTouches[i];
    145      // If the ended touch does not have the same id, skip it
    146      if (touch.identifier !== starttouch.identifier) {
    147        continue;
    148      }
    149 
    150      // Don't send a mousemove if the touchmove was prevented
    151      if (e.defaultPrevented) {
    152        return;
    153      }
    154 
    155      // See if we've moved too much to emit a click event
    156      let dx = Math.abs(touch.screenX - starttouch.screenX);
    157      let dy = Math.abs(touch.screenY - starttouch.screenY);
    158      if (
    159        dx > MouseEventShim.dragThresholdX ||
    160        dy > MouseEventShim.dragThresholdY
    161      ) {
    162        emitclick = false;
    163      }
    164 
    165      let tracking =
    166        MouseEventShim.trackMouseMoves && !MouseEventShim.capturing;
    167 
    168      let oldtarget;
    169      let newtarget;
    170      if (tracking) {
    171        // If the touch point moves, then the element it is over
    172        // may have changed as well. Note that calling elementFromPoint()
    173        // forces a layout if one is needed.
    174        // XXX: how expensive is it to do this on each touchmove?
    175        // Can we listen for (non-standard) touchleave events instead?
    176        oldtarget = target;
    177        newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
    178        if (newtarget === null) {
    179          // this can happen as the touch is moving off of the screen, e.g.
    180          newtarget = oldtarget;
    181        }
    182        if (newtarget !== oldtarget) {
    183          leave(oldtarget, newtarget, touch); // mouseout, mouseleave
    184          target = newtarget;
    185        }
    186      } else if (MouseEventShim.captureTarget) {
    187        target = MouseEventShim.captureTarget;
    188      }
    189 
    190      emitEvent("mousemove", target, touch);
    191 
    192      if (tracking && newtarget !== oldtarget) {
    193        enter(newtarget, oldtarget, touch); // mouseover, mouseenter
    194      }
    195    }
    196  }
    197 
    198  // Return true if element a contains element b
    199  function contains(a, b) {
    200    return (a.compareDocumentPosition(b) & 16) !== 0;
    201  }
    202 
    203  // A touch has left oldtarget and entered newtarget
    204  // Send out all the events that are required
    205  function leave(oldtarget, newtarget, touch) {
    206    emitEvent("mouseout", oldtarget, touch, newtarget);
    207 
    208    // If the touch has actually left oldtarget (and has not just moved
    209    // into a child of oldtarget) send a mouseleave event. mouseleave
    210    // events don't bubble, so we have to repeat this up the hierarchy.
    211    for (let e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
    212      emitEvent("mouseleave", e, touch, newtarget);
    213    }
    214  }
    215 
    216  // A touch has entered newtarget from oldtarget
    217  // Send out all the events that are required.
    218  function enter(newtarget, oldtarget, touch) {
    219    emitEvent("mouseover", newtarget, touch, oldtarget);
    220 
    221    // Emit non-bubbling mouseenter events if the touch actually entered
    222    // newtarget and wasn't already in some child of it
    223    for (let e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
    224      emitEvent("mouseenter", e, touch, oldtarget);
    225    }
    226  }
    227 
    228  function emitEvent(type, target, touch, relatedTarget) {
    229    let synthetic = document.createEvent("MouseEvents");
    230    let bubbles = type !== "mouseenter" && type !== "mouseleave";
    231    let count =
    232      type === "mousedown" || type === "mouseup" || type === "click" ? 1 : 0;
    233 
    234    synthetic.initMouseEvent(
    235      type,
    236      bubbles, // canBubble
    237      true, // cancelable
    238      window,
    239      count, // detail: click count
    240      touch.screenX,
    241      touch.screenY,
    242      touch.clientX,
    243      touch.clientY,
    244      false, // ctrlKey: we don't have one
    245      false, // altKey: we don't have one
    246      false, // shiftKey: we don't have one
    247      false, // metaKey: we don't have one
    248      0, // we're simulating the left button
    249      relatedTarget || null
    250    );
    251 
    252    try {
    253      return target.dispatchEvent(synthetic);
    254    } catch (e) {
    255      console.warn("Exception calling dispatchEvent", type, e);
    256      return true;
    257    }
    258  }
    259 })();
    260 
    261 const MouseEventShim = {
    262  // It is a known gecko bug that synthetic events have timestamps measured
    263  // in microseconds while regular events have timestamps measured in
    264  // milliseconds. This utility function returns a the timestamp converted
    265  // to milliseconds, if necessary.
    266  getEventTimestamp(e) {
    267    if (e.isTrusted) {
    268      // XXX: Are real events always trusted?
    269      return e.timeStamp;
    270    }
    271    return e.timeStamp / 1000;
    272  },
    273 
    274  // Set this to false if you don't care about mouseover/out events
    275  // and don't want the target of mousemove events to follow the touch
    276  trackMouseMoves: true,
    277 
    278  // Call this function from a mousedown event handler if you want to guarantee
    279  // that the mousemove and mouseup events will go to the same element
    280  // as the mousedown even if they leave the bounds of the element. This is
    281  // like setting trackMouseMoves to false for just one drag. It is a
    282  // substitute for event.target.setCapture(true)
    283  setCapture(target) {
    284    this.capturing = true; // Will be set back to false on mouseup
    285    if (target) {
    286      this.captureTarget = target;
    287    }
    288  },
    289 
    290  capturing: false,
    291 
    292  // Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
    293  // If a touch ever moves more than this many pixels from its starting point
    294  // then we will not synthesize a click event when the touch ends.
    295  dragThresholdX: 25,
    296  dragThresholdY: 25,
    297 };