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