angularjs-rails-resource 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +63 -6
- data/lib/angularjs-rails-resource/version.rb +1 -1
- data/test/lib/angular/angular-bootstrap-prettify.js +13 -8
- data/test/lib/angular/angular-bootstrap.js +3 -2
- data/test/lib/angular/angular-cookies.js +14 -1
- data/test/lib/angular/angular-loader.js +33 -5
- data/test/lib/angular/angular-locale_en-us.js +4 -0
- data/test/lib/angular/angular-mobile.js +267 -0
- data/test/lib/angular/angular-mocks.js +157 -52
- data/test/lib/angular/angular-resource.js +195 -102
- data/test/lib/angular/angular-sanitize.js +28 -5
- data/test/lib/angular/angular-scenario.js +27862 -0
- data/test/lib/angular/angular.js +2820 -907
- data/test/unit/angularjs/rails/fieldRenamingSpec.js +91 -0
- data/test/unit/angularjs/rails/httpSettingsSpec.js +219 -0
- data/test/unit/angularjs/rails/nestedUrlsSpec.js +327 -0
- data/test/unit/angularjs/rails/resourceSpec.js +37 -662
- data/test/unit/angularjs/rails/rootWrappingSpec.js +46 -0
- data/vendor/assets/javascripts/angularjs/rails/resource.js +62 -16
- metadata +16 -2
data/README.md
CHANGED
@@ -58,6 +58,7 @@ The following options are available for the config object passed to the factory
|
|
58
58
|
* **httpConfig** *(optional)* - By default we will add the following headers to ensure that the request is processed as JSON by Rails. You can specify additional http config options or override any of the defaults by setting this property. See the [AngularJS $http API](http://docs.angularjs.org/api/ng.$http) for more information.
|
59
59
|
* **headers**
|
60
60
|
* **Accept** - application/json
|
61
|
+
* **Content-Type** - application/json
|
61
62
|
* **defaultParams** *(optional)* - If the resource expects a default set of query params on every call you can specify them here.
|
62
63
|
* **requestTransformers** *(optional) - See [Transformers / Interceptors](#transformers--interceptors)
|
63
64
|
* **responseInterceptors** *(optional)* - See [Transformers / Interceptors](#transformers--interceptors)
|
@@ -117,9 +118,18 @@ object to keep track of what the field was originally pointing to. The original
|
|
117
118
|
that if response.data is reassigned that there's still a pointer to the original response.data object.
|
118
119
|
|
119
120
|
|
120
|
-
## Methods
|
121
|
+
## Resource Methods
|
121
122
|
Resources created using this factory have the following methods available and each one (except the constructor) returns a [Promise](#promises).
|
122
123
|
|
124
|
+
### $url
|
125
|
+
***
|
126
|
+
|
127
|
+
Returns the resource URL using the given context.
|
128
|
+
|
129
|
+
####Parameters
|
130
|
+
|
131
|
+
* **context** - The context to use when building the url. See [Resource URLs](#resource-urls) above for more information.
|
132
|
+
|
123
133
|
### constructor
|
124
134
|
***
|
125
135
|
|
@@ -128,10 +138,19 @@ The constructor is the function returned by the railsResourceFactory and can be
|
|
128
138
|
####Parameters
|
129
139
|
* **data** *(optional)* - An object containing the data to be stored in the instance.
|
130
140
|
|
141
|
+
### $get
|
142
|
+
***
|
143
|
+
|
144
|
+
Executes a GET request against the given URL and returns a promise that will be resolved with a new Resource instance (or instances in the case of an array response).
|
145
|
+
|
146
|
+
####Parameters
|
147
|
+
* **url** - The url to GET
|
148
|
+
* **queryParams** - The set of query parameters to include in the GET request
|
149
|
+
|
131
150
|
### query
|
132
151
|
***
|
133
152
|
|
134
|
-
|
153
|
+
Executes a GET request against the resource's base url and returns a promise that will be resolved with an array of new Resource instances.
|
135
154
|
|
136
155
|
####Parameters
|
137
156
|
* **query params** - An map of strings or objects that are passed to $http to be turned into query parameters
|
@@ -141,16 +160,54 @@ A "class" method that executes a GET request against the base url with query par
|
|
141
160
|
### get
|
142
161
|
***
|
143
162
|
|
144
|
-
|
163
|
+
Executs a GET request against the resource's url and returns a promise that will be resolved with a new instance of the Resource.
|
145
164
|
|
146
165
|
####Parameters
|
147
166
|
* **context** - A context object that is used during url evaluation to resolve expression variables. If you are using a basic url this can be an id number to append to the url.
|
148
167
|
|
149
168
|
|
169
|
+
### $post, $put, $patch
|
170
|
+
***
|
171
|
+
|
172
|
+
Transforms the given data and submits it using a POST/PUT/PATCH to the given URL and returns a promise that will be resolved with a new Resource instance (or instances in the case of an array response).
|
173
|
+
|
174
|
+
####Parameters
|
175
|
+
* **url** - The url to POST/PUT/PATCH
|
176
|
+
* **data** - The data to transform and submit to the server
|
177
|
+
|
178
|
+
### $delete
|
179
|
+
***
|
180
|
+
|
181
|
+
Executes a DELETE against the given URL and returns a promise that will be resolved with a new Resource instance (if the server returns a body).
|
182
|
+
|
183
|
+
####Parameters
|
184
|
+
* **url** - The url to POST/PUT/PATCH
|
185
|
+
|
186
|
+
## Resource Instance Methods
|
187
|
+
The instance methods can be used on any instance (created manually or returned in a promise response) of a resource.
|
188
|
+
All of the instance methods will update the instance in-place on response and will resolve the promise with the current instance.
|
189
|
+
|
190
|
+
### $url
|
191
|
+
***
|
192
|
+
|
193
|
+
Returns the url for the instance.
|
194
|
+
|
195
|
+
####Parameters
|
196
|
+
|
197
|
+
None
|
198
|
+
|
199
|
+
### $post, $put, $patch
|
200
|
+
***
|
201
|
+
|
202
|
+
Transforms the instance and submits it using POST/PUT/PATCH to the given URL and returns a promise that will be resolved with a new Resource instance (or instances in the case of an array response).
|
203
|
+
|
204
|
+
####Parameters
|
205
|
+
* **url** - The url to POST/PUT/PATCH/DELETE
|
206
|
+
|
150
207
|
### create
|
151
208
|
***
|
152
209
|
|
153
|
-
|
210
|
+
Transforms and submits the instance using a POST to the resource base URL.
|
154
211
|
|
155
212
|
####Parameters
|
156
213
|
|
@@ -160,7 +217,7 @@ None
|
|
160
217
|
### update
|
161
218
|
***
|
162
219
|
|
163
|
-
|
220
|
+
Transforms and submits the instance using a PUT to the resource's URL.
|
164
221
|
|
165
222
|
####Parameters
|
166
223
|
|
@@ -170,7 +227,7 @@ None
|
|
170
227
|
### remove / delete
|
171
228
|
***
|
172
229
|
|
173
|
-
|
230
|
+
Execute a DELETE to the resource's url.
|
174
231
|
|
175
232
|
####Parameters
|
176
233
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/**
|
2
|
-
* @license AngularJS v1.
|
2
|
+
* @license AngularJS v1.1.4
|
3
3
|
* (c) 2010-2012 Google, Inc. http://angularjs.org
|
4
4
|
* License: MIT
|
5
5
|
*/
|
@@ -10,10 +10,10 @@ var directive = {};
|
|
10
10
|
var service = { value: {} };
|
11
11
|
|
12
12
|
var DEPENDENCIES = {
|
13
|
-
'angular.js': 'http://code.angularjs.org/
|
14
|
-
'angular-resource.js': 'http://code.angularjs.org/
|
15
|
-
'angular-sanitize.js': 'http://code.angularjs.org/
|
16
|
-
'angular-cookies.js': 'http://code.angularjs.org/
|
13
|
+
'angular.js': 'http://code.angularjs.org/' + angular.version.full + '/angular.min.js',
|
14
|
+
'angular-resource.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-resource.min.js',
|
15
|
+
'angular-sanitize.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-sanitize.min.js',
|
16
|
+
'angular-cookies.js': 'http://code.angularjs.org/' + angular.version.full + '/angular-cookies.min.js'
|
17
17
|
};
|
18
18
|
|
19
19
|
|
@@ -185,7 +185,8 @@ directive.ngEvalJavascript = ['getEmbeddedTemplate', function(getEmbeddedTemplat
|
|
185
185
|
}];
|
186
186
|
|
187
187
|
|
188
|
-
directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
|
188
|
+
directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location', '$sniffer',
|
189
|
+
function($templateCache, $browser, docsRootScope, $location, $sniffer) {
|
189
190
|
return {
|
190
191
|
terminal: true,
|
191
192
|
link: function(scope, element, attrs) {
|
@@ -195,6 +196,7 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
|
|
195
196
|
$provide.value('$templateCache', $templateCache);
|
196
197
|
$provide.value('$anchorScroll', angular.noop);
|
197
198
|
$provide.value('$browser', $browser);
|
199
|
+
$provide.value('$sniffer', $sniffer);
|
198
200
|
$provide.provider('$location', function() {
|
199
201
|
this.$get = ['$rootScope', function($rootScope) {
|
200
202
|
docsRootScope.$on('$locationChangeSuccess', function(event, oldUrl, newUrl) {
|
@@ -216,7 +218,7 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
|
|
216
218
|
}, $delegate);
|
217
219
|
}]);
|
218
220
|
$provide.decorator('$rootScope', ['$delegate', function(embedRootScope) {
|
219
|
-
docsRootScope.$watch(function() {
|
221
|
+
docsRootScope.$watch(function embedRootScopeDigestWatch() {
|
220
222
|
embedRootScope.$digest();
|
221
223
|
});
|
222
224
|
return embedRootScope;
|
@@ -229,6 +231,7 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
|
|
229
231
|
event.preventDefault();
|
230
232
|
}
|
231
233
|
});
|
234
|
+
|
232
235
|
angular.bootstrap(element, modules);
|
233
236
|
}
|
234
237
|
};
|
@@ -290,6 +293,7 @@ service.getEmbeddedTemplate = ['reindentCode', function(reindentCode) {
|
|
290
293
|
|
291
294
|
|
292
295
|
angular.module('bootstrapPrettify', []).directive(directive).factory(service);
|
296
|
+
|
293
297
|
// Copyright (C) 2006 Google Inc.
|
294
298
|
//
|
295
299
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
@@ -935,7 +939,7 @@ var REGEXP_PRECEDER_PATTERN = '(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[
|
|
935
939
|
* recognized.
|
936
940
|
*
|
937
941
|
* Shortcut is an optional string of characters, any of which, if the first
|
938
|
-
* character,
|
942
|
+
* character, guarantee that this pattern and only this pattern matches.
|
939
943
|
*
|
940
944
|
* @param {Array} shortcutStylePatterns patterns that always start with
|
941
945
|
* a known character. Must have a shortcut string.
|
@@ -1829,5 +1833,6 @@ var REGEXP_PRECEDER_PATTERN = '(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[
|
|
1829
1833
|
}
|
1830
1834
|
})();
|
1831
1835
|
|
1836
|
+
|
1832
1837
|
})(window, window.angular);
|
1833
1838
|
angular.element(document).find('head').append('<style type="text/css">.com{color:#93a1a1;}.lit{color:#195f91;}.pun,.opn,.clo{color:#93a1a1;}.fun{color:#dc322f;}.str,.atv{color:#D14;}.kwd,.linenums .tag{color:#1e347b;}.typ,.atn,.dec,.var{color:teal;}.pln{color:#48484c;}.prettyprint{padding:8px;background-color:#f7f7f9;border:1px solid #e1e1e8;}.prettyprint.linenums{-webkit-box-shadow:inset 40px 0 0 #fbfbfc,inset 41px 0 0 #ececf0;-moz-box-shadow:inset 40px 0 0 #fbfbfc,inset 41px 0 0 #ececf0;box-shadow:inset 40px 0 0 #fbfbfc,inset 41px 0 0 #ececf0;}ol.linenums{margin:0 0 0 33px;}ol.linenums li{padding-left:12px;color:#bebec5;line-height:18px;text-shadow:0 1px 0 #fff;}</style>');
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/**
|
2
|
-
* @license AngularJS v1.
|
2
|
+
* @license AngularJS v1.1.4
|
3
3
|
* (c) 2010-2012 Google, Inc. http://angularjs.org
|
4
4
|
* License: MIT
|
5
5
|
*/
|
@@ -15,7 +15,7 @@ directive.dropdownToggle =
|
|
15
15
|
return {
|
16
16
|
restrict: 'C',
|
17
17
|
link: function(scope, element, attrs) {
|
18
|
-
scope.$watch(function(){return $location.path();}, function() {
|
18
|
+
scope.$watch(function dropdownTogglePathWatch(){return $location.path();}, function dropdownTogglePathWatchAction() {
|
19
19
|
close && close();
|
20
20
|
});
|
21
21
|
|
@@ -163,4 +163,5 @@ directive.tabPane = function() {
|
|
163
163
|
|
164
164
|
angular.module('bootstrap', []).directive(directive);
|
165
165
|
|
166
|
+
|
166
167
|
})(window, window.angular);
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/**
|
2
|
-
* @license AngularJS v1.
|
2
|
+
* @license AngularJS v1.1.4
|
3
3
|
* (c) 2010-2012 Google, Inc. http://angularjs.org
|
4
4
|
* License: MIT
|
5
5
|
*/
|
@@ -25,6 +25,18 @@ angular.module('ngCookies', ['ng']).
|
|
25
25
|
* this object, new cookies are created/deleted at the end of current $eval.
|
26
26
|
*
|
27
27
|
* @example
|
28
|
+
<doc:example>
|
29
|
+
<doc:source>
|
30
|
+
<script>
|
31
|
+
function ExampleController($cookies) {
|
32
|
+
// Retrieving a cookie
|
33
|
+
var favoriteCookie = $cookies.myFavorite;
|
34
|
+
// Setting a cookie
|
35
|
+
$cookies.myFavorite = 'oatmeal';
|
36
|
+
}
|
37
|
+
</script>
|
38
|
+
</doc:source>
|
39
|
+
</doc:example>
|
28
40
|
*/
|
29
41
|
factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) {
|
30
42
|
var cookies = {},
|
@@ -168,4 +180,5 @@ angular.module('ngCookies', ['ng']).
|
|
168
180
|
|
169
181
|
}]);
|
170
182
|
|
183
|
+
|
171
184
|
})(window, window.angular);
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/**
|
2
|
-
* @license AngularJS v1.
|
2
|
+
* @license AngularJS v1.1.4
|
3
3
|
* (c) 2010-2012 Google, Inc. http://angularjs.org
|
4
4
|
* License: MIT
|
5
5
|
*/
|
@@ -36,7 +36,7 @@ function setupModuleLoader(window) {
|
|
36
36
|
*
|
37
37
|
* # Module
|
38
38
|
*
|
39
|
-
* A module is a collocation of services, directives, filters, and
|
39
|
+
* A module is a collocation of services, directives, filters, and configuration information. Module
|
40
40
|
* is used to configure the {@link AUTO.$injector $injector}.
|
41
41
|
*
|
42
42
|
* <pre>
|
@@ -67,7 +67,7 @@ function setupModuleLoader(window) {
|
|
67
67
|
* @param {!string} name The name of the module to create or retrieve.
|
68
68
|
* @param {Array.<string>=} requires If specified then new module is being created. If unspecified then the
|
69
69
|
* the module is being retrieved for further configuration.
|
70
|
-
* @param {Function} configFn
|
70
|
+
* @param {Function} configFn Optional configuration function for the module. Same as
|
71
71
|
* {@link angular.Module#config Module#config()}.
|
72
72
|
* @returns {module} new module with the {@link angular.Module} api.
|
73
73
|
*/
|
@@ -170,6 +170,33 @@ function setupModuleLoader(window) {
|
|
170
170
|
*/
|
171
171
|
constant: invokeLater('$provide', 'constant', 'unshift'),
|
172
172
|
|
173
|
+
/**
|
174
|
+
* @ngdoc method
|
175
|
+
* @name angular.Module#animation
|
176
|
+
* @methodOf angular.Module
|
177
|
+
* @param {string} name animation name
|
178
|
+
* @param {Function} animationFactory Factory function for creating new instance of an animation.
|
179
|
+
* @description
|
180
|
+
*
|
181
|
+
* Defines an animation hook that can be later used with {@link ng.directive:ngAnimate ngAnimate}
|
182
|
+
* alongside {@link ng.directive:ngAnimate#Description common ng directives} as well as custom directives.
|
183
|
+
* <pre>
|
184
|
+
* module.animation('animation-name', function($inject1, $inject2) {
|
185
|
+
* return {
|
186
|
+
* //this gets called in preparation to setup an animation
|
187
|
+
* setup : function(element) { ... },
|
188
|
+
*
|
189
|
+
* //this gets called once the animation is run
|
190
|
+
* start : function(element, done, memo) { ... }
|
191
|
+
* }
|
192
|
+
* })
|
193
|
+
* </pre>
|
194
|
+
*
|
195
|
+
* See {@link ng.$animationProvider#register $animationProvider.register()} and
|
196
|
+
* {@link ng.directive:ngAnimate ngAnimate} for more information.
|
197
|
+
*/
|
198
|
+
animation: invokeLater('$animationProvider', 'register'),
|
199
|
+
|
173
200
|
/**
|
174
201
|
* @ngdoc method
|
175
202
|
* @name angular.Module#filter
|
@@ -222,8 +249,8 @@ function setupModuleLoader(window) {
|
|
222
249
|
* @param {Function} initializationFn Execute this function after injector creation.
|
223
250
|
* Useful for application initialization.
|
224
251
|
* @description
|
225
|
-
* Use this method to register work which
|
226
|
-
*
|
252
|
+
* Use this method to register work which should be performed when the injector is done
|
253
|
+
* loading all modules.
|
227
254
|
*/
|
228
255
|
run: function(block) {
|
229
256
|
runBlocks.push(block);
|
@@ -254,6 +281,7 @@ function setupModuleLoader(window) {
|
|
254
281
|
});
|
255
282
|
|
256
283
|
}
|
284
|
+
|
257
285
|
)(window);
|
258
286
|
|
259
287
|
/**
|
@@ -0,0 +1,4 @@
|
|
1
|
+
angular.module("ngLocale", [], ["$provide", function($provide) {
|
2
|
+
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
|
3
|
+
$provide.value("$locale", {"DATETIME_FORMATS":{"MONTH":["January","February","March","April","May","June","July","August","September","October","November","December"],"SHORTMONTH":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"DAY":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"SHORTDAY":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"AMPMS":["AM","PM"],"medium":"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a","fullDate":"EEEE, MMMM d, y","longDate":"MMMM d, y","mediumDate":"MMM d, y","shortDate":"M/d/yy","mediumTime":"h:mm:ss a","shortTime":"h:mm a"},"NUMBER_FORMATS":{"DECIMAL_SEP":".","GROUP_SEP":",","PATTERNS":[{"minInt":1,"minFrac":0,"macFrac":0,"posPre":"","posSuf":"","negPre":"-","negSuf":"","gSize":3,"lgSize":3,"maxFrac":3},{"minInt":1,"minFrac":2,"macFrac":0,"posPre":"\u00A4","posSuf":"","negPre":"(\u00A4","negSuf":")","gSize":3,"lgSize":3,"maxFrac":2}],"CURRENCY_SYM":"$"},"pluralCat":function (n) { if (n == 1) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;},"id":"en-us"});
|
4
|
+
}]);
|
@@ -0,0 +1,267 @@
|
|
1
|
+
/**
|
2
|
+
* @license AngularJS v1.1.4
|
3
|
+
* (c) 2010-2012 Google, Inc. http://angularjs.org
|
4
|
+
* License: MIT
|
5
|
+
*/
|
6
|
+
(function(window, angular, undefined) {
|
7
|
+
'use strict';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* @ngdoc overview
|
11
|
+
* @name ngMobile
|
12
|
+
* @description
|
13
|
+
*/
|
14
|
+
|
15
|
+
/*
|
16
|
+
* Touch events and other mobile helpers by Braden Shepherdson (braden.shepherdson@gmail.com)
|
17
|
+
* Based on jQuery Mobile touch event handling (jquerymobile.com)
|
18
|
+
*/
|
19
|
+
|
20
|
+
// define ngSanitize module and register $sanitize service
|
21
|
+
var ngMobile = angular.module('ngMobile', []);
|
22
|
+
|
23
|
+
/**
|
24
|
+
* @ngdoc directive
|
25
|
+
* @name ngMobile.directive:ngTap
|
26
|
+
*
|
27
|
+
* @description
|
28
|
+
* Specify custom behavior when element is tapped on a touchscreen device.
|
29
|
+
* A tap is a brief, down-and-up touch without much motion.
|
30
|
+
*
|
31
|
+
* @element ANY
|
32
|
+
* @param {expression} ngClick {@link guide/expression Expression} to evaluate
|
33
|
+
* upon tap. (Event object is available as `$event`)
|
34
|
+
*
|
35
|
+
* @example
|
36
|
+
<doc:example>
|
37
|
+
<doc:source>
|
38
|
+
<button ng-tap="count = count + 1" ng-init="count=0">
|
39
|
+
Increment
|
40
|
+
</button>
|
41
|
+
count: {{ count }}
|
42
|
+
</doc:source>
|
43
|
+
</doc:example>
|
44
|
+
*/
|
45
|
+
|
46
|
+
ngMobile.config(['$provide', function($provide) {
|
47
|
+
$provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
|
48
|
+
// drop the default ngClick directive
|
49
|
+
$delegate.shift();
|
50
|
+
return $delegate;
|
51
|
+
}]);
|
52
|
+
}]);
|
53
|
+
|
54
|
+
ngMobile.directive('ngClick', ['$parse', '$timeout', '$rootElement',
|
55
|
+
function($parse, $timeout, $rootElement) {
|
56
|
+
var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
|
57
|
+
var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
|
58
|
+
var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
|
59
|
+
var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
|
60
|
+
var lastPreventedTime;
|
61
|
+
var touchCoordinates;
|
62
|
+
|
63
|
+
|
64
|
+
// TAP EVENTS AND GHOST CLICKS
|
65
|
+
//
|
66
|
+
// Why tap events?
|
67
|
+
// Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
|
68
|
+
// double-tapping, and then fire a click event.
|
69
|
+
//
|
70
|
+
// This delay sucks and makes mobile apps feel unresponsive.
|
71
|
+
// So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when
|
72
|
+
// the user has tapped on something.
|
73
|
+
//
|
74
|
+
// What happens when the browser then generates a click event?
|
75
|
+
// The browser, of course, also detects the tap and fires a click after a delay. This results in
|
76
|
+
// tapping/clicking twice. So we do "clickbusting" to prevent it.
|
77
|
+
//
|
78
|
+
// How does it work?
|
79
|
+
// We attach global touchstart and click handlers, that run during the capture (early) phase.
|
80
|
+
// So the sequence for a tap is:
|
81
|
+
// - global touchstart: Sets an "allowable region" at the point touched.
|
82
|
+
// - element's touchstart: Starts a touch
|
83
|
+
// (- touchmove or touchcancel ends the touch, no click follows)
|
84
|
+
// - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
|
85
|
+
// too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
|
86
|
+
// - preventGhostClick() removes the allowable region the global touchstart created.
|
87
|
+
// - The browser generates a click event.
|
88
|
+
// - The global click handler catches the click, and checks whether it was in an allowable region.
|
89
|
+
// - If preventGhostClick was called, the region will have been removed, the click is busted.
|
90
|
+
// - If the region is still there, the click proceeds normally. Therefore clicks on links and
|
91
|
+
// other elements without ngTap on them work normally.
|
92
|
+
//
|
93
|
+
// This is an ugly, terrible hack!
|
94
|
+
// Yeah, tell me about it. The alternatives are using the slow click events, or making our users
|
95
|
+
// deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular
|
96
|
+
// encapsulates this ugly logic away from the user.
|
97
|
+
//
|
98
|
+
// Why not just put click handlers on the element?
|
99
|
+
// We do that too, just to be sure. The problem is that the tap event might have caused the DOM
|
100
|
+
// to change, so that the click fires in the same position but something else is there now. So
|
101
|
+
// the handlers are global and care only about coordinates and not elements.
|
102
|
+
|
103
|
+
// Checks if the coordinates are close enough to be within the region.
|
104
|
+
function hit(x1, y1, x2, y2) {
|
105
|
+
return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
|
106
|
+
}
|
107
|
+
|
108
|
+
// Checks a list of allowable regions against a click location.
|
109
|
+
// Returns true if the click should be allowed.
|
110
|
+
// Splices out the allowable region from the list after it has been used.
|
111
|
+
function checkAllowableRegions(touchCoordinates, x, y) {
|
112
|
+
for (var i = 0; i < touchCoordinates.length; i += 2) {
|
113
|
+
if (hit(touchCoordinates[i], touchCoordinates[i+1], x, y)) {
|
114
|
+
touchCoordinates.splice(i, i + 2);
|
115
|
+
return true; // allowable region
|
116
|
+
}
|
117
|
+
}
|
118
|
+
return false; // No allowable region; bust it.
|
119
|
+
}
|
120
|
+
|
121
|
+
// Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
|
122
|
+
// was called recently.
|
123
|
+
function onClick(event) {
|
124
|
+
if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
|
125
|
+
return; // Too old.
|
126
|
+
}
|
127
|
+
|
128
|
+
var touches = event.touches && event.touches.length ? event.touches : [event];
|
129
|
+
var x = touches[0].clientX;
|
130
|
+
var y = touches[0].clientY;
|
131
|
+
// Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
|
132
|
+
// and on the input element). Depending on the exact browser, this second click we don't want
|
133
|
+
// to bust has either (0,0) or negative coordinates.
|
134
|
+
if (x < 1 && y < 1) {
|
135
|
+
return; // offscreen
|
136
|
+
}
|
137
|
+
|
138
|
+
// Look for an allowable region containing this click.
|
139
|
+
// If we find one, that means it was created by touchstart and not removed by
|
140
|
+
// preventGhostClick, so we don't bust it.
|
141
|
+
if (checkAllowableRegions(touchCoordinates, x, y)) {
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
|
145
|
+
// If we didn't find an allowable region, bust the click.
|
146
|
+
event.stopPropagation();
|
147
|
+
event.preventDefault();
|
148
|
+
}
|
149
|
+
|
150
|
+
|
151
|
+
// Global touchstart handler that creates an allowable region for a click event.
|
152
|
+
// This allowable region can be removed by preventGhostClick if we want to bust it.
|
153
|
+
function onTouchStart(event) {
|
154
|
+
var touches = event.touches && event.touches.length ? event.touches : [event];
|
155
|
+
var x = touches[0].clientX;
|
156
|
+
var y = touches[0].clientY;
|
157
|
+
touchCoordinates.push(x, y);
|
158
|
+
|
159
|
+
$timeout(function() {
|
160
|
+
// Remove the allowable region.
|
161
|
+
for (var i = 0; i < touchCoordinates.length; i += 2) {
|
162
|
+
if (touchCoordinates[i] == x && touchCoordinates[i+1] == y) {
|
163
|
+
touchCoordinates.splice(i, i + 2);
|
164
|
+
return;
|
165
|
+
}
|
166
|
+
}
|
167
|
+
}, PREVENT_DURATION, false);
|
168
|
+
}
|
169
|
+
|
170
|
+
// On the first call, attaches some event handlers. Then whenever it gets called, it creates a
|
171
|
+
// zone around the touchstart where clicks will get busted.
|
172
|
+
function preventGhostClick(x, y) {
|
173
|
+
if (!touchCoordinates) {
|
174
|
+
$rootElement[0].addEventListener('click', onClick, true);
|
175
|
+
$rootElement[0].addEventListener('touchstart', onTouchStart, true);
|
176
|
+
touchCoordinates = [];
|
177
|
+
}
|
178
|
+
|
179
|
+
lastPreventedTime = Date.now();
|
180
|
+
|
181
|
+
checkAllowableRegions(touchCoordinates, x, y);
|
182
|
+
}
|
183
|
+
|
184
|
+
// Actual linking function.
|
185
|
+
return function(scope, element, attr) {
|
186
|
+
var expressionFn = $parse(attr.ngClick),
|
187
|
+
tapping = false,
|
188
|
+
tapElement, // Used to blur the element after a tap.
|
189
|
+
startTime, // Used to check if the tap was held too long.
|
190
|
+
touchStartX,
|
191
|
+
touchStartY;
|
192
|
+
|
193
|
+
function resetState() {
|
194
|
+
tapping = false;
|
195
|
+
}
|
196
|
+
|
197
|
+
element.bind('touchstart', function(event) {
|
198
|
+
tapping = true;
|
199
|
+
tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
|
200
|
+
// Hack for Safari, which can target text nodes instead of containers.
|
201
|
+
if(tapElement.nodeType == 3) {
|
202
|
+
tapElement = tapElement.parentNode;
|
203
|
+
}
|
204
|
+
|
205
|
+
startTime = Date.now();
|
206
|
+
|
207
|
+
var touches = event.touches && event.touches.length ? event.touches : [event];
|
208
|
+
var e = touches[0].originalEvent || touches[0];
|
209
|
+
touchStartX = e.clientX;
|
210
|
+
touchStartY = e.clientY;
|
211
|
+
});
|
212
|
+
|
213
|
+
element.bind('touchmove', function(event) {
|
214
|
+
resetState();
|
215
|
+
});
|
216
|
+
|
217
|
+
element.bind('touchcancel', function(event) {
|
218
|
+
resetState();
|
219
|
+
});
|
220
|
+
|
221
|
+
element.bind('touchend', function(event) {
|
222
|
+
var diff = Date.now() - startTime;
|
223
|
+
|
224
|
+
var touches = (event.changedTouches && event.changedTouches.length) ? event.changedTouches :
|
225
|
+
((event.touches && event.touches.length) ? event.touches : [event]);
|
226
|
+
var e = touches[0].originalEvent || touches[0];
|
227
|
+
var x = e.clientX;
|
228
|
+
var y = e.clientY;
|
229
|
+
var dist = Math.sqrt( Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2) );
|
230
|
+
|
231
|
+
if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
|
232
|
+
// Call preventGhostClick so the clickbuster will catch the corresponding click.
|
233
|
+
preventGhostClick(x, y);
|
234
|
+
|
235
|
+
// Blur the focused element (the button, probably) before firing the callback.
|
236
|
+
// This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
|
237
|
+
// I couldn't get anything to work reliably on Android Chrome.
|
238
|
+
if (tapElement) {
|
239
|
+
tapElement.blur();
|
240
|
+
}
|
241
|
+
|
242
|
+
scope.$apply(function() {
|
243
|
+
// TODO(braden): This is sending the touchend, not a tap or click. Is that kosher?
|
244
|
+
expressionFn(scope, {$event: event});
|
245
|
+
});
|
246
|
+
}
|
247
|
+
tapping = false;
|
248
|
+
});
|
249
|
+
|
250
|
+
// Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
|
251
|
+
// something else nearby.
|
252
|
+
element.onclick = function(event) { };
|
253
|
+
|
254
|
+
// Fallback click handler.
|
255
|
+
// Busted clicks don't get this far, and adding this handler allows ng-tap to be used on
|
256
|
+
// desktop as well, to allow more portable sites.
|
257
|
+
element.bind('click', function(event) {
|
258
|
+
scope.$apply(function() {
|
259
|
+
expressionFn(scope, {$event: event});
|
260
|
+
});
|
261
|
+
});
|
262
|
+
};
|
263
|
+
}]);
|
264
|
+
|
265
|
+
|
266
|
+
|
267
|
+
})(window, window.angular);
|