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.
@@ -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
+ }