uber_select_rails 0.1.2 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|