/** * Ajax Autocomplete for jQuery, version 1.2.27 * (c) 2015 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 * * Modified by Damian Góra: http://damiangora.com */ /*jslint browser: true, white: true, single: true, this: true, multivar: true */ /*global define, window, document, jQuery, exports, require */ // Expose plugin as an AMD module if AMD loader is present: ( function ( factory ) { "use strict"; 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 ); } }( 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.display = 'none'; return div; } }; }() ), keys = { ESC: 27, TAB: 9, RETURN: 13, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 }; function Autocomplete( el, options ) { var noop = $.noop, that = this, defaults = { ajaxSettings: { }, autoSelectFirst: false, appendTo: document.body, serviceUrl: null, lookup: null, onSelect: null, onMouseOver: null, onMouseLeave: null, width: '100%', minChars: 1, maxHeight: 1000, deferRequestBy: 0, params: { }, delimiter: null, zIndex: 9999, type: 'GET', noCache: false, is_rtl: false, onSearchStart: noop, onSearchComplete: noop, onSearchError: noop, preserveInput: false, wrapperaClass: 'search-wrapp', containerClass: 'search-suggestions-wrapp', preloaderClass: 'search-preloader', closeTrigger: 'search-close', 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' }; // Shared variables: that.element = el; that.el = $( el ); that.suggestions = [ ]; that.badQueries = [ ]; that.selectedIndex = -1; that.currentValue = that.element.value; that.intervalId = 0; that.cachedResponse = { }; that.detailsRequestsSent = [ ]; that.onChangeInterval = null; that.onChange = null; that.isLocal = false; that.suggestionsContainer = null; that.detailsContainer = null; that.noSuggestionsContainer = null; that.options = $.extend( { }, defaults, options ); that.classes = { selected: 'search-suggestion-selected', suggestion: 'search-suggestion' }; that.hint = null; that.hintValue = ''; that.selection = null; // Initialize and set options: that.initialize(); that.setOptions( options ); } Autocomplete.utils = utils; $.Autocomplete = Autocomplete; Autocomplete.prototype = { killerFn: null, initialize: function () { var that = this, suggestionSelector = '.' + that.classes.suggestion, selected = that.classes.selected, options = that.options, container, closeTrigger = '.' + options.closeTrigger; that.killerFn = function ( e ) { if ( $( e.target ).closest( '.' + that.options.containerClass ).length === 0 ) { that.killSuggestions(); that.disableKillerFn(); } }; // html() deals with many types: htmlString or Element or Array or jQuery // that.noSuggestionsContainer = $( '
' ) // .html( this.options.noSuggestionNotice ).get( 0 ); // Create sugestions container that.suggestionsContainer = Autocomplete.utils.createNode( options.containerClass ); container = $( that.suggestionsContainer ); container.appendTo( that.el.closest( '.' + options.wrapperaClass ) ); // Only set width if it was provided: if ( options.width !== 'auto' ) { container.width( options.width ); } // Listen for mouse over event on suggestions list: container.on( 'mouseover.autocomplete', suggestionSelector, function () { that.onMouseOver( $( this ).data( 'index' ) ); that.activate( $( this ).data( 'index' ) ); } ); // Deselect active element when mouse leaves suggestions container: container.on( 'mouseout.autocomplete', function () { //that.selectedIndex = -1; // container.children( '.' + selected ).removeClass( selected ); } ); // Listen for click event on suggestions list: $( document ).on( 'click.autocomplete', closeTrigger, function ( e ) { that.killerFn( e ); that.clear( e ); $( this ).removeClass( options.closeTrigger ); that.el.val( '' ).focus(); } ); // Listen for click close button: container.on( 'click.autocomplete', suggestionSelector, function () { that.select( $( this ).data( 'index' ) ); return false; } ); 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 ); } ); }, onFocus: function () { var that = this; if ( that.el.val().length >= that.options.minChars ) { 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.options.onSearchComplete = function () { that.preloader( 'hide', $( '.' + options.preloaderClass ), options.closeTrigger ); }; }, 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; }, enableKillerFn: function () { var that = this; $( document ).on( 'click.autocomplete', that.killerFn ); }, disableKillerFn: function () { var that = this; $( document ).off( 'click.autocomplete', that.killerFn ); }, killSuggestions: function () { var that = this, containerParent = $( that.suggestionsContainer ).parent(); that.stopKillSuggestions(); that.intervalId = window.setInterval( function () { if ( that.visible ) { // No need to restore value when // preserveInput === true, // because we did not change it if ( !that.options.preserveInput ) { that.el.val( that.currentValue ); } that.hide(); } 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 suggestions are hidden and user presses arrow down, display suggestions: 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 ( that.selectedIndex === -1 ) { that.hide(); return; } that.select( that.selectedIndex ); break; case keys.UP: that.moveUp(); break; case keys.DOWN: that.moveDown(); break; default: return; } // Cancel event if function did not 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 ) { // Defer lookup in case when value changes very quickly: 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; // Check existing suggestion for the match before proceeding: if ( options.triggerSelectOnValidInput && that.isExactMatch( query ) ) { that.select( 0 ); return; } if ( query.length < options.minChars ) { $( '.' + that.options.closeTrigger ).removeClass( that.options.closeTrigger ); 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 $.trim( 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, container = $( that.suggestionsContainer ), options = that.options, serviceUrl = options.serviceUrl, params, cacheKey, ajaxSettings; options.params[options.paramName] = q; params = options.ignoreParams ? null : options.params; that.preloader( 'show', $( '.' + options.preloaderClass ), 'search-inner-preloader', container ); if ( options.onSearchStart.call( that.element, options.params ) === false ) { return; } if ( $.isFunction( options.lookup ) ) { 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 ( $.isFunction( serviceUrl ) ) { 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 ); if ( typeof result.suggestions !== 'undefined' ) { 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 () { var that = this, container = $( that.suggestionsContainer ), containerParent = $( that.suggestionsContainer ).parent(); if ( $.isFunction( that.options.onHide ) && that.visible ) { that.options.onHide.call( that.element, container ); } that.visible = false; that.selectedIndex = -1; clearInterval( that.onChangeInterval ); $( that.suggestionsContainer ).hide(); $( that.detailsContainer ).hide(); containerParent.removeClass( 'search-open' ); that.signalHint( null ); }, suggest: function () { if ( this.suggestions == '') { 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, category, formatGroup = function ( suggestion, index ) { var currentCategory = suggestion.data[groupBy]; if ( category === currentCategory ) { return ''; } category = currentCategory; return '