| |
| // This plugin is an experiment for abstracting away the touch and mouse |
| // events so that developers don't have to worry about which method of input |
| // the device their document is loaded on supports. |
| // |
| // The idea here is to allow the developer to register listeners for the |
| // basic mouse events, such as mousedown, mousemove, mouseup, and click, |
| // and the plugin will take care of registering the correct listeners |
| // behind the scenes to invoke the listener at the fastest possible time |
| // for that device, while still retaining the order of event firing in |
| // the traditional mouse environment, should multiple handlers be registered |
| // on the same element for different events. |
| // |
| // The current version exposes the following virtual events to jQuery bind methods: |
| // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel" |
| |
| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| //>>description: Normalizes touch/mouse events. |
| //>>label: Virtual Mouse (vmouse) Bindings |
| //>>group: Core |
| |
| define( [ "jquery" ], function( jQuery ) { |
| //>>excludeEnd("jqmBuildExclude"); |
| (function( $, window, document, undefined ) { |
| |
| var dataPropertyName = "virtualMouseBindings", |
| touchTargetPropertyName = "virtualTouchID", |
| virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ), |
| touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ), |
| mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [], |
| mouseEventProps = $.event.props.concat( mouseHookProps ), |
| activeDocHandlers = {}, |
| resetTimerID = 0, |
| startX = 0, |
| startY = 0, |
| didScroll = false, |
| clickBlockList = [], |
| blockMouseTriggers = false, |
| blockTouchTriggers = false, |
| eventCaptureSupported = "addEventListener" in document, |
| $document = $.mobile.$document, |
| nextTouchID = 1, |
| lastTouchID = 0, threshold; |
| |
| $.vmouse = { |
| moveDistanceThreshold: 10, |
| clickDistanceThreshold: 10, |
| resetTimerDuration: 1500 |
| }; |
| |
| function getNativeEvent( event ) { |
| |
| while ( event && typeof event.originalEvent !== "undefined" ) { |
| event = event.originalEvent; |
| } |
| return event; |
| } |
| |
| function createVirtualEvent( event, eventType ) { |
| |
| var t = event.type, |
| oe, props, ne, prop, ct, touch, i, j, len; |
| |
| event = $.Event( event ); |
| event.type = eventType; |
| |
| oe = event.originalEvent; |
| props = $.event.props; |
| |
| // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280 |
| // https://github.com/jquery/jquery-mobile/issues/3280 |
| if ( t.search( /^(mouse|click)/ ) > -1 ) { |
| props = mouseEventProps; |
| } |
| |
| // copy original event properties over to the new event |
| // this would happen if we could call $.event.fix instead of $.Event |
| // but we don't have a way to force an event to be fixed multiple times |
| if ( oe ) { |
| for ( i = props.length, prop; i; ) { |
| prop = props[ --i ]; |
| event[ prop ] = oe[ prop ]; |
| } |
| } |
| |
| // make sure that if the mouse and click virtual events are generated |
| // without a .which one is defined |
| if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) { |
| event.which = 1; |
| } |
| |
| if ( t.search(/^touch/) !== -1 ) { |
| ne = getNativeEvent( oe ); |
| t = ne.touches; |
| ct = ne.changedTouches; |
| touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined ); |
| |
| if ( touch ) { |
| for ( j = 0, len = touchEventProps.length; j < len; j++) { |
| prop = touchEventProps[ j ]; |
| event[ prop ] = touch[ prop ]; |
| } |
| } |
| } |
| |
| return event; |
| } |
| |
| function getVirtualBindingFlags( element ) { |
| |
| var flags = {}, |
| b, k; |
| |
| while ( element ) { |
| |
| b = $.data( element, dataPropertyName ); |
| |
| for ( k in b ) { |
| if ( b[ k ] ) { |
| flags[ k ] = flags.hasVirtualBinding = true; |
| } |
| } |
| element = element.parentNode; |
| } |
| return flags; |
| } |
| |
| function getClosestElementWithVirtualBinding( element, eventType ) { |
| var b; |
| while ( element ) { |
| |
| b = $.data( element, dataPropertyName ); |
| |
| if ( b && ( !eventType || b[ eventType ] ) ) { |
| return element; |
| } |
| element = element.parentNode; |
| } |
| return null; |
| } |
| |
| function enableTouchBindings() { |
| blockTouchTriggers = false; |
| } |
| |
| function disableTouchBindings() { |
| blockTouchTriggers = true; |
| } |
| |
| function enableMouseBindings() { |
| lastTouchID = 0; |
| clickBlockList.length = 0; |
| blockMouseTriggers = false; |
| |
| // When mouse bindings are enabled, our |
| // touch bindings are disabled. |
| disableTouchBindings(); |
| } |
| |
| function disableMouseBindings() { |
| // When mouse bindings are disabled, our |
| // touch bindings are enabled. |
| enableTouchBindings(); |
| } |
| |
| function startResetTimer() { |
| clearResetTimer(); |
| resetTimerID = setTimeout( function() { |
| resetTimerID = 0; |
| enableMouseBindings(); |
| }, $.vmouse.resetTimerDuration ); |
| } |
| |
| function clearResetTimer() { |
| if ( resetTimerID ) { |
| clearTimeout( resetTimerID ); |
| resetTimerID = 0; |
| } |
| } |
| |
| function triggerVirtualEvent( eventType, event, flags ) { |
| var ve; |
| |
| if ( ( flags && flags[ eventType ] ) || |
| ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) { |
| |
| ve = createVirtualEvent( event, eventType ); |
| |
| $( event.target).trigger( ve ); |
| } |
| |
| return ve; |
| } |
| |
| function mouseEventCallback( event ) { |
| var touchID = $.data( event.target, touchTargetPropertyName ); |
| |
| if ( ( $.support.touch === true ) && ( touchID === undefined ) ) { |
| return; |
| } |
| |
| if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) { |
| var ve = triggerVirtualEvent( "v" + event.type, event ); |
| if ( ve ) { |
| if ( ve.isDefaultPrevented() ) { |
| event.preventDefault(); |
| } |
| if ( ve.isPropagationStopped() ) { |
| event.stopPropagation(); |
| } |
| if ( ve.isImmediatePropagationStopped() ) { |
| event.stopImmediatePropagation(); |
| } |
| } |
| } |
| } |
| |
| function handleTouchStart( event ) { |
| |
| var touches = getNativeEvent( event ).touches, |
| target, flags; |
| |
| if ( touches && touches.length === 1 ) { |
| |
| target = event.target; |
| flags = getVirtualBindingFlags( target ); |
| |
| if ( flags.hasVirtualBinding ) { |
| |
| lastTouchID = nextTouchID++; |
| $.data( target, touchTargetPropertyName, lastTouchID ); |
| |
| clearResetTimer(); |
| |
| disableMouseBindings(); |
| didScroll = false; |
| |
| var t = getNativeEvent( event ).touches[ 0 ]; |
| startX = t.pageX; |
| startY = t.pageY; |
| |
| triggerVirtualEvent( "vmouseover", event, flags ); |
| triggerVirtualEvent( "vmousedown", event, flags ); |
| } |
| } |
| } |
| |
| function handleScroll( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| if ( !didScroll ) { |
| triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) ); |
| } |
| |
| didScroll = true; |
| startResetTimer(); |
| } |
| |
| function handleTouchMove( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| var t = getNativeEvent( event ).touches[ 0 ], |
| didCancel = didScroll, |
| moveThreshold = $.vmouse.moveDistanceThreshold, |
| flags = getVirtualBindingFlags( event.target ); |
| |
| didScroll = didScroll || |
| ( Math.abs( t.pageX - startX ) > moveThreshold || |
| Math.abs( t.pageY - startY ) > moveThreshold ); |
| |
| |
| if ( didScroll && !didCancel ) { |
| triggerVirtualEvent( "vmousecancel", event, flags ); |
| } |
| |
| triggerVirtualEvent( "vmousemove", event, flags ); |
| startResetTimer(); |
| } |
| |
| function handleTouchEnd( event ) { |
| if ( blockTouchTriggers ) { |
| return; |
| } |
| |
| disableTouchBindings(); |
| |
| var flags = getVirtualBindingFlags( event.target ), |
| t; |
| triggerVirtualEvent( "vmouseup", event, flags ); |
| |
| if ( !didScroll ) { |
| var ve = triggerVirtualEvent( "vclick", event, flags ); |
| if ( ve && ve.isDefaultPrevented() ) { |
| // The target of the mouse events that follow the touchend |
| // event don't necessarily match the target used during the |
| // touch. This means we need to rely on coordinates for blocking |
| // any click that is generated. |
| t = getNativeEvent( event ).changedTouches[ 0 ]; |
| clickBlockList.push({ |
| touchID: lastTouchID, |
| target: event.target, |
| x: t.clientX, |
| y: t.clientY |
| }); |
| |
| // Prevent any mouse events that follow from triggering |
| // virtual event notifications. |
| blockMouseTriggers = true; |
| } |
| } |
| triggerVirtualEvent( "vmouseout", event, flags); |
| didScroll = false; |
| |
| startResetTimer(); |
| } |
| |
| function hasVirtualBindings( ele ) { |
| var bindings = $.data( ele, dataPropertyName ), |
| k; |
| |
| if ( bindings ) { |
| for ( k in bindings ) { |
| if ( bindings[ k ] ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| function dummyMouseHandler() {} |
| |
| function getSpecialEventObject( eventType ) { |
| var realType = eventType.substr( 1 ); |
| |
| return { |
| setup: function( data, namespace ) { |
| // If this is the first virtual mouse binding for this element, |
| // add a bindings object to its data. |
| |
| if ( !hasVirtualBindings( this ) ) { |
| $.data( this, dataPropertyName, {} ); |
| } |
| |
| // If setup is called, we know it is the first binding for this |
| // eventType, so initialize the count for the eventType to zero. |
| var bindings = $.data( this, dataPropertyName ); |
| bindings[ eventType ] = true; |
| |
| // If this is the first virtual mouse event for this type, |
| // register a global handler on the document. |
| |
| activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1; |
| |
| if ( activeDocHandlers[ eventType ] === 1 ) { |
| $document.bind( realType, mouseEventCallback ); |
| } |
| |
| // Some browsers, like Opera Mini, won't dispatch mouse/click events |
| // for elements unless they actually have handlers registered on them. |
| // To get around this, we register dummy handlers on the elements. |
| |
| $( this ).bind( realType, dummyMouseHandler ); |
| |
| // For now, if event capture is not supported, we rely on mouse handlers. |
| if ( eventCaptureSupported ) { |
| // If this is the first virtual mouse binding for the document, |
| // register our touchstart handler on the document. |
| |
| activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1; |
| |
| if ( activeDocHandlers[ "touchstart" ] === 1 ) { |
| $document.bind( "touchstart", handleTouchStart ) |
| .bind( "touchend", handleTouchEnd ) |
| |
| // On touch platforms, touching the screen and then dragging your finger |
| // causes the window content to scroll after some distance threshold is |
| // exceeded. On these platforms, a scroll prevents a click event from being |
| // dispatched, and on some platforms, even the touchend is suppressed. To |
| // mimic the suppression of the click event, we need to watch for a scroll |
| // event. Unfortunately, some platforms like iOS don't dispatch scroll |
| // events until *AFTER* the user lifts their finger (touchend). This means |
| // we need to watch both scroll and touchmove events to figure out whether |
| // or not a scroll happenens before the touchend event is fired. |
| |
| .bind( "touchmove", handleTouchMove ) |
| .bind( "scroll", handleScroll ); |
| } |
| } |
| }, |
| |
| teardown: function( data, namespace ) { |
| // If this is the last virtual binding for this eventType, |
| // remove its global handler from the document. |
| |
| --activeDocHandlers[ eventType ]; |
| |
| if ( !activeDocHandlers[ eventType ] ) { |
| $document.unbind( realType, mouseEventCallback ); |
| } |
| |
| if ( eventCaptureSupported ) { |
| // If this is the last virtual mouse binding in existence, |
| // remove our document touchstart listener. |
| |
| --activeDocHandlers[ "touchstart" ]; |
| |
| if ( !activeDocHandlers[ "touchstart" ] ) { |
| $document.unbind( "touchstart", handleTouchStart ) |
| .unbind( "touchmove", handleTouchMove ) |
| .unbind( "touchend", handleTouchEnd ) |
| .unbind( "scroll", handleScroll ); |
| } |
| } |
| |
| var $this = $( this ), |
| bindings = $.data( this, dataPropertyName ); |
| |
| // teardown may be called when an element was |
| // removed from the DOM. If this is the case, |
| // jQuery core may have already stripped the element |
| // of any data bindings so we need to check it before |
| // using it. |
| if ( bindings ) { |
| bindings[ eventType ] = false; |
| } |
| |
| // Unregister the dummy event handler. |
| |
| $this.unbind( realType, dummyMouseHandler ); |
| |
| // If this is the last virtual mouse binding on the |
| // element, remove the binding data from the element. |
| |
| if ( !hasVirtualBindings( this ) ) { |
| $this.removeData( dataPropertyName ); |
| } |
| } |
| }; |
| } |
| |
| // Expose our custom events to the jQuery bind/unbind mechanism. |
| |
| for ( var i = 0; i < virtualEventNames.length; i++ ) { |
| $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] ); |
| } |
| |
| // Add a capture click handler to block clicks. |
| // Note that we require event capture support for this so if the device |
| // doesn't support it, we punt for now and rely solely on mouse events. |
| if ( eventCaptureSupported ) { |
| document.addEventListener( "click", function( e ) { |
| var cnt = clickBlockList.length, |
| target = e.target, |
| x, y, ele, i, o, touchID; |
| |
| if ( cnt ) { |
| x = e.clientX; |
| y = e.clientY; |
| threshold = $.vmouse.clickDistanceThreshold; |
| |
| // The idea here is to run through the clickBlockList to see if |
| // the current click event is in the proximity of one of our |
| // vclick events that had preventDefault() called on it. If we find |
| // one, then we block the click. |
| // |
| // Why do we have to rely on proximity? |
| // |
| // Because the target of the touch event that triggered the vclick |
| // can be different from the target of the click event synthesized |
| // by the browser. The target of a mouse/click event that is syntehsized |
| // from a touch event seems to be implementation specific. For example, |
| // some browsers will fire mouse/click events for a link that is near |
| // a touch event, even though the target of the touchstart/touchend event |
| // says the user touched outside the link. Also, it seems that with most |
| // browsers, the target of the mouse/click event is not calculated until the |
| // time it is dispatched, so if you replace an element that you touched |
| // with another element, the target of the mouse/click will be the new |
| // element underneath that point. |
| // |
| // Aside from proximity, we also check to see if the target and any |
| // of its ancestors were the ones that blocked a click. This is necessary |
| // because of the strange mouse/click target calculation done in the |
| // Android 2.1 browser, where if you click on an element, and there is a |
| // mouse/click handler on one of its ancestors, the target will be the |
| // innermost child of the touched element, even if that child is no where |
| // near the point of touch. |
| |
| ele = target; |
| |
| while ( ele ) { |
| for ( i = 0; i < cnt; i++ ) { |
| o = clickBlockList[ i ]; |
| touchID = 0; |
| |
| if ( ( ele === target && target === o.target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) || |
| $.data( ele, touchTargetPropertyName ) === o.touchID ) { |
| // XXX: We may want to consider removing matches from the block list |
| // instead of waiting for the reset timer to fire. |
| e.preventDefault(); |
| e.stopPropagation(); |
| return; |
| } |
| } |
| ele = ele.parentNode; |
| } |
| } |
| }, true); |
| } |
| })( jQuery, window, document ); |
| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| }); |
| //>>excludeEnd("jqmBuildExclude"); |