diff options
| -rw-r--r-- | Gruntfile.js | 3 | ||||
| -rw-r--r-- | angularFiles.js | 3 | ||||
| -rw-r--r-- | src/ngMobile/directive/ngClick.js | 6 | ||||
| -rw-r--r-- | src/ngMobile/directive/ngSwipe.js | 175 | ||||
| -rw-r--r-- | src/ngMobile/mobile.js | 7 | ||||
| -rw-r--r-- | test/ngMobile/directive/ngSwipeSpec.js | 110 | 
6 files changed, 295 insertions, 9 deletions
| diff --git a/Gruntfile.js b/Gruntfile.js index c318a2a6..ac7e47b6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -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: { diff --git a/angularFiles.js b/angularFiles.js index 8384beb9..b2c23a8f 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -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', diff --git a/src/ngMobile/directive/ngClick.js b/src/ngMobile/directive/ngClick.js index a52893b1..b3e3007d 100644 --- a/src/ngMobile/directive/ngClick.js +++ b/src/ngMobile/directive/ngClick.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});        });      });    }; diff --git a/src/ngMobile/directive/ngSwipe.js b/src/ngMobile/directive/ngSwipe.js new file mode 100644 index 00000000..6de7aa57 --- /dev/null +++ b/src/ngMobile/directive/ngSwipe.js @@ -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); + diff --git a/src/ngMobile/mobile.js b/src/ngMobile/mobile.js index c8fd8843..daa28f5b 100644 --- a/src/ngMobile/mobile.js +++ b/src/ngMobile/mobile.js @@ -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', []); diff --git a/test/ngMobile/directive/ngSwipeSpec.js b/test/ngMobile/directive/ngSwipeSpec.js new file mode 100644 index 00000000..6bc7d300 --- /dev/null +++ b/test/ngMobile/directive/ngSwipeSpec.js @@ -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'); + | 
