uber_select_rails 0.6.0 → 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,371 +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
- // Show the pane if the user was tabbed onto the trigger and pressed enter, space, or down arrow
75
- $(outputContainer.view).on('keyup', function(event){
76
- if (outputContainer.view.hasClass('disabled')) { return }
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
+ })
77
77
 
78
- if (event.which === 32 || event.which === 40 && pane.isClosed()){
79
- pane.show()
80
- return false
81
- }
82
- else if (event.which === 13){ // toggle pane when enter is pressed
83
- pane.toggle()
84
- return false
85
- }
86
- })
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
+ })
87
84
 
88
- // When the pane is opened
89
- $(pane).on('shown', function(){
90
- search.clear()
91
- markSelected()
92
- view.addClass('open')
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
+ })
93
96
 
94
- if (options.search) {
95
- $(searchField.input).focus()
96
- } else {
97
- pane.view.find("ul.results li:first").focus()
98
- }
97
+ $(view).on('inputDownArrow', function(event) {
98
+ search.stepHighlight(1)
99
+ })
99
100
 
100
- triggerEvent(eventsTriggered.shown)
101
- })
101
+ $(view).on('inputUpArrow', function(event) {
102
+ outputContainer.view.focus()
103
+ })
102
104
 
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
+ }
103
119
 
104
- // When the pane is hidden
105
- $(pane).on('hidden', function(){
106
- view.removeClass('open')
107
- view.focus()
108
- })
120
+ if (event.which === 32 || event.which === 13){ // toggle pane when space or enter is pressed
121
+ pane.toggle()
122
+ return false
123
+ }
124
+ })
109
125
 
110
- // When the query is changed
111
- $(search).on('queryChanged', function(){
112
- updateMessages()
113
- })
126
+ // Show the pane when the select element is clicked
127
+ $(outputContainer.view).on('click', function(event){
128
+ if (outputContainer.view.hasClass('disabled')) { return }
114
129
 
115
- // When the search results are rendered
116
- $(search).on('renderedResults', function(event){
117
- if (options.onRender(resultsContainer, getSelection()) === false) {
118
- event.stopPropagation()
119
- return
120
- }
130
+ pane.show()
131
+ })
121
132
 
122
- markSelected()
123
- updateMessages()
124
- triggerEvent(eventsTriggered.renderedResults)
125
- })
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')
126
139
 
127
- // When the search field is cleared
128
- $(searchField).on('clear', function(){
129
- triggerEvent(eventsTriggered.clear)
130
- })
140
+ if (options.search) {
141
+ $(searchField.input).focus()
142
+ }
131
143
 
132
- // When a search result is chosen
133
- resultsContainer.on('click', '.result:not(.disabled)', function(event){
134
- var datum = $(this).data()
144
+ triggerEvent(eventsTriggered.shown)
145
+ })
135
146
 
136
- if (options.onSelect(datum, this, event) === false) {
137
- event.stopPropagation()
138
- return
139
- }
147
+ // When the pane is hidden
148
+ $(pane).on('hidden', function(){
149
+ setOutputContainerAria('aria-expanded', false)
150
+ view.removeClass('open')
151
+ view.focus()
152
+ })
140
153
 
141
- event.stopPropagation();
154
+ // When the query is changed
155
+ $(search).on('queryChanged', function(){
156
+ updateMessages()
157
+ })
142
158
 
143
- setValue(valueFromResult(this))
144
- pane.hide()
145
- triggerEvent(eventsTriggered.select, [datum, this, event])
146
- })
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
+ }
147
165
 
148
- // When query is submitted
149
- $(searchField.input).on('noHighlightSubmit', function(event) {
150
- options.onNoHighlightSubmit($(this).val())
151
- })
166
+ markSelected()
167
+ updateMessages()
168
+ triggerEvent(eventsTriggered.renderedResults)
169
+ })
152
170
 
171
+ // When the search field is cleared
172
+ $(searchField).on('clear', function(){
173
+ triggerEvent(eventsTriggered.clear)
174
+ })
153
175
 
154
- // INITIALIZATION
176
+ // When a search result is chosen
177
+ resultsContainer.on('click', '.result:not(.disabled)', function(event){
178
+ var datum = $(this).data()
155
179
 
156
- setDisabled(options.disabled)
157
- setData(data)
180
+ if (options.onSelect(datum, this, event) === false) {
181
+ event.stopPropagation()
182
+ return
183
+ }
158
184
 
159
- if (options.search) { pane.addContent('search', searchField.view) }
160
- pane.addContent('messages', messages)
161
- pane.addContent('results', resultsContainer)
185
+ event.stopPropagation();
162
186
 
163
- // If the output container isn't in the DOM yet, add it
164
- if (!$(outputContainer.view).closest('body').length){
165
- $(outputContainer.view).appendTo(view)
166
- }
187
+ setValue(valueFromResult(this))
188
+ if (!options.alwaysOpen) {
189
+ pane.hide()
190
+ }
191
+ triggerEvent(eventsTriggered.select, [datum, this, event])
192
+ })
167
193
 
168
- $(view).append(pane.view)
194
+ // When query is submitted
195
+ $(searchField.input).on('noHighlightSubmit', function(event) {
196
+ options.onNoHighlightSubmit($(this).val())
197
+ })
169
198
 
170
- updateMessages()
171
- updateSelectedText()
172
- markSelected()
173
199
 
200
+ // INITIALIZATION
174
201
 
175
- // HELPER FUNCTIONS
202
+ setDisabled(options.disabled)
203
+ setData(data)
176
204
 
177
- function setData(newData){
178
- data = setDataDefaults(newData)
179
- search.setData(data)
180
- updateSelectedText()
181
- markSelected()
182
- }
205
+ if (options.search) { pane.addContent('search', searchField.view) }
206
+ pane.addContent('messages', messages)
207
+ pane.addContent('results', resultsContainer)
183
208
 
184
- // Selects the result corresponding to the given value
185
- function setValue(value){
186
- if (selectedValue == value) { return }
187
- selectedValue = value
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)
215
+
216
+ updateMessages()
188
217
  updateSelectedText()
189
218
  markSelected()
190
- }
191
219
 
192
- // Returns the selected value
193
- function getValue(){
194
- return selectedValue
195
- }
220
+ if (options.alwaysOpen) {
221
+ pane.show()
222
+ }
196
223
 
197
- // Enables or disables UberSearch
198
- function setDisabled(boolean){
199
- outputContainer.setDisabled(boolean)
200
- }
201
224
 
202
- // Updates the enhanced select with the text of the selected result
203
- function setSelectedText(text){
204
- if (text) {
205
- outputContainer.setValue(text)
206
- } else {
207
- 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;
208
235
  }
209
- }
210
236
 
211
- // Inherit values for matchValue and value from text
212
- function setDataDefaults(data){
213
- return $.map(data, function(datum) {
214
- return $.extend({ value: datum.text, matchValue: datum.text }, datum)
215
- })
216
- }
237
+ function setData(newData){
238
+ data = setDataDefaults(newData)
239
+ search.setData(data)
240
+ updateSelectedText()
241
+ markSelected()
242
+ }
217
243
 
218
- // Converts the dataFromSelect into a datum list for matching
219
- function dataForMatching(processedQuery, data){
220
- // If a query is present, include only select options that should be used when searching
221
- // Else, include only options that should be visible when not searching
222
- if (processedQuery) {
223
- return $.map(data, function(datum){ if (datum.visibility != 'no-query' || datum.value == selectedValue) return datum })
224
- } else {
225
- 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()
226
250
  }
227
- }
228
251
 
229
- // Match against the datum.matchValue
230
- function datumPreprocessor(datum){
231
- return datum.matchValue
232
- }
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
+ }
233
261
 
234
- // Adds group support and blank option hiding
235
- function renderResults(data){
236
- var context = this
237
- 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
+ }
238
270
 
239
- $.each(data, function(_, datum){
240
- // Add the group name so we can group items
241
- 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
+ }
242
277
 
243
- // Omit blank option from results
244
- if (!options.hideBlankOption || datum.value){
245
- 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 })
246
286
  }
247
- })
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
+ }
248
297
 
249
- // Arrange ungrouped list items
250
- 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 = []
251
302
 
252
- // Arrange list items into sub lists
253
- while (sourceArray.length) {
254
- var group = $(sourceArray[0]).attr('data-group')
255
- var groupNodes = reject(sourceArray, 'li[data-group="' + group + '"]')
256
- var sublist = $('<ul class="sublist"></ul>').attr('data-group', group)
257
- 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)
258
306
 
259
- sublist.append(groupNodes)
260
- sublistNode.append(sublist)
307
+ // Omit blank option from results
308
+ if (!options.hideBlankOption || datum.value){
309
+ sourceArray.push(result)
310
+ }
311
+ })
261
312
 
262
- destArray.push(sublistNode)
263
- }
313
+ // Arrange ungrouped list items
314
+ var destArray = reject(sourceArray, 'li:not([data-group])')
264
315
 
265
- this.view.toggleClass('empty', !destArray.length)
266
- this.view.html(destArray)
267
- }
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))
268
322
 
269
- // Removes elements from the sourcArray that match the selector
270
- // Returns an array of removed elements
271
- function reject(sourceArray, selector){
272
- var dest = filter(sourceArray, selector)
273
- var source = filter(sourceArray, selector, true)
274
- sourceArray.splice(0, sourceArray.length)
275
- sourceArray.push.apply(sourceArray, source)
276
- return dest
277
- }
323
+ sublist.append(groupNodes)
324
+ sublistNode.append(sublist)
278
325
 
279
- function filter(sourceArray, selector, invert){
280
- return $.grep(sourceArray, function(node){ return node.is(selector) }, invert)
281
- }
326
+ destArray.push(sublistNode)
327
+ }
282
328
 
283
- function buildResult(datum){
284
- var result = $('<li class="result" tabindex="0"></li>')
285
- .html((options.treatBlankOptionAsPlaceholder ? datum.text || options.placeholder : datum.text) || "&nbsp;")
286
- .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
+ }
287
332
 
288
- if (datum.title) { result.attr('title', datum.title) }
289
- 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
+ }
290
342
 
291
- options.resultPostprocessor(result, datum)
343
+ function filter(sourceArray, selector, invert){
344
+ return $.grep(sourceArray, function(node){ return node.is(selector) }, invert)
345
+ }
292
346
 
293
- return result
294
- }
347
+ function buildResult(index, datum){
348
+ var text = (options.treatBlankOptionAsPlaceholder ? datum.text || options.placeholder : datum.text);
295
349
 
296
- function markSelected(){
297
- var selected = getSelection()
298
- 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
299
354
 
300
- $(results).filter('.selected').not(selected).removeClass('selected')
355
+ if (datum.title) { result.attr('title', datum.title) }
356
+ if (datum.disabled) { result.addClass('disabled') }
301
357
 
302
- // Ensure the selected result is unhidden
303
- $(selected).addClass('selected').removeClass('hidden')
358
+ options.resultPostprocessor(result, datum)
304
359
 
305
- if (selected) {
306
- search.highlightResult(selected)
307
- } else if (options.highlightByDefault) {
308
- search.highlightResult(results.not('.hidden').not('.disabled').first())
360
+ return result
309
361
  }
310
- }
311
362
 
312
- // Returns the selected element and its index
313
- function getSelection(){
314
- var results = search.getResults()
315
- var selected
316
- $.each(results, function(i, result){
317
- if (selectedValue == valueFromResult(result)){
318
- selected = result
319
- 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 })
320
378
  }
321
- })
322
- return selected
323
- }
379
+ }
324
380
 
325
- function valueFromResult(result){
326
- return $(result).data('value')
327
- }
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
+ }
328
393
 
329
- function updateSelectedText(){
330
- setSelectedText(textFromValue(selectedValue))
331
- }
394
+ function valueFromResult(result){
395
+ return $(result).data('value')
396
+ }
397
+
398
+ function updateSelectedText(){
399
+ setSelectedText(textFromValue(selectedValue))
400
+ }
332
401
 
333
- function textFromValue(value){
334
- return $.map(data, function(datum) {
335
- if (datum.value == value) {
336
- 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()
337
420
  }
338
- })[0]
339
- }
421
+ }
340
422
 
341
- function updateMessages(){
342
- messages.show()
343
- if (!queryLength() && !resultsCount()){
344
- messages.html(options.noDataText)
345
- } else if (options.minQueryLength && options.minQueryMessage && queryLength() < options.minQueryLength){
346
- messages.html(options.minQueryMessage === true ? 'Type at least ' + options.minQueryLength + (options.minQueryLength == 1 ? ' character' : ' characters') + ' to search' : options.minQueryMessage)
347
- } else if (options.noResultsText && !resultsCount()){
348
- messages.html(options.noResultsText)
349
- } else {
350
- messages.empty().hide()
423
+ function queryLength(){
424
+ return search.getQuery().length
351
425
  }
352
- }
353
426
 
354
- function queryLength(){
355
- return search.getQuery().length
356
- }
427
+ function resultsCount(){
428
+ return search.getResults().length
429
+ }
357
430
 
358
- function resultsCount(){
359
- return search.getResults().length
360
- }
431
+ function setOutputContainerAria() {
432
+ outputContainer.view.attr.apply(outputContainer.view, arguments)
433
+ }
361
434
 
362
- // Allow observer to be attached to the UberSearch itself
363
- function triggerEvent(eventType, callbackArgs){
364
- view.trigger(eventType, callbackArgs)
365
- $(context).trigger(eventType, callbackArgs)
366
- }
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
367
438
 
368
- // PUBLIC INTERFACE
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
+ }
445
+
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
+ }
451
+
452
+ // PUBLIC INTERFACE
369
453
 
370
- $.extend(this, {view:view, searchField:searchField, setValue:setValue, getValue: getValue, setData:setData, setDisabled:setDisabled, getSelection:getSelection, options:options})
371
- }
454
+ $.extend(this, {view:view, searchField:searchField, setValue:setValue, getValue: getValue, setData:setData, setDisabled:setDisabled, getSelection:getSelection, options:options})
455
+ }
456
+ })(jQuery)