/ajax/libs/fastclick/0.3.5/fastclick.js
JavaScript | 376 lines | 188 code | 70 blank | 118 comment | 33 complexity | a9e2db328c2eecebf537f469371c5ef1 MD5 | raw file
- /**
- * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
- *
- * @version 0.3.5
- * @copyright The Financial Times Limited [All Rights Reserved]
- * @license MIT License (see LICENSE.txt)
- */
- /*jslint browser:true*/
- /*global define*/
- /**
- * Instantiate fast-clicking listeners on the specificed layer.
- *
- * @constructor
- * @param {Element} layer The layer to listen on
- */
- function FastClick(layer) {
- 'use strict';
- var oldOnClick, that = this;
- /**
- * Whether a click is currently being tracked.
- *
- * @type boolean
- */
- this.trackingClick = false,
- /**
- * The element being tracked for a click.
- *
- * @type Element
- */
- this.targetElement = null;
- /**
- * The FastClick layer.
- *
- * @type Element
- */
- this.layer = layer;
- if (!layer || !layer.nodeType) {
- throw new TypeError('Layer must be a document node');
- }
- // Bind handlers to this instance
- this.onClick = function() { FastClick.prototype.onClick.apply(that, arguments); };
- this.onTouchStart = function() { FastClick.prototype.onTouchStart.apply(that, arguments); };
- this.onTouchMove = function() { FastClick.prototype.onTouchMove.apply(that, arguments); };
- this.onTouchEnd = function() { FastClick.prototype.onTouchEnd.apply(that, arguments); };
- this.onTouchCancel = function() { FastClick.prototype.onTouchCancel.apply(that, arguments); };
- // Devices that don't support touch don't need FastClick
- if (typeof window.ontouchstart === 'undefined') {
- return;
- }
- // Set up event handlers as required
- layer.addEventListener('click', this.onClick, true);
- layer.addEventListener('touchstart', this.onTouchStart, true);
- layer.addEventListener('touchmove', this.onTouchMove, true);
- layer.addEventListener('touchend', this.onTouchEnd, true);
- layer.addEventListener('touchcancel', this.onTouchCancel, true);
- // If a handler is already declared in the element's onclick attribute, it will be fired before
- // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
- // adding it as listener.
- if (typeof layer.onclick === 'function') {
- // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
- // - the old one won't work if passed to addEventListener directly.
- oldOnClick = layer.onclick;
- layer.addEventListener('click', function(event) {
- oldOnClick(event);
- }, false);
- layer.onclick = null;
- }
- }
- /**
- * Android requires an exception for labels.
- *
- * @type boolean
- */
- FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
- /**
- * Determine whether a given element requires a native click.
- *
- * @param {Element} target DOM element
- * @returns {boolean} Returns true if the element needs a native click
- */
- FastClick.prototype.needsClick = function(target) {
- 'use strict';
- switch (target.nodeName.toLowerCase()) {
- case 'label':
- case 'video':
- return true;
- default:
- return (/\bneedsclick\b/).test(target.className);
- }
- };
- /**
- * Determine whether a given element requires a call to focus to simulate click into element.
- *
- * @param {Element} target target DOM element.
- * @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
- */
- FastClick.prototype.needsFocus = function(target) {
- 'use strict';
- switch(target.nodeName.toLowerCase()) {
- case 'textarea':
- case 'select':
- return true;
- case 'input':
- switch (target.type) {
- case 'button':
- case 'checkbox':
- case 'file':
- case 'image':
- case 'radio':
- case 'submit':
- return false;
- default:
- return true;
- }
- break;
- default:
- return (/\bneedsfocus\b/).test(target.className);
- }
- };
- /**
- * Send a click event to the element if it needs it.
- *
- * @returns {boolean} Whether the click was sent or not
- */
- FastClick.prototype.maybeSendClick = function(targetElement, event) {
- 'use strict';
- var clickEvent, touch;
- // Prevent the actual click from going though - unless the target node is marked as requiring
- // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted
- // to open the options list and so the original event is required.
- if (this.needsClick(targetElement)) {
- return false;
- }
- // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
- if (document.activeElement && document.activeElement !== targetElement) {
- document.activeElement.blur();
- }
- touch = event.changedTouches[0];
- // Synthesise a click event, with an extra attribute so it can be tracked
- clickEvent = document.createEvent('MouseEvents');
- clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
- clickEvent.forwardedTouchEvent = true;
- targetElement.dispatchEvent(clickEvent);
- return true;
- };
- /**
- * On touch start, record the position and scroll offset.
- *
- * @param {Event} event
- * @returns {boolean}
- */
- FastClick.prototype.onTouchStart = function(event) {
- 'use strict';
- var touch = event.targetTouches[0];
- this.trackingClick = true;
- this.targetElement = event.target;
- this.touchStartX = touch.pageX;
- this.touchStartY = touch.pageY;
- return true;
- };
- /**
- * Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
- *
- * @param {Event} event
- * @returns {boolean}
- */
- FastClick.prototype.touchHasMoved = function(event) {
- 'use strict';
- var touch = event.targetTouches[0];
- if (Math.abs(touch.pageX - this.touchStartX) > 10
- || Math.abs(touch.pageY - this.touchStartY) > 10) {
- return true;
- }
- return false;
- };
- /**
- * Update the last position.
- *
- * @param {Event} event
- * @returns {boolean}
- */
- FastClick.prototype.onTouchMove = function(event) {
- 'use strict';
- if (!this.trackingClick) {
- return true;
- }
- // If the touch has moved, cancel the click tracking
- if (this.targetElement !== event.target || this.touchHasMoved(event)) {
- this.trackingClick = false;
- this.targetElement = null;
- }
- return true;
- };
- /**
- * On touch end, determine whether to send a click event at once.
- *
- * @param {Event} event
- * @returns {boolean}
- */
- FastClick.prototype.onTouchEnd = function(event) {
- 'use strict';
- var forElement, targetElement = this.targetElement;
- if (!this.trackingClick) {
- return true;
- }
- this.trackingClick = false;
- if (targetElement.nodeName.toLowerCase() === 'label' && targetElement.htmlFor) {
- forElement = document.getElementById(targetElement.htmlFor);
- if (forElement) {
- targetElement.focus();
- if (this.deviceIsAndroid) {
- return false;
- }
- if (this.maybeSendClick(forElement, event)) {
- event.preventDefault();
- }
- return false;
- }
- } else if (this.needsFocus(targetElement)) {
- targetElement.focus();
- if (targetElement.tagName.toLowerCase() !== 'select') {
- event.preventDefault();
- }
- return false;
- }
- if (!this.maybeSendClick(targetElement, event)) {
- return false;
- }
- event.preventDefault();
- return false;
- };
- /**
- * On touch cancel, stop tracking the click.
- *
- * @returns {void}
- */
- FastClick.prototype.onTouchCancel = function() {
- 'use strict';
- this.trackingClick = false;
- this.targetElement = null;
- };
- /**
- * On actual clicks, determine whether this is a touch-generated click, a click action occurring
- * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
- * an actual click which should be permitted.
- *
- * @param {Event} event
- * @returns {boolean}
- */
- FastClick.prototype.onClick = function(event) {
- 'use strict';
- var oldTargetElement;
- if (event.forwardedTouchEvent) {
- return true;
- }
- // If a target element was never set (because a touch event was never fired) allow the click
- if (!this.targetElement) {
- return true;
- }
- oldTargetElement = this.targetElement;
- this.targetElement = null;
- // Programmatically generated events targeting a specific element should be permitted
- if (!event.cancelable) {
- return true;
- }
- // 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.
- if (event.target.type === 'submit' && event.detail === 0) {
- return true;
- }
- // Derive and check the target element to see whether the click needs to be permitted;
- // unless explicitly enabled, prevent non-touch click events from triggering actions,
- // to prevent ghost/doubleclicks.
- if (!this.needsClick(oldTargetElement)) {
- // Prevent any user-added listeners declared on FastClick element from being fired.
- if (event.stopImmediatePropagation) {
- event.stopImmediatePropagation();
- }
- // Cancel the event
- event.stopPropagation();
- event.preventDefault();
- return false;
- }
- // If clicks are permitted, return true for the action to go through.
- return true;
- };
- /**
- * Remove all FastClick's event listeners.
- *
- * @returns {void}
- */
- FastClick.prototype.destroy = function() {
- 'use strict';
- var layer = this.layer;
- layer.removeEventListener('click', this.onClick, true);
- layer.removeEventListener('touchstart', this.onTouchStart, true);
- layer.removeEventListener('touchmove', this.onTouchMove, true);
- layer.removeEventListener('touchend', this.onTouchEnd, true);
- layer.removeEventListener('touchcancel', this.onTouchCancel, true);
- };
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as an anonymous module.
- define(function() {
- 'use strict';
- return FastClick;
- });
- }