# 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("
")
@container = @elem.parent()
@offLabel = $("""
#{@uncheckedLabel}
""").appendTo(@container)
@offSpan = @offLabel.children('span')
@onLabel = $("""
#{@checkedLabel}
""").appendTo(@container)
@onSpan = @onLabel.children('span')
@handle = $("""""").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)