UNPKG

14.5 kBJavaScriptView Raw
1;(function($) {
2 'use strict';
3 var undefined;
4 var slice = Array.prototype.slice;
5 var isFunction = $.isFunction;
6 var isString = function(obj) {
7 return typeof obj === 'string';
8 };
9 var isObject = function(obj) {
10 return typeof obj === 'object';
11 };
12 var returnFalse = function() {
13 return false;
14 };
15
16 function calculateAngle(x1, x2, y1, y2) {
17 var x = x1 - x2;
18 var y = y1 - y2;
19 var r = Math.atan2(y, x);
20 var angle = Math.round(r * 180 / Math.PI);
21 if (angle < 0) {
22 angle = 360 - Math.abs(angle);
23 }
24 return angle;
25 }
26
27 function calculateDirection(x1, x2, y1, y2) {
28 var angle = calculateAngle(x1, x2, y1, y2);
29 if ((angle <= 45 && angle >= 0) || (angle <= 360 && angle >= 315)) {
30 return 'left';
31 } else if (angle >= 135 && angle <= 225) {
32 return 'right';
33 } else if (angle > 45 && angle < 135) {
34 return 'down';
35 } else {
36 return 'up';
37 }
38 }
39
40 var PLUGIN_NS = '_TOUCH_';
41 var PLUGIN_EVENT_NS = '.' + PLUGIN_NS + 'EVENT_';
42 var SUPPORTS_TOUCH = 'ontouchstart' in window;
43 var SUPPORTS_POINTER_IE10 = window.navigator.msPointerEnabled && !window.navigator.pointerEnabled;
44 var SUPPORTS_POINTER = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
45 var useTouchEvents = SUPPORTS_TOUCH || SUPPORTS_POINTER;
46 var START_EV = (useTouchEvents ? (SUPPORTS_POINTER ? (SUPPORTS_POINTER_IE10 ? 'MSPointerDown' : 'pointerdown') : 'touchstart') : 'mousedown') + PLUGIN_EVENT_NS;
47 var MOVE_EV = (useTouchEvents ? (SUPPORTS_POINTER ? (SUPPORTS_POINTER_IE10 ? 'MSPointerMove' : 'pointermove') : 'touchmove') : 'mousemove') + PLUGIN_EVENT_NS;
48 var END_EV = (useTouchEvents ? (SUPPORTS_POINTER ? (SUPPORTS_POINTER_IE10 ? 'MSPointerUp' : 'pointerup') : 'touchend') : 'mouseup') + PLUGIN_EVENT_NS;
49 var CANCEL_EV = (SUPPORTS_POINTER ? (SUPPORTS_POINTER_IE10 ? 'MSPointerCancel' : 'pointercancel') : 'touchcancel') + PLUGIN_EVENT_NS;
50
51 var defaults = {
52 fingers: 1,
53 threshold: 75,
54 longTapThreshold: 500,
55 doubleTapThreshold: 200,
56 excludedElements: 'label, button, input, select, textarea, .noTouch',
57 pageScroll: true,
58 swipeMove: null
59 };
60
61 var _cache = {};
62 $.fn.touch = function(options) {
63 var opts = {};
64 var cid;
65 if (isObject(options)) {
66 $.extend(opts, $.fn.touch.defaults, options);
67 this.each(function () {
68 cid = _tid;
69 _cache[cid] = opts;
70 this._cid = cid;
71 _tid++;
72 });
73 }
74 return this;
75 };
76
77 $.fn.touch.defaults = defaults;
78
79 var singleTapTimeout = null;
80 var holdTimeout = null;
81
82 var _tid = 1;
83 var handlers = {};
84
85 function isTouchEvent(event) {
86 return /^(tap|doubleTap|longTap|swipe|swipeLeft|swipeRight|swipeUp|swipeDown)$/.test(parse(event).e);
87 }
88
89 function tid(element) {
90 return element._tid || (element._tid = _tid++);
91 }
92
93 function parse(event) {
94 var parts = ('' + event).split('.')
95 return {
96 e: parts[0],
97 ns: parts.slice(1).sort().join(' ')
98 }
99 }
100
101 function matcherFor(ns) {
102 return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)');
103 }
104
105 function removeTouch(element, event, selector, callback) {
106 var set = handlers[tid(element)];
107 var len = set.length;
108 if (!(Array.isArray(set) && len)) {
109 return;
110 }
111
112 if (!isString(selector) && !isFunction(callback) && callback !== false) {
113 callback = selector;
114 selector = undefined;
115 }
116 if (callback === false) {
117 callback = returnFalse;
118 }
119
120 event = parse(event);
121 if (event.ns) {
122 var matcher = matcherFor(event.ns);
123 }
124
125 var i = 0;
126 for (i; i < len;) {
127 var handler = set[i];
128 if (handler && handler.e === event.e && (!event.ns || matcher.test(handler.ns)) && (!callback || tid(handler.fn) === tid(callback)) && (!selector || handler.sel == selector)) {
129 set.splice(i, 1);
130 len--;
131 } else {
132 i++;
133 }
134 }
135 }
136
137 function Touch(element, event, selector, data, callback) {
138 if (!isString(selector) && !isFunction(callback) && callback !== false) {
139 callback = data;
140 data = selector;
141 selector = undefined;
142 }
143 if (callback === undefined || data === false) {
144 callback = data;
145 data = undefined;
146 }
147 if (callback === false) {
148 callback = returnFalse;
149 }
150
151 var handler = parse(event);
152 handler.fn = callback;
153 handler.callback = callback;
154
155 if (selector) {
156 handler.callback = function(e) {
157 var match = $(e.target).closest(selector, element).get(0);
158 if (match && match !== element) {
159 return callback.apply(match, arguments);
160 }
161 };
162 handler.sel = selector;
163 }
164
165 var hasTouch = !!element._tid;
166
167 var id = tid(element);
168 var set = (handlers[id] || (handlers[id] = []));
169 set.push(handler);
170
171 if (isObject(Touch.instance) && hasTouch) {
172 return Touch.instance;
173 }
174
175 this.handler = set;
176 this.el = element;
177 this.$el = $(element);
178 this.options = _cache[element._cid] || $.fn.touch.defaults;
179 this.$el.on(START_EV, $.proxy(this.touchStart, this)).on(CANCEL_EV, $.proxy(this.touchCancel, this));
180
181 Touch.instance = this;
182 }
183
184 Touch.prototype = {
185 _isTouch: false,
186 _doubleTapTime: 0,
187 touch: {},
188 touchStart: function(e) {
189 var _this = this;
190 var options = this.options;
191 if (this._isTouch || !this.handler.length || $(e.target).closest(options.excludedElements, this.el).length) {
192 return;
193 }
194
195 this._status = 'start';
196
197 var touches = e.touches;
198 var evt = touches ? touches[0] : e;
199 var fingerCount = 0;
200
201 if (touches) {
202 fingerCount = touches.length;
203 }
204
205 if (!options.pageScroll || !touches) {
206 e.preventDefault();
207 }
208
209 this.createTouchData(evt);
210
211 if (!touches || (fingerCount === options.fingers || options.fingers === 'all')) {
212 if (this.hasEvent('longTap')) {
213 holdTimeout = setTimeout(function() {
214 _this.trigger('longTap', e);
215 }, options.longTapThreshold);
216 }
217 this.setTouchProgress(true);
218 } else {
219 this._status = 'cancel';
220 this.triggerHandler(e);
221 return false;
222 }
223 },
224 touchMove: function(e) {
225 if (this._status === 'end' || this._status === 'cancel') {
226 return;
227 }
228 if (this.hasEvent('longTap')) {
229 holdTimeout && clearTimeout(holdTimeout);
230 holdTimeout = null;
231 return;
232 }
233 if (!this.options.pageScroll) {
234 e.preventDefault();
235 }
236 var touches = e.touches;
237 var evt = touches ? touches[0] : e;
238 this.updateTouchData(evt);
239 this.touch.now = e.timeStamp;
240 if (this.hasSwipe()) {
241 this._status = 'move';
242 if (this.isSwipe()) {
243 e.preventDefault();
244 }
245 if (isFunction(this.options.swipeMove)) {
246 var direction = this.getDirection();
247 var distance = Math.abs((direction === 'up' || direction === 'down') ? this.touch.y2 - this.touch.y1 : this.touch.x2 - this.touch.x1);
248 var duration = this.getDuration();
249 this.$el.trigger('swipeMove', [direction, distance, duration, e]);
250 this.options.swipeMove.call(this.el, e, direction, distance, duration);
251 }
252 } else {
253 this._status = 'cancel';
254 this.triggerHandler(e);
255 }
256 },
257 touchEnd: function(e) {
258 if (e.touches && e.touches.length) {
259 return true;
260 }
261 this.touch.now = e.timeStamp;
262 this._status = this._status === 'move' ? 'end' : 'cancel';
263
264 this.triggerHandler(e);
265
266 this.touchCancel();
267 },
268 touchCancel: function() {
269 this.touch = {};
270 this.setTouchProgress(false);
271 },
272 triggerHandler: function(e) {
273 var _this = this;
274 if (this._status === 'end' && this.isSwipe() && this.hasSwipe()) {
275 e.preventDefault();
276 var direction = this.getDirection();
277 this.trigger($.camelCase('swipe-' + direction), e);
278 if (this.hasEvent('swipe')) {
279 this.trigger('swipe', e, direction);
280 }
281 } else if (this._status === 'cancel' || this._status === 'end') {
282 holdTimeout && clearTimeout(holdTimeout);
283 singleTapTimeout && clearTimeout(singleTapTimeout);
284 holdTimeout = singleTapTimeout = null;
285
286 if (this.isDoubleTap()) {
287 this._doubleTapTime = null;
288 this.trigger('doubleTap', e);
289 } else if (this.isLongTap()) {
290 this._doubleTapTime = null;
291 this.trigger('longTap', e);
292 } else if (this.isTap()) {
293 if (this.hasEvent('doubleTap') && !this.isDoubleTap()) {
294 this._doubleTapTime = this.touch.now;
295 singleTapTimeout = setTimeout(function() {
296 _this._doubleTapTime = null;
297 }, this.options.doubleTapThreshold);
298 } else {
299 this._doubleTapTime = null;
300 }
301 if (this.hasEvent('tap')) {
302 this.trigger('tap', e);
303 }
304 }
305 this.touchCancel();
306 }
307 },
308 trigger: function(event, evt) {
309 var _this = this;
310 var args = slice.call(arguments, 1);
311 this.$el.trigger(event, evt);
312 this.handler.forEach(function (handler) {
313 handler && handler.e === event && handler.callback.apply(_this.el, args);
314 });
315 },
316 hasEvent: function(event) {
317 var handler = this.handler;
318 var ret = false;
319 var reg = new RegExp('^(' + event + ')$');
320 for (var i = handler.length - 1; i >= 0; i--) {
321 if (reg.test(handler[i].e)) {
322 ret = true;
323 break;
324 }
325 }
326 return ret;
327 },
328 hasSwipe: function() {
329 return this.hasEvent('swipe|swipeLeft|swipeRight|swipeUp|swipeDown');
330 },
331 isSwipe: function() {
332 return this.getDistance() >= this.options.threshold;
333 },
334 isDoubleTap: function() {
335 if (this._doubleTapTime === null) {
336 return false;
337 }
338 return this.hasEvent('doubleTap') && ((this.touch.now - this._doubleTapTime) <= this.options.doubleTapThreshold);
339 },
340 hasTap: function() {
341 return this.hasEvent('tap|doubleTap');
342 },
343 isTap: function() {
344 return this.hasTap() && this.getDistance() < this.options.threshold;
345 },
346 isLongTap: function() {
347 return this.hasEvent('longTap') && this.getDuration() > this.options.longTapThreshold && this.getDistance() < 10;
348 },
349 createTouchData: function(e) {
350 var touch = {};
351 touch.x1 = touch.x2 = e.pageX || e.clientX;
352 touch.y1 = touch.y2 = e.pageY || e.clientY;
353 touch.now = touch.last = new Date().getTime();
354 this.touch = touch;
355 return touch;
356 },
357 updateTouchData: function(e) {
358 this.touch.x2 = e.pageX || e.clientX;
359 this.touch.y2 = e.pageY || e.clientY;
360 },
361 getDirection: function() {
362 var touch = this.touch;
363 return calculateDirection(touch.x1, touch.x2, touch.y1, touch.y2);
364 },
365 getDistance: function() {
366 var touch = this.touch;
367 return Math.round(Math.sqrt(Math.pow(touch.x2 - touch.x1, 2) + Math.pow(touch.y2 - touch.y1, 2)));
368 },
369 getDuration: function() {
370 var touch = this.touch;
371 return touch.now - touch.last;
372 },
373 setTouchProgress: function(isTouch) {
374 if (isTouch) {
375 this.$el.on(MOVE_EV, $.proxy(this.touchMove, this)).on(END_EV, $.proxy(this.touchEnd, this));
376 } else {
377 this.$el.off(MOVE_EV, $.proxy(this.touchMove, this)).off(END_EV, $.proxy(this.touchEnd, this));
378 }
379 this._isTouch = isTouch;
380 }
381 };
382
383 var _on = $.fn.on;
384 var _off = $.fn.off;
385
386 $.fn.on = function(event, selector, data, callback) {
387 if (event && !isString(event)) {
388 return _on.apply(this, arguments);
389 }
390
391 if (isTouchEvent(event)) {
392 this.each(function() {
393 new Touch(this, event, selector, data, callback);
394 });
395 return this;
396 } else {
397 return _on.apply(this, arguments);
398 }
399 };
400
401 $.fn.off = function(event, selector, callback) {
402 if (event && !isString(event)) {
403 return _off.apply(this, arguments);
404 }
405
406 if (isTouchEvent(event)) {
407 this.each(function() {
408 removeTouch(this, event, selector, callback);
409 });
410 return this;
411 } else {
412 if (!event) {
413 this.each(function() {
414 handlers[tid(this)] = [];
415 });
416 }
417 return _off.apply(this, arguments);
418 }
419 };
420
421})(window.Zepto || window.jQuery);