uber_select_rails 0.1.2 → 0.4.0

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