(function (factory){
'use strict';
if(typeof define==='function'&&define.amd){
define(['jquery'], factory);
}else if(typeof exports==='object'&&typeof require==='function'){
factory(require('jquery'));
}else{
factory(jQuery);
}}(function ($){
'use strict';
var
utils=(function (){
return {
escapeRegExChars: function (value){
return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
createNode: function (containerClass){
var div=document.createElement('div');
div.className=containerClass;
div.style.position='absolute';
div.style.display='none';
return div;
}};}()),
keys={
ESC: 27,
TAB: 9,
RETURN: 13,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
};
function wdTrim(data){
if(null==data){
return '';
}else if('string'==typeof data){
return data.trim();
}else{
return (data + '').replace('/^[\\s\uFEFF\xA0]+|[\\s\uFEFF\xA0]+$/g', '');
}}
function Autocomplete(el, options){
var noop=function (){ },
that=this,
defaults={
ajaxSettings: {},
autoSelectFirst: false,
appendTo: document.body,
serviceUrl: null,
lookup: null,
onSelect: null,
width: 'auto',
minChars: 1,
maxHeight: 300,
deferRequestBy: 0,
params: {},
formatResult: Autocomplete.formatResult,
delimiter: null,
zIndex: 9999,
type: 'GET',
noCache: false,
onSearchStart: noop,
onSearchComplete: noop,
onSearchError: noop,
preserveInput: false,
containerClass: 'wd-search-suggestions',
tabDisabled: false,
dataType: 'text',
currentRequest: null,
triggerSelectOnValidInput: true,
preventBadQueries: true,
lookupFilter: function (suggestion, originalQuery, queryLowerCase){
return suggestion.value.toLowerCase().indexOf(queryLowerCase)!==-1;
},
paramName: 'query',
transformResult: function (response){
return typeof response==='string' ? JSON.parse(response):response;
},
showNoSuggestionNotice: false,
noSuggestionNotice: 'No results',
orientation: 'bottom',
forceFixPosition: false
};
that.element=el;
that.el=$(el);
that.suggestions=[];
that.badQueries=[];
that.selectedIndex=-1;
that.currentValue=that.element.value;
that.intervalId=0;
that.cachedResponse={};
that.onChangeInterval=null;
that.onChange=null;
that.isLocal=false;
that.suggestionsContainer=null;
that.noSuggestionsContainer=null;
that.options=$.extend({}, defaults, options);
that.classes={
selected: 'wd-active',
suggestion: 'wd-suggestion'
};
that.hint=null;
that.hintValue='';
that.selection=null;
that.initialize();
that.setOptions(options);
}
Autocomplete.utils=utils;
$.Autocomplete=Autocomplete;
Autocomplete.formatResult=function (suggestion, currentValue){
var pattern='(' + utils.escapeRegExChars(currentValue) + ')';
return suggestion.value
.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/&lt;(\/?strong)&gt;/g, '<$1>');
};
Autocomplete.prototype={
killerFn: null,
initialize: function (){
var that=this,
suggestionSelector=`.${that.classes.suggestion}`,
selected=that.classes.selected,
options=that.options,
container;
that.element.setAttribute('autocomplete', 'off');
that.killerFn=function (e){
if($(e.target).closest('.' + that.options.containerClass).length===0){
that.killSuggestions(e);
that.disableKillerFn();
}};
that.noSuggestionsContainer=$('<div class="autocomplete-no-suggestion"></div>')
.html(this.options.noSuggestionNotice).get(0);
that.suggestionsContainer=Autocomplete.utils.createNode(options.containerClass);
container=$(that.suggestionsContainer);
container.appendTo(options.appendTo);
if(options.width!=='auto'){
container.width(options.width);
}
container.on('mouseover.autocomplete', suggestionSelector, function (e){
if($(this).hasClass('wd-not-found')){
e.preventDefault();
return false;
}
that.activate($(this).data('index'));
});
container.on('mouseout.autocomplete', function (){
that.selectedIndex=-1;
container.find('.' + selected).removeClass(selected);
});
container.on('click.autocomplete', suggestionSelector, function (e){
if($(this).hasClass('wd-not-found')||$(this).hasClass('wd-search-title')){
e.preventDefault();
return false;
}
var doNothing=$(this).find('> a').length > 0;
that.select($(this).data('index'), doNothing);
});
that.fixPositionCapture=function (){
if(that.visible){
that.fixPosition();
}};
$(window).on('resize.autocomplete', that.fixPositionCapture);
that.el.on('keydown.autocomplete', function (e){ that.onKeyPress(e); });
that.el.on('keyup.autocomplete', function (e){ that.onKeyUp(e); });
that.el.on('blur.autocomplete', function (){ that.onBlur(); });
that.el.on('focus.autocomplete', function (){ that.onFocus(); });
that.el.on('change.autocomplete', function (e){ that.onKeyUp(e); });
that.el.on('input.autocomplete', function (e){ that.onKeyUp(e); });
var clearBtn=that.el.parent().find('.wd-clear-search');
if(clearBtn){
clearBtn.on('click', function (e){ that.onClearSearch(e); });
}},
onClearSearch: function (e){
var that=this;
if(e.target.classList.contains('wd-clear-search')){
e.target.classList.add('wd-hide');
}
that.clear();
that.killSuggestions(e);
that.el.trigger('focus');
},
onFocus: function (){
var that=this;
that.fixPosition();
if(that.options.minChars===0&&that.el.val().length===0){
that.onValueChange();
}},
onBlur: function (){
this.enableKillerFn();
},
abortAjax: function (){
var that=this;
if(that.currentRequest){
that.currentRequest.abort();
that.currentRequest=null;
}},
setOptions: function (suppliedOptions){
var that=this,
options=that.options;
$.extend(options, suppliedOptions);
that.isLocal=Array.isArray(options.lookup);
if(that.isLocal){
options.lookup=that.verifySuggestionsFormat(options.lookup);
}
options.orientation=that.validateOrientation(options.orientation, 'bottom');
$(that.suggestionsContainer).css({
'max-height': options.maxHeight + 'px',
'width': options.width + 'px',
'z-index': options.zIndex
});
},
clearCache: function (){
this.cachedResponse={};
this.badQueries=[];
},
clear: function (){
this.clearCache();
this.currentValue='';
this.suggestions=[];
},
disable: function (){
var that=this;
that.disabled=true;
clearInterval(that.onChangeInterval);
that.abortAjax();
},
enable: function (){
this.disabled=false;
},
fixPosition: function (){
var that=this,
$container=$(that.suggestionsContainer),
containerParent=$container.parent().get(0);
if(containerParent!==document.body&&!that.options.forceFixPosition){
return;
}
var orientation=that.options.orientation,
containerHeight=$container.outerHeight(),
height=that.el.outerHeight(),
offset=that.el.offset(),
styles={ 'top': offset.top, 'left': offset.left };
if(orientation==='auto'){
var viewPortHeight=$(window).height(),
scrollTop=$(window).scrollTop(),
topOverflow=-scrollTop + offset.top - containerHeight,
bottomOverflow=scrollTop + viewPortHeight - (offset.top + height + containerHeight);
orientation=(Math.max(topOverflow, bottomOverflow)===topOverflow) ? 'top':'bottom';
}
if(orientation==='top'){
styles.top +=-containerHeight;
}else{
styles.top +=height;
}
if(containerParent!==document.body){
var opacity=$container.css('opacity'),
parentOffsetDiff;
if(!that.visible){
$container.css('opacity', 0).show();
}
parentOffsetDiff=$container.offsetParent().offset();
styles.top -=parentOffsetDiff.top;
styles.left -=parentOffsetDiff.left;
if(!that.visible){
$container.css('opacity', opacity).hide();
}}
if(that.options.width==='auto'){
styles.width=(that.el.outerWidth() - 2) + 'px';
}
$container.css(styles);
},
enableKillerFn: function (){
var that=this;
$(document).on('click.autocomplete', that.killerFn);
},
disableKillerFn: function (){
var that=this;
$(document).off('click.autocomplete', that.killerFn);
},
killSuggestions: function (e){
var that=this;
var isClearBtn=$(e.target).hasClass('wd-clear-search');
that.stopKillSuggestions();
that.intervalId=window.setInterval(function (){
if(that.visible){
that.el.val(that.currentValue);
that.hide(false, isClearBtn);
}
that.stopKillSuggestions();
}, 50);
},
stopKillSuggestions: function (){
window.clearInterval(this.intervalId);
},
isCursorAtEnd: function (){
var that=this,
valLength=that.el.val().length,
selectionStart=that.element.selectionStart,
range;
if(typeof selectionStart==='number'){
return selectionStart===valLength;
}
if(document.selection){
range=document.selection.createRange();
range.moveStart('character', -valLength);
return valLength===range.text.length;
}
return true;
},
onKeyPress: function (e){
var that=this;
if(!that.disabled&&!that.visible&&e.which===keys.DOWN&&that.currentValue){
that.suggest();
return;
}
if(that.disabled||!that.visible){
return;
}
switch (e.which){
case keys.ESC:
that.el.val(that.currentValue);
that.hide();
break;
case keys.RIGHT:
if(that.hint&&that.options.onHint&&that.isCursorAtEnd()){
that.selectHint();
break;
}
return;
case keys.TAB:
if(that.hint&&that.options.onHint){
that.selectHint();
return;
}
if(that.selectedIndex===-1){
that.hide();
return;
}
that.select(that.selectedIndex);
if(that.options.tabDisabled===false){
return;
}
break;
case keys.RETURN:
if(-1===that.selectedIndex){
that.hide(true);
return;
}
that.select(that.selectedIndex, true);
break;
case keys.UP:
that.moveUp();
break;
case keys.DOWN:
that.moveDown();
break;
default:
return;
}
e.stopImmediatePropagation();
e.preventDefault();
},
onKeyUp: function (e){
var that=this;
if(that.disabled){
return;
}
switch (e.which){
case keys.UP:
case keys.DOWN:
return;
}
clearInterval(that.onChangeInterval);
if(that.currentValue!==that.el.val()){
that.findBestHint();
if(that.options.deferRequestBy > 0){
that.onChangeInterval=setInterval(function (){
that.onValueChange();
}, that.options.deferRequestBy);
}else{
that.onValueChange();
}}
},
onValueChange: function (){
var that=this,
options=that.options,
value=that.el.val(),
query=that.getQuery(value);
if(that.selection&&that.currentValue!==query){
that.selection=null;
(options.onInvalidateSelection||$.noop).call(that.element);
}
clearInterval(that.onChangeInterval);
that.currentValue=value;
that.selectedIndex=-1;
if(options.triggerSelectOnValidInput&&that.isExactMatch(query)){
that.select(0);
return;
}
if(query.length < options.minChars){
that.hide();
}else{
that.getSuggestions(query);
}},
isExactMatch: function (query){
var suggestions=this.suggestions;
return (suggestions.length===1&&suggestions[0].value.toLowerCase()===query.toLowerCase());
},
getQuery: function (value){
var delimiter=this.options.delimiter,
parts;
if(!delimiter){
return value;
}
parts=value.split(delimiter);
return wdTrim(parts[parts.length - 1]);
},
getSuggestionsLocal: function (query){
var that=this,
options=that.options,
queryLowerCase=query.toLowerCase(),
filter=options.lookupFilter,
limit=parseInt(options.lookupLimit, 10),
data;
data={
suggestions: $.grep(options.lookup, function (suggestion){
return filter(suggestion, query, queryLowerCase);
})
};
if(limit&&data.suggestions.length > limit){
data.suggestions=data.suggestions.slice(0, limit);
}
return data;
},
getSuggestions: function (q){
var response,
that=this,
options=that.options,
serviceUrl=options.serviceUrl,
params,
cacheKey,
ajaxSettings;
options.params[options.paramName]=q;
params=options.ignoreParams ? null:options.params;
if(options.onSearchStart.call(that.element, options.params)===false){
return;
}
if(typeof options.lookup==="function"){
options.lookup(q, function (data){
that.suggestions=data.suggestions;
that.suggest();
options.onSearchComplete.call(that.element, q, data.suggestions);
});
return;
}
if(that.isLocal){
response=that.getSuggestionsLocal(q);
}else{
if(typeof serviceUrl==="function"){
serviceUrl=serviceUrl.call(that.element, q);
}
cacheKey=serviceUrl + '?' + $.param(params||{});
response=that.cachedResponse[cacheKey];
}
if(response&&Array.isArray(response.suggestions)){
that.suggestions=response.suggestions;
that.suggest();
options.onSearchComplete.call(that.element, q, response.suggestions);
}else if(!that.isBadQuery(q)){
that.abortAjax();
ajaxSettings={
url: serviceUrl,
data: params,
type: options.type,
dataType: options.dataType
};
$.extend(ajaxSettings, options.ajaxSettings);
that.currentRequest=$.ajax(ajaxSettings).done(function (data){
var result;
that.currentRequest=null;
result=options.transformResult(data, q);
that.processResponse(result, q, cacheKey);
options.onSearchComplete.call(that.element, q, result.suggestions);
}).fail(function (jqXHR, textStatus, errorThrown){
options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown);
});
}else{
options.onSearchComplete.call(that.element, q, []);
}},
isBadQuery: function (q){
if(!this.options.preventBadQueries){
return false;
}
var badQueries=this.badQueries,
i=badQueries.length;
while (i--){
if(q.indexOf(badQueries[i])===0){
return true;
}}
return false;
},
hide: function (doNothing=false, isClearBtn=false){
if(doNothing){
return;
}
var that=this;
var container=$(that.suggestionsContainer);
if(typeof that.options.onHide==="function"&&that.visible){
that.options.onHide.call(that.element, container, isClearBtn);
}
that.visible=false;
that.selectedIndex=-1;
clearInterval(that.onChangeInterval);
$(that.suggestionsContainer).hide();
that.signalHint(null);
},
suggest: function (){
if(this.suggestions.length===0){
if(this.options.showNoSuggestionNotice){
this.noSuggestions();
}else{
this.hide();
}
return;
}
var that=this,
options=that.options,
groupBy=options.groupBy,
formatResult=options.formatResult,
value=that.getQuery(that.currentValue),
className=that.classes.suggestion,
classSelected=that.classes.selected,
container=$(that.suggestionsContainer),
noSuggestionsContainer=$(that.noSuggestionsContainer),
beforeRender=options.beforeRender,
html='',
category,
formatGroup=function (suggestion, index){
var currentCategory=suggestion.data[groupBy];
if(category===currentCategory){
return '';
}
category=currentCategory;
return '<div class="autocomplete-group"><strong>' + category + '</strong></div>';
};
if(options.triggerSelectOnValidInput&&that.isExactMatch(value)){
that.select(0);
return;
}
var getSuggestionsHtml=function(suggestions, html=''){
$.each(suggestions, function (i, suggestion){
if(groupBy){
html +=formatGroup(suggestion, value, i);
}
var itemClassName=className;
if(suggestion.item_classes){
itemClassName +=' ' + suggestion.item_classes;
}
html +='<div class="' + itemClassName + '" data-index="' + i + '">' + formatResult(suggestion, value) + '</div>';
});
return html;
}
var indexCounter=0;
var grupedSuggestions=that.suggestions.reduce((acc, suggestion)=> {
const group=suggestion.group||'default';
if(!acc[group]){
acc[group]={};}
acc[group][indexCounter]=suggestion;
indexCounter++;
return acc;
}, {});
if(grupedSuggestions){
$.each(grupedSuggestions, function (group, suggestions){
$.each(suggestions, function(i, suggestion){
if(! suggestion){
return;
}
if(suggestion.divider){
html +='<div class="wd-search-title title" data-index="' + i + '">' + suggestion.divider + '</div>';
delete suggestions[i];
}});
var groupClassName=`wd-suggestions-group wd-type-${group}`;
html +='<div class="' + groupClassName + '">';
html=getSuggestionsHtml(suggestions, html);
html +='</div>';
});
}else{
html=getSuggestionsHtml(that.suggestions);
}
this.adjustContainerWidth();
noSuggestionsContainer.detach();
container.html(html);
if(typeof beforeRender==="function"){
beforeRender.call(that.element, container);
}
that.fixPosition();
container.show();
if(options.autoSelectFirst){
that.selectedIndex=0;
container.scrollTop(0);
container.children('.' + className).first().addClass(classSelected);
}
that.visible=true;
that.findBestHint();
},
noSuggestions: function(){
var that=this,
container=$(that.suggestionsContainer),
noSuggestionsContainer=$(that.noSuggestionsContainer);
this.adjustContainerWidth();
noSuggestionsContainer.detach();
container.empty();
container.append(noSuggestionsContainer);
that.fixPosition();
container.show();
that.visible=true;
},
adjustContainerWidth: function(){
var that=this,
options=that.options,
width,
container=$(that.suggestionsContainer);
if(options.width==='auto'){
width=that.el.outerWidth() - 2;
container.width(width > 0 ? width:300);
}},
findBestHint: function (){
var that=this,
value=that.el.val().toLowerCase(),
bestMatch=null;
if(!value){
return;
}
$.each(that.suggestions, function (i, suggestion){
var foundMatch=suggestion.value.toLowerCase().indexOf(value)===0;
if(foundMatch){
bestMatch=suggestion;
}
return !foundMatch;
});
that.signalHint(bestMatch);
},
signalHint: function (suggestion){
var hintValue='',
that=this;
if(suggestion){
hintValue=that.currentValue + suggestion.value.substr(that.currentValue.length);
}
if(that.hintValue!==hintValue){
that.hintValue=hintValue;
that.hint=suggestion;
(this.options.onHint||$.noop)(hintValue);
}},
verifySuggestionsFormat: function (suggestions){
if(suggestions.length&&typeof suggestions[0]==='string'){
return $.map(suggestions, function (value){
return { value: value, data: null };});
}
return suggestions;
},
validateOrientation: function(orientation, fallback){
orientation=$.trim(orientation||'').toLowerCase();
if($.inArray(orientation, ['auto', 'bottom', 'top'])===-1){
orientation=fallback;
}
return orientation;
},
processResponse: function (result, originalQuery, cacheKey){
var that=this,
options=that.options;
result.suggestions=that.verifySuggestionsFormat(result.suggestions);
if(!options.noCache){
that.cachedResponse[cacheKey]=result;
if(options.preventBadQueries&&result.suggestions.length===0){
that.badQueries.push(originalQuery);
}}
if(originalQuery!==that.getQuery(that.currentValue)){
return;
}
that.suggestions=result.suggestions;
that.suggest();
},
activate: function (index){
var that=this;
var selected=that.classes.selected;
var container=$(that.suggestionsContainer);
var activeItem=container.find(`.${that.classes.suggestion}[data-index="${index}"]`);
container.find('.' + selected).removeClass(selected);
that.selectedIndex=index;
if(that.selectedIndex!==-1&&activeItem){
$(activeItem).addClass(selected);
return activeItem;
}
return null;
},
selectHint: function (){
var that=this,
i=$.inArray(that.hint, that.suggestions);
that.select(i);
},
select: function (i, doNothing=false){
if(doNothing){
return;
}
var that=this;
that.hide();
that.onSelect(i);
},
moveUp: function (){
var that=this;
if(that.selectedIndex===-1){
return;
}
if(that.selectedIndex===0){
$(that.suggestionsContainer).children().first().removeClass(that.classes.selected);
that.selectedIndex=-1;
that.el.val(that.currentValue);
that.findBestHint();
return;
}
that.adjustScroll(that.selectedIndex - 1);
},
moveDown: function (){
var that=this;
if(that.selectedIndex===(that.suggestions.length - 1)){
return;
}
that.adjustScroll(that.selectedIndex + 1);
},
adjustScroll: function (index){
var that=this,
activeItem=that.activate(index);
if(!activeItem){
return;
}
var offsetTop,
upperBound,
lowerBound,
heightDelta=$(activeItem).outerHeight();
offsetTop=activeItem.offsetTop;
upperBound=$(that.suggestionsContainer).scrollTop();
lowerBound=upperBound + that.options.maxHeight - heightDelta;
if(offsetTop < upperBound){
$(that.suggestionsContainer).scrollTop(offsetTop);
}else if(offsetTop > lowerBound){
$(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
}
if(!that.options.preserveInput){
that.el.val(that.getValue(that.suggestions[index].value));
}
that.signalHint(null);
},
onSelect: function (index){
var that=this,
onSelectCallback=that.options.onSelect,
suggestion=that.suggestions[index];
that.currentValue=that.getValue(suggestion.value);
if(that.currentValue!==that.el.val()&&!that.options.preserveInput){
that.el.val(that.currentValue);
}
that.signalHint(null);
that.suggestions=[];
that.selection=suggestion;
if(typeof onSelectCallback==="function"){
onSelectCallback.call(that.element, suggestion);
}},
getValue: function (value){
var that=this,
delimiter=that.options.delimiter,
currentValue,
parts;
if(!delimiter){
return value;
}
currentValue=that.currentValue;
parts=currentValue.split(delimiter);
if(parts.length===1){
return value;
}
return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
},
dispose: function (){
var that=this;
that.el.off('.autocomplete').removeData('autocomplete');
that.disableKillerFn();
$(window).off('resize.autocomplete', that.fixPositionCapture);
$(that.suggestionsContainer).remove();
}};
$.fn.devbridgeAutocomplete=function (options, args){
var dataKey='autocomplete';
if(arguments.length===0){
return this.first().data(dataKey);
}
return this.each(function (){
var inputElement=$(this),
instance=inputElement.data(dataKey);
if(typeof options==='string'){
if(instance&&typeof instance[options]==='function'){
instance[options](args);
}}else{
if(instance&&instance.dispose){
instance.dispose();
}
instance=new Autocomplete(this, options);
inputElement.data(dataKey, instance);
}});
};}));