diff --git a/angularFiles.js b/angularFiles.js index 54bbd4fa1825..5abdbd95e892 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -106,6 +106,7 @@ var angularFiles = { 'src/ngTouch/touch.js', 'src/ngTouch/swipe.js', 'src/ngTouch/directive/ngClick.js', + 'src/ngTouch/directive/ngEventDirs.js', 'src/ngTouch/directive/ngSwipe.js' ], 'ngAria': [ diff --git a/src/ngTouch/directive/ngEventDirs.js b/src/ngTouch/directive/ngEventDirs.js new file mode 100644 index 000000000000..5648344205e9 --- /dev/null +++ b/src/ngTouch/directive/ngEventDirs.js @@ -0,0 +1,75 @@ +'use strict'; + +/* global ngTouch: false, POINTER_EVENTS: false, getPointerEventNames: false */ + +/* + * A collection of directives that allows creation of custom event handlers that are defined as + * angular expressions and are compiled and executed within the current scope. + */ +var ngTouchEventDirectives = {}; + + +// Duplicate from the ng module... +var forceAsyncEvents = { + 'blur': true, + 'focus': true +}; + +// Duplicated from jqLite. +var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; +var MOZ_HACK_REGEXP = /^moz([A-Z])/; +/** + * Converts snake_case to camelCase. + * Also there is special case for Moz prefix starting with upper case letter. + * @param name Name to normalize + */ +function camelCase(name) { + return name. + replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }). + replace(MOZ_HACK_REGEXP, 'Moz$1'); +} + +angular.forEach(Object.keys(POINTER_EVENTS.mouse), + function (eventType) { + + var eventName = POINTER_EVENTS.mouse[eventType]; + var directiveName = camelCase('ng-' + eventName); + + ngTouch.config(['$provide', function ($provide) { + $provide.decorator(directiveName + 'Directive', ['$delegate', function ($delegate) { + // drop the default mouse directives + $delegate.shift(); + return $delegate; + }]); + }]); + + ngTouchEventDirectives[directiveName] = ['$parse', '$rootScope', function ($parse, $rootScope) { + return { + restrict: 'A', + compile: function ($element, attr) { + var fn = $parse(attr[directiveName]); + return function ngEventHandler(scope, element) { + // + element.on(getPointerEventNames(eventType), function (event) { + var callback = function () { + fn(scope, {$event: event}); + }; + if (forceAsyncEvents[eventName] && $rootScope.$$phase) { + scope.$evalAsync(callback); + } else { + scope.$apply(callback); + } + }); + + }; + } + }; + }]; + } +); + +ngTouch.directive(ngTouchEventDirectives); + + diff --git a/src/ngTouch/swipe.js b/src/ngTouch/swipe.js index 884e0800d83c..c6e8f833e46f 100644 --- a/src/ngTouch/swipe.js +++ b/src/ngTouch/swipe.js @@ -2,6 +2,40 @@ /* global ngTouch: false */ +// The pointer event matching map +var POINTER_EVENTS = { + 'mouse': { + start: 'mousedown', + move: 'mousemove', + end: 'mouseup' + }, + 'touch': { + start: 'touchstart', + move: 'touchmove', + end: 'touchend', + cancel: 'touchcancel' + } +}; + +/** + * + * @param {string} eventType The event type + * @param {Array} [pointerTypes] The pointer type restrictions. By default mouse and touch pointers. + * @returns {string} The event names + */ +function getPointerEventNames(eventType, pointerTypes) { + pointerTypes = pointerTypes || Object.keys(POINTER_EVENTS); + var res = []; + angular.forEach(pointerTypes, function (pointerType) { + var eventName = POINTER_EVENTS[pointerType][eventType]; + if (eventName) { + res.push(eventName); + } + }); + return res.join(' '); +} + + /** * @ngdoc service * @name $swipe @@ -25,20 +59,6 @@ ngTouch.factory('$swipe', [function() { // The total distance in any direction before we make the call on swipe vs. scroll. var MOVE_BUFFER_RADIUS = 10; - var POINTER_EVENTS = { - 'mouse': { - start: 'mousedown', - move: 'mousemove', - end: 'mouseup' - }, - 'touch': { - start: 'touchstart', - move: 'touchmove', - end: 'touchend', - cancel: 'touchcancel' - } - }; - function getCoordinates(event) { var touches = event.touches && event.touches.length ? event.touches : [event]; var e = (event.changedTouches && event.changedTouches[0]) || @@ -52,17 +72,6 @@ ngTouch.factory('$swipe', [function() { }; } - function getEvents(pointerTypes, eventType) { - var res = []; - angular.forEach(pointerTypes, function(pointerType) { - var eventName = POINTER_EVENTS[pointerType][eventType]; - if (eventName) { - res.push(eventName); - } - }); - return res.join(' '); - } - return { /** * @ngdoc method @@ -106,8 +115,7 @@ ngTouch.factory('$swipe', [function() { // Whether a swipe is active. var active = false; - pointerTypes = pointerTypes || ['mouse', 'touch']; - element.on(getEvents(pointerTypes, 'start'), function(event) { + element.on(getPointerEventNames('start', pointerTypes), function(event) { startCoords = getCoordinates(event); active = true; totalX = 0; @@ -115,7 +123,7 @@ ngTouch.factory('$swipe', [function() { lastPos = startCoords; eventHandlers['start'] && eventHandlers['start'](startCoords, event); }); - var events = getEvents(pointerTypes, 'cancel'); + var events = getPointerEventNames('cancel', pointerTypes); if (events) { element.on(events, function(event) { active = false; @@ -123,7 +131,7 @@ ngTouch.factory('$swipe', [function() { }); } - element.on(getEvents(pointerTypes, 'move'), function(event) { + element.on(getPointerEventNames('move', pointerTypes), function(event) { if (!active) return; // Android will send a touchcancel if it thinks we're starting to scroll. @@ -157,7 +165,7 @@ ngTouch.factory('$swipe', [function() { } }); - element.on(getEvents(pointerTypes, 'end'), function(event) { + element.on(getPointerEventNames('end', pointerTypes), function(event) { if (!active) return; active = false; eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); diff --git a/test/ngTouch/directive/ngEventDirsSpec.js b/test/ngTouch/directive/ngEventDirsSpec.js new file mode 100644 index 000000000000..bec68f3a087c --- /dev/null +++ b/test/ngTouch/directive/ngEventDirsSpec.js @@ -0,0 +1,86 @@ +'use strict'; + +describe('ngMousedown (touch)', function() { + var element; + + beforeEach(function() { + module('ngTouch'); + }); + + afterEach(function() { + dealoc(element); + }); + + it('should pass event object on mousedown', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'mousedown'); + expect($rootScope.event).toBeDefined(); + })); + + it('should pass event object on touchstart too', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'touchstart'); + expect($rootScope.event).toBeDefined(); + })); +}); + + +describe('ngMousemove (touch)', function() { + var element; + + beforeEach(function() { + module('ngTouch'); + }); + + afterEach(function() { + dealoc(element); + }); + + it('should pass event object on mousemove', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'mousemove'); + expect($rootScope.event).toBeDefined(); + })); + + it('should pass event object on touchstart too', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'mousemove'); + expect($rootScope.event).toBeDefined(); + })); +}); + +describe('ngMouseup (touch)', function() { + var element; + + beforeEach(function() { + module('ngTouch'); + }); + + afterEach(function() { + dealoc(element); + }); + + it('should pass event object on mouseup', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'mouseup'); + expect($rootScope.event).toBeDefined(); + })); + + it('should pass event object on touchstart too', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'mouseup'); + expect($rootScope.event).toBeDefined(); + })); +});