1
|
|
|
/** |
2
|
|
|
* @license AngularJS v1.5.0 |
3
|
|
|
* (c) 2010-2016 Google, Inc. http://angularjs.org |
4
|
|
|
* License: MIT |
5
|
|
|
*/ |
6
|
|
|
(function(window, angular, undefined) {'use strict'; |
7
|
|
|
|
8
|
|
|
/* global ngTouchClickDirectiveFactory: false, |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* @ngdoc module |
13
|
|
|
* @name ngTouch |
14
|
|
|
* @description |
15
|
|
|
* |
16
|
|
|
* # ngTouch |
17
|
|
|
* |
18
|
|
|
* The `ngTouch` module provides touch events and other helpers for touch-enabled devices. |
19
|
|
|
* The implementation is based on jQuery Mobile touch event handling |
20
|
|
|
* ([jquerymobile.com](http://jquerymobile.com/)). |
21
|
|
|
* |
22
|
|
|
* |
23
|
|
|
* See {@link ngTouch.$swipe `$swipe`} for usage. |
24
|
|
|
* |
25
|
|
|
* <div doc-module-components="ngTouch"></div> |
26
|
|
|
* |
27
|
|
|
*/ |
28
|
|
|
|
29
|
|
|
// define ngTouch module |
30
|
|
|
/* global -ngTouch */ |
31
|
|
|
var ngTouch = angular.module('ngTouch', []); |
32
|
|
|
|
33
|
|
|
ngTouch.provider('$touch', $TouchProvider); |
34
|
|
|
|
35
|
|
|
function nodeName_(element) { |
36
|
|
|
return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName)); |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @ngdoc provider |
41
|
|
|
* @name $touchProvider |
42
|
|
|
* |
43
|
|
|
* @description |
44
|
|
|
* The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}. |
45
|
|
|
*/ |
46
|
|
|
$TouchProvider.$inject = ['$provide', '$compileProvider']; |
47
|
|
|
function $TouchProvider($provide, $compileProvider) { |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @ngdoc method |
51
|
|
|
* @name $touchProvider#ngClickOverrideEnabled |
52
|
|
|
* |
53
|
|
|
* @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the |
54
|
|
|
* current ngClickOverrideEnabled state |
55
|
|
|
* @returns {*} current value if used as getter or itself (chaining) if used as setter |
56
|
|
|
* |
57
|
|
|
* @kind function |
58
|
|
|
* |
59
|
|
|
* @description |
60
|
|
|
* Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled, |
61
|
|
|
* the default ngClick directive will be replaced by a version that eliminates the 300ms delay for |
62
|
|
|
* click events on browser for touch-devices. |
63
|
|
|
* |
64
|
|
|
* The default is `false`. |
65
|
|
|
* |
66
|
|
|
*/ |
67
|
|
|
var ngClickOverrideEnabled = false; |
68
|
|
|
var ngClickDirectiveAdded = false; |
69
|
|
|
this.ngClickOverrideEnabled = function(enabled) { |
70
|
|
|
if (angular.isDefined(enabled)) { |
71
|
|
|
|
72
|
|
|
if (enabled && !ngClickDirectiveAdded) { |
73
|
|
|
ngClickDirectiveAdded = true; |
74
|
|
|
|
75
|
|
|
// Use this to identify the correct directive in the delegate |
76
|
|
|
ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch'; |
77
|
|
|
$compileProvider.directive('ngClick', ngTouchClickDirectiveFactory); |
78
|
|
|
|
79
|
|
|
$provide.decorator('ngClickDirective', ['$delegate', function($delegate) { |
80
|
|
|
if (ngClickOverrideEnabled) { |
81
|
|
|
// drop the default ngClick directive |
82
|
|
|
$delegate.shift(); |
83
|
|
|
} else { |
84
|
|
|
// drop the ngTouch ngClick directive if the override has been re-disabled (because |
85
|
|
|
// we cannot de-register added directives) |
86
|
|
|
var i = $delegate.length - 1; |
87
|
|
|
while (i >= 0) { |
88
|
|
|
if ($delegate[i].$$moduleName === 'ngTouch') { |
89
|
|
|
$delegate.splice(i, 1); |
90
|
|
|
break; |
91
|
|
|
} |
92
|
|
|
i--; |
93
|
|
|
} |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
return $delegate; |
97
|
|
|
}]); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
ngClickOverrideEnabled = enabled; |
101
|
|
|
return this; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
return ngClickOverrideEnabled; |
105
|
|
|
}; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @ngdoc service |
109
|
|
|
* @name $touch |
110
|
|
|
* @kind object |
111
|
|
|
* |
112
|
|
|
* @description |
113
|
|
|
* Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method. |
114
|
|
|
* |
115
|
|
|
*/ |
116
|
|
|
this.$get = function() { |
117
|
|
|
return { |
118
|
|
|
/** |
119
|
|
|
* @ngdoc method |
120
|
|
|
* @name $touch#ngClickOverrideEnabled |
121
|
|
|
* |
122
|
|
|
* @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider}, |
123
|
|
|
* i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled. |
124
|
|
|
* |
125
|
|
|
* @kind function |
126
|
|
|
*/ |
127
|
|
|
ngClickOverrideEnabled: function() { |
128
|
|
|
return ngClickOverrideEnabled; |
129
|
|
|
} |
130
|
|
|
}; |
131
|
|
|
}; |
132
|
|
|
|
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/* global ngTouch: false */ |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @ngdoc service |
139
|
|
|
* @name $swipe |
140
|
|
|
* |
141
|
|
|
* @description |
142
|
|
|
* The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe |
143
|
|
|
* behavior, to make implementing swipe-related directives more convenient. |
144
|
|
|
* |
145
|
|
|
* Requires the {@link ngTouch `ngTouch`} module to be installed. |
146
|
|
|
* |
147
|
|
|
* `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`. |
148
|
|
|
* |
149
|
|
|
* # Usage |
150
|
|
|
* The `$swipe` service is an object with a single method: `bind`. `bind` takes an element |
151
|
|
|
* which is to be watched for swipes, and an object with four handler functions. See the |
152
|
|
|
* documentation for `bind` below. |
153
|
|
|
*/ |
154
|
|
|
|
155
|
|
|
ngTouch.factory('$swipe', [function() { |
156
|
|
|
// The total distance in any direction before we make the call on swipe vs. scroll. |
157
|
|
|
var MOVE_BUFFER_RADIUS = 10; |
158
|
|
|
|
159
|
|
|
var POINTER_EVENTS = { |
160
|
|
|
'mouse': { |
161
|
|
|
start: 'mousedown', |
162
|
|
|
move: 'mousemove', |
163
|
|
|
end: 'mouseup' |
164
|
|
|
}, |
165
|
|
|
'touch': { |
166
|
|
|
start: 'touchstart', |
167
|
|
|
move: 'touchmove', |
168
|
|
|
end: 'touchend', |
169
|
|
|
cancel: 'touchcancel' |
170
|
|
|
} |
171
|
|
|
}; |
172
|
|
|
|
173
|
|
|
function getCoordinates(event) { |
174
|
|
|
var originalEvent = event.originalEvent || event; |
175
|
|
|
var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent]; |
176
|
|
|
var e = (originalEvent.changedTouches && originalEvent.changedTouches[0]) || touches[0]; |
177
|
|
|
|
178
|
|
|
return { |
179
|
|
|
x: e.clientX, |
180
|
|
|
y: e.clientY |
181
|
|
|
}; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
function getEvents(pointerTypes, eventType) { |
185
|
|
|
var res = []; |
186
|
|
|
angular.forEach(pointerTypes, function(pointerType) { |
187
|
|
|
var eventName = POINTER_EVENTS[pointerType][eventType]; |
188
|
|
|
if (eventName) { |
189
|
|
|
res.push(eventName); |
190
|
|
|
} |
191
|
|
|
}); |
192
|
|
|
return res.join(' '); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
return { |
196
|
|
|
/** |
197
|
|
|
* @ngdoc method |
198
|
|
|
* @name $swipe#bind |
199
|
|
|
* |
200
|
|
|
* @description |
201
|
|
|
* The main method of `$swipe`. It takes an element to be watched for swipe motions, and an |
202
|
|
|
* object containing event handlers. |
203
|
|
|
* The pointer types that should be used can be specified via the optional |
204
|
|
|
* third argument, which is an array of strings `'mouse'` and `'touch'`. By default, |
205
|
|
|
* `$swipe` will listen for `mouse` and `touch` events. |
206
|
|
|
* |
207
|
|
|
* The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end` |
208
|
|
|
* receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }` and the raw |
209
|
|
|
* `event`. `cancel` receives the raw `event` as its single parameter. |
210
|
|
|
* |
211
|
|
|
* `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is |
212
|
|
|
* watching for `touchmove` or `mousemove` events. These events are ignored until the total |
213
|
|
|
* distance moved in either dimension exceeds a small threshold. |
214
|
|
|
* |
215
|
|
|
* Once this threshold is exceeded, either the horizontal or vertical delta is greater. |
216
|
|
|
* - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow. |
217
|
|
|
* - If the vertical distance is greater, this is a scroll, and we let the browser take over. |
218
|
|
|
* A `cancel` event is sent. |
219
|
|
|
* |
220
|
|
|
* `move` is called on `mousemove` and `touchmove` after the above logic has determined that |
221
|
|
|
* a swipe is in progress. |
222
|
|
|
* |
223
|
|
|
* `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`. |
224
|
|
|
* |
225
|
|
|
* `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling |
226
|
|
|
* as described above. |
227
|
|
|
* |
228
|
|
|
*/ |
229
|
|
|
bind: function(element, eventHandlers, pointerTypes) { |
230
|
|
|
// Absolute total movement, used to control swipe vs. scroll. |
231
|
|
|
var totalX, totalY; |
232
|
|
|
// Coordinates of the start position. |
233
|
|
|
var startCoords; |
234
|
|
|
// Last event's position. |
235
|
|
|
var lastPos; |
236
|
|
|
// Whether a swipe is active. |
237
|
|
|
var active = false; |
238
|
|
|
|
239
|
|
|
pointerTypes = pointerTypes || ['mouse', 'touch']; |
240
|
|
|
element.on(getEvents(pointerTypes, 'start'), function(event) { |
241
|
|
|
startCoords = getCoordinates(event); |
242
|
|
|
active = true; |
243
|
|
|
totalX = 0; |
244
|
|
|
totalY = 0; |
245
|
|
|
lastPos = startCoords; |
246
|
|
|
eventHandlers['start'] && eventHandlers['start'](startCoords, event); |
247
|
|
|
}); |
248
|
|
|
var events = getEvents(pointerTypes, 'cancel'); |
249
|
|
|
if (events) { |
250
|
|
|
element.on(events, function(event) { |
251
|
|
|
active = false; |
252
|
|
|
eventHandlers['cancel'] && eventHandlers['cancel'](event); |
253
|
|
|
}); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
element.on(getEvents(pointerTypes, 'move'), function(event) { |
257
|
|
|
if (!active) return; |
258
|
|
|
|
259
|
|
|
// Android will send a touchcancel if it thinks we're starting to scroll. |
260
|
|
|
// So when the total distance (+ or - or both) exceeds 10px in either direction, |
261
|
|
|
// we either: |
262
|
|
|
// - On totalX > totalY, we send preventDefault() and treat this as a swipe. |
263
|
|
|
// - On totalY > totalX, we let the browser handle it as a scroll. |
264
|
|
|
|
265
|
|
|
if (!startCoords) return; |
266
|
|
|
var coords = getCoordinates(event); |
267
|
|
|
|
268
|
|
|
totalX += Math.abs(coords.x - lastPos.x); |
269
|
|
|
totalY += Math.abs(coords.y - lastPos.y); |
270
|
|
|
|
271
|
|
|
lastPos = coords; |
272
|
|
|
|
273
|
|
|
if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) { |
274
|
|
|
return; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
// One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll. |
278
|
|
|
if (totalY > totalX) { |
279
|
|
|
// Allow native scrolling to take over. |
280
|
|
|
active = false; |
281
|
|
|
eventHandlers['cancel'] && eventHandlers['cancel'](event); |
282
|
|
|
return; |
|
|
|
|
283
|
|
|
} else { |
|
|
|
|
284
|
|
|
// Prevent the browser from scrolling. |
285
|
|
|
event.preventDefault(); |
286
|
|
|
eventHandlers['move'] && eventHandlers['move'](coords, event); |
287
|
|
|
} |
288
|
|
|
}); |
289
|
|
|
|
290
|
|
|
element.on(getEvents(pointerTypes, 'end'), function(event) { |
291
|
|
|
if (!active) return; |
292
|
|
|
active = false; |
293
|
|
|
eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); |
294
|
|
|
}); |
295
|
|
|
} |
296
|
|
|
}; |
297
|
|
|
}]); |
298
|
|
|
|
299
|
|
|
/* global ngTouch: false, |
300
|
|
|
nodeName_: false |
301
|
|
|
*/ |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* @ngdoc directive |
305
|
|
|
* @name ngClick |
306
|
|
|
* @deprecated |
307
|
|
|
* |
308
|
|
|
* @description |
309
|
|
|
* <div class="alert alert-danger"> |
310
|
|
|
* **DEPRECATION NOTICE**: Beginning with Angular 1.5, this directive is deprecated and by default **disabled**. |
311
|
|
|
* The directive will receive no further support and might be removed from future releases. |
312
|
|
|
* If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled} |
313
|
|
|
* function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick). |
314
|
|
|
* To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/) |
315
|
|
|
* gives a good overview. |
316
|
|
|
* </div> |
317
|
|
|
* A more powerful replacement for the default ngClick designed to be used on touchscreen |
318
|
|
|
* devices. Most mobile browsers wait about 300ms after a tap-and-release before sending |
319
|
|
|
* the click event. This version handles them immediately, and then prevents the |
320
|
|
|
* following click event from propagating. |
321
|
|
|
* |
322
|
|
|
* Requires the {@link ngTouch `ngTouch`} module to be installed. |
323
|
|
|
* |
324
|
|
|
* This directive can fall back to using an ordinary click event, and so works on desktop |
325
|
|
|
* browsers as well as mobile. |
326
|
|
|
* |
327
|
|
|
* This directive also sets the CSS class `ng-click-active` while the element is being held |
328
|
|
|
* down (by a mouse click or touch) so you can restyle the depressed element if you wish. |
329
|
|
|
* |
330
|
|
|
* @element ANY |
331
|
|
|
* @param {expression} ngClick {@link guide/expression Expression} to evaluate |
332
|
|
|
* upon tap. (Event object is available as `$event`) |
333
|
|
|
* |
334
|
|
|
* @example |
335
|
|
|
<example module="ngClickExample" deps="angular-touch.js"> |
336
|
|
|
<file name="index.html"> |
337
|
|
|
<button ng-click="count = count + 1" ng-init="count=0"> |
338
|
|
|
Increment |
339
|
|
|
</button> |
340
|
|
|
count: {{ count }} |
341
|
|
|
</file> |
342
|
|
|
<file name="script.js"> |
343
|
|
|
angular.module('ngClickExample', ['ngTouch']); |
344
|
|
|
</file> |
345
|
|
|
</example> |
346
|
|
|
*/ |
347
|
|
|
|
348
|
|
|
var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement', |
349
|
|
|
function($parse, $timeout, $rootElement) { |
350
|
|
|
var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag. |
351
|
|
|
var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers. |
352
|
|
|
var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click |
353
|
|
|
var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks. |
354
|
|
|
|
355
|
|
|
var ACTIVE_CLASS_NAME = 'ng-click-active'; |
356
|
|
|
var lastPreventedTime; |
357
|
|
|
var touchCoordinates; |
358
|
|
|
var lastLabelClickCoordinates; |
359
|
|
|
|
360
|
|
|
|
361
|
|
|
// TAP EVENTS AND GHOST CLICKS |
362
|
|
|
// |
363
|
|
|
// Why tap events? |
364
|
|
|
// Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're |
365
|
|
|
// double-tapping, and then fire a click event. |
366
|
|
|
// |
367
|
|
|
// This delay sucks and makes mobile apps feel unresponsive. |
368
|
|
|
// So we detect touchstart, touchcancel and touchend ourselves and determine when |
369
|
|
|
// the user has tapped on something. |
370
|
|
|
// |
371
|
|
|
// What happens when the browser then generates a click event? |
372
|
|
|
// The browser, of course, also detects the tap and fires a click after a delay. This results in |
373
|
|
|
// tapping/clicking twice. We do "clickbusting" to prevent it. |
374
|
|
|
// |
375
|
|
|
// How does it work? |
376
|
|
|
// We attach global touchstart and click handlers, that run during the capture (early) phase. |
377
|
|
|
// So the sequence for a tap is: |
378
|
|
|
// - global touchstart: Sets an "allowable region" at the point touched. |
379
|
|
|
// - element's touchstart: Starts a touch |
380
|
|
|
// (- touchcancel ends the touch, no click follows) |
381
|
|
|
// - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold |
382
|
|
|
// too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). |
383
|
|
|
// - preventGhostClick() removes the allowable region the global touchstart created. |
384
|
|
|
// - The browser generates a click event. |
385
|
|
|
// - The global click handler catches the click, and checks whether it was in an allowable region. |
386
|
|
|
// - If preventGhostClick was called, the region will have been removed, the click is busted. |
387
|
|
|
// - If the region is still there, the click proceeds normally. Therefore clicks on links and |
388
|
|
|
// other elements without ngTap on them work normally. |
389
|
|
|
// |
390
|
|
|
// This is an ugly, terrible hack! |
391
|
|
|
// Yeah, tell me about it. The alternatives are using the slow click events, or making our users |
392
|
|
|
// deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular |
393
|
|
|
// encapsulates this ugly logic away from the user. |
394
|
|
|
// |
395
|
|
|
// Why not just put click handlers on the element? |
396
|
|
|
// We do that too, just to be sure. If the tap event caused the DOM to change, |
397
|
|
|
// it is possible another element is now in that position. To take account for these possibly |
398
|
|
|
// distinct elements, the handlers are global and care only about coordinates. |
399
|
|
|
|
400
|
|
|
// Checks if the coordinates are close enough to be within the region. |
401
|
|
|
function hit(x1, y1, x2, y2) { |
402
|
|
|
return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
// Checks a list of allowable regions against a click location. |
406
|
|
|
// Returns true if the click should be allowed. |
407
|
|
|
// Splices out the allowable region from the list after it has been used. |
408
|
|
|
function checkAllowableRegions(touchCoordinates, x, y) { |
409
|
|
|
for (var i = 0; i < touchCoordinates.length; i += 2) { |
410
|
|
|
if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) { |
411
|
|
|
touchCoordinates.splice(i, i + 2); |
412
|
|
|
return true; // allowable region |
413
|
|
|
} |
414
|
|
|
} |
415
|
|
|
return false; // No allowable region; bust it. |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
// Global click handler that prevents the click if it's in a bustable zone and preventGhostClick |
419
|
|
|
// was called recently. |
420
|
|
|
function onClick(event) { |
421
|
|
|
if (Date.now() - lastPreventedTime > PREVENT_DURATION) { |
422
|
|
|
return; // Too old. |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
var touches = event.touches && event.touches.length ? event.touches : [event]; |
426
|
|
|
var x = touches[0].clientX; |
427
|
|
|
var y = touches[0].clientY; |
428
|
|
|
// Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label |
429
|
|
|
// and on the input element). Depending on the exact browser, this second click we don't want |
430
|
|
|
// to bust has either (0,0), negative coordinates, or coordinates equal to triggering label |
431
|
|
|
// click event |
432
|
|
|
if (x < 1 && y < 1) { |
433
|
|
|
return; // offscreen |
434
|
|
|
} |
435
|
|
|
if (lastLabelClickCoordinates && |
436
|
|
|
lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) { |
437
|
|
|
return; // input click triggered by label click |
438
|
|
|
} |
439
|
|
|
// reset label click coordinates on first subsequent click |
440
|
|
|
if (lastLabelClickCoordinates) { |
441
|
|
|
lastLabelClickCoordinates = null; |
442
|
|
|
} |
443
|
|
|
// remember label click coordinates to prevent click busting of trigger click event on input |
444
|
|
|
if (nodeName_(event.target) === 'label') { |
445
|
|
|
lastLabelClickCoordinates = [x, y]; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
// Look for an allowable region containing this click. |
449
|
|
|
// If we find one, that means it was created by touchstart and not removed by |
450
|
|
|
// preventGhostClick, so we don't bust it. |
451
|
|
|
if (checkAllowableRegions(touchCoordinates, x, y)) { |
452
|
|
|
return; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
// If we didn't find an allowable region, bust the click. |
456
|
|
|
event.stopPropagation(); |
457
|
|
|
event.preventDefault(); |
458
|
|
|
|
459
|
|
|
// Blur focused form elements |
460
|
|
|
event.target && event.target.blur && event.target.blur(); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
|
464
|
|
|
// Global touchstart handler that creates an allowable region for a click event. |
465
|
|
|
// This allowable region can be removed by preventGhostClick if we want to bust it. |
466
|
|
|
function onTouchStart(event) { |
467
|
|
|
var touches = event.touches && event.touches.length ? event.touches : [event]; |
468
|
|
|
var x = touches[0].clientX; |
469
|
|
|
var y = touches[0].clientY; |
470
|
|
|
touchCoordinates.push(x, y); |
471
|
|
|
|
472
|
|
|
$timeout(function() { |
473
|
|
|
// Remove the allowable region. |
474
|
|
|
for (var i = 0; i < touchCoordinates.length; i += 2) { |
475
|
|
|
if (touchCoordinates[i] == x && touchCoordinates[i + 1] == y) { |
476
|
|
|
touchCoordinates.splice(i, i + 2); |
477
|
|
|
return; |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
}, PREVENT_DURATION, false); |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
// On the first call, attaches some event handlers. Then whenever it gets called, it creates a |
484
|
|
|
// zone around the touchstart where clicks will get busted. |
485
|
|
|
function preventGhostClick(x, y) { |
486
|
|
|
if (!touchCoordinates) { |
487
|
|
|
$rootElement[0].addEventListener('click', onClick, true); |
488
|
|
|
$rootElement[0].addEventListener('touchstart', onTouchStart, true); |
489
|
|
|
touchCoordinates = []; |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
lastPreventedTime = Date.now(); |
493
|
|
|
|
494
|
|
|
checkAllowableRegions(touchCoordinates, x, y); |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
// Actual linking function. |
498
|
|
|
return function(scope, element, attr) { |
499
|
|
|
var clickHandler = $parse(attr.ngClick), |
500
|
|
|
tapping = false, |
501
|
|
|
tapElement, // Used to blur the element after a tap. |
502
|
|
|
startTime, // Used to check if the tap was held too long. |
503
|
|
|
touchStartX, |
504
|
|
|
touchStartY; |
505
|
|
|
|
506
|
|
|
function resetState() { |
507
|
|
|
tapping = false; |
508
|
|
|
element.removeClass(ACTIVE_CLASS_NAME); |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
element.on('touchstart', function(event) { |
512
|
|
|
tapping = true; |
513
|
|
|
tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. |
514
|
|
|
// Hack for Safari, which can target text nodes instead of containers. |
515
|
|
|
if (tapElement.nodeType == 3) { |
516
|
|
|
tapElement = tapElement.parentNode; |
517
|
|
|
} |
518
|
|
|
|
519
|
|
|
element.addClass(ACTIVE_CLASS_NAME); |
520
|
|
|
|
521
|
|
|
startTime = Date.now(); |
522
|
|
|
|
523
|
|
|
// Use jQuery originalEvent |
524
|
|
|
var originalEvent = event.originalEvent || event; |
525
|
|
|
var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent]; |
526
|
|
|
var e = touches[0]; |
527
|
|
|
touchStartX = e.clientX; |
528
|
|
|
touchStartY = e.clientY; |
529
|
|
|
}); |
530
|
|
|
|
531
|
|
|
element.on('touchcancel', function(event) { |
532
|
|
|
resetState(); |
533
|
|
|
}); |
534
|
|
|
|
535
|
|
|
element.on('touchend', function(event) { |
536
|
|
|
var diff = Date.now() - startTime; |
537
|
|
|
|
538
|
|
|
// Use jQuery originalEvent |
539
|
|
|
var originalEvent = event.originalEvent || event; |
540
|
|
|
var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ? |
541
|
|
|
originalEvent.changedTouches : |
542
|
|
|
((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]); |
543
|
|
|
var e = touches[0]; |
544
|
|
|
var x = e.clientX; |
545
|
|
|
var y = e.clientY; |
546
|
|
|
var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2)); |
547
|
|
|
|
548
|
|
|
if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { |
549
|
|
|
// Call preventGhostClick so the clickbuster will catch the corresponding click. |
550
|
|
|
preventGhostClick(x, y); |
551
|
|
|
|
552
|
|
|
// Blur the focused element (the button, probably) before firing the callback. |
553
|
|
|
// This doesn't work perfectly on Android Chrome, but seems to work elsewhere. |
554
|
|
|
// I couldn't get anything to work reliably on Android Chrome. |
555
|
|
|
if (tapElement) { |
556
|
|
|
tapElement.blur(); |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
if (!angular.isDefined(attr.disabled) || attr.disabled === false) { |
560
|
|
|
element.triggerHandler('click', [event]); |
561
|
|
|
} |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
resetState(); |
565
|
|
|
}); |
566
|
|
|
|
567
|
|
|
// Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click |
568
|
|
|
// something else nearby. |
569
|
|
|
element.onclick = function(event) { }; |
570
|
|
|
|
571
|
|
|
// Actual click handler. |
572
|
|
|
// There are three different kinds of clicks, only two of which reach this point. |
573
|
|
|
// - On desktop browsers without touch events, their clicks will always come here. |
574
|
|
|
// - On mobile browsers, the simulated "fast" click will call this. |
575
|
|
|
// - But the browser's follow-up slow click will be "busted" before it reaches this handler. |
576
|
|
|
// Therefore it's safe to use this directive on both mobile and desktop. |
577
|
|
|
element.on('click', function(event, touchend) { |
578
|
|
|
scope.$apply(function() { |
579
|
|
|
clickHandler(scope, {$event: (touchend || event)}); |
580
|
|
|
}); |
581
|
|
|
}); |
582
|
|
|
|
583
|
|
|
element.on('mousedown', function(event) { |
584
|
|
|
element.addClass(ACTIVE_CLASS_NAME); |
585
|
|
|
}); |
586
|
|
|
|
587
|
|
|
element.on('mousemove mouseup', function(event) { |
588
|
|
|
element.removeClass(ACTIVE_CLASS_NAME); |
589
|
|
|
}); |
590
|
|
|
|
591
|
|
|
}; |
592
|
|
|
}]; |
593
|
|
|
|
594
|
|
|
/* global ngTouch: false */ |
595
|
|
|
|
596
|
|
|
/** |
597
|
|
|
* @ngdoc directive |
598
|
|
|
* @name ngSwipeLeft |
599
|
|
|
* |
600
|
|
|
* @description |
601
|
|
|
* Specify custom behavior when an element is swiped to the left on a touchscreen device. |
602
|
|
|
* A leftward swipe is a quick, right-to-left slide of the finger. |
603
|
|
|
* Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag |
604
|
|
|
* too. |
605
|
|
|
* |
606
|
|
|
* To disable the mouse click and drag functionality, add `ng-swipe-disable-mouse` to |
607
|
|
|
* the `ng-swipe-left` or `ng-swipe-right` DOM Element. |
608
|
|
|
* |
609
|
|
|
* Requires the {@link ngTouch `ngTouch`} module to be installed. |
610
|
|
|
* |
611
|
|
|
* @element ANY |
612
|
|
|
* @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate |
613
|
|
|
* upon left swipe. (Event object is available as `$event`) |
614
|
|
|
* |
615
|
|
|
* @example |
616
|
|
|
<example module="ngSwipeLeftExample" deps="angular-touch.js"> |
617
|
|
|
<file name="index.html"> |
618
|
|
|
<div ng-show="!showActions" ng-swipe-left="showActions = true"> |
619
|
|
|
Some list content, like an email in the inbox |
620
|
|
|
</div> |
621
|
|
|
<div ng-show="showActions" ng-swipe-right="showActions = false"> |
622
|
|
|
<button ng-click="reply()">Reply</button> |
623
|
|
|
<button ng-click="delete()">Delete</button> |
624
|
|
|
</div> |
625
|
|
|
</file> |
626
|
|
|
<file name="script.js"> |
627
|
|
|
angular.module('ngSwipeLeftExample', ['ngTouch']); |
628
|
|
|
</file> |
629
|
|
|
</example> |
630
|
|
|
*/ |
631
|
|
|
|
632
|
|
|
/** |
633
|
|
|
* @ngdoc directive |
634
|
|
|
* @name ngSwipeRight |
635
|
|
|
* |
636
|
|
|
* @description |
637
|
|
|
* Specify custom behavior when an element is swiped to the right on a touchscreen device. |
638
|
|
|
* A rightward swipe is a quick, left-to-right slide of the finger. |
639
|
|
|
* Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag |
640
|
|
|
* too. |
641
|
|
|
* |
642
|
|
|
* Requires the {@link ngTouch `ngTouch`} module to be installed. |
643
|
|
|
* |
644
|
|
|
* @element ANY |
645
|
|
|
* @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate |
646
|
|
|
* upon right swipe. (Event object is available as `$event`) |
647
|
|
|
* |
648
|
|
|
* @example |
649
|
|
|
<example module="ngSwipeRightExample" deps="angular-touch.js"> |
650
|
|
|
<file name="index.html"> |
651
|
|
|
<div ng-show="!showActions" ng-swipe-left="showActions = true"> |
652
|
|
|
Some list content, like an email in the inbox |
653
|
|
|
</div> |
654
|
|
|
<div ng-show="showActions" ng-swipe-right="showActions = false"> |
655
|
|
|
<button ng-click="reply()">Reply</button> |
656
|
|
|
<button ng-click="delete()">Delete</button> |
657
|
|
|
</div> |
658
|
|
|
</file> |
659
|
|
|
<file name="script.js"> |
660
|
|
|
angular.module('ngSwipeRightExample', ['ngTouch']); |
661
|
|
|
</file> |
662
|
|
|
</example> |
663
|
|
|
*/ |
664
|
|
|
|
665
|
|
|
function makeSwipeDirective(directiveName, direction, eventName) { |
666
|
|
|
ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) { |
667
|
|
|
// The maximum vertical delta for a swipe should be less than 75px. |
668
|
|
|
var MAX_VERTICAL_DISTANCE = 75; |
669
|
|
|
// Vertical distance should not be more than a fraction of the horizontal distance. |
670
|
|
|
var MAX_VERTICAL_RATIO = 0.3; |
671
|
|
|
// At least a 30px lateral motion is necessary for a swipe. |
672
|
|
|
var MIN_HORIZONTAL_DISTANCE = 30; |
673
|
|
|
|
674
|
|
|
return function(scope, element, attr) { |
675
|
|
|
var swipeHandler = $parse(attr[directiveName]); |
676
|
|
|
|
677
|
|
|
var startCoords, valid; |
678
|
|
|
|
679
|
|
|
function validSwipe(coords) { |
680
|
|
|
// Check that it's within the coordinates. |
681
|
|
|
// Absolute vertical distance must be within tolerances. |
682
|
|
|
// Horizontal distance, we take the current X - the starting X. |
683
|
|
|
// This is negative for leftward swipes and positive for rightward swipes. |
684
|
|
|
// After multiplying by the direction (-1 for left, +1 for right), legal swipes |
685
|
|
|
// (ie. same direction as the directive wants) will have a positive delta and |
686
|
|
|
// illegal ones a negative delta. |
687
|
|
|
// Therefore this delta must be positive, and larger than the minimum. |
688
|
|
|
if (!startCoords) return false; |
689
|
|
|
var deltaY = Math.abs(coords.y - startCoords.y); |
690
|
|
|
var deltaX = (coords.x - startCoords.x) * direction; |
691
|
|
|
return valid && // Short circuit for already-invalidated swipes. |
692
|
|
|
deltaY < MAX_VERTICAL_DISTANCE && |
693
|
|
|
deltaX > 0 && |
694
|
|
|
deltaX > MIN_HORIZONTAL_DISTANCE && |
695
|
|
|
deltaY / deltaX < MAX_VERTICAL_RATIO; |
696
|
|
|
} |
697
|
|
|
|
698
|
|
|
var pointerTypes = ['touch']; |
699
|
|
|
if (!angular.isDefined(attr['ngSwipeDisableMouse'])) { |
700
|
|
|
pointerTypes.push('mouse'); |
701
|
|
|
} |
702
|
|
|
$swipe.bind(element, { |
703
|
|
|
'start': function(coords, event) { |
704
|
|
|
startCoords = coords; |
705
|
|
|
valid = true; |
706
|
|
|
}, |
707
|
|
|
'cancel': function(event) { |
708
|
|
|
valid = false; |
709
|
|
|
}, |
710
|
|
|
'end': function(coords, event) { |
711
|
|
|
if (validSwipe(coords)) { |
712
|
|
|
scope.$apply(function() { |
713
|
|
|
element.triggerHandler(eventName); |
714
|
|
|
swipeHandler(scope, {$event: event}); |
715
|
|
|
}); |
716
|
|
|
} |
717
|
|
|
} |
718
|
|
|
}, pointerTypes); |
719
|
|
|
}; |
720
|
|
|
}]); |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
// Left is negative X-coordinate, right is positive. |
724
|
|
|
makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft'); |
725
|
|
|
makeSwipeDirective('ngSwipeRight', 1, 'swiperight'); |
726
|
|
|
|
727
|
|
|
|
728
|
|
|
|
729
|
|
|
})(window, window.angular); |
730
|
|
|
|