/*
Script: TextboxList.js
	Displays a textbox as a combination of boxes an inputs (eg: facebook tokenizer)

	Authors:
		Guillermo Rauch
		
	Note:
		TextboxList is not priceless for commercial use. See <http://devthought.com/projects/mootools/textboxlist/>. 
		Purchase to remove this message.
*/

var TextboxList = new Class({

    Implements: [Options, Events],

    plugins: [],

    options: {/*
    onFocus: $empty,
    onBlur: $empty,
    onBitFocus: $empty,
    onBitBlur: $empty,
    onBitAdd: $empty,
    onBitRemove: $empty,
    onBitBoxFocus: $empty,
    onBitBoxBlur: $empty,
    onBitBoxAdd: $empty,
    onBitBoxRemove: $empty,
    onBitEditableFocus: $empty,
    onBitEditableBlue: $empty,
    onBitEditableAdd: $empty,
    onBitEditableRemove: $empty,*/
        prefix: 'textboxlist',
        max: null,
        unique: false,
        uniqueInsensitive: true,
        endEditableBit: true,
        startEditableBit: true,
        hideEditableBits: true,
        inBetweenEditableBits: true,
        keys: { previous: Event.Keys.left, next: Event.Keys.right },
        bitsOptions: { editable: {}, box: {} },
        plugins: {},
        check: function(s) { return s.clean().replace(/,/g, '') != ''; },
        encode: function(o) {
            return o.map(function(v) {
                v = ($chk(v[0]) ? v[0] : v[1]);
                return $chk(v) ? v : null;
            }).clean().join(',');
        },
        decode: function(o) { return o.split(','); },
        withIcon: false
    },

    initialize: function(element, options) {
        this.setOptions(options);
        this.original = $(element).setStyle('display', 'none').set('autocomplete', 'off').addEvent('focus', this.focusLast.bind(this));
        this.container = new Element('div', { 'class': this.options.prefix }).inject(element, 'after');
        this.container.addEvent('click', function(e) {
            if ((e.target == this.list || e.target == this.container) && (!this.focused || $(this.current) != this.list.getLast())) this.focusLast();
        } .bind(this));
        this.list = new Element('ul', { 'class': this.options.prefix + '-bits' }).inject(this.container);
        this.list.addEvent('focus', this.focusLast.bind(this));
        for (var name in this.options.plugins) this.enablePlugin(name, this.options.plugins[name]);
        ['check', 'encode', 'decode'].each(function(i) { this.options[i] = this.options[i].bind(this); }, this);
        this.afterInit();
    },

    enablePlugin: function(name, options) {
        this.plugins[name] = new TextboxList[name.camelCase().capitalize()](this, options);
    },

    afterInit: function() {
        if (this.options.unique) this.index = [];
        if (this.options.endEditableBit) this.create('editable', null, { tabIndex: this.original.tabIndex }).inject(this.list);
        var update = this.update.bind(this);
        this.addEvent('bitAdd', update, true).addEvent('bitRemove', update, true);
        document.addEvents({
            click: function(e) {
                if (!this.focused) return;
                if (e.target.className.contains(this.options.prefix)) {
                    if (e.target == this.container) return;
                    var parent = e.target.getParent('.' + this.options.prefix);
                    if (parent == this.container) return;
                }
                this.blur();
            } .bind(this),
            keydown: function(ev) {
                if (!this.focused || !this.current) return;
                var caret = this.current.is('editable') ? this.current.getCaret() : null;
                var value = this.current.getValue()[1];
                var special = ['shift', 'alt', 'meta', 'ctrl'].some(function(e) { return ev[e]; });
                var custom = special || (this.current.is('editable') && this.current.isSelected());
                switch (ev.code) {
                    case Event.Keys.backspace:
                        if (this.current.is('box')) {
                            ev.stop();
                            return this.current.remove();
                        }
                    case this.options.keys.previous:
                        if (this.current.is('box') || ((caret == 0 || !value.length) && !custom)) {
                            ev.stop();
                            this.focusRelative('previous');
                        }
                        break;
                    case Event.Keys['delete']:
                        if (this.current.is('box')) {
                            ev.stop();
                            return this.current.remove();
                        }
                    case this.options.keys.next:
                        if (this.current.is('box') || (caret == value.length && !custom)) {
                            ev.stop();
                            this.focusRelative('next');
                        }
                }
            } .bind(this)
        });
        this.setValues(this.options.decode(this.original.get('value')));
    },

    create: function(klass, value, options) {
        if (klass == 'box') {
            if ((!value[0] && !value[1]) || ($chk(value[1]) && !this.options.check(value[1]))) return false;
            if ($chk(this.options.max) && this.list.getChildren('.' + this.options.prefix + '-bit-box').length + 1 > this.options.max) return false;
            if (this.options.unique && this.index.contains(this.uniqueValue(value))) return false;
            if (this.options.withIcon) {
                if (options) {
                    options.withIcon = true;
                }
                else {
                    options = { withIcon: true };
                }
            }
        }
        return new TextboxListBit[klass.capitalize()](value, this, $merge(this.options.bitsOptions[klass], options));
    },

    uniqueValue: function(value) {
        return $chk(value[0]) ? value[0] : (this.options.uniqueInsensitive ? value[1].toLowerCase() : value[1]);
    },

    onFocus: function(bit) {
        if (this.current) this.current.blur();
        $clear(this.blurtimer);
        this.current = bit;
        this.container.addClass(this.options.prefix + '-focus');
        if (!this.focused) {
            this.focused = true;
            this.fireEvent('focus', bit);
        }
    },

    onBlur: function(bit, all) {
        this.current = null;
        this.container.removeClass(this.options.prefix + '-focus');
        this.blurtimer = this.blur.delay(all ? 0 : 200, this);
    },

    onAdd: function(bit) {
        if (this.options.unique && bit.is('box')) this.index.push(this.uniqueValue(bit.value));
        if (bit.is('box')) {
            var prior = this.getBit($(bit).getPrevious());
            if ((prior && prior.is('box') && this.options.inBetweenEditableBits) || (!prior && this.options.startEditableBit)) {
                var b = this.create('editable').inject(prior || this.list, prior ? 'after' : 'top');
                if (this.options.hideEditableBits) b.hide();
            }
        }
        this.checkLink();
    },

    checkLink: function() {
        var totcount = this.list.getChildren('.' + this.options.prefix + '-bit-box').length;
        this.fireEvent('actionDone', totcount);
        if ($chk(this.options.max) && totcount >= this.options.max) {
            this.fireEvent('linkDisable',totcount);
        }
    },

    onRemove: function(bit) {
        if (!this.focused) return;
        if (this.options.unique && bit.is('box')) {
            this.index.erase(this.uniqueValue(bit.value));
        };
        var prior = this.getBit($(bit).getPrevious());
        if (prior && prior.is('editable')) prior.remove();
        this.focusRelative('next', bit);
    },

    focusRelative: function(dir, to) {
        var b = this.getBit($($pick(to, this.current))['get' + dir.capitalize()]());
        if (b) b.focus();
        return this;
    },

    focusLast: function() {
        var lastElement = this.list.getLast();
        if (lastElement) this.getBit(lastElement).focus();
        return this;
    },

    blur: function() {
        if (!this.focused) return this;
        if (this.current) this.current.blur();
        this.focused = false;
        return this.fireEvent('blur');
    },

    add: function(plain, id, html, afterEl) {
        var b = this.create('box', [id, plain, html]);
        if (b) {
            if (!afterEl) afterEl = this.list.getLast('.' + this.options.prefix + '-bit-box');
            b.inject(afterEl || this.list, afterEl ? 'after' : 'top');
        }
        return this;
    },

    getBit: function(obj) {
        return ($type(obj) == 'element') ? obj.retrieve('textboxlist:bit') : obj;
    },

    getValues: function() {
        return this.list.getChildren().map(function(el) {
            var bit = this.getBit(el);
            if (bit.is('editable')) return null;
            return bit.getValue();
        }, this).clean();
    },

    setValues: function(values) {
        if (!values) return;
        values.each(function(v) {
            if (v) this.add.apply(this, $type(v) == 'array' ? [v[1], v[0], v[2]] : [v]);
        }, this);
    },

    update: function() {
        this.original.set('value', this.options.encode(this.getValues()));
    }

});

var TextboxListBit = new Class({
  
  Implements: Options,  

  initialize: function(value, textboxlist, options){
		this.name = this.type.capitalize();
		this.value = value;
    this.textboxlist = textboxlist;
    this.setOptions(options);            
    this.prefix = this.textboxlist.options.prefix + '-bit';
		this.typeprefix = this.prefix + '-' + this.type;
    this.bit = new Element('li').addClass(this.prefix).addClass(this.typeprefix).store('textboxlist:bit', this);
		this.bit.addEvents({
			mouseenter: function(){ 
				this.bit.addClass(this.prefix + '-hover').addClass(this.typeprefix + '-hover'); 
			}.bind(this),
			mouseleave: function(){
				this.bit.removeClass(this.prefix + '-hover').removeClass(this.typeprefix + '-hover'); 
			}.bind(this)
		});
  },

	inject: function(element, where){
		this.bit.inject(element, where);	
		this.textboxlist.onAdd(this);	
		return this.fireBitEvent('add');
	},

	focus: function(){
		if (this.focused) return this;
		this.show();
		this.focused = true;
		this.textboxlist.onFocus(this);
		this.bit.addClass(this.prefix + '-focus').addClass(this.prefix + '-' + this.type + '-focus');
		return this.fireBitEvent('focus');
	},

	blur: function(){
		if (!this.focused) return this;
		this.focused = false;
		this.textboxlist.onBlur(this);
		this.bit.removeClass(this.prefix + '-focus').removeClass(this.prefix + '-' + this.type + '-focus');
		return this.fireBitEvent('blur');
	},
	
	remove: function(){
		this.blur();		
		this.textboxlist.onRemove(this);
		this.bit.destroy();
		return this.fireBitEvent('remove');
	},
	
	show: function(){
		this.bit.setStyle('display', 'block');
		return this;
	},
	
	hide: function(){
		this.bit.setStyle('display', 'none');
		return this;
	},
	
	fireBitEvent: function(type){
		type = type.capitalize();
		this.textboxlist.fireEvent('bit' + type, this).fireEvent('bit' + this.name + type, this);
		return this;
	},
	
  is: function(t){
    return this.type == t;
  },

	setValue: function(v){
		this.value = v;
		return this;
	},

	getValue: function(){
		return this.value;
	},

	toElement: function(){
		return this.bit;
	}
  
});

TextboxListBit.Editable = new Class({

    Extends: TextboxListBit,

    options: {
        tabIndex: null,
        growing: true,
        growingOptions: {},
        stopEnter: true,
        addOnBlur: false,
        addKeys: Event.Keys.enter
    },

    type: 'editable',

    initialize: function(value, textboxlist, options) {
        this.parent(value, textboxlist, options);
        this.element = new Element('input', { maxlength: '255', type: 'text', 'class': this.typeprefix + '-input', autocomplete: 'off', value: this.value ? this.value[1] : '' }).inject(this.bit);
        if ($chk(this.options.tabIndex)) this.element.tabIndex = this.options.tabIndex;
        var w = textboxlist.list.offsetWidth;
        if (w == 0) w = textboxlist.list.parentNode.style.width.replace("px", "");
        this.options.growingOptions = { maxwidth: w };
        if (this.options.growing) new GrowingInput(this.element, this.options.growingOptions);
        this.element.addEvents({
            focus: function() { this.focus(true); } .bind(this),
            blur: function() {
                this.blur(true);
                if (this.options.addOnBlur) this.toBox();
            } .bind(this)
        });
        if (this.options.addKeys || this.options.stopEnter) {
            // (bj) changed from -> this.element.addEvent('keydown', function(ev){
            this.element.addEvent('keypress', function(ev) {

                if (!this.focused) return;

                if (this.options.stopEnter && ev.code === Event.Keys.enter) ev.stop();
                if ($splat(this.options.addKeys).contains(ev.code)) {
                    ev.stop();
                    this.toBox();
                }
            } .bind(this));
        }
    },

    hide: function() {
        this.parent();
        this.hidden = true;
        return this;
    },

    focus: function(noReal) {
        this.parent();
        try {
            if (!noReal) this.element.focus();
        } catch (x) { }
        return this;
    },

    blur: function(noReal) {
        this.parent();
        try {
            if (!noReal) this.element.blur();
        } catch (x) { };
        if (this.hidden && !this.element.value.length) this.hide();
        return this;
    },

    getCaret: function() {
        if (this.element.createTextRange) {
            var r = document.selection.createRange().duplicate();
            r.moveEnd('character', this.element.value.length);
            if (r.text === '') return this.element.value.length;
            return this.element.value.lastIndexOf(r.text);
        } else return this.element.selectionStart;
    },

    getCaretEnd: function() {
        if (this.element.createTextRange) {
            var r = document.selection.createRange().duplicate();
            r.moveStart('character', -this.element.value.length);
            return r.text.length;
        } else return this.element.selectionEnd;
    },

    isSelected: function() {
        return this.focused && (this.getCaret() !== this.getCaretEnd());
    },

    setValue: function(val) {
        this.element.value = $chk(val[0]) ? val[0] : val[1];
        if (this.options.growing) {
            var growingPlugin = this.element.retrieve('growing');
            if (growingPlugin) growingPlugin.resize();
        };
        return this;
    },

    getValue: function() {
        return [null, this.element.value, null];
    },

    toBox: function() {
        var value = this.getValue();
        var b = this.textboxlist.create('box', value);
        if (b) {
            b.inject(this.bit, 'before');
            this.setValue([null, '', null])
            return b;
        }
        return null;
    }

});

TextboxListBit.Box = new Class({

    Extends: TextboxListBit,

    options: {
        deleteButton: true
    },

    type: 'box',

    initialize: function(value, textboxlist, options) {
        this.parent(value, textboxlist, options);
        if (!options.skip) {
            if (options.withIcon) {
                this.bit.set('html', $chk(this.value[2]) ? this.value[2] : this.value[1]);
            }
            else {
                this.bit.set('html', '<div>' + (this.encodeHtml($chk(this.value[2]) ? this.value[2] : this.value[1]) + '</div>' ));
            }
            this.bit.addEvent('click', this.focus.bind(this));
            if (this.options.deleteButton) {
                this.bit.addClass(this.typeprefix + '-deletable');
                this.close = new Element('a', { href: '#', 'class': this.typeprefix + '-deletebutton', events: { click: this.remove.bind(this)} }).inject(this.bit);
            }
            this.bit.getChildren().addEvent('click', function(e) { e.stop(); });
        }
    },

    encodeHtml: function(value) {
        //return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
        return value.replace(/</g, "&lt;").replace(/>/g, "&gt;");
    }

});
