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.
- checksums.yaml +5 -5
- data/lib/uber_select_rails/version.rb +1 -1
- data/uber_select.gemspec +1 -1
- data/vendor/assets/javascript/uber_select/.gitignore +18 -0
- data/vendor/assets/javascript/uber_select/README.md +250 -0
- data/vendor/assets/javascript/uber_select/javascript/jquery.uber-select.js +177 -0
- data/vendor/assets/javascript/uber_select/javascript/list.js +120 -0
- data/vendor/assets/javascript/uber_select/javascript/output_container.js +21 -0
- data/vendor/assets/javascript/uber_select/javascript/pane.js +90 -0
- data/vendor/assets/javascript/uber_select/javascript/search.js +151 -0
- data/vendor/assets/javascript/uber_select/javascript/search_field.js +80 -0
- data/vendor/assets/javascript/uber_select/javascript/string_extensions.js +8 -0
- data/vendor/assets/javascript/uber_select/javascript/uber_search.js +315 -0
- data/vendor/assets/javascript/uber_select/test.css +150 -0
- data/vendor/assets/javascript/uber_select/test.html +184 -0
- metadata +15 -3
@@ -0,0 +1,120 @@
|
|
1
|
+
function List(options) {
|
2
|
+
var context = this
|
3
|
+
|
4
|
+
var view = this.view = $('<ul class="results"></ul>')
|
5
|
+
|
6
|
+
|
7
|
+
// BEHAVIOUR
|
8
|
+
|
9
|
+
// Handle up and down arrow key presses
|
10
|
+
$(options.keypressInput).on('keydown', function(event){
|
11
|
+
switch (event.which) {
|
12
|
+
case 38: // Up Arrow
|
13
|
+
stepHighlight(-1, true)
|
14
|
+
return false
|
15
|
+
case 40: // Down Arrow
|
16
|
+
stepHighlight(1)
|
17
|
+
return false
|
18
|
+
case 13: // Enter
|
19
|
+
if (highlightedResult().length) {
|
20
|
+
highlightedResult().click()
|
21
|
+
} else {
|
22
|
+
$(this).trigger('noHighlightSubmit')
|
23
|
+
}
|
24
|
+
return false
|
25
|
+
}
|
26
|
+
})
|
27
|
+
|
28
|
+
// When a list item is hovered
|
29
|
+
$(view).on('mouseenter', '.result', function(){
|
30
|
+
unhighlightResults()
|
31
|
+
highlightResult(this, {scroll: false})
|
32
|
+
})
|
33
|
+
|
34
|
+
|
35
|
+
// HELPER FUNCTIONS
|
36
|
+
|
37
|
+
this.getResults = function(){
|
38
|
+
return $(view).find('.result')
|
39
|
+
}
|
40
|
+
|
41
|
+
this.renderResults = function(data){
|
42
|
+
var results = $.map(data, function(datum){
|
43
|
+
return context.buildResult(datum)
|
44
|
+
})
|
45
|
+
|
46
|
+
view.toggleClass('empty', !data.length)
|
47
|
+
|
48
|
+
view.html(results)
|
49
|
+
}
|
50
|
+
|
51
|
+
// Can be overridden to format how results are built
|
52
|
+
this.buildResult = function(datum){
|
53
|
+
return $('<li></li>').html(datum).addClass('result')
|
54
|
+
}
|
55
|
+
|
56
|
+
this.unhighlightResults = unhighlightResults
|
57
|
+
this.highlightResult = highlightResult
|
58
|
+
|
59
|
+
function stepHighlight(amount, allowUnhighlight){
|
60
|
+
var index = visibleResults().index(highlightedResult())
|
61
|
+
var result = visibleResults()[index + amount]
|
62
|
+
|
63
|
+
if (result || allowUnhighlight){
|
64
|
+
unhighlightResults()
|
65
|
+
highlightResult(result)
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
function highlightResult(result, options){
|
70
|
+
result = $(result)
|
71
|
+
options = $.extend({scroll: true}, options)
|
72
|
+
|
73
|
+
if (!result.length) { return }
|
74
|
+
|
75
|
+
var visibleResult = visibleResults().filter(result)
|
76
|
+
if (visibleResult.length) {
|
77
|
+
visibleResult.addClass('highlighted')
|
78
|
+
|
79
|
+
if (options.scroll){
|
80
|
+
scrollResultIntoView(visibleResult)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
function unhighlightResults(){
|
86
|
+
highlightedResult().removeClass('highlighted')
|
87
|
+
}
|
88
|
+
|
89
|
+
function highlightedResult(){
|
90
|
+
return results().filter('.highlighted')
|
91
|
+
}
|
92
|
+
|
93
|
+
function visibleResults(){
|
94
|
+
return results().not('.hidden')
|
95
|
+
}
|
96
|
+
|
97
|
+
function results(){
|
98
|
+
return view.find('.result')
|
99
|
+
}
|
100
|
+
|
101
|
+
function scrollResultIntoView(result){
|
102
|
+
result = $(result)
|
103
|
+
var container = result.closest('.results').css('position', 'relative') // Ensure the results container is positioned so offset is calculated correctly
|
104
|
+
var containerHeight = container.outerHeight()
|
105
|
+
var containerTop = container.get(0).scrollTop
|
106
|
+
var containerBottom = containerTop + containerHeight
|
107
|
+
var resultHeight = result.outerHeight()
|
108
|
+
var resultTop = result.get(0).offsetTop
|
109
|
+
var resultBottom = resultTop + resultHeight
|
110
|
+
|
111
|
+
if (containerBottom < resultBottom){
|
112
|
+
container.scrollTop(resultBottom - containerHeight)
|
113
|
+
} else if (containerTop > resultTop){
|
114
|
+
container.scrollTop(resultTop)
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
// INITIALIZATION
|
119
|
+
$.extend(this, options) // Allow overriding of functions
|
120
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
var OutputContainer = function(options){
|
2
|
+
options = $.extend({}, options)
|
3
|
+
var view = $('<span class="selected_text_container" tabindex=0 role="button"></span>')
|
4
|
+
var selectedText = $('<span class="selected_text"></span>').appendTo(view)
|
5
|
+
var selectCaret = $('<span class="select_caret"></span>').appendTo(view).html(options.selectCaret)
|
6
|
+
|
7
|
+
// INITIALIZATION
|
8
|
+
|
9
|
+
setValue()
|
10
|
+
|
11
|
+
// HELPER FUNCTIONS
|
12
|
+
|
13
|
+
function setValue(value){
|
14
|
+
selectedText.html(value || ' ')
|
15
|
+
view.toggleClass('empty', !value)
|
16
|
+
}
|
17
|
+
|
18
|
+
// PUBLIC INTERFACE
|
19
|
+
|
20
|
+
$.extend(this, {view: view, setValue: setValue})
|
21
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
function Pane(options){
|
2
|
+
options = $.extend({
|
3
|
+
trigger: null
|
4
|
+
}, options)
|
5
|
+
|
6
|
+
var context = this
|
7
|
+
var model = this.model = {}
|
8
|
+
var isOpen = false
|
9
|
+
var view = this.view = $('<div class="pane"></div>').toggle(isOpen)
|
10
|
+
var innerPane = $('<div class="pane_inner"></div>').appendTo(view)
|
11
|
+
|
12
|
+
|
13
|
+
// PUBLIC INTERFACE
|
14
|
+
|
15
|
+
$.extend(this, {view: view, addContent: addContent, removeContent: removeContent, show: show, hide: hide})
|
16
|
+
|
17
|
+
|
18
|
+
// BEHAVIOUR
|
19
|
+
|
20
|
+
if (options.trigger){
|
21
|
+
// Show the pane when the select element is clicked
|
22
|
+
$(options.trigger).on('click', function(event){
|
23
|
+
context.show()
|
24
|
+
})
|
25
|
+
|
26
|
+
// Show the pane if the user was tabbed onto the trigger and pressed enter or space
|
27
|
+
$(options.trigger).on('keyup', function(event){
|
28
|
+
if (event.which == 13 || event.which == 32){
|
29
|
+
context.show()
|
30
|
+
return false
|
31
|
+
}
|
32
|
+
})
|
33
|
+
}
|
34
|
+
|
35
|
+
// Hide the pane when clicked out
|
36
|
+
$(document).on('mousedown', function(event){
|
37
|
+
if (isEventOutsidePane(event) && isEventOutsideTrigger(event)){
|
38
|
+
context.hide()
|
39
|
+
}
|
40
|
+
})
|
41
|
+
|
42
|
+
// Make it possible to have elements in the pane that close it
|
43
|
+
view.on('click', '[data-behaviour~=close-pane]', function(event){
|
44
|
+
context.hide()
|
45
|
+
})
|
46
|
+
|
47
|
+
// Close the pane when the user presses escape
|
48
|
+
$(document).on('keyup', function(event){
|
49
|
+
if (event.which == 27 && isOpen){
|
50
|
+
context.hide()
|
51
|
+
options.trigger.focus()
|
52
|
+
}
|
53
|
+
})
|
54
|
+
|
55
|
+
|
56
|
+
// HELPER FUNCTIONS
|
57
|
+
|
58
|
+
function addContent(name, content){
|
59
|
+
model[name] = content
|
60
|
+
innerPane.append(content)
|
61
|
+
}
|
62
|
+
|
63
|
+
function removeContent(name){
|
64
|
+
$(model[name]).remove()
|
65
|
+
delete model['name']
|
66
|
+
}
|
67
|
+
|
68
|
+
function show(){
|
69
|
+
if (isOpen) { return }
|
70
|
+
isOpen = true
|
71
|
+
view.show()
|
72
|
+
$(context).trigger('shown')
|
73
|
+
}
|
74
|
+
function hide(){
|
75
|
+
if (!isOpen) { return }
|
76
|
+
isOpen = false
|
77
|
+
view.hide()
|
78
|
+
$(context).trigger('hidden')
|
79
|
+
}
|
80
|
+
|
81
|
+
// returns true if the event originated outside the pane
|
82
|
+
function isEventOutsidePane(event){
|
83
|
+
return !$(event.target).closest(view).length
|
84
|
+
}
|
85
|
+
|
86
|
+
function isEventOutsideTrigger(event){
|
87
|
+
return !$(event.target).closest(options.trigger).length
|
88
|
+
}
|
89
|
+
|
90
|
+
}
|
@@ -0,0 +1,151 @@
|
|
1
|
+
function Search(queryInput, resultsContainer, options){
|
2
|
+
var context = this
|
3
|
+
var model = new SearchModel(options.model)
|
4
|
+
var list = new List(options.view)
|
5
|
+
var resultsRendered = false
|
6
|
+
|
7
|
+
// HELPER FUNCTIONS
|
8
|
+
|
9
|
+
this.setData = function(data){
|
10
|
+
model.setData(data)
|
11
|
+
}
|
12
|
+
|
13
|
+
this.renderResults = function(){
|
14
|
+
list.renderResults(model.getResults())
|
15
|
+
$(this).trigger('renderedResults')
|
16
|
+
resultsRendered = true
|
17
|
+
}
|
18
|
+
|
19
|
+
this.getQuery = function(){
|
20
|
+
return model.getQuery()
|
21
|
+
}
|
22
|
+
|
23
|
+
this.getResults = function(){
|
24
|
+
return list.getResults()
|
25
|
+
}
|
26
|
+
|
27
|
+
this.clear = function(){
|
28
|
+
if (!resultsRendered){
|
29
|
+
this.renderResults()
|
30
|
+
}
|
31
|
+
|
32
|
+
if (queryInput.val() === '') {
|
33
|
+
list.unhighlightResults()
|
34
|
+
} else {
|
35
|
+
queryInput.val('').change()
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
this.highlightResult = function(element) {
|
40
|
+
list.unhighlightResults()
|
41
|
+
list.highlightResult(element)
|
42
|
+
}
|
43
|
+
|
44
|
+
// BEHAVIOUR
|
45
|
+
|
46
|
+
$(queryInput).on('searchInput', function(){
|
47
|
+
model.setQuery(this.value)
|
48
|
+
})
|
49
|
+
|
50
|
+
$(model).on('resultsUpdated', function(){
|
51
|
+
context.renderResults()
|
52
|
+
})
|
53
|
+
|
54
|
+
// Forward query change
|
55
|
+
$(model).on('queryChanged', function(){
|
56
|
+
$(context).trigger('queryChanged')
|
57
|
+
})
|
58
|
+
|
59
|
+
|
60
|
+
// INITIALIZATION
|
61
|
+
|
62
|
+
resultsContainer.html(list.view)
|
63
|
+
|
64
|
+
|
65
|
+
// PROTOTYPES
|
66
|
+
|
67
|
+
function SearchModel(options){
|
68
|
+
var data, results
|
69
|
+
var processedQuery = ''
|
70
|
+
var context = this
|
71
|
+
options = $.extend({minQueryLength: 0}, options)
|
72
|
+
|
73
|
+
this.setQuery = function(value){
|
74
|
+
value = context.queryPreprocessor(value)
|
75
|
+
|
76
|
+
if (processedQuery == value) { return }
|
77
|
+
processedQuery = value || ''
|
78
|
+
this.updateResults()
|
79
|
+
$(this).trigger('queryChanged')
|
80
|
+
}
|
81
|
+
|
82
|
+
this.getQuery = function(){
|
83
|
+
return processedQuery || ''
|
84
|
+
}
|
85
|
+
|
86
|
+
this.setData = function(value){
|
87
|
+
data = value || []
|
88
|
+
this.updateResults()
|
89
|
+
}
|
90
|
+
|
91
|
+
this.getResults = function(){
|
92
|
+
return results
|
93
|
+
}
|
94
|
+
|
95
|
+
this.updateResults = function(){
|
96
|
+
if (options.minQueryLength > processedQuery.length) {
|
97
|
+
results = []
|
98
|
+
} else if (this.isBlankQuery()){
|
99
|
+
results = $.each(this.dataForMatching(processedQuery, data), function(){ return this })
|
100
|
+
} else {
|
101
|
+
results = []
|
102
|
+
var pattern = this.patternForMatching(processedQuery)
|
103
|
+
$.each(this.dataForMatching(processedQuery, data), function(index, datum){
|
104
|
+
if (context.match(pattern, context.datumPreprocessor(datum), processedQuery)){
|
105
|
+
results.push(datum)
|
106
|
+
}
|
107
|
+
})
|
108
|
+
}
|
109
|
+
$(this).trigger('resultsUpdated')
|
110
|
+
}
|
111
|
+
|
112
|
+
this.isBlankQuery = function(){
|
113
|
+
return processedQuery === ''
|
114
|
+
}
|
115
|
+
|
116
|
+
// Can be overridden to select a subset of data for matching
|
117
|
+
// Defaults to the identity function
|
118
|
+
this.dataForMatching = function(processedQuery, data){
|
119
|
+
return data
|
120
|
+
}
|
121
|
+
|
122
|
+
// Provides a regexp for matching the processedDatum from the processedQuery
|
123
|
+
// Can be overridden to provide more sophisticated matching behaviour
|
124
|
+
this.patternForMatching = function(processedQuery){
|
125
|
+
return new RegExp(processedQuery.escapeForRegExp(), 'i')
|
126
|
+
}
|
127
|
+
|
128
|
+
// Can be overridden to provide more sophisticated matching behaviour
|
129
|
+
this.match = function(pattern, processedDatum, processedQuery){
|
130
|
+
return pattern.test(processedDatum)
|
131
|
+
}
|
132
|
+
|
133
|
+
// Can be overridden to mutate the query being used to match before matching
|
134
|
+
// Defaults to whitespace trim
|
135
|
+
this.queryPreprocessor = function(query){
|
136
|
+
return $.trim(query)
|
137
|
+
}
|
138
|
+
|
139
|
+
// Can be overridden to mutate the data the moment before it is matched
|
140
|
+
// Useful extract string from JSON datum
|
141
|
+
// Defaults to the identity function
|
142
|
+
this.datumPreprocessor = function(datum){
|
143
|
+
return datum
|
144
|
+
}
|
145
|
+
|
146
|
+
// INITIALIZATION
|
147
|
+
$.extend(this, options) // Allow overriding of functions
|
148
|
+
delete this.data // Data isn't an attribute we want to expose
|
149
|
+
this.setData(options.data)
|
150
|
+
}
|
151
|
+
}
|
@@ -0,0 +1,80 @@
|
|
1
|
+
function SearchField(options){
|
2
|
+
options = $.extend({
|
3
|
+
placeholder: 'Type to Search',
|
4
|
+
clearButton:'✕' // Text content of clear search button
|
5
|
+
}, options)
|
6
|
+
|
7
|
+
var context = this
|
8
|
+
var input = this.input = $('<input type="search" class="search_input">').attr('placeholder', options.placeholder)
|
9
|
+
var value = input.val()
|
10
|
+
var clearButton = this.clearButton = $('<span class="clear_search_button"></span>').html(options.clearButton)
|
11
|
+
var view = this.view = $('<span class="search_field_container"></span>').append(input).append(clearButton)
|
12
|
+
var eventNames = isOnInputSupported() ? 'input change' : 'keyup change'
|
13
|
+
|
14
|
+
|
15
|
+
// PUBLIC INTERFACE
|
16
|
+
|
17
|
+
$.extend(this, {refresh: refresh})
|
18
|
+
|
19
|
+
|
20
|
+
// BEHAVIOUR
|
21
|
+
|
22
|
+
// When a change is detected
|
23
|
+
input.on(eventNames, function() {
|
24
|
+
refresh() // Always refresh on input in case something has altered the state without informing us
|
25
|
+
|
26
|
+
if (input.val() == value) { return }
|
27
|
+
|
28
|
+
triggerEvent('searchInput')
|
29
|
+
value = input.val()
|
30
|
+
})
|
31
|
+
|
32
|
+
// When the clear button is pressed
|
33
|
+
clearButton.on('click', function(){
|
34
|
+
input.val('')
|
35
|
+
refresh()
|
36
|
+
input.focus()
|
37
|
+
triggerEvent('searchInput')
|
38
|
+
triggerEvent('clear')
|
39
|
+
})
|
40
|
+
|
41
|
+
// When the enter button is pressed
|
42
|
+
input.on('keydown', function(event){
|
43
|
+
if (event.which == 13){
|
44
|
+
triggerEvent('querySubmit')
|
45
|
+
}
|
46
|
+
})
|
47
|
+
|
48
|
+
|
49
|
+
// HELPER FUNCTIONS
|
50
|
+
|
51
|
+
function refresh(){
|
52
|
+
updateClearButtonVisiblity()
|
53
|
+
updateSearchInputClass()
|
54
|
+
}
|
55
|
+
|
56
|
+
function updateSearchInputClass(){
|
57
|
+
input.toggleClass('empty', !input.val())
|
58
|
+
}
|
59
|
+
|
60
|
+
function isOnInputSupported(){
|
61
|
+
// IE 8 and 9 are the only common browsers that don't completely support oninput
|
62
|
+
// IE 10 and 11 fire the input event when the element is focussed, treat them as unsupported as well
|
63
|
+
return !(navigator.userAgent.indexOf('MSIE')!==-1 || navigator.appVersion.indexOf('Trident/') > 0) // SOURCE: http://stackoverflow.com/questions/21825157/internet-explorer-11-detection
|
64
|
+
}
|
65
|
+
|
66
|
+
function updateClearButtonVisiblity(){
|
67
|
+
clearButton.toggle(!!input.val().length)
|
68
|
+
}
|
69
|
+
|
70
|
+
// Allow observer to be attached to the SearchField itself
|
71
|
+
function triggerEvent(eventType, callbackArgs){
|
72
|
+
input.trigger(eventType, callbackArgs)
|
73
|
+
$(context).trigger(eventType, callbackArgs)
|
74
|
+
}
|
75
|
+
|
76
|
+
|
77
|
+
// INITIALIZATION
|
78
|
+
|
79
|
+
refresh()
|
80
|
+
}
|