﻿/**
 * Autocompleter
 *
 * @version		1.0rc4
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 *
 * @changed-by Elad Ossadon
 */
var Autocompleter={};

Autocompleter.Base = new Class({

    Implements: [Events, Options],

    options: {
        minLength: 1,
        useSelection: true,
        markQuery: true,
        inheritWidth: true,
        maxChoices: 10,
        injectChoice: null,
        onSelect: Class.empty,
        onShow: Class.empty,
        onHide: Class.empty,
        customTarget: null,
        inputClassName: "autocompleter-input",
        choicesClassName: "autocompleter-choices",
        choicesWrapClassName: "autocompleter-choices-wrap",
        loadingClassName: "autocompleter-loading",
        zIndex: 100,
        observerOptions: {},
        fxOptions: {},
        findMode: "contains" // or "starts"
    },

    initialize: function(el, options) {
        this.setOptions(options);
        this.element = $(el);
        this.element.addClass(this.options.inputClassName);
        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.choicesClassName,
                styles: { zIndex: this.options.zIndex }
            }).inject(document.body);
            this.fix = new OverlayFix(this.choices);
        }
        this.choicesWrap =
			new Element("div", {
			    "class": this.options.choicesWrapClassName,
			    styles: { zIndex: this.options.zIndex }
			})
			.wraps(this.choices)
			.hide();
        this.fx = this.choices
			.effects($merge({
			    wait: false,
			    duration: 400
			}, this.options.fxOptions))
			.addEvent("onStart", function() {
			    if (this.fx.hasFinishedSlidingIn()) return;
			    this.fix.show();
			} .bind(this))
			.addEvent("onComplete", function() {
			    if (this.fx.hasFinishedSlidingIn()) return;
			    this.choicesWrap.hide();
			    this.fix.hide();
			} .bind(this));

        this.fx.hasFinishedSlidingIn = function() {
            return this.to.top[0].value == 0;
        };
        this.fx.slideIn = function() {
            this.choicesWrap.show();
            var pos = this.element.getCoordinates();
            this.choicesWrap.setStyles({
                left: pos.left,
                top: pos.bottom,
                overflow: "hidden",
                height: this.choices.offsetHeight,
                width: this.choices.offsetWidth
                // borders not included in the offsetWidth
					+ this.choices.getStyle("border-left-width").toInt()
					+ this.choices.getStyle("border-right-width").toInt()
            });
            this.choices.setStyles({ top: -this.choices.offsetHeight });
            this.fx.start({ top: 0 });
        } .bind(this);

        this.fx.slideOut = function() {
            this.fx.start({ top: -this.choices.offsetHeight });
        } .bind(this);

        this.element.setProperty("autocomplete", "off")
			.addEvent(Browser.Engine.trident ? "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.dispose();
    },

    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 && insert.length) { // && insert.length added by elado
            /*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.slideOut();
        this.fireEvent("onHide", [this.element, this.choices]);
    },

    showChoices: function() {
        if (this.visible || !this.choices.getFirst()) return;
        this.visible = true;
        if (this.options.inheritWidth) this.choices.setStyle("width",
			this.element.offsetWidth
			- this.choices.getStyle("border-left-width").toInt()
			- this.choices.getStyle("border-right-width").toInt()
		);
        this.fx.slideIn();
        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) {
            var el = new Element("li").set("html", this.markQueryValue(choice));
            el.inputValue = choice;
            el.store('inputValue', choice);
            this.addChoiceEvents(el).inject(this.choices);
        }, this);
        this.showChoices();
    },

    choiceOver: function(el) {
    if (typeof (el.length) != 'undefined')
        el = el[0];
        if (this.selected) this.selected.removeClass("autocompleter-selected");
        this.selected = el.addClass("autocompleter-selected");
    },

    choiceSelect: function(el) {
        if (typeof (el.length) != 'undefined')
            el = el[0];
        var iptValue = el.retrieve('inputValue');
        this.observer.value = this.element.value = iptValue; //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.options.findMode == "contains" ? "" : "^") + "(" + this.queryValue.escapeRegExp() + ")", "ig"), "<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.Local=new Class({
	Extends:Autocompleter.Base,

	options:{
		minLength:0,
		filterTokens :null
	},

	initialize:function(el,tokens,options) {
		arguments.callee.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=new Class({
	Extends:Autocompleter.Base,

	options:{
		postVar:"value",
		postData:{},
		ajaxOptions:{},
		onRequest:Class.empty,
		onComplete:Class.empty
	},

	initialize:function(el,url,options) {
		arguments.callee.parent(el,options);
		this.ajax=new Request.JSON($merge({
			url:url,
			autoCancel:true
		},this.options.ajaxOptions));
		this.ajax
			.addEvent("onComplete",this.queryResponse.bind(this))
			.addEvent("onFailure",this.queryResponse.bind(this,[false]));
	},

	query:function(){
		var data=$extend({},this.options.postData);
		data[this.options.postVar]=this.element.value;
		this.element.addClass(this.options.loadingClassName);
		this.fireEvent("onRequest",[this.element,this.ajax]);
		this.ajax.cancel();
		this.ajax.send({data:data});
	},

	/**
	 * queryResponse - abstract
	 *
	 * Inherated classes have to extend this function and use arguments.callee.parent(resp)
	 *
	 * @param		{String} Response
	 */
	queryResponse:function(resp) {
		this.value=this.queryValue=this.element.value;
		this.selected=false;
		this.hideChoices();
		this.element.removeClass(this.options.loadingClassName);
		this.fireEvent(resp ? "onComplete" :"onFailure",[this.element,this.ajax],20);
	}

});

Autocompleter.Ajax.Json=new Class({
	Extends:Autocompleter.Ajax.Base,

	queryResponse:function(resp) {
		arguments.callee.parent(resp);
		var choices=resp;
		if (!choices || !choices.length) return;
		this.updateChoices(choices);
	}

});


Autocompleter.Ajax.Xhtml=new Class({
	Extends:Autocompleter.Ajax.Base,

	options:{
		parseChoices:null
	},

	queryResponse:function(resp) {
		arguments.callee.parent(resp);
		if (!resp) return;
		this.choices.set("html",resp).getChildren().each(this.options.parseChoices || this.parseChoices,this);
		this.showChoices();
	},

	parseChoices:function(el) {
		var value=el.innerHTML;
		el.inputValue=value;
		el.set("html",this.markQueryValue(value));
	}

});



/**
 * Observer - Observe formelements for changes
 *
 * @version		1.0rc1
 *
 * @license		MIT-style license
 * @author		Harald Kirschner <mail [at] digitarald.de>
 * @copyright	Author
 */
var Observer=new Class({
	Implements:[Options,Events],

	options:{
		periodical:false,
		delay:1000
	},

	initialize:function(el,onFired,options){
		this.setOptions(options);
		this.addEvent("onFired",onFired);
		this.element=$(el);
		this.listener=this.fired.bind(this);
		this.value=this.element.get("value");
		if (this.options.periodical) this.timer=this.listener.periodical(this.options.periodical);
		else this.element.addEvent("keyup",this.listener);
	},

	fired:function() {
		var value=this.element.get("value");
		if (this.value==value) return;
		this.clear();
		this.value=value;
		this.timeout=this.fireEvent.delay(this.options.delay,this,["onFired",[value]]);
	},

	clear:function() {
		$clear(this.timeout);
		return this;
	}
});
