﻿/**
 * Autocompleter
 *
 * @version		1.0rc4
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */
var Autocompleter = {};

Autocompleter.Base = new Class({

	options: {
		minLength: 1,
		useSelection: true,
		markQuery: true,
		inheritWidth: true,
		maxChoices: 10,
		injectChoice: null,
		onSelect: Class.empty,
		onShow: Class.empty,
		onHide: Class.empty,
		customTarget: null,
		className: 'autocompleter-choices',
		zIndex: 42,
		observerOptions: {},
		fxOptions: {},
		overflown: []
	},

	initialize: function(el, options) {
		this.setOptions(options);
		this.element = $(el);
		this.build();
		this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({
			delay: 400
		}, this.options.observerOptions));
		this.value = this.observer.value;
		this.queryValue = null;
	},

	/**
	 * build - Initialize DOM
	 *
	 * Builds the html structure for choices and appends the events to the element.
	 * Override this function to modify the html generation.
	 */
	build: function() {
		if ($(this.options.customTarget)) this.choices = this.options.customTarget;
		else {
			this.choices = new Element('ul', {
				'class': this.options.className,
				styles: {zIndex: this.options.zIndex}
			}).injectInside(document.body);
			this.fix = new OverlayFix(this.choices);
		}
		this.fx = this.choices.effect('opacity', $merge({
			wait: false,
			duration: 200
		}, this.options.fxOptions))
			.addEvent('onStart', function() {
				if (this.fx.now) return;
				this.choices.setStyle('display', '');
				this.fix.show();
			}.bind(this))
			.addEvent('onComplete', function() {
				if (this.fx.now) return;
				this.choices.setStyle('display', 'none');
				this.fix.hide();
			}.bind(this)).set(0);
		this.element.setProperty('autocomplete', 'off')
			.addEvent(window.ie ? 'keydown' : 'keypress', this.onCommand.bindWithEvent(this))
			.addEvent('mousedown', this.onCommand.bindWithEvent(this, [true]))
			.addEvent('focus', this.toggleFocus.bind(this, [true]))
			.addEvent('blur', this.toggleFocus.bind(this, [false]))
			.addEvent('trash', this.destroy.bind(this));
	},

	destroy: function() {
		this.choices.remove();
	},

	toggleFocus: function(state) {
		this.focussed = state;
		if (!state) this.hideChoices();
	},

	onCommand: function(e, mouse) {
		if (mouse && this.focussed) this.prefetch();
		if (e.key && !e.shift) switch (e.key) {
			case 'enter':
				if (this.selected && this.visible) {
					this.choiceSelect(this.selected);
					e.stop();
				} return;
			case 'up': case 'down':
				if (this.observer.value != (this.value || this.queryValue)) this.prefetch();
				else if (this.queryValue === null) break;
				else if (!this.visible) this.showChoices();
				else {
					this.choiceOver((e.key == 'up')
						? this.selected.getPrevious() || this.choices.getLast()
						: this.selected.getNext() || this.choices.getFirst() );
					this.setSelection();
				}
				e.stop(); return;
			case 'esc': this.hideChoices(); return;
		}
		this.value = false;
	},

	setSelection: function() {
		if (!this.options.useSelection) return;
		var startLength = this.queryValue.length;
		if (this.element.value.indexOf(this.queryValue) != 0) return;
		var insert = this.selected.inputValue.substr(startLength);
		if (document.getSelection) {
			this.element.value = this.queryValue + insert;
			this.element.selectionStart = startLength;
			this.element.selectionEnd = this.element.value.length;
		} else if (document.selection) {
			var sel = document.selection.createRange();
			sel.text = insert;
			sel.move("character", - insert.length);
			sel.findText(insert);
			sel.select();
		}
		this.value = this.observer.value = this.element.value;
	},

	hideChoices: function() {
		if (!this.visible) return;
		this.visible = this.value = false;
		this.observer.clear();
		this.fx.start(0);
		this.fireEvent('onHide', [this.element, this.choices]);
	},

	showChoices: function() {
		if (this.visible || !this.choices.getFirst()) return;
		this.visible = true;
		var pos = this.element.getCoordinates(this.options.overflown);
		this.choices.setStyles({
			left: pos.left,
			top: pos.bottom		
		});
		//alert(pos.left + " - " + pos.bottom);
		if (this.options.inheritWidth) this.choices.setStyle('width', pos.width);
		this.fx.start(1);
		this.choiceOver(this.choices.getFirst());
		this.fireEvent('onShow', [this.element, this.choices]);
	},

	prefetch: function() {
		if (this.element.value.length < this.options.minLength) this.hideChoices();
		else if (this.element.value == this.queryValue) this.showChoices();
		else this.query();
	},

	updateChoices: function(choices) {
		this.choices.empty();
		this.selected = null;
		if (!choices || !choices.length) return;
		if (this.options.maxChoices < choices.length) choices.length = this.options.maxChoices;
		choices.each(this.options.injectChoice || function(choice, i){
			alert(this.markQueryValue(choice));
			var el = new Element('li').setHTML(this.markQueryValue(choice));
			el.inputValue = choice;
			this.addChoiceEvents(el).injectInside(this.choices);
		}, this);
		this.showChoices();
	},

	choiceOver: function(el) {
		if (this.selected) this.selected.removeClass('autocompleter-selected');
		this.selected = el.addClass('autocompleter-selected');
	},

	choiceSelect: function(el) {
		this.observer.value = this.element.value = el.inputValue;
//		alert(el.inputValue);
		this.hideChoices();
		this.fireEvent('onSelect', [this.element], 20);
	},

	/**
	 * markQueryValue
	 *
	 * Marks the queried word in the given string with <span class="autocompleter-queried">*</span>
	 * Call this i.e. from your custom parseChoices, same for addChoiceEvents
	 *
	 * @param		{String} Text
	 * @return		{String} Text
	 */
	markQueryValue: function(txt) {
		return (this.options.markQuery && this.queryValue) ? txt.replace(new RegExp('^(' + this.queryValue.escapeRegExp() + ')', 'i'), '<span class="autocompleter-queried">$1</span>') : txt;
	},

	/**
	 * addChoiceEvents
	 *
	 * Appends the needed event handlers for a choice-entry to the given element.
	 *
	 * @param		{Element} Choice entry
	 * @return		{Element} Choice entry
	 */
	addChoiceEvents: function(el) {
		return el.addEvents({
			mouseover: this.choiceOver.bind(this, [el]),
			mousedown: this.choiceSelect.bind(this, [el])
		});
	}
});

Autocompleter.Base.implement(new Events);
Autocompleter.Base.implement(new Options);

Autocompleter.Local = Autocompleter.Base.extend({

	options: {
		minLength: 0,
		filterTokens : null
	},

	initialize: function(el, tokens, options) {
		this.parent(el, options);
		this.tokens = tokens;
		if (this.options.filterTokens) this.filterTokens = this.options.filterTokens.bind(this);
	},

	query: function() {
		this.hideChoices();
		this.queryValue = this.element.value;
		this.updateChoices(this.filterTokens());
	},

	filterTokens: function(token) {
		var regex = new RegExp('^' + this.queryValue.escapeRegExp(), 'i');
		return this.tokens.filter(function(token) {
			return regex.test(token);
		});
	}

});

Autocompleter.Ajax = {};

Autocompleter.Ajax.Base = Autocompleter.Base.extend({

	options: {
		postVar: 'value',
		postData: {},
		ajaxOptions: {},
		onRequest: Class.empty,
		onSuccess: Class.empty
	},

	initialize: function(el, url, options) {
		this.parent(el, options);
		this.url = url;
		this.ajax = new XHR({
			 method:"GET",
			 async:false
			 });
		this.ajax.addEvent('onSuccess', this.queryResponse.bind(this));
		this.ajax.addEvent('onFailure', this.queryResponse.bind(this, [false]));
	},

	query: function(){
		this.data = $extend({}, this.options.postData);
		this.data[this.options.postVar] = this.element.value;
		this.fireEvent('onRequest', [this.element, this.ajax]);
		//this.ajax.send(this.url, this.data);
		// 還是自己手動組好 URL 比較安心
		this.queryString = '?' + this.options.postVar + '=' + this.element.value;
		this.queryString = this.queryString + '&' + this.options.queryString + '&field=' + this.options.field;
//		alert(encodeURI(this.url+this.queryString));
//		document.write(encodeURI(this.url+this.queryString));
		this.ajax.send(encodeURI(this.url+this.queryString), "");
	},

	/**
	 * queryResponse - abstract
	 *
	 * Inherated classes have to extend this function and use this.parent(resp)
	 *
	 * @param		{String} Response
	 */
	queryResponse: function(resp) {
		this.value = this.queryValue = this.element.value;
		this.selected = false;
		this.hideChoices();
		this.fireEvent(resp ? 'onSuccess' : 'onFailure', [this.element, this.ajax], 20);
	}

});


Autocompleter.Ajax.XML = Autocompleter.Ajax.Base.extend({

	options: {
		parseChoices: null,
		markQuery: true
	},

	queryResponse: function(resp) {
		this.parent(resp);
		if (!resp) return;

		this.results = "";
		
		// code for IE
		if (window.ActiveXObject)
		{
			this.xml = new ActiveXObject("Microsoft.XMLDOM");
			this.xml.async = false;
			this.xml.loadXML(this.ajax.response.text);
		}
		else
		{
			this.parser = new DOMParser();
			this.xml = this.parser.parseFromString(this.ajax.response.text, 'text/xml');
		}
		
//		alert(this.xml.xml);
		// 抓最長字數
		var l_str = 0;
		
		var keyword = this.xml.getElementsByTagName('keyword');
		
		// 先做個集合
		var resultSet = new Array(keyword.length+1);
		var keySet = new Array(keyword.length+1);
		
		for(var i=0; i<keyword.length; i++)
		{
			var key = keyword[i].getAttribute('name');
			var freq = keyword[i].getAttribute('queryCount');
			var count = keyword[i].getAttribute('resultCount');
			// 將 0~1 的數值轉成整數顯示 bar 的長度
			var len = parseInt(parseFloat(freq) * 100);

			if(key.length > l_str)
				l_str = key.length;		
			
			// %X 指為 bar 的起始點
			// %C 指為 bar 的顏色
			// %D 指為筆數的顯示字串樣版
			// 若有被設定 defName 是 Hot Keyword 時, 就只顯示 keyword
			if(!this.options.isHotKeyWord)
				{
					if((count == null || freq == null) && (count == '' || freq == ''))
					{
						resultSet[i] = "<li><span>" + key + "</span></li>";
					}
					else if( (count == null || count == '') && (freq != null || freq != ''))
					{
						resultSet[i] = "<li><span>" + key + "</span><div class=\"kwick\" style=\"width:" + len + ";left:%Xpx;background-color: %C;\" >" + "</div></li>";
					}
					else if( (count != null || count != '') && (freq == null || freq == ''))
					{
						resultSet[i] = "<li><span>" + key + "</span><span class=\"example-info\">" + count + " %D</span></li>";
					}
					else
					{
						resultSet[i] = "<li><span>" + key + "</span><div class=\"kwick\" style=\"width:" + len + ";left:%Xpx;background-color: %C;\" >" + "</div><span class=\"example-info\">" + count + " %D</span></li>";
					}
				}
			else
				resultSet[i] = "<li><span>" + key + "</span></li>";
				
			keySet[i] = key.length;
		}

		for(var j=0; j<keyword.length; j++)
		{
			var count = keyword[j].getAttribute('resultCount');
			
			var tempStr = resultSet[j];
			tempStr = tempStr.replace("%X", l_str*15);
			tempStr = tempStr.replace("%C", this.options.color);
			tempStr = tempStr.replace("%D", this.options.countString);
			
			if(this.options.noneFilter!="Y" || (this.options.noneFilter=="Y" && count>0))
			{
				this.results = this.results + tempStr;
			}
			
			//alert(this.results);
		}
		
		this.choices.setHTML(this.results).getChildren().each(this.options.parseChoices || this.parseChoices, this);
		this.showChoices();
	},

	parseChoices: function(el) {
		var value = el.innerHTML;
		el.inputValue = value;
		el.setHTML(this.markQueryValue(value));
	}

});


var OverlayFix = new Class({

	initialize: function(el) {
		this.element = $(el);
		if (window.ie){
			this.element.addEvent('trash', this.destroy.bind(this));
			this.fix = new Element('iframe', {
				properties: {
					frameborder: '0',
					scrolling: 'no',
					src: 'javascript:false;'
				},
				styles: {
					position: 'absolute',
					border: 'none',
					display: 'none',
					filter: 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
				}
			}).injectAfter(this.element);
		}
	},

	show: function() {
		if (this.fix) this.fix.setStyles($extend(
			this.element.getCoordinates(), {
				display: '',
				zIndex: (this.element.getStyle('zIndex') || 1) - 1
			}));
		return this;
	},

	hide: function() {
		if (this.fix) this.fix.setStyle('display', 'none');
		return this;
	},

	destroy: function() {
		this.fix.remove();
	}

});