uber_select_rails 0.1.2 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/uber_select_rails/version.rb +1 -1
- data/uber_select.gemspec +1 -1
- data/vendor/assets/javascript/uber_select/.gitignore +18 -0
- data/vendor/assets/javascript/uber_select/README.md +250 -0
- data/vendor/assets/javascript/uber_select/javascript/jquery.uber-select.js +177 -0
- data/vendor/assets/javascript/uber_select/javascript/list.js +120 -0
- data/vendor/assets/javascript/uber_select/javascript/output_container.js +21 -0
- data/vendor/assets/javascript/uber_select/javascript/pane.js +90 -0
- data/vendor/assets/javascript/uber_select/javascript/search.js +151 -0
- data/vendor/assets/javascript/uber_select/javascript/search_field.js +80 -0
- data/vendor/assets/javascript/uber_select/javascript/string_extensions.js +8 -0
- data/vendor/assets/javascript/uber_select/javascript/uber_search.js +315 -0
- data/vendor/assets/javascript/uber_select/test.css +150 -0
- data/vendor/assets/javascript/uber_select/test.html +184 -0
- metadata +15 -3
@@ -0,0 +1,315 @@
|
|
1
|
+
var UberSearch = function(data, options){
|
2
|
+
var eventsTriggered = {
|
3
|
+
shown: 'shown',
|
4
|
+
renderedResults: 'renderedResults',
|
5
|
+
clear: 'clear',
|
6
|
+
select: 'select'
|
7
|
+
}
|
8
|
+
|
9
|
+
options = $.extend({
|
10
|
+
value: null, // Initialize with this selectedValue
|
11
|
+
search:true, // Show the search input
|
12
|
+
clearSearchButton:'✕', // Text content of clear search button
|
13
|
+
selectCaret: '⌄', // Text content of select caret
|
14
|
+
hideBlankOption: false, // Should blank options be hidden automatically?
|
15
|
+
treatBlankOptionAsPlaceholder: false, // Should blank options use the placeholder as text?
|
16
|
+
highlightByDefault: true, // Should the first result be auto-highlighted?
|
17
|
+
minQueryLength: 0, // Number of characters to type before results are displayed
|
18
|
+
minQueryMessage: true, // Message to show when the query doesn't exceed the minimum length. True for default, false for none, or custom message.
|
19
|
+
placeholder: null, // Placeholder to show in the selected text area
|
20
|
+
searchPlaceholder: 'Type to search', // Placeholder to show in the search input
|
21
|
+
noResultsText: 'No Matches Found', // The message shown when there are no results
|
22
|
+
resultPostprocessor: function(result, datum){}, // A function that is run after a result is built and can be used to decorate it
|
23
|
+
buildResult: null, // A function that is used to build result elements
|
24
|
+
outputContainer: null, // An object that receives the output once a result is selected. Must respond to setValue(value), and view()
|
25
|
+
onRender: function(resultsContainer, result) {}, // A function to run when the results container is rendered. If the result returns false, the default select handler is not run and the event is cancelled
|
26
|
+
onSelect: function(datum, result, clickEvent) {}, // A function to run when a result is selected. If the result returns false, the default select handler is not run and the event is cancelled
|
27
|
+
onNoHighlightSubmit: function(value) {}, // A function to run when a user presses enter without selecting a result.
|
28
|
+
noDataText: 'No options' // Text to show in there is nothing in the set of data to pick from
|
29
|
+
}, options)
|
30
|
+
|
31
|
+
var context = this
|
32
|
+
var view = $('<span class="uber_select"></span>')
|
33
|
+
var selectedValue = options.value // Internally selected value
|
34
|
+
var outputContainer = options.outputContainer || new OutputContainer({selectCaret: options.selectCaret})
|
35
|
+
var searchField = new SearchField({placeholder: options.searchPlaceholder, clearButton: options.clearSearchButton})
|
36
|
+
var resultsContainer = $('<div class="results_container"></div>')
|
37
|
+
var messages = $('<div class="messages"></div>')
|
38
|
+
var pane = new Pane({trigger: outputContainer.view})
|
39
|
+
var search = new Search(searchField.input, resultsContainer, {
|
40
|
+
model: {
|
41
|
+
dataForMatching: dataForMatching,
|
42
|
+
minQueryLength: options.minQueryLength,
|
43
|
+
queryPreprocessor: options.queryPreprocessor || Search.prototype.queryPreprocessor,
|
44
|
+
datumPreprocessor: options.datumPreprocessor || datumPreprocessor,
|
45
|
+
patternForMatching: options.patternForMatching || Search.prototype.patternForMatching
|
46
|
+
},
|
47
|
+
view: {
|
48
|
+
renderResults: renderResults,
|
49
|
+
buildResult: options.buildResult || buildResult,
|
50
|
+
keypressInput: searchField.input
|
51
|
+
}
|
52
|
+
})
|
53
|
+
|
54
|
+
|
55
|
+
// BEHAVIOUR
|
56
|
+
|
57
|
+
// When the pane is opened
|
58
|
+
$(pane).on('shown', function(){
|
59
|
+
search.clear()
|
60
|
+
markSelected()
|
61
|
+
$(searchField.input).focus()
|
62
|
+
view.addClass('open')
|
63
|
+
|
64
|
+
triggerEvent(eventsTriggered.shown)
|
65
|
+
})
|
66
|
+
|
67
|
+
// When the query is changed
|
68
|
+
$(search).on('queryChanged', function(){
|
69
|
+
updateMessages()
|
70
|
+
})
|
71
|
+
|
72
|
+
// When the search results are rendered
|
73
|
+
$(search).on('renderedResults', function(event){
|
74
|
+
if (options.onRender(resultsContainer, getSelection()) === false) {
|
75
|
+
event.stopPropagation()
|
76
|
+
return
|
77
|
+
}
|
78
|
+
|
79
|
+
markSelected()
|
80
|
+
updateMessages()
|
81
|
+
triggerEvent(eventsTriggered.renderedResults)
|
82
|
+
})
|
83
|
+
|
84
|
+
// When the search field is cleared
|
85
|
+
$(searchField).on('clear', function(){
|
86
|
+
triggerEvent(eventsTriggered.clear)
|
87
|
+
})
|
88
|
+
|
89
|
+
// When a search result is chosen
|
90
|
+
resultsContainer.on('click', '.result', function(event){
|
91
|
+
var datum = $(this).data()
|
92
|
+
|
93
|
+
if (options.onSelect(datum, this, event) === false) {
|
94
|
+
event.stopPropagation()
|
95
|
+
return
|
96
|
+
}
|
97
|
+
|
98
|
+
setValue(valueFromResult(this))
|
99
|
+
pane.hide()
|
100
|
+
triggerEvent(eventsTriggered.select, [datum, this, event])
|
101
|
+
})
|
102
|
+
|
103
|
+
// When query is submitted
|
104
|
+
$(searchField.input).on('noHighlightSubmit', function(event) {
|
105
|
+
options.onNoHighlightSubmit($(this).val())
|
106
|
+
})
|
107
|
+
|
108
|
+
// When the pane is hidden
|
109
|
+
$(pane).on('hidden', function(){
|
110
|
+
view.removeClass('open')
|
111
|
+
})
|
112
|
+
|
113
|
+
|
114
|
+
// INITIALIZATION
|
115
|
+
|
116
|
+
setData(data)
|
117
|
+
|
118
|
+
if (options.search){
|
119
|
+
pane.addContent('search', searchField.view)
|
120
|
+
}
|
121
|
+
pane.addContent('messages', messages)
|
122
|
+
pane.addContent('results', resultsContainer)
|
123
|
+
|
124
|
+
// If the output container isn't in the DOM yet, add it
|
125
|
+
if (!$(outputContainer.view).closest('body').length){
|
126
|
+
$(outputContainer.view).appendTo(view)
|
127
|
+
}
|
128
|
+
|
129
|
+
$(view).append(pane.view)
|
130
|
+
|
131
|
+
updateMessages()
|
132
|
+
updateSelectedText()
|
133
|
+
markSelected()
|
134
|
+
|
135
|
+
|
136
|
+
// HELPER FUNCTIONS
|
137
|
+
|
138
|
+
function setData(newData){
|
139
|
+
data = setDataDefaults(newData)
|
140
|
+
search.setData(data)
|
141
|
+
updateSelectedText()
|
142
|
+
markSelected()
|
143
|
+
}
|
144
|
+
|
145
|
+
// Selects the result corresponding to the given value
|
146
|
+
function setValue(value){
|
147
|
+
if (selectedValue == value) { return }
|
148
|
+
selectedValue = value
|
149
|
+
updateSelectedText()
|
150
|
+
markSelected()
|
151
|
+
}
|
152
|
+
|
153
|
+
// Updates the enhanced select with the text of the selected result
|
154
|
+
function setSelectedText(text){
|
155
|
+
if (text) {
|
156
|
+
outputContainer.setValue(text)
|
157
|
+
} else {
|
158
|
+
outputContainer.setValue(options.placeholder)
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
// Inherit values for matchValue and value from text
|
163
|
+
function setDataDefaults(data){
|
164
|
+
return $.map(data, function(datum) {
|
165
|
+
return $.extend({ value: datum.text, matchValue: datum.text }, datum)
|
166
|
+
})
|
167
|
+
}
|
168
|
+
|
169
|
+
// Converts the dataFromSelect into a datum list for matching
|
170
|
+
function dataForMatching(processedQuery, data){
|
171
|
+
// If a query is present, include only select options that should be used when searching
|
172
|
+
// Else, include only options that should be visible when not searching
|
173
|
+
if (processedQuery) {
|
174
|
+
return $.map(data, function(datum){ if (datum.visibility != 'no-query' || datum.value == selectedValue) return datum })
|
175
|
+
} else {
|
176
|
+
return $.map(data, function(datum){ if (datum.visibility != 'query' || datum.value == selectedValue) return datum })
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
// Match against the datum.matchValue
|
181
|
+
function datumPreprocessor(datum){
|
182
|
+
return datum.matchValue
|
183
|
+
}
|
184
|
+
|
185
|
+
// Adds group support and blank option hiding
|
186
|
+
function renderResults(data){
|
187
|
+
var context = this
|
188
|
+
var sourceArray = []
|
189
|
+
|
190
|
+
$.each(data, function(_, datum){
|
191
|
+
// Add the group name so we can group items
|
192
|
+
var result = context.buildResult(datum).attr('data-group', datum.group)
|
193
|
+
|
194
|
+
// Omit blank option from results
|
195
|
+
if (!options.hideBlankOption || datum.value){
|
196
|
+
sourceArray.push(result)
|
197
|
+
}
|
198
|
+
})
|
199
|
+
|
200
|
+
// Arrange ungrouped list items
|
201
|
+
var destArray = reject(sourceArray, 'li:not([data-group])')
|
202
|
+
|
203
|
+
// Arrange list items into sub lists
|
204
|
+
while (sourceArray.length) {
|
205
|
+
var group = $(sourceArray[0]).attr('data-group')
|
206
|
+
var groupNodes = reject(sourceArray, 'li[data-group="' + group + '"]')
|
207
|
+
var sublist = $('<ul class="sublist"></ul>').attr('data-group', group)
|
208
|
+
var sublistNode = $('<li></li>').append('<span class="sublist_name">' + group + '</span>')
|
209
|
+
|
210
|
+
sublist.append(groupNodes)
|
211
|
+
sublistNode.append(sublist)
|
212
|
+
|
213
|
+
destArray.push(sublistNode)
|
214
|
+
}
|
215
|
+
|
216
|
+
this.view.toggleClass('empty', !destArray.length)
|
217
|
+
this.view.html(destArray)
|
218
|
+
}
|
219
|
+
|
220
|
+
// Removes elements from the sourcArray that match the selector
|
221
|
+
// Returns an array of removed elements
|
222
|
+
function reject(sourceArray, selector){
|
223
|
+
var dest = filter(sourceArray, selector)
|
224
|
+
var source = filter(sourceArray, selector, true)
|
225
|
+
sourceArray.splice(0, sourceArray.length)
|
226
|
+
sourceArray.push.apply(sourceArray, source)
|
227
|
+
return dest
|
228
|
+
}
|
229
|
+
|
230
|
+
function filter(sourceArray, selector, invert){
|
231
|
+
return $.grep(sourceArray, function(node){ return node.is(selector) }, invert)
|
232
|
+
}
|
233
|
+
|
234
|
+
function buildResult(datum){
|
235
|
+
var result = $('<li class="result"></li>')
|
236
|
+
.html((options.treatBlankOptionAsPlaceholder ? datum.text || options.placeholder : datum.text) || " ")
|
237
|
+
.data(datum) // Store the datum so we can get know what the value of the selected item is
|
238
|
+
|
239
|
+
options.resultPostprocessor(result, datum)
|
240
|
+
|
241
|
+
return result
|
242
|
+
}
|
243
|
+
|
244
|
+
function markSelected(){
|
245
|
+
var selected = getSelection()
|
246
|
+
var results = search.getResults()
|
247
|
+
|
248
|
+
$(results).filter('.selected').not(selected).removeClass('selected')
|
249
|
+
|
250
|
+
// Ensure the selected result is unhidden
|
251
|
+
$(selected).addClass('selected').removeClass('hidden')
|
252
|
+
|
253
|
+
if (selected) {
|
254
|
+
search.highlightResult(selected)
|
255
|
+
} else if (options.highlightByDefault) {
|
256
|
+
search.highlightResult(results.not('.hidden').first())
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
// Returns the selected element and its index
|
261
|
+
function getSelection(){
|
262
|
+
var results = search.getResults()
|
263
|
+
var selected
|
264
|
+
$.each(results, function(i, result){
|
265
|
+
if (selectedValue == valueFromResult(result)){
|
266
|
+
selected = result
|
267
|
+
return false
|
268
|
+
}
|
269
|
+
})
|
270
|
+
return selected
|
271
|
+
}
|
272
|
+
|
273
|
+
function valueFromResult(result){
|
274
|
+
return $(result).data('value')
|
275
|
+
}
|
276
|
+
|
277
|
+
function updateSelectedText(){
|
278
|
+
setSelectedText(textFromValue(selectedValue))
|
279
|
+
}
|
280
|
+
|
281
|
+
function textFromValue(value){
|
282
|
+
return $.map(data, function(datum){ if (datum.value == value) return datum.text })[0]
|
283
|
+
}
|
284
|
+
|
285
|
+
function updateMessages(){
|
286
|
+
messages.show()
|
287
|
+
if (!queryLength() && !resultsCount()){
|
288
|
+
messages.html(options.noDataText)
|
289
|
+
} else if (options.minQueryLength && options.minQueryMessage && queryLength() < options.minQueryLength){
|
290
|
+
messages.html(options.minQueryMessage === true ? 'Type at least ' + options.minQueryLength + (options.minQueryLength == 1 ? ' character' : ' characters') + ' to search' : options.minQueryMessage)
|
291
|
+
} else if (options.noResultsText && !resultsCount()){
|
292
|
+
messages.html(options.noResultsText)
|
293
|
+
} else {
|
294
|
+
messages.empty().hide()
|
295
|
+
}
|
296
|
+
}
|
297
|
+
|
298
|
+
function queryLength(){
|
299
|
+
return search.getQuery().length
|
300
|
+
}
|
301
|
+
|
302
|
+
function resultsCount(){
|
303
|
+
return search.getResults().length
|
304
|
+
}
|
305
|
+
|
306
|
+
// Allow observer to be attached to the UberSearch itself
|
307
|
+
function triggerEvent(eventType, callbackArgs){
|
308
|
+
view.trigger(eventType, callbackArgs)
|
309
|
+
$(context).trigger(eventType, callbackArgs)
|
310
|
+
}
|
311
|
+
|
312
|
+
// PUBLIC INTERFACE
|
313
|
+
|
314
|
+
$.extend(this, {view:view, searchField:searchField, setValue:setValue, setData:setData, options:options})
|
315
|
+
}
|
@@ -0,0 +1,150 @@
|
|
1
|
+
body {
|
2
|
+
padding: 30px;
|
3
|
+
}
|
4
|
+
.example {
|
5
|
+
display: inline-block;
|
6
|
+
margin-right: 5em;
|
7
|
+
margin-bottom: 5em;
|
8
|
+
max-width: 300px;
|
9
|
+
vertical-align: top;
|
10
|
+
}
|
11
|
+
|
12
|
+
label {
|
13
|
+
font-family: sans-serif;
|
14
|
+
font-weight: bold;
|
15
|
+
text-transform: uppercase;
|
16
|
+
font-size: 0.8em;
|
17
|
+
display: block;
|
18
|
+
position: relative;
|
19
|
+
z-index: 1; /* Place above pane */
|
20
|
+
color: #444;
|
21
|
+
}
|
22
|
+
|
23
|
+
.uber_select{
|
24
|
+
min-width: 100%; /* ensure select is at least as long as label */
|
25
|
+
max-width: 300px;
|
26
|
+
display: inline-block;
|
27
|
+
margin: 5px 0;
|
28
|
+
position: relative; /* Contain pane */
|
29
|
+
}
|
30
|
+
.uber_select .selected_text_container{
|
31
|
+
border: 1px solid #E0E0E0;
|
32
|
+
cursor: pointer;
|
33
|
+
font-family: sans-serif;
|
34
|
+
padding: 1px 31px 1px 3px;
|
35
|
+
font-size: 20px;
|
36
|
+
white-space: nowrap;
|
37
|
+
display: block;
|
38
|
+
overflow: hidden;
|
39
|
+
text-overflow: ellipsis;
|
40
|
+
position: relative; /* Contain caret */
|
41
|
+
}
|
42
|
+
.uber_select .selected_text.empty{
|
43
|
+
color: #aaa;
|
44
|
+
}
|
45
|
+
|
46
|
+
.uber_select .select_caret{
|
47
|
+
font-size: 12px;
|
48
|
+
font-family: 'FontAwesome';
|
49
|
+
line-height: 24px;
|
50
|
+
position: absolute;
|
51
|
+
right: 0;
|
52
|
+
top: 0;
|
53
|
+
padding: 0 10px;
|
54
|
+
background-color: white;
|
55
|
+
}
|
56
|
+
|
57
|
+
.uber_select .pane{
|
58
|
+
position: absolute;
|
59
|
+
border: 1px solid #E0E0E0;
|
60
|
+
background-color: white;
|
61
|
+
box-shadow: 0 3px 3px rgba(0,0,0,.1);
|
62
|
+
margin: -60px 0 0 -20px;
|
63
|
+
padding-top: 40px; /* Make room for input label */
|
64
|
+
padding-right: 40px; /* grow the right side of the pane by the same as the left side */
|
65
|
+
min-width: 100%; /* Don't be any narrower than the uber_select itself */
|
66
|
+
}
|
67
|
+
|
68
|
+
.uber_select .pane_inner{
|
69
|
+
margin-right: -40px; /* grow the right side of the pane by the same as the left side */
|
70
|
+
}
|
71
|
+
|
72
|
+
.uber_select .results_container ul{
|
73
|
+
font-family: sans-serif;
|
74
|
+
font-size: 14px;
|
75
|
+
list-style: none;
|
76
|
+
margin: 0;
|
77
|
+
padding: 0;
|
78
|
+
line-height: 1.5em;
|
79
|
+
}
|
80
|
+
|
81
|
+
.uber_select .results_container .results{
|
82
|
+
overflow: auto;
|
83
|
+
max-height: 310px;
|
84
|
+
margin: 5px 0;
|
85
|
+
}
|
86
|
+
|
87
|
+
.uber_select .results_container .results.empty{
|
88
|
+
display: none;
|
89
|
+
}
|
90
|
+
|
91
|
+
.uber_select .sublist_name{
|
92
|
+
padding: 1px 15px;
|
93
|
+
color: #888;
|
94
|
+
display: list-item;
|
95
|
+
font-size: 0.8em;
|
96
|
+
text-transform: uppercase;
|
97
|
+
margin-top: 1em;
|
98
|
+
font-weight: bold;
|
99
|
+
}
|
100
|
+
.uber_select .result{
|
101
|
+
cursor: pointer;
|
102
|
+
padding: 1px 20px;
|
103
|
+
overflow: hidden;
|
104
|
+
text-overflow: ellipsis;
|
105
|
+
}
|
106
|
+
.uber_select .result.selected{
|
107
|
+
font-weight: bold;
|
108
|
+
}
|
109
|
+
.uber_select .result.highlighted{
|
110
|
+
background-color: #C4E4FF;
|
111
|
+
color: inherit;
|
112
|
+
}
|
113
|
+
.uber_select .messages{
|
114
|
+
padding: 1px 20px;
|
115
|
+
color: #AAA;
|
116
|
+
margin: 5px 0;
|
117
|
+
font-family: sans-serif;
|
118
|
+
font-size: 14px;
|
119
|
+
line-height: 1.5;
|
120
|
+
}
|
121
|
+
|
122
|
+
.uber_select.open .selected_text{
|
123
|
+
visibility: hidden;
|
124
|
+
}
|
125
|
+
|
126
|
+
.uber_select .search_input{
|
127
|
+
font-family: sans-serif;
|
128
|
+
font-size: 20px;
|
129
|
+
width: 100%;
|
130
|
+
border: none;
|
131
|
+
border-bottom: 1px solid #E0E0E0;
|
132
|
+
padding: 0px 20px 15px;
|
133
|
+
outline: none;
|
134
|
+
box-sizing: border-box; /* IE8 */
|
135
|
+
}
|
136
|
+
|
137
|
+
.uber_select .search_input::-webkit-search-cancel-button{
|
138
|
+
-webkit-appearance: none;
|
139
|
+
}
|
140
|
+
|
141
|
+
.uber_select .clear_search_button{
|
142
|
+
cursor: pointer;
|
143
|
+
position: absolute;
|
144
|
+
top: 35px;
|
145
|
+
height: 23px;
|
146
|
+
font-size: 20px;
|
147
|
+
right: 5px;
|
148
|
+
padding: 5px;
|
149
|
+
background-color: inherit;
|
150
|
+
}
|