diff --git a/bower.json b/bower.json index 62b3e594b..811e3f939 100644 --- a/bower.json +++ b/bower.json @@ -29,6 +29,7 @@ "requirejs-text": "2.x", "underscore": "1.x", "blanket": "1.x", + "jquery-simulate-ext": "~1.3.0", "require-handlebars-plugin": "~1.0.0" }, "ignore": [ diff --git a/index.js b/index.js index c4c996264..6651d6eae 100644 --- a/index.js +++ b/index.js @@ -72,7 +72,10 @@ define(function (require) { /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - COMBOBOX - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - + $('#myCombobox').combobox({ + filterOnKeypress: false, + showOptionsOnKeypress: true + }); // sample method buttons $('#btnComboboxGetSelectedItem').on('click', function () { var selectedItem = $('#myCombobox').combobox('selectedItem'); diff --git a/js/combobox.js b/js/combobox.js index d866d9b64..5ab998884 100644 --- a/js/combobox.js +++ b/js/combobox.js @@ -39,10 +39,12 @@ this.$dropMenu = this.$element.find('.dropdown-menu'); this.$input = this.$element.find('input'); this.$button = this.$element.find('.btn'); + this.$inputGroupBtn = this.$element.find('.input-group-btn'); this.$element.on('click.fu.combobox', 'a', $.proxy(this.itemclicked, this)); this.$element.on('change.fu.combobox', 'input', $.proxy(this.inputchanged, this)); this.$element.on('shown.bs.dropdown', $.proxy(this.menuShown, this)); + this.$input.on('keyup.fu.combobox', $.proxy(this.keypress, this)); // set default selection this.setDefaultSelection(); @@ -53,6 +55,11 @@ this.$button.addClass('disabled'); } + // filter on load in case the first thing they do is press navigational key to pop open the menu + if (this.options.filterOnKeypress) { + this.options.filter(this.$dropMenu.find('li'), this.$input.val(), this); + } + }; Combobox.prototype = { @@ -77,6 +84,7 @@ doSelect: function ($item) { if (typeof $item[0] !== 'undefined') { + $item.addClass('selected'); this.$selectedItem = $item; this.$input.val(this.$selectedItem.text().trim()); } else { @@ -84,6 +92,12 @@ } }, + clearSelection: function () { + this.$selectedItem = null; + this.$input.val(''); + this.$dropMenu.find('li').removeClass('selected'); + }, + menuShown: function () { if (this.options.autoResizeMenu) { this.resizeMenu(); @@ -106,7 +120,7 @@ }, this.$selectedItem.data()); } else { data = { - text: this.$input.val() + text: this.$input.val().trim() }; } @@ -116,11 +130,12 @@ selectByText: function (text) { var $item = $([]); this.$element.find('li').each(function () { - if ((this.textContent || this.innerText || $(this).text() || '').toLowerCase() === (text || '').toLowerCase()) { + if ((this.textContent || this.innerText || $(this).text() || '').trim().toLowerCase() === (text || '').trim().toLowerCase()) { $item = $(this); return false; } }); + this.doSelect($item); }, @@ -185,6 +200,75 @@ this.$element.find('.dropdown-toggle').focus(); }, + keypress: function (e) { + var ENTER = 13; + //var TAB = 9; + var ESC = 27; + var LEFT = 37; + var UP = 38; + var RIGHT = 39; + var DOWN = 40; + + var IS_NAVIGATIONAL = ( + e.which === UP || + e.which === DOWN || + e.which === LEFT || + e.which === RIGHT + ); + + if(this.options.showOptionsOnKeypress && !this.$inputGroupBtn.hasClass('open')){ + this.$button.dropdown('toggle'); + this.$input.focus(); + } + + if (e.which === ENTER) { + e.preventDefault(); + + var selected = this.$dropMenu.find('li.selected').text().trim(); + if(selected.length > 0){ + this.selectByText(selected); + }else{ + this.selectByText(this.$input.val()); + } + + this.$inputGroupBtn.removeClass('open'); + this.inputchanged(e); + } else if (e.which === ESC) { + e.preventDefault(); + this.clearSelection(); + this.$inputGroupBtn.removeClass('open'); + } else if (this.options.showOptionsOnKeypress) { + if (e.which === DOWN || e.which === UP) { + e.preventDefault(); + var $selected = this.$dropMenu.find('li.selected'); + if ($selected.length > 0) { + if (e.which === DOWN) { + $selected = $selected.next(':not(.hidden)'); + } else { + $selected = $selected.prev(':not(.hidden)'); + } + } + + if ($selected.length === 0){ + if (e.which === DOWN) { + $selected = this.$dropMenu.find('li:not(.hidden):first'); + } else { + $selected = this.$dropMenu.find('li:not(.hidden):last'); + } + } + this.$dropMenu.find('li').removeClass('selected'); + $selected.addClass('selected'); + } + } + + // Avoid filtering on navigation key presses + if (this.options.filterOnKeypress && !IS_NAVIGATIONAL) { + this.options.filter(this.$dropMenu.find('li'), this.$input.val(), this); + } + + this.previousKeyPress = e.which; + }, + inputchanged: function (e, extra) { // skip processing for internally-generated synthetic event // to avoid double processing @@ -232,7 +316,34 @@ }; $.fn.combobox.defaults = { - autoResizeMenu: true + autoResizeMenu: true, + filterOnKeypress: false, + showOptionsOnKeypress: false, + filter: function filter (list, predicate, self) { + var visible = 0; + self.$dropMenu.find('.empty-indicator').remove(); + + list.each(function (i) { + var $li = $(this); + var text = $(this).text().trim(); + + $li.removeClass(); + + if (text === predicate) { + $li.addClass('text-success'); + visible++; + } else if (text.substr(0, predicate.length) === predicate) { + $li.addClass('text-info'); + visible++; + } else { + $li.addClass('hidden'); + } + }); + + if (visible === 0) { + self.$dropMenu.append('
  • No Matches
  • '); + } + } }; $.fn.combobox.Constructor = Combobox; diff --git a/less/combobox.less b/less/combobox.less index f38d4dc31..e334cae50 100644 --- a/less/combobox.less +++ b/less/combobox.less @@ -12,6 +12,23 @@ display:none; } } + .dropdown-menu > li.selected > a { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; + } + + .dropdown-menu > li > em { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; + } } + + } diff --git a/test/combobox-test.js b/test/combobox-test.js index 17ed53547..598725e14 100644 --- a/test/combobox-test.js +++ b/test/combobox-test.js @@ -4,6 +4,12 @@ define(function(require){ var $ = require('jquery'); + require('sim/libs/bililiteRange'); + require('sim/libs/jquery.simulate'); + require('sim/src/jquery.simulate.ext'); + require('sim/src/jquery.simulate.drag-n-drop'); + require('sim/src/jquery.simulate.key-sequence'); + require('sim/src/jquery.simulate.key-combo'); var html = require('text!test/markup/combobox-markup.html!strip'); /* FOR DEV TESTING - uncomment to test against index.html */ //html = require('text!index.html!strip'); @@ -110,6 +116,52 @@ define(function(require){ deepEqual(item, expectedItem, 'item selected'); }); + var userInteracts = function userInteracts($combobox) { + var DOWN_KEY = 40; + var UP_KEY = 38; + var deleteAllTypeT = "{backspace}{backspace}{backspace}{backspace}{backspace}T"; + var hitEnter = "{enter}"; + + var hitDown = jQuery.Event("keyup"); + hitDown.which = DOWN_KEY; + hitDown.keyCode = DOWN_KEY; + hitDown.keypress = DOWN_KEY; + + $combobox.find('input').simulate("key-sequence", {sequence: deleteAllTypeT}); + $combobox.find('input').trigger(hitDown); + $combobox.find('input').simulate("key-sequence", {sequence: hitEnter}); + }; + + test("should respond to keypresses appropriately with filter and showOptionsOnKeypress off", function() { + var $combobox = $(html).find("#MyCombobox").combobox(); + + userInteracts($combobox); + + var item = $combobox.combobox('selectedItem'); + var expectedItem = { text:'T' }; + deepEqual(item, expectedItem, 'Combobox was not triggered, filter not activated'); + }); + + test("should respond to keypresses appropriately with filter off and showOptionsOnKeypress on", function() { + var $combobox = $(html).find("#MyComboboxWithSelectedForOptions").combobox({ showOptionsOnKeypress: true }); + + userInteracts($combobox); + + var item = $combobox.combobox('selectedItem'); + var expectedItem = { text:'Four', value: 4 }; + deepEqual(item, expectedItem, 'Combobox was triggered with filter inactive but showOptionsOnKeypress active'); + }); + + test("should respond to keypresses appropriately with filter and showOptionsOnKeypress on", function() { + var $combobox = $(html).find("#MyComboboxWithSelectedForFilter").combobox({ showOptionsOnKeypress: true, filterOnKeypress: true }); + + userInteracts($combobox); + + var item = $combobox.combobox('selectedItem'); + var expectedItem = { text:'Two', value: 2 }; + deepEqual(item, expectedItem, 'Combobox was triggered with filter active'); + }); + test("should select by text with whitespace", function() { var $combobox = $(html).find("#MyCombobox").combobox(); $combobox.combobox('selectByText', 'Item Five'); diff --git a/test/markup/combobox-markup.html b/test/markup/combobox-markup.html index ca78c3df7..dd4344b91 100644 --- a/test/markup/combobox-markup.html +++ b/test/markup/combobox-markup.html @@ -29,6 +29,56 @@ +
    + +
    + + + +
    +
    + +
    + +
    + + + +
    +
    +