/** * 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, };