mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-04-17 05:11:03 +00:00
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.
This commit is contained in:
parent
f24cf4b58f
commit
5e0f876c39
6 changed files with 295 additions and 9 deletions
|
|
@ -89,7 +89,8 @@ module.exports = function(grunt) {
|
|||
dest: 'build/angular-mobile.js',
|
||||
src: util.wrap([
|
||||
'src/ngMobile/mobile.js',
|
||||
'src/ngMobile/directive/ngClick.js'
|
||||
'src/ngMobile/directive/ngClick.js',
|
||||
'src/ngMobile/directive/ngSwipe.js'
|
||||
], 'module')
|
||||
},
|
||||
mocks: {
|
||||
|
|
|
|||
3
angularFiles.js
vendored
3
angularFiles.js
vendored
|
|
@ -73,6 +73,8 @@ angularFiles = {
|
|||
'src/ngMock/angular-mocks.js',
|
||||
'src/ngMobile/mobile.js',
|
||||
'src/ngMobile/directive/ngClick.js',
|
||||
'src/ngMobile/directive/ngSwipe.js',
|
||||
|
||||
'src/bootstrap/bootstrap.js'
|
||||
],
|
||||
|
||||
|
|
@ -151,6 +153,7 @@ angularFiles = {
|
|||
'src/ngResource/resource.js',
|
||||
'src/ngMobile/mobile.js',
|
||||
'src/ngMobile/directive/ngClick.js',
|
||||
'src/ngMobile/directive/ngSwipe.js',
|
||||
'src/ngSanitize/sanitize.js',
|
||||
'src/ngSanitize/directive/ngBindHtml.js',
|
||||
'src/ngSanitize/filter/linky.js',
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
|
|||
|
||||
// Actual linking function.
|
||||
return function(scope, element, attr) {
|
||||
var expressionFn = $parse(attr.ngClick),
|
||||
var clickHandler = $parse(attr.ngClick),
|
||||
tapping = false,
|
||||
tapElement, // Used to blur the element after a tap.
|
||||
startTime, // Used to check if the tap was held too long.
|
||||
|
|
@ -221,7 +221,7 @@ ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
|
|||
|
||||
scope.$apply(function() {
|
||||
// TODO(braden): This is sending the touchend, not a tap or click. Is that kosher?
|
||||
expressionFn(scope, {$event: event});
|
||||
clickHandler(scope, {$event: event});
|
||||
});
|
||||
}
|
||||
tapping = false;
|
||||
|
|
@ -236,7 +236,7 @@ ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
|
|||
// desktop as well, to allow more portable sites.
|
||||
element.bind('click', function(event) {
|
||||
scope.$apply(function() {
|
||||
expressionFn(scope, {$event: event});
|
||||
clickHandler(scope, {$event: event});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
|||
175
src/ngMobile/directive/ngSwipe.js
Normal file
175
src/ngMobile/directive/ngSwipe.js
Normal file
|
|
@ -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);
|
||||
|
||||
|
|
@ -4,13 +4,10 @@
|
|||
* @ngdoc overview
|
||||
* @name ngMobile
|
||||
* @description
|
||||
*/
|
||||
|
||||
/*
|
||||
* Touch events and other mobile helpers by Braden Shepherdson (braden.shepherdson@gmail.com)
|
||||
* 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', []);
|
||||
|
||||
|
|
|
|||
110
test/ngMobile/directive/ngSwipeSpec.js
Normal file
110
test/ngMobile/directive/ngSwipeSpec.js
Normal file
|
|
@ -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');
|
||||
|
||||
Loading…
Reference in a new issue