Skip to content
This repository was archived by the owner on Nov 27, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 125 additions & 104 deletions fancySelect.coffee
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
$ = window.jQuery || window.Zepto || window.$
$ = window.jQuery or window.Zepto or window.$

$.fn.fancySelect = (opts = {}) ->
$.fn.fancySelect = (opts) ->
isiOS = undefined
settings = undefined
if opts == null
opts = {}
settings = $.extend({
forceiOS: false
includeBlank: false
optionTemplate: (optionEl) ->
return optionEl.text()
optionEl.text()
triggerTemplate: (optionEl) ->
return optionEl.text()
}, opts)

isiOS = !!navigator.userAgent.match /iP(hone|od|ad)/i
optionEl.text()

return this.each ->
}, opts)
isiOS = ! !navigator.userAgent.match(/iP(hone|od|ad)/i)
@each ->
copyOptionsToList = undefined
disabled = undefined
options = undefined
sel = undefined
trigger = undefined
updateTriggerText = undefined
wrapper = undefined
searchTerm = ''
searchTimeout = undefined
sel = $(this)
return if sel.hasClass('fancified') || sel[0].tagName != 'SELECT'
sel.addClass('fancified')

# hide the native select
if sel.hasClass('fancified') or sel[0].tagName != 'SELECT'
return
sel.addClass 'fancified'
sel.css
width: 1
height: 1
Expand All @@ -26,169 +37,179 @@ $.fn.fancySelect = (opts = {}) ->
top: 0
left: 0
opacity: 0

# some global setup stuff
sel.wrap '<div class="fancy-select">'
wrapper = sel.parent()

wrapper.addClass(sel.data('class')) if sel.data('class')

if sel.data('class')
wrapper.addClass sel.data('class')
wrapper.append '<div class="trigger">'
wrapper.append '<ul class="options">' unless isiOS && !settings.forceiOS

trigger = wrapper.find '.trigger'
options = wrapper.find '.options'

# disabled in markup?
if !(isiOS and !settings.forceiOS)
wrapper.append '<ul class="options">'
trigger = wrapper.find('.trigger')
options = wrapper.find('.options')
disabled = sel.prop('disabled')
if disabled
wrapper.addClass 'disabled'

updateTriggerText = ->
triggerHtml = undefined
triggerHtml = settings.triggerTemplate(sel.find(':selected'))
trigger.html(triggerHtml)
trigger.html triggerHtml

sel.on 'blur.fs', ->
if trigger.hasClass 'open'
setTimeout ->
if trigger.hasClass('open')
return setTimeout((->
trigger.trigger 'close.fs'
, 120

), 120)
return
trigger.on 'close.fs', ->
trigger.removeClass 'open'
options.removeClass 'open'

trigger.on 'click.fs', ->
unless disabled
offParent = undefined
parent = undefined
if !disabled
trigger.toggleClass 'open'

# fancySelect defaults to using native selects with a styled trigger on mobile
# don't show the options if we're on mobile and haven't set `forceiOS`
if isiOS && !settings.forceiOS
if trigger.hasClass 'open'
sel.focus()
if isiOS and !settings.forceiOS
if trigger.hasClass('open')
return sel.focus()
else
if trigger.hasClass 'open'
if trigger.hasClass('open')
parent = trigger.parent()
offParent = parent.offsetParent()

# TODO 20 is very static
if (parent.offset().top + parent.outerHeight() + options.outerHeight() + 20) > $(window).height() + $(window).scrollTop()
if parent.offset().top + parent.outerHeight() + options.outerHeight() + 20 > $(window).height() + $(window).scrollTop()
options.addClass 'overflowing'
else
options.removeClass 'overflowing'

options.toggleClass 'open'

sel.focus() unless isiOS

if !isiOS
return sel.focus()
return
sel.on 'enable', ->
sel.prop 'disabled', false
wrapper.removeClass 'disabled'
disabled = false
copyOptionsToList()

sel.on 'disable', ->
sel.prop 'disabled', true
wrapper.addClass 'disabled'
disabled = true

sel.on 'change.fs', (e) ->
if e.originalEvent && e.originalEvent.isTrusted
# discard firefox-only automatic event when hitting enter, we want to trigger our own
if e.originalEvent and e.originalEvent.isTrusted
e.stopPropagation()
else
updateTriggerText()

# keyboard control
sel.on 'keydown', (e) ->
hovered = undefined
newHovered = undefined
w = undefined
w = e.which
hovered = options.find('.hover')
hovered.removeClass('hover')

hovered.removeClass 'hover'
if !options.hasClass('open')
if w in [13, 32, 38, 40] # enter, space, up, down
if w == 13 or w == 38 or w == 40
e.preventDefault()
trigger.trigger 'click.fs'
return trigger.trigger('click.fs')
else
clearTimeout searchTimeout
searchTimeout = setTimeout((->
searchTerm = ''
return
), 500)
searchTerm += String.fromCharCode(w).toLowerCase()
optCount = options.find('li').length + 1
i = 1
while i < optCount
current = options.find('li:nth-child(' + i + ')')
text = current.text()
if text.toLowerCase().indexOf(searchTerm) >= 0
current.trigger 'mousedown.fs'
return
i++
else
if w == 38 # up
if w == 38
e.preventDefault()
if hovered.length && hovered.index() > 0 # move up
hovered.prev().addClass('hover')
else # move to bottom
options.find('li:last-child').addClass('hover')
else if w == 40 # down
if hovered.length and hovered.index() > 0
hovered.prev().addClass 'hover'
else
options.find('li:last-child').addClass 'hover'
else if w == 40
e.preventDefault()
if hovered.length && hovered.index() < options.find('li').length - 1 # move down
hovered.next().addClass('hover')
else # move to top
options.find('li:first-child').addClass('hover')
else if w == 27 # escape
if hovered.length and hovered.index() < options.find('li').length - 1
hovered.next().addClass 'hover'
else
options.find('li:first-child').addClass 'hover'
else if w == 27
e.preventDefault()
trigger.trigger 'click.fs'
else if w in [13, 32] # enter, space
else if w == 13
e.preventDefault()
hovered.trigger 'mousedown.fs'
else if w == 9 # tab
if trigger.hasClass 'open' then trigger.trigger 'close.fs'

else if w == 9
if trigger.hasClass('open')
trigger.trigger 'close.fs'
else
clearTimeout searchTimeout
searchTimeout = setTimeout((->
`var current`
`var i`
searchTerm = ''
return
), 500)
searchTerm += String.fromCharCode(w).toLowerCase()
optCount = options.find('li').length + 1
i = 1
while i < optCount
current = options.find('li:nth-child(' + i + ')')
text = current.text()
if text.toLowerCase().indexOf(searchTerm) >= 0
current.addClass 'hover'
return
i++
newHovered = options.find('.hover')
if newHovered.length
options.scrollTop 0
options.scrollTop newHovered.position().top - 12

# Handle item selection, and
# Add class selected to selected item
return options.scrollTop(newHovered.position().top - 12)
return
options.on 'mousedown.fs', 'li', (e) ->
clicked = undefined
clicked = $(this)

sel.val(clicked.data('raw-value'))

sel.trigger('blur.fs').trigger('focus.fs') unless isiOS

options.find('.selected').removeClass('selected')
sel.val clicked.data('raw-value')
if !isiOS
sel.trigger('blur.fs').trigger 'focus.fs'
options.find('.selected').removeClass 'selected'
clicked.addClass 'selected'
trigger.addClass 'selected'
return sel.val(clicked.data('raw-value')).trigger('change.fs').trigger('blur.fs').trigger('focus.fs')

# handle mouse selection
trigger.toggleClass 'selected', clicked.data('raw-value') != ''
sel.val(clicked.data('raw-value')).trigger('change.fs').trigger('blur.fs').trigger 'focus.fs'
options.on 'mouseenter.fs', 'li', ->
hovered = undefined
nowHovered = undefined
nowHovered = $(this)
hovered = options.find('.hover')
hovered.removeClass 'hover'

nowHovered.addClass 'hover'

options.on 'mouseleave.fs', 'li', ->
options.find('.hover').removeClass('hover')
options.find('.hover').removeClass 'hover'

copyOptionsToList = ->
# update our trigger to reflect the select (it really already should, this is just a safety)
selOpts = undefined
updateTriggerText()

return if isiOS && !settings.forceiOS

# snag current options before we add a default one
selOpts = sel.find 'option'

# generate list of options for the fancySelect

if isiOS and !settings.forceiOS
return
selOpts = sel.find('option')
sel.find('option').each (i, opt) ->
optHtml = undefined
opt = $(opt)

if !opt.prop('disabled') && (opt.val() || settings.includeBlank)
# Generate the inner HTML for the option from our template
if !opt.prop('disabled') and (opt.val() or settings.includeBlank)
optHtml = settings.optionTemplate(opt)

# Is there a select option on page load?
if opt.prop('selected')
options.append "<li data-raw-value=\"#{opt.val()}\" class=\"selected\">#{optHtml}</li>"
return options.append('<li data-raw-value="' + opt.val() + '" class="selected">' + optHtml + '</li>')
else
options.append "<li data-raw-value=\"#{opt.val()}\">#{optHtml}</li>"
return options.append('<li data-raw-value="' + opt.val() + '">' + optHtml + '</li>')
return

# for updating the list of options after initialization
sel.on 'update.fs', ->
wrapper.find('.options').empty()
copyOptionsToList()

copyOptionsToList()

return
Loading