/** * 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; } var starttouch; // The Touch object that we started with var target; // The element the touch is currently over var 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 var 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 (var i = 0; i < e.changedTouches.length; i++) { var 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 (var i = 0; i < e.changedTouches.length; i++) { var 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 var dx = Math.abs(touch.screenX - starttouch.screenX); var dy = Math.abs(touch.screenY - starttouch.screenY); if (dx > MouseEventShim.dragThresholdX || dy > MouseEventShim.dragThresholdY) { emitclick = false; } var tracking = MouseEventShim.trackMouseMoves && !MouseEventShim.capturing; 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? var oldtarget = target; var 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 (var 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 (var e = newtarget; !contains(e, oldtarget); e = e.parentNode) { emitEvent('mouseenter', e, touch, oldtarget); } } function emitEvent(type, target, touch, relatedTarget) { var synthetic = document.createEvent('MouseEvents'); var bubbles = (type !== 'mouseenter' && type !== 'mouseleave'); var 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; } } }()); var 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: function(e) { if (e.isTrusted) // XXX: Are real events always trusted? return e.timeStamp; else 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: function(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 };