Skip to content
This repository has been archived by the owner on Nov 22, 2021. It is now read-only.

Commit

Permalink
feat(tagsInput): Add support for tag navigation
Browse files Browse the repository at this point in the history
Add support for tag navigation so any tag can be selected and removed
by using the keyboard. Navigation is only enabled when the enableEditingLastTag
option is false.

Closes #350
  • Loading branch information
mbenford committed Mar 14, 2015
1 parent 8359e60 commit 18760b2
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 17 deletions.
3 changes: 3 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ var KEYS = {
space: 32,
up: 38,
down: 40,
left: 37,
right: 39,
delete: 46,
comma: 188
};

Expand Down
70 changes: 53 additions & 17 deletions src/tags-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
Expand All @@ -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();
}
})
Expand Down
78 changes: 78 additions & 0 deletions test/tags-input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
});
});

Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 18760b2

Please sign in to comment.