This repository has been archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ngSwipe): Add ngSwipeRight/Left directives to ngMobile
These directives fire an event handler on a touch-and-drag or click-and-drag to the left or right. Includes unit tests and docs update. Manually tested on Chrome 26, IE8, Android Chrome and iOS Safari.
- Loading branch information
1 parent
f24cf4b
commit 5e0f876
Showing
6 changed files
with
295 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
'use strict'; | ||
|
||
/** | ||
* @ngdoc directive | ||
* @name ngMobile.directive:ngSwipeLeft | ||
* | ||
* @description | ||
* Specify custom behavior when an element is swiped to the left on a touchscreen device. | ||
* A leftward swipe is a quick, right-to-left slide of the finger. | ||
* Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag too. | ||
* | ||
* @element ANY | ||
* @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate | ||
* upon left swipe. (Event object is available as `$event`) | ||
* | ||
* @example | ||
<doc:example> | ||
<doc:source> | ||
<div ng-show="!showActions" ng-swipe-left="showActions = true"> | ||
Some list content, like an email in the inbox | ||
</div> | ||
<div ng-show="showActions" ng-swipe-right="showActions = false"> | ||
<button ng-click="reply()">Reply</button> | ||
<button ng-click="delete()">Delete</button> | ||
</div> | ||
</doc:source> | ||
</doc:example> | ||
*/ | ||
|
||
/** | ||
* @ngdoc directive | ||
* @name ngMobile.directive:ngSwipeRight | ||
* | ||
* @description | ||
* Specify custom behavior when an element is swiped to the right on a touchscreen device. | ||
* A rightward swipe is a quick, left-to-right slide of the finger. | ||
* Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag too. | ||
* | ||
* @element ANY | ||
* @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate | ||
* upon right swipe. (Event object is available as `$event`) | ||
* | ||
* @example | ||
<doc:example> | ||
<doc:source> | ||
<div ng-show="!showActions" ng-swipe-left="showActions = true"> | ||
Some list content, like an email in the inbox | ||
</div> | ||
<div ng-show="showActions" ng-swipe-right="showActions = false"> | ||
<button ng-click="reply()">Reply</button> | ||
<button ng-click="delete()">Delete</button> | ||
</div> | ||
</doc:source> | ||
</doc:example> | ||
*/ | ||
|
||
function makeSwipeDirective(directiveName, direction) { | ||
ngMobile.directive(directiveName, ['$parse', function($parse) { | ||
// The maximum vertical delta for a swipe should be less than 75px. | ||
var MAX_VERTICAL_DISTANCE = 75; | ||
// Vertical distance should not be more than a fraction of the horizontal distance. | ||
var MAX_VERTICAL_RATIO = 0.3; | ||
// At least a 30px lateral motion is necessary for a swipe. | ||
var MIN_HORIZONTAL_DISTANCE = 30; | ||
// The total distance in any direction before we make the call on swipe vs. scroll. | ||
var MOVE_BUFFER_RADIUS = 10; | ||
|
||
function getCoordinates(event) { | ||
var touches = event.touches && event.touches.length ? event.touches : [event]; | ||
var e = (event.changedTouches && event.changedTouches[0]) || | ||
(event.originalEvent && event.originalEvent.changedTouches && | ||
event.originalEvent.changedTouches[0]) || | ||
touches[0].originalEvent || touches[0]; | ||
|
||
return { | ||
x: e.clientX, | ||
y: e.clientY | ||
}; | ||
} | ||
|
||
return function(scope, element, attr) { | ||
var swipeHandler = $parse(attr[directiveName]); | ||
var startCoords, valid; | ||
var totalX, totalY; | ||
var lastX, lastY; | ||
|
||
function validSwipe(event) { | ||
// Check that it's within the coordinates. | ||
// Absolute vertical distance must be within tolerances. | ||
// Horizontal distance, we take the current X - the starting X. | ||
// This is negative for leftward swipes and positive for rightward swipes. | ||
// After multiplying by the direction (-1 for left, +1 for right), legal swipes | ||
// (ie. same direction as the directive wants) will have a positive delta and | ||
// illegal ones a negative delta. | ||
// Therefore this delta must be positive, and larger than the minimum. | ||
if (!startCoords) return false; | ||
var coords = getCoordinates(event); | ||
var deltaY = Math.abs(coords.y - startCoords.y); | ||
var deltaX = (coords.x - startCoords.x) * direction; | ||
return valid && // Short circuit for already-invalidated swipes. | ||
deltaY < MAX_VERTICAL_DISTANCE && | ||
deltaX > 0 && | ||
deltaX > MIN_HORIZONTAL_DISTANCE && | ||
deltaY / deltaX < MAX_VERTICAL_RATIO; | ||
} | ||
|
||
element.bind('touchstart mousedown', function(event) { | ||
startCoords = getCoordinates(event); | ||
valid = true; | ||
totalX = 0; | ||
totalY = 0; | ||
lastX = startCoords.x; | ||
lastY = startCoords.y; | ||
}); | ||
|
||
element.bind('touchcancel', function(event) { | ||
valid = false; | ||
}); | ||
|
||
element.bind('touchmove mousemove', function(event) { | ||
if (!valid) return; | ||
|
||
// Android will send a touchcancel if it thinks we're starting to scroll. | ||
// So when the total distance (+ or - or both) exceeds 10px in either direction, | ||
// we either: | ||
// - On totalX > totalY, we send preventDefault() and treat this as a swipe. | ||
// - On totalY > totalX, we let the browser handle it as a scroll. | ||
|
||
// Invalidate a touch while it's in progress if it strays too far away vertically. | ||
// We don't want a scroll down and back up while drifting sideways to be a swipe just | ||
// because you happened to end up vertically close in the end. | ||
if (!startCoords) return; | ||
var coords = getCoordinates(event); | ||
|
||
if (Math.abs(coords.y - startCoords.y) > MAX_VERTICAL_DISTANCE) { | ||
valid = false; | ||
return; | ||
} | ||
|
||
totalX += Math.abs(coords.x - lastX); | ||
totalY += Math.abs(coords.y - lastY); | ||
|
||
lastX = coords.x; | ||
lastY = coords.y; | ||
|
||
if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) { | ||
return; | ||
} | ||
|
||
// One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll. | ||
if (totalY > totalX) { | ||
valid = false; | ||
return; | ||
} else { | ||
event.preventDefault(); | ||
} | ||
}); | ||
|
||
element.bind('touchend mouseup', function(event) { | ||
if (validSwipe(event)) { | ||
// Prevent this swipe from bubbling up to any other elements with ngSwipes. | ||
event.stopPropagation(); | ||
scope.$apply(function() { | ||
swipeHandler(scope, {$event:event}); | ||
}); | ||
} | ||
}); | ||
}; | ||
}]); | ||
} | ||
|
||
// Left is negative X-coordinate, right is positive. | ||
makeSwipeDirective('ngSwipeLeft', -1); | ||
makeSwipeDirective('ngSwipeRight', 1); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,10 @@ | |
* @ngdoc overview | ||
* @name ngMobile | ||
* @description | ||
*/ | ||
|
||
/* | ||
* Touch events and other mobile helpers by Braden Shepherdson ([email protected]) | ||
* Touch events and other mobile helpers. | ||
* Based on jQuery Mobile touch event handling (jquerymobile.com) | ||
*/ | ||
|
||
// define ngSanitize module and register $sanitize service | ||
// define ngMobile module | ||
var ngMobile = angular.module('ngMobile', []); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
'use strict'; | ||
|
||
// Wrapper to abstract over using touch events or mouse events. | ||
var swipeTests = function(description, restrictBrowsers, startEvent, moveEvent, endEvent) { | ||
describe('ngSwipe with ' + description + ' events', function() { | ||
var element; | ||
|
||
if (restrictBrowsers) { | ||
// TODO(braden): Once we have other touch-friendly browsers on CI, allow them here. | ||
// Currently Firefox and IE refuse to fire touch events. | ||
var chrome = /chrome/.test(navigator.userAgent.toLowerCase()); | ||
if (!chrome) { | ||
return; | ||
} | ||
} | ||
|
||
// Skip tests on IE < 9. These versions of IE don't support createEvent(), and so | ||
// we cannot control the (x,y) position of events. | ||
// It works fine in IE 8 under manual testing. | ||
var msie = +((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1]); | ||
if (msie < 9) { | ||
return; | ||
} | ||
|
||
beforeEach(function() { | ||
module('ngMobile'); | ||
}); | ||
|
||
afterEach(function() { | ||
dealoc(element); | ||
}); | ||
|
||
it('should swipe to the left', inject(function($rootScope, $compile) { | ||
element = $compile('<div ng-swipe-left="swiped = true"></div>')($rootScope); | ||
$rootScope.$digest(); | ||
expect($rootScope.swiped).toBeUndefined(); | ||
|
||
browserTrigger(element, startEvent, [], 100, 20); | ||
browserTrigger(element, endEvent, [], 20, 20); | ||
expect($rootScope.swiped).toBe(true); | ||
})); | ||
|
||
it('should swipe to the right', inject(function($rootScope, $compile) { | ||
element = $compile('<div ng-swipe-right="swiped = true"></div>')($rootScope); | ||
$rootScope.$digest(); | ||
expect($rootScope.swiped).toBeUndefined(); | ||
|
||
browserTrigger(element, startEvent, [], 20, 20); | ||
browserTrigger(element, endEvent, [], 90, 20); | ||
expect($rootScope.swiped).toBe(true); | ||
})); | ||
|
||
it('should not swipe if you move too far vertically', inject(function($rootScope, $compile, $rootElement) { | ||
element = $compile('<div ng-swipe-left="swiped = true"></div>')($rootScope); | ||
$rootElement.append(element); | ||
$rootScope.$digest(); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
|
||
browserTrigger(element, startEvent, [], 90, 20); | ||
browserTrigger(element, moveEvent, [], 70, 200); | ||
browserTrigger(element, endEvent, [], 20, 20); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
})); | ||
|
||
it('should not swipe if you slide only a short distance', inject(function($rootScope, $compile, $rootElement) { | ||
element = $compile('<div ng-swipe-left="swiped = true"></div>')($rootScope); | ||
$rootElement.append(element); | ||
$rootScope.$digest(); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
|
||
browserTrigger(element, startEvent, [], 90, 20); | ||
browserTrigger(element, endEvent, [], 80, 20); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
})); | ||
|
||
it('should not swipe if the swipe leaves the element', inject(function($rootScope, $compile, $rootElement) { | ||
element = $compile('<div ng-swipe-right="swiped = true"></div>')($rootScope); | ||
$rootElement.append(element); | ||
$rootScope.$digest(); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
|
||
browserTrigger(element, startEvent, [], 20, 20); | ||
browserTrigger(element, moveEvent, [], 40, 20); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
})); | ||
|
||
it('should not swipe if the swipe starts outside the element', inject(function($rootScope, $compile, $rootElement) { | ||
element = $compile('<div ng-swipe-right="swiped = true"></div>')($rootScope); | ||
$rootElement.append(element); | ||
$rootScope.$digest(); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
|
||
browserTrigger(element, moveEvent, [], 10, 20); | ||
browserTrigger(element, endEvent, [], 90, 20); | ||
|
||
expect($rootScope.swiped).toBeUndefined(); | ||
})); | ||
}); | ||
} | ||
|
||
swipeTests('touch', true /* restrictBrowers */, 'touchstart', 'touchmove', 'touchend'); | ||
swipeTests('mouse', false /* restrictBrowers */, 'mousedown', 'mousemove', 'mouseup'); | ||
|