This repository has been archived by the owner on Nov 27, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 165
/
fancySelect.coffee
194 lines (155 loc) · 5.86 KB
/
fancySelect.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
$ = window.jQuery || window.Zepto || window.$
$.fn.fancySelect = (opts = {}) ->
settings = $.extend({
forceiOS: false
includeBlank: false
optionTemplate: (optionEl) ->
return optionEl.text()
triggerTemplate: (optionEl) ->
return optionEl.text()
}, opts)
isiOS = !!navigator.userAgent.match /iP(hone|od|ad)/i
return this.each ->
sel = $(this)
return if sel.hasClass('fancified') || sel[0].tagName != 'SELECT'
sel.addClass('fancified')
# hide the native select
sel.css
width: 1
height: 1
display: 'block'
position: 'absolute'
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')
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?
disabled = sel.prop('disabled')
if disabled
wrapper.addClass 'disabled'
updateTriggerText = ->
triggerHtml = settings.triggerTemplate(sel.find(':selected'))
trigger.html(triggerHtml)
sel.on 'blur.fs', ->
if trigger.hasClass 'open'
setTimeout ->
trigger.trigger 'close.fs'
, 120
trigger.on 'close.fs', ->
trigger.removeClass 'open'
options.removeClass 'open'
trigger.on 'click.fs', ->
unless 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()
else
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()
options.addClass 'overflowing'
else
options.removeClass 'overflowing'
options.toggleClass 'open'
sel.focus() unless isiOS
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
e.stopPropagation()
else
updateTriggerText()
# keyboard control
sel.on 'keydown', (e) ->
w = e.which
hovered = options.find('.hover')
hovered.removeClass('hover')
if !options.hasClass('open')
if w in [13, 32, 38, 40] # enter, space, up, down
e.preventDefault()
trigger.trigger 'click.fs'
else
if w == 38 # up
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
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
e.preventDefault()
trigger.trigger 'click.fs'
else if w in [13, 32] # enter, space
e.preventDefault()
hovered.trigger 'mousedown.fs'
else if w == 9 # tab
if trigger.hasClass 'open' then trigger.trigger 'close.fs'
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
options.on 'mousedown.fs', 'li', (e) ->
clicked = $(this)
sel.val(clicked.data('raw-value'))
sel.trigger('blur.fs').trigger('focus.fs') unless isiOS
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
options.on 'mouseenter.fs', 'li', ->
nowHovered = $(this)
hovered = options.find('.hover')
hovered.removeClass 'hover'
nowHovered.addClass 'hover'
options.on 'mouseleave.fs', 'li', ->
options.find('.hover').removeClass('hover')
copyOptionsToList = ->
# update our trigger to reflect the select (it really already should, this is just a safety)
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
sel.find('option').each (i, opt) ->
opt = $(opt)
if !opt.prop('disabled') && (opt.val() || settings.includeBlank)
# Generate the inner HTML for the option from our template
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>"
else
options.append "<li data-raw-value=\"#{opt.val()}\">#{optHtml}</li>"
# for updating the list of options after initialization
sel.on 'update.fs', ->
wrapper.find('.options').empty()
copyOptionsToList()
copyOptionsToList()