angular-auto-complete
Version:
AngularJS auto-complete plugin
1,166 lines (976 loc) • 42.8 kB
JavaScript
(function (global, factory) {
'use strict';
if (typeof exports === 'object' && typeof module !== 'undefined') {
// commonJS
module.exports = factory(require('angular'));
}
else if (typeof define === 'function' && define.amd) {
// AMD
define(['module', 'angular'], function (module, angular) {
module.exports = factory(angular);
});
}
else {
factory(global.angular);
}
}(this, function (angular) {
var helperService = new HelperService();
angular
.module('autoCompleteModule', ['ngSanitize'])
.directive('autoComplete', autoCompleteDirective)
.directive('autoCompleteItem', autoCompleteItemDirective)
.directive('autoCompleteNoMatch', autoCompleteNoMatchDirective);
autoCompleteDirective.$inject = ['$q', '$compile', '$document', '$window', '$timeout'];
function autoCompleteDirective($q, $compile, $document, $window, $timeout) {
return {
restrict: 'A',
scope: {},
transclude: false,
controllerAs: 'ctrl',
bindToController: {
initialOptions: '&autoComplete'
},
require: ['autoComplete', 'ngModel'],
link: postLinkFn,
controller: MainCtrl
};
function postLinkFn(scope, element, attrs, ctrls) {
var ctrl = ctrls[0]; //directive controller
ctrl.textModelCtrl = ctrls[1]; // textbox model controller
// store the jquery element on the controller
ctrl.target = element;
$timeout(function () {
// execute the options expression
$q.when(ctrl.initialOptions()).then(_initialize);
});
function _initialize(options) {
options = options || {};
ctrl.init(angular.extend({}, defaultOptions, options));
_initializeContainer();
_wireupEvents();
}
function _initializeContainer() {
ctrl.container = _getContainer();
if (ctrl.options.containerCssClass) {
ctrl.container.addClass(ctrl.options.containerCssClass);
}
// if a jquery parent is specified in options append the container to that
// otherwise append to body
if (ctrl.options.dropdownParent) {
ctrl.options.dropdownParent.append(ctrl.container);
}
else {
$document.find('body').append(ctrl.container);
ctrl.container.addClass('auto-complete-absolute-container');
}
// keep a reference to the <ul> element
ctrl.elementUL = angular.element(ctrl.container[0].querySelector('ul.auto-complete-results'));
}
function _getContainer() {
if (angular.isElement(ctrl.options.dropdownParent)) {
return _getCustomContainer();
}
return _getDefaultContainer();
}
function _getCustomContainer() {
var container = ctrl.options.dropdownParent;
container.addClass('auto-complete-container unselectable');
container.attr('data-instance-id', ctrl.instanceId);
var linkFn = $compile(_getDropdownListTemplate());
var elementUL = linkFn(scope);
container.append(elementUL);
return container;
}
function _getDefaultContainer() {
var linkFn = $compile(_getContainerTemplate());
return linkFn(scope);
}
function _getContainerTemplate() {
var html = '';
html += '<div class="auto-complete-container unselectable"';
html += ' data-instance-id="{{ ctrl.instanceId }}"';
html += ' ng-show="ctrl.containerVisible">';
html += _getDropdownListTemplate();
html += '</div>';
return html;
}
function _getDropdownListTemplate() {
var html = '';
html += ' <ul class="auto-complete-results">';
html += ' <li ng-if="ctrl.renderItems.length"';
html += ' ng-repeat="renderItem in ctrl.renderItems track by renderItem.id"';
html += ' ng-click="ctrl.selectItem($index, true)"';
html += ' class="auto-complete-item" data-index="{{ $index }}"';
html += ' ng-class="ctrl.getSelectedCssClass($index)">';
html += ' <auto-complete-item index="$index"';
html += ' item-template-link-fn="ctrl.itemTemplateLinkFn"';
html += ' render-item="renderItem"';
html += ' search-text="ctrl.searchText" />';
html += ' </li>';
html += ' <li ng-if="!ctrl.renderItems.length && ctrl.options.noMatchTemplateEnabled"';
html += ' class="auto-complete-item auto-complete-no-match">';
html += ' <auto-complete-no-match';
html += ' template="ctrl.options.noMatchTemplate"';
html += ' search-text="ctrl.searchText" />';
html += ' </li>';
html += ' </ul>';
return html;
}
function _wireupEvents() {
// when the target(textbox) gets focus activate the corresponding container
element.on(DOM_EVENT.FOCUS, function () {
scope.$evalAsync(function () {
ctrl.activate();
if (ctrl.options.activateOnFocus) {
_waitAndQuery(element.val(), 100);
}
});
});
element.on(DOM_EVENT.INPUT, function () {
scope.$evalAsync(function () {
_tryQuery(element.val());
});
});
element.on(DOM_EVENT.KEYDOWN, function (event) {
var $event = event;
scope.$evalAsync(function () {
_handleElementKeyDown($event);
});
});
ctrl.container.find('ul').on(DOM_EVENT.SCROLL, function () {
if (!ctrl.options.pagingEnabled) {
return;
}
var list = this;
scope.$evalAsync(function () {
if (!ctrl.containerVisible) {
return;
}
// scrolled to the bottom?
if ((list.offsetHeight + list.scrollTop) >= list.scrollHeight) {
ctrl.tryLoadNextPage();
}
});
});
$document.on(DOM_EVENT.KEYDOWN, function (event) {
var $event = event;
scope.$evalAsync(function () {
_handleDocumentKeyDown($event);
});
});
$document.on(DOM_EVENT.CLICK, function (event) {
var $event = event;
scope.$evalAsync(function () {
_handleDocumentClick($event);
});
});
// $window is a reference to the browser's window object
angular.element($window).on(DOM_EVENT.RESIZE, function () {
if (ctrl.options.hideDropdownOnWindowResize) {
scope.$evalAsync(function () {
ctrl.autoHide();
});
}
});
}
function _ignoreKeyCode(keyCode) {
return [
KEYCODE.TAB,
KEYCODE.ALT,
KEYCODE.CTRL,
KEYCODE.LEFTARROW,
KEYCODE.RIGHTARROW,
KEYCODE.MAC_COMMAND_LEFT,
KEYCODE.MAC_COMMAND_RIGHT
].indexOf(keyCode) !== -1;
}
function _handleElementKeyDown(event) {
var keyCode = event.charCode || event.keyCode || 0;
if (_ignoreKeyCode(keyCode)) {
return;
}
switch (keyCode) {
case KEYCODE.UPARROW:
ctrl.scrollToPreviousItem();
event.stopPropagation();
event.preventDefault();
break;
case KEYCODE.DOWNARROW:
ctrl.scrollToNextItem();
event.stopPropagation();
event.preventDefault();
break;
case KEYCODE.ENTER:
ctrl.selectItem(ctrl.selectedIndex, true);
//prevent postback upon hitting enter
event.preventDefault();
event.stopPropagation();
break;
case KEYCODE.ESCAPE:
ctrl.restoreOriginalText();
ctrl.autoHide();
event.preventDefault();
event.stopPropagation();
break;
default:
break;
}
}
function _handleDocumentKeyDown() {
// hide inactive dropdowns when multiple auto complete exist on a page
helperService.hideAllInactive();
}
function _handleDocumentClick(event) {
// hide inactive dropdowns when multiple auto complete exist on a page
helperService.hideAllInactive();
// ignore inline
if (ctrl.isInline()) {
return;
}
// no container. probably destroyed in scope $destroy
if (!ctrl.container) {
return;
}
// ignore target click
if (event.target === ctrl.target[0]) {
event.stopPropagation();
return;
}
if (_containerContainsTarget(event.target)) {
event.stopPropagation();
return;
}
ctrl.autoHide();
}
function _tryQuery(searchText) {
// query only if minimum number of chars are typed; else hide dropdown
if ((ctrl.options.minimumChars === 0)
|| (searchText && searchText.trim().length !== 0 && searchText.length >= ctrl.options.minimumChars)) {
_waitAndQuery(searchText);
return;
}
ctrl.autoHide();
}
function _waitAndQuery(searchText, delay) {
// wait few millisecs before calling query(); this to check if the user has stopped typing
var promise = $timeout(function () {
// has searchText unchanged?
if (searchText === element.val()) {
ctrl.query(searchText);
}
//cancel the timeout
$timeout.cancel(promise);
}, (delay || 300));
}
function _containerContainsTarget(target) {
// use native Node.contains
// https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
var container = ctrl.container[0];
if (angular.isFunction(container.contains) && container.contains(target)) {
return true;
}
// otherwise use .has() if jQuery is available
if (window.jQuery && angular.isFunction(ctrl.container.has) &&
ctrl.container.has(target).length > 0) {
return true;
}
// assume target is not in container
return false;
}
// cleanup on destroy
var destroyFn = scope.$on('$destroy', function () {
if (ctrl.container) {
ctrl.container.remove();
ctrl.container = null;
}
destroyFn();
});
}
}
MainCtrl.$inject = ['$q', '$window', '$document', '$timeout', '$templateRequest', '$compile', '$exceptionHandler'];
function MainCtrl($q, $window, $document, $timeout, $templateRequest, $compile, $exceptionHandler) {
var that = this;
var originalSearchText = null;
var queryCounter = 0;
var dataLoadInProgress = false;
var endOfPagedList = false;
var currentPageIndex = 0;
this.target = null;
this.instanceId = -1;
this.selectedIndex = -1;
this.renderItems = [];
this.containerVisible = false;
this.searchText = null;
this.itemTemplateLinkFn = null;
this.isInline = function () {
// if a dropdown jquery parent is provided it is assumed inline
return angular.isElement(that.options.dropdownParent);
};
this.init = function (options) {
that.instanceId = helperService.registerInstance(that);
that.options = options;
that.containerVisible = that.isInline();
_safeCallback(that.options.ready, publicApi);
};
this.activate = function () {
helperService.setActiveInstanceId(that.instanceId);
// do not reset if the container (dropdown list) is currently visible
// Ex: Switching to a different tab or window and switching back
// again when the dropdown list is visible.
if (!that.containerVisible) {
originalSearchText = that.searchText = null;
}
};
this.query = function (searchText) {
that.empty();
_reset();
return _query(searchText, 0);
};
this.show = function () {
// the show() method is called after the items are ready for display
// the textbox position can change (ex: window resize) when it has focus
// so reposition the dropdown before it's shown
_positionDropdown();
// callback
_safeCallback(that.options.dropdownShown);
};
this.autoHide = function () {
if (that.options && that.options.autoHideDropdown) {
_hideDropdown();
}
};
this.empty = function () {
that.selectedIndex = -1;
that.renderItems = [];
};
this.restoreOriginalText = function () {
if (!originalSearchText) {
return;
}
_setTargetValue(originalSearchText);
};
this.scrollToPreviousItem = function () {
var itemIndex = _getItemIndexFromOffset(-1);
if (itemIndex === -1) {
return;
}
_scrollToItem(itemIndex);
};
this.scrollToNextItem = function () {
var itemIndex = _getItemIndexFromOffset(1);
if (itemIndex === -1) {
return;
}
_scrollToItem(itemIndex);
if (_shouldLoadNextPageAtItemIndex(itemIndex)) {
_loadNextPage();
}
};
this.selectItem = function (itemIndex, closeDropdownAndRaiseCallback) {
var item = that.renderItems[itemIndex];
if (!item) {
return;
}
that.selectedIndex = itemIndex;
_updateTarget();
if (closeDropdownAndRaiseCallback) {
that.autoHide();
_safeCallback(that.options.itemSelected, { item: item.data });
}
};
this.getSelectedCssClass = function (itemIndex) {
return (itemIndex === that.selectedIndex) ? that.options.selectedCssClass : '';
};
this.tryLoadNextPage = function () {
if (_shouldLoadNextPage()) {
_loadNextPage();
}
};
function _loadNextPage() {
return _query(originalSearchText, (currentPageIndex + 1));
}
function _query(searchText, pageIndex) {
var params = {
searchText: searchText,
paging: {
pageIndex: pageIndex,
pageSize: that.options.pageSize
},
queryId: ++queryCounter
};
var renderListFn = (that.options.pagingEnabled ? _renderPagedList : _renderList);
return _queryInternal(params, renderListFn.bind(that, params));
}
function _queryInternal(params, renderListFn) {
// backup original search term in case we need to restore if user hits ESCAPE
originalSearchText = params.searchText;
dataLoadInProgress = true;
_safeCallback(that.options.loading);
return $q.when(that.options.data(params.searchText, params.paging),
function successCallback(result) {
// verify that the queryId did not change since the possibility exists that the
// search text changed before the 'data' promise was resolved. Say, due to a lag
// in getting data from a remote web service.
if (_didQueryIdChange(params)) {
that.autoHide();
return;
}
if (_shouldHideDropdown(params, result)) {
that.autoHide();
return;
}
renderListFn(result).then(function () {
that.searchText = params.searchText;
that.show();
});
// callback
_safeCallback(that.options.loadingComplete);
},
function errorCallback(error) {
that.autoHide();
_safeCallback(that.options.loadingComplete, { error: error });
}).then(function () {
dataLoadInProgress = false;
});
}
function _getItemIndexFromOffset(itemOffset) {
var itemIndex = that.selectedIndex + itemOffset;
if (itemIndex >= that.renderItems.length) {
return -1;
}
return itemIndex;
}
function _scrollToItem(itemIndex) {
if (!that.containerVisible) {
return;
}
that.selectItem(itemIndex);
var attrSelector = 'li[data-index="' + itemIndex + '"]';
// use jquery.scrollTo plugin if available
// http://flesler.blogspot.com/2007/10/jqueryscrollto.html
if (window.jQuery && window.jQuery.scrollTo) { // requires jquery to be loaded
that.elementUL.scrollTo(that.elementUL.find(attrSelector));
return;
}
var li = that.elementUL[0].querySelector(attrSelector);
if (li) {
// this was causing the page to jump/scroll
// li.scrollIntoView(true);
that.elementUL[0].scrollTop = li.offsetTop;
}
}
function _safeCallback(fn, args) {
if (!angular.isFunction(fn)) {
return;
}
try {
return fn.call(that.target, args);
} catch (ex) {
//ignore
}
}
function _positionDropdownIfVisible() {
if (that.containerVisible) {
_positionDropdown();
}
}
function _positionDropdown() {
// no need to position if container has been appended to
// parent specified in options
if (that.isInline()) {
return;
}
var dropdownWidth = null;
if (that.options.dropdownWidth && that.options.dropdownWidth !== 'auto') {
dropdownWidth = that.options.dropdownWidth;
}
else {
// same as textbox width
dropdownWidth = that.target[0].getBoundingClientRect().width + 'px';
}
that.container.css({ 'width': dropdownWidth });
if (that.options.dropdownHeight && that.options.dropdownHeight !== 'auto') {
that.elementUL.css({ 'max-height': that.options.dropdownHeight });
}
// use the .position() function from jquery.ui if available (requires both jquery and jquery-ui)
var hasJQueryUI = !!(window.jQuery && window.jQuery.ui);
if (that.options.positionUsingJQuery && hasJQueryUI) {
_positionUsingJQuery();
}
else {
_positionUsingDomAPI();
}
}
function _positionUsingJQuery() {
var defaultPosition = {
my: 'left top',
at: 'left bottom',
of: that.target,
collision: 'none flip'
};
var position = angular.extend({}, defaultPosition, that.options.positionUsing);
// jquery.ui position() requires the container to be visible to calculate its position.
if (!that.containerVisible) {
that.container.css({ 'visibility': 'hidden' });
}
that.containerVisible = true; // used in the template to set ng-show.
$timeout(function () {
that.container.position(position);
that.container.css({ 'visibility': 'visible' });
});
}
function _positionUsingDomAPI() {
var rect = that.target[0].getBoundingClientRect();
var DOCUMENT = $document[0];
var scrollTop = DOCUMENT.body.scrollTop || DOCUMENT.documentElement.scrollTop || $window.pageYOffset;
var scrollLeft = DOCUMENT.body.scrollLeft || DOCUMENT.documentElement.scrollLeft || $window.pageXOffset;
that.container.css({
'left': rect.left + scrollLeft + 'px',
'top': rect.top + rect.height + scrollTop + 'px'
});
that.containerVisible = true;
}
function _updateTarget() {
var item = that.renderItems[that.selectedIndex];
if (!item) {
return;
}
_setTargetValue(item.value);
}
function _setTargetValue(value) {
that.target.val(value);
that.textModelCtrl.$setViewValue(value);
}
function _hideDropdown() {
if (that.isInline() || !that.containerVisible) {
return;
}
// reset scroll position
//that.elementUL[0].scrollTop = 0;
that.containerVisible = false;
that.empty();
_reset();
// callback
_safeCallback(that.options.dropdownHidden);
}
function _shouldHideDropdown(params, result) {
// do not hide the dropdown if the no match template is enabled
// because the no match template is rendered within the dropdown container
if (that.options.noMatchTemplateEnabled) {
return false;
}
// do we have results to render?
if (!_.isEmpty(result)) {
return false;
}
// if paging is enabled hide the dropdown only when rendering the first page
if (that.options.pagingEnabled) {
return (params.paging.pageIndex === 0);
}
return true;
}
function _didQueryIdChange(params) {
return (params.queryId !== queryCounter);
}
function _renderList(params, result) {
return _getRenderFn().then(function (renderFn) {
if (_.isEmpty(result)) {
return;
}
that.renderItems = _renderItems(renderFn, result);
});
}
function _renderPagedList(params, result) {
return _getRenderFn().then(function (renderFn) {
if (_.isEmpty(result)) {
return;
}
var items = _renderItems(renderFn, result);
// in case of paged list we add to the array instead of replacing it
angular.forEach(items, function (item) {
that.renderItems.push(item);
});
currentPageIndex = params.paging.pageIndex;
endOfPagedList = (items.length < that.options.pageSize);
});
}
function _renderItems(renderFn, dataItems) {
// limit number of items rendered in the dropdown
var dataItemsToRender = _.slice(dataItems, 0, that.options.maxItemsToRender);
var itemsToRender = _.map(dataItemsToRender, function (data, index) {
// invoke render callback with the data as parameter
// this should return an object with a 'label' and 'value' property where
// 'label' is the template for display and 'value' is the text for the textbox
// If the object has an 'id' property, it will be used in the 'track by' clause of ng-repeat in the template
var item = renderFn(data);
if (!item || !item.hasOwnProperty('label') || !item.hasOwnProperty('value')) {
return null;
}
// store the data on the renderItem and add to array
item.data = data;
// unique 'id' for use in the 'track by' clause
item.id = item.hasOwnProperty('id') ? item.id : (item.value + item.label + index);
return item;
});
return _.filter(itemsToRender, function (item) {
return (item !== null);
});
}
function _getRenderFn() {
// user provided function
if (angular.isFunction(that.options.renderItem) && that.options.renderItem !== angular.noop) {
that.itemTemplateLinkFn = null;
return $q.when(that.options.renderItem.bind(null));
}
return _getItemTemplate().then(function (template) {
that.itemTemplateLinkFn = $compile(template);
return _getRenderItem.bind(null, template);
}).catch($exceptionHandler);
}
function _getItemTemplate() {
// itemTemplateUrl
if (that.options.itemTemplateUrl) {
return $templateRequest(that.options.itemTemplateUrl);
}
// itemTemplate or default
var template = that.options.itemTemplate || '<span ng-bind-html="entry.item"></span>';
return $q.when(template);
}
function _getRenderItem(template, data) {
var value = (angular.isObject(data) && that.options.selectedTextAttr) ? data[that.options.selectedTextAttr] : data;
return {
value: value,
label: template
};
}
function _shouldLoadNextPage() {
return that.options.pagingEnabled && !dataLoadInProgress && !endOfPagedList;
}
function _shouldLoadNextPageAtItemIndex(itemIndex) {
if (!_shouldLoadNextPage()) {
return false;
}
var triggerIndex = that.renderItems.length - that.options.invokePageLoadWhenItemsRemaining - 1;
return itemIndex >= triggerIndex;
}
function _reset() {
originalSearchText = that.searchText = null;
currentPageIndex = 0;
endOfPagedList = false;
}
function _setOptions(options) {
if (_.isEmpty(options)) {
return;
}
angular.forEach(options, function (value, key) {
if (defaultOptions.hasOwnProperty(key)) {
that.options[key] = value;
}
});
}
var publicApi = (function () {
return {
setOptions: _setOptions,
positionDropdown: _positionDropdownIfVisible,
hideDropdown: _hideDropdown
};
})();
}
autoCompleteItemDirective.$inject = ['$compile', '$rootScope', '$sce', '$controller'];
function autoCompleteItemDirective($compile, $rootScope, $sce, $controller) {
return {
restrict: 'E',
transclude: 'element',
scope: {},
controllerAs: 'ctrl',
bindToController: {
index: '<',
renderItem: '<',
searchText: '<',
itemTemplateLinkFn: '<'
},
controller: function () { },
link: function (scope, element) {
var linkFn = null;
if (_.isFunction(scope.ctrl.itemTemplateLinkFn)) {
linkFn = scope.ctrl.itemTemplateLinkFn;
}
else {
// Needed to maintain backward compatibility since the parameter passed to $compile must be html.
// When 'item' is returned from the 'options.renderItem' callback the 'label' might contain
// a trusted value [returned by a call to $sce.trustAsHtml(html)]. We can get the original
// html that was provided to $sce.trustAsHtml using the valueOf() function.
// If 'label' is not a value that had been returned by $sce.trustAsHtml, it will be returned unchanged.
var template = $sce.valueOf(scope.ctrl.renderItem.label);
linkFn = $compile(template);
}
linkFn(createEntryScope(scope), function (clonedElement) {
// append to the directive element's parent (<li>) since this directive element is replaced (transclude is set to 'element').
$(element[0].parentNode).append(clonedElement);
});
}
};
function createEntryScope(directiveScope) {
var entryScope = $rootScope.$new(true);
// for now its an empty controller. Additional logic can be added to this controller if needed
var entry = entryScope.entry = $controller(angular.noop);
var deregisterWatchesFn = _.map(['index', 'renderItem', 'searchText'], function (key) {
return directiveScope.$watch(('ctrl.' + key), function (newVal) {
switch (key) {
case 'renderItem':
// add 'item' property on entryScope for backward compatibility
entry.item = entryScope.item = newVal.data;
break;
default:
entry[key] = newVal;
break;
}
});
});
helperService.deregisterOnDestroy(directiveScope, deregisterWatchesFn);
return entryScope;
}
}
autoCompleteNoMatchDirective.$inject = ['$compile', '$rootScope', '$controller'];
function autoCompleteNoMatchDirective($compile, $rootScope, $controller) {
return {
restrict: 'E',
transclude: 'element',
scope: {},
controllerAs: 'ctrl',
bindToController: {
template: '<',
searchText: '<'
},
controller: function () { },
link: function (scope, element) {
var linkFn = $compile(scope.ctrl.template);
linkFn(createEntryScope(scope), function (clonedElement) {
// append to the directive element's parent (<li>) since this directive element is replaced (transclude is set to 'element').
$(element[0].parentNode).append(clonedElement);
});
}
};
function createEntryScope(directiveScope) {
var entryScope = $rootScope.$new(true);
// for now its an empty controller. Additional logic can be added to this controller if needed
var entry = entryScope.entry = $controller(angular.noop);
var deregisterFn = directiveScope.$watch('ctrl.searchText', function (newVal) {
entry.searchText = newVal;
});
helperService.deregisterOnDestroy(directiveScope, [deregisterFn]);
return entryScope;
}
}
function HelperService() {
var that = this;
var plugins = [];
var instanceCount = 0;
var activeInstanceId = 0;
this.registerInstance = function (instance) {
if (instance) {
plugins.push(instance);
return ++instanceCount;
}
return -1;
};
this.setActiveInstanceId = function (instanceId) {
activeInstanceId = instanceId;
that.hideAllInactive();
};
this.hideAllInactive = function () {
angular.forEach(plugins, function (ctrl) {
// hide if this is not the active instance
if (ctrl.instanceId !== activeInstanceId) {
ctrl.autoHide();
}
});
};
this.deregisterOnDestroy = function (scope, deregisterWatchesFn) {
// cleanup on destroy
var destroyFn = scope.$on('$destroy', function () {
_.each(deregisterWatchesFn, function (deregisterFn) {
deregisterFn();
});
destroyFn();
});
};
}
var DOM_EVENT = {
RESIZE: 'resize',
SCROLL: 'scroll',
CLICK: 'click',
KEYDOWN: 'keydown',
FOCUS: 'focus',
INPUT: 'input'
};
var KEYCODE = {
TAB: 9,
ENTER: 13,
CTRL: 17,
ALT: 18,
ESCAPE: 27,
LEFTARROW: 37,
UPARROW: 38,
RIGHTARROW: 39,
DOWNARROW: 40,
MAC_COMMAND_LEFT: 91,
MAC_COMMAND_RIGHT: 93
};
var defaultOptions = {
/**
* CSS class applied to the dropdown container.
* @default null
*/
containerCssClass: null,
/**
* CSS class applied to the selected list element.
* @default auto-complete-item-selected
*/
selectedCssClass: 'auto-complete-item-selected',
/**
* Minimum number of characters required to display the dropdown.
* @default 1
*/
minimumChars: 1,
/**
* Maximum number of items to render in the list.
* @default 20
*/
maxItemsToRender: 20,
/**
* If true displays the dropdown list when the textbox gets focus.
* @default false
*/
activateOnFocus: false,
/**
* Width in "px" of the dropddown list. This can also be applied using CSS.
* @default 'auto'
*/
dropdownWidth: 'auto',
/**
* Maximum height in "px" of the dropddown list. This can also be applied using CSS.
* @default 'auto'
*/
dropdownHeight: 'auto',
/**
* a jQuery object to append the dropddown list.
* @default null
*/
dropdownParent: null,
/**
* If the data for the dropdown is a collection of objects, this should be the name
* of a property on the object. The property value will be used to update the input textbox.
* @default null
*/
selectedTextAttr: null,
/**
* A template for the dropddown list item. For example "<p ng-bind-html='entry.item.name'></p>";
* Or using interpolation "<p>{{entry.item.lastName}}, {{entry.item.firstName}}></p>".
* @default null
*/
itemTemplate: null,
/**
* This is similar to template but the template is loaded from the specified URL, asynchronously.
* @default null
*/
itemTemplateUrl: null,
/**
* Set to true to enable server side paging. See "data" callback for more information.
* @default false
*/
pagingEnabled: false,
/**
* The number of items to display per page when paging is enabled.
* @default 5
*/
pageSize: 5,
/**
* When using the keyboard arrow key to scroll down the list, the "data" callback will
* be invoked when at least this many items remain below the current focused item.
* Note that dragging the vertical scrollbar to the bottom of the list might also invoke a "data" callback.
* @default 1
*/
invokePageLoadWhenItemsRemaining: 1,
/**
* Set to true to position the dropdown list using the position() method from the jQueryUI library.
* See <a href="https://api.jqueryui.com/position/">jQueryUI.position() documentation</a>
* @default true
* @bindAsHtml true
*/
positionUsingJQuery: true,
/**
* Options that will be passed to jQueryUI position() method.
* @default null
*/
positionUsing: null,
/**
* Set to true to let the plugin hide the dropdown list. If this option is set to false you can hide the dropdown list
* with the hideDropdown() method available in the ready callback.
* @default true
*/
autoHideDropdown: true,
/**
* Set to true to hide the dropdown list when the window is resized. If this option is set to false you can hide
* or re-position the dropdown list with the hideDropdown() or positionDropdown() methods available in the ready.
* callback.
* @default true
*/
hideDropdownOnWindowResize: true,
/**
* Set to true to enable the template to display a message when no items match the search text.
* @default true
*/
noMatchTemplateEnabled: true,
/**
* The template used to display the message when no items match the search text.
* @default "<span>No results match '{{entry.searchText}}'></span>"
*/
noMatchTemplate: "<span>No results match '{{entry.searchText}}'</span>",
/**
* Callback after the plugin is initialized and ready. The callback receives an object with the following methods:
* @default angular.noop
*/
ready: angular.noop,
/**
* Callback before the "data" callback is invoked.
* @default angular.noop
*/
loading: angular.noop,
/**
* Callback to get the data for the dropdown. The callback receives the search text as the first parameter.
* If paging is enabled the callback receives an object with "pageIndex" and "pageSize" properties as the second parameter.
* This function must return a promise.
* @default angular.noop
*/
data: angular.noop,
/**
* Callback after the items are rendered in the dropdown
* @default angular.noop
*/
loadingComplete: angular.noop,
/**
* Callback for custom rendering a list item. This is called for each item in the dropdown.
* This must return an object literal with "value" and "label" properties where
* "label" is the template for display and "value" is the text for the textbox.
* If the object has an "id" property, it will be used in the "track by" clause of the ng-repeat of the dropdown list.
* @default angular.noop
*/
renderItem: angular.noop,
/**
* Callback after an item is selected from the dropdown. The callback receives an object with an "item" property representing the selected item.
* @default angular.noop
*/
itemSelected: angular.noop,
/**
* Callback after the dropdown is shown.
* @default angular.noop
*/
dropdownShown: angular.noop,
/**
* Callback after the dropdown is hidden.
* @default angular.noop
*/
dropdownHidden: angular.noop
};
}));