diff --git a/css/autocomplete.css b/css/autocomplete.css index b14d5d35..1128e98f 100644 --- a/css/autocomplete.css +++ b/css/autocomplete.css @@ -18,10 +18,9 @@ } .ngTagsInput .autocomplete li.suggestion { - padding: 3px 20px; - font-family: Arial, sans-serif; - font-size: 18px; - line-height: 22px; + padding: 3px 16px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 20px; cursor: pointer; text-align: left; white-space: nowrap; @@ -32,4 +31,9 @@ .ngTagsInput .autocomplete li.selected { color: #fff; background-color: #0097cf +} + +.ngTagsInput .autocomplete li em { + font-weight: bold; + font-style: normal; } \ No newline at end of file diff --git a/css/tags-input.css b/css/tags-input.css index 3590e12e..28cf9e73 100644 --- a/css/tags-input.css +++ b/css/tags-input.css @@ -59,7 +59,7 @@ } .ngTagsInput .tags span { - font: 13px Arial, sans-serif; + font: 13px "Helvetica Neue", Helvetica, Arial, sans-serif; } .ngTagsInput .tags button { @@ -83,7 +83,7 @@ padding: 0px 0px 0px 4px; height: 21px; float: left; - font: 13px Arial, sans-serif; + font: 13px "Helvetica Neue", Helvetica, Arial, sans-serif; } .ngTagsInput .tags input::-ms-clear { diff --git a/src/auto-complete.js b/src/auto-complete.js index 27c06187..3329860e 100644 --- a/src/auto-complete.js +++ b/src/auto-complete.js @@ -8,14 +8,16 @@ * @description * Provides autocomplete support for the tagsInput directive. * - * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as $text. - * The result of the expression must be a promise that resolves to an array of strings. - * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait after the last keystroke before evaluating - * the expression in the source option. - * @param {number=3} [minLength=3] Minimum number of characters that must be entered before evaluating the expression + * @param {expression} source Expression to evaluate upon changing the input content. The input value is available as + * $query. The result of the expression must be a promise that resolves to an array of strings. + * @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait after the last keystroke before + * evaluating the expression in the source option. + * @param {number=} [minLength=3] Minimum number of characters that must be entered before evaluating the expression * in the source option. + * @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the + * suggestions list. */ -angular.module('tags-input').directive('autoComplete', function($document, $timeout, configuration) { +angular.module('tags-input').directive('autoComplete', function($document, $timeout, $sce, configuration) { function SuggestionList(loadFn, options) { var self = {}, debouncedLoadId; @@ -24,6 +26,7 @@ angular.module('tags-input').directive('autoComplete', function($document, $time self.visible = false; self.index = -1; self.selected = null; + self.query = null; $timeout.cancel(debouncedLoadId); }; @@ -34,15 +37,16 @@ angular.module('tags-input').directive('autoComplete', function($document, $time self.hide = function() { self.visible = false; }; - self.load = function(text) { - if (text.length < options.minLength) { + self.load = function(query) { + if (query.length < options.minLength) { self.reset(); return; } $timeout.cancel(debouncedLoadId); debouncedLoadId = $timeout(function() { - loadFn({ $text: text }).then(function(items) { + self.query = query; + loadFn({ $query: query }).then(function(items) { self.items = items; if (items.length > 0) { self.show(); @@ -81,22 +85,36 @@ angular.module('tags-input').directive('autoComplete', function($document, $time '
  • {{ item }}
  • ' + + ' ng-mouseenter="suggestionList.select($index)"' + + ' ng-bind-html="highlight(item)">' + ' ' + '', link: function(scope, element, attrs, tagsInputCtrl) { var hotkeys = [KEYS.enter, KEYS.tab, KEYS.escape, KEYS.up, KEYS.down], - suggestionList, tagsInput, input; + suggestionList, tagsInput, input, highlight; configuration.load(scope, attrs, { debounceDelay: { type: Number, defaultValue: 100 }, - minLength: { type: Number, defaultValue: 3 } + minLength: { type: Number, defaultValue: 3 }, + highlightMatchedText: { type: Boolean, defaultValue: true } }); suggestionList = new SuggestionList(scope.source, scope.options); tagsInput = tagsInputCtrl.registerAutocomplete(); input = tagsInput.input; + if (scope.options.highlightMatchedText) { + highlight = function(item, text) { + var expression = new RegExp(text, 'gi'); + return item.replace(expression, '$&'); + }; + } + else { + highlight = function(item) { + return item; + }; + } + scope.suggestionList = suggestionList; scope.addSuggestion = function() { @@ -112,6 +130,10 @@ angular.module('tags-input').directive('autoComplete', function($document, $time return added; }; + scope.highlight = function(item) { + return $sce.trustAsHtml(highlight(item, suggestionList.query)); + }; + input.change(function(value) { if (value) { suggestionList.load(value); diff --git a/test/auto-complete.spec.js b/test/auto-complete.spec.js index 6013534c..954ffe64 100644 --- a/test/auto-complete.spec.js +++ b/test/auto-complete.spec.js @@ -41,7 +41,7 @@ describe('autocomplete-directive', function() { spyOn(parentCtrl, 'registerAutocomplete').andReturn(tagsInput); options = jQuery.makeArray(arguments).join(' '); - element = angular.element(''); + element = angular.element(''); parent.append(element); $compile(element)($scope); @@ -88,8 +88,8 @@ describe('autocomplete-directive', function() { return !getSuggestionsBox().hasClass('ng-hide'); } - function loadSuggestions(items) { - suggestionList.load('foobar'); + function loadSuggestions(items, text) { + suggestionList.load(text || 'foobar'); $timeout.flush(); resolve(items); } @@ -629,6 +629,63 @@ describe('autocomplete-directive', function() { expect(isSuggestionsBoxVisible()).toBe(false); }); }); + + describe('highlight-matched-text option', function() { + it('initializes the option to true', function() { + // Arrange/Act + compile(); + + // Assert + expect(isolateScope.options.highlightMatchedText).toBe(true); + }); + + it('sets the option given a static string', function() { + // Arrange/Act + compile('highlight-matched-text="false"'); + + // Assert + expect(isolateScope.options.highlightMatchedText).toBe(false); + }); + + it('sets the option given an interpolated string', function() { + // Arrange/Act + $scope.value = false; + compile('highlight-matched-text="{{ value }}"'); + + // Assert + expect(isolateScope.options.highlightMatchedText).toBe(false); + }); + + it('highlights the matched text in the suggestions list', function() { + // Arrange + compile('highlight-matched-text="true"', 'min-length="1"'); + + // Act + loadSuggestions(['a', 'ab', 'ba', 'aba', 'bab'], 'a'); + + // Assert + expect(getSuggestionText(0)).toBe('a'); + expect(getSuggestionText(1)).toBe('ab'); + expect(getSuggestionText(2)).toBe('ba'); + expect(getSuggestionText(3)).toBe('aba'); + expect(getSuggestionText(4)).toBe('bab'); + }); + + it('doesn\'t highlight the matched text in the suggestions list whe the option is false', function() { + // Arrange + compile('highlight-matched-text="false"', 'min-length="1"'); + + // Act + loadSuggestions(['a', 'ab', 'ba', 'aba', 'bab'], 'a'); + + // Assert + expect(getSuggestionText(0)).toBe('a'); + expect(getSuggestionText(1)).toBe('ab'); + expect(getSuggestionText(2)).toBe('ba'); + expect(getSuggestionText(3)).toBe('aba'); + expect(getSuggestionText(4)).toBe('bab'); + }); + }); }); })(); diff --git a/test/test-page.html b/test/test-page.html index ebcf5a79..3baa6061 100644 --- a/test/test-page.html +++ b/test/test-page.html @@ -8,18 +8,18 @@ - +