+ * **DEPRECATION NOTICE**: Beginning with Angular 1.5, this directive is deprecated and by default **disabled**.
+ * The directive will receive no further support and might be removed from future releases.
+ * If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled}
+ * function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick).
+ * To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/)
+ * gives a good overview.
+ *
* A more powerful replacement for the default ngClick designed to be used on touchscreen
* devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
* the click event. This version handles them immediately, and then prevents the
@@ -40,15 +49,7 @@
*/
-ngTouch.config(['$provide', function($provide) {
- $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
- // drop the default ngClick directive
- $delegate.shift();
- return $delegate;
- }]);
-}]);
-
-ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
+var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
function($parse, $timeout, $rootElement) {
var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
@@ -292,5 +293,5 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
});
};
-}]);
+}];
diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js
index 8191ff0b753a..09966953884d 100644
--- a/src/ngTouch/touch.js
+++ b/src/ngTouch/touch.js
@@ -1,5 +1,8 @@
'use strict';
+/* global ngTouchClickDirectiveFactory: false,
+ */
+
/**
* @ngdoc module
* @name ngTouch
@@ -22,6 +25,104 @@
/* global -ngTouch */
var ngTouch = angular.module('ngTouch', []);
+ngTouch.provider('$touch', $TouchProvider);
+
function nodeName_(element) {
return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName));
}
+
+/**
+ * @ngdoc provider
+ * @name $touchProvider
+ *
+ * @description
+ * The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}.
+ */
+$TouchProvider.$inject = ['$provide', '$compileProvider'];
+function $TouchProvider($provide, $compileProvider) {
+
+ /**
+ * @ngdoc method
+ * @name $touchProvider#ngClickOverrideEnabled
+ *
+ * @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the
+ * current ngClickOverrideEnabled state
+ * @returns {*} current value if used as getter or itself (chaining) if used as setter
+ *
+ * @kind function
+ *
+ * @description
+ * Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled,
+ * the default ngClick directive will be replaced by a version that eliminates the 300ms delay for
+ * click events on browser for touch-devices.
+ *
+ * The default is `false`.
+ *
+ */
+ var ngClickOverrideEnabled = false;
+ var ngClickDirectiveAdded = false;
+ this.ngClickOverrideEnabled = function(enabled) {
+ if (angular.isDefined(enabled)) {
+
+ if (enabled && !ngClickDirectiveAdded) {
+ ngClickDirectiveAdded = true;
+
+ // Use this to identify the correct directive in the delegate
+ ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch';
+ $compileProvider.directive('ngClick', ngTouchClickDirectiveFactory);
+
+ $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
+ if (ngClickOverrideEnabled) {
+ // drop the default ngClick directive
+ $delegate.shift();
+ } else {
+ // drop the ngTouch ngClick directive if the override has been re-disabled (because
+ // we cannot de-register added directives)
+ var i = $delegate.length - 1;
+ while (i >= 0) {
+ if ($delegate[i].$$moduleName === 'ngTouch') {
+ $delegate.splice(i, 1);
+ break;
+ }
+ i--;
+ }
+ }
+
+ return $delegate;
+ }]);
+ }
+
+ ngClickOverrideEnabled = enabled;
+ return this;
+ }
+
+ return ngClickOverrideEnabled;
+ };
+
+ /**
+ * @ngdoc service
+ * @name $touch
+ * @kind object
+ *
+ * @description
+ * Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method.
+ *
+ */
+ this.$get = function() {
+ return {
+ /**
+ * @ngdoc method
+ * @name $touch#ngClickOverrideEnabled
+ *
+ * @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider},
+ * i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled.
+ *
+ * @kind function
+ */
+ ngClickOverrideEnabled: function() {
+ return ngClickOverrideEnabled;
+ }
+ };
+ };
+
+}
diff --git a/test/ngTouch/directive/ngClickSpec.js b/test/ngTouch/directive/ngClickSpec.js
index c9765b4c8df9..dc453cd83345 100644
--- a/test/ngTouch/directive/ngClickSpec.js
+++ b/test/ngTouch/directive/ngClickSpec.js
@@ -15,197 +15,170 @@ describe('ngClick (touch)', function() {
}
- beforeEach(function() {
- module('ngTouch');
- orig_now = Date.now;
- time = 0;
- Date.now = mockTime;
- });
-
- afterEach(function() {
- dealoc(element);
- Date.now = orig_now;
- });
+ describe('config', function() {
+ beforeEach(module('ngTouch'));
+ it('should expose ngClickOverrideEnabled in the $touchProvider', function() {
+ var _$touchProvider;
- it('should get called on a tap', inject(function($rootScope, $compile) {
- element = $compile('')($rootScope);
- $rootScope.$digest();
- expect($rootScope.tapped).toBeUndefined();
-
- browserTrigger(element, 'touchstart');
- browserTrigger(element, 'touchend');
- expect($rootScope.tapped).toEqual(true);
- }));
-
+ module(function($touchProvider) {
+ _$touchProvider = $touchProvider;
+ });
- it('should pass event object', inject(function($rootScope, $compile) {
- element = $compile('')($rootScope);
- $rootScope.$digest();
+ inject(function() {
+ expect(_$touchProvider.ngClickOverrideEnabled).toEqual(jasmine.any(Function));
+ });
+ });
- browserTrigger(element, 'touchstart');
- browserTrigger(element, 'touchend');
- expect($rootScope.event).toBeDefined();
- }));
- if (window.jQuery) {
- it('should not unwrap a jQuery-wrapped event object on click', inject(function($rootScope, $compile) {
- element = $compile('')($rootScope);
- $rootScope.$digest();
+ it('should return "false" for ngClickOverrideEnabled by default', function() {
+ var enabled;
- browserTrigger(element, 'click', {
- keys: [],
- x: 10,
- y: 10
+ module(function($touchProvider) {
+ enabled = $touchProvider.ngClickOverrideEnabled();
});
- expect($rootScope.event.originalEvent).toBeDefined();
- expect($rootScope.event.originalEvent.clientX).toBe(10);
- expect($rootScope.event.originalEvent.clientY).toBe(10);
- }));
- it('should not unwrap a jQuery-wrapped event object on touchstart/touchend',
- inject(function($rootScope, $compile, $rootElement) {
- element = $compile('')($rootScope);
- $rootElement.append(element);
- $rootScope.$digest();
+ inject(function() {
+ expect(enabled).toBe(false);
+ });
+ });
- browserTrigger(element, 'touchstart');
- browserTrigger(element, 'touchend');
- expect($rootScope.event.originalEvent).toBeDefined();
- }));
- }
+ it('should not apply the ngClick override directive by default', function() {
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect($rootScope.tapped).toBeUndefined();
+ browserTrigger(element, 'touchstart');
+ browserTrigger(element, 'touchend');
+ expect($rootScope.tapped).toBeUndefined();
+ });
+ });
+ });
- it('should not click if the touch is held too long', inject(function($rootScope, $compile, $rootElement) {
- element = $compile('')($rootScope);
- $rootElement.append(element);
- $rootScope.count = 0;
- $rootScope.$digest();
+ describe('interaction with custom ngClick directives', function() {
- expect($rootScope.count).toBe(0);
+ it('should not remove other ngClick directives when removing ngTouch ngClick in the decorator', function() {
+ // Add another ngClick before ngTouch
+ module(function($compileProvider) {
+ $compileProvider.directive('ngClick', function() {
+ return {};
+ });
+ });
- time = 10;
- browserTrigger(element, 'touchstart',{
- keys: [],
- x: 10,
- y: 10
- });
+ module('ngTouch');
- time = 900;
- browserTrigger(element, 'touchend',{
- keys: [],
- x: 10,
- y: 10
- });
+ module(function($touchProvider) {
+ $touchProvider.ngClickOverrideEnabled(true);
+ $touchProvider.ngClickOverrideEnabled(false);
+ });
- expect($rootScope.count).toBe(0);
- }));
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect($rootScope.tapped).toBeUndefined();
+ browserTrigger(element, 'touchstart');
+ browserTrigger(element, 'touchend');
+ expect($rootScope.tapped).toBeUndefined();
+ });
+ });
- it('should not click if the touchend is too far away', inject(function($rootScope, $compile, $rootElement) {
- element = $compile('')($rootScope);
- $rootElement.append(element);
- $rootScope.$digest();
+ });
- expect($rootScope.tapped).toBeUndefined();
+ describe('directive', function() {
- browserTrigger(element, 'touchstart',{
- keys: [],
- x: 10,
- y: 10
- });
- browserTrigger(element, 'touchend',{
- keys: [],
- x: 400,
- y: 400
+ beforeEach(function() {
+ module('ngTouch');
+ module(function($touchProvider) {
+ $touchProvider.ngClickOverrideEnabled(true);
+ });
+ orig_now = Date.now;
+ time = 0;
+ Date.now = mockTime;
});
- expect($rootScope.tapped).toBeUndefined();
- }));
-
+ afterEach(function() {
+ dealoc(element);
+ Date.now = orig_now;
+ });
- it('should not prevent click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) {
- element = $compile('')($rootScope);
- $rootElement.append(element);
- $rootScope.$digest();
+ it('should not apply the ngClick override directive if ngClickOverrideEnabled has been set to false again', function() {
+ module(function($touchProvider) {
+ // beforeEach calls this with "true"
+ $touchProvider.ngClickOverrideEnabled(false);
+ });
- expect($rootScope.tapped).toBeUndefined();
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect($rootScope.tapped).toBeUndefined();
- browserTrigger(element, 'touchstart',{
- keys: [],
- x: 10,
- y: 10
- });
- browserTrigger(element, 'touchmove');
- browserTrigger(element, 'touchend',{
- keys: [],
- x: 15,
- y: 15
+ browserTrigger(element, 'touchstart');
+ browserTrigger(element, 'touchend');
+ expect($rootScope.tapped).toBeUndefined();
+ });
});
- expect($rootScope.tapped).toEqual(true);
- }));
- it('should add the CSS class while the element is held down, and then remove it', inject(function($rootScope, $compile, $rootElement) {
- element = $compile('')($rootScope);
- $rootElement.append(element);
- $rootScope.$digest();
- expect($rootScope.tapped).toBeUndefined();
+ it('should get called on a tap', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect($rootScope.tapped).toBeUndefined();
- var CSS_CLASS = 'ng-click-active';
+ browserTrigger(element, 'touchstart');
+ browserTrigger(element, 'touchend');
+ expect($rootScope.tapped).toEqual(true);
+ }));
- expect(element.hasClass(CSS_CLASS)).toBe(false);
- browserTrigger(element, 'touchstart',{
- keys: [],
- x: 10,
- y: 10
- });
- expect(element.hasClass(CSS_CLASS)).toBe(true);
- browserTrigger(element, 'touchend',{
- keys: [],
- x: 10,
- y: 10
- });
- expect(element.hasClass(CSS_CLASS)).toBe(false);
- expect($rootScope.tapped).toBe(true);
- }));
- it('should click when target element is an SVG', inject(
- function($rootScope, $compile, $rootElement) {
- element = $compile('')($rootScope);
- $rootElement.append(element);
+ it('should pass event object', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
$rootScope.$digest();
browserTrigger(element, 'touchstart');
browserTrigger(element, 'touchend');
- browserTrigger(element, 'click', {x:1, y:1});
+ expect($rootScope.event).toBeDefined();
+ }));
- expect($rootScope.tapped).toEqual(true);
- }));
+ if (window.jQuery) {
+ it('should not unwrap a jQuery-wrapped event object on click', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
- describe('the clickbuster', function() {
- var element1, element2;
+ browserTrigger(element, 'click', {
+ keys: [],
+ x: 10,
+ y: 10
+ });
+ expect($rootScope.event.originalEvent).toBeDefined();
+ expect($rootScope.event.originalEvent.clientX).toBe(10);
+ expect($rootScope.event.originalEvent.clientY).toBe(10);
+ }));
- beforeEach(inject(function($rootElement, $document) {
- $document.find('body').append($rootElement);
- }));
+ it('should not unwrap a jQuery-wrapped event object on touchstart/touchend',
+ inject(function($rootScope, $compile, $rootElement) {
+ element = $compile('')($rootScope);
+ $rootElement.append(element);
+ $rootScope.$digest();
- afterEach(inject(function($document) {
- $document.find('body').empty();
- }));
+ browserTrigger(element, 'touchstart');
+ browserTrigger(element, 'touchend');
+
+ expect($rootScope.event.originalEvent).toBeDefined();
+ }));
+ }
- it('should cancel the following click event', inject(function($rootScope, $compile, $rootElement, $document) {
+ it('should not click if the touch is held too long', inject(function($rootScope, $compile, $rootElement) {
element = $compile('')($rootScope);
$rootElement.append(element);
-
$rootScope.count = 0;
$rootScope.$digest();
expect($rootScope.count).toBe(0);
- // Fire touchstart at 10ms, touchend at 50ms, the click at 300ms.
time = 10;
browserTrigger(element, 'touchstart',{
keys: [],
@@ -213,443 +186,565 @@ describe('ngClick (touch)', function() {
y: 10
});
- time = 50;
+ time = 900;
browserTrigger(element, 'touchend',{
keys: [],
x: 10,
y: 10
});
- expect($rootScope.count).toBe(1);
+ expect($rootScope.count).toBe(0);
+ }));
+
+
+ it('should not click if the touchend is too far away', inject(function($rootScope, $compile, $rootElement) {
+ element = $compile('')($rootScope);
+ $rootElement.append(element);
+ $rootScope.$digest();
+
+ expect($rootScope.tapped).toBeUndefined();
- time = 100;
- browserTrigger(element, 'click',{
+ browserTrigger(element, 'touchstart',{
keys: [],
x: 10,
y: 10
});
+ browserTrigger(element, 'touchend',{
+ keys: [],
+ x: 400,
+ y: 400
+ });
- expect($rootScope.count).toBe(1);
+ expect($rootScope.tapped).toBeUndefined();
}));
- it('should cancel the following click event even when the element has changed', inject(
- function($rootScope, $compile, $rootElement) {
- $rootElement.append(
- '
x
' +
- '
y
'
- );
- $compile($rootElement)($rootScope);
-
- element1 = $rootElement.find('div').eq(0);
- element2 = $rootElement.find('div').eq(1);
-
- $rootScope.count1 = 0;
- $rootScope.count2 = 0;
-
+ it('should not prevent click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) {
+ element = $compile('')($rootScope);
+ $rootElement.append(element);
$rootScope.$digest();
- expect($rootScope.count1).toBe(0);
- expect($rootScope.count2).toBe(0);
+ expect($rootScope.tapped).toBeUndefined();
- time = 10;
- browserTrigger(element1, 'touchstart',{
+ browserTrigger(element, 'touchstart',{
keys: [],
x: 10,
y: 10
});
+ browserTrigger(element, 'touchmove');
+ browserTrigger(element, 'touchend',{
+ keys: [],
+ x: 15,
+ y: 15
+ });
+
+ expect($rootScope.tapped).toEqual(true);
+ }));
+
+ it('should add the CSS class while the element is held down, and then remove it', inject(function($rootScope, $compile, $rootElement) {
+ element = $compile('')($rootScope);
+ $rootElement.append(element);
+ $rootScope.$digest();
+ expect($rootScope.tapped).toBeUndefined();
+
+ var CSS_CLASS = 'ng-click-active';
- time = 50;
- browserTrigger(element1, 'touchend',{
+ expect(element.hasClass(CSS_CLASS)).toBe(false);
+ browserTrigger(element, 'touchstart',{
keys: [],
x: 10,
y: 10
});
-
- expect($rootScope.count1).toBe(1);
-
- time = 100;
- browserTrigger(element2, 'click',{
+ expect(element.hasClass(CSS_CLASS)).toBe(true);
+ browserTrigger(element, 'touchend',{
keys: [],
x: 10,
y: 10
});
+ expect(element.hasClass(CSS_CLASS)).toBe(false);
+ expect($rootScope.tapped).toBe(true);
+ }));
- expect($rootScope.count1).toBe(1);
- expect($rootScope.count2).toBe(0);
+ it('should click when target element is an SVG', inject(
+ function($rootScope, $compile, $rootElement) {
+ element = $compile('')($rootScope);
+ $rootElement.append(element);
+ $rootScope.$digest();
+
+ browserTrigger(element, 'touchstart');
+ browserTrigger(element, 'touchend');
+ browserTrigger(element, 'click', {x:1, y:1});
+
+ expect($rootScope.tapped).toEqual(true);
}));
+ describe('the clickbuster', function() {
+ var element1, element2;
- it('should not cancel clicks on distant elements', inject(function($rootScope, $compile, $rootElement) {
- $rootElement.append(
- '
x
' +
- '
y
'
- );
- $compile($rootElement)($rootScope);
+ beforeEach(inject(function($rootElement, $document) {
+ $document.find('body').append($rootElement);
+ }));
- element1 = $rootElement.find('div').eq(0);
- element2 = $rootElement.find('div').eq(1);
+ afterEach(inject(function($document) {
+ $document.find('body').empty();
+ }));
- $rootScope.count1 = 0;
- $rootScope.count2 = 0;
- $rootScope.$digest();
+ it('should cancel the following click event', inject(function($rootScope, $compile, $rootElement, $document) {
+ element = $compile('')($rootScope);
+ $rootElement.append(element);
- expect($rootScope.count1).toBe(0);
- expect($rootScope.count2).toBe(0);
+ $rootScope.count = 0;
+ $rootScope.$digest();
- time = 10;
- browserTrigger(element1, 'touchstart',{
- keys: [],
- x: 10,
- y: 10
- });
+ expect($rootScope.count).toBe(0);
- time = 50;
- browserTrigger(element1, 'touchend',{
- keys: [],
- x: 10,
- y: 10
- });
+ // Fire touchstart at 10ms, touchend at 50ms, the click at 300ms.
+ time = 10;
+ browserTrigger(element, 'touchstart',{
+ keys: [],
+ x: 10,
+ y: 10
+ });
- expect($rootScope.count1).toBe(1);
+ time = 50;
+ browserTrigger(element, 'touchend',{
+ keys: [],
+ x: 10,
+ y: 10
+ });
- time = 90;
- // Verify that it is blurred so we don't get soft-keyboard
- element1[0].blur = jasmine.createSpy('blur');
- browserTrigger(element1, 'click',{
- keys: [],
- x: 10,
- y: 10
- });
- expect(element1[0].blur).toHaveBeenCalled();
+ expect($rootScope.count).toBe(1);
- expect($rootScope.count1).toBe(1);
+ time = 100;
+ browserTrigger(element, 'click',{
+ keys: [],
+ x: 10,
+ y: 10
+ });
- time = 100;
- browserTrigger(element1, 'touchstart',{
- keys: [],
- x: 10,
- y: 10
- });
+ expect($rootScope.count).toBe(1);
+ }));
- time = 130;
- browserTrigger(element1, 'touchend',{
- keys: [],
- x: 10,
- y: 10
- });
- expect($rootScope.count1).toBe(2);
+ it('should cancel the following click event even when the element has changed', inject(
+ function($rootScope, $compile, $rootElement) {
+ $rootElement.append(
+ '