UNPKG

20 kBJavaScriptView Raw
1;(function () {
2 'use strict';
3
4 /**
5 * WARNING!!!!
6 * For Quasar Framework, this is only needed for
7 * iOS (PWA or Cordova) platform. This is assumed by default.
8 */
9
10 /**
11 * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
12 *
13 * @codingstandard ftlabs-jsv2
14 * @copyright The Financial Times Limited [All Rights Reserved]
15 * @license MIT License (see LICENSE.txt)
16 */
17
18 /*jslint browser:true, node:true*/
19 /*global define, Event, Node*/
20
21
22 /**
23 * Instantiate fast-clicking listeners on the specified layer.
24 *
25 * @constructor
26 * @param {Element} layer The layer to listen on
27 */
28 function FastClick(layer) {
29 var oldOnClick;
30
31 /**
32 * Whether a click is currently being tracked.
33 *
34 * @type boolean
35 */
36 this.trackingClick = false;
37
38
39 /**
40 * Timestamp for when click tracking started.
41 *
42 * @type number
43 */
44 this.trackingClickStart = 0;
45
46
47 /**
48 * The element being tracked for a click.
49 *
50 * @type EventTarget
51 */
52 this.targetElement = null;
53
54
55 /**
56 * X-coordinate of touch start event.
57 *
58 * @type number
59 */
60 this.touchStartX = 0;
61
62
63 /**
64 * Y-coordinate of touch start event.
65 *
66 * @type number
67 */
68 this.touchStartY = 0;
69
70
71 /**
72 * ID of the last touch, retrieved from Touch.identifier.
73 *
74 * @type number
75 */
76 this.lastTouchIdentifier = 0;
77
78
79 /**
80 * Touchmove boundary, beyond which a click will be cancelled.
81 *
82 * @type number
83 */
84 this.touchBoundary = 10;
85
86
87 /**
88 * The FastClick layer.
89 *
90 * @type Element
91 */
92 this.layer = layer;
93
94 /**
95 * The minimum time between tap(touchstart and touchend) events
96 *
97 * @type number
98 */
99 this.tapDelay = 200;
100
101 /**
102 * The maximum time for a tap
103 *
104 * @type number
105 */
106 this.tapTimeout = 700;
107
108 // Some old versions of Android don't have Function.prototype.bind
109 function bind(method, context) {
110 return function() { return method.apply(context, arguments); };
111 }
112
113
114 var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
115 var context = this;
116 for (var i = 0, l = methods.length; i < l; i++) {
117 context[methods[i]] = bind(context[methods[i]], context);
118 }
119
120 layer.addEventListener('click', this.onClick, true);
121 layer.addEventListener('touchstart', this.onTouchStart, false);
122 layer.addEventListener('touchmove', this.onTouchMove, false);
123 layer.addEventListener('touchend', this.onTouchEnd, false);
124 layer.addEventListener('touchcancel', this.onTouchCancel, false);
125
126 // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
127 // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
128 // layer when they are cancelled.
129 if (!Event.prototype.stopImmediatePropagation) {
130 layer.removeEventListener = function(type, callback, capture) {
131 var rmv = Node.prototype.removeEventListener;
132 if (type === 'click') {
133 rmv.call(layer, type, callback.hijacked || callback, capture);
134 } else {
135 rmv.call(layer, type, callback, capture);
136 }
137 };
138
139 layer.addEventListener = function(type, callback, capture) {
140 var adv = Node.prototype.addEventListener;
141 if (type === 'click') {
142 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
143 if (!event.propagationStopped) {
144 callback(event);
145 }
146 }), capture);
147 } else {
148 adv.call(layer, type, callback, capture);
149 }
150 };
151 }
152
153 // If a handler is already declared in the element's onclick attribute, it will be fired before
154 // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
155 // adding it as listener.
156 if (typeof layer.onclick === 'function') {
157 // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
158 // - the old one won't work if passed to addEventListener directly.
159 oldOnClick = layer.onclick;
160 layer.addEventListener('click', function(event) {
161 oldOnClick(event);
162 }, false);
163 layer.onclick = null;
164 }
165 }
166
167 /**
168 * Valid types for text inputs
169 *
170 * @type array
171 */
172 var inputTypes = ['email', 'number', 'password', 'search', 'tel', 'text', 'url'];
173
174 /**
175 * @param {EventTarget} targetElement
176 * @returns {boolean}
177 */
178 FastClick.prototype.isInput = function(targetElement) {
179 return (
180 targetElement.tagName.toLowerCase() === 'textarea'
181 || inputTypes.indexOf(targetElement.type) !== -1
182 );
183 };
184
185 /**
186 * Determine whether a given element requires a native click.
187 *
188 * @param {EventTarget|Element} target Target DOM element
189 * @returns {boolean} Returns true if the element needs a native click
190 */
191 FastClick.prototype.needsClick = function(target) {
192 switch (target.nodeName.toLowerCase()) {
193
194 // Don't send a synthetic click to disabled inputs (issue #62)
195 case 'button':
196 case 'select':
197 case 'textarea':
198 if (target.disabled) {
199 return true;
200 }
201
202 break;
203 case 'input':
204
205 // File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
206 if (target.type === 'file' || target.disabled) {
207 return true;
208 }
209
210 break;
211 case 'label':
212 case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames
213 case 'video':
214 return true;
215 }
216
217 return (/\bneedsclick\b/).test(target.className);
218 };
219
220
221 /**
222 * Determine whether a given element requires a call to focus to simulate click into element.
223 *
224 * @param {EventTarget|Element} target Target DOM element
225 * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
226 */
227 FastClick.prototype.needsFocus = function(target) {
228 switch (target.nodeName.toLowerCase()) {
229 case 'textarea':
230 return true;
231 case 'select':
232 return true;
233 case 'input':
234 switch (target.type) {
235 case 'button':
236 case 'checkbox':
237 case 'file':
238 case 'image':
239 case 'radio':
240 case 'submit':
241 return false;
242 }
243
244 // No point in attempting to focus disabled inputs
245 return !target.disabled && !target.readOnly;
246 default:
247 return (/\bneedsfocus\b/).test(target.className);
248 }
249 };
250
251
252 /**
253 * Send a click event to the specified element.
254 *
255 * @param {EventTarget|Element} targetElement
256 * @param {Event} event
257 */
258 FastClick.prototype.sendClick = function(targetElement, event) {
259 var clickEvent, touch;
260
261 // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
262 if (document.activeElement && document.activeElement !== targetElement) {
263 document.activeElement.blur();
264 }
265
266 touch = event.changedTouches[0];
267
268 // Synthesise a click event, with an extra attribute so it can be tracked
269 clickEvent = document.createEvent('MouseEvents');
270 clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
271 clickEvent.forwardedTouchEvent = true;
272 targetElement.dispatchEvent(clickEvent);
273 };
274
275 /**
276 * @param {EventTarget|Element} targetElement
277 */
278 FastClick.prototype.focus = function(targetElement) {
279 var length;
280
281 targetElement.focus();
282
283 // Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
284 if (targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month' && targetElement.type !== 'email' && targetElement.type !== 'number') {
285 length = targetElement.value.length;
286 targetElement.setSelectionRange(length, length);
287 }
288 };
289
290
291 /**
292 * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
293 *
294 * @param {EventTarget|Element} targetElement
295 */
296 FastClick.prototype.updateScrollParent = function(targetElement) {
297 var scrollParent, parentElement;
298
299 scrollParent = targetElement.fastClickScrollParent;
300
301 // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
302 // target element was moved to another parent.
303 if (!scrollParent || !scrollParent.contains(targetElement)) {
304 parentElement = targetElement;
305 do {
306 if (parentElement.scrollHeight > parentElement.offsetHeight) {
307 scrollParent = parentElement;
308 targetElement.fastClickScrollParent = parentElement;
309 break;
310 }
311
312 parentElement = parentElement.parentElement;
313 } while (parentElement);
314 }
315
316 // Always update the scroll top tracker if possible.
317 if (scrollParent) {
318 scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
319 }
320 };
321
322
323 /**
324 * @param {EventTarget} targetElement
325 * @returns {Element|EventTarget}
326 */
327 FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
328
329 // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
330 if (eventTarget.nodeType === Node.TEXT_NODE) {
331 return eventTarget.parentNode;
332 }
333
334 return eventTarget;
335 };
336
337
338 /**
339 * On touch start, record the position and scroll offset.
340 *
341 * @param {Event} event
342 * @returns {boolean}
343 */
344 FastClick.prototype.onTouchStart = function(event) {
345 var targetElement, touch;
346
347 // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
348 if (event.targetTouches.length > 1) {
349 return true;
350 }
351
352 targetElement = this.getTargetElementFromEventTarget(event.target);
353 touch = event.targetTouches[0];
354
355 // Ignore touches on contenteditable elements to prevent conflict with text selection.
356 // (For details: https://github.com/ftlabs/fastclick/pull/211 )
357 if (targetElement.isContentEditable) {
358 return true;
359 }
360
361 // ignore touchstart in focused inputs, otherwise user needs to hold the input
362 // for the copy/paste menu
363 if (targetElement === document.activeElement && this.isInput(targetElement)) {
364 return true;
365 }
366
367 // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
368 // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
369 // with the same identifier as the touch event that previously triggered the click that triggered the alert.
370 // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
371 // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
372 // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
373 // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
374 // random integers, it's safe to to continue if the identifier is 0 here.
375 if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
376 event.preventDefault();
377 return false;
378 }
379
380 this.lastTouchIdentifier = touch.identifier;
381
382 // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
383 // 1) the user does a fling scroll on the scrollable layer
384 // 2) the user stops the fling scroll with another tap
385 // then the event.target of the last 'touchend' event will be the element that was under the user's finger
386 // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
387 // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
388 this.updateScrollParent(targetElement);
389
390 this.trackingClick = true;
391 this.trackingClickStart = event.timeStamp;
392 this.targetElement = targetElement;
393
394 this.touchStartX = touch.pageX;
395 this.touchStartY = touch.pageY;
396
397 // Prevent phantom clicks on fast double-tap (issue #36)
398 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
399 event.preventDefault();
400 }
401
402 return true;
403 };
404
405
406 /**
407 * Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
408 *
409 * @param {Event} event
410 * @returns {boolean}
411 */
412 FastClick.prototype.touchHasMoved = function(event) {
413 var touch = event.changedTouches[0], boundary = this.touchBoundary;
414
415 if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
416 return true;
417 }
418
419 return false;
420 };
421
422
423 /**
424 * Update the last position.
425 *
426 * @param {Event} event
427 * @returns {boolean}
428 */
429 FastClick.prototype.onTouchMove = function(event) {
430 if (!this.trackingClick) {
431 return true;
432 }
433
434 // If the touch has moved, cancel the click tracking
435 if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
436 this.trackingClick = false;
437 this.targetElement = null;
438 }
439
440 return true;
441 };
442
443
444 /**
445 * Attempt to find the labelled control for the given label element.
446 *
447 * @param {EventTarget|HTMLLabelElement} labelElement
448 * @returns {Element|null}
449 */
450 FastClick.prototype.findControl = function(labelElement) {
451
452 // Fast path for newer browsers supporting the HTML5 control attribute
453 if (labelElement.control !== undefined) {
454 return labelElement.control;
455 }
456
457 // All browsers under test that support touch events also support the HTML5 htmlFor attribute
458 if (labelElement.htmlFor) {
459 return document.getElementById(labelElement.htmlFor);
460 }
461
462 // If no for attribute exists, attempt to retrieve the first labellable descendant element
463 // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
464 return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
465 };
466
467
468 /**
469 * On touch end, determine whether to send a click event at once.
470 *
471 * @param {Event} event
472 * @returns {boolean}
473 */
474 FastClick.prototype.onTouchEnd = function(event) {
475 var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
476
477 if (!this.trackingClick) {
478 return true;
479 }
480
481 // Prevent phantom clicks on fast double-tap (issue #36)
482 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
483 this.cancelNextClick = true;
484 return true;
485 }
486
487 if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
488 return true;
489 }
490
491 // Reset to prevent wrong click cancel on input (issue #156).
492 this.cancelNextClick = false;
493
494 this.lastClickTime = event.timeStamp;
495
496 trackingClickStart = this.trackingClickStart;
497 this.trackingClick = false;
498 this.trackingClickStart = 0;
499
500 targetTagName = targetElement.tagName.toLowerCase();
501 if (targetTagName === 'label') {
502 forElement = this.findControl(targetElement);
503 if (forElement) {
504 this.focus(targetElement);
505 targetElement = forElement;
506 }
507 } else if (this.needsFocus(targetElement)) {
508
509 // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
510 // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
511 if ((event.timeStamp - trackingClickStart) > 100 || (window.top !== window && targetTagName === 'input')) {
512 this.targetElement = null;
513 return false;
514 }
515
516 this.focus(targetElement);
517 this.sendClick(targetElement, event);
518
519 // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
520 // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
521 if (targetTagName !== 'select') {
522 this.targetElement = null;
523 event.preventDefault();
524 }
525
526 return false;
527 }
528
529 // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
530 // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
531 scrollParent = targetElement.fastClickScrollParent;
532 if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
533 return true;
534 }
535
536 // Prevent the actual click from going though - unless the target node is marked as requiring
537 // real clicks or if it is in the allowlist in which case only non-programmatic clicks are permitted.
538 if (!this.needsClick(targetElement)) {
539 event.preventDefault();
540 this.sendClick(targetElement, event);
541 }
542
543 return false;
544 };
545
546
547 /**
548 * On touch cancel, stop tracking the click.
549 *
550 * @returns {void}
551 */
552 FastClick.prototype.onTouchCancel = function() {
553 this.trackingClick = false;
554 this.targetElement = null;
555 };
556
557
558 /**
559 * Determine mouse events which should be permitted.
560 *
561 * @param {Event} event
562 * @returns {boolean}
563 */
564 FastClick.prototype.onMouse = function(event) {
565
566 // If a target element was never set (because a touch event was never fired) allow the event
567 if (!this.targetElement) {
568 return true;
569 }
570
571 if (event.forwardedTouchEvent) {
572 return true;
573 }
574
575 // Programmatically generated events targeting a specific element should be permitted
576 if (!event.cancelable) {
577 return true;
578 }
579
580 // Derive and check the target element to see whether the mouse event needs to be permitted;
581 // unless explicitly enabled, prevent non-touch click events from triggering actions,
582 // to prevent ghost/doubleclicks.
583 if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
584
585 // Prevent any user-added listeners declared on FastClick element from being fired.
586 if (event.stopImmediatePropagation) {
587 event.stopImmediatePropagation();
588 } else {
589
590 // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
591 event.propagationStopped = true;
592 }
593
594 // Cancel the event
595 event.stopPropagation();
596 event.preventDefault();
597
598 return false;
599 }
600
601 // If the mouse event is permitted, return true for the action to go through.
602 return true;
603 };
604
605
606 /**
607 * On actual clicks, determine whether this is a touch-generated click, a click action occurring
608 * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
609 * an actual click which should be permitted.
610 *
611 * @param {Event} event
612 * @returns {boolean}
613 */
614 FastClick.prototype.onClick = function(event) {
615 var permitted;
616
617 // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
618 if (this.trackingClick) {
619 this.targetElement = null;
620 this.trackingClick = false;
621 return true;
622 }
623
624 // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
625 if (event.target.type === 'submit' && event.detail === 0) {
626 return true;
627 }
628
629 permitted = this.onMouse(event);
630
631 // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
632 if (!permitted) {
633 this.targetElement = null;
634 }
635
636 // If clicks are permitted, return true for the action to go through.
637 return permitted;
638 };
639
640 define(function() {
641 return function () {
642 document.addEventListener('DOMContentLoaded', () => {
643 new FastClick(document.body)
644 }, false)
645 };;
646 });
647}());