
/*
 * Public
 * @desc: Classe UIElement, cria um objeto que funciona como um 'wrap' de um elemento com controlador 
 * de eventos e extende as funcionalidades do elemento com a Classe Element do Mootools.
 */
var UIElement = new Class({
	initialize: function(element)
	{
		this._element = new Element(element);
	},
	
	getElement: function()
	{
		return this._element;
	}
});
UIElement.implement(new Events());

/*
 * Public
 * @desc: Classe CustomCombo, cria um combo customizável por css atravéz de um existente 
 * objeto <select> mantendo os dados e os estilos definidos para o mesmo
 */
var CustomCombo = UIElement.extend({

    /*
    * Public / Readonly
    * @desc: Número de opção disponível na lista
    * @tipo: Number
    */
    listSize: 1,

    selectElement: null,

    className: "",

    /*
    * Private
    * @desc: Array dos itens de opções
    * @tipo: Array
    */
    _items: null,

    /*
    * Public / Readonly
    * @desc: Número de opção disponível na lista
    * @tipo: Number
    */
    selectedIndex: 0,

    /*
    * Private
    * @desc: String de busca pelo teclado
    * @tipo: String
    */
    _searchString: "",

    /*
    * Private
    * @desc: Id de timeout da busca
    * @tipo: Number
    */
    _timeoutSearch: null,

    /*
    * Public
    * @desc: Funcao construtora, inicializa os elementos pertinentes <input> e <div> 
    * formatando as propriedades a partir do elemento <select> e a definição dos 
    * eventos a serem monitorados
    * @param: element - Elemento <Select>
    */
    initialize: function(p_element, p_size, p_class) {
        if (!CustomCombo.elements) CustomCombo.elements = [];
        if (p_element.tagName.toLowerCase() != "select") return false;
        this.parent(p_element);
        this._items = new Array();
        var el = this.getElement();
        var size = el.getProperty("size") ? el.getProperty("size").toInt() : p_size || this.listSize;
        this.listSize = Math.max(size, this.listSize);
        this.selectedIndex = Math.max(0, el.selectedIndex);
        this.className = p_class || el.getProperty("class") || "";
        el.selectedIndex = this.selectedIndex;
        el.customComboInstance = this;
        var w = this.getOriginalWidth(el);
        this.input = new Element('input', {
            'styles': {
                cursor: 'default'
            },
            'events': {
                'click': this.open.bindWithEvent(this),
                'keydown': this.keys.bindWithEvent(this),
                'focus': this.open.bindWithEvent(this)
            },
            'class': "CustomComboSelector",
            'name': "CustomComboSelector_" + el.getProperty("name"),
            'id': "CustomComboSelector_" + el.getProperty("id"),
            'readonly': 'readonly',
            'value': '',
            'tabindex': el.getProperty('tabindex') || ''
        });
        document.createStyle(".CustomComboSelector", {
            "border": "1px solid black",
            'background-color': '#ffffff',
            'width': w + 'px'
        });
        var evt1 = function(p_event) { p_event.target.addClass("CustomComboSelector_on"); } .bindWithEvent();
        var evt2 = function(p_event) { p_event.target.removeClass("CustomComboSelector_on"); } .bindWithEvent();
        this.input.addEvents({
            mouseenter: evt1,
            mouseleave: evt2,
            focus: evt1,
            blur: evt2
        });
        this.list = new Element('div', {
            'styles': {
                position: 'absolute',
                overflow: 'auto',
                visibility: 'hidden',
                'z-index': '9999'
            },
            'class': "CustomComboList",
            'id': "CustomComboList_" + el.getProperty("id")
        });
        document.createStyle(".CustomComboList", {
            'border': '1px solid black',
            'background-color': '#ffffff',
            'width': (w + 2) + 'px'
        });
        CustomCombo.elements.push(this);
        this.container = new Element("div", { "class": this.className });
        this.container.adopt(this.input);
        this.containerList = new Element("div", { "class": this.className });
        this.containerList.adopt(this.list);
        $(document.body).addEvent('click', function(p_event) {
            var kl = $(p_event.target).getProperty("class") || "";
            if (kl.indexOf("CustomCombo") == -1) this.close();
        } .bindWithEvent(this));
        window.addEvent('resize', function() {
            this.list.setStyles({
                left: this.input.getLeft(),
                top: (this.input.getTop() + this.input.getSize().size.y)
            });
        } .bind(this));
        this.draw();
    },

    getOriginalWidth: function(p_elem) {
        var w = Math.max(p_elem.getSize().size.x, p_elem.getStyle("width").toInt());
        if (w == 0) {
            var parent = p_elem.getParent();
            while (parent.getStyle("display") == "block" && parent != document.body) {
                parent = parent.getParent();
            }
            if (parent.getStyle("display") == "none") {
                parent.setStyle("display", "block");
                w = Math.max(p_elem.getSize().size.x, p_elem.getStyle("width").toInt());
                parent.setStyle("display", "none");
            }
        }
        return w;
    },

    /*
    * Private
    * @desc: Método responsável pela criacao dos elementos a partir do elemento <select> e faz a estruturação gráfica
    * @param: void
    * @return: void
    */
    draw: function() {
        var el = this.getElement();
        el.setStyle("display", "none");
        this.container.injectBefore(el);
        this.containerList.injectInside(document.body);
        el.getChildren().each(function(p_item, p_index) {
            var option = $(p_item);
            this.addOption(option.getText(), option.getProperty("value"), option.selected || false);
        } .bind(this));
        this.list.setStyle("width", Math.max(this.input.getSize().size.x, this.list.getSize().size.x))

        //verifica se tem itens no combo
        if (this._items.length > 0) {
            this.input.setProperty("value", this.getSelectedItem().label);
        }

        this.updateList();
        this.close();
    },

    /*
    * Public
    * @desc: Adiciona uma opção à lista
    * @param: p_label:String <Texto da opção>, p_value:String <Valor da opção>, p_selected:Boolean <Atributo para definir o estado de selecionado da opção>
    * @return: void
    */
    addOption: function(p_label, p_value, p_selected) {
        var el = this.getElement();
        var option = new Element('div', {
            'styles': {
                cursor: 'default'
            },
            'class': 'CustomComboItem',
            'id': "CustomComboItem_" + el.getProperty("id") + "_" + this._items.length
        });
        option.setProperty('index', this._items.length);
        option.setText(p_label);
        option.addEvents({
            mouseenter: this.itemRollOver.bindWithEvent(this),
            mouseleave: this.itemRollOut.bindWithEvent(this),
            click: this.itemRelease.bindWithEvent(this)
        });
        if (p_selected) this.selectedIndex = this._items.length;
        this.list.adopt(option);
        this._items.push({ label: p_label, value: p_value, element: option });
    },

    /*
    * 
    */
    updateList: function() {
        this.list.setStyles({
            left: this.container.getLeft(),
            top: (this.container.getTop() + this.container.getSize().size.y - 1)
        });
        if (this.listSize > 1) {
            this.list.setStyle('height', this.listSize * this.list.getFirst().getSize().size.y);
        }
    },

    /*
    * Public
    * @desc: Seleciona uma opção da lista
    * @param: p_index:Number <Índice da opção>, p_trigger:Boolean <Atributo opcional para não haver o disparo do evento 'change' do objeto, padrao:false>
    * @return: void
    */
    selectOption: function(p_index, p_trigger) {
        var el = this.getElement();

        this._items.each(function(p_item, p_index) {
            p_item.element.removeClass('CustomComboItem_on');
        });
        this.selectedIndex = p_index;
        var now = this.getSelectedItem();
        now.element.addClass('CustomComboItem_on');
        this.input.setProperty("value", now.label);
        el.selectedIndex = this.selectedIndex;
        this.input.focus();

        if (el.onchange != null) {
            el.onchange();
        }
        


        if (!p_trigger) this.fireEvent('change');
    },

    /*
    * Public
    * @desc: Faz o posicionamento do scroll de acordo com o item selecionado
    * @param: void
    */
    scrollPolicy: function() {
        var selected = this.getSelectedItem().element;
        var height = selected.getSize().size.y;
        var top = selected.offsetTop;
        var bottom = (top + height);
        var sizes = this.list.getSize();
        var minY = sizes.scroll.y;
        var maxY = minY + sizes.size.y;
        if (bottom > maxY) this.list.scrollTo(0, ((bottom - maxY) + minY) + 2);
        else if (top < minY) this.list.scrollTo(0, top);
    },

    /*
    * Public
    * @desc: Obter item selecionado
    * @param: void
    */
    getListLabels: function() {
        var a = [];
        this._items.each(function(p_item, p_index) {
            a.push(p_item.label);
        });
        return a;
    },

    /*
    * Public
    * @desc: Obtêm item selecionado
    * @param: void
    * @return: Objeto > {label:<Texto da Opção>, value:<Valor da Opção>, className:<Nome da classe padrão>, element:<Objeto do Elemento>}
    */
    getSelectedItem: function() {
        return this._items[this.selectedIndex];
    },

    /*
    * Public
    * @desc: Abre lista de opções
    * @param: void
    */
    open: function(p_event) {
        var selected = this.getSelectedItem();
        selected.element.addClass("CustomComboItem_on");
        this.input.addClass("CustomComboSelector_open");
        this.list.setStyles({
            display: 'block',
            visibility: 'visible'
        });
        this.updateList();
        CustomCombo.elements.each(function(p_item) {
            if (p_item != this) p_item.close();
        } .bind(this));
    },

    /*
    * Public
    * @desc: Fechar lista de opções
    * @param: void
    * @return: void
    */
    close: function(p_event) {
        this.list.setStyle("display", "none");
        this.input.removeClass("CustomComboSelector_open")
    },

    /*
    * Private
    * @desc: Monitora o evento 'keydown'
    * @param: p_event:Object
    * @return: void
    */
    keys: function(p_event) {
        //Pega o código da tecla e delega uma ação para cada
        switch (p_event.code) {
            /*
            * Caso for a tecla seta para baixo e seta para direta seleciona o próximo item
            * após o selecionado e posiciona o scroll
            */ 
            case 40: case 39:
                this.selectOption(Math.min(this._items.length - 1, this.selectedIndex + 1));
                this.scrollPolicy();
                break;
            /*
            * Caso for a tecla seta para cima e seta para esquerda seleciona o item anterior
            * ao selecionado e posiciona o scroll
            */ 
            case 38: case 37:
                this.selectOption(Math.max(0, this.selectedIndex - 1));
                this.scrollPolicy();
                break;
            /*
            * Caso for a tecla tab e enter fecha a lista
            */ 
            case 13: case 9:
                this.close();
                break;
            /*
            * Caso for outras teclas faz a busca por letras nas opções e posiciona o scroll
            */ 
            default:
                var occur = new Array();
                this._searchString += p_event.key;
                this.getListLabels().each(function(p_item, p_index) {
                    if (p_item.toLowerCase().indexOf(this._searchString) == 0) {
                        occur.push({ label: p_item, index: p_index });
                    }
                } .bind(this));
                for (var i = 0; i < occur.length; i++) {
                    if (occur[i].index > this.selectedIndex) {
                        this.selectOption(occur[i].index);
                        break;
                    }
                    else if (i == occur.length - 1) {
                        this.selectOption(occur[0].index);
                    }
                }
                clearTimeout(this._timeoutSearch);
                this._timeoutSearch = setTimeout(function() { this._searchString = ""; } .bind(this), 150)
                this.scrollPolicy();
                break;
        }
        //Se a tecla for diferente de tab impede a execução padrão da tecla.
        if (p_event.code != 9) {
            p_event.preventDefault();
            p_event.stopPropagation();
        }
    },

    /*
    * Private
    * @desc: Controla o evento de 'click' de cada opção, fazendo a seleção do item e fechando a lista de opções
    * @params: p_event:Objeto <Event>
    * @return: void
    */
    itemRelease: function(p_event) {
        this.selectOption(p_event.target.getProperty("index").toInt());
        this.close.delay(50, this);
    },

    /*
    * Private
    * @desc: Controla os eventos de mouseover de cada opção, fazendo a troca de classes
    * @params: p_event:Objeto <Event>, p_className:String <Nome da classe padrão>
    * @return: void
    */
    itemRollOver: function(p_event) {
        p_event.target.addClass("CustomComboItem_on");
    },

    /*
    * Private
    * @desc: Controla os eventos de mouseout fazendo a troca de classes
    * @params: p_event:Objeto <Event>, p_className:String <Nome da classe padrão>
    * @return: void
    */
    itemRollOut: function(p_event, p_className) {
        this._items.each(function(p_item, p_index) {
            if (p_index != this.selectedIndex) p_item.element.removeClass('CustomComboItem_on');
        } .bind(this));
    }
});

/*
 * Objeto Behaviors
 * @desc: Contém funções para definir comportamentos dos elementos da página
 */
Behaviors = 
{
    
    /*
     * Método customCombos - Public
     * @desc:
     * @param: void
     * @return: void
     */
    customCombos:function()
    {
        $$(".customCombo").each(function(p_item)
		{
            new CustomCombo(p_item);
		});
    },
    
    /*
     * Método hover - Public
     * @desc: Defini a interação de mouseover e mouseout adicionando aos elementos a classe 'on' 
     * no evento mouseover e o retirando no evento mouseout. Para usar, os elementos desejados devem 
     * conter a classe 'hover' e no css uma classe 'on' do elemento eg:(HTML -- <a href='' class='hover'>Teste</a>, CSS -- a.on {color:#ff0000})
     * @param: void
     * @return: void
     */
	hover:function(p_target)
	{
        var elements = arguments.length > 0 ? $A(arguments) : $$(".hover");
        elements.each(function(p_item)
        {
            var f1 = function(p_event){
                p_event.target.addClass("on");
            }.bindWithEvent();
            var f2 = function(p_event){
                p_event.target.removeClass("on");
            }.bindWithEvent();
            p_item.addEvents({
                mouseenter:f1,
                mouseleave:f2,
                focus:f1,
                blur:f2
            });
        });
	},
	
    /*
     * Método install - Public
     * @desc: Instala automaticamente todos os métodos/funções/comportamentos do objeto Behaviors 
     * (é necessário que a página já esteja carregada e que os elementos estejam disponíveis para manipulação)
     * @return: void
     */
	install:function(p_function, p_target)
	{
        for(var p in this)
        {
            if(typeof this[p] == "function" && p != "install")
            {
                this[p]();
            }
        }  
	}
}

/*
 * Objeto Page
 * @desc: Responsável por conter os objetos que controlam as ações/execuções de cada página
 */
Page = new Object();

/*
 * Objeto Page.Common
 * @desc: Inicializa as execuções pertinentes às todas as páginas (ex. Behaviors)
 */
Page.Common = 
{
    
    /*
     * Método initialize
     * @desc: Anexar o método main ao evento de carregamento total do documento
     * @param: void
     * @return: void
     */
    initialize:function()
    {
        window.addEvent('domready', this.main.bind(this));
    },
    
    /*
     * Método main
     * @desc: Inicializa e executa as ações comuns
     * @param: void
     * @return: void
     */
    main:function()
    {
        /*
         * Instalar Comportamentos
         */
        Behaviors.install();
    }
}

/*
 * Chamada Page.Common.initialize
 */
Page.Common.initialize();