diff --git a/README.md b/README.md index ad3ae9e..3ceaa55 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,36 @@ Then just activate the plugin on a normal select box(suggest having a blank opti }); +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: + + + + + +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": + + + ## Live Example http://dl.dropbox.com/u/21368/bootstrap-combobox/index.html diff --git a/js/bootstrap-combobox.js b/js/bootstrap-combobox.js index 6d9e139..a2b5e91 100755 --- a/js/bootstrap-combobox.js +++ b/js/bootstrap-combobox.js @@ -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 @@ -55,6 +58,7 @@ , parse: function () { var that = this , map = {} + , mapi = {} , source = [] , selected = false , selectedValue = '' @@ -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) @@ -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') } @@ -100,6 +108,7 @@ this.clearTarget() this.triggerChange() this.clearElement() + this.lookup() } else { if (this.shown) { this.hide() @@ -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 } @@ -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 @@ -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 @@ -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() @@ -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) } @@ -236,7 +337,7 @@ $.fn.combobox.defaults = { template: '
' - , menu: '' + , menu: '' , item: '
  • ' } diff --git a/js/tests/index.html b/js/tests/index.html index e135dd8..b56eb9c 100644 --- a/js/tests/index.html +++ b/js/tests/index.html @@ -13,7 +13,7 @@ - + diff --git a/js/tests/unit/bootstrap-combobox.js b/js/tests/unit/bootstrap-combobox.js index 2c007f7..a2e1d8f 100644 --- a/js/tests/unit/bootstrap-combobox.js +++ b/js/tests/unit/bootstrap-combobox.js @@ -37,7 +37,7 @@ $(function () { combobox.$menu.remove() }) - test("should listen to an button", function () { + test("should listen to a button", function () { var $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 = $('').appendTo('body') , $input = $select.combobox().data('combobox').$element @@ -138,7 +158,7 @@ $(function () { , $input = combobox.$element , $source = combobox.$source , $target = combobox.$target - + $input.val('a') combobox.lookup() @@ -237,6 +257,21 @@ $(function () { combobox.$menu.remove() }) + test("should keep input on blur when value does not exist", function() { + var $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 = $('') , $input = $select.combobox().data('combobox').$element @@ -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 = $('') + , $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() + }) })