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.
@@ -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,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) || "&nbsp;")
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
+ }