diff --git a/src/constants.js b/src/constants.js index 8849848e..c9ee8545 100644 --- a/src/constants.js +++ b/src/constants.js @@ -8,6 +8,9 @@ var KEYS = { space: 32, up: 38, down: 40, + left: 37, + right: 39, + delete: 46, comma: 188 }; diff --git a/src/tags-input.js b/src/tags-input.js index 3c07bb6d..daf70d10 100644 --- a/src/tags-input.js +++ b/src/tags-input.js @@ -98,25 +98,43 @@ tagsInput.directive('tagsInput', function($timeout, $document, tagsInputConfig, if (onTagRemoving({ $tag: tag })) { self.items.splice(index, 1); + self.clearSelection(); events.trigger('tag-removed', { $tag: tag }); return tag; } }; - self.removeLast = function() { - var tag, lastTagIndex = self.items.length - 1; - - if (options.enableEditingLastTag || self.selected) { - self.selected = null; - tag = self.remove(lastTagIndex); + self.select = function(index) { + if (index < 0) { + index = self.items.length - 1; } - else if (!self.selected) { - self.selected = self.items[lastTagIndex]; + else if (index >= self.items.length) { + index = 0; } - return tag; + self.index = index; + self.selected = self.items[index]; + }; + + self.selectPrior = function() { + self.select(--self.index); + }; + + self.selectNext = function() { + self.select(++self.index); }; + self.removeSelected = function() { + return self.remove(self.index); + }; + + self.clearSelection = function() { + self.selected = null; + self.index = -1; + }; + + self.clearSelection(); + return self; } @@ -197,7 +215,7 @@ tagsInput.directive('tagsInput', function($timeout, $document, tagsInputConfig, }; }, link: function(scope, element, attrs, ngModelCtrl) { - var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace], + var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace, KEYS.delete, KEYS.left, KEYS.right], tagList = scope.tagList, events = scope.events, options = scope.options, @@ -301,7 +319,7 @@ tagsInput.directive('tagsInput', function($timeout, $document, tagsInputConfig, } }) .on('input-change', function() { - tagList.selected = null; + tagList.clearSelection(); scope.newTag.invalid = null; }) .on('input-focus', function() { @@ -319,7 +337,7 @@ tagsInput.directive('tagsInput', function($timeout, $document, tagsInputConfig, var key = event.keyCode, isModifier = event.shiftKey || event.altKey || event.ctrlKey || event.metaKey, addKeys = {}, - shouldAdd, shouldRemove; + shouldAdd, shouldRemove, shouldSelect, shouldEditLastTag; if (isModifier || hotkeys.indexOf(key) === -1) { return; @@ -330,18 +348,36 @@ tagsInput.directive('tagsInput', function($timeout, $document, tagsInputConfig, addKeys[KEYS.space] = options.addOnSpace; shouldAdd = !options.addFromAutocompleteOnly && addKeys[key]; - shouldRemove = !shouldAdd && key === KEYS.backspace && scope.newTag.text.length === 0; + shouldRemove = (key === KEYS.backspace || key === KEYS.delete) && tagList.selected; + shouldEditLastTag = key === KEYS.backspace && scope.newTag.text.length === 0 && options.enableEditingLastTag; + shouldSelect = (key === KEYS.backspace || key === KEYS.left || key === KEYS.right) && scope.newTag.text.length === 0 && !options.enableEditingLastTag; if (shouldAdd) { tagList.addText(scope.newTag.text); - event.preventDefault(); } - else if (shouldRemove) { - var tag = tagList.removeLast(); - if (tag && options.enableEditingLastTag) { + else if (shouldEditLastTag) { + var tag; + + tagList.selectPrior(); + tag = tagList.removeSelected(); + + if (tag) { scope.newTag.setText(tag[options.displayProperty]); } + } + else if (shouldRemove) { + tagList.removeSelected(); + } + else if (shouldSelect) { + if (key === KEYS.left || key === KEYS.backspace) { + tagList.selectPrior(); + } + else if (key === KEYS.right) { + tagList.selectNext(); + } + } + if (shouldAdd || shouldSelect || shouldRemove || shouldEditLastTag) { event.preventDefault(); } }) diff --git a/test/tags-input.spec.js b/test/tags-input.spec.js index f5e10056..1b2fa8f8 100644 --- a/test/tags-input.spec.js +++ b/test/tags-input.spec.js @@ -178,6 +178,8 @@ describe('tags-input directive', function() { // Assert expect($scope.tags).toEqual([{ text: 'Tag1' }, { text: 'Tag3' }]); + expect(isolateScope.tagList.selected).toBe(null); + expect(isolateScope.tagList.index).toBe(-1); }); it('sets focus on the input field when the container div is clicked', function() { @@ -1031,6 +1033,8 @@ describe('tags-input directive', function() { // Assert expect(getTag(2)).not.toHaveClass('selected'); + expect(isolateScope.tagList.selected).toBe(null); + expect(isolateScope.tagList.index).toBe(-1); }); }); @@ -1043,6 +1047,8 @@ describe('tags-input directive', function() { // Assert expect(getInput().val()).toBe(''); expect($scope.tags).toEqual([{ text: 'Tag1' }, { text: 'Tag2' }]); + expect(isolateScope.tagList.selected).toBe(null); + expect(isolateScope.tagList.index).toBe(-1); }); it('does nothing when the input field is not empty', function() { @@ -1455,6 +1461,78 @@ describe('tags-input directive', function() { }); }); + describe('navigation through tags', function() { + describe('navigation is enabled', function() { + beforeEach(function() { + compile('enable-editing-last-tag="false"'); + }); + + it('selects the leftward tag when the left arrow key is pressed and the input is empty', function() { + // Arrange + $scope.tags = generateTags(3); + $scope.$digest(); + + // Act/Assert + sendKeyDown(KEYS.left); + expect(isolateScope.tagList.selected).toBe($scope.tags[2]); + + sendKeyDown(KEYS.left); + expect(isolateScope.tagList.selected).toBe($scope.tags[1]); + + sendKeyDown(KEYS.left); + expect(isolateScope.tagList.selected).toBe($scope.tags[0]); + + sendKeyDown(KEYS.left); + expect(isolateScope.tagList.selected).toBe($scope.tags[2]); + }); + + it('selects the rightward tag when the right arrow key is pressed and the input is empty', function() { + // Arrange + $scope.tags = generateTags(3); + $scope.$digest(); + + // Act/Assert + sendKeyDown(KEYS.right); + expect(isolateScope.tagList.selected).toBe($scope.tags[0]); + + sendKeyDown(KEYS.right); + expect(isolateScope.tagList.selected).toBe($scope.tags[1]); + + sendKeyDown(KEYS.right); + expect(isolateScope.tagList.selected).toBe($scope.tags[2]); + + sendKeyDown(KEYS.right); + expect(isolateScope.tagList.selected).toBe($scope.tags[0]); + }); + + it('removes the selected tag when the backspace key is pressed', function() { + // Arrange + $scope.tags = generateTags(3); + $scope.$digest(); + sendKeyDown(KEYS.left); + + // Act + sendKeyDown(KEYS.backspace); + + // Assert + expect($scope.tags).toEqual([{ text: 'Tag1' }, { text: 'Tag2' }]); + }); + + it('removes the selected tag when the delete key is pressed', function() { + // Arrange + $scope.tags = generateTags(3); + $scope.$digest(); + sendKeyDown(KEYS.left); + + // Act + sendKeyDown(KEYS.delete); + + // Assert + expect($scope.tags).toEqual([{ text: 'Tag1' }, { text: 'Tag2' }]); + }); + }); + }); + describe('on-tag-added option', function() { it('calls the provided callback when a new tag is added', function() { // Arrange