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 };