forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D62026 --HG-- extra : moz-landing-system : lando
297 lines
9.8 KiB
JavaScript
297 lines
9.8 KiB
JavaScript
/**
|
|
* mouse_event_shim.js: generate mouse events from touch events.
|
|
*
|
|
* This library listens for touch events and generates mousedown, mousemove
|
|
* mouseup, and click events to match them. It captures and dicards any
|
|
* real mouse events (non-synthetic events with isTrusted true) that are
|
|
* send by gecko so that there are not duplicates.
|
|
*
|
|
* This library does emit mouseover/mouseout and mouseenter/mouseleave
|
|
* events. You can turn them off by setting MouseEventShim.trackMouseMoves to
|
|
* false. This means that mousemove events will always have the same target
|
|
* as the mousedown even that began the series. You can also call
|
|
* MouseEventShim.setCapture() from a mousedown event handler to prevent
|
|
* mouse tracking until the next mouseup event.
|
|
*
|
|
* This library does not support multi-touch but should be sufficient
|
|
* to do drags based on mousedown/mousemove/mouseup events.
|
|
*
|
|
* This library does not emit dblclick events or contextmenu events
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
(function() {
|
|
// Make sure we don't run more than once
|
|
if (MouseEventShim) {
|
|
return;
|
|
}
|
|
|
|
// Bail if we're not on running on a platform that sends touch
|
|
// events. We don't need the shim code for mouse events.
|
|
try {
|
|
document.createEvent("TouchEvent");
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
let starttouch; // The Touch object that we started with
|
|
let target; // The element the touch is currently over
|
|
let emitclick; // Will we be sending a click event after mouseup?
|
|
|
|
// Use capturing listeners to discard all mouse events from gecko
|
|
window.addEventListener("mousedown", discardEvent, true);
|
|
window.addEventListener("mouseup", discardEvent, true);
|
|
window.addEventListener("mousemove", discardEvent, true);
|
|
window.addEventListener("click", discardEvent, true);
|
|
|
|
function discardEvent(e) {
|
|
if (e.isTrusted) {
|
|
e.stopImmediatePropagation(); // so it goes no further
|
|
if (e.type === "click") {
|
|
e.preventDefault();
|
|
} // so it doesn't trigger a change event
|
|
}
|
|
}
|
|
|
|
// Listen for touch events that bubble up to the window.
|
|
// If other code has called stopPropagation on the touch events
|
|
// then we'll never see them. Also, we'll honor the defaultPrevented
|
|
// state of the event and will not generate synthetic mouse events
|
|
window.addEventListener("touchstart", handleTouchStart);
|
|
window.addEventListener("touchmove", handleTouchMove);
|
|
window.addEventListener("touchend", handleTouchEnd);
|
|
window.addEventListener("touchcancel", handleTouchEnd); // Same as touchend
|
|
|
|
function handleTouchStart(e) {
|
|
// If we're already handling a touch, ignore this one
|
|
if (starttouch) {
|
|
return;
|
|
}
|
|
|
|
// Ignore any event that has already been prevented
|
|
if (e.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
// Sometimes an unknown gecko bug causes us to get a touchstart event
|
|
// for an iframe target that we can't use because it is cross origin.
|
|
// Don't start handling a touch in that case
|
|
try {
|
|
e.changedTouches[0].target.ownerDocument;
|
|
} catch (e) {
|
|
// Ignore the event if we can't see the properties of the target
|
|
return;
|
|
}
|
|
|
|
// If there is more than one simultaneous touch, ignore all but the first
|
|
starttouch = e.changedTouches[0];
|
|
target = starttouch.target;
|
|
emitclick = true;
|
|
|
|
// Move to the position of the touch
|
|
emitEvent("mousemove", target, starttouch);
|
|
|
|
// Now send a synthetic mousedown
|
|
let result = emitEvent("mousedown", target, starttouch);
|
|
|
|
// If the mousedown was prevented, pass that on to the touch event.
|
|
// And remember not to send a click event
|
|
if (!result) {
|
|
e.preventDefault();
|
|
emitclick = false;
|
|
}
|
|
}
|
|
|
|
function handleTouchEnd(e) {
|
|
if (!starttouch) {
|
|
return;
|
|
}
|
|
|
|
// End a MouseEventShim.setCapture() call
|
|
if (MouseEventShim.capturing) {
|
|
MouseEventShim.capturing = false;
|
|
MouseEventShim.captureTarget = null;
|
|
}
|
|
|
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
let touch = e.changedTouches[i];
|
|
// If the ended touch does not have the same id, skip it
|
|
if (touch.identifier !== starttouch.identifier) {
|
|
continue;
|
|
}
|
|
|
|
emitEvent("mouseup", target, touch);
|
|
|
|
// If target is still the same element we started and the touch did not
|
|
// move more than the threshold and if the user did not prevent
|
|
// the mousedown, then send a click event, too.
|
|
if (emitclick) {
|
|
emitEvent("click", starttouch.target, touch);
|
|
}
|
|
|
|
starttouch = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
function handleTouchMove(e) {
|
|
if (!starttouch) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
let touch = e.changedTouches[i];
|
|
// If the ended touch does not have the same id, skip it
|
|
if (touch.identifier !== starttouch.identifier) {
|
|
continue;
|
|
}
|
|
|
|
// Don't send a mousemove if the touchmove was prevented
|
|
if (e.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
// See if we've moved too much to emit a click event
|
|
let dx = Math.abs(touch.screenX - starttouch.screenX);
|
|
let dy = Math.abs(touch.screenY - starttouch.screenY);
|
|
if (
|
|
dx > MouseEventShim.dragThresholdX ||
|
|
dy > MouseEventShim.dragThresholdY
|
|
) {
|
|
emitclick = false;
|
|
}
|
|
|
|
let tracking =
|
|
MouseEventShim.trackMouseMoves && !MouseEventShim.capturing;
|
|
|
|
let oldtarget;
|
|
let newtarget;
|
|
if (tracking) {
|
|
// If the touch point moves, then the element it is over
|
|
// may have changed as well. Note that calling elementFromPoint()
|
|
// forces a layout if one is needed.
|
|
// XXX: how expensive is it to do this on each touchmove?
|
|
// Can we listen for (non-standard) touchleave events instead?
|
|
oldtarget = target;
|
|
newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
if (newtarget === null) {
|
|
// this can happen as the touch is moving off of the screen, e.g.
|
|
newtarget = oldtarget;
|
|
}
|
|
if (newtarget !== oldtarget) {
|
|
leave(oldtarget, newtarget, touch); // mouseout, mouseleave
|
|
target = newtarget;
|
|
}
|
|
} else if (MouseEventShim.captureTarget) {
|
|
target = MouseEventShim.captureTarget;
|
|
}
|
|
|
|
emitEvent("mousemove", target, touch);
|
|
|
|
if (tracking && newtarget !== oldtarget) {
|
|
enter(newtarget, oldtarget, touch); // mouseover, mouseenter
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return true if element a contains element b
|
|
function contains(a, b) {
|
|
return (a.compareDocumentPosition(b) & 16) !== 0;
|
|
}
|
|
|
|
// A touch has left oldtarget and entered newtarget
|
|
// Send out all the events that are required
|
|
function leave(oldtarget, newtarget, touch) {
|
|
emitEvent("mouseout", oldtarget, touch, newtarget);
|
|
|
|
// If the touch has actually left oldtarget (and has not just moved
|
|
// into a child of oldtarget) send a mouseleave event. mouseleave
|
|
// events don't bubble, so we have to repeat this up the hierarchy.
|
|
for (let e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
|
|
emitEvent("mouseleave", e, touch, newtarget);
|
|
}
|
|
}
|
|
|
|
// A touch has entered newtarget from oldtarget
|
|
// Send out all the events that are required.
|
|
function enter(newtarget, oldtarget, touch) {
|
|
emitEvent("mouseover", newtarget, touch, oldtarget);
|
|
|
|
// Emit non-bubbling mouseenter events if the touch actually entered
|
|
// newtarget and wasn't already in some child of it
|
|
for (let e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
|
|
emitEvent("mouseenter", e, touch, oldtarget);
|
|
}
|
|
}
|
|
|
|
function emitEvent(type, target, touch, relatedTarget) {
|
|
let synthetic = document.createEvent("MouseEvents");
|
|
let bubbles = type !== "mouseenter" && type !== "mouseleave";
|
|
let count =
|
|
type === "mousedown" || type === "mouseup" || type === "click" ? 1 : 0;
|
|
|
|
synthetic.initMouseEvent(
|
|
type,
|
|
bubbles, // canBubble
|
|
true, // cancelable
|
|
window,
|
|
count, // detail: click count
|
|
touch.screenX,
|
|
touch.screenY,
|
|
touch.clientX,
|
|
touch.clientY,
|
|
false, // ctrlKey: we don't have one
|
|
false, // altKey: we don't have one
|
|
false, // shiftKey: we don't have one
|
|
false, // metaKey: we don't have one
|
|
0, // we're simulating the left button
|
|
relatedTarget || null
|
|
);
|
|
|
|
try {
|
|
return target.dispatchEvent(synthetic);
|
|
} catch (e) {
|
|
console.warn("Exception calling dispatchEvent", type, e);
|
|
return true;
|
|
}
|
|
}
|
|
})();
|
|
|
|
const MouseEventShim = {
|
|
// It is a known gecko bug that synthetic events have timestamps measured
|
|
// in microseconds while regular events have timestamps measured in
|
|
// milliseconds. This utility function returns a the timestamp converted
|
|
// to milliseconds, if necessary.
|
|
getEventTimestamp(e) {
|
|
if (e.isTrusted) {
|
|
// XXX: Are real events always trusted?
|
|
return e.timeStamp;
|
|
}
|
|
return e.timeStamp / 1000;
|
|
},
|
|
|
|
// Set this to false if you don't care about mouseover/out events
|
|
// and don't want the target of mousemove events to follow the touch
|
|
trackMouseMoves: true,
|
|
|
|
// Call this function from a mousedown event handler if you want to guarantee
|
|
// that the mousemove and mouseup events will go to the same element
|
|
// as the mousedown even if they leave the bounds of the element. This is
|
|
// like setting trackMouseMoves to false for just one drag. It is a
|
|
// substitute for event.target.setCapture(true)
|
|
setCapture(target) {
|
|
this.capturing = true; // Will be set back to false on mouseup
|
|
if (target) {
|
|
this.captureTarget = target;
|
|
}
|
|
},
|
|
|
|
capturing: false,
|
|
|
|
// Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
|
|
// If a touch ever moves more than this many pixels from its starting point
|
|
// then we will not synthesize a click event when the touch ends.
|
|
dragThresholdX: 25,
|
|
dragThresholdY: 25,
|
|
};
|