From a27363da25e0eade3c1294a6da84b5ab94d19867 Mon Sep 17 00:00:00 2001 From: Michael Benford Date: Sun, 1 Dec 2013 21:44:20 -0200 Subject: [PATCH] feat(autocomplete): Added tag filtering support 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. --- src/auto-complete.js | 115 ++++++++++++++++++++----------------- src/tags-input.js | 3 + test/auto-complete.spec.js | 11 ++-- test/tags-input.spec.js | 12 +++- test/test-page.html | 7 ++- 5 files changed, 88 insertions(+), 60 deletions(-) diff --git a/src/auto-complete.js b/src/auto-complete.js index 58679ec5..f2424fba 100644 --- a/src/auto-complete.js +++ b/src/auto-complete.js @@ -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 = []; @@ -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; @@ -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(); } @@ -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) { diff --git a/src/tags-input.js b/src/tags-input.js index 7a707615..a6bf6267 100644 --- a/src/tags-input.js +++ b/src/tags-input.js @@ -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; } }; }; diff --git a/test/auto-complete.spec.js b/test/auto-complete.spec.js index ee6e07a9..a819d788 100644 --- a/test/auto-complete.spec.js +++ b/test/auto-complete.spec.js @@ -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('')($scope); @@ -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); } @@ -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() { diff --git a/test/tags-input.spec.js b/test/tags-input.spec.js index 77260b1e..2f177a54 100644 --- a/test/tags-input.spec.js +++ b/test/tags-input.spec.js @@ -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) }); }); @@ -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']); + }); }); }); diff --git a/test/test-page.html b/test/test-page.html index 9ee1a4ca..16a9eef0 100644 --- a/test/test-page.html +++ b/test/test-page.html @@ -7,12 +7,15 @@ - + + max-results-to-show="10"> +