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

Commit

Permalink
feat(autocomplete): Added tag filtering support
Browse files Browse the repository at this point in the history
Implemented tag filtering support for the autocomplete directive in
order to filter out the tags that are already added and thus cannot be
inserted again.

Closes #25.
  • Loading branch information
mbenford committed Dec 1, 2013
1 parent 30c2d21 commit a27363d
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 60 deletions.
115 changes: 63 additions & 52 deletions src/auto-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@
*/
angular.module('tags-input').directive('autoComplete', function($document, $timeout, $sce, configuration) {
function SuggestionList(loadFn, options) {
var self = {}, debouncedLoadId;
var self = {}, debouncedLoadId, getDifference;

getDifference = function(array1, array2) {
var result = [];

array1.forEach(function(item) {
if (array2.indexOf(item) === -1) {
result.push(item);
}
});

return result;
};

self.reset = function() {
self.items = [];
Expand All @@ -38,7 +50,7 @@ angular.module('tags-input').directive('autoComplete', function($document, $time
self.hide = function() {
self.visible = false;
};
self.load = function(query) {
self.load = function(query, tags) {
if (query.length < options.minLength) {
self.reset();
return;
Expand All @@ -48,7 +60,7 @@ angular.module('tags-input').directive('autoComplete', function($document, $time
debouncedLoadId = $timeout(function() {
self.query = query;
loadFn({ $query: query }).then(function(items) {
self.items = items;
self.items = getDifference(items, tags);
if (items.length > 0) {
self.show();
}
Expand Down Expand Up @@ -135,60 +147,59 @@ angular.module('tags-input').directive('autoComplete', function($document, $time
return $sce.trustAsHtml(highlight(item, suggestionList.query));
};

tagsInput
.on('input-changed', function(value) {
if (value) {
suggestionList.load(value);
} else {
tagsInput.on('input-changed', function(value) {
if (value) {
suggestionList.load(value, tagsInput.getTags());
} else {
suggestionList.reset();
}
})
.on('input-keydown', function(e) {
var key, handled;

if (hotkeys.indexOf(e.keyCode) === -1) {
return;
}

// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
var immediatePropagationStopped = false;
e.stopImmediatePropagation = function() {
immediatePropagationStopped = true;
e.stopPropagation();
};
e.isImmediatePropagationStopped = function() {
return immediatePropagationStopped;
};

if (suggestionList.visible) {
key = e.keyCode;
handled = false;

if (key === KEYS.down) {
suggestionList.selectNext();
handled = true;
}
else if (key === KEYS.up) {
suggestionList.selectPrior();
handled = true;
}
else if (key === KEYS.escape) {
suggestionList.reset();
handled = true;
}
})
.on('input-keydown', function(e) {
var key, handled;

if (hotkeys.indexOf(e.keyCode) === -1) {
return;
else if (key === KEYS.enter || key === KEYS.tab) {
handled = scope.addSuggestion();
}

// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
var immediatePropagationStopped = false;
e.stopImmediatePropagation = function() {
immediatePropagationStopped = true;
e.stopPropagation();
};
e.isImmediatePropagationStopped = function() {
return immediatePropagationStopped;
};

if (suggestionList.visible) {
key = e.keyCode;
handled = false;

if (key === KEYS.down) {
suggestionList.selectNext();
handled = true;
}
else if (key === KEYS.up) {
suggestionList.selectPrior();
handled = true;
}
else if (key === KEYS.escape) {
suggestionList.reset();
handled = true;
}
else if (key === KEYS.enter || key === KEYS.tab) {
handled = scope.addSuggestion();
}

if (handled) {
e.preventDefault();
e.stopImmediatePropagation();
scope.$apply();
}
if (handled) {
e.preventDefault();
e.stopImmediatePropagation();
scope.$apply();
}
});
}
});

$document.on('click', function() {
if (suggestionList.visible) {
Expand Down
3 changes: 3 additions & 0 deletions src/tags-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ angular.module('tags-input').directive('tagsInput', function(configuration) {
on: function(name, handler) {
events.on(name, handler);
return this;
},
getTags: function() {
return $scope.tags;
}
};
};
Expand Down
11 changes: 6 additions & 5 deletions test/auto-complete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ describe('autocomplete-directive', function() {
on: jasmine.createSpy().andCallFake(function(name, handler) {
eventHandlers[name] = handler;
return this;
})
}),
getTags: jasmine.createSpy().andReturn([])
};

parent = $compile('<tags-input ng-model="whatever"></tags-input>')($scope);
Expand Down Expand Up @@ -88,7 +89,7 @@ describe('autocomplete-directive', function() {
}

function loadSuggestions(items, text) {
suggestionList.load(text || 'foobar');
suggestionList.load(text || 'foobar', tagsInput.getTags());
$timeout.flush();
resolve(items);
}
Expand All @@ -98,15 +99,15 @@ describe('autocomplete-directive', function() {
expect(isSuggestionsBoxVisible()).toBe(false);
});

it('renders all elements returned by the load function', function() {
it('renders all elements returned by the load function that aren\'t already added', function() {
// Act
tagsInput.getTags.andReturn(['Item3']);
loadSuggestions(['Item1','Item2','Item3']);

// Assert
expect(getSuggestions().length).toBe(3);
expect(getSuggestions().length).toBe(2);
expect(getSuggestionText(0)).toBe('Item1');
expect(getSuggestionText(1)).toBe('Item2');
expect(getSuggestionText(2)).toBe('Item3');
});

it('shows the suggestions list when there are items to show', function() {
Expand Down
12 changes: 11 additions & 1 deletion test/tags-input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,8 @@ describe('tags-input-directive', function() {
expect(autocompleteObj).toEqual({
changeInputValue: jasmine.any(Function),
focusInput: jasmine.any(Function),
on: jasmine.any(Function)
on: jasmine.any(Function),
getTags: jasmine.any(Function)
});
});

Expand All @@ -790,6 +791,15 @@ describe('tags-input-directive', function() {
// Assert
expect(input.focus).toHaveBeenCalled();
});

it('returns the list of tags', function() {
// Arrange
$scope.tags = ['a', 'b', 'c'];
$scope.$digest();

// Act/Assert
expect(autocompleteObj.getTags()).toEqual(['a', 'b', 'c']);
});
});
});

Expand Down
7 changes: 5 additions & 2 deletions test/test-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
<link rel="stylesheet" href="../build/ng-tags-input.css"/>
</head>
<body ng-controller="Ctrl">
<tags-input ng-model="tags" placeholder="{{ placeholder.value }}">
<tags-input ng-model="tags"
placeholder="{{ placeholder.value }}"
replace-spaces-with-dashes="false">
<auto-complete source="loadItems($query)"
debounce-delay="0"
min-length="1"
highlight-matched-text="true"
max-results-to-show="10"></auto-complete>
max-results-to-show="10">
</auto-complete>
</tags-input>

<script type="text/javascript">
Expand Down

0 comments on commit a27363d

Please sign in to comment.