Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,36 @@ Then just activate the plugin on a normal select box(suggest having a blank opti
});
</script>

The combobox also has a "freeform" mode, which when activated, will allow the user to type in an option and submit it,
even if it isn't one of the selectable options in the list. To use it, simply pass a JSON option object with "freeform"
set to "true". Also, if you need to pass a freeform value back to the combobox control (for example, as a default, or
the result of a form post), you may do so by specifying an attribute of "data-value" on the select element of the
combobox:

<select class="combobox" data-value="Arizona">
<option></option>
<option value="PA">Pennsylvania</option>
<option value="CT">Connecticut</option>
<option value="NY">New York</option>
<option value="MD">Maryland</option>
<option value="VA">Virginia</option>
</select>

<script type="text/javascript">
$(document).ready(function(){
$('.combobox').combobox({freeform: true});
});
</script>

Finally, if you need to keep the values on the input of the combobox upon submit (for instance, when you are doing
validation via an AJAX call), you can pass via the JSON option object the attribute "keeponblur" set to "true":

<script type="text/javascript">
$(document).ready(function(){
$('.combobox').combobox({freeform: true, keeponblur: true});
});
</script>

## Live Example

http://dl.dropbox.com/u/21368/bootstrap-combobox/index.html
131 changes: 116 additions & 15 deletions js/bootstrap-combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@
this.$element = this.$container.find('input[type=text]')
this.$target = this.$container.find('input[type=hidden]')
this.$button = this.$container.find('.dropdown-toggle')

this.$menu = $(this.options.menu).appendTo('body')
this.matcher = this.options.matcher || this.matcher
this.sorter = this.options.sorter || this.sorter
this.highlighter = this.options.highlighter || this.highlighter
this.shown = false
this.selected = false

this.refresh()
this.transferAttributes()
this.listen()
this.getdataval()
}

/* NOTE: COMBOBOX EXTENDS BOOTSTRAP-TYPEAHEAD.js
Expand All @@ -55,6 +58,7 @@
, parse: function () {
var that = this
, map = {}
, mapi = {}
, source = []
, selected = false
, selectedValue = ''
Expand All @@ -64,14 +68,16 @@
that.options.placeholder = option.text()
return
}
map[option.text()] = option.val()
map[option.val()] = option.text()
mapi[option.text()] = option.val()
source.push(option.text())
if (option.prop('selected')) {
selected = option.text()
selectedValue = option.val()
selectedValue = option.val() ? option.val() : option.text()
}
})
this.map = map
this.mapi = mapi
if (selected) {
this.$element.val(selected)
this.$target.val(selectedValue)
Expand All @@ -90,7 +96,9 @@
this.$element.attr('required', this.$source.attr('required'))
this.$element.attr('rel', this.$source.attr('rel'))
this.$element.attr('title', this.$source.attr('title'))
this.$element.attr('class', this.$source.attr('class'))
if (!this.options.freeform) {
this.$element.attr('class', this.$source.attr('class'))
}
this.$element.attr('tabindex', this.$source.attr('tabindex'))
this.$source.removeAttr('tabindex')
}
Expand All @@ -100,6 +108,7 @@
this.clearTarget()
this.triggerChange()
this.clearElement()
this.lookup()
} else {
if (this.shown) {
this.hide()
Expand All @@ -111,13 +120,23 @@
}

, clearElement: function () {
this.$element.val('').focus()
if (!this.options.freeform) {
this.$element.val('').focus()
}
else {
this.$element.focus()
}
}

, clearTarget: function () {
this.$source.val('')
this.$target.val('')

if (!this.options.freeform) {
this.$target.val('')
}

this.$container.removeClass('combobox-selected')

this.selected = false
}

Expand All @@ -131,22 +150,50 @@
}

// modified typeahead function adding container and target handling
, select: function () {
, select: function (tab) {
if (!tab) {
var val = this.$menu.find('.active').attr('data-value')
this.$element.val(this.updater(val)).trigger('change')
this.$source.val(this.map[val]).trigger('change')
this.$target.val(this.map[val]).trigger('change')
this.$container.addClass('combobox-selected')
this.selected = true
return this.hide()
}

this.$container.addClass('combobox-selected')

this.selected = (!this.options.freeform)

return this.hide()
}

// modified typeahead function removing the blank handling and source function handling
, lookup: function (event) {
this.query = this.$element.val()
if (!this.options.freeform) {
this.query = this.$element.val()
}
else {
this.query = this.$element.val()

var check = this.matches(this.query)

if (!check) {
this.query = ''
}
}

return this.process(this.source)
}

, matches: function (item) {
for(var i in this.source) {
var pos = this.source[i].toLowerCase().indexOf(item.trim().toLowerCase())
if (pos != -1) {
return true
}
}

return false
}

// modified typeahead function adding button handling and remove mouseleave
, listen: function () {
this.$element
Expand All @@ -168,10 +215,34 @@
.on('click', $.proxy(this.toggle, this))
}

, getdataval: function () {
// get passed-in combobox data
var val = this.$source.attr('data-value')

if (this.options.freeform) {
// clear hidden field
this.$target.val('').trigger('change')

if (this.map[val]) {
this.$element.val(this.map[val])
}
else {
this.$element.val(val)
}
}

if (val !== '' && val !== undefined) {
this.$source.val(val).trigger('change')
this.$target.val(val).trigger('change')
}
}

// modified typeahead function to clear on type and prevent on moving around
, keyup: function (e) {
switch(e.keyCode) {
case 40: // down arrow
if (!this.shown) this.lookup() // open dropdown on down arrow
break
case 39: // right arrow
case 38: // up arrow
case 37: // left arrow
Expand All @@ -183,6 +254,11 @@
break

case 9: // tab
if (this.options.freeform) {
if (!this.shown) return
this.select(true)
break
}
case 13: // enter
if (!this.shown) return
this.select()
Expand All @@ -207,11 +283,36 @@
var that = this
this.focused = false
var val = this.$element.val()
if (!this.selected && val !== '' ) {
this.$element.val('')
this.$source.val('').trigger('change')
this.$target.val('').trigger('change')

if (val !== '') {
if (!this.selected) {
// only override user's data if freeform option is not set
if (!this.options.freeform) {
this.$element.val('')
this.$source.val('').trigger('change')
this.$target.val('').trigger('change')
}
else {
this.$source.val(val).trigger('change')
this.$target.val(val).trigger('change')
}
}
else {
if (this.options.freeform) {
this.$target.val(this.map[val]).trigger('change')
}
else {
if (!this.options.keeponblur) {
this.$element.val('')
}

this.$target.val(this.mapi[val]).trigger('change')
}
}
}

this.$container.addClass('combobox-selected')

if (!this.mousedover && this.shown) setTimeout(function () { that.hide() }, 200)
}

Expand All @@ -236,7 +337,7 @@

$.fn.combobox.defaults = {
template: '<div class="combobox-container"><input type="hidden" /><input type="text" autocomplete="off" /><span class="add-on btn dropdown-toggle" data-dropdown="dropdown"><span class="caret"/><span class="combobox-clear"><i class="icon-remove"/></span></span></div>'
, menu: '<ul class="typeahead typeahead-long dropdown-menu"></ul>'
, menu: '<ul class="typeahead typeahead-long dropdown-menu"></ul>'
, item: '<li><a href="#"></a></li>'
}

Expand Down
2 changes: 1 addition & 1 deletion js/tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<script src="vendor/qunit.js"></script>

<!-- plugin sources -->

<script src="../../js/bootstrap-combobox.js"></script>

<!-- unit tests -->
Expand Down
54 changes: 52 additions & 2 deletions js/tests/unit/bootstrap-combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ $(function () {
combobox.$menu.remove()
})

test("should listen to an button", function () {
test("should listen to a button", function () {
var $select = $('<select />')
, $button = $select.combobox().data('combobox').$button
ok($._data($button[0], 'events').click, 'has a click event')
Expand Down Expand Up @@ -98,6 +98,26 @@ $(function () {
combobox.$container.remove()
})

test("should show menu when no item is selected and down arrow is pressed", function() {
var $select = $('<select><option></option><option>aa</option><option>ab</option><option>ac</option></select>').appendTo('body')
, $input = $select.combobox().data('combobox').$element
, combobox = $select.data('combobox')

$input.trigger({
type: 'keyup'
, keyCode: 40
})

ok(combobox.$menu.is(":visible"), 'menu is visible')
equal(combobox.$menu.find('li').length, 3, 'has 3 items in menu')
equal(combobox.$menu.find('.active').length, 1, 'one item is active')
ok(combobox.$menu.find('li').first().hasClass('active'), 'first item is active')

combobox.$menu.remove()
$select.remove()
combobox.$container.remove()
})

test("should set next item when down arrow is pressed", function () {
var $select = $('<select><option></option><option>aa</option><option>ab</option><option>ac</option></select>').appendTo('body')
, $input = $select.combobox().data('combobox').$element
Expand Down Expand Up @@ -138,7 +158,7 @@ $(function () {
, $input = combobox.$element
, $source = combobox.$source
, $target = combobox.$target


$input.val('a')
combobox.lookup()
Expand Down Expand Up @@ -237,6 +257,21 @@ $(function () {
combobox.$menu.remove()
})

test("should keep input on blur when value does not exist", function() {
var $select = $('<select><option>aa</option></select>')
, $input = $select.combobox({keeponblur: true}).data('combobox').$element
, combobox = $select.data('combobox')

$input.val('KEEP ON BLUR')
combobox.lookup()
$input.trigger('blur')

equal($input.val(), 'KEEP ON BLUR', 'input value was correctly set')
equal($select.val(), 'aa', 'select value was correctly set')

combobox.$menu.remove()
})

test("should set placeholder text on the input if specified text of no value option", function() {
var $select = $('<select><option value="">Pick One</option><option value="aa">aa</option><option value="ab">ab</option><option value="ac">ac</option></select>')
, $input = $select.combobox().data('combobox').$element
Expand Down Expand Up @@ -296,4 +331,19 @@ $(function () {

combobox.$menu.remove()
})

test("should copy data-value attribute to the input if specified on the select and freeform is set", function() {
var $select = $('<select data-value="bb"><option></option><option>aa</option><option selected>ab</option><option>ac</option></select>')
, $input = $select.combobox({freeform: true}).data('combobox').$element
, $target = $select.combobox({freeform: true}).data('combobox').$target
, combobox = $select.data('combobox')

equal($input.val(), 'bb', 'input value was correctly set')
equal($target.val(), 'bb', 'hidden input value was correctly set')
equal($select.val(), '', 'select value was correctly set')

combobox.$menu.remove()
$select.remove()
combobox.$container.remove()
})
})