From 427fa2dfe710fbea357fd6d1c68b9b82d90af5ed Mon Sep 17 00:00:00 2001 From: Christopher McCulloh Date: Fri, 19 Feb 2016 14:28:22 -0500 Subject: [PATCH 1/4] (GH1710) adds filter option to combobox and keyboard nav for dropdown --- js/combobox.js | 91 +++++++++++++++++++++++++++++++++++++++++++++- less/combobox.less | 17 +++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/js/combobox.js b/js/combobox.js index d866d9b64..a05a9e14f 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(); @@ -84,6 +86,11 @@ } }, + clearSelection: function () { + this.$selectedItem = null; + this.$input.val(''); + }, + menuShown: function () { if (this.options.autoResizeMenu) { this.resizeMenu(); @@ -116,11 +123,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 +193,58 @@ 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; + + if(this.options.showOptionsOnKeypress){ + this.$inputGroupBtn.addClass('open'); + } + + 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'); + } else if (e.which === ESC) { + e.preventDefault(); + this.clearSelection(); + this.$inputGroupBtn.removeClass('open'); + } else if (this.options.showOptionsOnKeypress && (e.which === DOWN || e.which === UP)) { + 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'); + } else if (this.options.showOptionsOnKeypress && this.options.filterOnKeypress) { + this.options.filter(this.$dropMenu.find('li'), this.$input.val(), this); + } + }, + inputchanged: function (e, extra) { // skip processing for internally-generated synthetic event // to avoid double processing @@ -232,7 +292,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; + } } + + } From bc15b2e988a2b08de98c6d332a5a7c00478dbdb2 Mon Sep 17 00:00:00 2001 From: Christopher McCulloh Date: Fri, 19 Feb 2016 15:18:56 -0500 Subject: [PATCH 2/4] (GH1710) integrates example into index page. Adjust filter firing and changed event to match user intent --- index.js | 5 ++++- js/combobox.js | 60 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 5f6dc35d1..5fb536b56 100644 --- a/index.js +++ b/index.js @@ -71,7 +71,10 @@ define(function (require) { /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - COMBOBOX - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - + $('#myCombobox').combobox({ + filterOnKeypress: true, + 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 a05a9e14f..9f41abdca 100644 --- a/js/combobox.js +++ b/js/combobox.js @@ -55,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 = { @@ -89,6 +94,7 @@ clearSelection: function () { this.$selectedItem = null; this.$input.val(''); + this.$dropMenu.find('li').removeClass('selected'); }, menuShown: function () { @@ -202,8 +208,16 @@ var RIGHT = 39; var DOWN = 40; - if(this.options.showOptionsOnKeypress){ - this.$inputGroupBtn.addClass('open'); + 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) { @@ -217,32 +231,41 @@ } 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 && (e.which === DOWN || e.which === UP)) { - 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)'); + } 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'); + 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'); } - this.$dropMenu.find('li').removeClass('selected'); - $selected.addClass('selected'); - } else if (this.options.showOptionsOnKeypress && this.options.filterOnKeypress) { + } + + // 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) { @@ -296,6 +319,7 @@ filterOnKeypress: false, showOptionsOnKeypress: false, filter: function filter(list, predicate, self) { + console.log('do filter'); var visible = 0; self.$dropMenu.find('.empty-indicator').remove(); From f3cc57d717037e97d434ca2ba1c71f466eb6b534 Mon Sep 17 00:00:00 2001 From: Christopher McCulloh Date: Fri, 19 Feb 2016 15:40:13 -0500 Subject: [PATCH 3/4] (GH1710) fixes jshint errors --- js/combobox.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/js/combobox.js b/js/combobox.js index 9f41abdca..0e5e1f660 100644 --- a/js/combobox.js +++ b/js/combobox.js @@ -209,10 +209,10 @@ var DOWN = 40; var IS_NAVIGATIONAL = ( - e.which === UP - || e.which === DOWN - || e.which === LEFT - || e.which === RIGHT + e.which === UP || + e.which === DOWN || + e.which === LEFT || + e.which === RIGHT ); if(this.options.showOptionsOnKeypress && !this.$inputGroupBtn.hasClass('open')){ @@ -319,7 +319,6 @@ filterOnKeypress: false, showOptionsOnKeypress: false, filter: function filter(list, predicate, self) { - console.log('do filter'); var visible = 0; self.$dropMenu.find('.empty-indicator').remove(); From 9023395e0e8c6af5f190fff14991c6a378b26d4d Mon Sep 17 00:00:00 2001 From: Christopher McCulloh Date: Tue, 23 Feb 2016 13:50:30 -0500 Subject: [PATCH 4/4] (GH1710) adds unit tests for #1710, fixes bug with whitespace in selectedItem --- bower.json | 3 +- index.js | 2 +- js/combobox.js | 7 +++-- test/combobox-test.js | 52 ++++++++++++++++++++++++++++++++ test/markup/combobox-markup.html | 50 ++++++++++++++++++++++++++++++ test/tests.html | 3 +- 6 files changed, 111 insertions(+), 6 deletions(-) diff --git a/bower.json b/bower.json index 7a70fa7e2..475581848 100644 --- a/bower.json +++ b/bower.json @@ -28,7 +28,8 @@ "qunit": "1.x", "requirejs-text": "2.x", "underscore": "1.x", - "blanket": "1.x" + "blanket": "1.x", + "jquery-simulate-ext": "~1.3.0" }, "ignore": [ "**/.*", diff --git a/index.js b/index.js index 5fb536b56..b1cf5e141 100644 --- a/index.js +++ b/index.js @@ -72,7 +72,7 @@ define(function (require) { COMBOBOX - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ $('#myCombobox').combobox({ - filterOnKeypress: true, + filterOnKeypress: false, showOptionsOnKeypress: true }); // sample method buttons diff --git a/js/combobox.js b/js/combobox.js index 0e5e1f660..5ab998884 100644 --- a/js/combobox.js +++ b/js/combobox.js @@ -84,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 { @@ -119,7 +120,7 @@ }, this.$selectedItem.data()); } else { data = { - text: this.$input.val() + text: this.$input.val().trim() }; } @@ -318,11 +319,11 @@ autoResizeMenu: true, filterOnKeypress: false, showOptionsOnKeypress: false, - filter: function filter(list, predicate, self) { + filter: function filter (list, predicate, self) { var visible = 0; self.$dropMenu.find('.empty-indicator').remove(); - list.each(function(i){ + list.each(function (i) { var $li = $(this); var text = $(this).text().trim(); 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 @@ +
    + +
    + + + +
    +
    + +
    + +
    + + + +
    +
    +