PageSourceSearch

https://ecatalog.ounb.km.ua/irbis64r_14/js/jquery.autocomplete.js

js ounb.km.ua collected 2026-10-02 10:05:48 UTC 22,062 bytes, 817 lines download raw bytes

1/*
2 * jQuery Autocomplete plugin 1.1
3 *
4 * Copyright (c) 2009 Jörn Zaefferer
5 *
6 * Dual licensed under the MIT and GPL licenses:
7 *   http://www.opensource.org/licenses/mit-license.php
8 *   http://www.gnu.org/licenses/gpl.html
9 *
10 * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $
11 */
12
13;(function($) {
14	
15$.fn.extend({
16	autocomplete: function(urlOrData, options) {
17		var isUrl = typeof urlOrData == "string";
18		options = $.extend({}, $.Autocompleter.defaults, {
19			url: isUrl ? urlOrData : null,
20			data: isUrl ? null : urlOrData,
21			delay: isUrl ? $.Autocompleter.defaults.delay : 10,
22			max: options && !options.scroll ? 10 : 150
23		}, options);
24		
25		// if highlight is set to false, replace it with a do-nothing function
26		options.highlight = options.highlight || function(value) { return value; };
27		
28		// if the formatMatch option is not specified, then use formatItem for backwards compatibility
29		options.formatMatch = options.formatMatch || options.formatItem;
30		
31		return this.each(function() {
32			new $.Autocompleter(this, options);
33		});
34	},
35	result: function(handler) {
36		return this.bind("result", handler);
37	},
38	search: function(handler) {
39		return this.trigger("search", [handler]);
40	},
41	flushCache: function() {
42		return this.trigger("flushCache");
43	},
44	setOptions: function(options){
45		return this.trigger("setOptions", [options]);
46	},
47	unautocomplete: function() {
48		return this.trigger("unautocomplete");
49	}
50});
51
52$.Autocompleter = function(input, options) {
53
54	var KEY = {
55		UP: 38,
56		DOWN: 40,
57		DEL: 46,
58		TAB: 9,
59		RETURN: 13,
60		ESC: 27,
61		COMMA: 191,
62		PAGEUP: 33,
63		PAGEDOWN: 34,
64		BACKSPACE: 8
65	};
66
67	// Create $ object for input element
68	var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
69
70	var timeout;
71	var previousValue = "";
72	var cache = $.Autocompleter.Cache(options);
73	var hasFocus = 0;
74	var lastKeyPressCode;
75	var config = {
76		mouseDownOnSelect: false
77	};
78	var select = $.Autocompleter.Select(options, input, selectCurrent, config);
79	
80	var blockSubmit;
81	
82	// prevent form submit in opera when selecting with return key
83	$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
84		if (blockSubmit) {
85			blockSubmit = false;
86			return false;
87		}
88	});
89	
90	// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
91	$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
92		// a keypress means the input has focus
93		// avoids issue where input had focus before the autocomplete was applied
94		hasFocus = 1;
95		// track last key pressed
96		lastKeyPressCode = event.keyCode;
97		switch(event.keyCode) {
98		
99			case KEY.UP:
100				event.preventDefault();
101				if ( select.visible() ) {
102					select.prev();
103				} else {
104					onChange(0, true);
105				}
106				break;
107				
108			case KEY.DOWN:
109				event.preventDefault();
110				if ( select.visible() ) {
111					select.next();
112				} else {
113					onChange(0, true);
114				}
115				break;
116				
117			case KEY.PAGEUP:
118				event.preventDefault();
119				if ( select.visible() ) {
120					select.pageUp();
121				} else {
122					onChange(0, true);
123				}
124				break;
125				
126			case KEY.PAGEDOWN:
127				event.preventDefault();
128				if ( select.visible() ) {
129					select.pageDown();
130				} else {
131					onChange(0, true);
132				}
133				break;
134			
135			// matches also semicolon
136			// Отключаем некорректную реакцию на запятую 
137			//case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
138			case KEY.TAB:
139			case KEY.RETURN:
140				if( selectCurrent() ) {
141					// stop default to prevent a form submit, Opera needs special handling
142					event.preventDefault();
143					blockSubmit = true;
144					return false;
145				}
146				break;
147				
148			case KEY.ESC:
149				select.hide();
150				break;
151				
152			default:
153				clearTimeout(timeout);
154				timeout = setTimeout(onChange, options.delay);
155				break;
156		}
157	}).focus(function(){
158		// track whether the field has focus, we shouldn't process any
159		// results if the field no longer has focus
160		hasFocus++;
161	}).blur(function() {
162		hasFocus = 0;
163		if (!config.mouseDownOnSelect) {
164			hideResults();
165		}
166	}).click(function() {
167		// show select when clicking in a focused field
168		if ( hasFocus++ > 1 && !select.visible() ) {
169			onChange(0, true);
170		}
171	}).bind("search", function() {
172		// TODO why not just specifying both arguments?
173		var fn = (arguments.length > 1) ? arguments[1] : null;
174		function findValueCallback(q, data) {
175			var result;
176			if( data && data.length ) {
177				for (var i=0; i < data.length; i++) {
178					if( data[i].result.toLowerCase() == q.toLowerCase() ) {
179						result = data[i];
180						break;
181					}
182				}
183			}
184			if( typeof fn == "function" ) fn(result);
185			else $input.trigger("result", result && [result.data, result.value]);
186		}
187		$.each(trimWords($input.val()), function(i, value) {
188			request(value, findValueCallback, findValueCallback);
189		});
190	}).bind("flushCache", function() {
191		cache.flush();
192	}).bind("setOptions", function() {
193		$.extend(options, arguments[1]);
194		// if we've updated the data, repopulate
195		if ( "data" in arguments[1] )
196			cache.populate();
197	}).bind("unautocomplete", function() {
198		select.unbind();
199		$input.unbind();
200		$(input.form).unbind(".autocomplete");
201	});
202	
203	
204	function selectCurrent() {
205		var selected = select.selected();
206		if( !selected )
207			return false;
208		
209		var v = selected.result;
210		previousValue = v;
211		
212		if ( options.multiple ) {
213			var words = trimWords($input.val());
214			if ( words.length > 1 ) {
215				var seperator = options.multipleSeparator.length;
216				var cursorAt = $(input).selection().start;
217				var wordAt, progress = 0;
218				// Цель настоящего цикла -- выяснить какое по порядку слово нуждается в автозамене
219				$.each(words, function(i, word) {
220					progress += word.length;
221					// В том случае если длинна всех слов больше или равна положению курсора, увеличиваем wordAt(номер заменяемого слова) и прекращаем выполнение цикла.
222					//alert("cursorAt"+cursorAt);
223					//alert("progress"+progress);
224					if (cursorAt <= progress) {
225						wordAt = i;
226						return false;
227					}
228					progress += seperator;
229				});
230				if (cursorAt<=1) wordAt=words.length-1;
231				words[wordAt] = v;
232				// TODO this should set the cursor to the right position, but it gets overriden somewhere
233				//$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
234				//$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
235				v = words.join( options.multipleSeparator );
236			}
237			v += options.multipleSeparator;
238		}
239		
240		//if (cursorAt<2) cursorAt=v.length;
241		//alert(cursorAt);
242		$input.val(v);
243		hideResultsNow();
244		$input.trigger("result", [selected.data, selected.value]);
245		return true;
246	}
247	
248	function onChange(crap, skipPrevCheck) {
249		if( lastKeyPressCode == KEY.DEL ) {
250			select.hide();
251			return;
252		}
253		
254		var currentValue = $input.val();
255		
256		if ( !skipPrevCheck && currentValue == previousValue )
257			return;
258		
259		previousValue = currentValue;
260		// Проблему нужно отслеживать здесь 
261		currentValue = lastWord(currentValue);
262		if ( currentValue.length >= options.minChars) {
263			$input.addClass(options.loadingClass);
264			if (!options.matchCase)
265				currentValue = currentValue.toLowerCase();
266			request(currentValue, receiveData, hideResultsNow);
267		} else {
268			stopLoading();
269			select.hide();
270		}
271	};
272	
273	function trimWords(value) {
274		if (!value)
275			return [""];
276		if (!options.multiple)
277			return [$.trim(value)];
278		return $.map(value.split(options.multipleSeparator), function(word) {
279			return $.trim(value).length ? $.trim(word) : null;
280		});
281	}
282	
283	function lastWord(value) {
284		if ( !options.multiple )
285			return value;
286		var words = trimWords(value);
287		if (words.length == 1) 
288			return words[0];
289		var cursorAt = $(input).selection().start;
290		if (cursorAt == value.length) {
291			words = trimWords(value)
292		} else {
293			words = trimWords(value.replace(value.substring(cursorAt), ""));
294		}
295		return words[words.length - 1];
296	}
297	
298	// fills in the input box w/the first match (assumed to be the best match)
299	// q: the term entered
300	// sValue: the first matching result
301	function autoFill(q, sValue){
302		// autofill in the complete box w/the first match as long as the user hasn't entered in more data
303		// if the last user key pressed was backspace, don't autofill
304		if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
305			// fill in the value (keep the case the user has typed)
306			$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
307			// select the portion of the value not typed by the user (so the next character will erase)
308			$(input).selection(previousValue.length, previousValue.length + sValue.length);
309		}
310	};
311
312	function hideResults() {
313		clearTimeout(timeout);
314		timeout = setTimeout(hideResultsNow, 200);
315	};
316
317	function hideResultsNow() {
318		var wasVisible = select.visible();
319		select.hide();
320		clearTimeout(timeout);
321		stopLoading();
322		if (options.mustMatch) {
323			// call search and run callback
324			$input.search(
325				function (result){
326					// if no value found, clear the input box
327					if( !result ) {
328						if (options.multiple) {
329							var words = trimWords($input.val()).slice(0, -1);
330							$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
331						}
332						else {
333							$input.val( "" );
334							$input.trigger("result", null);
335						}
336					}
337				}
338			);
339		}
340	};
341
342	function receiveData(q, data) {
343		if ( data && data.length && hasFocus ) {
344			stopLoading();
345			select.display(data, q);
346			autoFill(q, data[0].value);
347			select.show();
348		} else {
349			hideResultsNow();
350		}
351	};
352
353	function request(term, success, failure) {
354		if (!options.matchCase)
355			term = term.toLowerCase();
356		var data = cache.load(term);
357		// recieve the cached data
358		if (data && data.length) {
359			success(term, data);
360		// if an AJAX url has been supplied, try loading the data now
361		} else if( (typeof options.url == "string") && (options.url.length > 0) ){
362			
363			var extraParams = {
364				timestamp: +new Date()
365			};
366			$.each(options.extraParams, function(key, param) {
367				extraParams[key] = typeof param == "function" ? param() : param;
368			});
369			
370			$.ajax({
371				// try to leverage ajaxQueue plugin to abort previous requests
372				mode: "abort",
373				// limit abortion to this input
374				port: "autocomplete" + input.name,
375				dataType: options.dataType,
376				url: options.url,
377				data: $.extend({
378					q: lastWord(term),
379					limit: options.max
380				}, extraParams),
381				success: function(data) {
382					var parsed = options.parse && options.parse(data) || parse(data);
383					cache.add(term, parsed);
384					success(term, parsed);
385				}
386			});
387		} else {
388			// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
389			select.emptyList();
390			failure(term);
391		}
392	};
393	
394	function parse(data) {
395		var parsed = [];
396		var rows = data.split("\n");
397		for (var i=0; i < rows.length; i++) {
398			var row = $.trim(rows[i]);
399			if (row) {
400				row = row.split("|");
401				parsed[parsed.length] = {
402					data: row,
403					value: row[0],
404					result: options.formatResult && options.formatResult(row, row[0]) || row[0]
405				};
406			}
407		}
408		return parsed;
409	};
410
411	function stopLoading() {
412		$input.removeClass(options.loadingClass);
413	};
414
415};
416
417$.Autocompleter.defaults = {
418	inputClass: "ac_input",
419	resultsClass: "ac_results",
420	loadingClass: "ac_loading",
421	minChars: 1,
422	delay: 400,
423	matchCase: false,
424	matchSubset: true,
425	matchContains: false,
426	cacheLength: 10,
427	max: 100,
428	mustMatch: false,
429	extraParams: {},
430	selectFirst: true,
431	formatItem: function(row) { return row[0]; },
432	formatMatch: null,
433	autoFill: false,
434	width: 0,
435	multiple: false,
436	multipleSeparator: ", ",
437	highlight: function(value, term) {
438		return value.replace(new RegExp("^(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")", "gi"), "<strong>$1</strong>");
439	},
440    scroll: true,
441    scrollHeight: 180
442};
443
444$.Autocompleter.Cache = function(options) {
445
446	var data = {};
447	var length = 0;
448	
449	function matchSubset(s, sub) {
450		if (!options.matchCase) 
451			s = s.toLowerCase();
452		var i = s.indexOf(sub);
453		if (options.matchContains == "word"){
454			i = s.toLowerCase().search("\\b" + sub.toLowerCase());
455		}
456		if (i == -1) return false;
457		return i == 0 || options.matchContains;
458	};
459	
460	function add(q, value) {
461		if (length > options.cacheLength){
462			flush();
463		}
464		if (!data[q]){ 
465			length++;
466		}
467		data[q] = value;
468	}
469	
470	function populate(){
471		if( !options.data ) return false;
472		// track the matches
473		var stMatchSets = {},
474			nullData = 0;
475
476		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
477		if( !options.url ) options.cacheLength = 1;
478		
479		// track all options for minChars = 0
480		stMatchSets[""] = [];
481		
482		// loop through the array and create a lookup structure
483		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
484			var rawValue = options.data[i];
485			// if rawValue is a string, make an array otherwise just reference the array
486			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
487			
488			var value = options.formatMatch(rawValue, i+1, options.data.length);
489			if ( value === false )
490				continue;
491				
492			var firstChar = value.charAt(0).toLowerCase();
493			// if no lookup array for this character exists, look it up now
494			if( !stMatchSets[firstChar] ) 
495				stMatchSets[firstChar] = [];
496
497			// if the match is a string
498			var row = {
499				value: value,
500				data: rawValue,
501				result: options.formatResult && options.formatResult(rawValue) || value
502			};
503			
504			// push the current match into the set list
505			stMatchSets[firstChar].push(row);
506
507			// keep track of minChars zero items
508			if ( nullData++ < options.max ) {
509				stMatchSets[""].push(row);
510			}
511		};
512
513		// add the data items to the cache
514		$.each(stMatchSets, function(i, value) {
515			// increase the cache size
516			options.cacheLength++;
517			// add to the cache
518			add(i, value);
519		});
520	}
521	
522	// populate any existing data
523	setTimeout(populate, 25);
524	
525	function flush(){
526		data = {};
527		length = 0;
528	}
529	
530	return {
531		flush: flush,
532		add: add,
533		populate: populate,
534		load: function(q) {
535			if (!options.cacheLength || !length)
536				return null;
537			/* 
538			 * if dealing w/local data and matchContains than we must make sure
539			 * to loop through all the data collections looking for matches
540			 */
541			if( !options.url && options.matchContains ){
542				// track all matches
543				var csub = [];
544				// loop through all the data grids for matches
545				for( var k in data ){
546					// don't search through the stMatchSets[""] (minChars: 0) cache
547					// this prevents duplicates
548					if( k.length > 0 ){
549						var c = data[k];
550						$.each(c, function(i, x) {
551							// if we've got a match, add it to the array
552							if (matchSubset(x.value, q)) {
553								csub.push(x);
554							}
555						});
556					}
557				}				
558				return csub;
559			} else 
560			// if the exact item exists, use it
561			if (data[q]){
562				return data[q];
563			} else
564			if (options.matchSubset) {
565				for (var i = q.length - 1; i >= options.minChars; i--) {
566					var c = data[q.substr(0, i)];
567					if (c) {
568						var csub = [];
569						$.each(c, function(i, x) {
570							if (matchSubset(x.value, q)) {
571								csub[csub.length] = x;
572							}
573						});
574						return csub;
575					}
576				}
577			}
578			return null;
579		}
580	};
581};
582
583$.Autocompleter.Select = function (options, input, select, config) {
584	var CLASSES = {
585		ACTIVE: "ac_over"
586	};
587	
588	var listItems,
589		active = -1,
590		data,
591		term = "",
592		needsInit = true,
593		element,
594		list;
595	
596	// Create results
597	function init() {
598		if (!needsInit)
599			return;
600		element = $("<div/>")
601		.hide()
602		.addClass(options.resultsClass)
603		.css("position", "absolute")
604		.appendTo(document.body);
605	
606		list = $("<ul/>").appendTo(element).mouseover( function(event) {
607			if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
608	            active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
609			    $(target(event)).addClass(CLASSES.ACTIVE);            
610	        }
611		}).click(function(event) {
612			$(target(event)).addClass(CLASSES.ACTIVE);
613			select();
614			// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
615			input.focus();
616			return false;
617		}).mousedown(function() {
618			config.mouseDownOnSelect = true;
619		}).mouseup(function() {
620			config.mouseDownOnSelect = false;
621		});
622		
623		if( options.width > 0 )
624			element.css("width", options.width);
625			
626		needsInit = false;
627	} 
628	
629	function target(event) {
630		var element = event.target;
631		while(element && element.tagName != "LI")
632			element = element.parentNode;
633		// more fun with IE, sometimes event.target is empty, just ignore it then
634		if(!element)
635			return [];
636		return element;
637	}
638
639	function moveSelect(step) {
640		listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
641		movePosition(step);
642        var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
643        if(options.scroll) {
644            var offset = 0;
645            listItems.slice(0, active).each(function() {
646				offset += this.offsetHeight;
647			});
648            if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
649                list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
650            } else if(offset < list.scrollTop()) {
651                list.scrollTop(offset);
652            }
653        }
654	};
655	
656	function movePosition(step) {
657		active += step;
658		if (active < 0) {
659			active = listItems.size() - 1;
660		} else if (active >= listItems.size()) {
661			active = 0;
662		}
663	}
664	
665	function limitNumberOfItems(available) {
666		return options.max && options.max < available
667			? options.max
668			: available;
669	}
670	
671	function fillList() {
672		list.empty();
673		var max = limitNumberOfItems(data.length);
674		for (var i=0; i < max; i++) {
675			if (!data[i])
676				continue;
677			var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
678			if ( formatted === false )
679				continue;
680			var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
681			$.data(li, "ac_data", data[i]);
682		}
683		listItems = list.find("li");
684		if ( options.selectFirst ) {
685			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
686			active = 0;
687		}
688		// apply bgiframe if available
689		if ( $.fn.bgiframe )
690			list.bgiframe();
691	}
692	
693	return {
694		display: function(d, q) {
695			init();
696			data = d;
697			term = q;
698			fillList();
699		},
700		next: function() {
701			moveSelect(1);
702		},
703		prev: function() {
704			moveSelect(-1);
705		},
706		pageUp: function() {
707			if (active != 0 && active - 8 < 0) {
708				moveSelect( -active );
709			} else {
710				moveSelect(-8);
711			}
712		},
713		pageDown: function() {
714			if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
715				moveSelect( listItems.size() - 1 - active );
716			} else {
717				moveSelect(8);
718			}
719		},
720		hide: function() {
721			element && element.hide();
722			listItems && listItems.removeClass(CLASSES.ACTIVE);
723			active = -1;
724		},
725		visible : function() {
726			return element && element.is(":visible");
727		},
728		current: function() {
729			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
730		},
731		show: function() {
732			var offset = $(input).offset();
733			element.css({
734				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
735				top: offset.top + input.offsetHeight,
736				left: offset.left
737			}).show();
738            if(options.scroll) {
739                list.scrollTop(0);
740                list.css({
741					maxHeight: options.scrollHeight,
742					overflow: 'auto'
743				});
744				
745                if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
746					var listHeight = 0;
747					listItems.each(function() {
748						listHeight += this.offsetHeight;
749					});
750					var scrollbarsVisible = listHeight > options.scrollHeight;
751                    list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
752					if (!scrollbarsVisible) {
753						// IE doesn't recalculate width when scrollbar disappears
754						listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
755					}
756                }
757                
758            }
759		},
760		selected: function() {
761			var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
762			return selected && selected.length && $.data(selected[0], "ac_data");
763		},
764		emptyList: function (){
765			list && list.empty();
766		},
767		unbind: function() {
768			element && element.remove();
769		}
770	};
771};
772
773$.fn.selection = function(start, end) {
774	if (start !== undefined) {
775		return this.each(function() {
776			if( this.createTextRange ){
777				var selRange = this.createTextRange();
778				if (end === undefined || start == end) {
779					selRange.move("character", start);
780					selRange.select();
781				} else {
782					selRange.collapse(true);
783					selRange.moveStart("character", start);
784					selRange.moveEnd("character", end);
785					selRange.select();
786				}
787			} else if( this.setSelectionRange ){
788				this.setSelectionRange(start, end);
789			} else if( this.selectionStart ){
790				this.selectionStart = start;
791				this.selectionEnd = end;
792			}
793		});
794	}
795	var field = this[0];
796	if ( field.createTextRange ) {
797		var range = document.selection.createRange(),
798			orig = field.value,
799			teststring = "<->",
800			textLength = range.text.length;
801		range.text = teststring;
802		var caretAt = field.value.indexOf(teststring);
803		field.value = orig;
804		this.selection(caretAt, caretAt + textLength);
805		return {
806			start: caretAt,
807			end: caretAt + textLength
808		}
809	} else if( field.selectionStart !== undefined ){
810		return {
811			start: field.selectionStart,
812			end: field.selectionEnd
813		}
814	}
815};
816
817}
817)(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.