PageSourceSearch

https://foraqsa.com/media/com_finder/js/autocompleter.js

js foraqsa.com collected 2026-09-24 10:06:37 UTC 16,256 bytes, 505 lines download raw bytes

1var Observer = new Class({
2	Implements: [Options, Events],
3	options: {
4		periodical: false,
5		delay: 1000
6	},
7	initialize: function (el, onFired, options) {
8		this.element = document.id(el) || $document.id(el);
9		this.addEvent('onFired', onFired);
10		this.setOptions(options);
11		this.bound = this.changed.bind(this);
12		this.resume();
13	},
14	changed: function () {
15		var value = this.element.get('value');
16		if ($equals(this.value, value)) return;
17		this.clear();
18		this.value = value;
19		this.timeout = this.onFired.delay(this.options.delay, this);
20	},
21	setValue: function (value) {
22		this.value = value;
23		this.element.set('value', value);
24		return this.clear();
25	},
26	onFired: function () {
27		this.fireEvent('onFired', [this.value, this.element]);
28	},
29	clear: function () {
30		clearTimeout(this.timeout || null);
31		return this;
32	},
33	pause: function () {
34		if (this.timer) clearTimeout(this.timer);
35		else this.element.removeEvent('keyup', this.bound);
36		return this.clear();
37	},
38	resume: function () {
39		this.value = this.element.get('value');
40		if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this);
41		else this.element.addEvent('keyup', this.bound);
42		return this;
43	}
44});
45var $equals = function (obj1, obj2) {
46		return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2));
47	};
48var Autocompleter = new Class({
49	Implements: [Options, Events],
50	options: {
51		minLength: 1,
52		markQuery: true,
53		width: 'inherit',
54		maxChoices: 10,
55		injectChoice: null,
56		customChoices: null,
57		emptyChoices: null,
58		visibleChoices: true,
59		className: 'autocompleter-choices',
60		zIndex: 1000,
61		delay: 400,
62		observerOptions: {},
63		fxOptions: {},
64		autoSubmit: false,
65		overflow: false,
66		overflowMargin: 25,
67		selectFirst: false,
68		filter: null,
69		filterCase: false,
70		filterSubset: false,
71		forceSelect: false,
72		selectMode: true,
73		choicesMatch: null,
74		multiple: false,
75		separator: ', ',
76		separatorSplit: /\s*[,;]\s*/,
77		autoTrim: false,
78		allowDupes: false,
79		cache: true,
80		relative: false
81	},
82	initialize: function (element, options) {
83		this.element = document.id(element);
84		this.setOptions(options);
85		this.build();
86		this.observer = new Observer(this.element, this.prefetch.bind(this), Object.merge({}, {
87			'delay': this.options.delay
88		}, this.options.observerOptions));
89		this.queryValue = null;
90		if (this.options.filter) this.filter = this.options.filter.bind(this);
91		var mode = this.options.selectMode;
92		this.typeAhead = (mode == 'type-ahead');
93		this.selectMode = (mode === true) ? 'selection' : mode;
94		this.cached = [];
95	},
96	build: function () {
97		if (document.id(this.options.customChoices)) {
98			this.choices = this.options.customChoices;
99		} else {
100			this.choices = new Element('ul', {
101				'class': this.options.className,
102				'styles': {
103					'zIndex': this.options.zIndex
104				}
105			}).inject(document.body);
106			this.relative = false;
107			if (this.options.relative) {
108				this.choices.inject(this.element, 'after');
109				this.relative = this.element.getOffsetParent();
110			}
111			this.fix = new OverlayFix(this.choices);
112		}
113		if (!this.options.separator.test(this.options.separatorSplit)) {
114			this.options.separatorSplit = this.options.separator;
115		}
116		this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, Ob
116ject.merge({}, {
117			'property': 'opacity',
118			'link': 'cancel',
119			'duration': 200
120		}, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0);
121		this.element.setProperty('autocomplete', 'off').addEvent((Browser.ie || Browser.safari || Browser.chrome) ? 'keydown' : 'keypress', this.onCommand.bind(this)).addEvent('click', this.onCommand.bind(this, [false])).addEvent('focus', this.toggleFocus.create({
122			bind: this,
123			arguments: true,
124			delay: 100
125		})).addEvent('blur', this.toggleFocus.create({
126			bind: this,
127			arguments: false,
128			delay: 100
129		}));
130	},
131	destroy: function () {
132		if (this.fix) this.fix.destroy();
133		this.choices = this.selected = this.choices.destroy();
134	},
135	toggleFocus: function (state) {
136		this.focussed = state;
137		if (!state) this.hideChoices(true);
138		this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]);
139	},
140	onCommand: function (e) {
141		if (!e && this.focussed) return this.prefetch();
142		if (e && e.key && !e.shift) {
143			switch (e.key) {
144			case 'enter':
145				if (this.element.value != this.opted) return true;
146				if (this.selected && this.visible) {
147					this.choiceSelect(this.selected);
148					return !!(this.options.autoSubmit);
149				}
150				break;
151			case 'up':
152			case 'down':
153				if (!this.prefetch() && this.queryValue !== null) {
154					var up = (e.key == 'up');
155					this.choiceOver((this.selected || this.choices)[(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')](this.options.choicesMatch), true);
156				}
157				return false;
158			case 'esc':
159			case 'tab':
160				this.hideChoices(true);
161				break;
162			}
163		}
164		return true;
165	},
166	setSelection: function (finish) {
167		var input = this.selected.inputValue,
168			value = input;
169		var start = this.queryValue.length,
170			end = input.length;
171		if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0;
172		if (this.options.multiple) {
173			var split = this.options.separatorSplit;
174			value = this.element.value;
175			start += this.queryIndex;
176			end += this.queryIndex;
177			var old = value.substr(this.queryIndex).split(split, 1)[0];
178			value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length);
179			if (finish) {
180				var tokens = value.split(this.options.separatorSplit).filter(function (entry) {
181					return this.test(entry);
182				}, /[^\s,]+/);
183				if (!this.options.allowDupes) tokens = [].combine(tokens);
184				var sep = this.options.separator;
185				value = tokens.join(sep) + sep;
186				end = value.length;
187			}
188		}
189		this.observer.setValue(value);
190		this.opted = value;
191		if (finish || this.selectMode == 'pick') start = end;
192		this.element.selectRange(start, end);
193		this.fireEvent('onSelection', [this.element, this.selected, value, input]);
194	},
195	showChoices: function () {
196		var match = this.options.choicesMatch,
197			first = this.choices.getFirst(match);
198		this.selected = this.selectedValue = null;
199		if (this.fix) {
200			var pos = this.element.getCoordinates(this.relative),
201				width = this.options.width || 'auto';
202			this.choices.setStyles({
203				'left': pos.left,
204				'top': pos.bottom,
205				'width': (width === true || width == 'inherit') ? pos.width : width
206			});
207		}
208		if (!first) return;
209		if (!this.visible) {
210			this.visible = true;
211			this.choices.setStyle('display', '');
212			if (this.fx) this.fx.start(1);
213			this.fireEvent('onShow', [this.element, this.choices]);
214		}
215		if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead);
216		var items = this.choices.getChildren(match),
217			max = this.options.maxChoices;
218		var styles = {
219			'overflowY': 'hidden',
220			'height': ''
221		};
222		this.overflown = false;
223		if (items.length > max) {
224			var item = items[max - 1];
225			styles.overflowY = 'scroll';
226			styles.height = item.getCoordinates(this.choices).bottom;
227			this.overflown = true;
228		};
229		this.choices.setStyles(styles);
230		this.fix.show();
231		if (this.options.visibleChoices) {
232			var scroll = document.getScroll(),
233				size = document.getSize(),
234				coords = this.choices.getCoordinates();
235			if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x;
236			if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y;
237			window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top));
238		}
239	},
240	// TODO: No $arguments in MT 1.3
241	hideChoices: function (clear) {
242		if (clear) {
243			var value = this.element.value;
244			if (this.options.forceSelect) value = this.opted;
245			if (this.options.autoTrim) {
246				value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator);
247			}
248			this.observer.setValue(value);
249		}
250		if (!this.visible) return;
251		this.visible = false;
252		if (this.selected) this.selected.removeClass('autocompleter-selected');
253		this.observer.clear();
254		var hide = function () {
255				this.choices.setStyle('display', 'none');
256				this.fix.hide();
257			}.bind(this);
258		if (this.fx) this.fx.start(0).chain(hide);
259		else hide();
260		this.fireEvent('onHide', [this.element, this.choices]);
261	},
262	prefetch: function () {
263		var value = this.element.value,
264			query = value;
265		if (this.options.multiple) {
266			var split = this.options.separatorSplit;
267			var values = value.split(split);
268			var index = this.element.getSelectedRange().start;
269			var toIndex = value.substr(0, index).split(split);
270			var last = toIndex.length - 1;
271			index -= toIndex[last].length;
272			query = values[last];
273		}
274		if (query.length < this.options.minLength) {
275			this.hideChoices();
276		} else {
277			if (query === this.queryValue || (this.visible && query == this.selectedValue)) {
278				if (this.visible) return false;
279				this.showChoices();
280			} else {
281				this.queryValue = query;
282				this.queryIndex = index;
283				if (!this.fetchCached()) this.query();
284			}
285		}
286		return true;
287	},
288	fetchCached: function () {
289		return false;
290		if (!this.options.cache || !this.cached || !this.cached.length || this.cached.length >= this.options.maxChoices || this.queryValue) return false;
291		this.update(this.filter(this.cached));
292		return true;
293	},
294	update: function (tokens) {
295		this.choices.empty();
296		this.cached = tokens;
297		var type = tokens && typeOf(tokens);
298		if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())) {
299			(this.options.emptyChoices || this.hideChoices).call(this);
300		} else {
301			if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices;
302			tokens.each(this.options.injectChoice ||
303			function (token) {
304				var choice = new Element('li', {
305					'html': this.markQueryValue(token)
306				});
307				choice.inputValue = token;
308				this.addChoiceEvents(choice).inject(this.choices);
309			}, this);
310			this.showChoices();
311		}
312	},
313	choiceOver: function (choice, selection) {
314		if (!choice || choice == this.selected) return;
315		if (this.selected) this.selected.removeClass('autocompleter-selected');
316		this.selected = choice.addClass('autocompleter-selected');
317		this.fireEvent('onSelect', [this.element, this.selected, selection]);
318		if (!this.selectMode) this.opted = this.element.value;
319		if (!selection) return;
320		this.selectedValue = this.selected.inputValue;
321		if (this.overflown) {
322			var coords = this.selected.getCoordinates(this.choices),
323				margin = this.options.overflowMargin,
324				top = this.choices.scrollTop,
325				height = this.choices.offsetHeight,
326				bottom = top + height;
327			if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0);
328			else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom);
329		}
330		if (this.selectMode) this.setSelection();
331	},
332	choiceSelect: function (choice) {
333		if (choice) this.choiceOver(choice);
334		this.setSelection(true);
335		this.queryValue = false;
336		this.hideChoices();
337	},
338	filter: function (tokens) {
339		return (tokens || this.tokens).filter(function (token) {
340			return this.test(token);
341		}, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i'));
342	},
343	markQueryValue: function (str) {
344		return (!this.options.markQuery || !this.queryValue) ? str : str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '<span class="autocompleter-queried">$1</span>');
345	},
346	addChoiceEvents: function (el) {
347		return el.addEvents({
348			'mouseover': this.choiceOver.bind(this, el),
349			'click': this.choiceSelect.bind(this, el)
350		});
351	}
352});
353var OverlayFix = new Class({
354	initialize: function (el) {
355		if (Browser.ie) {
356			this.element = document.id(el);
357			this.relative = this.element.getOffsetParent();
358			this.fix = new Element('iframe', {
359				'frameborder': '0',
360				'scrolling': 'no',
361				'src': 'javascript:false;',
362				'styles': {
363					'position': 'absolute',
364					'border': 'none',
365					'display': 'none',
366					'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
367				}
368			}).inject(this.element, 'after');
369		}
370	},
371	show: function () {
372		if (this.fix) {
373			var coords = this.element.getCoordinates(this.relative);
374			delete coords.right;
375			delete coords.bottom;
376			this.fix.setStyles(Object.append(coords, {
377				'display': '',
378				'zIndex': (this.element.getStyle('zIndex') || 1) - 1
379			}));
380		}
381		return this;
382	},
383	hide: function () {
384		if (this.fix) this.fix.setStyle('display', 'none');
385		return this;
386	},
387	destroy: function () {
388		if (this.fix) this.fix = this.fix.destroy();
389	}
390});
391Element.implement({
392	getSelectedRange: function () {
393		if (!Browser.ie) return {
394			start: this.selectionStart,
395			end: this.selectionEnd
396		};
397		var pos = {
398			start: 0,
399			end: 0
400		};
401		var range = this.getDocument().selection.createRange();
402		if (!range || range.parentElement() != this) return pos;
403		var dup = range.duplicate();
404		if (this.type == 'text') {
405			pos.start = 0 - dup.moveStart('character', -100000);
406			pos.end = pos.start + range.text.length;
407		} else {
408			var value = this.value;
409			var offset = value.length - value.match(/[\n\r]*$/)[0].length;
410			dup.moveToElementText(this);
411			dup.setEndPoint('StartToEnd', range);
412			pos.end = offset - dup.text.length;
413			dup.setEndPoint('StartToStart', range);
414			pos.start = offset - dup.text.length;
415		}
416		return pos;
417	},
418	selectRange: function (start, end) {
419		if (Browser.ie) {
420			var diff = this.value.substr(start, end - start).replace(/\r/g, '').length;
421			start = this.value.substr(0, start).replace(/\r/g, '').length;
422			var range = this.createTextRange();
423			range.collapse(true);
424			range.moveEnd('character', start + diff);
425			range.moveStart('character', start);
426			range.select();
427		} else {
428			this.focus();
429			this.setSelectionRange(start, end);
430		}
431		return this;
432	}
433});
434Autocompleter.Base = Autocompleter;
435Autocompleter.Request = new Class({
436	Extends: Autocompleter,
437	options: {
438		postData: {},
439		ajaxOptions: {},
440		postVar: 'value'
441	},
442	query: function () {
443		var data = this.options.postData.unlink || {};
444		data[this.options.postVar] = this.queryValue;
445		var indicator = document.id(this.options.indicator);
446		if (indicator) indicator.setStyle('display', '');
447		var cls = this.options.indicatorClass;
448		if (cls) this.element.addClass(cls);
449		this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]);
450		this.request.send({
451			'data': data
452		});
453	},
454	queryResponse: function () {
455		var indicator = document.id(this.options.indicator);
456		if (indicator) indicator.setStyle('display', 'none');
457		var cls = this.options.indicatorClass;
458		if (cls) this.element.removeClass(cls);
459		return this.fireEvent('onComplete', [this.element, this.request]);
460	}
461});
462Autocompleter.Request.JSON = new Class({
463	Extends: Autocompleter.Request,
464	initialize: function (el, url, options) {
465		this.parent(el, options);
466		this.request = new Request.JSON(Object.merge({}, {
467			'url': url,
468			'link': 'cancel'
469		}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
470	},
471	queryResponse: function (response) {
472		this.parent();
473		this.update(response);
474	}
475});
476Autocompleter.Request.HTML = new Class({
477	Extends: Autocompleter.Request,
478	initialize: function (el, url, options) {
479		this.parent(el, options);
480		this.request = new Request.HTML(Object.merge({}, {
481			'url': url,
482			'link': 'cancel',
483			'update': this.choices
484		}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
485	},
486	queryResponse: function (tree, elements) {
487		this.parent();
488		if (!elements || !elements.length) {
489			this.hideChoices();
490		} else {
491			this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice ||
492			function (choice) {
493				var value = choice.innerHTML;
494				choice.inputValue = value;
495				this.addChoiceEvents(choice.set('html', this.markQueryValue(value)));
496			}, this);
497			this.showChoices();
498		}
499	}
500});
501Autocompleter.Ajax = {
502	Base: Autocompleter.Request,
503	Json: Autocompleter.Request.JSON,
504	Xhtml: Autocompleter.Request.HTML
505};

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.