PageSourceSearch

https://www.hotac.org.tw/modules/civicrm/packages/jquery/plugins/jquery.autocomplete.js?tlhygj

js hotac.org.tw collected 2026-09-24 20:09:54 UTC 19,252 bytes, 750 lines download raw bytes

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