forked from vistaprint/PointyJS
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpointy.js
More file actions
688 lines (592 loc) · 28.5 KB
/
Copy pathpointy.js
File metadata and controls
688 lines (592 loc) · 28.5 KB
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
/*!
* Pointy.js
* Pointer Events polyfill for jQuery
* https://github.com/vistaprint/PointyJS
*
* Depends on jQuery, see http://jquery.org
*
* Developed by Vistaprint.com
*/
(function ($, window, document, undefined) {
var support = {
touch: "ontouchend" in document,
pointer: !! (navigator.pointerEnabled || navigator.msPointerEnabled)
};
$.extend($.support, support);
function triggerCustomEvent(elem, eventType, originalEvent) {
// support for IE7-IE8
originalEvent = originalEvent || window.event;
// Create a writable copy of the event object and normalize some properties
var event = new jQuery.Event(originalEvent);
event.type = eventType;
// Copy over properties for ease of access
var i, copy = $.event.props.concat($.event.pointerHooks.props);
i = copy.length;
while (i--) {
var prop = copy[i];
event[prop] = originalEvent[prop];
}
// Support: IE<9
// Fix target property (#1925)
if (!event.target) {
event.target = originalEvent.srcElement || document;
}
// Support: Chrome 23+, Safari?
// Target should not be a text node (#504, #13143)
if (event.target.nodeType === 3) {
event.target = event.target.parentNode;
}
// Support: IE<9
// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
event.metaKey = !! event.metaKey;
// run the filter now
event = $.event.pointerHooks.filter(event, originalEvent);
// trigger the emulated pointer event
// the filter can return an array (only if the original was a touchmove),
// which means we need to trigger independent events
if ($.isArray(event)) {
$.each(event, function (i, ev) {
$.event.dispatch.call(elem, ev);
});
} else {
$.event.dispatch.call(elem, event);
}
// return the manipulated jQuery event
return event;
}
function addEvent(elem, type, selector, func) {
// when we have a selector, let jQuery do the delegation
if (selector) {
func._pointerEventWrapper = function (event) {
return func.call(elem, event.originalEvent);
};
$(elem).on(type, selector, func._pointerEventWrapper);
}
// if we do not have a selector, we optimize by cutting jQuery out
else {
if (elem.addEventListener) {
elem.addEventListener(type, func, false);
} else if (elem.attachEvent) {
// bind the function to correct "this" for IE8-
func._pointerEventWrapper = function (e) {
return func.call(elem, e);
};
elem.attachEvent("on" + type, func._pointerEventWrapper);
}
}
}
function removeEvent(elem, type, selector, func) {
// Make sure for IE8- we unbind the wrapper
if (func._pointerEventWrapper) {
func = func._pointerEventWrapper;
}
if (selector) {
$(elem).off(type, selector, func);
} else {
$.removeEvent(elem, type, func);
}
}
// get the standardized "buttons" property as per the Pointer Events spec from a mouse event
function getStandardizedButtonsProperty(event) {
// in the DOM LEVEL 3 spec there is a new standard for the "buttons" property
// sadly, no browser currently supports this and only sends us the single "button" property
if (event.buttons) {
return event.buttons;
}
// standardize "which" property for use
var which = event.which;
if (!which && event.button !== undefined) {
which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0)));
}
// no button down (can happen on mousemove)
if (which === 0) {
return 0;
}
// left button
else if (which === 1) {
return 1;
}
// middle mouse
else if (which === 2) {
return 4;
}
// right mouse
else if (which === 3) {
return 2;
}
// unknown?
return 0;
}
function returnFalse() { return false; }
function returnTrue() { return true; }
var POINTER_TYPE_UNAVAILABLE = "unavailable";
var POINTER_TYPE_TOUCH = "touch";
var POINTER_TYPE_PEN = "pen";
var POINTER_TYPE_MOUSE = "mouse";
// indicator to mark whether touch events are in progress
// when null, it means we have never received a touch event
// when true, user is currently touching something
// when false, user just released their finger (reset on mousedown if needed)
var _touching = null;
// bitmask to identify which buttons are currently down
var _buttons = 0;
// storage of the last seen touches provided by the native touch events spec
var _lastTouches = [];
// ------ NOTE: THIS IS UNUSED, WE DO NOT ASSIGN BUTTON ------
// pointer events defines the "button" property as:
// mouse move (no buttons down) -1
// left mouse, touch contact and normal pen contact 0
// middle mouse 1
// right mouse, pen with barrel button pressed 2
// x1 (back button on mouse) 3
// x2 (forward button on mouse) 4
// pen contact with eraser button pressed 5
// ------ NOTE: THIS IS UNUSED, WE DO NOT ASSIGN BUTTON ------
// pointer events defines the "buttons" property as:
// mouse move (no buttons down) 0
// left mouse, touch contact, and normal pen contact 1
// middle mouse 4
// right mouse, pen contact with barrel button pressed 2
// x1 (back) mouse 8
// x2 (forward) mouse 16
// pen contact with eraser button pressed 32
// add our own pointer event hook/filter
$.event.pointerHooks = {
props: "pointerType pointerId pressure buttons clientX clientY relatedTarget fromElement offsetX offsetY pageX pageY screenX screenY width height toElement".split(" "),
filter: function (event, original) {
// Calculate pageX/Y if missing and clientX/Y available
// this is just copied from jQuery's standard pageX/pageY fix
if (!original.touches && event.pageX == null && original.clientX != null) {
var eventDoc = event.target.ownerDocument || document;
var doc = eventDoc.documentElement;
var body = eventDoc.body;
event.pageX = original.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = original.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
}
// Add relatedTarget, if necessary
// also copied from jQuery's standard event fix
if (!event.relatedTarget && original.fromElement) {
event.relatedTarget = original.fromElement === event.target ? original.toElement : original.fromElement;
}
// Add pointerType
if (!event.pointerType || typeof event.pointerType === "number") {
if (event.pointerType == 2) {
event.pointerType = POINTER_TYPE_TOUCH;
} else if (event.pointerType == 3) {
event.pointerType = POINTER_TYPE_PEN;
} else if (event.pointerType == 4) {
event.pointerType = POINTER_TYPE_MOUSE;
} else if (/^touch/i.test(original.type)) {
event.pointerType = POINTER_TYPE_TOUCH;
event.buttons = original.type === "touchend" || original.type === "touchcancel" ? 0 : 1;
} else if (/^mouse/i.test(original.type) || original.type === "click") {
event.pointerId = 1; // as per the pointer events spec, the mouse is always pointer id 1
event.pointerType = POINTER_TYPE_MOUSE;
event.buttons = original.type === "mouseup" ? 0 : getStandardizedButtonsProperty(original);
} else {
event.pointerType = POINTER_TYPE_UNAVAILABLE;
event.buttons = 0;
}
}
// if we have the bitmask for the depressed buttons from the mouse events polyfill, use it to mimic buttons for
// browsers that do not support the HTML DOM LEVEL 3 events spec
if (event.type === "pointermove" && _touching === null && _buttons !== event.buttons) {
event.buttons = _buttons;
}
// standardize the pressure attribute
if (!event.pressure) {
event.pressure = event.buttons > 0 ? 0.5 : 0;
}
// standardize the width and height, these represent the contact geometry
if (event.width === undefined || event.height === undefined) {
event.width = event.height = 0;
}
// prevent the follow native click event from occurring, can be used to prevent
// clicks on pointerdown or pointerup or from gestures like press and presshold
event.preventClick = function () {
event.isClickPrevented = returnTrue;
$(event.target).one("click", returnFalse);
};
event.isClickPrevented = returnFalse;
// touch events send an array of touches we need to convert to the pointer events format
// which means we need to fire multiple events per touch
if (original.touches && event.type !== "pointercancel") {
var touches = original.touches;
var events = [];
var ev, i, j;
// the problem with this is that on touchend it will remove the
// touch which has ended from the touches list, this means we do
// not want to fire pointerup for touches that are still there,
// we instead want to send a pointerup with the removed touch's identifier
if (event.type === "pointerup") {
// convert TouchList to a standard array
_lastTouches = Array.prototype.slice.call(_lastTouches);
// find the touch that was removed
for (i = 0; i < original.touches.length; i++) {
for (j = 0; j < _lastTouches.length; j++) {
if (_lastTouches[j].identifier === original.touches[i].identifier) {
_lastTouches.splice(j, 1);
}
}
}
// if we narrowed down the ended touch to one, then we found it
if (_lastTouches.length === 1) {
event.pointerId = _lastTouches[0].identifier;
_lastTouches = original.touches;
return event;
}
}
// on pointerdown we need to only trigger a new pointerdown for the touch,
// and not the touches that were already there
else if (event.type === "pointerdown") {
// convert TouchList to a standard array
touches = Array.prototype.slice.call(original.touches);
// find the new touch that was just added
for (i = 0; i < touches.length; i++) {
// last touches will be a list with one less touch
for (j = 0; j < _lastTouches.length; j++) {
if (touches[i].identifier === _lastTouches[j].identifier) {
touches.splice(i, 1);
}
}
}
}
// this will be used on pointermove and pointerdown
for (i = 0; i < original.touches.length; i++) {
var touch = original.touches[i];
ev = $.extend({}, event);
// copy over information from the touch to the event
ev.clientX = touch.clientX;
ev.clientY = touch.clientY;
ev.pageX = touch.pageX;
ev.pageY = touch.pageY;
ev.screenX = touch.screenX;
ev.screenY = touch.screenY;
// the touch id on emulated touch events from chrome is always 0 (zero)
ev.pointerId = touch.identifier;
events.push(ev);
}
// do as little processing as you can here, this is done on touchmove and
// there can be a lot of those events firing quickly, we do not want the
// polyfill slowing down the application
_lastTouches = original.touches;
return events;
}
return event;
}
};
$.event.delegateSpecial = function (setup) {
return function (handleObj) {
var thisObject = this,
data = jQuery._data(thisObject);
if (!data.pointerEvents) {
data.pointerEvents = {};
}
if (!data.pointerEvents[handleObj.type]) {
data.pointerEvents[handleObj.type] = [];
}
if (!data.pointerEvents[handleObj.type].length) {
setup.call(thisObject, handleObj);
}
data.pointerEvents[handleObj.type].push(handleObj);
};
};
$.event.delegateSpecial.remove = function (teardown) {
return function (handleObj) {
var handlers,
thisObject = this,
data = jQuery._data(thisObject);
if (!data.pointerEvents) {
data.pointerEvents = {};
}
handlers = data.pointerEvents[handleObj.type];
handlers.splice(handlers.indexOf(handleObj), 1);
if (!handlers.length) {
teardown.call(thisObject, handleObj);
}
};
};
// allow jQuery's native $.event.fix to find our pointer hooks
$.extend($.event.fixHooks, {
pointerdown: $.event.pointerHooks,
pointerup: $.event.pointerHooks,
pointermove: $.event.pointerHooks,
pointerover: $.event.pointerHooks,
pointerout: $.event.pointerHooks,
pointercancel: $.event.pointerHooks
});
// if browser does not natively handle pointer events,
// create special custom events to mimic them
if (!support.pointer) {
// stores the scroll-y offest on touchstart and is compared
// on touchend to see if we should trigger a click event
var _startScrollOffset;
// utility to return the scroll-y position
var scrollY = function () {
return Math.floor(window.scrollY || $(window).scrollTop());
};
$.event.special.pointerdown = {
touch: function (event) {
// set the pointer as currently down to prevent chorded PointerDown events
_touching = true;
// trigger a new PointerDown event
triggerCustomEvent(this, "pointerdown", event);
// set the scroll offset which is compared on TouchEnd
_startScrollOffset = scrollY();
},
mouse: function (event) {
// if we just had a touchstart, ignore this MouseDown event, to prevent double firing of PointerDown
if (_touching === true) {
return;
}
// we reset the touch to null, to indicate that we're listening to mouse events currently
_touching = null;
// update the _buttons bitmask
var button = getStandardizedButtonsProperty(event);
var wasAButtonDownAlready = _buttons !== 0;
_buttons |= button;
// do not trigger another PointerDown event if currently down, prevent chorded PointerDown events
if (wasAButtonDownAlready && _buttons !== button) {
// per the Pointer Events spec, when the active buttons change it fires only a PointerMove event
triggerCustomEvent(this, "pointermove", event);
return;
}
triggerCustomEvent(this, "pointerdown", event);
},
add: $.event.delegateSpecial(function (handleObj) {
// bind to touch events, some devices (chromebook) can send both touch and mouse events
if (support.touch) {
addEvent(this, "touchstart", handleObj.selector, $.event.special.pointerdown.touch);
}
// bind to mouse events
addEvent(this, "mousedown", handleObj.selector, $.event.special.pointerdown.mouse);
// ensure we also bind to "pointerup" to properly clear signals and fire click event on "touchend"
handleObj.pointerup = $.noop;
$(this).on("pointerup", handleObj.selector, handleObj.pointerup);
}),
remove: $.event.delegateSpecial.remove(function (handleObj) {
// unbind touch events
if (support.touch) {
removeEvent(this, "touchstart", handleObj.selector, $.event.special.pointerdown.touch);
}
// unbind mouse events
removeEvent(this, "mousedown", handleObj.selector, $.event.special.pointerdown.mouse);
// unbind the special "pointerup" we added for cleanup
if (handleObj.pointerup) {
$(this).off("pointerup", handleObj.selector, handleObj.pointerup);
}
})
};
$.event.special.pointerup = {
touch: function (event) {
// prevent default to prevent the emulated "mouseup" event from being triggered
event.preventDefault();
// safety check, if _touching is null then we just had a mouse event and shouldn't
// listen to touch events right now
if (_touching === null) {
return;
}
// indicate that currently release the pointerdown lock
_touching = false;
var jEvent = triggerCustomEvent(this, "pointerup", event);
// if we are preventing the next click event, then simply don't trigger one below
if (jEvent.isClickPrevented()) {
$(event.target).off("click", returnFalse);
return;
}
// on "touchend", calling prevent default prevents the "mouseup" and "click" event
// however on native "mouseup" events preventing default does not cancel the "click" event
// as per the pointer event spec on "pointerup" preventing default should not cancel the "click" event
//
// we really do want to call this all the time, because if the function binded to this emulated
// "poiunterup" triggered above called prevent default it would also prevent the click, which
// would cause inconsistent behavior. To prevent the possibility of two click events though,
// we want to call prevent default all the time (as we do above) and then force trigger the click here
//
// we confirm that the user did not scroll, as touch events are very related to scrolling on
// touch devices, it's possible we may mis-fire a click event on an <a> anchor tag causing
// navigation even though this was a scroll attempt. Do the the browser's built in threshold
// to prevent accidental scrolling we do not add a threshold here.
if (event.target && event.target.click && _startScrollOffset === scrollY()) {
event.target.click();
}
// on iOS 5 there is no native click function on elements, therefore we need to let jQuery handle it (AR-16405)
// on Android 4.1, you cannot prevent it from firing the native click event on touchend (GD-155106)
else if (event.target && _startScrollOffset === scrollY()) {
var clickTimer = setTimeout(function () {
$(event.target).click();
}, 200);
$(event.target).one("click", function () {
clearTimeout(clickTimer);
});
}
},
mouse: function (event) {
// if originally we had a TouchStart or we ended with a TouchEnd event, ignore this MouseUp
if (_touching === false) {
return;
}
_buttons ^= getStandardizedButtonsProperty(event);
// we only trigger a PointerUp event if no buttons are down, prevent chorded PointerDown events
if (_buttons === 0) {
triggerCustomEvent(this, "pointerup", event);
} else {
// per the Pointer Events spec, when the active buttons change it fires only a PointerMove event
triggerCustomEvent(this, "pointermove", event);
}
// Mouse Events spec shows that after a MouseUp it fires a MouseMove, which will trigger
// the PointerMove needed to follow the Pointer Events spec which describes the same thing
},
add: $.event.delegateSpecial(function (handleObj) {
// bind to touch events, some devices (chromebook) can send both touch and mouse events
if (support.touch) {
addEvent(this, "touchend", handleObj.selector, $.event.special.pointerup.touch);
}
// bind mouse events
addEvent(this, "mouseup", handleObj.selector, $.event.special.pointerup.mouse);
}),
remove: $.event.delegateSpecial.remove(function (handleObj) {
// unbind touch events
if (support.touch) {
removeEvent(this, "touchend", handleObj.selector, $.event.special.pointerup.touch);
}
// unbind mouse events
removeEvent(this, "mouseup", handleObj.selector, $.event.special.pointerup.mouse);
})
};
$.event.special.pointermove = {
touch: function (event) {
triggerCustomEvent(this, "pointermove", event);
},
mouse: function (event) {
// _touching will be true if they currently have their finger (touch only) down
// because we cannot call preventDefault on the "touchmove" without preventing
// scrolling on most things, we do this check to ensure we don't double fire
// move events.
if (_touching === true) {
return;
}
triggerCustomEvent(this, "pointermove", event);
},
add: $.event.delegateSpecial(function (handleObj) {
// bind to touch events, some devices (chromebook) can send both touch and mouse events
if (support.touch) {
addEvent(this, "touchmove", handleObj.selector, $.event.special.pointermove.touch);
}
// bind mouse events
addEvent(this, "mousemove", handleObj.selector, $.event.special.pointermove.mouse);
}),
remove: $.event.delegateSpecial.remove(function (handleObj) {
// unbind touch events
if (support.touch) {
removeEvent(this, "touchmove", handleObj.selector, $.event.special.pointermove.touch);
}
// unbind mouse events
removeEvent(this, "mousemove", handleObj.selector, $.event.special.pointermove.mouse);
})
};
jQuery.each({
pointerover: {
mouse: "mouseover"
},
pointerout: {
mouse: "mouseout"
},
pointercancel: {
touch: "touchcancel"
}
}, function (pointerEventType, natives) {
function onTouch(event) {
triggerCustomEvent(this, pointerEventType, event);
}
function onMouse(event) {
triggerCustomEvent(this, pointerEventType, event);
}
$.event.special[pointerEventType] = {
setup: function () {
if (support.touch && natives.touch) {
addEvent(this, natives.touch, null, onTouch);
}
if (natives.mouse) {
addEvent(this, natives.mouse, null, onMouse);
}
},
teardown: function () {
if (support.touch && natives.touch) {
removeEvent(this, natives.touch, null, onTouch);
}
if (natives.mouse) {
removeEvent(this, natives.mouse, null, onMouse);
}
}
};
});
}
// for IE10 specific, we proxy though events so we do not need to deal
// with the various names or renaming of events.
else if (navigator.msPointerEnabled && !navigator.pointerEnabled) {
$.extend($.event.special, {
pointerdown: {
delegateType: "MSPointerDown",
bindType: "MSPointerDown"
},
pointerup: {
delegateType: "MSPointerUp",
bindType: "MSPointerUp"
},
pointermove: {
delegateType: "MSPointerMove",
bindType: "MSPointerMove"
},
pointerover: {
delegateType: "MSPointerOver",
bindType: "MSPointerOver"
},
pointerout: {
delegateType: "MSPointerOut",
bindType: "MSPointerOut"
},
pointercancel: {
delegateType: "MSPointerCancel",
bindType: "MSPointerCancel"
}
});
$.extend($.event.fixHooks, {
MSPointerDown: $.event.pointerHooks,
MSPointerUp: $.event.pointerHooks,
MSPointerMove: $.event.pointerHooks,
MSPointerOver: $.event.pointerHooks,
MSPointerOut: $.event.pointerHooks,
MSPointerCancel: $.event.pointerHooks
});
}
// add support for pointerenter and pointerlave
// pointerenter and pointerleave were added in IE11, they do not exist in IE10
if (!support.pointer || (navigator.msPointerEnabled && !navigator.pointerEnabled)) {
// Create a wrapper similar to jQuery's mouseenter/leave events
// using pointer events (pointerover/out) and event-time checks
jQuery.each({
pointerenter: navigator.msPointerEnabled ? "MSPointerOver" : "mouseover",
pointerleave: navigator.msPointerEnabled ? "MSPointerOut" : "mouseout"
}, function (orig, fix) {
jQuery.event.special[orig] = {
delegateType: fix,
bindType: fix,
handle: function (event) {
var ret,
target = this,
related = event.relatedTarget,
handleObj = event.handleObj;
// For mousenter/leave call the handler if related is outside the target.
// NB: No relatedTarget if the mouse left/entered the browser window
if (!related || (related !== target && !jQuery.contains(target, related))) {
event.type = handleObj.origType;
ret = handleObj.handler.apply(this, arguments);
event.type = fix;
}
return ret;
}
};
});
}
})(jQuery, window, document);