2012-12-27 18:37:05 -08:00
/ * *
* @ preserve FastClick : polyfill to remove click delays on browsers with touch UIs .
*
* @ version 0.4 . 6
* @ codingstandard ftlabs - jsv2
* @ copyright The Financial Times Limited [ All Rights Reserved ]
* @ license MIT License ( see LICENSE . txt )
* /
2012-12-25 19:03:50 -08:00
2012-12-27 18:37:05 -08:00
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/ * *
* Instantiate fast - clicking listeners on the specificed layer .
*
* @ constructor
* @ param { Element } layer The layer to listen on
* /
function FastClick ( layer ) {
'use strict' ;
var oldOnClick , self = this ;
/ * *
* Whether a click is currently being tracked .
*
* @ type boolean
* /
this . trackingClick = false ;
/ * *
* Timestamp for when when click tracking started .
*
* @ type number
* /
this . trackingClickStart = 0 ;
/ * *
* The element being tracked for a click .
*
* @ type EventTarget
* /
this . targetElement = null ;
/ * *
* X - coordinate of touch start event .
*
* @ type number
* /
this . touchStartX = 0 ;
/ * *
* Y - coordinate of touch start event .
*
* @ type number
* /
this . touchStartY = 0 ;
/ * *
* ID of the last touch , retrieved from Touch . identifier .
*
* @ type number
* /
this . lastTouchIdentifier = 0 ;
/ * *
* The FastClick layer .
*
* @ type Element
* /
this . layer = layer ;
if ( ! layer || ! layer . nodeType ) {
throw new TypeError ( 'Layer must be a document node' ) ;
}
/** @type function() */
this . onClick = function ( ) { FastClick . prototype . onClick . apply ( self , arguments ) ; } ;
/** @type function() */
this . onTouchStart = function ( ) { FastClick . prototype . onTouchStart . apply ( self , arguments ) ; } ;
/** @type function() */
this . onTouchMove = function ( ) { FastClick . prototype . onTouchMove . apply ( self , arguments ) ; } ;
/** @type function() */
this . onTouchEnd = function ( ) { FastClick . prototype . onTouchEnd . apply ( self , arguments ) ; } ;
/** @type function() */
this . onTouchCancel = function ( ) { FastClick . prototype . onTouchCancel . apply ( self , 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 , false ) ;
layer . addEventListener ( 'touchmove' , this . onTouchMove , false ) ;
layer . addEventListener ( 'touchend' , this . onTouchEnd , false ) ;
layer . addEventListener ( 'touchcancel' , this . onTouchCancel , false ) ;
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if ( ! Event . prototype . stopImmediatePropagation ) {
layer . removeEventListener = function ( type , callback , capture ) {
var rmv = Node . prototype . removeEventListener ;
if ( type === 'click' ) {
rmv . call ( layer , type , callback . hijacked || callback , capture ) ;
} else {
rmv . call ( layer , type , callback , capture ) ;
}
} ;
layer . addEventListener = function ( type , callback , capture ) {
var adv = Node . prototype . addEventListener ;
if ( type === 'click' ) {
adv . call ( layer , type , callback . hijacked || ( callback . hijacked = function ( event ) {
if ( ! event . propagationStopped ) {
callback ( event ) ;
}
} ) , capture ) ;
} else {
adv . call ( layer , type , callback , capture ) ;
}
} ;
}
// 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 ;
}
2012-12-25 19:03:50 -08:00
}
2012-12-27 18:37:05 -08:00
/ * *
* Android requires an exception for labels .
*
* @ type boolean
* /
FastClick . prototype . deviceIsAndroid = navigator . userAgent . indexOf ( 'Android' ) > 0 ;
/ * *
* iOS requires an exception for alert confirm dialogs .
*
* @ type boolean
* /
FastClick . prototype . deviceIsIOS = /iP(ad|hone|od)/ . test ( navigator . userAgent ) ;
/ * *
* iOS 4 requires an exception for select elements .
*
* @ type boolean
* /
FastClick . prototype . deviceIsIOS4 = FastClick . prototype . deviceIsIOS && ( /OS 4_\d(_\d)?/ ) . test ( navigator . userAgent ) ;
/ * *
* Determine whether a given element requires a native click .
*
* @ param { EventTarget | Element } target 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 { EventTarget | 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 ;
}
return true ;
default :
return ( /\bneedsfocus\b/ ) . test ( target . className ) ;
}
} ;
/ * *
* Send a click event to the specified element .
*
* @ param { EventTarget | Element } targetElement
* @ param { Event } event
* /
FastClick . prototype . sendClick = function ( targetElement , event ) {
'use strict' ;
var clickEvent , touch ;
// 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 ) ;
} ;
/ * *
* 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 . trackingClickStart = event . timeStamp ;
this . targetElement = event . target ;
this . theTarget = $ ( this . targetElement ) . closest ( 'a' ) . get ( 0 ) ;
this . theTarget . className += ' pressed' ;
this . touchStartX = touch . pageX ;
this . touchStartY = touch . pageY ;
this . startClickTime = new Date ;
// Prevent phantom clicks on fast double-tap (issue #36)
if ( ( event . timeStamp - this . lastClickTime ) < 200 ) {
event . preventDefault ( ) ;
}
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 ;
2012-12-25 19:03:50 -08:00
this . theTarget . className = this . theTarget . className . replace ( / ?pressed/gi , '' ) ;
2012-12-27 18:37:05 -08:00
this . targetElement = null ;
}
return true ;
} ;
/ * *
* Attempt to find the labelled control for the given label element .
*
* @ param { EventTarget | HTMLLabelElement } labelElement
* @ returns { Element | null }
* /
FastClick . prototype . findControl = function ( labelElement ) {
'use strict' ;
// Fast path for newer browsers supporting the HTML5 control attribute
if ( labelElement . control !== undefined ) {
return labelElement . control ;
}
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
if ( labelElement . htmlFor ) {
return document . getElementById ( labelElement . htmlFor ) ;
}
// If no for attribute exists, attempt to retrieve the first labellable descendant element
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
return labelElement . querySelector ( 'button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea' ) ;
} ;
/ * *
* 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 , trackingClickStart , targetElement = this . targetElement , touch = event . changedTouches [ 0 ] ;
if ( ! this . trackingClick ) {
return true ;
}
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
if ( this . deviceIsIOS ) {
if ( touch . identifier === this . lastTouchIdentifier ) {
event . preventDefault ( ) ;
return false ;
}
this . lastTouchIdentifier = touch . identifier ;
}
// Prevent phantom clicks on fast double-tap (issue #36)
if ( ( event . timeStamp - this . lastClickTime ) < 200 ) {
this . cancelNextClick = true ;
return true ;
}
if ( ( new Date - this . startClickTime ) > 115 ) {
return false ;
2012-12-25 19:03:50 -08:00
}
2012-12-27 18:37:05 -08:00
this . lastClickTime = event . timeStamp ;
trackingClickStart = this . trackingClickStart ;
this . trackingClick = false ;
this . theTarget . className = this . theTarget . className . replace ( / ?pressed/gi , '' ) ;
this . trackingClickStart = 0 ;
if ( targetElement . nodeName . toLowerCase ( ) === 'label' ) {
forElement = this . findControl ( targetElement ) ;
if ( forElement ) {
targetElement . focus ( ) ;
if ( this . deviceIsAndroid ) {
return false ;
}
if ( ! this . needsClick ( forElement ) ) {
event . preventDefault ( ) ;
this . sendClick ( forElement , event ) ;
}
return false ;
}
} else if ( this . needsFocus ( targetElement ) ) {
// 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.
if ( ( event . timeStamp - trackingClickStart ) > 100 ) {
this . targetElement = null ;
return true ;
}
targetElement . focus ( ) ;
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
if ( ! this . deviceIsIOS4 || targetElement . tagName . toLowerCase ( ) !== 'select' ) {
this . targetElement = null ;
event . preventDefault ( ) ;
}
return false ;
}
// 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.
if ( ! this . needsClick ( targetElement ) ) {
event . preventDefault ( ) ;
this . sendClick ( targetElement , event ) ;
}
return false ;
2012-12-25 19:03:50 -08:00
} ;
2012-12-27 18:37:05 -08:00
/ * *
* On touch cancel , stop tracking the click .
*
* @ returns { void }
* /
FastClick . prototype . onTouchCancel = function ( ) {
'use strict' ;
this . theTarget . className = this . theTarget . className . replace ( / ?pressed/gi , '' ) ;
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 a target element was never set (because a touch event was never fired) allow the click
if ( ! this . targetElement ) {
return true ;
}
if ( event . forwardedTouchEvent ) {
return true ;
}
oldTargetElement = this . targetElement ;
this . targetElement = null ;
// 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.
if ( this . trackingClick ) {
this . trackingClick = false ;
return true ;
}
// 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 ) || this . cancelNextClick ) {
this . cancelNextClick = false ;
// Prevent any user-added listeners declared on FastClick element from being fired.
if ( event . stopImmediatePropagation ) {
event . stopImmediatePropagation ( ) ;
} else {
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
event . propagationStopped = true ;
}
// 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 , false ) ;
layer . removeEventListener ( 'touchmove' , this . onTouchMove , false ) ;
layer . removeEventListener ( 'touchend' , this . onTouchEnd , false ) ;
layer . removeEventListener ( 'touchcancel' , this . onTouchCancel , false ) ;
} ;
if ( typeof define !== 'undefined' && define . amd ) {
// AMD. Register as an anonymous module.
define ( function ( ) {
'use strict' ;
return FastClick ;
} ) ;
}
if ( typeof module !== 'undefined' && module . exports ) {
module . exports = function ( layer ) {
'use strict' ;
return new FastClick ( layer ) ;
} ;
module . exports . FastClick = FastClick ;
}
function attachFastClick ( options ) {
options = options || { } ;
2012-12-25 19:03:50 -08:00
var avatars = document . getElementsByClassName ( "NB-show-profile" ) ;
Array . prototype . slice . call ( avatars , 0 ) . forEach ( function ( avatar ) {
2012-12-27 18:37:05 -08:00
new FastClick ( avatar , options ) ;
2012-12-25 19:03:50 -08:00
} ) ;
var tags = document . getElementsByClassName ( "NB-story-tag" ) ;
Array . prototype . slice . call ( tags , 0 ) . forEach ( function ( tag ) {
2012-12-27 18:37:05 -08:00
new FastClick ( tag , options ) ;
2012-12-25 19:03:50 -08:00
} ) ;
2012-12-27 00:15:26 -08:00
var authors = document . getElementsByClassName ( "NB-story-author" ) ;
Array . prototype . slice . call ( authors , 0 ) . forEach ( function ( author ) {
2012-12-27 18:37:05 -08:00
new FastClick ( author , options ) ;
2012-12-27 00:15:26 -08:00
} ) ;
var publishers = document . getElementsByClassName ( "NB-story-publisher" ) ;
Array . prototype . slice . call ( publishers , 0 ) . forEach ( function ( publisher ) {
2012-12-27 18:37:05 -08:00
new FastClick ( publisher , options ) ;
2012-12-27 00:15:26 -08:00
} ) ;
var titles = document . getElementsByClassName ( "NB-story-title" ) ;
Array . prototype . slice . call ( titles , 0 ) . forEach ( function ( title ) {
2012-12-27 18:37:05 -08:00
new FastClick ( title , options ) ;
2012-12-27 00:15:26 -08:00
} ) ;
2012-12-25 19:03:50 -08:00
var author = document . getElementById ( "NB-story-author" ) ;
if ( author ) {
2012-12-27 18:37:05 -08:00
new FastClick ( author , options ) ;
2012-12-25 19:03:50 -08:00
}
}
Zepto ( function ( $ ) {
attachFastClick ( ) ;
} ) ;