-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjquery.touchdragv.coffee
553 lines (434 loc) · 13.8 KB
/
jquery.touchdragv.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# encapsulate plugin
do ($=jQuery, window=window, document=document) ->
$document = $(document)
ns = $.TouchdragvNs = {}
# ============================================================
# pageX/Y normalizer
ns.normalizeXY = (event) ->
res = {}
orig = event.originalEvent
if orig.changedTouches?
# if it was a touch event
touch = orig.changedTouches[0]
res.x = touch.pageX
res.y = touch.pageY
else
# jQuery cannnot handle pointerevents, so check orig.pageX/Y too.
res.x = event.pageX or orig.pageX
res.y = event.pageY or orig.pageY
return res
# ============================================================
# detect / normalize event names
ns.support = {}
ns.ua = {}
ns.support.addEventListener = 'addEventListener' of document
# from Modernizr
ns.support.touch = 'ontouchend' of document
# http://msdn.microsoft.com/en-us/library/ie/hh673557(v=vs.85).aspx
ns.support.mspointer = window.navigator.msPointerEnabled or false
# http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx
ns.ua.win8orhigh = do ->
# windows browsers has str like "Windows NT 6.2" in its UA
# Win8 UAs' version is "6.2"
# browsers above this version may has touch events.
ua = navigator.userAgent
matched = ua.match(/Windows NT ([\d\.]+)/)
return false unless matched
version = matched[1] * 1
return false if version < 6.2
return true
# returns related eventNameSet
ns.getEventNameSet = (eventName) ->
res = {}
switch eventName
when 'touchstart'
res.move = 'touchmove'
res.end = 'touchend'
when 'mousedown'
res.move = 'mousemove'
res.end = 'mouseup'
when 'MSPointerDown'
res.move = 'MSPointerMove'
res.end = 'MSPointerUp'
when 'pointerdown'
res.move = 'pointermove'
res.end = 'pointerup'
return res
# ============================================================
# top value getter
ns.getTopPx = ($el) ->
l = $el.css 'top'
if l is 'auto'
l = 0
else
l = (l.replace /px/, '') * 1
return l
# ============================================================
# gesture handler
ns.startWatchGestures = do ->
initDone = false
init = ->
initDone = true
$document.bind 'gesturestart', ->
ns.whileGesture = true
$document.bind 'gestureend', ->
ns.whileGesture = false
->
return if @initDone
init()
# ============================================================
# event module
class ns.Event
on: (ev, callback) ->
@_callbacks = {} unless @_callbacks?
evs = ev.split(' ')
for name in evs
@_callbacks[name] or= []
@_callbacks[name].push(callback)
return this
once: (ev, callback) ->
@on ev, ->
@off(ev, arguments.callee)
callback.apply(@, arguments)
return this
trigger: (args...) ->
ev = args.shift()
list = @_callbacks?[ev]
return unless list
for callback in list
if callback.apply(@, args) is false
break
return this
off: (ev, callback) ->
unless ev
@_callbacks = {}
return this
list = @_callbacks?[ev]
return this unless list
unless callback
delete @_callbacks[ev]
return this
for cb, i in list when cb is callback
list = list.slice()
list.splice(i, 1)
@_callbacks[ev] = list
break
return this
# ============================================================
# OneDrag
class ns.OneDrag extends ns.Event
constructor: ->
@_scrollDirectionDecided = false
applyTouchStart: (touchStartEvent) ->
coords = ns.normalizeXY touchStartEvent
@startPageX = coords.x
@startPageY = coords.y
return this
applyTouchMove: (touchMoveEvent) ->
coords = ns.normalizeXY touchMoveEvent
triggerEvent = =>
diffY = coords.y - @startPageY
@trigger 'dragmove', { y: diffY }
if @_scrollDirectionDecided
triggerEvent()
else
distX = Math.abs(coords.x - @startPageX)
distY = Math.abs(coords.y - @startPageY)
if (distX > 5) or (distY > 5)
@_scrollDirectionDecided = true
if distX > 5
@trigger 'xscrolldetected'
else if distY > 5
@trigger 'yscrolldetected'
return this
destroy: ->
@off()
return this
# ============================================================
# TouchdragvEl
class ns.TouchdragvEl extends ns.Event
defaults:
inner: '> *' # selector
backanim_duration: 250
backanim_easing: 'swing'
beforefirstrefresh: null # fn
triggerrefreshimmediately: true
tweakinnerpositionstyle: false
constructor: (@$el, options) ->
@el = @$el[0]
@options = $.extend {}, @defaults, options
@disabled = false
ns.startWatchGestures()
@_handlePointerEvents()
@_prepareEls()
@_eventify()
@refresh() if @options.triggerrefreshimmediately
refresh: ->
@_calcMinMaxTop()
@_handleTooNarrow()
@_handleInnerOver()
unless @_firstRefreshDone
if @options.beforefirstrefresh
@options.beforefirstrefresh this
@trigger 'firstrefresh', this
@_firstRefreshDone = true
@trigger 'refresh', this
return this
_handlePointerEvents: ->
return @ unless ns.support.mspointer
@el.style.msTouchAction = 'none'
return this
_prepareEls: ->
@$inner = @$el.find @options.inner
if @options.tweakinnerpositionstyle
@$inner.css
position: 'relative'
return this
_calcMinMaxTop: ->
@_maxTop = 0
@_minTop = -(@$inner.outerHeight() - @$el.innerHeight())
return this
_eventify: ->
eventNames = 'pointerdown MSPointerDown touchstart mousedown'
@$el.bind eventNames, @_handleTouchStart
if ns.support.addEventListener
@el.addEventListener 'click', $.noop , true
return this
_handleClickToIgnore: (event) =>
event.stopPropagation()
event.preventDefault()
return this
_handleTouchStart: (event) =>
return this if @disabled
return this if @_whileDrag
# It'll be bugged if gestured
return this if ns.whileGesture
# prevent if mouseclick
event.preventDefault() if event.type is 'mousedown'
# detect eventNameSet then save
@_currentEventNameSet = ns.getEventNameSet event.type
@_whileDrag = true
@_slidecanceled = false
@_shouldSlideInner = false
# handle drag via OneDrag class
d = @_currentDrag = new ns.OneDrag
d.on 'xscrolldetected', =>
@_whileDrag = false
@_slidecanceled = true
@trigger 'slidecancel'
d.on 'yscrolldetected', =>
@_shouldSlideInner = true
@trigger 'dragstart'
# ignore click if drag
@$el.delegate 'a', 'click', @_handleClickToIgnore
d.on 'dragmove', (data) =>
@trigger 'drag'
@_moveInner data.y
@_innerStartTop = ns.getTopPx @$inner
d.applyTouchStart event
# Let's observe move/end now
$document.bind @_currentEventNameSet.move, @_handleTouchMove
$document.bind @_currentEventNameSet.end, @_handleTouchEnd
return this
_handleTouchMove: (event) =>
return this unless @_whileDrag
return this if ns.whileGesture
@_currentDrag.applyTouchMove event
if @_shouldSlideInner
event.preventDefault()
event.stopPropagation()
return this
_handleTouchEnd: (event) =>
@_whileDrag = false
# unbind everything about this drag
$document.unbind @_currentEventNameSet.move, @_handleTouchMove
$document.unbind @_currentEventNameSet.end, @_handleTouchEnd
@_currentDrag.destroy()
# we don't need nameset anymore
@_currentEventNameSet = null
@trigger 'dragend' unless @_slidecanceled
# enable click again
setTimeout =>
@$el.undelegate 'a', 'click', @_handleClickToIgnore
, 10
# if inner was over, fit it to inside.
@_handleInnerOver true
return this
_moveInner: (y) ->
top = @_innerStartTop + y
# slow down if over
if (top > @_maxTop)
top = @_maxTop + ((top - @_maxTop) / 3)
else if (top < @_minTop)
top = @_minTop + ((top - @_minTop) / 3)
@$inner.css 'top', top
data = { top: top }
@trigger 'move', data
return this
_handleInnerOver: (invokeEndEvent = false) ->
return this if @isInnerTooNarrow()
triggerEvent = =>
@trigger 'moveend' if invokeEndEvent
to = null
top = @currentSlideTop()
# check if top is over
overMax = top > @_maxTop
belowMin = top < @_minTop
unless overMax or belowMin
triggerEvent()
return this
# normalize top
to = @_maxTop if overMax
to = @_minTop if belowMin
# then do slide
@slide to, true, =>
triggerEvent()
return this
_handleTooNarrow: ->
if @isInnerTooNarrow()
@disable()
@$inner.css 'top', 0
else
@enable()
return this
isInnerTooNarrow: ->
elH = @$el.height()
innerH = @$inner.height()
innerH <= elH
disable: ->
@disabled = true
return this
enable: ->
@disabled = false
return this
slide: (val, animate=false, callback) ->
val = @_maxTop if val > @_maxTop
val = @_minTop if val < @_minTop
d = @options.backanim_duration
e = @options.backanim_easing
to =
top: val
return $.Deferred (defer) =>
@trigger 'beforeslide'
onDone = =>
@trigger 'afterslide'
callback?()
defer.resolve()
if animate
@$inner.stop().animate to, d, e, => onDone()
else
@$inner.stop().css to
onDone()
.promise()
currentSlideTop: ->
ns.getTopPx @$inner
updateInnerHeight: (val) ->
@$inner.height val
return this
# ============================================================
# TouchdragvFitty
class ns.TouchdragvFitty extends ns.Event
defaults:
item: null # selector
beforefirstfresh: null # fn
startindex: 0
triggerrefreshimmediately: true
constructor: (@$el, options) ->
@options = $.extend {}, @defaults, options
@currentIndex = @options.startindex
@_preparetouchdragv()
@refresh() if @options.triggerrefreshimmediately
_preparetouchdragv: ->
options = $.extend {}, @options
options.triggerrefreshimmediately = false
options.beforefirstrefresh = (touchdragv) =>
touchdragv.once 'firstrefresh', =>
@options.beforefirstrefresh?(this)
@trigger 'firstrefresh', this
@_firstRefreshDone = true
touchdragv.on 'refresh', => @trigger 'refresh'
touchdragv.on 'slidecancel', => @trigger 'slidecancel'
touchdragv.on 'dragstart', => @trigger 'dragstart'
touchdragv.on 'drag', => @trigger 'drag'
touchdragv.on 'dragend', => @trigger 'dragend'
touchdragv.on 'moveend', =>
slidedDistance = -touchdragv.currentSlideTop()
itemH = @$el.innerHeight()
nextIndex = null
caliculatedIndex = slidedDistance / itemH
if caliculatedIndex < @currentIndex
nextIndex = @currentIndex - 1
else if caliculatedIndex > @currentIndex
nextIndex = @currentIndex + 1
unless nextIndex is null
@updateIndex nextIndex
@adjustToFit itemH, true
@_touchdragv = new ns.TouchdragvEl @$el, options
return this
updateIndex: (index) ->
unless 0 <= index < @$items.length
return false
lastIndex = @currentIndex
@currentIndex = index
if lastIndex isnt index
data =
index: @currentIndex
@trigger 'indexchange', data
return true
refresh: ->
@$items = @$el.find @options.item
itemH = @_itemHeight = @$el.innerHeight()
innerH = (itemH * @$items.length)
@_touchdragv.updateInnerHeight innerH
@$items.height itemH
@_touchdragv.refresh()
@adjustToFit itemH
return this
adjustToFit: (itemHeight, animate=false, callback) ->
itemHeight = @$items.height() unless itemHeight?
return $.Deferred (defer) =>
i = @currentIndex
top_after = -itemHeight * i
top_pre = @_touchdragv.currentSlideTop()
if top_after is top_pre
defer.resolve()
return this
@trigger 'slidestart' unless @_sliding
@_sliding = true
@_touchdragv.slide top_after, animate, =>
@_sliding = false
data =
index: @currentIndex
@trigger 'slideend', data
callback?()
defer.resolve()
.promise()
to: (index, animate=false) ->
updated = @updateIndex (index)
return $.Deferred (defer) =>
if updated
@adjustToFit null, animate, => defer.resolve()
else
@trigger 'invalidindexrequested'
defer.resolve()
.promise()
next: (animate=false) ->
return @to (@currentIndex + 1), animate
prev: (animate=false) ->
return @to (@currentIndex - 1), animate
# ============================================================
# bridge to plugin
$.fn.touchdragv = (options) ->
@each (i, el) ->
$el = $(el)
instance = new ns.TouchdragvEl $el, options
$el.data 'touchdragv', instance
return
$.fn.touchdragvfitty = (options) ->
@each (i, el) ->
$el = $(el)
instance = new ns.TouchdragvFitty $el, options
$el.data 'touchdragvfitty', instance
return
$.Touchdragv = ns.touchdragvEl
$.TouchdragvFitty = ns.touchdragvFitty