Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 5e0f876

Browse files
bshepherdsonIgorMinar
authored andcommitted
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.
1 parent f24cf4b commit 5e0f876

File tree

6 files changed

+295
-9
lines changed

6 files changed

+295
-9
lines changed

Gruntfile.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ module.exports = function(grunt) {
8989
dest: 'build/angular-mobile.js',
9090
src: util.wrap([
9191
'src/ngMobile/mobile.js',
92-
'src/ngMobile/directive/ngClick.js'
92+
'src/ngMobile/directive/ngClick.js',
93+
'src/ngMobile/directive/ngSwipe.js'
9394
], 'module')
9495
},
9596
mocks: {

angularFiles.js

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ angularFiles = {
7373
'src/ngMock/angular-mocks.js',
7474
'src/ngMobile/mobile.js',
7575
'src/ngMobile/directive/ngClick.js',
76+
'src/ngMobile/directive/ngSwipe.js',
77+
7678
'src/bootstrap/bootstrap.js'
7779
],
7880

@@ -151,6 +153,7 @@ angularFiles = {
151153
'src/ngResource/resource.js',
152154
'src/ngMobile/mobile.js',
153155
'src/ngMobile/directive/ngClick.js',
156+
'src/ngMobile/directive/ngSwipe.js',
154157
'src/ngSanitize/sanitize.js',
155158
'src/ngSanitize/directive/ngBindHtml.js',
156159
'src/ngSanitize/filter/linky.js',

src/ngMobile/directive/ngClick.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
163163

164164
// Actual linking function.
165165
return function(scope, element, attr) {
166-
var expressionFn = $parse(attr.ngClick),
166+
var clickHandler = $parse(attr.ngClick),
167167
tapping = false,
168168
tapElement, // Used to blur the element after a tap.
169169
startTime, // Used to check if the tap was held too long.
@@ -221,7 +221,7 @@ ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
221221

222222
scope.$apply(function() {
223223
// TODO(braden): This is sending the touchend, not a tap or click. Is that kosher?
224-
expressionFn(scope, {$event: event});
224+
clickHandler(scope, {$event: event});
225225
});
226226
}
227227
tapping = false;
@@ -236,7 +236,7 @@ ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
236236
// desktop as well, to allow more portable sites.
237237
element.bind('click', function(event) {
238238
scope.$apply(function() {
239-
expressionFn(scope, {$event: event});
239+
clickHandler(scope, {$event: event});
240240
});
241241
});
242242
};

src/ngMobile/directive/ngSwipe.js

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
'use strict';
2+
3+
/**
4+
* @ngdoc directive
5+
* @name ngMobile.directive:ngSwipeLeft
6+
*
7+
* @description
8+
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
9+
* A leftward swipe is a quick, right-to-left slide of the finger.
10+
* Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag too.
11+
*
12+
* @element ANY
13+
* @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate
14+
* upon left swipe. (Event object is available as `$event`)
15+
*
16+
* @example
17+
<doc:example>
18+
<doc:source>
19+
<div ng-show="!showActions" ng-swipe-left="showActions = true">
20+
Some list content, like an email in the inbox
21+
</div>
22+
<div ng-show="showActions" ng-swipe-right="showActions = false">
23+
<button ng-click="reply()">Reply</button>
24+
<button ng-click="delete()">Delete</button>
25+
</div>
26+
</doc:source>
27+
</doc:example>
28+
*/
29+
30+
/**
31+
* @ngdoc directive
32+
* @name ngMobile.directive:ngSwipeRight
33+
*
34+
* @description
35+
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
36+
* A rightward swipe is a quick, left-to-right slide of the finger.
37+
* Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag too.
38+
*
39+
* @element ANY
40+
* @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate
41+
* upon right swipe. (Event object is available as `$event`)
42+
*
43+
* @example
44+
<doc:example>
45+
<doc:source>
46+
<div ng-show="!showActions" ng-swipe-left="showActions = true">
47+
Some list content, like an email in the inbox
48+
</div>
49+
<div ng-show="showActions" ng-swipe-right="showActions = false">
50+
<button ng-click="reply()">Reply</button>
51+
<button ng-click="delete()">Delete</button>
52+
</div>
53+
</doc:source>
54+
</doc:example>
55+
*/
56+
57+
function makeSwipeDirective(directiveName, direction) {
58+
ngMobile.directive(directiveName, ['$parse', function($parse) {
59+
// The maximum vertical delta for a swipe should be less than 75px.
60+
var MAX_VERTICAL_DISTANCE = 75;
61+
// Vertical distance should not be more than a fraction of the horizontal distance.
62+
var MAX_VERTICAL_RATIO = 0.3;
63+
// At least a 30px lateral motion is necessary for a swipe.
64+
var MIN_HORIZONTAL_DISTANCE = 30;
65+
// The total distance in any direction before we make the call on swipe vs. scroll.
66+
var MOVE_BUFFER_RADIUS = 10;
67+
68+
function getCoordinates(event) {
69+
var touches = event.touches && event.touches.length ? event.touches : [event];
70+
var e = (event.changedTouches && event.changedTouches[0]) ||
71+
(event.originalEvent && event.originalEvent.changedTouches &&
72+
event.originalEvent.changedTouches[0]) ||
73+
touches[0].originalEvent || touches[0];
74+
75+
return {
76+
x: e.clientX,
77+
y: e.clientY
78+
};
79+
}
80+
81+
return function(scope, element, attr) {
82+
var swipeHandler = $parse(attr[directiveName]);
83+
var startCoords, valid;
84+
var totalX, totalY;
85+
var lastX, lastY;
86+
87+
function validSwipe(event) {
88+
// Check that it's within the coordinates.
89+
// Absolute vertical distance must be within tolerances.
90+
// Horizontal distance, we take the current X - the starting X.
91+
// This is negative for leftward swipes and positive for rightward swipes.
92+
// After multiplying by the direction (-1 for left, +1 for right), legal swipes
93+
// (ie. same direction as the directive wants) will have a positive delta and
94+
// illegal ones a negative delta.
95+
// Therefore this delta must be positive, and larger than the minimum.
96+
if (!startCoords) return false;
97+
var coords = getCoordinates(event);
98+
var deltaY = Math.abs(coords.y - startCoords.y);
99+
var deltaX = (coords.x - startCoords.x) * direction;
100+
return valid && // Short circuit for already-invalidated swipes.
101+
deltaY < MAX_VERTICAL_DISTANCE &&
102+
deltaX > 0 &&
103+
deltaX > MIN_HORIZONTAL_DISTANCE &&
104+
deltaY / deltaX < MAX_VERTICAL_RATIO;
105+
}
106+
107+
element.bind('touchstart mousedown', function(event) {
108+
startCoords = getCoordinates(event);
109+
valid = true;
110+
totalX = 0;
111+
totalY = 0;
112+
lastX = startCoords.x;
113+
lastY = startCoords.y;
114+
});
115+
116+
element.bind('touchcancel', function(event) {
117+
valid = false;
118+
});
119+
120+
element.bind('touchmove mousemove', function(event) {
121+
if (!valid) return;
122+
123+
// Android will send a touchcancel if it thinks we're starting to scroll.
124+
// So when the total distance (+ or - or both) exceeds 10px in either direction,
125+
// we either:
126+
// - On totalX > totalY, we send preventDefault() and treat this as a swipe.
127+
// - On totalY > totalX, we let the browser handle it as a scroll.
128+
129+
// Invalidate a touch while it's in progress if it strays too far away vertically.
130+
// We don't want a scroll down and back up while drifting sideways to be a swipe just
131+
// because you happened to end up vertically close in the end.
132+
if (!startCoords) return;
133+
var coords = getCoordinates(event);
134+
135+
if (Math.abs(coords.y - startCoords.y) > MAX_VERTICAL_DISTANCE) {
136+
valid = false;
137+
return;
138+
}
139+
140+
totalX += Math.abs(coords.x - lastX);
141+
totalY += Math.abs(coords.y - lastY);
142+
143+
lastX = coords.x;
144+
lastY = coords.y;
145+
146+
if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) {
147+
return;
148+
}
149+
150+
// One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll.
151+
if (totalY > totalX) {
152+
valid = false;
153+
return;
154+
} else {
155+
event.preventDefault();
156+
}
157+
});
158+
159+
element.bind('touchend mouseup', function(event) {
160+
if (validSwipe(event)) {
161+
// Prevent this swipe from bubbling up to any other elements with ngSwipes.
162+
event.stopPropagation();
163+
scope.$apply(function() {
164+
swipeHandler(scope, {$event:event});
165+
});
166+
}
167+
});
168+
};
169+
}]);
170+
}
171+
172+
// Left is negative X-coordinate, right is positive.
173+
makeSwipeDirective('ngSwipeLeft', -1);
174+
makeSwipeDirective('ngSwipeRight', 1);
175+

src/ngMobile/mobile.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
* @ngdoc overview
55
* @name ngMobile
66
* @description
7-
*/
8-
9-
/*
10-
* Touch events and other mobile helpers by Braden Shepherdson ([email protected])
7+
* Touch events and other mobile helpers.
118
* Based on jQuery Mobile touch event handling (jquerymobile.com)
129
*/
1310

14-
// define ngSanitize module and register $sanitize service
11+
// define ngMobile module
1512
var ngMobile = angular.module('ngMobile', []);
1613

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict';
2+
3+
// Wrapper to abstract over using touch events or mouse events.
4+
var swipeTests = function(description, restrictBrowsers, startEvent, moveEvent, endEvent) {
5+
describe('ngSwipe with ' + description + ' events', function() {
6+
var element;
7+
8+
if (restrictBrowsers) {
9+
// TODO(braden): Once we have other touch-friendly browsers on CI, allow them here.
10+
// Currently Firefox and IE refuse to fire touch events.
11+
var chrome = /chrome/.test(navigator.userAgent.toLowerCase());
12+
if (!chrome) {
13+
return;
14+
}
15+
}
16+
17+
// Skip tests on IE < 9. These versions of IE don't support createEvent(), and so
18+
// we cannot control the (x,y) position of events.
19+
// It works fine in IE 8 under manual testing.
20+
var msie = +((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1]);
21+
if (msie < 9) {
22+
return;
23+
}
24+
25+
beforeEach(function() {
26+
module('ngMobile');
27+
});
28+
29+
afterEach(function() {
30+
dealoc(element);
31+
});
32+
33+
it('should swipe to the left', inject(function($rootScope, $compile) {
34+
element = $compile('<div ng-swipe-left="swiped = true"></div>')($rootScope);
35+
$rootScope.$digest();
36+
expect($rootScope.swiped).toBeUndefined();
37+
38+
browserTrigger(element, startEvent, [], 100, 20);
39+
browserTrigger(element, endEvent, [], 20, 20);
40+
expect($rootScope.swiped).toBe(true);
41+
}));
42+
43+
it('should swipe to the right', inject(function($rootScope, $compile) {
44+
element = $compile('<div ng-swipe-right="swiped = true"></div>')($rootScope);
45+
$rootScope.$digest();
46+
expect($rootScope.swiped).toBeUndefined();
47+
48+
browserTrigger(element, startEvent, [], 20, 20);
49+
browserTrigger(element, endEvent, [], 90, 20);
50+
expect($rootScope.swiped).toBe(true);
51+
}));
52+
53+
it('should not swipe if you move too far vertically', inject(function($rootScope, $compile, $rootElement) {
54+
element = $compile('<div ng-swipe-left="swiped = true"></div>')($rootScope);
55+
$rootElement.append(element);
56+
$rootScope.$digest();
57+
58+
expect($rootScope.swiped).toBeUndefined();
59+
60+
browserTrigger(element, startEvent, [], 90, 20);
61+
browserTrigger(element, moveEvent, [], 70, 200);
62+
browserTrigger(element, endEvent, [], 20, 20);
63+
64+
expect($rootScope.swiped).toBeUndefined();
65+
}));
66+
67+
it('should not swipe if you slide only a short distance', inject(function($rootScope, $compile, $rootElement) {
68+
element = $compile('<div ng-swipe-left="swiped = true"></div>')($rootScope);
69+
$rootElement.append(element);
70+
$rootScope.$digest();
71+
72+
expect($rootScope.swiped).toBeUndefined();
73+
74+
browserTrigger(element, startEvent, [], 90, 20);
75+
browserTrigger(element, endEvent, [], 80, 20);
76+
77+
expect($rootScope.swiped).toBeUndefined();
78+
}));
79+
80+
it('should not swipe if the swipe leaves the element', inject(function($rootScope, $compile, $rootElement) {
81+
element = $compile('<div ng-swipe-right="swiped = true"></div>')($rootScope);
82+
$rootElement.append(element);
83+
$rootScope.$digest();
84+
85+
expect($rootScope.swiped).toBeUndefined();
86+
87+
browserTrigger(element, startEvent, [], 20, 20);
88+
browserTrigger(element, moveEvent, [], 40, 20);
89+
90+
expect($rootScope.swiped).toBeUndefined();
91+
}));
92+
93+
it('should not swipe if the swipe starts outside the element', inject(function($rootScope, $compile, $rootElement) {
94+
element = $compile('<div ng-swipe-right="swiped = true"></div>')($rootScope);
95+
$rootElement.append(element);
96+
$rootScope.$digest();
97+
98+
expect($rootScope.swiped).toBeUndefined();
99+
100+
browserTrigger(element, moveEvent, [], 10, 20);
101+
browserTrigger(element, endEvent, [], 90, 20);
102+
103+
expect($rootScope.swiped).toBeUndefined();
104+
}));
105+
});
106+
}
107+
108+
swipeTests('touch', true /* restrictBrowers */, 'touchstart', 'touchmove', 'touchend');
109+
swipeTests('mouse', false /* restrictBrowers */, 'mousedown', 'mousemove', 'mouseup');
110+

0 commit comments

Comments
 (0)