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 highlight support
Browse files Browse the repository at this point in the history
Added an option to enable highlighting of the matched text in the
suggestion list so the search terms can be easily spotted.

Closes #22.
  • Loading branch information
mbenford committed Nov 30, 2013
1 parent 248c703 commit ce73779
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 24 deletions.
12 changes: 8 additions & 4 deletions css/autocomplete.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,4 +31,9 @@
.ngTagsInput .autocomplete li.selected {
color: #fff;
background-color: #0097cf
}

.ngTagsInput .autocomplete li em {
font-weight: bold;
font-style: normal;
}
4 changes: 2 additions & 2 deletions css/tags-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
}

.ngTagsInput .tags span {
font: 13px Arial, sans-serif;
font: 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.ngTagsInput .tags button {
Expand All @@ -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 {
Expand Down
46 changes: 34 additions & 12 deletions src/auto-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
};
Expand All @@ -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();
Expand Down Expand Up @@ -81,22 +85,36 @@ angular.module('tags-input').directive('autoComplete', function($document, $time
' <li class="suggestion" ng-repeat="item in suggestionList.items"' +
' ng-class="{selected: item == suggestionList.selected}"' +
' ng-click="addSuggestion()"' +
' ng-mouseenter="suggestionList.select($index)">{{ item }}</li>' +
' ng-mouseenter="suggestionList.select($index)"' +
' ng-bind-html="highlight(item)"></li>' +
' </ul>' +
'</div>',
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, '<em>$&</em>');
};
}
else {
highlight = function(item) {
return item;
};
}

scope.suggestionList = suggestionList;

scope.addSuggestion = function() {
Expand All @@ -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);
Expand Down
63 changes: 60 additions & 3 deletions test/auto-complete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('autocomplete-directive', function() {
spyOn(parentCtrl, 'registerAutocomplete').andReturn(tagsInput);

options = jQuery.makeArray(arguments).join(' ');
element = angular.element('<auto-complete source="loadItems($text)" ' + options + '></auto-complete>');
element = angular.element('<auto-complete source="loadItems($query)" ' + options + '></auto-complete>');
parent.append(element);

$compile(element)($scope);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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('<em>a</em>');
expect(getSuggestionText(1)).toBe('<em>a</em>b');
expect(getSuggestionText(2)).toBe('b<em>a</em>');
expect(getSuggestionText(3)).toBe('<em>a</em>b<em>a</em>');
expect(getSuggestionText(4)).toBe('b<em>a</em>b');
});

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');
});
});
});

})();
6 changes: 3 additions & 3 deletions test/test-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
</head>
<body ng-controller="Ctrl">
<tags-input ng-model="tags" placeholder="{{ placeholder.value }}">
<auto-complete source="loadItems($text)" debounce-delay="100" min-length="10"></auto-complete>
<auto-complete source="loadItems($query)" debounce-delay="0" min-length="1" highlight-matched-text="true"></auto-complete>
</tags-input>

<script type="text/javascript">
angular.module('app', ['tags-input'])
.controller('Ctrl', function($scope, $q) {
$scope.tags = ['some', 'cool', 'tags'];
$scope.tags = ['Batman', 'Superman', 'Flash'];
$scope.placeholder = {value: "New tag" };
$scope.loadItems = function(text) {
console.log(text);
var deferred = $q.defer();
deferred.resolve(['Item1', 'Item2', 'Item3']);
deferred.resolve(['Batman', 'Superman', 'Flash', 'Iron Man', 'Hulk', 'Wolverine', "Green Lantern", "Green Arrow", "Spiderman"]);
return deferred.promise;
};
});
Expand Down

0 comments on commit ce73779

Please sign in to comment.