1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
|
/**
* 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,
};
|