77c1db60a8ffa546dc8e718c7de53b98e6c81567
[emels.git] / emels / static / django_extensions / js / jquery.autocomplete.js
1 /**
2  * @fileOverview jquery-autocomplete, the jQuery Autocompleter
3  * @author <a href="mailto:dylan@dyve.net">Dylan Verheul</a>
4  * @version 2.4.4
5  * @requires jQuery 1.6+
6  * @license MIT | GPL | Apache 2.0, see LICENSE.txt
7  * @see https://github.com/dyve/jquery-autocomplete
8  */
9 (function($) {
10     "use strict";
11
12     /**
13      * jQuery autocomplete plugin
14      * @param {object|string} options
15      * @returns (object} jQuery object
16      */
17     $.fn.autocomplete = function(options) {
18         var url;
19         if (arguments.length > 1) {
20             url = options;
21             options = arguments[1];
22             options.url = url;
23         } else if (typeof options === 'string') {
24             url = options;
25             options = { url: url };
26         }
27         var opts = $.extend({}, $.fn.autocomplete.defaults, options);
28         return this.each(function() {
29             var $this = $(this);
30             $this.data('autocompleter', new $.Autocompleter(
31                 $this,
32                 $.meta ? $.extend({}, opts, $this.data()) : opts
33             ));
34         });
35     };
36
37     /**
38      * Store default options
39      * @type {object}
40      */
41     $.fn.autocomplete.defaults = {
42         inputClass: 'acInput',
43         loadingClass: 'acLoading',
44         resultsClass: 'acResults',
45         selectClass: 'acSelect',
46         queryParamName: 'q',
47         extraParams: {},
48         remoteDataType: false,
49         lineSeparator: '\n',
50         cellSeparator: '|',
51         minChars: 2,
52         maxItemsToShow: 10,
53         delay: 400,
54         useCache: true,
55         maxCacheLength: 10,
56         matchSubset: true,
57         matchCase: false,
58         matchInside: true,
59         mustMatch: false,
60         selectFirst: false,
61         selectOnly: false,
62         showResult: null,
63         preventDefaultReturn: 1,
64         preventDefaultTab: 0,
65         autoFill: false,
66         filterResults: true,
67         filter: true,
68         sortResults: true,
69         sortFunction: null,
70         onItemSelect: null,
71         onNoMatch: null,
72         onFinish: null,
73         matchStringConverter: null,
74         beforeUseConverter: null,
75         autoWidth: 'min-width',
76         useDelimiter: false,
77         delimiterChar: ',',
78         delimiterKeyCode: 188,
79         processData: null,
80         onError: null,
81         enabled: true
82     };
83
84     /**
85      * Sanitize result
86      * @param {Object} result
87      * @returns {Object} object with members value (String) and data (Object)
88      * @private
89      */
90     var sanitizeResult = function(result) {
91         var value, data;
92         var type = typeof result;
93         if (type === 'string') {
94             value = result;
95             data = {};
96         } else if ($.isArray(result)) {
97             value = result[0];
98             data = result.slice(1);
99         } else if (type === 'object') {
100             value = result.value;
101             data = result.data;
102         }
103         value = String(value);
104         if (typeof data !== 'object') {
105             data = {};
106         }
107         return {
108             value: value,
109             data: data
110         };
111     };
112
113     /**
114      * Sanitize integer
115      * @param {mixed} value
116      * @param {Object} options
117      * @returns {Number} integer
118      * @private
119      */
120     var sanitizeInteger = function(value, stdValue, options) {
121         var num = parseInt(value, 10);
122         options = options || {};
123         if (isNaN(num) || (options.min && num < options.min)) {
124             num = stdValue;
125         }
126         return num;
127     };
128
129     /**
130      * Create partial url for a name/value pair
131      */
132     var makeUrlParam = function(name, value) {
133         return [name, encodeURIComponent(value)].join('=');
134     };
135
136     /**
137      * Build an url
138      * @param {string} url Base url
139      * @param {object} [params] Dictionary of parameters
140      */
141     var makeUrl = function(url, params) {
142         var urlAppend = [];
143         $.each(params, function(index, value) {
144             urlAppend.push(makeUrlParam(index, value));
145         });
146         if (urlAppend.length) {
147             url += url.indexOf('?') === -1 ? '?' : '&';
148             url += urlAppend.join('&');
149         }
150         return url;
151     };
152
153     /**
154      * Default sort filter
155      * @param {object} a
156      * @param {object} b
157      * @param {boolean} matchCase
158      * @returns {number}
159      */
160     var sortValueAlpha = function(a, b, matchCase) {
161         a = String(a.value);
162         b = String(b.value);
163         if (!matchCase) {
164             a = a.toLowerCase();
165             b = b.toLowerCase();
166         }
167         if (a > b) {
168             return 1;
169         }
170         if (a < b) {
171             return -1;
172         }
173         return 0;
174     };
175
176     /**
177      * Parse data received in text format
178      * @param {string} text Plain text input
179      * @param {string} lineSeparator String that separates lines
180      * @param {string} cellSeparator String that separates cells
181      * @returns {array} Array of autocomplete data objects
182      */
183     var plainTextParser = function(text, lineSeparator, cellSeparator) {
184         var results = [];
185         var i, j, data, line, value, lines;
186         // Be nice, fix linebreaks before splitting on lineSeparator
187         lines = String(text).replace('\r\n', '\n').split(lineSeparator);
188         for (i = 0; i < lines.length; i++) {
189             line = lines[i].split(cellSeparator);
190             data = [];
191             for (j = 0; j < line.length; j++) {
192                 data.push(decodeURIComponent(line[j]));
193             }
194             value = data.shift();
195             results.push({ value: value, data: data });
196         }
197         return results;
198     };
199
200     /**
201      * Autocompleter class
202      * @param {object} $elem jQuery object with one input tag
203      * @param {object} options Settings
204      * @constructor
205      */
206     $.Autocompleter = function($elem, options) {
207
208         /**
209          * Assert parameters
210          */
211         if (!$elem || !($elem instanceof $) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') {
212             throw new Error('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected.');
213         }
214
215         /**
216          * @constant Link to this instance
217          * @type object
218          * @private
219          */
220         var self = this;
221
222         /**
223          * @property {object} Options for this instance
224          * @public
225          */
226         this.options = options;
227
228         /**
229          * @property object Cached data for this instance
230          * @private
231          */
232         this.cacheData_ = {};
233
234         /**
235          * @property {number} Number of cached data items
236          * @private
237          */
238         this.cacheLength_ = 0;
239
240         /**
241          * @property {string} Class name to mark selected item
242          * @private
243          */
244         this.selectClass_ = 'jquery-autocomplete-selected-item';
245
246         /**
247          * @property {number} Handler to activation timeout
248          * @private
249          */
250         this.keyTimeout_ = null;
251
252         /**
253          * @property {number} Handler to finish timeout
254          * @private
255          */
256         this.finishTimeout_ = null;
257
258         /**
259          * @property {number} Last key pressed in the input field (store for behavior)
260          * @private
261          */
262         this.lastKeyPressed_ = null;
263
264         /**
265          * @property {string} Last value processed by the autocompleter
266          * @private
267          */
268         this.lastProcessedValue_ = null;
269
270         /**
271          * @property {string} Last value selected by the user
272          * @private
273          */
274         this.lastSelectedValue_ = null;
275
276         /**
277          * @property {boolean} Is this autocompleter active (showing results)?
278          * @see showResults
279          * @private
280          */
281         this.active_ = false;
282
283         /**
284          * @property {boolean} Is this autocompleter allowed to finish on blur?
285          * @private
286          */
287         this.finishOnBlur_ = true;
288
289         /**
290          * Sanitize options
291          */
292         this.options.minChars = sanitizeInteger(this.options.minChars, $.fn.autocomplete.defaults.minChars, { min: 0 });
293         this.options.maxItemsToShow = sanitizeInteger(this.options.maxItemsToShow, $.fn.autocomplete.defaults.maxItemsToShow, { min: 0 });
294         this.options.maxCacheLength = sanitizeInteger(this.options.maxCacheLength, $.fn.autocomplete.defaults.maxCacheLength, { min: 1 });
295         this.options.delay = sanitizeInteger(this.options.delay, $.fn.autocomplete.defaults.delay, { min: 0 });
296         if (this.options.preventDefaultReturn != 2) {
297             this.options.preventDefaultReturn = this.options.preventDefaultReturn ? 1 : 0;
298         }
299         if (this.options.preventDefaultTab != 2) {
300             this.options.preventDefaultTab = this.options.preventDefaultTab ? 1 : 0;
301         }
302
303         /**
304          * Init DOM elements repository
305          */
306         this.dom = {};
307
308         /**
309          * Store the input element we're attached to in the repository
310          */
311         this.dom.$elem = $elem;
312
313         /**
314          * Switch off the native autocomplete and add the input class
315          */
316         this.dom.$elem.attr('autocomplete', 'off').addClass(this.options.inputClass);
317
318         /**
319          * Create DOM element to hold results, and force absolute position
320          */
321         this.dom.$results = $('<div></div>').hide().addClass(this.options.resultsClass).css({
322             position: 'absolute'
323         });
324         $('body').append(this.dom.$results);
325
326         /**
327          * Attach keyboard monitoring to $elem
328          */
329         $elem.keydown(function(e) {
330             self.lastKeyPressed_ = e.keyCode;
331             switch(self.lastKeyPressed_) {
332
333                 case self.options.delimiterKeyCode: // comma = 188
334                     if (self.options.useDelimiter && self.active_) {
335                         self.selectCurrent();
336                     }
337                     break;
338
339                 // ignore navigational & special keys
340                 case 35: // end
341                 case 36: // home
342                 case 16: // shift
343                 case 17: // ctrl
344                 case 18: // alt
345                 case 37: // left
346                 case 39: // right
347                     break;
348
349                 case 38: // up
350                     e.preventDefault();
351                     if (self.active_) {
352                         self.focusPrev();
353                     } else {
354                         self.activate();
355                     }
356                     return false;
357
358                 case 40: // down
359                     e.preventDefault();
360                     if (self.active_) {
361                         self.focusNext();
362                     } else {
363                         self.activate();
364                     }
365                     return false;
366
367                 case 9: // tab
368                     if (self.active_) {
369                         self.selectCurrent();
370                         if (self.options.preventDefaultTab) {
371                             e.preventDefault();
372                             return false;
373                         }
374                     }
375                     if (self.options.preventDefaultTab === 2) {
376                         e.preventDefault();
377                         return false;
378                     }
379                 break;
380
381                 case 13: // return
382                     if (self.active_) {
383                         self.selectCurrent();
384                         if (self.options.preventDefaultReturn) {
385                             e.preventDefault();
386                             return false;
387                         }
388                     }
389                     if (self.options.preventDefaultReturn === 2) {
390                         e.preventDefault();
391                         return false;
392                     }
393                 break;
394
395                 case 27: // escape
396                     if (self.active_) {
397                         e.preventDefault();
398                         self.deactivate(true);
399                         return false;
400                     }
401                 break;
402
403                 default:
404                     self.activate();
405
406             }
407         });
408
409         /**
410          * Attach paste event listener because paste may occur much later then keydown or even without a keydown at all
411          */
412         $elem.on('paste', function() {
413             self.activate();
414         });
415
416         /**
417          * Finish on blur event
418          * Use a timeout because instant blur gives race conditions
419          */
420         var onBlurFunction = function() {
421             self.deactivate(true);
422         }
423         $elem.blur(function() {
424             if (self.finishOnBlur_) {
425                 self.finishTimeout_ = setTimeout(onBlurFunction, 200);
426             }
427         });
428         /**
429          * Catch a race condition on form submit
430          */
431         $elem.parents('form').on('submit', onBlurFunction);
432
433     };
434
435     /**
436      * Position output DOM elements
437      * @private
438      */
439     $.Autocompleter.prototype.position = function() {
440         var offset = this.dom.$elem.offset();
441         var height = this.dom.$results.outerHeight();
442         var totalHeight = $(window).outerHeight();
443         var inputBottom = offset.top + this.dom.$elem.outerHeight();
444         var bottomIfDown = inputBottom + height;
445         // Set autocomplete results at the bottom of input
446         var position = {top: inputBottom, left: offset.left};
447         if (bottomIfDown > totalHeight) {
448             // Try to set autocomplete results at the top of input
449             var topIfUp = offset.top - height;
450             if (topIfUp >= 0) {
451                 position.top = topIfUp;
452             }
453         }
454         this.dom.$results.css(position);
455     };
456
457     /**
458      * Read from cache
459      * @private
460      */
461     $.Autocompleter.prototype.cacheRead = function(filter) {
462         var filterLength, searchLength, search, maxPos, pos;
463         if (this.options.useCache) {
464             filter = String(filter);
465             filterLength = filter.length;
466             if (this.options.matchSubset) {
467                 searchLength = 1;
468             } else {
469                 searchLength = filterLength;
470             }
471             while (searchLength <= filterLength) {
472                 if (this.options.matchInside) {
473                     maxPos = filterLength - searchLength;
474                 } else {
475                     maxPos = 0;
476                 }
477                 pos = 0;
478                 while (pos <= maxPos) {
479                     search = filter.substr(0, searchLength);
480                     if (this.cacheData_[search] !== undefined) {
481                         return this.cacheData_[search];
482                     }
483                     pos++;
484                 }
485                 searchLength++;
486             }
487         }
488         return false;
489     };
490
491     /**
492      * Write to cache
493      * @private
494      */
495     $.Autocompleter.prototype.cacheWrite = function(filter, data) {
496         if (this.options.useCache) {
497             if (this.cacheLength_ >= this.options.maxCacheLength) {
498                 this.cacheFlush();
499             }
500             filter = String(filter);
501             if (this.cacheData_[filter] !== undefined) {
502                 this.cacheLength_++;
503             }
504             this.cacheData_[filter] = data;
505             return this.cacheData_[filter];
506         }
507         return false;
508     };
509
510     /**
511      * Flush cache
512      * @public
513      */
514     $.Autocompleter.prototype.cacheFlush = function() {
515         this.cacheData_ = {};
516         this.cacheLength_ = 0;
517     };
518
519     /**
520      * Call hook
521      * Note that all called hooks are passed the autocompleter object
522      * @param {string} hook
523      * @param data
524      * @returns Result of called hook, false if hook is undefined
525      */
526     $.Autocompleter.prototype.callHook = function(hook, data) {
527         var f = this.options[hook];
528         if (f && $.isFunction(f)) {
529             return f(data, this);
530         }
531         return false;
532     };
533
534     /**
535      * Set timeout to activate autocompleter
536      */
537     $.Autocompleter.prototype.activate = function() {
538         if (!this.options.enabled) return;
539         var self = this;
540         if (this.keyTimeout_) {
541             clearTimeout(this.keyTimeout_);
542         }
543         this.keyTimeout_ = setTimeout(function() {
544             self.activateNow();
545         }, this.options.delay);
546     };
547
548     /**
549      * Activate autocompleter immediately
550      */
551     $.Autocompleter.prototype.activateNow = function() {
552         var value = this.beforeUseConverter(this.dom.$elem.val());
553         if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) {
554             this.fetchData(value);
555         }
556     };
557
558     /**
559      * Get autocomplete data for a given value
560      * @param {string} value Value to base autocompletion on
561      * @private
562      */
563     $.Autocompleter.prototype.fetchData = function(value) {
564         var self = this;
565         var processResults = function(results, filter) {
566             if (self.options.processData) {
567                 results = self.options.processData(results);
568             }
569             self.showResults(self.filterResults(results, filter), filter);
570         };
571         this.lastProcessedValue_ = value;
572         if (value.length < this.options.minChars) {
573             processResults([], value);
574         } else if (this.options.data) {
575             processResults(this.options.data, value);
576         } else {
577             this.fetchRemoteData(value, function(remoteData) {
578                 processResults(remoteData, value);
579             });
580         }
581     };
582
583     /**
584      * Get remote autocomplete data for a given value
585      * @param {string} filter The filter to base remote data on
586      * @param {function} callback The function to call after data retrieval
587      * @private
588      */
589     $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) {
590         var data = this.cacheRead(filter);
591         if (data) {
592             callback(data);
593         } else {
594             var self = this;
595             var dataType = self.options.remoteDataType === 'json' ? 'json' : 'text';
596             var ajaxCallback = function(data) {
597                 var parsed = false;
598                 if (data !== false) {
599                     parsed = self.parseRemoteData(data);
600                     self.cacheWrite(filter, parsed);
601                 }
602                 self.dom.$elem.removeClass(self.options.loadingClass);
603                 callback(parsed);
604             };
605             this.dom.$elem.addClass(this.options.loadingClass);
606             $.ajax({
607                 url: this.makeUrl(filter),
608                 success: ajaxCallback,
609                 error: function(jqXHR, textStatus, errorThrown) {
610                     if($.isFunction(self.options.onError)) {
611                         self.options.onError(jqXHR, textStatus, errorThrown);
612                     } else {
613                       ajaxCallback(false);
614                     }
615                 },
616                 dataType: dataType
617             });
618         }
619     };
620
621     /**
622      * Create or update an extra parameter for the remote request
623      * @param {string} name Parameter name
624      * @param {string} value Parameter value
625      * @public
626      */
627     $.Autocompleter.prototype.setExtraParam = function(name, value) {
628         var index = $.trim(String(name));
629         if (index) {
630             if (!this.options.extraParams) {
631                 this.options.extraParams = {};
632             }
633             if (this.options.extraParams[index] !== value) {
634                 this.options.extraParams[index] = value;
635                 this.cacheFlush();
636             }
637         }
638
639         return this;
640     };
641
642     /**
643      * Build the url for a remote request
644      * If options.queryParamName === false, append query to url instead of using a GET parameter
645      * @param {string} param The value parameter to pass to the backend
646      * @returns {string} The finished url with parameters
647      */
648     $.Autocompleter.prototype.makeUrl = function(param) {
649         var self = this;
650         var url = this.options.url;
651         var params = $.extend({}, this.options.extraParams);
652
653         if (this.options.queryParamName === false) {
654             url += encodeURIComponent(param);
655         } else {
656             params[this.options.queryParamName] = param;
657         }
658
659         return makeUrl(url, params);
660     };
661
662     /**
663      * Parse data received from server
664      * @param remoteData Data received from remote server
665      * @returns {array} Parsed data
666      */
667     $.Autocompleter.prototype.parseRemoteData = function(remoteData) {
668         var remoteDataType;
669         var data = remoteData;
670         if (this.options.remoteDataType === 'json') {
671             remoteDataType = typeof(remoteData);
672             switch (remoteDataType) {
673                 case 'object':
674                     data = remoteData;
675                     break;
676                 case 'string':
677                     data = $.parseJSON(remoteData);
678                     break;
679                 default:
680                     throw new Error("Unexpected remote data type: " + remoteDataType);
681             }
682             return data;
683         }
684         return plainTextParser(data, this.options.lineSeparator, this.options.cellSeparator);
685     };
686
687     /**
688      * Default filter for results
689      * @param {Object} result
690      * @param {String} filter
691      * @returns {boolean} Include this result
692      * @private
693      */
694     $.Autocompleter.prototype.defaultFilter = function(result, filter) {
695         if (!result.value) {
696             return false;
697         }
698         if (this.options.filterResults) {
699             var pattern = this.matchStringConverter(filter);
700             var testValue = this.matchStringConverter(result.value);
701             if (!this.options.matchCase) {
702                 pattern = pattern.toLowerCase();
703                 testValue = testValue.toLowerCase();
704             }
705             var patternIndex = testValue.indexOf(pattern);
706             if (this.options.matchInside) {
707                 return patternIndex > -1;
708             } else {
709                 return patternIndex === 0;
710             }
711         }
712         return true;
713     };
714
715     /**
716      * Filter result
717      * @param {Object} result
718      * @param {String} filter
719      * @returns {boolean} Include this result
720      * @private
721      */
722     $.Autocompleter.prototype.filterResult = function(result, filter) {
723         // No filter
724         if (this.options.filter === false) {
725             return true;
726         }
727         // Custom filter
728         if ($.isFunction(this.options.filter)) {
729             return this.options.filter(result, filter);
730         }
731         // Default filter
732         return this.defaultFilter(result, filter);
733     };
734
735     /**
736      * Filter results
737      * @param results
738      * @param filter
739      */
740     $.Autocompleter.prototype.filterResults = function(results, filter) {
741         var filtered = [];
742         var i, result;
743
744         for (i = 0; i < results.length; i++) {
745             result = sanitizeResult(results[i]);
746             if (this.filterResult(result, filter)) {
747                 filtered.push(result);
748             }
749         }
750         if (this.options.sortResults) {
751             filtered = this.sortResults(filtered, filter);
752         }
753         if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) {
754             filtered.length = this.options.maxItemsToShow;
755         }
756         return filtered;
757     };
758
759     /**
760      * Sort results
761      * @param results
762      * @param filter
763      */
764     $.Autocompleter.prototype.sortResults = function(results, filter) {
765         var self = this;
766         var sortFunction = this.options.sortFunction;
767         if (!$.isFunction(sortFunction)) {
768             sortFunction = function(a, b, f) {
769                 return sortValueAlpha(a, b, self.options.matchCase);
770             };
771         }
772         results.sort(function(a, b) {
773             return sortFunction(a, b, filter, self.options);
774         });
775         return results;
776     };
777
778     /**
779      * Convert string before matching
780      * @param s
781      * @param a
782      * @param b
783      */
784     $.Autocompleter.prototype.matchStringConverter = function(s, a, b) {
785         var converter = this.options.matchStringConverter;
786         if ($.isFunction(converter)) {
787             s = converter(s, a, b);
788         }
789         return s;
790     };
791
792     /**
793      * Convert string before use
794      * @param {String} s
795      */
796     $.Autocompleter.prototype.beforeUseConverter = function(s) {
797         s = this.getValue(s);
798         var converter = this.options.beforeUseConverter;
799         if ($.isFunction(converter)) {
800             s = converter(s);
801         }
802         return s;
803     };
804
805     /**
806      * Enable finish on blur event
807      */
808     $.Autocompleter.prototype.enableFinishOnBlur = function() {
809         this.finishOnBlur_ = true;
810     };
811
812     /**
813      * Disable finish on blur event
814      */
815     $.Autocompleter.prototype.disableFinishOnBlur = function() {
816         this.finishOnBlur_ = false;
817     };
818
819     /**
820      * Create a results item (LI element) from a result
821      * @param result
822      */
823     $.Autocompleter.prototype.createItemFromResult = function(result) {
824         var self = this;
825         var $li = $('<li/>');
826         $li.html(this.showResult(result.value, result.data));
827         $li.data({value: result.value, data: result.data})
828             .click(function() {
829                 self.selectItem($li);
830             })
831             .mousedown(self.disableFinishOnBlur)
832             .mouseup(self.enableFinishOnBlur)
833         ;
834         return $li;
835     };
836
837     /**
838      * Get all items from the results list
839      * @param result
840      */
841     $.Autocompleter.prototype.getItems = function() {
842         return $('>ul>li', this.dom.$results);
843     };
844
845     /**
846      * Show all results
847      * @param results
848      * @param filter
849      */
850     $.Autocompleter.prototype.showResults = function(results, filter) {
851         var numResults = results.length;
852         var self = this;
853         var $ul = $('<ul></ul>');
854         var i, result, $li, autoWidth, first = false, $first = false;
855
856         if (numResults) {
857             for (i = 0; i < numResults; i++) {
858                 result = results[i];
859                 $li = this.createItemFromResult(result);
860                 $ul.append($li);
861                 if (first === false) {
862                     first = String(result.value);
863                     $first = $li;
864                     $li.addClass(this.options.firstItemClass);
865                 }
866                 if (i === numResults - 1) {
867                     $li.addClass(this.options.lastItemClass);
868                 }
869             }
870
871             this.dom.$results.html($ul).show();
872
873             // Always recalculate position since window size or
874             // input element location may have changed.
875             this.position();
876             if (this.options.autoWidth) {
877                 autoWidth = this.dom.$elem.outerWidth() - this.dom.$results.outerWidth() + this.dom.$results.width();
878                 this.dom.$results.css(this.options.autoWidth, autoWidth);
879             }
880             this.getItems().hover(
881                 function() { self.focusItem(this); },
882                 function() { /* void */ }
883             );
884             if (this.autoFill(first, filter) || this.options.selectFirst || (this.options.selectOnly && numResults === 1)) {
885                 this.focusItem($first);
886             }
887             this.active_ = true;
888         } else {
889             this.hideResults();
890             this.active_ = false;
891         }
892     };
893
894     $.Autocompleter.prototype.showResult = function(value, data) {
895         if ($.isFunction(this.options.showResult)) {
896             return this.options.showResult(value, data);
897         } else {
898             return $('<p></p>').text(value).html();
899         }
900     };
901
902     $.Autocompleter.prototype.autoFill = function(value, filter) {
903         var lcValue, lcFilter, valueLength, filterLength;
904         if (this.options.autoFill && this.lastKeyPressed_ !== 8) {
905             lcValue = String(value).toLowerCase();
906             lcFilter = String(filter).toLowerCase();
907             valueLength = value.length;
908             filterLength = filter.length;
909             if (lcValue.substr(0, filterLength) === lcFilter) {
910                 var d = this.getDelimiterOffsets();
911                 var pad = d.start ? ' ' : ''; // if there is a preceding delimiter
912                 this.setValue( pad + value );
913                 var start = filterLength + d.start + pad.length;
914                 var end = valueLength + d.start + pad.length;
915                 this.selectRange(start, end);
916                 return true;
917             }
918         }
919         return false;
920     };
921
922     $.Autocompleter.prototype.focusNext = function() {
923         this.focusMove(+1);
924     };
925
926     $.Autocompleter.prototype.focusPrev = function() {
927         this.focusMove(-1);
928     };
929
930     $.Autocompleter.prototype.focusMove = function(modifier) {
931         var $items = this.getItems();
932         modifier = sanitizeInteger(modifier, 0);
933         if (modifier) {
934             for (var i = 0; i < $items.length; i++) {
935                 if ($($items[i]).hasClass(this.selectClass_)) {
936                     this.focusItem(i + modifier);
937                     return;
938                 }
939             }
940         }
941         this.focusItem(0);
942     };
943
944     $.Autocompleter.prototype.focusItem = function(item) {
945         var $item, $items = this.getItems();
946         if ($items.length) {
947             $items.removeClass(this.selectClass_).removeClass(this.options.selectClass);
948             if (typeof item === 'number') {
949                 if (item < 0) {
950                     item = 0;
951                 } else if (item >= $items.length) {
952                     item = $items.length - 1;
953                 }
954                 $item = $($items[item]);
955             } else {
956                 $item = $(item);
957             }
958             if ($item) {
959                 $item.addClass(this.selectClass_).addClass(this.options.selectClass);
960             }
961         }
962     };
963
964     $.Autocompleter.prototype.selectCurrent = function() {
965         var $item = $('li.' + this.selectClass_, this.dom.$results);
966         if ($item.length === 1) {
967             this.selectItem($item);
968         } else {
969             this.deactivate(false);
970         }
971     };
972
973     $.Autocompleter.prototype.selectItem = function($li) {
974         var value = $li.data('value');
975         var data = $li.data('data');
976         var displayValue = this.displayValue(value, data);
977         var processedDisplayValue = this.beforeUseConverter(displayValue);
978         this.lastProcessedValue_ = processedDisplayValue;
979         this.lastSelectedValue_ = processedDisplayValue;
980         var d = this.getDelimiterOffsets();
981         var delimiter = this.options.delimiterChar;
982         var elem = this.dom.$elem;
983         var extraCaretPos = 0;
984         if ( this.options.useDelimiter ) {
985             // if there is a preceding delimiter, add a space after the delimiter
986             if ( elem.val().substring(d.start-1, d.start) == delimiter && delimiter != ' ' ) {
987                 displayValue = ' ' + displayValue;
988             }
989             // if there is not already a delimiter trailing this value, add it
990             if ( elem.val().substring(d.end, d.end+1) != delimiter && this.lastKeyPressed_ != this.options.delimiterKeyCode ) {
991                 displayValue = displayValue + delimiter;
992             } else {
993                 // move the cursor after the existing trailing delimiter
994                 extraCaretPos = 1;
995             }
996         }
997         this.setValue(displayValue);
998         this.setCaret(d.start + displayValue.length + extraCaretPos);
999         this.callHook('onItemSelect', { value: value, data: data });
1000         this.deactivate(true);
1001         elem.focus();
1002     };
1003
1004     $.Autocompleter.prototype.displayValue = function(value, data) {
1005         if ($.isFunction(this.options.displayValue)) {
1006             return this.options.displayValue(value, data);
1007         }
1008         return value;
1009     };
1010
1011     $.Autocompleter.prototype.hideResults = function() {
1012         this.dom.$results.hide();
1013     };
1014
1015     $.Autocompleter.prototype.deactivate = function(finish) {
1016         if (this.finishTimeout_) {
1017             clearTimeout(this.finishTimeout_);
1018         }
1019         if (this.keyTimeout_) {
1020             clearTimeout(this.keyTimeout_);
1021         }
1022         if (finish) {
1023             if (this.lastProcessedValue_ !== this.lastSelectedValue_) {
1024                 if (this.options.mustMatch) {
1025                     this.setValue('');
1026                 }
1027                 this.callHook('onNoMatch');
1028             }
1029             if (this.active_) {
1030                 this.callHook('onFinish');
1031             }
1032             this.lastKeyPressed_ = null;
1033             this.lastProcessedValue_ = null;
1034             this.lastSelectedValue_ = null;
1035             this.active_ = false;
1036         }
1037         this.hideResults();
1038     };
1039
1040     $.Autocompleter.prototype.selectRange = function(start, end) {
1041         var input = this.dom.$elem.get(0);
1042         if (input.setSelectionRange) {
1043             input.focus();
1044             input.setSelectionRange(start, end);
1045         } else if (input.createTextRange) {
1046             var range = input.createTextRange();
1047             range.collapse(true);
1048             range.moveEnd('character', end);
1049             range.moveStart('character', start);
1050             range.select();
1051         }
1052     };
1053
1054     /**
1055      * Move caret to position
1056      * @param {Number} pos
1057      */
1058     $.Autocompleter.prototype.setCaret = function(pos) {
1059         this.selectRange(pos, pos);
1060     };
1061
1062     /**
1063      * Get caret position
1064      */
1065     $.Autocompleter.prototype.getCaret = function() {
1066         var $elem = this.dom.$elem;
1067         var elem = $elem[0];
1068         var val, selection, range, start, end, stored_range;
1069         if (elem.createTextRange) { // IE
1070             selection = document.selection;
1071             if (elem.tagName.toLowerCase() != 'textarea') {
1072                 val = $elem.val();
1073                 range = selection.createRange().duplicate();
1074                 range.moveEnd('character', val.length);
1075                 if (range.text === '') {
1076                     start = val.length;
1077                 } else {
1078                     start = val.lastIndexOf(range.text);
1079                 }
1080                 range = selection.createRange().duplicate();
1081                 range.moveStart('character', -val.length);
1082                 end = range.text.length;
1083             } else {
1084                 range = selection.createRange();
1085                 stored_range = range.duplicate();
1086                 stored_range.moveToElementText(elem);
1087                 stored_range.setEndPoint('EndToEnd', range);
1088                 start = stored_range.text.length - range.text.length;
1089                 end = start + range.text.length;
1090             }
1091         } else {
1092             start = $elem[0].selectionStart;
1093             end = $elem[0].selectionEnd;
1094         }
1095         return {
1096             start: start,
1097             end: end
1098         };
1099     };
1100
1101     /**
1102      * Set the value that is currently being autocompleted
1103      * @param {String} value
1104      */
1105     $.Autocompleter.prototype.setValue = function(value) {
1106         if ( this.options.useDelimiter ) {
1107             // set the substring between the current delimiters
1108             var val = this.dom.$elem.val();
1109             var d = this.getDelimiterOffsets();
1110             var preVal = val.substring(0, d.start);
1111             var postVal = val.substring(d.end);
1112             value = preVal + value + postVal;
1113         }
1114         this.dom.$elem.val(value);
1115     };
1116
1117     /**
1118      * Get the value currently being autocompleted
1119      * @param {String} value
1120      */
1121     $.Autocompleter.prototype.getValue = function(value) {
1122         if ( this.options.useDelimiter ) {
1123             var d = this.getDelimiterOffsets();
1124             return value.substring(d.start, d.end).trim();
1125         } else {
1126             return value;
1127         }
1128     };
1129
1130     /**
1131      * Get the offsets of the value currently being autocompleted
1132      */
1133     $.Autocompleter.prototype.getDelimiterOffsets = function() {
1134         var val = this.dom.$elem.val();
1135         if ( this.options.useDelimiter ) {
1136             var preCaretVal = val.substring(0, this.getCaret().start);
1137             var start = preCaretVal.lastIndexOf(this.options.delimiterChar) + 1;
1138             var postCaretVal = val.substring(this.getCaret().start);
1139             var end = postCaretVal.indexOf(this.options.delimiterChar);
1140             if ( end == -1 ) end = val.length;
1141             end += this.getCaret().start;
1142         } else {
1143             start = 0;
1144             end = val.length;
1145         }
1146         return {
1147             start: start,
1148             end: end
1149         };
1150     };
1151
1152 })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined')? django.jQuery : jQuery);