diff --git a/src/js/dropdown_view.js b/src/js/dropdown_view.js index 62429c84..eabba355 100644 --- a/src/js/dropdown_view.js +++ b/src/js/dropdown_view.js @@ -33,13 +33,13 @@ var DropdownView = (function() { this.isMouseOverDropdown = false; }, - _handleMouseover: function(e) { + _handleMouseover: function($e) { this._getSuggestions().removeClass('tt-is-under-cursor'); - $(e.currentTarget).addClass('tt-is-under-cursor'); + $($e.currentTarget).addClass('tt-is-under-cursor'); }, - _handleSelection: function(e) { - this.trigger('select', formatDataForSuggestion($(e.currentTarget))); + _handleSelection: function($e) { + this.trigger('select', formatDataForSuggestion($($e.currentTarget))); }, _moveCursor: function(increment) { diff --git a/src/js/input_view.js b/src/js/input_view.js index beb2252d..bbfc7ae4 100644 --- a/src/js/input_view.js +++ b/src/js/input_view.js @@ -15,13 +15,13 @@ var InputView = (function() { utils.bindAll(this); this.specialKeyCodeMap = { - 9: { event: 'tab' }, - 27: { event: 'esc' }, - 37: { event: 'left' }, - 39: { event: 'right' }, - 13: { event: 'enter' }, - 38: { event: 'up', preventDefault: true }, - 40: { event: 'down', preventDefault: true } + 9: 'tab', + 27: 'esc', + 37: 'left', + 39: 'right', + 13: 'enter', + 38: 'up', + 40: 'down' }; this.query = ''; @@ -64,14 +64,11 @@ var InputView = (function() { this.trigger('blur'); }, - _handleSpecialKeyEvent: function(e) { + _handleSpecialKeyEvent: function($e) { // which is normalized and consistent (but not for IE) - var keyCode = this.specialKeyCodeMap[e.which || e.keyCode]; + var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; - if (keyCode) { - this.trigger(keyCode.event, e); - keyCode.preventDefault && e.preventDefault(); - } + keyName && this.trigger(keyName, $e); }, _compareQueryToInputValue: function() { @@ -100,10 +97,6 @@ var InputView = (function() { this.$input.blur(); }, - setPreventDefaultValueForKey: function(key, value) { - this.specialKeyCodeMap[key].preventDefault = !!value; - }, - getQuery: function() { return this.query; }, @@ -138,7 +131,7 @@ var InputView = (function() { selectionStart = this.$input[0].selectionStart, range; - if (selectionStart) { + if (utils.isNumber(selectionStart)) { return selectionStart === valueLength; } diff --git a/src/js/typeahead_view.js b/src/js/typeahead_view.js index 729b68ee..15ac1a29 100644 --- a/src/js/typeahead_view.js +++ b/src/js/typeahead_view.js @@ -74,9 +74,9 @@ var TypeaheadView = (function() { .on('queryChange whitespaceChange', this._setLanguageDirection) .on('esc', this._hideDropdown) .on('esc', this._setInputValueToQuery) + .on('tab up down', this._managePreventDefault) .on('up down', this._moveDropdownCursor) .on('up down', this._showDropdown) - .on('tab', this._setPreventDefaultValueForTab) .on('tab left right', this._autocomplete); } @@ -84,14 +84,26 @@ var TypeaheadView = (function() { // private methods // --------------- - _setPreventDefaultValueForTab: function(e) { - var hint = this.inputView.getHintValue(), - inputValue = this.inputView.getInputValue(), + _managePreventDefault: function(e) { + var $e = e.data, + hint, + inputValue, + preventDefault = false; + + switch (e.type) { + case 'tab': + hint = this.inputView.getHintValue(); + inputValue = this.inputView.getInputValue(); preventDefault = hint && hint !== inputValue; + break; + + case 'up': + case 'down': + preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; + break; + } - // if the user tabs to autocomplete while the menu is open - // this will prevent the focus from being lost from the query input - this.inputView.setPreventDefaultValueForKey('9', preventDefault); + preventDefault && $e.preventDefault(); }, _setLanguageDirection: function() { @@ -136,7 +148,9 @@ var TypeaheadView = (function() { }, _setInputValueToSuggestionUnderCursor: function(e) { - this.inputView.setInputValue(e.data.value, true); + var suggestion = e.data; + + this.inputView.setInputValue(suggestion.value, true); }, _showDropdown: function() { @@ -149,7 +163,12 @@ var TypeaheadView = (function() { }, _moveDropdownCursor: function(e) { - this.dropdownView[e.type === 'up' ? 'moveCursorUp' : 'moveCursorDown'](); + var $e = e.data; + + if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { + this.dropdownView[e.type === 'up' ? + 'moveCursorUp' : 'moveCursorDown'](); + } }, _handleSelection: function(e) { diff --git a/test/input_view_spec.js b/test/input_view_spec.js index 8411cbfd..27233070 100644 --- a/test/input_view_spec.js +++ b/test/input_view_spec.js @@ -162,18 +162,6 @@ describe('InputView', function() { }); }); - describe('#setPreventDefaultValueForKey', function() { - it('should act as a setter for keyCodeMap', function() { - var key = '9'; - - this.inputView.setPreventDefaultValueForKey(key, 'truthy value'); - expect(this.inputView.specialKeyCodeMap[key].preventDefault).toBe(true); - - this.inputView.setPreventDefaultValueForKey(key, false); - expect(this.inputView.specialKeyCodeMap[key].preventDefault).toBe(false); - }); - }); - describe('#getQuery', function() { it('should act as a getter for query', function() { this.inputView.query = 'i am the query value'; diff --git a/test/playground.html b/test/playground.html new file mode 100644 index 00000000..3fa83c0d --- /dev/null +++ b/test/playground.html @@ -0,0 +1,87 @@ + + + + + + + + + + + +
+ +
+ + + + diff --git a/test/typeahead_view_spec.js b/test/typeahead_view_spec.js index 471ee570..2ca3500e 100644 --- a/test/typeahead_view_spec.js +++ b/test/typeahead_view_spec.js @@ -229,35 +229,116 @@ describe('TypeaheadView', function() { }); }); - ['up', 'down'].forEach(function(eventType) { - var fnModifier = eventType.charAt(0).toUpperCase() + eventType.slice(1); + describe('when inputView triggers up', function() { - describe('when inputView triggers ' + eventType, function() { + describe('if modifier key was pressed', function() { beforeEach(function() { - this.inputView.trigger(eventType); + this.$e = $.extend($.Event('keydown'), { keyCode: 38, shiftKey: true }); + spyOn(this.$e, 'preventDefault'); + + this.inputView.trigger('up', this.$e); + }); + + it('should show the dropdown menu', function() { + expect(this.dropdownView.show).toHaveBeenCalled(); + }); + + it('should not prevent default browser behavior', function() { + expect(this.$e.preventDefault).not.toHaveBeenCalled(); + }); + + it('should not move cursor up', function() { + expect(this.dropdownView.moveCursorUp).not.toHaveBeenCalled(); + }); + }); + + describe('if modifier key was not pressed', function() { + beforeEach(function() { + this.$e = $.extend($.Event('keydown'), { keyCode: 38 }); + spyOn(this.$e, 'preventDefault'); + + this.inputView.trigger('up', this.$e); }); it('should show the dropdown menu', function() { expect(this.dropdownView.show).toHaveBeenCalled(); }); - it('should move cursor ' + eventType, function() { - expect(this.dropdownView['moveCursor' + fnModifier]).toHaveBeenCalled(); + it('should prevent default browser behavior', function() { + expect(this.$e.preventDefault).toHaveBeenCalled(); + }); + + it('should move cursor up', function() { + expect(this.dropdownView.moveCursorUp).toHaveBeenCalled(); + }); + }); + }); + + describe('when inputView triggers down', function() { + + describe('if modifier key was pressed', function() { + beforeEach(function() { + this.$e = $.extend($.Event('keydown'), { keyCode: 40, shiftKey: true }); + spyOn(this.$e, 'preventDefault'); + + this.inputView.trigger('down', this.$e); + }); + + it('should show the dropdown menu', function() { + expect(this.dropdownView.show).toHaveBeenCalled(); + }); + + it('should not prevent default browser behavior', function() { + expect(this.$e.preventDefault).not.toHaveBeenCalled(); + }); + + it('should not move cursor down', function() { + expect(this.dropdownView.moveCursorDown).not.toHaveBeenCalled(); + }); + }); + + describe('if modifier key was not pressed', function() { + beforeEach(function() { + this.$e = $.extend($.Event('keydown'), { keyCode: 40 }); + spyOn(this.$e, 'preventDefault'); + + this.inputView.trigger('down', this.$e); + }); + + it('should show the dropdown menu', function() { + expect(this.dropdownView.show).toHaveBeenCalled(); + }); + + it('should prevent default browser behavior', function() { + expect(this.$e.preventDefault).toHaveBeenCalled(); + }); + + it('should move cursor down', function() { + expect(this.dropdownView.moveCursorDown).toHaveBeenCalled(); }); }); }); describe('when inputView triggers tab', function() { + beforeEach(function() { + this.$e = $.extend($.Event('keydown'), { keyCode: 9 }); + spyOn(this.$e, 'preventDefault'); + }); + describe('if hint is empty string', function() { beforeEach(function() { this.inputView.getHintValue.andReturn(''); - this.inputView.trigger('tab'); + this.inputView.trigger('tab', this.$e); }); it('should not update input value', function() { expect(this.inputView.setInputValue).not.toHaveBeenCalled(); }); + + it('should not prevent default browser behavior', function() { + expect(this.$e.preventDefault).not.toHaveBeenCalled(); + }); }); describe('if hint differs from query', function() { @@ -265,12 +346,16 @@ describe('TypeaheadView', function() { this.inputView.getQuery.andReturn('app'); this.inputView.getHintValue.andReturn('apple'); - this.inputView.trigger('tab'); + this.inputView.trigger('tab', this.$e); }); it('should update input value', function() { expect(this.inputView.setInputValue).toHaveBeenCalled(); }); + + it('should prevent default browser behavior', function() { + expect(this.$e.preventDefault).toHaveBeenCalled(); + }); }); });