forked from tdreyno/iphone-style-checkboxes
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathiphone-style-checkboxes.coffee
269 lines (201 loc) · 8.12 KB
/
iphone-style-checkboxes.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
# iPhone-style Checkboxes Coffee plugin
# Copyright Thomas Reynolds, licensed GPL & MIT
class iOSCheckbox
constructor: (elem, options) ->
@elem = $(elem)
opts = $.extend({}, iOSCheckbox.defaults, options)
# Import options into instance variables
for key, value of opts
@[key] = value
@elem.data(@dataName, this)
# Initialize the control
@wrapCheckboxWithDivs()
@attachEvents()
@disableTextSelection()
@optionallyResize('handle') if @resizeHandle
@optionallyResize('container') if @resizeContainer
@initialPosition()
isDisabled: -> @elem.is(':disabled')
# Wrap the existing input[type=checkbox] with divs for styling and grab
# DOM references to the created nodes
wrapCheckboxWithDivs: ->
@elem.wrap("<div class='#{@containerClass}' />")
@container = @elem.parent()
@offLabel = $("""<label class='#{@labelOffClass}'>
<span>#{@uncheckedLabel}</span>
</label>""").appendTo(@container)
@offSpan = @offLabel.children('span')
@onLabel = $("""<label class='#{@labelOnClass}'>
<span>#{@checkedLabel}</span>
</label>""").appendTo(@container)
@onSpan = @onLabel.children('span')
@handle = $("""<div class='#{@handleClass}'>
<div class='#{@handleRightClass}'>
<div class='#{@handleCenterClass}' />
</div>
</div>""").appendTo(this.container)
# Disable IE text selection, other browsers are handled in CSS
disableTextSelection: ->
# Elements containing text should be unselectable
if $.browser.msie
$([@handle, @offLabel, @onLabel, @container]).attr("unselectable", "on")
_getDimension: (elem, dimension) ->
if $.fn.actual?
elem.actual(dimension)
else
elem[dimension]()
# Automatically resize the handle or container
optionallyResize: (mode) ->
onLabelWidth = @_getDimension(@onLabel, "width")
offLabelWidth = @_getDimension(@offLabel, "width")
if mode == "container"
newWidth = if (onLabelWidth > offLabelWidth)
onLabelWidth
else
offLabelWidth
newWidth += @_getDimension(@handle, "width") + @handleMargin
@container.css(width: newWidth)
else
newWidth = if (onLabelWidth > offLabelWidth)
onLabelWidth
else
offLabelWidth
@handle.css(width: newWidth)
onMouseDown: (event) ->
event.preventDefault()
return if @isDisabled()
x = event.pageX || event.originalEvent.changedTouches[0].pageX
iOSCheckbox.currentlyClicking = @handle
iOSCheckbox.dragStartPosition = x
iOSCheckbox.handleLeftOffset = parseInt(@handle.css('left'), 10) || 0
onDragMove: (event, x) ->
return unless iOSCheckbox.currentlyClicking == @handle
p = (x + iOSCheckbox.handleLeftOffset - iOSCheckbox.dragStartPosition) / @rightSide
p = 0 if p < 0
p = 1 if p > 1
newWidth = p * @rightSide
@handle.css(left: newWidth)
@onLabel.css(width: newWidth + @handleRadius)
@offSpan.css(marginRight: -newWidth)
@onSpan.css(marginLeft: -(1 - p) * @rightSide)
onDragEnd: (event, x) ->
return unless iOSCheckbox.currentlyClicking == @handle
return if @isDisabled()
if iOSCheckbox.dragging
p = (x - iOSCheckbox.dragStartPosition) / @rightSide
@elem.prop('checked', (p >= 0.5))
else
@elem.prop('checked', !@elem.prop('checked'))
iOSCheckbox.currentlyClicking = null
iOSCheckbox.dragging = null
@didChange()
refresh: -> @didChange() #TODO: Verify - this might fire event unnecessarily
didChange: ->
@onChange?(@elem, @elem.prop('checked'))
if @isDisabled()
@container.addClass(@disabledClass)
return false
else
@container.removeClass(@disabledClass)
new_left = if @elem.prop('checked') then @rightSide else 0
@handle.animate(left: new_left, @duration)
@onLabel.animate(width: new_left + @handleRadius, @duration)
@offSpan.animate(marginRight: -new_left, @duration)
@onSpan.animate(marginLeft: new_left - @rightSide, @duration)
attachEvents: ->
self = this
localMouseMove = (event) ->
self.onGlobalMove.apply(self, arguments)
localMouseUp = (event) ->
self.onGlobalUp.apply(self, arguments)
$(document).unbind 'mousemove touchmove', localMouseMove
$(document).unbind 'mouseup touchend', localMouseUp
# The original checkbox value might be changed by clickig on the associated label or other means
# To make sure we are in sync:
@elem.change -> self.refresh()
# A mousedown anywhere in the control will start tracking for dragging
@container.bind 'mousedown touchstart', (event) ->
self.onMouseDown.apply(self, arguments)
# As the mouse moves on the page, animate if we are in a drag state
$(document).bind 'mousemove touchmove', localMouseMove
# When the mouse comes up, leave drag state
$(document).bind 'mouseup touchend', localMouseUp
# Setup the control's inital position
initialPosition: ->
containerWidth = @_getDimension(@container, "width")
@offLabel.css(width: containerWidth - @containerRadius)
offset = @containerRadius + 1
offset -= 3 if $.browser.msie and $.browser.version < 7
@rightSide = containerWidth - @_getDimension(@handle, "width") - offset
if @elem.is(':checked')
@handle.css(left: @rightSide)
@onLabel.css(width: @rightSide + @handleRadius)
@offSpan.css(marginRight: -@rightSide)
else
@onLabel.css(width: 0)
@onSpan.css(marginLeft: -@rightSide)
@container.addClass(@disabledClass) if @isDisabled()
onGlobalMove: (event) ->
return unless !@isDisabled() && iOSCheckbox.currentlyClicking
event.preventDefault()
x = event.pageX || event.originalEvent.changedTouches[0].pageX
if (!iOSCheckbox.dragging &&
(Math.abs(iOSCheckbox.dragStartPosition - x) > @dragThreshold))
iOSCheckbox.dragging = true
@onDragMove(event, x)
onGlobalUp: (event) ->
return unless iOSCheckbox.currentlyClicking
event.preventDefault()
x = event.pageX || event.originalEvent.changedTouches[0].pageX
@onDragEnd(event, x)
false
@defaults:
# Time spent during slide animation
duration: 200
# Text content of "on" state
checkedLabel: 'ON'
# Text content of "off" state
uncheckedLabel: 'OFF'
# Automatically resize the handle to cover either label
resizeHandle: true
# Automatically resize the widget to contain the labels
resizeContainer: true
disabledClass: 'iPhoneCheckDisabled'
containerClass: 'iPhoneCheckContainer'
labelOnClass: 'iPhoneCheckLabelOn'
labelOffClass: 'iPhoneCheckLabelOff'
handleClass: 'iPhoneCheckHandle'
handleCenterClass: 'iPhoneCheckHandleCenter'
handleRightClass: 'iPhoneCheckHandleRight'
# Pixels that must be dragged for a click to be ignored
dragThreshold: 5
handleMargin: 15
handleRadius: 4
containerRadius: 5
dataName: "iphoneStyle"
onChange: ->
$.iphoneStyle = @iOSCheckbox = iOSCheckbox
$.fn.iphoneStyle = (args...) ->
dataName = args[0]?.dataName ? iOSCheckbox.defaults.dataName
for checkbox in @filter(':checkbox')
existingControl = $(checkbox).data(dataName)
if existingControl?
[method, params...] = args
existingControl[method]?.apply(existingControl, params)
else
new iOSCheckbox(checkbox, args[0])
this
$.fn.iOSCheckbox = (options={}) ->
# iOS5 style only supports circular handle
opts = $.extend({}, options, {
resizeHandle: false
disabledClass: 'iOSCheckDisabled'
containerClass: 'iOSCheckContainer'
labelOnClass: 'iOSCheckLabelOn'
labelOffClass: 'iOSCheckLabelOff'
handleClass: 'iOSCheckHandle'
handleCenterClass: 'iOSCheckHandleCenter'
handleRightClass: 'iOSCheckHandleRight'
dataName: 'iOSCheckbox'
})
this.iphoneStyle(opts)