uber_select_rails 0.6.1 → 1.0.0

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.
@@ -1,387 +1,456 @@
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
- ariaLabel: null,
11
- value: null, // Initialize with this selectedValue
12
- disabled: false, // Initialize with this disabled value
13
- search: true, // Show the search input
14
- clearSearchButton:'✕', // Text content of clear search button
15
- selectCaret: '⌄', // Text content of select caret
16
- hideBlankOption: false, // Should blank options be hidden automatically?
17
- treatBlankOptionAsPlaceholder: false, // Should blank options use the placeholder as text?
18
- highlightByDefault: true, // Should the first result be auto-highlighted?
19
- minQueryLength: 0, // Number of characters to type before results are displayed
20
- minQueryMessage: true, // Message to show when the query doesn't exceed the minimum length. True for default, false for none, or custom message.
21
- placeholder: null, // Placeholder to show in the selected text area
22
- searchPlaceholder: 'Type to search', // Placeholder to show in the search input
23
- noResultsText: 'No Matches Found', // The message shown when there are no results
24
- resultPostprocessor: function(result, datum){}, // A function that is run after a result is built and can be used to decorate it
25
- buildResult: null, // A function that is used to build result elements
26
- outputContainer: null, // An object that receives the output once a result is selected. Must respond to setValue(value), and view()
27
- 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
28
- 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
29
- onNoHighlightSubmit: function(value) {}, // A function to run when a user presses enter without selecting a result.
30
- noDataText: 'No options' // Text to show in there is nothing in the set of data to pick from
31
- }, options)
32
-
33
- var context = this
34
- var view = $('<span class="uber_select" role="listbox"></span>')
35
- var selectedValue = options.value // Internally selected value
36
- var outputContainer = options.outputContainer || new OutputContainer({selectCaret: options.selectCaret})
37
- var resultsContainer = $('<div class="results_container"></div>')
38
- var messages = $('<div class="messages"></div>')
39
- var pane = new Pane()
40
-
41
- if (options.ariaLabel) { view.attr("aria-label", options.ariaLabel) }
42
-
43
- var searchField = new SearchField({
44
- placeholder: options.searchPlaceholder,
45
- clearButton: options.clearSearchButton,
46
- searchInputAttributes: options.searchInputAttributes
47
- })
48
-
49
- var search = new Search(searchField.input, resultsContainer, {
50
- model: {
51
- dataForMatching: dataForMatching,
52
- minQueryLength: options.minQueryLength,
53
- queryPreprocessor: options.queryPreprocessor || Search.prototype.queryPreprocessor,
54
- datumPreprocessor: options.datumPreprocessor || datumPreprocessor,
55
- patternForMatching: options.patternForMatching || Search.prototype.patternForMatching
56
- },
57
- view: {
58
- renderResults: renderResults,
59
- buildResult: options.buildResult || buildResult,
60
- keypressInput: options.search ? searchField.input : null
1
+ //= require_self
2
+ //= require_tree ./uber_search
3
+
4
+ (function($) {
5
+ window.UberSearch = function(data, options){
6
+ var eventsTriggered = {
7
+ shown: 'shown',
8
+ renderedResults: 'renderedResults',
9
+ clear: 'clear',
10
+ select: 'select'
61
11
  }
62
- })
63
12
 
13
+ options = $.extend({
14
+ uberSelectId: generateUUID(), // A unique identifier for select
15
+ ariaLabel: null, // Label of the select for screen readers
16
+ value: null, // Initialize with this selectedValue
17
+ disabled: false, // Initialize with this disabled value
18
+ search: true, // Show the search input
19
+ clearSearchButton:'&#x2715;', // Text content of clear search button
20
+ selectCaret: '&#x2304;', // Text content of select caret
21
+ hideBlankOption: false, // Should blank options be hidden automatically?
22
+ treatBlankOptionAsPlaceholder: false, // Should blank options use the placeholder as text?
23
+ highlightByDefault: true, // Should the first result be auto-highlighted?
24
+ minQueryLength: 0, // Number of characters to type before results are displayed
25
+ minQueryMessage: true, // Message to show when the query doesn't exceed the minimum length. True for default, false for none, or custom message.
26
+ placeholder: null, // Placeholder to show in the selected text area
27
+ searchPlaceholder: 'Type to search', // Placeholder to show in the search input
28
+ noResultsText: 'No Matches Found', // The message shown when there are no results
29
+ resultPostprocessor: function(result, datum){}, // A function that is run after a result is built and can be used to decorate it
30
+ buildResult: null, // A function that is used to build result elements
31
+ outputContainer: null, // An object that receives the output once a result is selected. Must respond to setValue(value), and view()
32
+ onRender: function(resultsContainer, result) {}, // A function to run when the results container is rendered. If the result returns false, the default render handler is not run and the event is cancelled
33
+ 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
34
+ onNoHighlightSubmit: function(value) {}, // A function to run when a user presses enter without selecting a result.
35
+ noDataText: 'No options', // Text to show in there is nothing in the set of data to pick from
36
+ matchGroupNames: false, // Show results for searches that match the result's group name
37
+ alwaysOpen: false // Should the options list always appear open?
38
+ }, options)
39
+
40
+ var context = this
41
+ var view = $('<span>', { class: "uber_select", id: options.uberSelectId })
42
+ var selectedValue = options.value // Internally selected value
43
+ var outputContainer = options.outputContainer || new UberSearch.OutputContainer({selectCaret: options.selectCaret, ariaLabel: options.ariaLabel})
44
+ var resultsContainer = $('<div class="results_container"></div>')
45
+ var messages = $('<div class="messages"></div>')
46
+ var pane = new UberSearch.Pane()
47
+
48
+ var searchField = new UberSearch.SearchField({
49
+ placeholder: options.searchPlaceholder,
50
+ clearButton: options.clearSearchButton,
51
+ searchInputAttributes: options.searchInputAttributes
52
+ })
64
53
 
65
- // BEHAVIOUR
54
+ var search = new UberSearch.Search(searchField.input, resultsContainer, {
55
+ model: {
56
+ dataForMatching: dataForMatching,
57
+ minQueryLength: options.minQueryLength,
58
+ queryPreprocessor: options.queryPreprocessor || UberSearch.Search.prototype.queryPreprocessor,
59
+ datumPreprocessor: options.datumPreprocessor || datumPreprocessor,
60
+ patternForMatching: options.patternForMatching || UberSearch.Search.prototype.patternForMatching
61
+ },
62
+ view: {
63
+ renderResults: renderResults,
64
+ buildResult: options.buildResult || buildResult,
65
+ }
66
+ })
66
67
 
67
- // Show the pane when the select element is clicked
68
- $(outputContainer.view).on('click', function(event){
69
- if (outputContainer.view.hasClass('disabled')) { return }
70
68
 
71
- pane.show()
72
- })
69
+ // BEHAVIOUR
73
70
 
74
- // Hide the pane when clicked out or another pane is opened
75
- $(document).on('click shown.UberSelect', function(event){
76
- if (isEventOutsidePane(event) && isEventOutsideOutputContainer(event)){
77
- pane.hide()
78
- }
79
- })
71
+ // Hide the pane when clicked out or another pane is opened
72
+ $(document).on('click shown', function(event){
73
+ if (!options.alwaysOpen && pane.isOpen() && isEventOutside(event)){
74
+ pane.hide()
75
+ }
76
+ })
80
77
 
81
- // Show the pane if the user was tabbed onto the trigger and pressed enter, space, or down arrow
82
- $(outputContainer.view).on('keyup', function(event){
83
- if (outputContainer.view.hasClass('disabled')) { return }
78
+ // Hide the pane when tabbing away from view
79
+ $(view).on('keydown', function(event){
80
+ if (!options.alwaysOpen && pane.isOpen() && event.which === 9) {
81
+ pane.hide()
82
+ }
83
+ })
84
84
 
85
- if (event.which === 32 || event.which === 40 && pane.isClosed()){
86
- pane.show()
87
- return false
88
- }
89
- else if (event.which === 13){ // toggle pane when enter is pressed
90
- pane.toggle()
91
- return false
92
- }
93
- })
85
+ $(view).on('setHighlight', function(event, result, index) {
86
+ if (index < 0 && options.search) {
87
+ setOutputContainerAria("aria-activedescendant", "")
88
+ $(searchField.input).focus()
89
+ } else if (index < 0) {
90
+ setOutputContainerAria("aria-activedescendant", "")
91
+ $(outputContainer.view).focus()
92
+ } else {
93
+ setOutputContainerAria("aria-activedescendant", result.id)
94
+ }
95
+ })
94
96
 
95
- // When the pane is opened
96
- $(pane).on('shown', function(){
97
- search.clear()
98
- markSelected()
99
- view.addClass('open')
97
+ $(view).on('inputDownArrow', function(event) {
98
+ search.stepHighlight(1)
99
+ })
100
100
 
101
- if (options.search) {
102
- $(searchField.input).focus()
103
- } else {
104
- pane.view.find("ul.results li:first").focus()
105
- }
101
+ $(view).on('inputUpArrow', function(event) {
102
+ outputContainer.view.focus()
103
+ })
106
104
 
107
- triggerEvent(eventsTriggered.shown)
108
- })
105
+ // Show the pane if the user was tabbed onto the trigger and pressed enter, space, or down arrow
106
+ $(outputContainer.view).on('keydown', function(event){
107
+ if (outputContainer.view.hasClass('disabled')) { return }
108
+
109
+ if (event.which === 40) { // open and focus pane when down key is pressed
110
+ if (pane.isClosed()) {
111
+ pane.show()
112
+ } else if (options.search) {
113
+ $(searchField.input).focus()
114
+ } else {
115
+ search.stepHighlight(1)
116
+ }
117
+ return false
118
+ }
109
119
 
120
+ if (event.which === 32 || event.which === 13){ // toggle pane when space or enter is pressed
121
+ pane.toggle()
122
+ return false
123
+ }
124
+ })
110
125
 
111
- // When the pane is hidden
112
- $(pane).on('hidden', function(){
113
- view.removeClass('open')
114
- view.focus()
115
- })
126
+ // Show the pane when the select element is clicked
127
+ $(outputContainer.view).on('click', function(event){
128
+ if (outputContainer.view.hasClass('disabled')) { return }
116
129
 
117
- // When the query is changed
118
- $(search).on('queryChanged', function(){
119
- updateMessages()
120
- })
130
+ pane.show()
131
+ })
121
132
 
122
- // When the search results are rendered
123
- $(search).on('renderedResults', function(event){
124
- if (options.onRender(resultsContainer, getSelection()) === false) {
125
- event.stopPropagation()
126
- return
127
- }
133
+ // When the pane is opened
134
+ $(pane).on('shown', function(){
135
+ setOutputContainerAria('aria-expanded', true)
136
+ search.clear()
137
+ markSelected(true)
138
+ view.addClass('open')
128
139
 
129
- markSelected()
130
- updateMessages()
131
- triggerEvent(eventsTriggered.renderedResults)
132
- })
140
+ if (options.search) {
141
+ $(searchField.input).focus()
142
+ }
133
143
 
134
- // When the search field is cleared
135
- $(searchField).on('clear', function(){
136
- triggerEvent(eventsTriggered.clear)
137
- })
144
+ triggerEvent(eventsTriggered.shown)
145
+ })
138
146
 
139
- // When a search result is chosen
140
- resultsContainer.on('click', '.result:not(.disabled)', function(event){
141
- var datum = $(this).data()
147
+ // When the pane is hidden
148
+ $(pane).on('hidden', function(){
149
+ setOutputContainerAria('aria-expanded', false)
150
+ view.removeClass('open')
151
+ view.focus()
152
+ })
142
153
 
143
- if (options.onSelect(datum, this, event) === false) {
144
- event.stopPropagation()
145
- return
146
- }
154
+ // When the query is changed
155
+ $(search).on('queryChanged', function(){
156
+ updateMessages()
157
+ })
147
158
 
148
- event.stopPropagation();
159
+ // When the search results are rendered
160
+ $(search).on('renderedResults', function(event){
161
+ if (options.onRender(resultsContainer, getSelection()) === false) {
162
+ event.stopPropagation()
163
+ return
164
+ }
149
165
 
150
- setValue(valueFromResult(this))
151
- pane.hide()
152
- triggerEvent(eventsTriggered.select, [datum, this, event])
153
- })
166
+ markSelected()
167
+ updateMessages()
168
+ triggerEvent(eventsTriggered.renderedResults)
169
+ })
154
170
 
155
- // When query is submitted
156
- $(searchField.input).on('noHighlightSubmit', function(event) {
157
- options.onNoHighlightSubmit($(this).val())
158
- })
171
+ // When the search field is cleared
172
+ $(searchField).on('clear', function(){
173
+ triggerEvent(eventsTriggered.clear)
174
+ })
159
175
 
176
+ // When a search result is chosen
177
+ resultsContainer.on('click', '.result:not(.disabled)', function(event){
178
+ var datum = $(this).data()
160
179
 
161
- // INITIALIZATION
180
+ if (options.onSelect(datum, this, event) === false) {
181
+ event.stopPropagation()
182
+ return
183
+ }
162
184
 
163
- setDisabled(options.disabled)
164
- setData(data)
185
+ event.stopPropagation();
165
186
 
166
- if (options.search) { pane.addContent('search', searchField.view) }
167
- pane.addContent('messages', messages)
168
- pane.addContent('results', resultsContainer)
187
+ setValue(valueFromResult(this))
188
+ if (!options.alwaysOpen) {
189
+ pane.hide()
190
+ }
191
+ triggerEvent(eventsTriggered.select, [datum, this, event])
192
+ })
169
193
 
170
- // If the output container isn't in the DOM yet, add it
171
- if (!$(outputContainer.view).closest('body').length){
172
- $(outputContainer.view).appendTo(view)
173
- }
194
+ // When query is submitted
195
+ $(searchField.input).on('noHighlightSubmit', function(event) {
196
+ options.onNoHighlightSubmit($(this).val())
197
+ })
174
198
 
175
- $(view).append(pane.view)
176
199
 
177
- updateMessages()
178
- updateSelectedText()
179
- markSelected()
200
+ // INITIALIZATION
180
201
 
202
+ setDisabled(options.disabled)
203
+ setData(data)
181
204
 
182
- // HELPER FUNCTIONS
205
+ if (options.search) { pane.addContent('search', searchField.view) }
206
+ pane.addContent('messages', messages)
207
+ pane.addContent('results', resultsContainer)
183
208
 
184
- function setData(newData){
185
- data = setDataDefaults(newData)
186
- search.setData(data)
187
- updateSelectedText()
188
- markSelected()
189
- }
209
+ // If the output container isn't in the DOM yet, add it
210
+ if (!$(outputContainer.view).closest('body').length){
211
+ $(outputContainer.view).appendTo(view)
212
+ }
213
+
214
+ $(view).append(pane.view)
190
215
 
191
- // Selects the result corresponding to the given value
192
- function setValue(value){
193
- if (selectedValue == value) { return }
194
- selectedValue = value
216
+ updateMessages()
195
217
  updateSelectedText()
196
218
  markSelected()
197
- }
198
219
 
199
- // Returns the selected value
200
- function getValue(){
201
- return selectedValue
202
- }
220
+ if (options.alwaysOpen) {
221
+ pane.show()
222
+ }
203
223
 
204
- // Enables or disables UberSearch
205
- function setDisabled(boolean){
206
- outputContainer.setDisabled(boolean)
207
- }
208
224
 
209
- // Updates the enhanced select with the text of the selected result
210
- function setSelectedText(text){
211
- if (text) {
212
- outputContainer.setValue(text)
213
- } else {
214
- outputContainer.setValue(options.placeholder)
225
+ // HELPER FUNCTIONS
226
+ function generateUUID() {
227
+ // https://www.w3resource.com/javascript-exercises/javascript-math-exercise-23.php
228
+ var dt = new Date().getTime();
229
+ var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
230
+ var r = (dt + Math.random()*16)%16 | 0;
231
+ dt = Math.floor(dt/16);
232
+ return (c=='x' ? r :(r&0x3|0x8)).toString(16);
233
+ });
234
+ return uuid;
215
235
  }
216
- }
217
236
 
218
- // Inherit values for matchValue and value from text
219
- function setDataDefaults(data){
220
- return $.map(data, function(datum) {
221
- return $.extend({ value: datum.text, matchValue: datum.text }, datum)
222
- })
223
- }
237
+ function setData(newData){
238
+ data = setDataDefaults(newData)
239
+ search.setData(data)
240
+ updateSelectedText()
241
+ markSelected()
242
+ }
224
243
 
225
- // Converts the dataFromSelect into a datum list for matching
226
- function dataForMatching(processedQuery, data){
227
- // If a query is present, include only select options that should be used when searching
228
- // Else, include only options that should be visible when not searching
229
- if (processedQuery) {
230
- return $.map(data, function(datum){ if (datum.visibility != 'no-query' || datum.value == selectedValue) return datum })
231
- } else {
232
- return $.map(data, function(datum){ if (datum.visibility != 'query' || datum.value == selectedValue) return datum })
244
+ // Selects the result corresponding to the given value
245
+ function setValue(value){
246
+ if (selectedValue == value) { return }
247
+ selectedValue = value
248
+ updateSelectedText()
249
+ markSelected()
233
250
  }
234
- }
235
251
 
236
- // Match against the datum.matchValue
237
- function datumPreprocessor(datum){
238
- return datum.matchValue
239
- }
252
+ // Returns the selected value
253
+ function getValue(){
254
+ return selectedValue
255
+ }
256
+
257
+ // Enables or disables UberSearch
258
+ function setDisabled(boolean){
259
+ outputContainer.setDisabled(boolean)
260
+ }
240
261
 
241
- // Adds group support and blank option hiding
242
- function renderResults(data){
243
- var context = this
244
- var sourceArray = []
262
+ // Updates the enhanced select with the text of the selected result
263
+ function setSelectedText(text){
264
+ if (text) {
265
+ outputContainer.setValue(text)
266
+ } else {
267
+ outputContainer.setValue(options.placeholder)
268
+ }
269
+ }
245
270
 
246
- $.each(data, function(_, datum){
247
- // Add the group name so we can group items
248
- var result = context.buildResult(datum).attr('data-group', datum.group)
271
+ // Inherit values for matchValue and value from text
272
+ function setDataDefaults(data){
273
+ return $.map(data, function(datum) {
274
+ return $.extend({ value: datum.text, matchValue: datum.text }, datum)
275
+ })
276
+ }
249
277
 
250
- // Omit blank option from results
251
- if (!options.hideBlankOption || datum.value){
252
- sourceArray.push(result)
278
+ // Converts the dataFromSelect into a datum list for matching
279
+ function dataForMatching(processedQuery, data){
280
+ // If a query is present, include only select options that should be used when searching
281
+ // Else, include only options that should be visible when not searching
282
+ if (processedQuery) {
283
+ return $.map(data, function(datum){ if (datum.visibility != 'no-query' || datum.value == selectedValue) return datum })
284
+ } else {
285
+ return $.map(data, function(datum){ if (datum.visibility != 'query' || datum.value == selectedValue) return datum })
253
286
  }
254
- })
287
+ }
288
+
289
+ // Match against the datum.group and datum.matchValue
290
+ function datumPreprocessor(datum){
291
+ if (options.matchGroupNames && datum.group) {
292
+ return datum.group + " " + datum.matchValue
293
+ } else {
294
+ return datum.matchValue
295
+ }
296
+ }
255
297
 
256
- // Arrange ungrouped list items
257
- var destArray = reject(sourceArray, 'li:not([data-group])')
298
+ // Adds group support and blank option hiding
299
+ function renderResults(data){
300
+ var context = this
301
+ var sourceArray = []
258
302
 
259
- // Arrange list items into sub lists
260
- while (sourceArray.length) {
261
- var group = $(sourceArray[0]).attr('data-group')
262
- var groupNodes = reject(sourceArray, 'li[data-group="' + group + '"]')
263
- var sublist = $('<ul class="sublist"></ul>').attr('data-group', group)
264
- var sublistNode = $('<li></li>').append('<span class="sublist_name">' + group + '</span>')
303
+ $.each(data, function(index, datum){
304
+ // Add the group name so we can group items
305
+ var result = context.buildResult(index, datum).attr('data-group', datum.group)
265
306
 
266
- sublist.append(groupNodes)
267
- sublistNode.append(sublist)
307
+ // Omit blank option from results
308
+ if (!options.hideBlankOption || datum.value){
309
+ sourceArray.push(result)
310
+ }
311
+ })
268
312
 
269
- destArray.push(sublistNode)
270
- }
313
+ // Arrange ungrouped list items
314
+ var destArray = reject(sourceArray, 'li:not([data-group])')
271
315
 
272
- this.view.toggleClass('empty', !destArray.length)
273
- this.view.html(destArray)
274
- }
316
+ // Arrange list items into sub lists
317
+ while (sourceArray.length) {
318
+ var group = $(sourceArray[0]).attr('data-group')
319
+ var groupNodes = reject(sourceArray, 'li[data-group="' + group + '"]')
320
+ var sublist = $('<ul>', { class: "sublist", 'data-group': group })
321
+ var sublistNode = $('<li>', { role: "listitem", 'aria-label': group }).append($('<span>', { class: "sublist_name" }).html(group))
275
322
 
276
- // Removes elements from the sourcArray that match the selector
277
- // Returns an array of removed elements
278
- function reject(sourceArray, selector){
279
- var dest = filter(sourceArray, selector)
280
- var source = filter(sourceArray, selector, true)
281
- sourceArray.splice(0, sourceArray.length)
282
- sourceArray.push.apply(sourceArray, source)
283
- return dest
284
- }
323
+ sublist.append(groupNodes)
324
+ sublistNode.append(sublist)
285
325
 
286
- function filter(sourceArray, selector, invert){
287
- return $.grep(sourceArray, function(node){ return node.is(selector) }, invert)
288
- }
326
+ destArray.push(sublistNode)
327
+ }
289
328
 
290
- function buildResult(datum){
291
- var result = $('<li class="result" tabindex="0"></li>')
292
- .html((options.treatBlankOptionAsPlaceholder ? datum.text || options.placeholder : datum.text) || "&nbsp;")
293
- .data(datum) // Store the datum so we can get know what the value of the selected item is
329
+ this.view.toggleClass('empty', !destArray.length)
330
+ this.view.html(destArray)
331
+ }
294
332
 
295
- if (datum.title) { result.attr('title', datum.title) }
296
- if (datum.disabled) { result.addClass('disabled') }
333
+ // Removes elements from the sourcArray that match the selector
334
+ // Returns an array of removed elements
335
+ function reject(sourceArray, selector){
336
+ var dest = filter(sourceArray, selector)
337
+ var source = filter(sourceArray, selector, true)
338
+ sourceArray.splice(0, sourceArray.length)
339
+ sourceArray.push.apply(sourceArray, source)
340
+ return dest
341
+ }
297
342
 
298
- options.resultPostprocessor(result, datum)
343
+ function filter(sourceArray, selector, invert){
344
+ return $.grep(sourceArray, function(node){ return node.is(selector) }, invert)
345
+ }
299
346
 
300
- return result
301
- }
347
+ function buildResult(index, datum){
348
+ var text = (options.treatBlankOptionAsPlaceholder ? datum.text || options.placeholder : datum.text);
302
349
 
303
- function markSelected(){
304
- var selected = getSelection()
305
- var results = search.getResults()
350
+ var result = $('<li class="result" role="listitem" tabindex="-1"></li>') // Use -1 tabindex so that the result can be focusable but not tabbable.
351
+ .attr('id', (options.uberSelectId + "-" + index))
352
+ .text(text || String.fromCharCode(160)) // Insert text or &nbsp;
353
+ .data(datum) // Store the datum so we can get know what the value of the selected item is
306
354
 
307
- $(results).filter('.selected').not(selected).removeClass('selected')
355
+ if (datum.title) { result.attr('title', datum.title) }
356
+ if (datum.disabled) { result.addClass('disabled') }
308
357
 
309
- // Ensure the selected result is unhidden
310
- $(selected).addClass('selected').removeClass('hidden')
358
+ options.resultPostprocessor(result, datum)
311
359
 
312
- if (selected) {
313
- search.highlightResult(selected)
314
- } else if (options.highlightByDefault) {
315
- search.highlightResult(results.not('.hidden').not('.disabled').first())
360
+ return result
316
361
  }
317
- }
318
362
 
319
- // Returns the selected element and its index
320
- function getSelection(){
321
- var results = search.getResults()
322
- var selected
323
- $.each(results, function(i, result){
324
- if (selectedValue == valueFromResult(result)){
325
- selected = result
326
- return false
363
+ function markSelected(focus) {
364
+ focus = focus || false
365
+ var selected = getSelection()
366
+ var results = search.getResults()
367
+
368
+ $(results).filter('.selected').not(selected).removeClass('selected').attr('aria-selected', false)
369
+
370
+ // Ensure the selected result is unhidden
371
+ $(selected).addClass('selected').removeClass('hidden')
372
+ $(selected).attr('aria-selected', true)
373
+
374
+ if (selected) {
375
+ search.highlightResult(selected, { focus: focus })
376
+ } else if (options.highlightByDefault) {
377
+ search.highlightResult(results.not('.hidden').not('.disabled').first(), { focus: focus })
327
378
  }
328
- })
329
- return selected
330
- }
379
+ }
331
380
 
332
- function valueFromResult(result){
333
- return $(result).data('value')
334
- }
381
+ // Returns the selected element and its index
382
+ function getSelection(){
383
+ var results = search.getResults()
384
+ var selected
385
+ $.each(results, function(i, result){
386
+ if (selectedValue == valueFromResult(result)){
387
+ selected = result
388
+ return false
389
+ }
390
+ })
391
+ return selected
392
+ }
335
393
 
336
- function updateSelectedText(){
337
- setSelectedText(textFromValue(selectedValue))
338
- }
394
+ function valueFromResult(result){
395
+ return $(result).data('value')
396
+ }
397
+
398
+ function updateSelectedText(){
399
+ setSelectedText(textFromValue(selectedValue))
400
+ }
339
401
 
340
- function textFromValue(value){
341
- return $.map(data, function(datum) {
342
- if (datum.value == value) {
343
- return datum.selectedText || datum.text
402
+ function textFromValue(value){
403
+ return $.map(data, function(datum) {
404
+ if (datum.value == value) {
405
+ return datum.selectedText || datum.text
406
+ }
407
+ })[0]
408
+ }
409
+
410
+ function updateMessages(){
411
+ messages.show()
412
+ if (!queryLength() && !resultsCount()){
413
+ messages.html(options.noDataText)
414
+ } else if (options.minQueryLength && options.minQueryMessage && queryLength() < options.minQueryLength){
415
+ messages.html(options.minQueryMessage === true ? 'Type at least ' + options.minQueryLength + (options.minQueryLength == 1 ? ' character' : ' characters') + ' to search' : options.minQueryMessage)
416
+ } else if (options.noResultsText && !resultsCount()){
417
+ messages.html(options.noResultsText)
418
+ } else {
419
+ messages.empty().hide()
344
420
  }
345
- })[0]
346
- }
421
+ }
347
422
 
348
- function updateMessages(){
349
- messages.show()
350
- if (!queryLength() && !resultsCount()){
351
- messages.html(options.noDataText)
352
- } else if (options.minQueryLength && options.minQueryMessage && queryLength() < options.minQueryLength){
353
- messages.html(options.minQueryMessage === true ? 'Type at least ' + options.minQueryLength + (options.minQueryLength == 1 ? ' character' : ' characters') + ' to search' : options.minQueryMessage)
354
- } else if (options.noResultsText && !resultsCount()){
355
- messages.html(options.noResultsText)
356
- } else {
357
- messages.empty().hide()
423
+ function queryLength(){
424
+ return search.getQuery().length
358
425
  }
359
- }
360
426
 
361
- function queryLength(){
362
- return search.getQuery().length
363
- }
427
+ function resultsCount(){
428
+ return search.getResults().length
429
+ }
364
430
 
365
- function resultsCount(){
366
- return search.getResults().length
367
- }
431
+ function setOutputContainerAria() {
432
+ outputContainer.view.attr.apply(outputContainer.view, arguments)
433
+ }
368
434
 
369
- // returns true if the event originated outside the pane
370
- function isEventOutsidePane(event){
371
- return !$(event.target).closest(pane.view).length
372
- }
435
+ // returns true if the event originated outside this component
436
+ function isEventOutside(event){
437
+ event = event.originalEvent || event // Handle both jQuery events and standard JS events
373
438
 
374
- function isEventOutsideOutputContainer(event){
375
- return !$(event.target).closest(outputContainer.view).length
376
- }
439
+ if (event.composed) { // Support UberSelect when used in the Shadow DOM
440
+ return !event.composedPath().includes(view[0])
441
+ } else {
442
+ return !$(event.target).closest(view).length
443
+ }
444
+ }
377
445
 
378
- // Allow observer to be attached to the UberSearch itself
379
- function triggerEvent(eventType, callbackArgs){
380
- view.trigger(eventType, callbackArgs)
381
- $(context).trigger(eventType, callbackArgs)
382
- }
446
+ // Allow observer to be attached to the UberSearch itself
447
+ function triggerEvent(eventType, callbackArgs){
448
+ view.trigger(eventType, callbackArgs)
449
+ $(context).triggerHandler(eventType, callbackArgs)
450
+ }
383
451
 
384
- // PUBLIC INTERFACE
452
+ // PUBLIC INTERFACE
385
453
 
386
- $.extend(this, {view:view, searchField:searchField, setValue:setValue, getValue: getValue, setData:setData, setDisabled:setDisabled, getSelection:getSelection, options:options})
387
- }
454
+ $.extend(this, {view:view, searchField:searchField, setValue:setValue, getValue: getValue, setData:setData, setDisabled:setDisabled, getSelection:getSelection, options:options})
455
+ }
456
+ })(jQuery)