Skip to content
This repository was archived by the owner on Nov 22, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions src/auto-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* Provides autocomplete support for the tagsInput directive.
*
* @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 eventually resolves to an array of strings.
* $query and for pagination there is $skip and $limit.
* The result of the expression must be a promise that eventually resolves to an array of strings.
* @param {string=} [template=NA] URL or id of a custom template for rendering each element of the autocomplete list.
* @param {string=} [displayProperty=tagsInput.displayText] Property to be rendered as the autocomplete label.
* @param {number=} [debounceDelay=100] Amount of time, in milliseconds, to wait before evaluating the expression in
Expand All @@ -31,10 +32,12 @@
* The expression is provided with the current match as $match, its index as $index and its state as $selected. The result
* of the evaluation must be one of the values supported by the ngClass directive (either a string, an array or an object).
* See https://docs.angularjs.org/api/ng/directive/ngClass for more information.
* @param {boolean=} [loadMore=false] calls the source function with $query, $skip and $limit when the user reached the
* bottom of the suggestion list.
*/
tagsInput.directive('autoComplete', function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil) {
function SuggestionList(loadFn, options, events) {
var self = {}, getDifference, lastPromise, getTagId;
tagsInput.directive('autoComplete', function($document, $timeout, $sce, $q, tagsInputConfig, tiUtil, $window) {
function SuggestionList(loadFn, options, events, element) {
var self = {}, getDifference, lastPromise, getTagId, lastTags, resetLoadMore;

getTagId = function() {
return options.tagsInput.keyProperty || options.tagsInput.displayProperty;
Expand All @@ -52,6 +55,35 @@ tagsInput.directive('autoComplete', function($document, $timeout, $sce, $q, tags
});
};

function setupLoadMore() {
var bottom = element[0].querySelector('.suggestion-bottom');
var list = bottom.parentNode, queued = false;
function onScroll() {
var listRect = list.getBoundingClientRect();
var bottomRect = bottom.getBoundingClientRect();
var distance = bottomRect.bottom - listRect.bottom;
if (distance < 150)
{
self.load(self.query, lastTags, self.loadedPages + 1);
}
queued = false;
}
self.queueOnScroll = function() {
if (queued || self.eof || lastPromise !== undefined) {
return;
}
queued = true;
if ($window.requestAnimationFrame && arguments.length > 0) {
$window.requestAnimationFrame(onScroll);
}
else {
$timeout(onScroll, 16);
}
};

list.addEventListener('scroll',self.queueOnScroll);
resetLoadMore = function() { list.removeEventListener('scroll',self.queueOnScroll); };
}
self.reset = function() {
lastPromise = null;

Expand All @@ -60,30 +92,49 @@ tagsInput.directive('autoComplete', function($document, $timeout, $sce, $q, tags
self.index = -1;
self.selected = null;
self.query = null;
self.loadedPages = 0;
self.eof = false;

if (resetLoadMore) {
resetLoadMore();
}
};
self.show = function() {
if (options.selectFirstMatch) {
if (options.selectFirstMatch && self.loadedPages === 1) {
self.select(0);
}
else {
self.selected = null;
}
self.visible = true;
if (options.loadMore) {
$timeout(setupLoadMore);
}
};
self.load = tiUtil.debounce(function(query, tags) {
self.load = tiUtil.debounce(function(query, tags, page) {
page = page || 1;
lastTags = tags;
self.query = query;

var promise = $q.when(loadFn({ $query: query }));
var promise = $q.when(loadFn({ $query: query, $skip: (page-1)*options.maxResultsToShow, $limit: options.maxResultsToShow }));
lastPromise = promise;

promise.then(function(items) {
if (promise !== lastPromise) {
return;
}
lastPromise = undefined;
self.loadedPages = page;
self.eof = items.length < options.maxResultsToShow;

items = tiUtil.makeObjectArray(items.data || items, getTagId());
items = getDifference(items, tags);
self.items = items.slice(0, options.maxResultsToShow);
if (page !== 1) {
self.items = self.items.concat(items);
}
else {
self.items = items.slice(0, options.maxResultsToShow);
}

if (self.items.length > 0) {
self.show();
Expand Down Expand Up @@ -154,10 +205,11 @@ tagsInput.directive('autoComplete', function($document, $timeout, $sce, $q, tags
loadOnEmpty: [Boolean, false],
loadOnFocus: [Boolean, false],
selectFirstMatch: [Boolean, true],
displayProperty: [String, '']
displayProperty: [String, ''],
loadMore: [Boolean, false]
});

$scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events);
$scope.suggestionList = new SuggestionList($scope.source, $scope.options, $scope.events, $element);

this.registerAutocompleteMatch = function() {
return {
Expand Down
1 change: 1 addition & 0 deletions templates/auto-complete.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
ng-mouseenter="suggestionList.select($index)">
<ti-autocomplete-match scope="templateScope" data="::item"></ti-autocomplete-match>
</li>
<li class="suggestion-bottom"></li>
</ul>
</div>
55 changes: 47 additions & 8 deletions test/auto-complete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('autoComplete directive', function() {
spyOn(parentCtrl, 'registerAutocomplete').and.returnValue(tagsInput);

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

$compile(element)($scope);
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('autoComplete directive', function() {
}

function getSuggestions() {
return getSuggestionsBox().find('li');
return getSuggestionsBox().find('.suggestion-item');
}

function getSuggestion(index) {
Expand Down Expand Up @@ -117,7 +117,6 @@ describe('autoComplete directive', function() {
$timeout.flush();
resolve(items);
}

describe('basic features', function() {
it('ensures that the suggestions list is hidden by default', function() {
expect(isSuggestionsBoxVisible()).toBe(false);
Expand Down Expand Up @@ -631,7 +630,7 @@ describe('autoComplete directive', function() {
$timeout.flush();

// Assert
expect($scope.loadItems).toHaveBeenCalledWith('ABC');
expect($scope.loadItems).toHaveBeenCalledWith('ABC', 0, 10);
});

it('doesn\'t call the load function when the down arrow key is pressed and the option is false', function() {
Expand Down Expand Up @@ -680,7 +679,7 @@ describe('autoComplete directive', function() {
$timeout.flush();

// Assert
expect($scope.loadItems).toHaveBeenCalledWith('');
expect($scope.loadItems).toHaveBeenCalledWith('', 0, 10);
});

it('doesn\'t call the load function when the input field becomes empty and the option is false', function(){
Expand Down Expand Up @@ -715,7 +714,7 @@ describe('autoComplete directive', function() {
$timeout.flush();

// Assert
expect($scope.loadItems).toHaveBeenCalledWith('ABC');
expect($scope.loadItems).toHaveBeenCalledWith('ABC', 0, 10);
});

it('doesn\' call the load function when the input element gains focus and the option is false', function() {
Expand Down Expand Up @@ -766,7 +765,7 @@ describe('autoComplete directive', function() {
$timeout.flush(100);

// Assert
expect($scope.loadItems).toHaveBeenCalledWith('ABC');
expect($scope.loadItems).toHaveBeenCalledWith('ABC', 0, 10);
});

it('doesn\'t call the load function when the reset method is called', function() {
Expand Down Expand Up @@ -805,7 +804,7 @@ describe('autoComplete directive', function() {

// Assert
expect($scope.loadItems.calls.count()).toBe(1);
expect($scope.loadItems.calls.argsFor(0)).toEqual(['ABC']);
expect($scope.loadItems.calls.argsFor(0)).toEqual(['ABC', 0, 10]);
});

it('doesn\'t call the load function when the minimum amount of characters isn\'t entered', function() {
Expand Down Expand Up @@ -1372,4 +1371,44 @@ describe('autoComplete directive', function() {
});
});
});
describe('load-more', function() {
it('initializes the option to false', function() {
// Arrange
compile();

// Assert
expect(isolateScope.options.loadMore).toBe(false);
});
it('calls the load function with skip and limit params when user approaches the bottom',function() {
// Arrange
compile('load-more="true"');
loadSuggestions(12);
$scope.$digest();
$timeout.flush();
$scope.loadItems = jasmine.createSpy().and.returnValue($q.when([]));

// Act
suggestionList.queueOnScroll();
$timeout.flush();
$timeout.flush();

// Assert
expect($scope.loadItems).toHaveBeenCalledWith('foobar',10,10);
});
it('avoids calling the load function when there are no more results',function() {
// Arrange
compile('load-more="true"');
loadSuggestions(9);
$scope.$digest();
$timeout.flush();
$scope.loadItems = jasmine.createSpy().and.returnValue($q.when([]));

// Act
suggestionList.queueOnScroll();
$timeout.verifyNoPendingTasks();

// Assert
expect($scope.loadItems).not.toHaveBeenCalled();
});
});
});