Skip to content
This repository has been archived by the owner on Oct 8, 2021. It is now read-only.

Commit

Permalink
Swipe event: Trigger only one event per mouse sequence and let it bubble
Browse files Browse the repository at this point in the history
The element to which a mousedown handler is attached by the swipe setup() code
takes ownership of the next swipe event. This means that, if the same mousedown
event triggers other swipe setup() -attached mousedown handlers as it bubbles,
the subsequent ones will bail. Thus, if a swipe event results from the mousedown
then such a swipe event will be triggered on the deepest element with an
attached swipe handler, and it will be allowed to bubble from there. This
provides the tools that developers can use to avoid scenarios such as the one
mentioned in the referenced issue.

Fixes gh-6262
  • Loading branch information
Gabriel Schulhof committed Jan 21, 2014
1 parent 86c75da commit 6f97d29
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 21 deletions.
90 changes: 75 additions & 15 deletions js/events/touch.js
Expand Up @@ -30,10 +30,14 @@ define( [ "jquery", "../jquery.mobile.vmouse", "../jquery.mobile.support.touch"
}
});

function triggerCustomEvent( obj, eventType, event ) {
function triggerCustomEvent( obj, eventType, event, bubble ) {
var originalType = event.type;
event.type = eventType;
$.event.dispatch.call( obj, event );
if ( bubble ) {
$.event.trigger( event, undefined, obj );
} else {
$.event.dispatch.call( obj, event );
}
event.type = originalType;
}

Expand Down Expand Up @@ -170,49 +174,105 @@ define( [ "jquery", "../jquery.mobile.vmouse", "../jquery.mobile.support.touch"
Math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticalDistanceThreshold ) {
var direction = start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight";

triggerCustomEvent( thisObject, "swipe", $.Event( "swipe", { target: origTarget, swipestart: start, swipestop: stop }) );
triggerCustomEvent( thisObject, direction,$.Event( direction, { target: origTarget, swipestart: start, swipestop: stop } ) );
triggerCustomEvent( thisObject, "swipe", $.Event( "swipe", { target: origTarget, swipestart: start, swipestop: stop }), true );
triggerCustomEvent( thisObject, direction,$.Event( direction, { target: origTarget, swipestart: start, swipestop: stop } ), true );
return true;
}
return false;

},

// This serves as a flag to ensure that at most one swipe event event is
// in work at any given time
eventInProgress: false,

setup: function() {
var thisObject = this,
$this = $( thisObject );
var events,
thisObject = this,
$this = $( thisObject ),
context = {};

// Retrieve the events data for this element and add the swipe context
events = $.data( this, "mobile-events" );
if ( !events ) {
events = { length: 0 };
$.data( this, "mobile-events", events );
}
events.length++;
events.swipe = context;

context.start = function( event ) {

// Bail if we're already working on a swipe event
if ( $.event.special.swipe.eventInProgress ) {
return;
}
$.event.special.swipe.eventInProgress = true;

$this.bind( touchStartEvent, function( event ) {
var stop,
start = $.event.special.swipe.start( event ),
origTarget = event.target,
emitted = false;

function moveHandler( event ) {
context.move = function( event ) {
if ( !start ) {
return;
}

stop = $.event.special.swipe.stop( event );
if ( !emitted ) {
emitted = $.event.special.swipe.handleSwipe( start, stop, thisObject, origTarget );
if ( emitted ) {

// Reset the context to make way for the next swipe event
$.event.special.swipe.eventInProgress = false;
}
}
// prevent scrolling
if ( Math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollSupressionThreshold ) {
event.preventDefault();
}
}
};

$this.bind( touchMoveEvent, moveHandler )
.one( touchStopEvent, function() {
context.stop = function() {
emitted = true;
$this.unbind( touchMoveEvent, moveHandler );
});
});

// Reset the context to make way for the next swipe event
$.event.special.swipe.eventInProgress = false;
$document.off( touchMoveEvent, context.move );
context.move = null;
};

$document.on( touchMoveEvent, context.move )
.one( touchStopEvent, context.stop );
};
$this.on( touchStartEvent, context.start );
},

teardown: function() {
$( this ).unbind( touchStartEvent ).unbind( touchMoveEvent ).unbind( touchStopEvent );
var events, context;

events = $.data( this, "mobile-events" );
if ( events ) {
context = events.swipe;
delete events.swipe;
events.length--;
if ( events.length === 0 ) {
$.removeData( this, "mobile-events" );
}
}

if ( context ) {
if ( context.start ) {
$( this ).off( touchStartEvent, context.start );
}
if ( context.move ) {
$document.off( touchMoveEvent, context.move );
}
if ( context.stop ) {
$document.off( touchStopEvent, context.stop );
}
}
}
};
$.each({
Expand Down
77 changes: 72 additions & 5 deletions tests/unit/event/event_core.js
Expand Up @@ -6,6 +6,7 @@
var libName = "jquery.mobile.events.js",
components = [ "events/touch.js", "events/throttledresize.js", "events/orientationchange.js" ],
absFn = Math.abs,
originalPageContainer = $.mobile.pageContainer,
originalEventFn = $.Event.prototype.originalEvent,
preventDefaultFn = $.Event.prototype.preventDefault,
events = ("touchstart touchmove touchend tap taphold " +
Expand All @@ -28,7 +29,12 @@
// the collections existence in non touch enabled test browsers
$.Event.prototype.touches = [{pageX: 1, pageY: 1 }];

$.mobile.pageContainer = originalPageContainer || $( "body" );

$($.mobile.pageContainer).unbind( "throttledresize" );
},
teardown: function() {
$.mobile.pageContainer = originalPageContainer;
}
});

Expand Down Expand Up @@ -344,14 +350,60 @@
});

var swipeTimedTest = function(opts){
var swipe = false;
var newHandlerCount, origHandlerCount,
origHandleSwipe = $.event.special.swipe.handleSwipe,
handleSwipeAlwaysOnInner = true,
swipe = false,
bubble = false,
qunitFixture = $( "#qunit-fixture" ),
body = $( "body" ),
dummyFunction = function() {},
getHandlerCount = function( element ) {
var event, index,
eventNames = [ "touchstart", "touchmove", "touchend" ],
returnValue = {},
events = $._data( element, "events" );

for ( index in eventNames ) {
returnValue[ eventNames[ index ] ] = 0;
if ( events && events[ eventNames[ index ] ] ) {
returnValue[ eventNames[ index ] ] =
( events[ eventNames[ index ] ].length || 0 );
}
}

return returnValue;
};

forceTouchSupport();

$( "#qunit-fixture" ).bind('swipe', function(){
// Attach a dummy function to ensure that the swipe teardown leaves it attached
body.add( qunitFixture )
.on( "touchstart touchmove touchend", dummyFunction );

// Count handlers - this will include the function added above
origHandlerCount = {
body: getHandlerCount( body[ 0 ] ),
qunitFixture: getHandlerCount( qunitFixture[ 0 ] )
};

qunitFixture.one('swipe', function(){
swipe = true;
});

body.one( "swipe", function() {
bubble = true;
});

// Instrument method handleSwipe
$.event.special.swipe.handleSwipe =
function( start, stop, thisObject, origTarget ) {
if ( thisObject !== qunitFixture[ 0 ] ) {
handleSwipeAlwaysOnInner = false;
}
return origHandleSwipe.apply( this, arguments );
};

//NOTE bypass the trigger source check
$.Event.prototype.originalEvent = {
touches: [{
Expand All @@ -360,19 +412,34 @@
}]
};

$( "#qunit-fixture" ).trigger("touchstart");
qunitFixture.trigger("touchstart");

//NOTE make sure the coordinates are calculated within range
// to be registered as a swipe
mockAbs(opts.coordChange);

setTimeout(function(){
$( "#qunit-fixture" ).trigger("touchmove");
$( "#qunit-fixture" ).trigger("touchend");
qunitFixture.trigger("touchmove");
qunitFixture.trigger("touchend");
}, opts.timeout + 100);

setTimeout(function(){
deepEqual(swipe, opts.expected, "swipe expected");
deepEqual( bubble, opts.expected, "swipe bubbles when present" );
deepEqual( handleSwipeAlwaysOnInner, true, "handleSwipe is always called on the inner element" );

// Make sure swipe handlers are removed in case swipe never fired
qunitFixture.off( "swipe" );
$( "body" ).off( "swipe" );

deepEqual({
body: getHandlerCount( body[ 0 ] ),
qunitFixture: getHandlerCount( qunitFixture[ 0 ] )
}, origHandlerCount, "exactly the swipe-related event handlers are removed." );

// Remove dummy event handler
body.add( qunitFixture )
.off( "touchstart touchmove touchend", dummyFunction );
start();
}, opts.timeout + 200);

Expand Down
3 changes: 2 additions & 1 deletion tests/unit/event/index.html
Expand Up @@ -13,12 +13,13 @@
</script>

<script src="../../../js/jquery.mobile.define.js"></script>
<script src="../../../js/jquery.mobile.ns.js"></script>
<script src="../../../js/jquery.mobile.support.touch.js"></script>
<script src="../../../js/jquery.mobile.events.js"></script>
<script src="../../../js/jquery.mobile.vmouse.js"></script>
<script src="../../../js/events/touch.js"></script>
<script src="../../../js/events/throttledresize.js"></script>
<script src="../../../js/events/orientationchange.js"></script>
<script src="../../../js/"></script>

<link rel="stylesheet" href="../../../external/qunit/qunit.css"/>
<script src="../../../external/qunit/qunit.js"></script>
Expand Down

0 comments on commit 6f97d29

Please sign in to comment.