Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/shared/js/mouse_event_shim.js
blob: 053ef7fce23763e38213120b3491c81cb96e0ba9 (plain)
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
/**
 * 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
};