diff --git a/.gitignore b/.gitignore index acc0f65..f970be0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.idea* /*.xml /bower_components +/node_modules/* diff --git a/bower.json b/bower.json index 984336b..5e71d8e 100644 --- a/bower.json +++ b/bower.json @@ -1,24 +1,24 @@ { - "name": "devbridge-autocomplete", - "version": "1.2.9", - "homepage": "https://github.com/devbridge/jQuery-Autocomplete", - "authors": [ - "Tomas Kirda" - ], - "description": "Autocomplete provides suggestions while you type into the text field.", - "main": "dist/jquery.autocomplete.js", - "keywords": [ - "ajax", - "autocomplete" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "spec" - ], - "dependencies": { - "jquery": ">=1.7" - } -} + "name": "devbridge-autocomplete", + "version": "1.2.12", + "homepage": "https://github.com/devbridge/jQuery-Autocomplete", + "authors": [ + "Tomas Kirda" + ], + "description": "Autocomplete provides suggestions while you type into the text field.", + "main": "dist/jquery.autocomplete.js", + "keywords": [ + "ajax", + "autocomplete" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "spec" + ], + "dependencies": { + "jquery": ">=1.7" + } +} \ No newline at end of file diff --git a/devbridge-autocomplete.jquery.json b/devbridge-autocomplete.jquery.json index 89076e3..26c0f1d 100644 --- a/devbridge-autocomplete.jquery.json +++ b/devbridge-autocomplete.jquery.json @@ -6,7 +6,7 @@ "ajax", "autocomplete" ], - "version": "1.2.9", + "version": "1.2.12", "author": { "name": "Tomas Kirda", "url": "https://github.com/tkirda" @@ -20,7 +20,7 @@ "bugs": "https://github.com/devbridge/jQuery-Autocomplete/issues?state=open", "homepage": "https://github.com/devbridge/jQuery-Autocomplete", "docs": "https://github.com/devbridge/jQuery-Autocomplete", - "demo": "http://www.devbridge.com/projects/autocomplete/jquery/", + "demo": "http://www.devbridge.com/sourcery/components/jquery-autocomplete/", "download": "https://github.com/devbridge/jQuery-Autocomplete/tree/master/dist", "dependencies": { "jquery": ">=1.7" diff --git a/dist/jquery.autocomplete.js b/dist/jquery.autocomplete.js index 88548d4..2a5029e 100644 --- a/dist/jquery.autocomplete.js +++ b/dist/jquery.autocomplete.js @@ -1,14 +1,13 @@ /** -* Ajax Autocomplete for jQuery, version 1.2.9 -* (c) 2013 Tomas Kirda +* Ajax Autocomplete for jQuery, version 1.2.12 +* (c) 2014 Tomas Kirda * * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete -* */ /*jslint browser: true, white: true, plusplus: true */ -/*global define, window, document, jQuery */ +/*global define, window, document, jQuery, exports */ // Expose plugin as an AMD module if AMD loader is present: (function (factory) { @@ -16,6 +15,9 @@ if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); + } else if (typeof exports === 'object' && typeof require === 'function') { + // Browserify + factory(require('jquery')); } else { // Browser globals factory(jQuery); @@ -53,8 +55,9 @@ var noop = function () { }, that = this, defaults = { + ajaxSettings: {}, autoSelectFirst: false, - appendTo: 'body', + appendTo: document.body, serviceUrl: null, lookup: null, onSelect: null, @@ -76,13 +79,18 @@ 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' ? $.parseJSON(response) : response; - } + }, + showNoSuggestionNotice: false, + noSuggestionNotice: 'No results', + orientation: 'bottom', + forceFixPosition: false }; // Shared variables: @@ -98,6 +106,7 @@ that.onChange = null; that.isLocal = false; that.suggestionsContainer = null; + that.noSuggestionsContainer = null; that.options = $.extend({}, defaults, options); that.classes = { selected: 'autocomplete-selected', @@ -143,6 +152,10 @@ } }; + // html() deals with many types: htmlString or Element or Array or jQuery + that.noSuggestionsContainer = $('
') + .html(this.options.noSuggestionNotice).get(0); + that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass); container = $(that.suggestionsContainer); @@ -170,8 +183,6 @@ that.select($(this).data('index')); }); - that.fixPosition(); - that.fixPositionCapture = function () { if (that.visible) { that.fixPosition(); @@ -211,6 +222,8 @@ options.lookup = that.verifySuggestionsFormat(options.lookup); } + options.orientation = that.validateOrientation(options.orientation, 'bottom'); + // Adjust height, width and z-index: $(that.suggestionsContainer).css({ 'max-height': options.maxHeight + 'px', @@ -219,6 +232,7 @@ }); }, + clearCache: function () { this.cachedResponse = {}; this.badQueries = []; @@ -243,27 +257,65 @@ }, fixPosition: function () { - var that = this, - offset, - styles; + // Use only when container has already its content - // Don't adjsut position if custom container has been specified: - if (that.options.appendTo !== 'body') { + var that = this, + $container = $(that.suggestionsContainer), + containerParent = $container.parent().get(0); + // Fix position automatically when appended to body. + // In other cases force parameter must be given. + if (containerParent !== document.body && !that.options.forceFixPosition) return; + + // Choose orientation + 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'; } - offset = that.el.offset(); + if (orientation === 'top') { + styles.top += -containerHeight; + } else { + styles.top += height; + } - styles = { - top: (offset.top + that.el.outerHeight()) + 'px', - left: offset.left + 'px' - }; + // If container is not positioned to body, + // correct its position using offset parent offset + 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(); + } + } + + // -2px to account for suggestions border. if (that.options.width === 'auto') { styles.width = (that.el.outerWidth() - 2) + 'px'; } - $(that.suggestionsContainer).css(styles); + $container.css(styles); }, enableKillerFn: function () { @@ -396,7 +448,7 @@ query = that.getQuery(value), index; - if (that.selection) { + if (that.selection && that.currentValue !== query) { that.selection = null; (options.onInvalidateSelection || $.noop).call(that.element); } @@ -473,11 +525,12 @@ that = this, options = that.options, serviceUrl = options.serviceUrl, - data, - cacheKey; + params, + cacheKey, + ajaxSettings; options.params[options.paramName] = q; - data = options.ignoreParams ? null : options.params; + params = options.ignoreParams ? null : options.params; if (that.isLocal) { response = that.getSuggestionsLocal(q); @@ -485,7 +538,7 @@ if ($.isFunction(serviceUrl)) { serviceUrl = serviceUrl.call(that.element, q); } - cacheKey = serviceUrl + '?' + $.param(data || {}); + cacheKey = serviceUrl + '?' + $.param(params || {}); response = that.cachedResponse[cacheKey]; } @@ -499,15 +552,22 @@ if (that.currentRequest) { that.currentRequest.abort(); } - that.currentRequest = $.ajax({ + + ajaxSettings = { url: serviceUrl, - data: data, + data: params, type: options.type, dataType: options.dataType - }).done(function (data) { + }; + + $.extend(ajaxSettings, options.ajaxSettings); + + that.currentRequest = $.ajax(ajaxSettings).done(function (data) { + var result; that.currentRequest = null; - that.processResponse(data, q, cacheKey); - options.onSearchComplete.call(that.element, q); + result = options.transformResult(data); + 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); }); @@ -515,6 +575,10 @@ }, isBadQuery: function (q) { + if (!this.options.preventBadQueries){ + return false; + } + var badQueries = this.badQueries, i = badQueries.length; @@ -537,7 +601,7 @@ suggest: function () { if (this.suggestions.length === 0) { - this.hide(); + this.options.showNoSuggestionNotice ? this.noSuggestions() : this.hide(); return; } @@ -548,10 +612,10 @@ className = that.classes.suggestion, classSelected = that.classes.selected, container = $(that.suggestionsContainer), + noSuggestionsContainer = $(that.noSuggestionsContainer), beforeRender = options.beforeRender, html = '', - index, - width; + index; if (options.triggerSelectOnValidInput) { index = that.findSuggestionIndex(value); @@ -566,15 +630,9 @@ html += '
' + formatResult(suggestion, value) + '
'; }); - // If width is auto, adjust width before displaying suggestions, - // because if instance was created before input had width, it will be zero. - // Also it adjusts if input width has changed. - // -2px to account for suggestions border. - if (options.width === 'auto') { - width = that.el.outerWidth() - 2; - container.width(width > 0 ? width : 300); - } + this.adjustContainerWidth(); + noSuggestionsContainer.detach(); container.html(html); // Select first value by default: @@ -587,12 +645,49 @@ beforeRender.call(that.element, container); } + that.fixPosition(); + container.show(); that.visible = true; that.findBestHint(); }, + noSuggestions: function() { + var that = this, + container = $(that.suggestionsContainer), + noSuggestionsContainer = $(that.noSuggestionsContainer); + + this.adjustContainerWidth(); + + // Some explicit steps. Be careful here as it easy to get + // noSuggestionsContainer removed from DOM if not detached properly. + noSuggestionsContainer.detach(); + container.empty(); // clean suggestions if any + container.append(noSuggestionsContainer); + + that.fixPosition(); + + container.show(); + that.visible = true; + }, + + adjustContainerWidth: function() { + var that = this, + options = that.options, + width, + container = $(that.suggestionsContainer); + + // If width is auto, adjust width before displaying suggestions, + // because if instance was created before input had width, it will be zero. + // Also it adjusts if input width has changed. + // -2px to account for suggestions border. + 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(), @@ -637,18 +732,27 @@ return suggestions; }, - processResponse: function (response, originalQuery, cacheKey) { + 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 = options.transformResult(response, originalQuery); + options = that.options; result.suggestions = that.verifySuggestionsFormat(result.suggestions); // Cache results if cache is not disabled: if (!options.noCache) { that.cachedResponse[cacheKey] = result; - if (result.suggestions.length === 0) { - that.badQueries.push(cacheKey); + if (options.preventBadQueries && result.suggestions.length === 0) { + that.badQueries.push(originalQuery); } } @@ -666,9 +770,9 @@ activeItem, selected = that.classes.selected, container = $(that.suggestionsContainer), - children = container.children(); + children = container.find('.' + that.classes.suggestion); - container.children('.' + selected).removeClass(selected); + container.find('.' + selected).removeClass(selected); that.selectedIndex = index; @@ -754,7 +858,11 @@ suggestion = that.suggestions[index]; that.currentValue = that.getValue(suggestion.value); - that.el.val(that.currentValue); + + if (that.currentValue !== that.el.val()) { + that.el.val(that.currentValue); + } + that.signalHint(null); that.suggestions = []; that.selection = suggestion; @@ -794,7 +902,7 @@ }; // Create chainable jQuery plugin: - $.fn.autocomplete = function (options, args) { + $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) { var dataKey = 'autocomplete'; // If function invoked without argument return // instance of the first matched element: diff --git a/dist/jquery.autocomplete.min.js b/dist/jquery.autocomplete.min.js index 57b3b8d..f11e494 100644 --- a/dist/jquery.autocomplete.min.js +++ b/dist/jquery.autocomplete.min.js @@ -1,29 +1,8 @@ -/** -* Ajax Autocomplete for jQuery, version 1.2.9 -* (c) 2013 Tomas Kirda +/** +* Ajax Autocomplete for jQuery, version 1.2.12 +* (c) 2014 Tomas Kirda * * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete -* */ -(function(d){"function"===typeof define&&define.amd?define(["jquery"],d):d(jQuery)})(function(d){function g(a,b){var c=function(){},c={autoSelectFirst:!1,appendTo:"body",serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:g.formatResult,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:c,onSearchComplete:c,onSearchError:c,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",currentRequest:null,triggerSelectOnValidInput:!0, -lookupFilter:function(a,b,c){return-1!==a.value.toLowerCase().indexOf(c)},paramName:"query",transformResult:function(a){return"string"===typeof a?d.parseJSON(a):a}};this.element=a;this.el=d(a);this.suggestions=[];this.badQueries=[];this.selectedIndex=-1;this.currentValue=this.element.value;this.intervalId=0;this.cachedResponse={};this.onChange=this.onChangeInterval=null;this.isLocal=!1;this.suggestionsContainer=null;this.options=d.extend({},c,b);this.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"}; -this.hint=null;this.hintValue="";this.selection=null;this.initialize();this.setOptions(b)}var k=function(){return{escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},createNode:function(a){var b=document.createElement("div");b.className=a;b.style.position="absolute";b.style.display="none";return b}}}();g.utils=k;d.Autocomplete=g;g.formatResult=function(a,b){var c="("+k.escapeRegExChars(b)+")";return a.value.replace(RegExp(c,"gi"),"$1")};g.prototype= -{killerFn:null,initialize:function(){var a=this,b="."+a.classes.suggestion,c=a.classes.selected,e=a.options,f;a.element.setAttribute("autocomplete","off");a.killerFn=function(b){0===d(b.target).closest("."+a.options.containerClass).length&&(a.killSuggestions(),a.disableKillerFn())};a.suggestionsContainer=g.utils.createNode(e.containerClass);f=d(a.suggestionsContainer);f.appendTo(e.appendTo);"auto"!==e.width&&f.width(e.width);f.on("mouseover.autocomplete",b,function(){a.activate(d(this).data("index"))}); -f.on("mouseout.autocomplete",function(){a.selectedIndex=-1;f.children("."+c).removeClass(c)});f.on("click.autocomplete",b,function(){a.select(d(this).data("index"))});a.fixPosition();a.fixPositionCapture=function(){a.visible&&a.fixPosition()};d(window).on("resize.autocomplete",a.fixPositionCapture);a.el.on("keydown.autocomplete",function(b){a.onKeyPress(b)});a.el.on("keyup.autocomplete",function(b){a.onKeyUp(b)});a.el.on("blur.autocomplete",function(){a.onBlur()});a.el.on("focus.autocomplete",function(){a.onFocus()}); -a.el.on("change.autocomplete",function(b){a.onKeyUp(b)})},onFocus:function(){this.fixPosition();if(this.options.minChars<=this.el.val().length)this.onValueChange()},onBlur:function(){this.enableKillerFn()},setOptions:function(a){var b=this.options;d.extend(b,a);if(this.isLocal=d.isArray(b.lookup))b.lookup=this.verifySuggestionsFormat(b.lookup);d(this.suggestionsContainer).css({"max-height":b.maxHeight+"px",width:b.width+"px","z-index":b.zIndex})},clearCache:function(){this.cachedResponse={};this.badQueries= -[]},clear:function(){this.clearCache();this.currentValue="";this.suggestions=[]},disable:function(){this.disabled=!0;this.currentRequest&&this.currentRequest.abort()},enable:function(){this.disabled=!1},fixPosition:function(){var a;"body"===this.options.appendTo&&(a=this.el.offset(),a={top:a.top+this.el.outerHeight()+"px",left:a.left+"px"},"auto"===this.options.width&&(a.width=this.el.outerWidth()-2+"px"),d(this.suggestionsContainer).css(a))},enableKillerFn:function(){d(document).on("click.autocomplete", -this.killerFn)},disableKillerFn:function(){d(document).off("click.autocomplete",this.killerFn)},killSuggestions:function(){var a=this;a.stopKillSuggestions();a.intervalId=window.setInterval(function(){a.hide();a.stopKillSuggestions()},50)},stopKillSuggestions:function(){window.clearInterval(this.intervalId)},isCursorAtEnd:function(){var a=this.el.val().length,b=this.element.selectionStart;return"number"===typeof b?b===a:document.selection?(b=document.selection.createRange(),b.moveStart("character", --a),a===b.text.length):!0},onKeyPress:function(a){if(!this.disabled&&!this.visible&&40===a.which&&this.currentValue)this.suggest();else if(!this.disabled&&this.visible){switch(a.which){case 27:this.el.val(this.currentValue);this.hide();break;case 39:if(this.hint&&this.options.onHint&&this.isCursorAtEnd()){this.selectHint();break}return;case 9:if(this.hint&&this.options.onHint){this.selectHint();return}case 13:if(-1===this.selectedIndex){this.hide();return}this.select(this.selectedIndex);if(9===a.which&& -!1===this.options.tabDisabled)return;break;case 38:this.moveUp();break;case 40:this.moveDown();break;default:return}a.stopImmediatePropagation();a.preventDefault()}},onKeyUp:function(a){var b=this;if(!b.disabled){switch(a.which){case 38:case 40:return}clearInterval(b.onChangeInterval);if(b.currentValue!==b.el.val())if(b.findBestHint(),0f&&(b.suggestions=b.suggestions.slice(0,f));return b},getSuggestions:function(a){var b,c=this,e=c.options,f=e.serviceUrl,l,g;e.params[e.paramName]=a;l=e.ignoreParams?null:e.params; -c.isLocal?b=c.getSuggestionsLocal(a):(d.isFunction(f)&&(f=f.call(c.element,a)),g=f+"?"+d.param(l||{}),b=c.cachedResponse[g]);b&&d.isArray(b.suggestions)?(c.suggestions=b.suggestions,c.suggest()):c.isBadQuery(a)||!1===e.onSearchStart.call(c.element,e.params)||(c.currentRequest&&c.currentRequest.abort(),c.currentRequest=d.ajax({url:f,data:l,type:e.type,dataType:e.dataType}).done(function(b){c.currentRequest=null;c.processResponse(b,a,g);e.onSearchComplete.call(c.element,a)}).fail(function(b,d,f){e.onSearchError.call(c.element, -a,b,d,f)}))},isBadQuery:function(a){for(var b=this.badQueries,c=b.length;c--;)if(0===a.indexOf(b[c]))return!0;return!1},hide:function(){this.visible=!1;this.selectedIndex=-1;d(this.suggestionsContainer).hide();this.signalHint(null)},suggest:function(){if(0===this.suggestions.length)this.hide();else{var a=this.options,b=a.formatResult,c=this.getQuery(this.currentValue),e=this.classes.suggestion,f=this.classes.selected,g=d(this.suggestionsContainer),k=a.beforeRender,m="",h;if(a.triggerSelectOnValidInput&& -(h=this.findSuggestionIndex(c),-1!==h)){this.select(h);return}d.each(this.suggestions,function(a,d){m+='
'+b(d,c)+"
"});"auto"===a.width&&(h=this.el.outerWidth()-2,g.width(0this.selectedIndex?(a=e.get(this.selectedIndex),d(a).addClass(b),a):null},selectHint:function(){var a=d.inArray(this.hint,this.suggestions); -this.select(a)},select:function(a){this.hide();this.onSelect(a)},moveUp:function(){-1!==this.selectedIndex&&(0===this.selectedIndex?(d(this.suggestionsContainer).children().first().removeClass(this.classes.selected),this.selectedIndex=-1,this.el.val(this.currentValue),this.findBestHint()):this.adjustScroll(this.selectedIndex-1))},moveDown:function(){this.selectedIndex!==this.suggestions.length-1&&this.adjustScroll(this.selectedIndex+1)},adjustScroll:function(a){var b=this.activate(a),c,e;b&&(b=b.offsetTop, -c=d(this.suggestionsContainer).scrollTop(),e=c+this.options.maxHeight-25,be&&d(this.suggestionsContainer).scrollTop(b-this.options.maxHeight+25),this.el.val(this.getValue(this.suggestions[a].value)),this.signalHint(null))},onSelect:function(a){var b=this.options.onSelect;a=this.suggestions[a];this.currentValue=this.getValue(a.value);this.el.val(this.currentValue);this.signalHint(null);this.suggestions=[];this.selection=a;d.isFunction(b)&&b.call(this.element, -a)},getValue:function(a){var b=this.options.delimiter,c;if(!b)return a;c=this.currentValue;b=c.split(b);return 1===b.length?a:c.substr(0,c.length-b[b.length-1].length)+a},dispose:function(){this.el.off(".autocomplete").removeData("autocomplete");this.disableKillerFn();d(window).off("resize.autocomplete",this.fixPositionCapture);d(this.suggestionsContainer).remove()}};d.fn.autocomplete=function(a,b){return 0===arguments.length?this.first().data("autocomplete"):this.each(function(){var c=d(this),e= -c.data("autocomplete");if("string"===typeof a){if(e&&"function"===typeof e[a])e[a](b)}else e&&e.dispose&&e.dispose(),e=new g(this,a),c.data("autocomplete",e)})}}); \ No newline at end of file +!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports&&"function"==typeof require?require("jquery"):jQuery)}(function(a){"use strict";function b(c,d){var e=function(){},f=this,g={ajaxSettings:{},autoSelectFirst:!1,appendTo:document.body,serviceUrl:null,lookup:null,onSelect:null,width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:b.formatResult,delimiter:null,zIndex:9999,type:"GET",noCache:!1,onSearchStart:e,onSearchComplete:e,onSearchError:e,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",currentRequest:null,triggerSelectOnValidInput:!0,preventBadQueries:!0,lookupFilter:function(a,b,c){return-1!==a.value.toLowerCase().indexOf(c)},paramName:"query",transformResult:function(b){return"string"==typeof b?a.parseJSON(b):b},showNoSuggestionNotice:!1,noSuggestionNotice:"No results",orientation:"bottom",forceFixPosition:!1};f.element=c,f.el=a(c),f.suggestions=[],f.badQueries=[],f.selectedIndex=-1,f.currentValue=f.element.value,f.intervalId=0,f.cachedResponse={},f.onChangeInterval=null,f.onChange=null,f.isLocal=!1,f.suggestionsContainer=null,f.noSuggestionsContainer=null,f.options=a.extend({},g,d),f.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"},f.hint=null,f.hintValue="",f.selection=null,f.initialize(),f.setOptions(d)}var c=function(){return{escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},createNode:function(a){var b=document.createElement("div");return b.className=a,b.style.position="absolute",b.style.display="none",b}}}(),d={ESC:27,TAB:9,RETURN:13,LEFT:37,UP:38,RIGHT:39,DOWN:40};b.utils=c,a.Autocomplete=b,b.formatResult=function(a,b){var d="("+c.escapeRegExChars(b)+")";return a.value.replace(new RegExp(d,"gi"),"$1")},b.prototype={killerFn:null,initialize:function(){var c,d=this,e="."+d.classes.suggestion,f=d.classes.selected,g=d.options;d.element.setAttribute("autocomplete","off"),d.killerFn=function(b){0===a(b.target).closest("."+d.options.containerClass).length&&(d.killSuggestions(),d.disableKillerFn())},d.noSuggestionsContainer=a('
').html(this.options.noSuggestionNotice).get(0),d.suggestionsContainer=b.utils.createNode(g.containerClass),c=a(d.suggestionsContainer),c.appendTo(g.appendTo),"auto"!==g.width&&c.width(g.width),c.on("mouseover.autocomplete",e,function(){d.activate(a(this).data("index"))}),c.on("mouseout.autocomplete",function(){d.selectedIndex=-1,c.children("."+f).removeClass(f)}),c.on("click.autocomplete",e,function(){d.select(a(this).data("index"))}),d.fixPositionCapture=function(){d.visible&&d.fixPosition()},a(window).on("resize.autocomplete",d.fixPositionCapture),d.el.on("keydown.autocomplete",function(a){d.onKeyPress(a)}),d.el.on("keyup.autocomplete",function(a){d.onKeyUp(a)}),d.el.on("blur.autocomplete",function(){d.onBlur()}),d.el.on("focus.autocomplete",function(){d.onFocus()}),d.el.on("change.autocomplete",function(a){d.onKeyUp(a)})},onFocus:function(){var a=this;a.fixPosition(),a.options.minChars<=a.el.val().length&&a.onValueChange()},onBlur:function(){this.enableKillerFn()},setOptions:function(b){var c=this,d=c.options;a.extend(d,b),c.isLocal=a.isArray(d.lookup),c.isLocal&&(d.lookup=c.verifySuggestionsFormat(d.lookup)),d.orientation=c.validateOrientation(d.orientation,"bottom"),a(c.suggestionsContainer).css({"max-height":d.maxHeight+"px",width:d.width+"px","z-index":d.zIndex})},clearCache:function(){this.cachedResponse={},this.badQueries=[]},clear:function(){this.clearCache(),this.currentValue="",this.suggestions=[]},disable:function(){var a=this;a.disabled=!0,a.currentRequest&&a.currentRequest.abort()},enable:function(){this.disabled=!1},fixPosition:function(){var b=this,c=a(b.suggestionsContainer),d=c.parent().get(0);if(d===document.body||b.options.forceFixPosition){var e=b.options.orientation,f=c.outerHeight(),g=b.el.outerHeight(),h=b.el.offset(),i={top:h.top,left:h.left};if("auto"==e){var j=a(window).height(),k=a(window).scrollTop(),l=-k+h.top-f,m=k+j-(h.top+g+f);e=Math.max(l,m)===l?"top":"bottom"}if(i.top+="top"===e?-f:g,d!==document.body){var n,o=c.css("opacity");b.visible||c.css("opacity",0).show(),n=c.offsetParent().offset(),i.top-=n.top,i.left-=n.left,b.visible||c.css("opacity",o).hide()}"auto"===b.options.width&&(i.width=b.el.outerWidth()-2+"px"),c.css(i)}},enableKillerFn:function(){var b=this;a(document).on("click.autocomplete",b.killerFn)},disableKillerFn:function(){var b=this;a(document).off("click.autocomplete",b.killerFn)},killSuggestions:function(){var a=this;a.stopKillSuggestions(),a.intervalId=window.setInterval(function(){a.hide(),a.stopKillSuggestions()},50)},stopKillSuggestions:function(){window.clearInterval(this.intervalId)},isCursorAtEnd:function(){var a,b=this,c=b.el.val().length,d=b.element.selectionStart;return"number"==typeof d?d===c:document.selection?(a=document.selection.createRange(),a.moveStart("character",-c),c===a.text.length):!0},onKeyPress:function(a){var b=this;if(!b.disabled&&!b.visible&&a.which===d.DOWN&&b.currentValue)return void b.suggest();if(!b.disabled&&b.visible){switch(a.which){case d.ESC:b.el.val(b.currentValue),b.hide();break;case d.RIGHT:if(b.hint&&b.options.onHint&&b.isCursorAtEnd()){b.selectHint();break}return;case d.TAB:if(b.hint&&b.options.onHint)return void b.selectHint();case d.RETURN:if(-1===b.selectedIndex)return void b.hide();if(b.select(b.selectedIndex),a.which===d.TAB&&b.options.tabDisabled===!1)return;break;case d.UP:b.moveUp();break;case d.DOWN:b.moveDown();break;default:return}a.stopImmediatePropagation(),a.preventDefault()}},onKeyUp:function(a){var b=this;if(!b.disabled){switch(a.which){case d.UP:case d.DOWN:return}clearInterval(b.onChangeInterval),b.currentValue!==b.el.val()&&(b.findBestHint(),b.options.deferRequestBy>0?b.onChangeInterval=setInterval(function(){b.onValueChange()},b.options.deferRequestBy):b.onValueChange())}},onValueChange:function(){var b,c=this,d=c.options,e=c.el.val(),f=c.getQuery(e);return c.selection&&c.currentValue!==f&&(c.selection=null,(d.onInvalidateSelection||a.noop).call(c.element)),clearInterval(c.onChangeInterval),c.currentValue=e,c.selectedIndex=-1,d.triggerSelectOnValidInput&&(b=c.findSuggestionIndex(f),-1!==b)?void c.select(b):void(f.lengthh&&(c.suggestions=c.suggestions.slice(0,h)),c},getSuggestions:function(b){var c,d,e,f,g=this,h=g.options,i=h.serviceUrl;if(h.params[h.paramName]=b,d=h.ignoreParams?null:h.params,g.isLocal?c=g.getSuggestionsLocal(b):(a.isFunction(i)&&(i=i.call(g.element,b)),e=i+"?"+a.param(d||{}),c=g.cachedResponse[e]),c&&a.isArray(c.suggestions))g.suggestions=c.suggestions,g.suggest();else if(!g.isBadQuery(b)){if(h.onSearchStart.call(g.element,h.params)===!1)return;g.currentRequest&&g.currentRequest.abort(),f={url:i,data:d,type:h.type,dataType:h.dataType},a.extend(f,h.ajaxSettings),g.currentRequest=a.ajax(f).done(function(a){var c;g.currentRequest=null,c=h.transformResult(a),g.processResponse(c,b,e),h.onSearchComplete.call(g.element,b,c.suggestions)}).fail(function(a,c,d){h.onSearchError.call(g.element,b,a,c,d)})}},isBadQuery:function(a){if(!this.options.preventBadQueries)return!1;for(var b=this.badQueries,c=b.length;c--;)if(0===a.indexOf(b[c]))return!0;return!1},hide:function(){var b=this;b.visible=!1,b.selectedIndex=-1,a(b.suggestionsContainer).hide(),b.signalHint(null)},suggest:function(){if(0===this.suggestions.length)return void(this.options.showNoSuggestionNotice?this.noSuggestions():this.hide());var b,c=this,d=c.options,e=d.formatResult,f=c.getQuery(c.currentValue),g=c.classes.suggestion,h=c.classes.selected,i=a(c.suggestionsContainer),j=a(c.noSuggestionsContainer),k=d.beforeRender,l="";return d.triggerSelectOnValidInput&&(b=c.findSuggestionIndex(f),-1!==b)?void c.select(b):(a.each(c.suggestions,function(a,b){l+='
'+e(b,f)+"
"}),this.adjustContainerWidth(),j.detach(),i.html(l),d.autoSelectFirst&&(c.selectedIndex=0,i.children().first().addClass(h)),a.isFunction(k)&&k.call(c.element,i),c.fixPosition(),i.show(),c.visible=!0,void c.findBestHint())},noSuggestions:function(){var b=this,c=a(b.suggestionsContainer),d=a(b.noSuggestionsContainer);this.adjustContainerWidth(),d.detach(),c.empty(),c.append(d),b.fixPosition(),c.show(),b.visible=!0},adjustContainerWidth:function(){var b,c=this,d=c.options,e=a(c.suggestionsContainer);"auto"===d.width&&(b=c.el.outerWidth()-2,e.width(b>0?b:300))},findBestHint:function(){var b=this,c=b.el.val().toLowerCase(),d=null;c&&(a.each(b.suggestions,function(a,b){var e=0===b.value.toLowerCase().indexOf(c);return e&&(d=b),!e}),b.signalHint(d))},signalHint:function(b){var c="",d=this;b&&(c=d.currentValue+b.value.substr(d.currentValue.length)),d.hintValue!==c&&(d.hintValue=c,d.hint=b,(this.options.onHint||a.noop)(c))},verifySuggestionsFormat:function(b){return b.length&&"string"==typeof b[0]?a.map(b,function(a){return{value:a,data:null}}):b},validateOrientation:function(b,c){return b=a.trim(b||"").toLowerCase(),-1===a.inArray(b,["auto","bottom","top"])&&(b=c),b},processResponse:function(a,b,c){var d=this,e=d.options;a.suggestions=d.verifySuggestionsFormat(a.suggestions),e.noCache||(d.cachedResponse[c]=a,e.preventBadQueries&&0===a.suggestions.length&&d.badQueries.push(b)),b===d.getQuery(d.currentValue)&&(d.suggestions=a.suggestions,d.suggest())},activate:function(b){var c,d=this,e=d.classes.selected,f=a(d.suggestionsContainer),g=f.find("."+d.classes.suggestion);return f.find("."+e).removeClass(e),d.selectedIndex=b,-1!==d.selectedIndex&&g.length>d.selectedIndex?(c=g.get(d.selectedIndex),a(c).addClass(e),c):null},selectHint:function(){var b=this,c=a.inArray(b.hint,b.suggestions);b.select(c)},select:function(a){var b=this;b.hide(),b.onSelect(a)},moveUp:function(){var b=this;if(-1!==b.selectedIndex)return 0===b.selectedIndex?(a(b.suggestionsContainer).children().first().removeClass(b.classes.selected),b.selectedIndex=-1,b.el.val(b.currentValue),void b.findBestHint()):void b.adjustScroll(b.selectedIndex-1)},moveDown:function(){var a=this;a.selectedIndex!==a.suggestions.length-1&&a.adjustScroll(a.selectedIndex+1)},adjustScroll:function(b){var c,d,e,f=this,g=f.activate(b),h=25;g&&(c=g.offsetTop,d=a(f.suggestionsContainer).scrollTop(),e=d+f.options.maxHeight-h,d>c?a(f.suggestionsContainer).scrollTop(c):c>e&&a(f.suggestionsContainer).scrollTop(c-f.options.maxHeight+h),f.el.val(f.getValue(f.suggestions[b].value)),f.signalHint(null))},onSelect:function(b){var c=this,d=c.options.onSelect,e=c.suggestions[b];c.currentValue=c.getValue(e.value),c.currentValue!==c.el.val()&&c.el.val(c.currentValue),c.signalHint(null),c.suggestions=[],c.selection=e,a.isFunction(d)&&d.call(c.element,e)},getValue:function(a){var b,c,d=this,e=d.options.delimiter;return e?(b=d.currentValue,c=b.split(e),1===c.length?a:b.substr(0,b.length-c[c.length-1].length)+a):a},dispose:function(){var b=this;b.el.off(".autocomplete").removeData("autocomplete"),b.disableKillerFn(),a(window).off("resize.autocomplete",b.fixPositionCapture),a(b.suggestionsContainer).remove()}},a.fn.autocomplete=a.fn.devbridgeAutocomplete=function(c,d){var e="autocomplete";return 0===arguments.length?this.first().data(e):this.each(function(){var f=a(this),g=f.data(e);"string"==typeof c?g&&"function"==typeof g[c]&&g[c](d):(g&&g.dispose&&g.dispose(),g=new b(this,c),f.data(e,g))})}}); \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js new file mode 100644 index 0000000..ab394be --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,69 @@ +module.exports = function(grunt) { + + var pkg = grunt.file.readJSON('package.json'); + + var banner = [ + '/**', + '* Ajax Autocomplete for jQuery, version ' + pkg.version, + '* (c) 2014 Tomas Kirda', + '*', + '* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.', + '* For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete', + '*/'].join('\n') + '\n'; + + // Project configuration. + grunt.initConfig({ + pkg: pkg, + uglify: { + options: { + banner: banner + }, + build: { + src: 'src/jquery.autocomplete.js', + dest: 'dist/jquery.autocomplete.min.js' + } + } + }); + + // Load the plugin that provides the "uglify" task. + grunt.loadNpmTasks('grunt-contrib-uglify'); + + // Default task(s). + grunt.registerTask('default', ['uglify']); + + grunt.task.registerTask('build', 'Create release', function() { + var version = pkg.version + src = grunt.file.read('src/jquery.autocomplete.js').replace('%version%', version), + filePath = 'dist/jquery.autocomplete.js'; + + // Update not minimized release version: + console.log('Updating: ' + filePath); + grunt.file.write(filePath, src); + + // Update plugin version: + filePath = 'devbridge-autocomplete.jquery.json'; + src = grunt.file.readJSON(filePath); + + if (src.version !== version){ + src.version = version; + console.log('Updating: ' + filePath); + grunt.file.write(filePath, JSON.stringify(src, null, 4)); + } else { + console.log('No updates for: ' + filePath); + } + + // Update bower version: + filePath = 'bower.json'; + src = grunt.file.readJSON(filePath); + + if (src.version !== version){ + src.version = version; + console.log('Updating: ' + filePath); + grunt.file.write(filePath, JSON.stringify(src, null, 4)); + } else { + console.log('No updates for: ' + filePath); + } + + + }); +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8ebbb5b --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "devbridge-autocomplete", + "version": "1.2.12", + "description": "Autocomplete provides suggestions while you type into the text field.", + "homepage": "https://github.com/devbridge/jQuery-Autocomplete", + "author": "Tomas Kirda (https://twitter.com/tkirda)", + "main": "dist/jquery.autocomplete.js", + "license": "MIT", + "dependencies": { + "jquery": ">=1.7" + }, + "devDependencies": { + "grunt": "^0.4.5", + "grunt-contrib-uglify": "^0.5.1" + } +} diff --git a/readme.md b/readme.md index 38a416a..662eed4 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,7 @@ The standard jquery.autocomplete.js file is around 2.7KB when minified via Closu * Sets up autocomplete for input field(s). * `options`: An object literal which defines the settings to use for the autocomplete plugin. * `serviceUrl`: Server side URL or callback function that returns serviceUrl string. Optional if local lookup data is provided. + * `ajaxSettings`: Any additional [Ajax Settings](http://api.jquery.com/jquery.ajax/#jQuery-ajax-settings) that configure the jQuery Ajax request. * `lookup`: Lookup array for the suggestions. It may be array of strings or `suggestion` object literals. * `suggestion`: An object literal with the following format: `{ value: 'string', data: any }`. * `lookupFilter`: `function (suggestion, query, queryLowerCase) {}` filter function for local lookups. By default it does partial string match (case insensitive). @@ -158,6 +159,11 @@ you can supply the "paramName" and "transformResult" options: } }) +##Known Issues + +If you use it with jQuery UI library it also has plugin named `autocomplete`. In this case you can use plugin alias `devbridgeAutocomplete`: + + $('.autocomplete').devbridgeAutocomplete({ ... }); ##License diff --git a/scripts/demo.js b/scripts/demo.js index ab887b8..ea03dff 100644 --- a/scripts/demo.js +++ b/scripts/demo.js @@ -47,7 +47,7 @@ $(function () { }); // Initialize autocomplete with local lookup: - $('#autocomplete').autocomplete({ + $('#autocomplete').devbridgeAutocomplete({ lookup: countriesArray, minChars: 0, onSelect: function (suggestion) { diff --git a/src/jquery.autocomplete.js b/src/jquery.autocomplete.js index f06d0d0..91fa30d 100644 --- a/src/jquery.autocomplete.js +++ b/src/jquery.autocomplete.js @@ -1,14 +1,13 @@ /** -* Ajax Autocomplete for jQuery, version 1.2.9 -* (c) 2013 Tomas Kirda +* Ajax Autocomplete for jQuery, version %version% +* (c) 2014 Tomas Kirda * * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete -* */ /*jslint browser: true, white: true, plusplus: true */ -/*global define, window, document, jQuery */ +/*global define, window, document, jQuery, exports */ // Expose plugin as an AMD module if AMD loader is present: (function (factory) { @@ -16,6 +15,9 @@ if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); + } else if (typeof exports === 'object' && typeof require === 'function') { + // Browserify + factory(require('jquery')); } else { // Browser globals factory(jQuery); @@ -53,6 +55,7 @@ var noop = function () { }, that = this, defaults = { + ajaxSettings: {}, autoSelectFirst: false, appendTo: document.body, serviceUrl: null, @@ -137,8 +140,7 @@ suggestionSelector = '.' + that.classes.suggestion, selected = that.classes.selected, options = that.options, - container, - noSuggestionsContainer; + container; // Remove autocomplete attribute to prevent native suggestions: that.element.setAttribute('autocomplete', 'off'); @@ -275,34 +277,37 @@ if (orientation == 'auto') { var viewPortHeight = $(window).height(), scrollTop = $(window).scrollTop(), - top_overflow = -scrollTop + offset.top - containerHeight, - bottom_overflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); + topOverflow = -scrollTop + offset.top - containerHeight, + bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); - if (Math.max(top_overflow, bottom_overflow) === top_overflow) - orientation = 'top'; - else - orientation = 'bottom'; + orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) + ? 'top' + : 'bottom'; } - if (orientation === 'top') + if (orientation === 'top') { styles.top += -containerHeight; - else - styles.top += height; + } else { + styles.top += height; + } // If container is not positioned to body, // correct its position using offset parent offset if(containerParent !== document.body) { var opacity = $container.css('opacity'), parentOffsetDiff; - if (!that.visible) - $container.css('opacity', 0).show(); + + if (!that.visible){ + $container.css('opacity', 0).show(); + } parentOffsetDiff = $container.offsetParent().offset(); styles.top -= parentOffsetDiff.top; styles.left -= parentOffsetDiff.left; - if (!that.visible) + if (!that.visible){ $container.css('opacity', opacity).hide(); + } } // -2px to account for suggestions border. @@ -443,7 +448,7 @@ query = that.getQuery(value), index; - if (that.selection) { + if (that.selection && that.currentValue !== query) { that.selection = null; (options.onInvalidateSelection || $.noop).call(that.element); } @@ -521,7 +526,8 @@ options = that.options, serviceUrl = options.serviceUrl, params, - cacheKey; + cacheKey, + ajaxSettings; options.params[options.paramName] = q; params = options.ignoreParams ? null : options.params; @@ -546,12 +552,17 @@ if (that.currentRequest) { that.currentRequest.abort(); } - that.currentRequest = $.ajax({ + + ajaxSettings = { url: serviceUrl, data: params, type: options.type, dataType: options.dataType - }).done(function (data) { + }; + + $.extend(ajaxSettings, options.ajaxSettings); + + that.currentRequest = $.ajax(ajaxSettings).done(function (data) { var result; that.currentRequest = null; result = options.transformResult(data); @@ -604,8 +615,7 @@ noSuggestionsContainer = $(that.noSuggestionsContainer), beforeRender = options.beforeRender, html = '', - index, - width; + index; if (options.triggerSelectOnValidInput) { index = that.findSuggestionIndex(value); @@ -723,10 +733,13 @@ }, validateOrientation: function(orientation, fallback) { - orientation = orientation.trim().toLowerCase(); - if(['auto', 'bottom', 'top'].indexOf(orientation) == '-1') + orientation = $.trim(orientation || '').toLowerCase(); + + if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){ orientation = fallback; - return orientation + } + + return orientation; }, processResponse: function (result, originalQuery, cacheKey) { @@ -757,9 +770,9 @@ activeItem, selected = that.classes.selected, container = $(that.suggestionsContainer), - children = container.children(); + children = container.find('.' + that.classes.suggestion); - container.children('.' + selected).removeClass(selected); + container.find('.' + selected).removeClass(selected); that.selectedIndex = index; @@ -889,7 +902,7 @@ }; // Create chainable jQuery plugin: - $.fn.autocomplete = function (options, args) { + $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) { var dataKey = 'autocomplete'; // If function invoked without argument return // instance of the first matched element: