var box = { version: '0.6.0' };

(function() {

var global = this,
    W = global.window,
    D = W.document,
    DE = D.documentElement,
    $ = global.jQuery,
    noop = function() {};

// @todo expand and correct features detect
if (!DE || !D.getElementById || !D.getElementsByTagName || !D.createElement || !D.createTextNode) {
    box.disabled = true;
    return;
}

box.disabled = false;

var html = $(DE).attr('id', 'js');

// flag dom is ready
box.domIsReady = false;
$(D).ready(function() {
    box.domIsReady = true;
});

// flag page is loaded
box.loadIsDone = false;
$(W).load(function() {
    box.loadIsDone = true;
});


box.getGlobal = function() {
    return global;
};

box.getWin = function() {
    return W;
};

box.getDoc = function() {
    return D;
};

box.getJWin = function() {
    return $(W);
};

box.getJDoc = function() {
    return $(D);
};


// feature testing
box.isHostMethod = function(o, p) {
    var t = typeof o[p];
    return t == 'function' || !!(t == 'object' && o[p]) || t == 'unknown';
};

box.areHostMethods = function(o) {
    var i = arguments.length - 1;
    while(i > 0) {
        if(!box.isHostMethod(o, arguments[i])) {
            return false;
        }
        i--;
    }
    return true;
};

box.isHostCollection = function(o, p) {
    var t = typeof o[p];
    return !!(t == 'object' && o[p]) || t == 'function';
};

box.isHostObject = function(o, p) {
    return !!(typeof(o[p]) == 'object' && o[p]);
};


// bridge to jQuery
box.dom = function(a1, a2) {
    return $(a1, a2);
};


var inherit, extend, clone;

box.inherit = inherit = (function() {
    var Fn = function() {};
    return function(fnSub, fnSuper) {
        Fn.prototype = fnSuper.prototype;
        fnSub.prototype = new Fn();
        fnSub.prototype.constructor = fnSub;
    };
})();

box.extend = extend = function(fn, oFn) {
    var oProto = fn.prototype;
    for(var sPropName in oFn) {
        if(oFn.hasOwnProperty(sPropName)) {
            oProto[sPropName] = oFn[sPropName];
        }
    }
};

box.clone = clone = (function() {
    var Fn = function() {};
    return function(oSrc) {
        Fn.prototype = oSrc;
        return new Fn();
    };
})();

/*!
 * news v0.5.7, a JavaScript notification library
 * Copyright (C) 2011 Manuel Catez
 * 
 * Distributed under an MIT-style license
 * See https://github.com/mcatez/news
 */
var reSubscribe = /^([a-z0-9_-]+)(@[a-z0-9_-]+)?(>(?:\*|[^>\n\r\f\t]+))$/i,
    reUnsubscribe = /^(\*|[a-z0-9_-]+)(@(?:\*|[a-z0-9_-]+))?(>(?:\*|[^>\n\r\f\t]+))$/i,
    oSubscriptions = {},
    Notification,
    subscribe,
    subscribeAll,
    unsubscribe,
    unsubscribeAll,
    publish,
    removeHandlersMain,
    removeHandlers,
    execHandlers;

Notification = function(oDatas) {
    this.type = oDatas.type;
    this.label = oDatas.label;
    this.propagation = (oDatas.propagation !== false);
    this.stopped = false;
    this.prevented = false;
    this.source = oDatas.source || null;
    this.data = oDatas.data || {};
    this.timeStamp = (new Date()).getTime();
};
Notification.prototype = {
    stopPropagation: function() {
        this.propagation = false;
    },
    
    stopImmediatePropagation: function() {
        this.propagation = false;
        this.stopped = true;
    },
    
    preventDefault: function() {
        this.prevented = true;
    }
};

subscribe = function(sNews, fHandler, oContext) {
    var aTokens = sNews.match(reSubscribe);
    if(aTokens) {
        var sType = aTokens[1],
            sNamespace = aTokens[2] || '@!',
            sLabel = aTokens[3],
            oDatas = { handler: fHandler, ns: sNamespace, context: oContext },
            oType = oSubscriptions[sType],
            oActions;
        if(!oType) {
            oType = oSubscriptions[sType] = { count: 0 };
        }
        oActions = oType[sLabel];
        if(oActions) {
            oActions[oActions.length] = oDatas;
        } else {
            oType[sLabel] = [ oDatas ];
            oType.count++;
        }
    }/*<debug>*/ else {
        publish({
            type: 'error',
            label: 'box.subscribe',
            data: { message: 'Incorrect news name: ' + sNews }
        });
    }/*</debug>*/
};

subscribeAll = function() {
    var sType = typeof arguments[0],
        i, l, oDatas;
    if(sType === 'string') {
        subscribe(arguments[0], arguments[1], arguments[2]);
    } else if(sType === 'object' && arguments[0]) {
        i = -1;
        l = arguments.length;
        while(++i < l) {
            oDatas = arguments[i];
            subscribe(oDatas.name, oDatas.handler, oDatas.context);
        }
    }
};

removeHandlersMain = function(oType, sLabel, sNamespace) {
    var aLabel = oType[sLabel];
    if(aLabel) {
        if(sNamespace === '@?') {
            delete oType[sLabel];
            oType.count--;
            return;
        }
        var i = aLabel.length;
        if(sNamespace === '@*') {
            while(i--) {
                if(aLabel[i].ns !== '@!') {
                    aLabel.splice(i, i + 1);
                }
            }
        } else {
            while(i--) {
                if(aLabel[i].ns === sNamespace) {
                    aLabel.splice(i, 1);
                }
            }
        }
        if(aLabel.length === 0) {
            delete oType[sLabel];
            oType.count--;
        }
    }
};

removeHandlers = function(sType, sLabel, sNamespace) {
    var oType = oSubscriptions[sType];
    if(oType) {
        if(sLabel === '>*') {
            var sName;
            for(sName in oType) {
                if(oType.hasOwnProperty(sName) && sName !== 'count') {
                    removeHandlersMain(oType, sName, sNamespace);
                }
            }
        } else {
            removeHandlersMain(oType, sLabel, sNamespace);
        }
        if(oType.count === 0) {
            delete oSubscriptions[sType];
        }
    }
};

unsubscribe = function(sNews) {
    if(sNews === '*' || sNews === '*@?>*') {
        oSubscriptions = {};
    } else {
        var aTokens = sNews.match(reUnsubscribe);
        if(aTokens) {
            var sType = aTokens[1],
                sNamespace = aTokens[2] || '@!',
                sLabel = aTokens[3];
            if(sType === '*') {
                for(sType in oSubscriptions) {
                    if(oSubscriptions.hasOwnProperty(sType)) {
                        removeHandlers(sType, sLabel, sNamespace);
                    }
                }
            } else {
                removeHandlers(sType, sLabel, sNamespace);
            }
        }/*<debug>*/ else {
            publish({
                type: 'error',
                label: 'box.unsubscribe',
                data: { message: 'Incorrect news name: ' + sNews }
            });
        }/*</debug>*/
    }
};

unsubscribeAll = function() {
    var i = arguments.length;
    while(i--) {
        unsubscribe(arguments[i]);
    }
};

execHandlers = function(oNews, aReg) {
    if(aReg) {
        var i = -1,
            l = aReg.length,
            oListener;
        while(++i < l) {
            oListener = aReg[i];
            oListener.handler.call(oListener.context || oNews.source || null, oNews);
            if(oNews.stopped === true) {
                break;
            }
        }
    }
};

publish = function(oDatas) {
    var oType = oSubscriptions[oDatas.type];
    if(oType) {
        var sLabel = '>' + oDatas.label,
            oNews = new Notification(oDatas),
            aParts, i;
        if(oNews.propagation === false) {
            execHandlers(oNews, oType[sLabel]);
        } else {
            aParts = sLabel.split('.');
            i = aParts.length;
            while(i--) {
                sLabel = aParts.slice(0, i + 1).join('.');
                execHandlers(oNews, oType[sLabel]);
                if(oNews.propagation === false || oNews.stopped === true) {
                    break;
                }
            }
            if(oNews.propagation !== false && oType['>*']) {
                execHandlers(oNews, oType['>*']);
            }
        }
        return oNews;
    }
    return null;
};

box.subscribe = subscribeAll;
box.unsubscribe = unsubscribeAll;
box.publish = publish;

var Store,
    
    boxStore = {},
    boxUID = 0,
    
    reBoxStore = /^[a-zA-Z_][a-zA-Z0-9_-]*$/,
    reBoxExtractStore = /^([a-zA-Z_][a-zA-Z0-9]*):(.+)/,
    reBoxAdd = /^([a-zA-Z_][a-zA-Z0-9_-]*)$/,
    reBoxNames = /^([A-Z_][A-Z0-9_-]*)(?:\.([A-Z_][A-Z0-9_.:-]+))?$/i,
    
    boxGetName = function() {
        return this.boxName + '.' + this.id;
    },
    
    boxPublish = function(sType, oDatas) {
        return box.publish({ type: sType, label: this.boxGetName(), source: this, data: oDatas });
    };

Store = function(sId) {
    this.id = sId;
};
Store.prototype = {
    boxType: 'Store',
    boxName: 'store',
    
    boxGetPublishLabel: function() {
        return this.boxName + '.' + this.id + (this.namespace ? '.' + this.namespace : '');
    },
    
    boxPublish: function(sType, oDatas) {
        box.publish({ type: sType, label: this.boxGetPublishLabel(), source: this, data: oDatas });
    },
    
    isValidAddName: function(sName) {
        return typeof sName === 'string' && reBoxAdd.test(sName);
    },
    
    create: function(sName, oDatas) {
        this.namespace = 'create';
        var aName = sName.match(reBoxNames),
            sRoot = this.id + ':',
            sId, sNameObj, oBox, oCfg, sNameCtr, sNameCfg;
        sName = aName[1];
        if(aName[2]) {
            sId = aName[2];
        } else {
            if(oDatas && typeof oDatas.rootElm === 'string') {
                sId = $(oDatas.rootElm).getAnId();
            }
            if(!sId) {
                sId = 'box' + (++boxUID);
            }
        }
        sNameObj = sRoot + sName + '.' + sId;
        sNameCtr = sRoot + sName + '#constructor';
        sNameCfg = sRoot + sName + '#config';
        if(boxStore[sNameCtr]) {
            /*<debug>*/if(boxStore[sNameObj]) {
                this.boxPublish(
                    'warning',
                    { message: 'Creating an object that overwrites an existing one ("' + sNameObj + '")' }
                );
            }/*</debug>*/
            oCfg = boxStore[sNameCfg];
            boxStore[sNameObj] = oBox = new boxStore[sNameCtr](sId, oCfg ? clone(oCfg) : null);
            if(oBox.boxCreate) {
                oBox.boxCreate(oDatas);
            }
            return oBox;
        }
        /*<debug>*/this.boxPublish(
            'warning',
            { message: 'Trying to create an object from an undefined constructor ("' + sNameCtr + '")' }
        );/*</debug>*/
        return null;
    },
    
    destroy: function(sName) {
        var sId = this.id + ':' + sName;
        if(boxStore[sId]) {
            if(boxStore[sId].boxDestroy) {
                boxStore[sId].boxDestroy();
            }
            delete boxStore[sId];
        }
    },
    
    addConstructor: function(sName, fComponent) {
        this.namespace = 'addConstructor';
        if(this.isValidAddName(sName) && typeof fComponent === 'function') {
            var sRoot = this.id + ':',
                sId = sRoot + sName + '#constructor';
            if(!boxStore[sId]) {
                var oBox = fComponent($, box);
                if(oBox && oBox.prototype.boxCreate) {
                    oBox.prototype.boxName = sRoot + sName;
                    if(!oBox.prototype.boxGetName) {
                        oBox.prototype.boxGetName = boxGetName;
                    }
                    if(!oBox.prototype.boxPublish) {
                        oBox.prototype.boxPublish = boxPublish;
                    }
                    if(!oBox.prototype.boxDestroy) {
                        oBox.prototype.boxDestroy = noop;
                    }
                    boxStore[sId] = oBox;
                }/*<debug>*/ else {
                    this.boxPublish(
                        'error',
                        { message: 'No constructor returned or prototype is missing a "boxCreate" method ("' + sName + '")' }
                    );
                }
                return;/*</debug>*/
            }
            /*<debug>*/this.boxPublish(
                'error',
                { message: 'Overwriting an existing constructor ("' + sName + '")' }
            );/*</debug>*/
        }/*<debug>*/ else {
            this.boxPublish(
                'error',
                { message: 'Invalid name or function missing ("' + sName + '")' }
            );
        }/*</debug>*/
        return this;
    },
    
    addModule: function(sName, fModule) {
        this.namespace = 'addModule';
        if(this.isValidAddName(sName) && typeof fModule === 'function') {
            var sId = this.id + ':' + sName;
            if(!boxStore[sId]) {
                var oBox = fModule($, box);
                if(oBox) {
                    boxStore[sId] = oBox;
                }/*<debug>*/ else {
                    this.boxPublish(
                        'error',
                        { message: 'No module returned ("' + sName + '")' }
                    );
                }
                return;/*</debug>*/
            }
            /*<debug>*/this.boxPublish(
                'error',
                { message: 'Overwriting an existing module ("' + sName + '")' }
            );/*</debug>*/
        }/*<debug>*/ else {
            this.boxPublish(
                'error',
                { message: 'Invalid name or function missing ("' + sName + '")' }
            );
        }/*</debug>*/
        return this;
    },
    
    addConfig: function(sName, oDatas) {
        this.namespace = 'addConfig';
        var sId = this.id + ':' + sName + '#config';
        /*<debug>*/if(boxStore[sId]) {
            this.boxPublish(
                'warning',
                { message: 'Overwriting an existing configuration ("' + sName + '")' }
            );
        }/*</debug>*/
        boxStore[sId] = oDatas;
        return this;
    },
    
    addDatas: function(sName, uDatas) {
        this.namespace = 'addDatas';
        var sId = this.id + ':' + sName;
        /*<debug>*/if(boxStore[sId]) {
            this.boxPublish(
                'warning',
                { message: 'Overwriting an existing set of datas ("' + sName + '")' }
            );
        }/*</debug>*/
        boxStore[sId] = uDatas;
        return this;
    },
    
    modifyConfig: function(sName, oDatas) {
        var sId = this.id + ':' + sName + '#config',
            oCfg = boxStore[sId],
            sProp;
        if(oCfg) {
            for(sProp in oDatas) {
                if(oDatas.hasOwnProperty(sProp)) {
                    oCfg[sProp] = oDatas[sProp];
                }
            }
        }
        return this;
    },
    
    remove: function(sName) {
        // @todo check if possible to remove?
        if(boxStore[sName]) {
            delete boxStore[sName];
        }
        return this;
    }
};

box.store = function(sId) {
    if(reBoxStore.test(sId)) {
        if(typeof boxStore[sId] === 'undefined') {
            boxStore[sId] = new Store(sId);
        }
        return boxStore[sId];
    }
    /*<debug>*/box.publish({ type: 'error', label: 'box.store', data: { message: 'Invalid store name ("' + sId + '")' } });/*</debug>*/
};

box.get = function(sId) {
    return typeof boxStore[sId] !== 'undefined' ? boxStore[sId] : null;
};

box.store('ui');
box.store('util');
box.store('const').addDatas('NOTIFY_OFF', 0).addDatas('NOTIFY_ON', 1);
/*<debug>*/box.store('internal').addDatas('subscriptions', oSubscriptions);/*</debug>*/

// get an ID from an element
$.fn.getAnId = function() {
    return this.length ? (this.getBoxDatas('id') || this.attr('id')) : null;
};

var reBoxDatasInClass = /box\[([^\]]+)\]/,
    reBoxDatasPairs = /([^=]+)=([^;]+);?/g,
    reBoxDatasLast = /;$/,
    oReBoxDatasCache = {};

$.fn.getBoxDatas = function(sProp) {
    if(this.length) {
        var sDatas = this.data('box'),
            aParts, oDatas;
        if(!sDatas) {
            aParts = this[0].className.match(reBoxDatasInClass);
            if(aParts) {
                sDatas = aParts[1];
                this.removeClass('box[' + sDatas + ']');
                this.data('box', sDatas);
            }
        }
        if(sDatas) {
            if(typeof sProp === 'string') {
                if(!oReBoxDatasCache[sProp]) {
                    oReBoxDatasCache[sProp] = new RegExp(sProp + '=([^;\\]]+)');
                }
                aParts = sDatas.match(oReBoxDatasCache[sProp]);
                return aParts ? aParts[1] : null;
            } else {
                oDatas = {};
                while(aParts = reBoxDatasPairs.exec(sDatas)) {
                    oDatas[aParts[1]] = aParts[2];
                }
                return oDatas;
            }
        }
    }
    return null;
};

$.fn.setBoxDatas = function(oDatas) {
    if(this.length && oDatas && typeof oDatas === 'object') {
        var oExisting = this.getBoxDatas() || {},
            sDatas = '',
            sProp;
        for(sProp in oDatas) {
            if(oDatas.hasOwnProperty(sProp)) {
                oExisting[sProp] = oDatas[sProp];
            }
        }
        for(sProp in oExisting) {
            if(oExisting.hasOwnProperty(sProp)) {
                sDatas += sProp + '=' + oExisting[sProp] + ';';
            }
        }
        this.data('box', sDatas.replace(reBoxDatasLast, ''));
    }
    return this;
};

$.fn.clearBoxDatas = function() {
    if(this.length) {
        var oExisting = this.getBoxDatas(),
            sDatas = '',
            i = arguments.length,
            sProp;
        if(!i) {
            this.data('box', null);
        } else if(oExisting) {
            while(i--) {
                delete oExisting[arguments[i]];
            }
            for(sProp in oExisting) {
                if(oExisting.hasOwnProperty(sProp)) {
                    sDatas += sProp + '=' + oExisting[sProp] + ';';
                }
            }
            this.data('box', sDatas.replace(reBoxDatasLast, ''));
        }
    }
    return this;
};

// clear fields onfocus / restore on blur
var reTextFieldTypes = /(text|password)/i,
    reEmpty = /^\s*$/,
    
    clearTextFieldValue = function() {
        if(this.value == this.defaultValue) {
            this.value = '';
        }
    },
    
    restoreTextFieldValue = function() {
        if(reEmpty.test(this.value)) {
            this.value = this.defaultValue;
        }
    };

$.fn.clearTextFields = function() {
    this.each(function(i, elm) {
        if(elm.nodeName.toLowerCase() == 'input' && reTextFieldTypes.test(elm.type)) {
            $(elm).focus(clearTextFieldValue).blur(restoreTextFieldValue);
        } else {
            $('input[type=text], input[type=password]', elm).focus(clearTextFieldValue).blur(restoreTextFieldValue);
        }
    });
};

// get outer HTML
var divDummyContainer = $('<div></div>');
$.fn.outerHTML = function() {
    divDummyContainer.html('');
    return divDummyContainer.append(this.eq(0).clone()).html();
};

// replace the innerHTML of an element
$.fn.replaceIn = function(s) {
    var html = '';
    this.each(function(i, elm) {
        html += $(elm).outerHTML();
    });
    $(s).html(html);
    return this;
};

// get scroll offset
$.fn.getScroll = function() {
    return {
        top: this.scrollTop(),
        left: this.scrollLeft()
    };
};

// get different size types
var getASize = {
    'viewport-width': function() {
        return $(W).width();
    },'viewport-height': function() {
        return $(W).height();
    },'document-width': function() {
        return $(D).width();
    },'document-height': function() {
        return $(D).height();
    },'content-box-width': function(elm) {
        return elm.width();
    },'content-box-height': function(elm) {
        return elm.height();
    },'padding-box-width': function(elm) {
        return elm.innerWidth();
    },'padding-box-height': function(elm) {
        return elm.innerHeight();
    },'border-box-width': function(elm) {
        return elm.outerWidth();
    },'border-box-height': function(elm) {
        return elm.outerHeight();
    },'margin-box-width': function(elm) {
        return elm.outerWidth(true);
    },'margin-box-height': function(elm) {
        return elm.outerHeight(true);
    }
};

// get a size from a keyword
// if no (recognized) keyword, default to content-box size
var getSizeFromKeyword = function(elm, type, keyword) {
    if(elm[0] === W) {
        return getASize['viewport-' + type.toLowerCase()]();
    } else if(elm[0] === D) {
        return getASize['document-' + type.toLowerCase()]();
    } else {
        var method = typeof keyword == 'string' ? keyword.toLowerCase() + '-' + type : 'content-box-' + type;
        if(getASize[method]) {
            return getASize[method](elm);
        } else {
            return getASize['content-box-' + type.toLowerCase()](elm);
        }
    }
};

// get size from a keyword, different keywords possible for width / height
// if no keywords, default to content-box size
$.fn.getSize = function(keyword) {
    return {
        width: getSizeFromKeyword(this, 'width', keyword),
        height: getSizeFromKeyword(this, 'height', keyword)
    };
};
$.fn.getWidth = function(keyword) {
    return getSizeFromKeyword(this, 'width', keyword);
};
$.fn.getHeight = function(keyword) {
    return getSizeFromKeyword(this, 'height', keyword);
};

// set size, only with numbers, or 'auto' keyword
$.fn.setSize = function(datas) {
    if(typeof datas == 'number' || datas == 'auto') {
        this.width(datas).height(datas);
    } else if(typeof datas == 'object') {
        if(typeof datas.width == 'number' || datas.width == 'auto') {
            this.width(datas.width);
        }
        if(typeof datas.height == 'number' || datas.height == 'auto') {
            this.height(datas.height);
        }
    }
    return this;
};
$.fn.setWidth = function(value) {
    if(typeof value == 'number' || value == 'auto') {
        this.width(value);
    }
    return this;
};
$.fn.setHeight = function(value) {
    if(typeof value == 'number' || value == 'auto') {
        this.height(value);
    }
    return this;
};

// get a position from a keyword
// if no (recognized) keyword, default to offset from the document origin
var getXYFromKeyword = function(elm, keyword) {
    if(elm[0] === D) {
        return {top: 0, left: 0};
    } else if(elm[0] === W) {
        return elm.getScroll();
    } else if(keyword == 'positioned-ancestor') {
        return elm.position();
    } else {
        return elm.offset();
    }
};

// get position from a keyword, different keywords possible for top / left
// if no keywords, default to offsets from the document origin
$.fn.getXY = function(keyword) {
    return getXYFromKeyword(this, keyword);
};
$.fn.getX = function(keyword) {
    return getXYFromKeyword(this, keyword).left;
};
$.fn.getY = function(keyword) {
    return getXYFromKeyword(this, keyword).top;
};

// set position, only with numbers
$.fn.setXY = function(datas) {
    if(typeof datas == 'number') {
        this.css({top: datas + 'px', left: datas + 'px'});
    } else if(typeof datas == 'object') {
        var pos = {};
        if(typeof datas.top == 'number') {
            pos.top = datas.top + 'px';
        } else if(datas.top == 'auto') {
            pos.top = 'auto';
        }
        if(typeof datas.left == 'number') {
            pos.left = datas.left + 'px';
        } else if(datas.left == 'auto') {
            pos.left = 'auto';
        }
        this.css(pos);
    }
};

var getAPosition = {
    'root': function(curElm, refElm, type) {
        return refElm.offset()[type];
    },'positioned-ancestor': function(curElm, refElm, type) {
        return refElm.position()[type];
    },'before': function(curElm, refElm, type, relType) {
        var curDim = curElm['get' + relType]('border-box');
        var refPos = refElm.getXY()[type];
        return refPos - curDim;
    },'start': function(curElm, refElm, type, relType) {
        return refElm.getXY()[type];
    },'middle': function(curElm, refElm, type, relType) {
        var curDim = curElm['get' + relType]('border-box');
        var refDim = getSizeFromKeyword(refElm, relType);
        var refPos = refElm.getXY()[type];
        return refPos + (refDim - curDim) / 2;
    },'end': function(curElm, refElm, type, relType) {
        var curDim = curElm['get' + relType]('border-box');
        var refDim = getSizeFromKeyword(refElm, relType);
        var refPos = refElm.getXY()[type];
        return refPos + refDim - curDim;
    },'in-before': function(curElm, refElm, type, relType) {
        return -curElm['get' + relType]('border-box');
    },'in-start': function() {
        return 0;
    },'in-middle': function(curElm, refElm, type, relType) {
        var curDim = curElm['get' + relType]('border-box');
        var refDim = getSizeFromKeyword(refElm, relType);
        return (refDim - curDim) / 2;
    },'in-end': function(curElm, refElm, type, relType) {
        var curDim = curElm['get' + relType]('border-box');
        var refDim = getSizeFromKeyword(refElm, relType);
        return refDim - curDim;
    }
};

var getAlternateSelectorNames = function(name) {
    var ref = {
        'viewport': W,
        'document': D
    };
    return ref[name] || name;
};

var getStyleDim = function(styles, elm) {
    var s;
    if(typeof styles.width == 'string') {
        s = styles.width.split(':');
        if(s.length == 2) {
            styles.width = getASize[s[1] + '-width']($(getAlternateSelectorNames(s[0])), 'width');
        }
        if(!isNaN(styles['min-width']) && !isNaN(styles.width)) {
            styles.width = styles.width < styles['min-width'] ? styles['min-width'] : styles.width;
            delete styles['min-width'];
        }
        if(!isNaN(styles['max-width']) && !isNaN(styles.width)) {
            styles.width = styles.width > styles['max-width'] ? styles['max-width'] : styles.width;
            delete styles['max-width'];
        }
    }
    if(typeof styles.height == 'string') {
        s = styles.height.split(':');
        if(s.length == 2) {
            styles.height = getASize[s[1] + '-height']($(getAlternateSelectorNames(s[0])), 'height');
        }
        if(!isNaN(styles['min-height']) && !isNaN(styles.height)) {
            styles.height = styles.height < styles['min-height'] ? styles['min-height'] : styles.height;
            delete styles['min-height'];
        }
        if(!isNaN(styles['max-height']) && !isNaN(styles.height)) {
            styles.height = styles.height > styles['max-height'] ? styles['max-height'] : styles.height;
            delete styles['max-height'];
        }
    }
    return styles;
};

var getStylePos = function(styles, elm) {
    var s;
    if(typeof styles.top == 'string') {
        s = styles.top.split(':');
        if(s.length == 2) {
            styles.top = getAPosition[s[1]](elm, $(getAlternateSelectorNames(s[0])), 'top', 'Height');
        }
        if(!isNaN(styles['min-top']) && !isNaN(styles.top)) {
            styles.top = styles.top < styles['min-top'] ? styles['min-top'] : styles.top;
            delete styles['min-top'];
        }
        if(!isNaN(styles['max-top']) && !isNaN(styles.top)) {
            styles.top = styles.top > styles['max-top'] ? styles['max-top'] : styles.top;
            delete styles['max-top'];
        }
    }
    if(typeof styles.left == 'string') {
        s = styles.left.split(':');
        if(s.length == 2) {
            styles.left = getAPosition[s[1]](elm, $(getAlternateSelectorNames(s[0])), 'left', 'Width');
        }
        if(!isNaN(styles['min-left']) && !isNaN(styles.left)) {
            styles.left = styles.left < styles['min-left'] ? styles['min-left'] : styles.left;
            delete styles['min-left'];
        }
        if(!isNaN(styles['max-left']) && !isNaN(styles.left)) {
            styles.left = styles.left > styles['max-left'] ? styles['max-left'] : styles.left;
            delete styles['max-left'];
        }
    }
    return styles;
};

// apply styles from a reference element or not
$.fn.applyStyles = function(styles) {
    if(this.length && styles && typeof styles == 'object') {
        var elm = this.eq(0), nStyles = clone(styles);
        
        // size can affect position, so compute first
        nStyles = getStyleDim(nStyles, elm);
        
        if(!isNaN(nStyles.width)) {
            elm.width(nStyles.width);
            delete nStyles.width;
        }
        if(!isNaN(nStyles.height)) {
            elm.height(nStyles.height);
            delete nStyles.height;
        }
        
        nStyles = getStylePos(nStyles, elm);
        
        elm.css(nStyles);
    }
    return this;
};

// get styles from a reference element or not
$.fn.getStyles = function(styles) {
    if(this.length && styles && typeof styles == 'object') {
        var elm = this.eq(0), nStyles = clone(styles);
        
        nStyles = getStyleDim(nStyles, elm);
        nStyles = getStylePos(nStyles, elm);
        
        return nStyles;
    }
    return null;
};

})();

box.get('util').addConstructor('loadhtml', function($, box) {
    var UtilDatasHTML = function(sId) {
        this.id = sId;
    };
    UtilDatasHTML.prototype = {
        boxCreate: function(oDatas) {
            this.timeout = oDatas.requestTimeout || 15000;
            this.available = true;
        },
        
        boxPublish: function(sType, oDatas) {
            if(this.ui) {
                this.ui.boxPublish(sType, oDatas);
            } else {
                box.publish({ type: sType, label: this.boxName + '.' + this.id, source: this, data: oDatas });
            }
        },
        
        bindToUi: function(oUi) {
            this.ui = oUi;
            return this;
        },
        
        set: function(str) {
            if(!this.xhr) {
                this.available = true;
                this.boxPublish('htmlready', { element: $(str) });
            }
        },
        
        request: function(datas) {
            var oUtil = this;
            oUtil.available = false;
            oUtil.boxPublish('requeststart');
            oUtil.xhr = $.ajax({
                cache: datas.cache !== false,
                data: datas.params,
                timeout: oUtil.timeout,
                type: datas.method || 'GET',
                url: datas.url,
                error: function(http, status) {
                    oUtil.xhr = null;
                    oUtil.available = true;
                    oUtil.boxPublish('requesterror', { status: status });
                },
                success: function(txt) {
                    oUtil.xhr = null;
                    oUtil.datas = txt;
                    oUtil.boxPublish('requestsuccess');
                    oUtil.set(oUtil.datas);
                }
            });
        },
        
        abort: function() {
            if(this.xhr) {
                this.xhr.abort();
                this.available = true;
            }
        }
    };
    
    return UtilDatasHTML;
});

box.get('util').addConstructor('loadimage', function($, box) {
    var UtilDatasImage = function(sId) {
        this.id = sId;
    };
    UtilDatasImage.prototype = {
        boxCreate: function(oDatas) {
            this.timeout = oDatas.preloadTimeout || 30000;
            this.available = true;
        },
        
        boxPublish: function(sType, oDatas) {
            if(this.ui) {
                this.ui.boxPublish(sType, oDatas);
            } else {
                box.publish({ type: sType, label: this.boxName + '.' + this.id, source: this, data: oDatas });
            }
        },
        
        bindToUi: function(oUi) {
            this.ui = oUi;
            return this;
        },
        
        getURLs: function(oElm) {
            var aURL = [];
            (oElm.jquery ? oElm : $(oElm)).find('img').each(function(i, oImg) {
                aURL[i] = oImg.getAttribute('src', 2);
            });
            return aURL;
        },
        
        single: function(sURL, nTimeout) {
            var oUtil = this;
            oUtil.start(nTimeout);
            oUtil.preload = new Image();
            oUtil.preload.onload = function() {
                oUtil.stop({ status: 'success', src: sURL, width: this.width, height: this.height });
            };
            oUtil.preload.src = sURL;
        },
        
        batch: function(aURL, nTimeout) {
            if(!aURL.length) {
                this.stop({ status: 'success' });
                return;
            }
            var oUtil = this,
                oEvt = {},
                i = -1,
                l = aURL.length,
                c = l;
            oEvt.img = [];
            oUtil.preload = [];
            oUtil.start(nTimeout);
            while(++i < l) {
                oUtil.preload[i] = new Image();
                oUtil.preload[i].onload = function() {
                    oEvt.img[oEvt.img.length] = { src: this.getAttribute('src', 2), width: this.width, height: this.height };
                    c--;
                    if(c === 0) {
                        oEvt.status = 'success';
                        oUtil.stop(oEvt);
                    }
                };
                oUtil.preload[i].src = aURL[i];
            }
        },
        
        // @todo implement a queue method
        
        start: function(nTimeout) {
            var oUtil = this;
            oUtil.available = false;
            oUtil.timer = box.getWin().setTimeout(function() {
                oUtil.stop({ status: 'timeout' });
            }, nTimeout || oUtil.timeout);
        },
        
        abort: function() {
            if(this.preload) {
                this.stop({ status: 'abort' });
            }
        },
        
        stop: function(oEvt) {
            box.getWin().clearTimeout(this.timer);
            this.timer = null;
            if(this.preload) {
                // changing the src attribute won't stop the former request
                // window.stop() / document.execCommand('Stop', false) would do the job but it's risky
                var sURL = box.get('const:TRANSPARENT_GIF');
                if(this.preload.nodeName) {
                    this.preload.onload = null;
                    this.preload.src = sURL;
                } else {
                    var i = this.preload.length;
                    while(i--) {
                        this.preload[i].onload = null;
                        this.preload[i].src = sURL;
                    }
                }
                this.preload = null;
            }
            this.available = true;
            var sType = oEvt.status == 'success' ? 'preloadready': 'preloaderror';
            this.boxPublish(sType, oEvt);
        }
    };
    
    return UtilDatasImage;
});

box.get('util').addConstructor('dombridge', function($, box) {
    var doRemove = function(oUtil, jElm, sEvt) {
        jElm.remove();
        oUtil.inDOM = false;
        oUtil.boxPublish('removefromdom');
        oUtil.available = true;
        oUtil.boxPublish(sEvt);
    };
    
    var UtilDOMBridge = function(sId) {
        this.id = sId;
    };
    UtilDOMBridge.prototype = {
        boxCreate: function(oDatas) {
            this.insertTarget = oDatas.insertTarget || 'body';
            this.insertMethod = oDatas.insertMethod || 'appendTo';
            this.insertStyles = oDatas.insertStyles || {};
            this.insertAnimStyles = oDatas.insertAnimStyles;
            this.insertAnimDuration = oDatas.insertAnimDuration;
            this.removeAnimStyles = oDatas.removeAnimStyles;
            this.removeAnimDuration = oDatas.removeAnimDuration;
            this.resizeStyles = oDatas.resizeStyles;
            this.available = true;
            this.inDOM = false;
        },
        
        boxPublish: function(sType, oDatas) {
            if(this.ui) {
                this.ui.boxPublish(sType, oDatas);
            } else {
                box.publish({ type: sType, label: this.boxName + '.' + this.id, source: this, data: oDatas });
            }
        },
        
        bindToUi: function(oUi) {
            this.ui = oUi;
            return this;
        },
        
        insert: function(jElm, sEvt) {
            var oUtil = this;
            if(jElm && oUtil.available && !oUtil.inDOM) {
                oUtil.available = false;
                
                jElm
                    .css('visibility', 'hidden')
                    [oUtil.insertMethod](oUtil.insertTarget)
                    .css(jElm.getStyles(oUtil.insertStyles))
                    .css('visibility', 'visible');
                oUtil.inDOM = true;
                oUtil.boxPublish('addtodom');
                
                if(oUtil.insertAnimStyles && oUtil.insertAnimDuration) {
                    jElm.filter(function(i, oElm) {
                        return oElm.tagName && oElm.tagName !== 'SCRIPT'; // debug jQuery 1.5
                    }).animate(
                        jElm.getStyles(oUtil.insertAnimStyles),
                        oUtil.insertAnimDuration,
                        function() {
                            oUtil.available = true;
                            oUtil.boxPublish(sEvt);
                        }
                    );
                } else {
                    oUtil.available = true;
                    oUtil.boxPublish(sEvt);
                }
            }
        },
        
        remove: function(jElm, sEvt, bBypassAnim) {
            var oUtil = this;
            if(jElm && oUtil.available && oUtil.inDOM) {
                oUtil.available = false;
                
                if(oUtil.removeAnimStyles && oUtil.removeAnimDuration) {
                    if(bBypassAnim === true) {
                        jElm.css(jElm.getStyles(oUtil.removeAnimStyles));
                        doRemove(oUtil, jElm, sEvt);
                    } else {
                        jElm.filter(function(i, oElm) {
                            return oElm.tagName && oElm.tagName !== 'SCRIPT'; // debug jQuery 1.5
                        }).animate(
                            jElm.getStyles(oUtil.removeAnimStyles),
                            oUtil.removeAnimDuration,
                            function() {
                                doRemove(oUtil, jElm, sEvt);
                            }
                        );
                    }
                } else {
                    doRemove(oUtil, jElm, sEvt);
                }
            }
        },
        
        update: function(jElm) {
            if(jElm && this.available && this.resizeStyles) {
                jElm.applyStyles(this.resizeStyles);
            }
        },
        
        applyStyles: function(jElm, oStyles, oOptions) {
            var oUtil = this;
            if(jElm && oUtil.available) {
                jElm.filter(function(i, oElm) {
                    return oElm.tagName && oElm.tagName !== 'SCRIPT'; // debug jQuery 1.5
                }).animate(
                    jElm.getStyles(oStyles),
                    oOptions.duration,
                    function() {
                        oUtil.available = true;
                        oUtil.boxPublish(oOptions.endEventType);
                    }
                );
            }
        },
        
        stop: function(jElm) {
            if(jElm && !this.available) {
                jElm.stop();
                this.available = true;
            }
        }
    };
    
    return UtilDOMBridge;
});

box.get('util').addConstructor('template', function($, box) {
    var reTplDatas = /{data@([a-zA-Z0-9.]+)}/g,
        reTplUiDatas = /{ui@([a-zA-Z0-9.]+)}/g;
    
    var UtilTemplate = function(sId) {
        this.id = sId;
    };
    UtilTemplate.prototype = {
        boxCreate: function(aTpl) {
            this.tpl = aTpl;
        },
        
        setDatas: function(datas, uiDatas) {
            this.datas = datas;
            this.uiDatas = uiDatas;
            return this;
        },
        
        getDatas: function(o, n) {
            if(!n) {
                n = o;
                o = this.datas;
            }
            if(n.indexOf('.') > -1) {
                var ns = n.split('.'), i = 0, l = ns.length, d = o;
                while(i < l) {
                    d = d[ns[i]];
                    if(d === undefined) {
                        return undefined;
                    }
                    i++;
                }
                return d;
            } else {
                return o[n];
            }
        },
        
        compile: function() {
            var that = this, i = 0, l = that.tpl.length, tmp, o, j, lg, p, compiled = '';
            
            while(i < l) {
                tmp = that.tpl[i];
                if(typeof tmp == 'object') {
                    if(tmp['loop-object']) {
                        o = that.getDatas(tmp['loop-object']);
                        if(o) {
                            for(p in o) {
                                if(o.hasOwnProperty(p) && !o[p].disabled) {
                                    compiled += tmp.tpl.replace(reTplDatas, function(m, n) {
                                        return that.getDatas(o[p], n) !== undefined ? that.getDatas(o[p], n) : '';
                                    });
                                }
                            }
                        }
                    } else if(tmp['loop-array']) {
                        o = that.getDatas(tmp['loop-array']);
                        if(o) {
                            for(j = 0, lg = o.length; j < lg; j++) {
                                if(!o[j].disabled) {
                                    compiled += tmp.tpl.replace(reTplDatas, function(m, n) {
                                        return that.getDatas(o[j], n) !== undefined ? that.getDatas(o[j], n) : '';
                                    });
                                }
                            }
                        }
                    }
                } else {
                    compiled += tmp.replace(reTplDatas, function(m, n) {
                        return that.getDatas(n) !== undefined ? that.getDatas(n) : '';
                    });
                }
                
                i++;
            }
            
            if(this.uiDatas) {
                compiled = compiled.replace(reTplUiDatas, function(m, n) {
                    return that.uiDatas[n] !== undefined ? that.uiDatas[n] : '';
                });
            }
            
            return compiled;
        }
    };
    
    return UtilTemplate;
});

box.get('util').addConstructor('loadscript', function($, box) {
    var UtilLoadScript = function(sId) {
        this.id = sId;
    };
    UtilLoadScript.prototype = {
        request: function(oDatas) {
            var oUtil = this;
            if(oUtil.available === true) {
                oUtil.xhr = $.ajax({
                    type: 'GET',
                    url: oDatas.url,
                    dataType: 'script',
                    cache: oDatas.cache !== false,
                    error: function() {
                        oUtil.xhr = null;
                        oUtil.boxPublish('requesterror');
                    },
                    success: function() {
                        oUtil.xhr = null;
                        oUtil.boxPublish('requestsuccess');
                    }
                });
            }
        },
        
        abort: function() {
            if(this.available === false && this.xhr) {
                this.xhr.abort();
                this.available = true;
            }
        }
    };
    
    return UtilLoadScript;
});

box.get('util').addModule('delegate-click', function($, box) {
    var manageDelegate = function(oEvt) {
        var oDelegates = $(this).data('boxDelegates'),
            sProp, oDatas, i, oTargetElm;
        for(sProp in oDelegates) {
            if(oDelegates.hasOwnProperty(sProp) && sProp != 'total') {
                oDatas = oDelegates[sProp];
                i = oDatas.deepness || 1000;
                oTargetElm = oEvt.target;
                while(i-- && oTargetElm) {
                    if(oDatas.test(oTargetElm)) {
                        oDatas.handler(oEvt, oTargetElm);
                        break;
                    }
                    if(oTargetElm == this) {
                        break;
                    }
                    oTargetElm = oTargetElm.parentNode;
                }
            }
        }
    };
    
    return {
        add: function(oDatas) {
            var jRoot = $(oDatas.root || document),
                oDelegates = jRoot.data('boxDelegates');
            if(!oDelegates) {
                oDelegates = { total: 0 };
                jRoot.bind('click.boxDelegates', manageDelegate);
            }
            oDelegates[oDatas.id] = oDatas;
            oDelegates.total++;
            jRoot.data('boxDelegates', oDelegates);
            return this;
        },
        
        remove: function(oDatas) {
            var jRoot = $(oDatas.root || document),
                oDelegates = jRoot.data('boxDelegates');
            if(oDelegates[oDatas.id]) {
                delete oDelegates[oDatas.id];
                oDelegates.total--;
                jRoot.data('boxDelegates', oDelegates);
                if(oDelegates.total === 0) {
                    jRoot.unbind('click.boxDelegates');
                }
            }
            return this;
        }
    };
});

box.getURLParams = function() {
    var qs = location.search, r = {};
    if(qs) {
        qs = qs.replace('?', '');
        var p = qs.split('&'), i = p.length, t;
        while(i--) {
            t = p[i].split('=');
            r[t[0]] = t[1];
        }
    }
    return r;
};

box.getURLHashParts = function() {
    var h = location.hash, r = {};
    if(h) {
        h = h.replace('#', '');
        var p = h.split(';'), i = p.length, t;
        while(i--) {
            t = p[i].split('=');
            r[t[0]] = t[1];
        }
    }
    return r;
};

box.get('util').addModule('escape', function($, box) {
    var aStore = [],
        oEscape =  {
            disable: function() {
                if(this.disabled !== true) {
                    box.getJDoc().unbind('keydown.escape');
                    this.disabled = true;
                }
                return this;
            },
            
            enable: function() {
                if(this.disabled !== true) {
                    box.getJDoc().bind('keydown.escape', function(oEvt) {
                        if(oEvt.which == 27 && aStore.length) {
                            var sId = aStore.pop();
                            box.publish({ type: 'escape', label: 'util:key.' + sId, source: this, propagation: false });
                        }
                    });
                    this.disabled = false;
                }
                return this;
            },
            
            add: function(sId) {
                aStore[aStore.length] = sId;
                return this;
            },
            
            remove: function(sId) {
                var i = aStore.length;
                while(i--) {
                    if(aStore[i] == sId) {
                        aStore.splice(i, 1);
                        break;
                    }
                }
                return this;
            },
            
            clear: function(uNotify) {
                if(uNotify === box.get('const:NOTIFY_ON')) {
                    var i = aStore.length;
                    while(i--) {
                        box.publish({ type: 'escape', label: 'util:key.' + aStore[i], source: this, propagation: false });
                    }
                }
                aStore = [];
                return this;
            }
        };
    
    return oEscape.enable();
});

box.get('ui').addConstructor('panel', function($, box) {
    var UiPanel = function(sID) {
        this.id = sID;
    };
    UiPanel.prototype = {
        boxCreate: function(datas) {
            var oUi = this;
            
            oUi.element = $(datas.html);
            oUi.dom = box.get('util').create('dombridge.' + oUi.boxGetName(), datas).bindToUi(this);
            
            if(datas.resizeStyles) {
                box.getJWin().bind('resize.' + oUi.boxName + oUi.id, function() {
                    oUi.dom.update(oUi.element);
                });
            }
            
            oUi.boxPublish('init');
        },
        
        boxDestroy: function() {
            this.element.remove();
            box.getJWin().unbind('resize.' + this.boxName + this.id);
        },
        
        show: function() {
            if(this.dom.available) {
                this.dom.insert(this.element, 'show');
            }
            return this;
        },
        
        hide: function() {
            if(this.dom.available) {
                this.dom.remove(this.element, 'hide');
            }
            return this;
        },
        
        stop: function() {
            this.dom.stop(this.element);
            return this;
        },
        
        remove: function() {
            this.stop();
            this.dom.remove(this.element, 'hide', true);
            return this;
        },
        
        update: function() {
            if(this.dom.available) {
                this.dom.update(this.element);
            }
        }
    };
    
    return UiPanel;
});

box.get('ui').addConstructor('mask', function($, box) {
    var UiMask = function(sID) {
        this.id = sID;
    };
    box.inherit(UiMask, box.get('ui:panel#constructor'));
    
    return UiMask;
});

box.get('ui').addConstructor('loader', function($, box) {
    var UiLoader = function(sID) {
        this.id = sID;
    };
    box.inherit(UiLoader, box.get('ui:panel#constructor'));
    
    return UiLoader;
});

box.get('ui').addConstructor('popin', function($, box) {
    var addPopinBindings, openTest, closeTest, UiPopin;
    
    openTest = function(oElm) {
        return (oElm.tagName === 'A' && $(oElm).getBoxDatas('action') === 'open-popin');
    };
    
    closeTest = function(oElm) {
        return (oElm.tagName === 'A' && $(oElm).getBoxDatas('action') === 'close-popin');
    };
    
    addPopinBindings = function(ui) {
        var subscribe = box.subscribe,
            sBoxId = ui.id,
            uiId = '@' + sBoxId + '>' + ui.boxGetName(true),
            uiFullReady = (ui.loader ? 'preloadready' : 'htmlready') + uiId,
            uiClose = 'close' + uiId,
            uiMaskShow, uiMaskHide, uiLoaderShow, uiLoaderHide,
            sOtherId;
        
        if(ui.mask) {
            sOtherId = ui.mask.boxGetName();
            uiMaskShow = 'show@' + sBoxId + '>' + sOtherId;
            uiMaskHide = 'hide@' + sBoxId + '>' + sOtherId;
        }
        if(ui.loader) {
            sOtherId = ui.loader.boxGetName();
            uiLoaderShow = 'show@' + sBoxId + '>' + sOtherId;
            uiLoaderHide = 'hide@' + sBoxId + '>' + sOtherId;
        }
        
        box.get('util:delegate-click').add({
            id: 'open' + uiId,
            deepness: 5,
            test: typeof ui.openTest === 'function' ? ui.openTest : openTest,
            handler: function(e, element) {
                e.preventDefault();
                if(ui.available) {
                    var jOpener = $(element),
                        datas = jOpener.getBoxDatas() || {},
                        hash = element.hash;
                    datas.opener = jOpener;
                    if(hash && ui.cache[element.hash]) {
                        datas.hash = hash;
                    } else {
                        datas.url = element.href;
                    }
                    ui.open(datas);
                }
            }
        });
        
        subscribe(
            {
                name: 'beforeopen' + uiId,
                handler: function(e) {
                    if(e.source.opened) {
                        e.source.pending = true;
                        e.source.close();
                    } else {
                        if(e.source.mask) {
                            e.source.mask.show();
                        } else if(e.source.loader) {
                            e.source.loader.show();
                        }
                    }
                }
            }, {
                name: 'htmlready' + uiId,
                handler: function(e) {
                    if(e.source.pending) {
                        e.source.pending = e.data.element;
                    } else {
                        e.source.element = e.data.element;
                    }
                    if(e.source.preload) {
                        e.source.preload.batch(e.source.preload.getURLs(e.data.element));
                    }
                }
            }, {
                name: 'addtodom' + uiId,
                handler: function(e) {
                    if(e.source.pending) {
                        e.source.element = e.source.pending;
                        e.source.pending = null;
                    }
                }
            }, {
                name: 'open' + uiId,
                handler: function(e) {
                    box.get('util:escape').add(e.source.boxGetName(true));
                    box.get('util:delegate-click').add({
                        id: uiClose,
                        root: e.source.element,
                        deepness: 5,
                        test: typeof ui.closeTest === 'function' ? ui.closeTest : closeTest,
                        handler: function(e, element) {
                            e.preventDefault();
                            ui.close();
                        }
                    });
                    e.source.opened = true;
                    e.source.available = true;
                }
            }, {
                name: 'beforeclose' + uiId,
                handler: function(e) {
                    box.get('util:escape').remove(e.source.boxGetName(true));
                    box.get('util:delegate-click').remove({ id: uiClose, root: ui.element });
                }
            }, {
                name: 'removefromdom' + uiId,
                handler: function(e) {
                    e.source.opened = false;
                    e.source.element = null;
                }
            }, {
                name: 'escape>util:key.' + ui.boxGetName(true),
                handler: function() {
                    ui.close();
                }
            }
        );
        
        if(!ui.mask && !ui.loader) {
            var fn = function(e) {
                if(e.source.pending) {
                    if(typeof e.source.pending != 'boolean') {
                        e.source.dom.insert(e.source.pending, 'open');
                    }
                } else {
                    e.source.dom.insert(e.source.element, 'open');
                }
                if(e.type == 'close') {
                    e.source.available = true;
                }
            };
            subscribe(
                { name: uiFullReady, handler: fn },
                { name: uiClose, handler: fn }
            );
        } else if(ui.mask && !ui.loader) {
            subscribe(
                {
                    name: uiFullReady,
                    handler: function(e) {
                        if(e.source.mask.dom.available) {
                            e.source.dom.insert(e.source.element || e.source.pending, 'open');
                        }
                    }
                }, {
                    name: uiClose,
                    handler: function(e) {
                        if(e.source.pending) {
                            if(typeof e.source.pending != 'boolean') {
                                e.source.dom.insert(e.source.pending, 'open');
                            }
                        } else {
                            e.source.mask.hide();
                        }
                    }
                }, {
                    name: uiMaskShow,
                    handler: function(e) {
                        if(ui.element) {
                            ui.dom.insert(ui.element, 'open');
                        }
                    }
                }, {
                    name: uiMaskHide,
                    handler: function(e) {
                        ui.available = true;
                    }
                }
            );
        } else if(!ui.mask && ui.loader) {
            subscribe(
                {
                    name: uiFullReady,
                    handler: function(e) {
                        if(e.source.loader.dom.available) {
                            e.source.loader.hide();
                        }
                    }
                }, {
                    name: uiClose,
                    handler: function(e) {
                        if(e.source.pending) {
                            if(e.source.preload.available && typeof e.source.pending != 'boolean') {
                                e.source.dom.insert(e.source.pending, 'open');
                            } else {
                                e.source.loader.show();
                            }
                        } else {
                            e.source.available = true;
                        }
                    }
                }, {
                    name: uiLoaderShow,
                    handler: function(e) {
                        if(ui.preload.available) {
                            if(ui.element) {
                                e.source.hide();
                            } else if(ui.pending && typeof ui.pending != 'boolean') {
                                e.source.hide();
                            }
                        }
                    }
                }, {
                    name: uiLoaderHide,
                    handler: function(e) {
                        ui.dom.insert(ui.element || ui.pending, 'open');
                    }
                }
            );
        } else {
            subscribe(
                {
                    name: uiFullReady,
                    handler: function(e) {
                        if(e.source.mask.dom.available && e.source.loader.dom.available) {
                            e.source.loader.hide();
                        }
                    }
                }, {
                    name: uiClose,
                    handler: function(e) {
                        if(e.source.pending) {
                            if(e.source.preload.available && typeof e.source.pending != 'boolean') {
                                e.source.dom.insert(e.source.pending, 'open');
                            } else {
                                e.source.loader.show();
                            }
                        } else {
                            e.source.mask.hide();
                        }
                    }
                }, {
                    name: uiMaskShow,
                    handler: function(e) {
                        if(ui.preload.available && ui.element) {
                            ui.dom.insert(ui.element, 'open');
                        } else {
                            ui.loader.show();
                        }
                    }
                }, {
                    name: uiMaskHide,
                    handler: function(e) {
                        ui.available = true;
                    }
                }, {
                    name: uiLoaderShow,
                    handler: function(e) {
                        if(ui.preload.available) {
                            if(ui.element) {
                                e.source.hide();
                            } else if(ui.pending && typeof ui.pending != 'boolean') {
                                e.source.hide();
                            }
                        }
                    }
                }, {
                    name: uiLoaderHide,
                    handler: function(e) {
                        ui.dom.insert(ui.element || ui.pending, 'open');
                    }
                }
            );
        }
    };
    
    UiPopin = function(sId) {
        this.id = sId;
    };
    UiPopin.prototype = {
        boxCreate: function(datas) {
            var sId = this.boxGetName(true);
            
            if(datas.mask) {
                this.mask = box.get('ui:mask.' + datas.mask);
            }
            if(datas.loader) {
                this.loader = box.get('ui:loader.' + datas.loader);
                this.preload = box.get('util').create('loadimage.' + sId, datas).bindToUi(this);
            }
            
            this.openTest = datas.openTest;
            this.closeTest = datas.closeTest;
            
            this.html = box.get('util').create('loadhtml.popin.' + sId, datas).bindToUi(this);
            this.dom = box.get('util').create('dombridge.popin.' + sId, datas).bindToUi(this);
            this.cache = {};
            
            this.enable();
            
            this.boxPublish('init');
        },
        
        boxDestroy: function() {
            this.disable();
        },
        
        boxGetName: function(bIgnoreNamespace) {
            var sBoxId = this.boxName + '.' + this.id;
            return bIgnoreNamespace ? sBoxId : sBoxId + (this.datas && this.datas.id ? '.' + this.datas.id : '');
        },
        
        addToCache: function(sSelector) {
            var oUi = this;
            $(sSelector).each(function(i, oElm) {
                var sId = oElm.id;
                if(sId) {
                    oUi.cache['#' + sId] = $(oElm).outerHTML();
                    oElm.parentNode.removeChild(oElm);
                }
            });
        },
        
        disable: function() {
            var sId = this.id,
                sBoxId = this.boxGetName(true),
                sOtherId;
            box.get('util:delegate-click').remove({ id: 'open@' + sId + '>' + sBoxId });
            box.unsubscribe(
                'htmlready@' + sId + '>' + sBoxId,
                'preloadready@' + sId + '>' + sBoxId,
                'beforeopen@' + sId + '>' + sBoxId,
                'beforeclose@' + sId + '>' + sBoxId,
                'open@' + sId + '>' + sBoxId,
                'close@' + sId + '>' + sBoxId,
                'removefromdom@' + sId + '>' + sBoxId
            );
            if(this.mask) {
                sOtherId = this.mask.boxGetName();
                box.unsubscribe(
                    'show@' + sId + '>' + sOtherId,
                    'hide@' + sId + '>' + sOtherId
                );
            }
            if(this.loader) {
                sOtherId = this.loader.boxGetName();
                box.unsubscribe(
                    'show@' + sId + '>' + sOtherId,
                    'hide@' + sId + '>' + sOtherId
                );
            }
            this.available = false;
        },
        
        enable: function() {
            addPopinBindings(this);
            this.available = true;
        },
        
        error: function(html) {
            this.available = false;
            this.boxPublish('beforeopen');
            this.html.set(html);
        },
        
        open: function(datas) {
            if(this.available) {
                this.available = false;
                this.datas = datas;
                if(typeof this.datas.cache == 'string') {
                    this.datas.cache = this.datas.cache == 'false' ? false : true;
                }
                this.boxPublish('beforeopen');
                var sHash = this.datas.hash;
                if(sHash && this.cache[sHash]) {
                    this.html.set(this.cache[sHash]);
                } else {
                    this.html.request(this.datas);
                }
            }
        },
        
        close: function() {
            this.available = false;
            this.boxPublish('beforeclose');
            this.dom.remove(this.element, 'close');
        }
    };
    
    return UiPopin;
});

box.get('ui').addConstructor('slideshow', function($, box) {
    var getSlideshowDatas, getPrevSlide, getNextSlide, changeInToolbar, getCurrentSlide, 
        createSlideshow, createSlideshowToolbar, playSlideshow, pauseSlideshow, addSlideshowBindings,
        UiSlideshow;
    
    getSlideshowDatas = function(ui, srcElm) {
        var datas = [];
        $(srcElm).each(function(i) {
            var elm = $(this);
            datas[i] = {};
            datas[i].title = elm.find('.title').html();
            datas[i].description = elm.find('.description').html();
            datas[i].lowres = elm.find('.lowres').attr('href');
            datas[i].highres = elm.find('.highres').attr('href');
        });
        return datas;
    };
    
    getPrevSlide = function(ui) {
        return ui.datas[ui.current === 0 ? ui.datas.length - 1 : ui.current - 1];
    };
    
    getNextSlide = function(ui) {
        return ui.datas[ui.current == ui.datas.length - 1 ? 0 : ui.current + 1];
    };
    
    changeInToolbar = function(ui) {
        if(ui.counter) {
            ui.counter.html(ui.current + 1);
        }
        var title;
        if(ui.btnPrev) {
            title = ui.datasToolbar.btn.prev.title.replace('{ui@prevImageTitle}', getPrevSlide(ui).title);
            ui.btnPrev.attr('title', title);
        }
        if(ui.btnNext) {
            title = ui.datasToolbar.btn.next.title.replace('{ui@nextImageTitle}', getNextSlide(ui).title);
            ui.btnNext.attr('title', title);
        }
    };
    
    getCurrentSlide = function(ui) {
        return ui.datas[ui.current];
    };
    
    createSlideshow = function(ui, html, datas) {
        ui.datasMain = datas;
        
        var tpl = box.get('util').create('template.slideshow', html);
        
        ui.slideshow = box.get('util').create('dombridge.' + ui.boxGetName(true), datas).bindToUi(ui);
        ui.slideshowElement = $(tpl.setDatas(datas).compile());
    };
    
    createSlideshowToolbar = function(ui, html, datas) {
        ui.datasToolbar = datas;
        
        if(ui.autoplay) {
            if(datas.btn.play) {
                datas.btn.play.disabled = true;
            }
        } else {
            if(datas.btn.pause) {
                datas.btn.pause.disabled = true;
            }
        }
        
        var uiDatas = {
            currentIndex: ui.current + 1,
            totalImages: ui.datas.length,
            prevImageTitle: getPrevSlide(ui).title,
            nextImageTitle: getNextSlide(ui).title,
            currentImageTitle: getCurrentSlide(ui).title
        };
        
        var tpl = box.get('util').create('template.slideshow-toolbar', html);
        
        ui.toolbar = box.get('util').create('dombridge.' + ui.boxGetName(true), datas).bindToUi(ui);
        ui.toolbarElement = $(tpl.setDatas(datas, uiDatas).compile());
    };
    
    playSlideshow = function(ui) {
        ui.autoplay = true;
        ui.timer = box.getWin().setTimeout(function() {
            ui.next();
        }, ui.playTime);
    };
    
    pauseSlideshow = function(ui) {
        box.getWin().clearTimeout(ui.timer);
        ui.timer = null;
    };
    
    addSlideshowBindings = function(ui) {
        var uiId = '@' + ui.id + '>' + ui.boxGetName(true),
            uiLoaderId = '@' + ui.id + '>' + ui.loader.boxGetName();
        
        box.subscribe(
            {
                name: 'preloadready' + uiId,
                handler: function(e) {
                    e.source.currentImage = {src: e.data.src, width: e.data.width, height: e.data.height};
                    if(e.source.loader.dom.available) {
                        e.source.loader.hide();
                    }
                }
            }, {
                name: 'show' + uiLoaderId,
                handler: function(e) {
                    if(ui.currentImage) {
                        ui.loader.hide();
                    }
                }
            }, {
                name: 'hide' + uiLoaderId,
                handler: function(e) {
                    ui.image.attr('src', ui.currentImage.src).attr('alt', getCurrentSlide(ui).title);
                    ui.slide.animate({width: ui.currentImage.width, height: ui.currentImage.height}, 800, function() {
                        ui.image.animate({opacity: 1}, 400, function() {
                            changeInToolbar(ui);
                            ui.available = true;
                            ui.boxPublish('change');
                            if(ui.autoplay) {
                                playSlideshow(ui);
                            }
                        });
                    });
                }
            }, {
                name: 'addtodom' + uiId,
                handler: function(e) {
                    if(e.source.namespace == 'slideshow') {
                        e.source.slide = $('#' + e.source.datasMain.slideId);
                        e.source.image = e.source.slideshowElement.find('img').css('opacity', '0');
                    }
                }
            }, {
                name: 'addslideshow' + uiId,
                handler: function(e) {
                    e.source.loader.show();
                    e.source.preload.single(e.source.datas[e.source.current][e.source.useSrc]);
                    if(e.source.toolbar) {
                        e.source.namespace = 'toolbar';
                        e.source.toolbar.insert(e.source.toolbarElement, 'addtoolbar');
                    }
                }
            }, {
                name: 'addtoolbar' + uiId,
                handler: function(e) {
                    var d = e.source.datasToolbar;
                    if(d.counter && d.counter.id) {
                        e.source.counter = $('#' + d.counter.id);
                    }
                    if(d.btn) {
                        if(d.btn.prev) {
                            e.source.btnPrev = e.source.toolbarElement.find('a.' + d.btn.prev.cls);
                        }
                        if(d.btn.next) {
                            e.source.btnNext = e.source.toolbarElement.find('a.' + d.btn.next.cls);
                        }
                        if(d.btn.play && d.btn.pause) {
                            e.source.btnPlayPause = e.source.toolbarElement.find('a.' + d.btn[e.source.autoplay ? 'pause' : 'play'].cls);
                        }
                    }
                }
            }
        );
    };
    
    UiSlideshow = function(sId) {
        this.id = sId;
    };
    UiSlideshow.prototype = {
        boxCreate: function(datas) {
            this.datas = getSlideshowDatas(this, datas.source);
            this.useSrc = datas.useSrc || 'highres';
            this.current = (datas.start && datas.start < this.datas.length && datas.start > 0) ? datas.start : 0;
            
            this.loader = box.get('ui:loader.' + datas.loader);
            this.preload = box.get('util').create('loadimage.' + this.boxGetName(true), datas).bindToUi(this);
            
            this.autoplay = datas.autoplay === true;
            this.playTime = datas.playTime || 5000;
            
            if(datas.datasToolbar) {
                createSlideshowToolbar(this, datas.tplToolbar, datas.datasToolbar);
            }
            
            createSlideshow(this, datas.tplMain, datas.datasMain);
            
            this.enable();
            
            this.boxPublish('init');
        },
        
        boxDestroy: function() {
            this.disable();
        },
        
        boxGetName: function(bIgnoreNamespace) {
            var sBoxId = this.boxName + '.' + this.id;
            return bIgnoreNamespace ? sBoxId : sBoxId + (this.namespace ? '.' + this.namespace : '');
        },
        
        disable: function() {
            if(this.autoplay) {
                this.pause();
            }
            var uiId = '@' + this.id + '>' + this.boxGetName(true),
                uiLoaderId = '@' + this.id + '>' + this.loader.boxGetName();
            box.unsubscribe(
                'preloadready' + uiId,
                'addtodom' + uiId,
                'addslideshow' + uiId,
                'addtoolbar' + uiId,
                'show' + uiLoaderId,
                'hide' + uiLoaderId
            );
            if(this.toolbarElement) {
                box.get('util:delegate-click').remove({ id: 'btn.slideshow', root: this.toolbarElement });
            }
            this.available = false;
        },
        
        enable: function() {
            var oUi = this;
            addSlideshowBindings(oUi);
            oUi.namespace = 'slideshow';
            oUi.slideshow.insert(oUi.slideshowElement, 'addslideshow');
            if(oUi.toolbarElement) {
                box.get('util:delegate-click').add({
                    id: 'btn.slideshow',
                    root: oUi.toolbarElement,
                    deepness: 3,
                    test: function(element) {
                        return (element.nodeName.toLowerCase() == 'a' && element.className);
                    },
                    handler: function(e, element) {
                        e.preventDefault();
                        var cls = element.className.replace(/[\s\t]*/g, '');
                        if(cls && oUi[cls]) {
                            oUi[cls]();
                        }
                        oUi.boxPublish('clicktoolbar', { element: element, cls: cls });
                    }
                });
            }
        },
        
        getImageDatas: function() {
            return getCurrentSlide(this);
        },
        
        prev: function() {
            var that = this;
            if(that.available) {
                pauseSlideshow(that);
                that.currentImage = null;
                that.available = false;
                that.current = that.current === 0 ? that.datas.length - 1 : that.current - 1;
                that.preload.single(that.datas[that.current][this.useSrc]);
                that.image.animate({opacity: 0}, 400, function() {
                    that.loader.show();
                });
            }
        },
        
        next: function() {
            var that = this;
            if(that.available) {
                pauseSlideshow(that);
                that.currentImage = null;
                that.available = false;
                that.current = that.current == that.datas.length - 1 ? 0 : that.current + 1;
                that.preload.single(that.datas[that.current][this.useSrc]);
                that.image.animate({opacity: 0}, 400, function() {
                    that.loader.show();
                });
            }
        },
        
        play: function() {
            var btn = this.datasToolbar.btn;
            if(this.btnPlayPause && this.btnPlayPause.length) {
                this.btnPlayPause
                    .removeClass(btn.play.cls)
                    .addClass(btn.pause.cls)
                    .attr('title', btn.pause.title)
                    .html(btn.pause.text);
            }
            playSlideshow(this);
        },
        
        pause: function() {
            box.getWin().clearTimeout(this.timer);
            this.timer = null;
            this.autoplay = false;
            if(this.btnPlayPause && this.btnPlayPause.length) {
                var btn = this.datasToolbar.btn;
                this.btnPlayPause
                    .removeClass(btn.pause.cls)
                    .addClass(btn.play.cls)
                    .attr('title', btn.play.title)
                    .html(btn.play.text);
            }
        }
    };
    
    return UiSlideshow;
});

box.get('ui').addConfig('draggable', {
    cls: 'on'
}).addConstructor('draggable', function($, box) {
    var getEvtPageXY = function(jEvt) {
        var bTouch = jEvt.originalEvent.touches && jEvt.originalEvent.touches.length,
            oSrc = bTouch ? jEvt.originalEvent.touches[0] : jEvt;
        return {
            pageX: oSrc.pageX,
            pageY: oSrc.pageY
        };
    };
    
    var UiDraggable = function(sID, oDefaultCfg) {
        this.id = sID;
        this.cfg = oDefaultCfg;
    };
    UiDraggable.prototype = {
        boxCreate: function(oDatas) {
            this.rootElm = $(oDatas.rootElm);
            this.handleElm = oDatas.handleElm ? $(oDatas.handleElm) : null;
            if(!this.handleElm || !this.handleElm.length) {
                this.handleElm = this.rootElm;
            }
            
            this.setMinMax(oDatas);
            this.enable();
            
            this.boxPublish('init');
        },
        
        boxDestroy: function() {
            this.disable();
        },
        
        setMinMax: function(oDatas) {
            if(typeof oDatas == 'object') {
                if(oDatas.bindTo) {
                    var bindTo = $(oDatas.bindTo == 'document' ?
                        box.getDoc() :
                        oDatas.bindTo == 'viewport' ?
                            box.getWin() :
                            oDatas.bindTo);
                    var posRef = bindTo.getXY();
                    var dimRef = bindTo.getSize('border-box');
                    var dimElm = this.rootElm.getSize('border-box');
                    this.minX = posRef.left;
                    this.maxX = this.minX + dimRef.width - dimElm.width;
                    this.minY = posRef.top;
                    this.maxY = this.minY + dimRef.height - dimElm.height;
                    this.rootElm.css({top: this.minY, left: this.minX});
                    bindTo = null;
                } else {
                    this.minX = typeof oDatas.minX == 'number' ? oDatas.minX : this.minX;
                    this.maxX = typeof oDatas.maxX == 'number' ? oDatas.maxX : this.maxX;
                    this.minY = typeof oDatas.minY == 'number' ? oDatas.minY : this.minY;
                    this.maxY = typeof oDatas.maxY == 'number' ? oDatas.maxY : this.maxY;
                }
            }
            return this;
        },
        
        disable: function() {
            if(this.disabled === false) {
                this.rootElm.removeClass(this.cfg.cls);
                this.handleElm.unbind('mousedown.' + this.id).unbind('touchstart.' + this.id);
                this.disabled = true;
            }
        },
        
        enable: function() {
            var that = this;
            if(that.disabled !== false) {
                var startMove = function(e) {
                    that.startMove(e);
                };
                that.handleElm.bind('mousedown.' + that.id, startMove).bind('touchstart.' + that.id, startMove);
                this.rootElm.addClass(this.cfg.cls);
                that.disabled = false;
            }
        },
        
        move: function(e) {
            e.preventDefault();
            var oXY = getEvtPageXY(e),
                y = oXY.pageY - this.sy,
                x = oXY.pageX - this.sx;
            if(this.minX !== undefined) {x = Math.max(x, this.minX);}
            if(this.maxX !== undefined) {x = Math.min(x, this.maxX);}
            if(this.minY !== undefined) {y = Math.max(y, this.minY);}
            if(this.maxY !== undefined) {y = Math.min(y, this.maxY);}
            this.rootElm.css({
                'top': y +'px',
                'left': x +'px'
            });
            this.boxPublish('move', { x: x, y: y });
        },
        
        startMove: function(e) {
            e.preventDefault();
            var that = this,
                oXY = getEvtPageXY(e),
                x = parseFloat(that.rootElm.css('left')) || 0,
                y = parseFloat(that.rootElm.css('top')) || 0,
                endMove = function(e) {
                    that.endMove(e);
                },
                move = function(e) {
                    that.move(e);
                };
            that.rootElm.css('top', y + 'px');
            that.rootElm.css('left', x + 'px');
            that.sx = oXY.pageX - x;
            that.sy = oXY.pageY - y;
            box.getJDoc()
                .bind('mouseup.' + that.id, endMove)
                .bind('mousemove.' + that.id, move)
                .bind('touchend.' + that.id, endMove)
                .bind('touchmove.' + that.id, move);
            that.boxPublish('startmove');
        },
        
        endMove: function(e) {
            box.getJDoc()
                .unbind('mouseup.' + this.id)
                .unbind('mousemove.' + this.id)
                .unbind('touchend.' + this.id)
                .unbind('touchmove.' + this.id);
            this.boxPublish('endmove');
        }
    };
    
    return UiDraggable;
});

box.get('ui').addConfig('scroll', {
    wrapScrollbar: '<div class="{$wrapScrollbarCls}">{$content}</div>',
    wrapScrollbarCls: 'scrollbar',
    
    wrapContent: '<div class="{$wrapContentCls}"></div>',
    wrapContentScrollCls: 'scrolled',
    wrapContentNoScrollCls: 'notScrolled',
    
    btnPrev: '<span class="{$btnPrevCls}"></span>',
    btnPrevCls: 'prev',
    
    btnNext: '<span class="{$btnNextCls}"></span>',
    btnNextCls: 'next',
    
    gutter: '<div class="{$gutterCls}">{$bar}</div>',
    gutterCls: 'gutter',
    
    bar: '<a href="#" class="{$barCls}"></a>',
    barCls: 'bar'
}).addConstructor('scroll', function($, box) {
    var dimTotal, dimPartial, bIE6,
        getScrollbarHTML, addBarDraggable, clickToPosition, wheelEvent,
        UiScroll;
    
    dimTotal = { top: 'offsetHeight', left: 'offsetWidth' };
    dimPartial = { top: 'height', left: 'width' };
    bIE6 = box.ie6;
    
    getScrollbarHTML = function(oUi, bar, buttons) {
        var tmp = bar ? oUi.cfg.gutter.replace('{$bar}', oUi.cfg.bar) : '';
        if(buttons) {
            tmp = oUi.cfg.btnPrev + tmp + oUi.cfg.btnNext;
        }
        var html = oUi.cfg.wrapScrollbar.replace('{$content}', tmp);
        $.each(['wrapScrollbar', 'btnPrev', 'btnNext', 'gutter', 'bar'], function(i, cls) {
            html = html.replace('{$' + cls + 'Cls}', oUi.cfg[cls + 'Cls']);
        });
        return html;
    };
    
    addBarDraggable = function(oUi) {
        box.get('ui').create('draggable.' + oUi.dragId, {
            rootElm: oUi.bar
        });
        
        if(oUi.position == 'top') {
            box.get('ui:draggable.' + oUi.dragId).setMinMax({
                minX: 0,
                maxX: 0,
                minY: 0
            });
        } else {
            box.get('ui:draggable.' + oUi.dragId).setMinMax({
                minX: 0,
                minY: 0,
                maxY: 0
            });
        }
        
        box.subscribe('move>ui:draggable.' + oUi.dragId, function(oEvt) {
            var coord = oEvt.data[oUi.dragCoord];
            if(coord == Math.round(oUi.size.scrollDiff)) {
                coord = oUi.size.scrollDiff;
            }
            var pos = Math.round(coord / oUi.size.scrollDiff * oUi.size.elementDiff);
            oUi.wrapper.css(oUi.position, - pos + 'px');
        });
    };
    
    clickToPosition = function(oEvt, oUi) {
        oEvt.preventDefault();
        var t = $(oEvt.target), pos;
        if(t.hasClass(oUi.cfg.btnPrevCls)) {
            pos = Math.round(oUi.getWrapperOffset() + oUi.moveBy);
            oUi.moveContentTo(pos);
        } else if(t.hasClass(oUi.cfg.btnNextCls)) {
            pos = Math.round(oUi.getWrapperOffset() - oUi.moveBy);
            oUi.moveContentTo(pos);
        } else if(t.hasClass(oUi.cfg.gutterCls)) {
            var coord = oUi.position == 'top' ? oEvt.pageY : oEvt.pageX;
            pos = coord - oUi.gutter.getXY()[oUi.position] - Math.round(oUi.size.bar / 2);
            oUi.moveBarTo(pos);
        }
        t = null;
    };
    
    wheelEvent = function(oEvt, oUi) {
        if(!oUi.disabled) {
            oEvt.preventDefault();
            var n = oEvt.detail ? - oEvt.detail / 3 : oEvt.wheelDelta / 120;
            var pos = Math.round(oUi.getWrapperOffset() + (n * oUi.moveBy));
            oUi.moveContentTo(pos);
        }
    };
    box.wheelEventForScroll = wheelEvent; // for IE6
    
    // @todo add some events
    UiScroll = function(sID, oDefaultCfg) {
        this.id = sID;
        this.cfg = oDefaultCfg;
    };
    UiScroll.prototype = {
        boxCreate: function(datas) {
            var that = this;
            
            that.direction = datas.horizontal ? 'horizontal' : 'vertical';
            that.position = that.direction == 'vertical' ? 'top' : 'left';
            that.moveBy = (!isNaN(datas.moveBy) && datas.moveBy > 0) ? datas.moveBy : null;
            that.barMinSize = (!isNaN(datas.barMinSize) && datas.barMinSize > 10) ? datas.barMinSize : 10;
            
            that.element = $(datas.rootElm);
            var wrapHTML = that.cfg.wrapContent.replace('{$wrapContentCls}', that.cfg.wrapContentNoScrollCls);
            if(!that.element.html()) {
                that.element.html(wrapHTML);
            } else {
                that.element.wrapInner(wrapHTML);
            }
            that.wrapper = that.element.children();
            
            var insertMethod = datas.insertMethod || 'prependTo',
                insertTarget = datas.insertTarget || that.element;
            
            // @todo add support for scroll without bar
            if(!datas.bar && !datas.buttons) {
                datas.bar = true;
            }
            that.scrollbar = $(getScrollbarHTML(that, datas.bar, datas.buttons))[insertMethod](insertTarget);
            if(datas.bar) {
                that.gutter = that.scrollbar.find('.' + that.cfg.gutterCls);
                that.bar = that.scrollbar.find('.' + that.cfg.barCls);
            }
            
            that.dragId = 'scroll.' + that.id;
            that.dragCoord = that.position == 'top' ? 'y' : 'x';
            addBarDraggable(this);
            
            that.boxPublish('beforefirstcompute');
            
            that.compute();
            
            if(that.wrapper.find('img').length && !box.loadIsDone) {
                box.getJWin().load(function() {
                    that.compute();
                });
            }
            
            that.boxPublish('init');
        },
        
        boxDestroy: function() {
            this.disable();
            box.get('ui').destroy('draggable.' + this.dragId);
            this.element.html(this.wrapper.html());
        },
        
        disable: function() {
            if(this.disabled !== true) {
                this.scrollbar.css('visibility', 'hidden');
                this.wrapper.removeClass(this.cfg.wrapContentScrollCls).addClass(this.cfg.wrapContentNoScrollCls);
                box.get('ui:draggable.' + this.dragId).disable();
                box.unsubscribe('move>ui:draggable.' + this.dragId);
                this.element.unbind('DOMMouseScroll').unbind('mousewheel');
                this.scrollbar.unbind('click');
                this.disabled = true;
            }
            return this;
        },
        
        enable: function() {
            var that = this;
            if(that.disabled !== false) {
                that.element.bind('DOMMouseScroll', function(e) {
                    wheelEvent(e, that);
                }).bind('mousewheel', function(e) {
                    wheelEvent(e, that);
                });
                
                that.scrollbar.click(function(e) {
                    clickToPosition(e, that);
                });
                
                box.get('ui:draggable.' + that.dragId).enable();
                
                that.wrapper.removeClass(that.cfg.wrapContentNoScrollCls).addClass(that.cfg.wrapContentScrollCls);
                
                // scrollbar should always be above the wrapper to be accessible
                var zIndex = parseInt(that.wrapper.css('zIndex'), 10);
                that.scrollbar.css({zIndex: isNaN(zIndex) ? 1 : ++zIndex, visibility: 'visible'});
                
                that.disabled = false;
            }
            return that;
        },
        
        reposition: function() {
            this.wrapper.css(this.position, 0);
            this.bar.css(this.position, 0);
            return this;
        },
        
        compute: function() {
            this.size = {};
            
            this.size.element = this.element[dimPartial[this.position]]();
            this.size.wrapper = this.wrapper[0][dimTotal[this.position]];
            
            if(this.size.wrapper > this.size.element) {
                this.size.gutter = this.gutter[0][dimTotal[this.position]];
                this.size.bar = this.size.element / this.size.wrapper * this.size.gutter;
                
                if(this.size.bar < this.barMinSize) {
                    this.size.bar = this.barMinSize;
                }
                
                // debug IE6 with bottom/right positioning inside bar
                if(bIE6 && Math.round(this.size.bar) % 2 !== 0) {
                    this.size.bar = Math.round(this.size.bar) - 1;
                }
                
                this.size.scrollDiff = this.size.gutter - this.size.bar;
                this.size.elementDiff = this.size.wrapper - this.size.element;
                
                this.bar.css(dimPartial[this.position], Math.round(this.size.bar) + 'px');
                
                if(!this.moveBy) {
                    var amount = Math.ceil((this.size.gutter - this.size.bar) / this.size.gutter * this.size.bar);
                    this.moveBy = (amount > 10) ? amount : 10;
                }
                
                var dragMax = {};
                if(this.direction == 'horizontal') {
                    dragMax.maxX = Math.round(this.size.scrollDiff);
                } else {
                    dragMax.maxY = Math.round(this.size.scrollDiff);
                }
                box.get('ui:draggable.' + this.dragId).setMinMax(dragMax);
                
                this.boxPublish('computesuccess');
                this.enable();
            } else {
                this.disable();
            }
            return this;
        },
        
        getWrapperOffset: function() {
            return parseInt(this.wrapper.css(this.position), 10) || 0;
        },
        
        moveBarTo: function(scrollPos) {
            if(!this.disabled && !isNaN(scrollPos)) {
                if(scrollPos < 0) {
                    scrollPos = 0;
                } else if(scrollPos > this.size.scrollDiff) {
                    scrollPos = this.size.scrollDiff;
                }
                var wrapperPos = - Math.round(Math.abs(scrollPos) / this.size.scrollDiff * this.size.elementDiff);
                this.wrapper.css(this.position, wrapperPos + 'px');
                this.bar.css(this.position, Math.round(scrollPos) + 'px');
            }
            return this;
        },
        
        moveContentTo: function(wrapperPos) {
            if(!this.disabled && !isNaN(wrapperPos)) {
                if(wrapperPos > 0) {
                    wrapperPos = 0;
                } else if(wrapperPos < -this.size.elementDiff) {
                    wrapperPos = -this.size.elementDiff;
                }
                var scrollPos = Math.round(Math.abs(wrapperPos) / this.size.elementDiff * this.size.scrollDiff);
                this.wrapper.css(this.position, Math.round(wrapperPos) + 'px');
                this.bar.css(this.position, scrollPos + 'px');
            }
            return this;
        },
        
        moveToElement: function(elm) {
            if(!this.disabled) {
                if(typeof elm == 'string') {
                    elm = this.wrapper.find(elm);
                }
                if(elm && elm.jquery && elm.length) {
                    var targetStart = elm.getXY('positioned-ancestor')[this.position],
                        targetDim = elm['get' + (this.position == 'top' ? 'Height' : 'Width')]('margin-box'),
                        targetEnd = targetStart + targetDim,
                        offset = -this.getWrapperOffset(),
                        visibleEnd = offset + this.size.element;
                    
                    if(targetStart < offset) {
                        this.moveContentTo(-targetStart);
                    } else if(targetEnd > visibleEnd) {
                        if(targetDim < this.size.element) {
                            this.moveContentTo(-(targetEnd - this.size.element));
                        } else {
                            this.moveContentTo(-targetStart);
                        }
                    }
                }
            }
            return this;
        }
    };
    
    return UiScroll;
});

box.get('ui').addConfig('carousel', {
    btnPrev: '<a href="#" class="{$btnPrevCls} {$btnDisabledCls}">{$btnPrevText}</a>',
    btnPrevCls: 'prev',
    
    btnNext: '<a href="#" class="{$btnNextCls} {$btnDisabledCls}">{$btnNextText}</a>',
    btnNextCls: 'next',
    
    btnDisabledCls: 'off',
    
    pagesWrap: '<div class="{$pagesWrapCls}"><ul>{$content}</ul></div>',
    pagesWrapCls: 'pagination',
    
    pagesItem: '<li{$pagesItemActiveCls}><a href="#">{$content}</a></li>',
    pagesItemActiveCls: 'on'
}).addConstructor('carousel', function($, box) {
    var getBtnsHTML, getPosition, getIndex, setCurrent, prepareCircularMovePrev, prepareCircularMoveNext,
        prepareMove, positionFirstElements, checkRepositionFirstElements, moveToPosition,
        getPageNumber, getPrevPageIndex, getNextPageIndex, managePagination,
        UiCarousel;
    
    getBtnsHTML = function(oUi, type) {
        type = type == 'next' ? 'btnNext' : 'btnPrev';
        return oUi.cfg[type]
            .replace('{$' + type + 'Cls}', oUi.cfg[type + 'Cls'])
            .replace('{$btnDisabledCls}', oUi.cfg.btnDisabledCls)
            .replace('{$' + type + 'Text}', oUi.cfg[type + 'Text']);
    };
    
    getPosition = function(carousel) {
        return parseInt(carousel.moveable.css(carousel.property), 10) || 0;
    };
    
    getIndex = function(carousel, index) {
        if(isNaN(index)) {
            return 0;
        } else if(index < 0) {
            return index + carousel.length;
        } else if(index < carousel.length) {
            return index;
        } else {
            return index - carousel.length;
        }
    };
    
    setCurrent = function(carousel, index) {
        carousel.current = getIndex(carousel, index);
        if(carousel.currentPage !== undefined) {
            var page = Math.ceil((carousel.current + carousel.display) / carousel.display);
            $('li', carousel.pagination)
                .eq(carousel.currentPage - 1)
                    .removeClass(carousel.cfg.pagesItemActiveCls)
                .end()
                .eq(page - 1)
                    .addClass(carousel.cfg.pagesItemActiveCls);
            carousel.currentPage = page;
        }
    };
    
    prepareCircularMovePrev = function(carousel, index) {
        if(carousel.autoplay) {
            carousel.pauseAutoplay();
        }
        
        carousel.moving = true;
        
        var actualPos = getPosition(carousel);
        var futurePos = actualPos + carousel.moveBy * (carousel.current - index);
        var itemPos = parseInt(carousel.items.eq(carousel.current).css(carousel.property), 10);
        
        var min = index - (carousel.hasOffset && actualPos % carousel.moveBy ? 1 : 0);
        var max = carousel.current;
        var c, pos;
        
        for(var i = min; i < max; i++) {
            c = getIndex(carousel, i);
            pos = itemPos - (carousel.current - i) * carousel.moveBy;
            carousel.items.eq(c).css(carousel.property, pos + 'px');
        }
        
        setCurrent(carousel, index);
        moveToPosition(carousel, futurePos);
    };
    
    prepareCircularMoveNext = function(carousel, index) {
        if(carousel.autoplaying) {
            carousel.pauseAutoplay();
        }
        
        carousel.moving = true;
        
        var actualPos = getPosition(carousel);
        var futurePos = actualPos + (-carousel.moveBy * (index - carousel.current));
        var itemPos = parseInt(carousel.items.eq(carousel.current).css(carousel.property), 10) + carousel.display * carousel.moveBy;
        
        var min = carousel.current + carousel.display - (carousel.hasOffset && actualPos % carousel.moveBy ? 1 : 0);
        var max = index + carousel.display - (carousel.hasOffset && actualPos % carousel.moveBy ? 1 : 0);
        var c, pos;
        
        for(var i = min; i < max; i++) {
            c = getIndex(carousel, i);
            pos = itemPos + (i - carousel.display - carousel.current) * carousel.moveBy;
            carousel.items.eq(c).css(carousel.property, pos + 'px');
        }
        
        setCurrent(carousel, index);
        moveToPosition(carousel, futurePos);
    };
    
    prepareMove = function(carousel, index) {
        if(carousel.autoplaying) {
            carousel.pauseAutoplay();
        }
        
        carousel.moving = true;
        
        index = Math.min(index, carousel.length - carousel.display);
        if(carousel.buttons) {
            if(!index) {
                carousel.buttonPrev.addClass(carousel.cfg.btnDisabledCls);
                carousel.buttonNext.removeClass(carousel.cfg.btnDisabledCls);
            } else if(index == carousel.length - carousel.display) {
                carousel.buttonPrev.removeClass(carousel.cfg.btnDisabledCls);
                carousel.buttonNext.addClass(carousel.cfg.btnDisabledCls);
            } else {
                carousel.buttonPrev.removeClass(carousel.cfg.btnDisabledCls);
                carousel.buttonNext.removeClass(carousel.cfg.btnDisabledCls);
            }
        }
        
        setCurrent(carousel, index);
        moveToPosition(carousel, -carousel.moveBy * index);
    };
    
    positionFirstElements = function(carousel, fromReposition) {
        var min = (carousel.hasOffset && carousel.offset) ? carousel.startAt - 1 : carousel.startAt;
        var max = min + carousel.length;
        var c, pos;
        for(var i = min; i < max; i++) {
            c = getIndex(carousel, i);
            carousel.items.eq(c).css(carousel.property, i * carousel.moveBy + 'px');
        }
    };
    
    checkRepositionFirstElements = function(carousel, to) {
        if(carousel.circular && to == (-(carousel.length * carousel.moveBy) + carousel.offset)) {
            carousel.moveable.css(carousel.property, carousel.offset + 'px');
            positionFirstElements(carousel, true);
        }
    };
    
    moveToPosition = function(carousel, to) {
        carousel.boxPublish('startmove');
        if(carousel.duration) {
            var p = {};
            p[carousel.property] = to;
            carousel.moveable.animate(p, carousel.duration, function() {
                checkRepositionFirstElements(carousel, to);
                if(carousel.autoplaying) {
                    carousel.startAutoplay(carousel.autoplay);
                }
                carousel.moving = false;
                carousel.boxPublish('endmove');
            });
        } else {
            carousel.moveable.css(carousel.property, to + 'px');
            checkRepositionFirstElements(carousel, to);
            if(carousel.autoplaying) {
                carousel.startAutoplay(carousel.autoplay);
            }
            carousel.moving = false;
            carousel.boxPublish('endmove');
        }
    };
    
    getPageNumber = function(carousel) {
        return Math.ceil(carousel.length / carousel.display);
    };
    
    getPrevPageIndex = function(carousel) {
        var page = carousel.currentPage - 1;
        if(page < 1) {
            page = carousel.circular ? getPageNumber(carousel) : 1;
        }
        return page * carousel.display - carousel.display;
    };
    
    getNextPageIndex = function(carousel) {
        var page = carousel.currentPage + 1;
        if(page > getPageNumber(carousel)) {
            page = carousel.circular ? 1 : getPageNumber(carousel);
        }
        return page * carousel.display - carousel.display;
    };
    
    managePagination = function(e, carousel) {
        if(e.target.nodeName.toLowerCase() == 'a') {
            e.preventDefault();
            if(!carousel.moving) {
                carousel.moveToItem((Number($(e.target).text()) - 1) * carousel.display + 1);
            }
        }
    };
    
    UiCarousel = function(sID, oDefaultCfg) {
        this.id = sID;
        this.cfg = oDefaultCfg;
    };
    UiCarousel.prototype = {
        boxCreate: function(datas) {
            var that = this;
            
            that.cfg.btnPrevText = datas.btnPrevText;
            that.cfg.btnNextText = datas.btnNextText;
            
            that.property = datas.horizontal ? 'left' : 'top';
            that.buttons = datas.buttons === false ? false : true;
            that.circular = !!datas.circular || false;
            that.duration = !isNaN(datas.duration) ? datas.duration : null;
            that.autoplay = !isNaN(datas.autoplay) && datas.autoplay > 10 && that.circular ? datas.autoplay : null;
            that.hasOffset = !!datas.hasOffset;
            
            that.element = $(datas.rootElm);
            that.mask = datas.mask ? that.element.find(datas.mask) : that.element.children();
            that.moveable = datas.moveable ? that.mask.find(datas.moveable) : that.mask.children();
            that.items = datas.items ? that.moveable.find(datas.items) : that.moveable.children();
            
            that.length = that.items.length;
            that.display = datas.display;
            that.startAt = !isNaN(datas.startAt) ? datas.startAt - 1 : 0;
            // startAt must be >= 0 && < length
            if(that.startAt < 0 || that.startAt >= that.length) {
                that.startAt = 0;
            }
            
            that.offset = parseInt(that.moveable.css(that.property), 10) || 0;
            // negative offset are forbidden
            // if offset, length must be > display + 1
            // no offset possible on a non circular carousel
            // a paginated carousel cannot be circular (too strange)
            if(that.hasOffset && that.offset > 0 && that.length > that.display + 1) {
                ++that.display;
            }
            that.moveBy = that.items.eq(0)[that.property == 'top' ? 'outerHeight' : 'outerWidth'](true);
            
            setCurrent(that, that.startAt);
            
            if(that.property == 'left') {
                that.moveable.width(that.moveBy * that.length);
            }
            if(that.circular) {
                positionFirstElements(that);
            }
            
            if(that.length > that.display) {
                that.disabled = false;
                
                if(!that.circular && that.current > that.length - that.display) {
                    that.current = that.length - that.display;
                }
                
                if(that.current) {
                    that.moveable.css(that.property, -that.moveBy * that.current + that.offset);
                    that.offset = -that.moveBy * that.current + that.offset;
                }
                
                if(that.buttons) {
                    that.buttonNext = $(getBtnsHTML(that, 'next')).appendTo(that.element).click(function(e) {
                        that.moveNext(e);
                        e.preventDefault();
                    });
                    that.buttonPrev = $(getBtnsHTML(that, 'prev')).prependTo(that.element).click(function(e) {
                        that.movePrev(e);
                        e.preventDefault();
                    });
                    
                    if(that.circular || that.current) {
                        that.buttonPrev.removeClass(that.cfg.btnDisabledCls);
                    }
                    if(that.circular || that.current + that.display < that.length) {
                        that.buttonNext.removeClass(that.cfg.btnDisabledCls);
                    }
                }
                
                if(datas.paginate) {
                    that.addPagination();
                }
                
                if(that.autoplay) {
                    that.startAutoplay(that.autoplay);
                }
            } else {
                that.disabled = true;
            }
        },
        
        movePrev: function() {
            if(!this.moving) {
                var index = !isNaN(this.currentPage) ? getPrevPageIndex(this) : this.current - 1;
                if(this.circular) {
                    prepareCircularMovePrev(this, index);
                } else if(index > -1) {
                    prepareMove(this, index);
                }
            }
        },
        
        moveNext: function() {
            if(!this.moving) {
                var index = !isNaN(this.currentPage) ? getNextPageIndex(this) : this.current + 1;
                if(this.circular) {
                    prepareCircularMoveNext(this, index);
                } else if(index < this.length) {
                    prepareMove(this, index);
                }
            }
        },
        
        moveToItem: function(i) {
            if(!this.moving && typeof i == 'number') {
                --i;
                if(this.items[i]) {
                    if(this.currentPage) {
                        var page = Math.floor(i / this.display) + 1;
                        i = (page - 1) * this.display;
                    }
                    if(this.circular) {
                        if(i > this.current && i - this.current > this.length - i) {
                            i = i - this.length;
                        } else if(i < this.current && this.current - i > i + this.length - this.current) {
                            i = this.length + i;
                        }
                        if(i < this.current) {
                            prepareCircularMovePrev(this, i);
                        } else if(i > this.current) {
                            prepareCircularMoveNext(this, i);
                        }
                    } else {
                        prepareMove(this, i);
                    }
                }
            }
        },
        
        startAutoplay: function(delay) {
            var that = this;
            if(that.circular && (!isNaN(delay) || that.autoplay)) {
                if(isNaN(delay)) {
                    delay = that.autoplay;
                } else {
                    that.autoplay = delay;
                }
                that.autoplaying = true;
                that.timer = box.getWin().setInterval(function() {
                    that.moveNext();
                }, delay);
            }
        },
        
        pauseAutoplay: function() {
            box.getWin().clearInterval(this.timer);
            this.timer = null;
        },
        
        endAutoplay: function() {
            this.pauseAutoplay();
            this.autoplaying = false;
        },
        
        addPagination: function() {
            var that = this,
                html = that.cfg.pagesWrap.replace('{$pagesWrapCls}', that.cfg.pagesWrapCls),
                pages = getPageNumber(that),
                items = '',
                startItem,
                endItem;
            for(var i = 1; i <= pages; i++) {
                startItem = (i - 1) * that.display;
                endItem = startItem + that.display - 1;
                if(that.startAt >= startItem && that.startAt <= endItem) {
                    that.currentPage = i;
                    items += that.cfg.pagesItem.replace('{$pagesItemActiveCls}', ' class="' + that.cfg.pagesItemActiveCls + '"');
                } else {
                    items += that.cfg.pagesItem.replace('{$pagesItemActiveCls}', '');
                }
                items = items.replace(/{\$content}/g, i);
            }
            html = html.replace('{$content}', items);
            this.pagination = $(html).appendTo(that.element).click(function(e) {
                managePagination(e, that);
            });
        },
        
        removePagination: function() {
            this.pagination.unbind('click').remove();
        }
    };
    
    return UiCarousel;
});

box.get('ui').addConfig('tabs', {
    activeCls: 'on'
}).addConstructor('tabs', function($, box) {
    var clickOnLinksList, openTab, closeTab, setBindings,
        UiTabs;
    
    clickOnLinksList = function(oUi) {
        return function(oEvt) {
            if(!oUi.waiting) {
                var oElm = oEvt.target;
                while(oElm && oElm != this) {
                    if(oElm.tagName == 'A') {
                        oUi.open(oElm.href.replace(/.*#tab=(.+)/, '$1'));
                        break;
                    }
                    oElm = oElm.parentNode;
                }
                oElm = null;
            } else {
                oEvt.preventDefault();
            }
        };
    };
    
    openTab = function(oUi) {
        $('#' + oUi.waiting).addClass(oUi.cfg.activeCls);
        oUi.active = oUi.waiting;
        oUi.waiting = null;
    };
    
    closeTab = function(oUi) {
        $('#' + oUi.active).removeClass(oUi.cfg.activeCls);
    };
    
    setBindings = function(oUi) {
        var sLabel = oUi.boxName + '.' + oUi.id;
        box.subscribe('open@' + oUi.id + '>' + sLabel, function(oEvt) {
            openTab(oEvt.source);
        });
        box.subscribe('close@' + oUi.id + '>' + sLabel, function(oEvt) {
            closeTab(oEvt.source);
            oEvt.source.phase = 'open';
            oUi.boxPublish('beforeopen');
        });
    };
    
    UiTabs = function(sID, oDefaultCfg) {
        this.id = sID;
        this.cfg = oDefaultCfg;
    };
    UiTabs.prototype = {
        boxCreate: function(oDatas) {
            var oUi = this;
            
            oUi.rootElm = $(oDatas.rootElm);
            oUi.rootElm.find('a').each(function(i, oAnchorElm) {
                var sID = oAnchorElm.href.replace(/.+#(.+)/, '$1');
                if(sID) {
                    oAnchorElm.href = '#tab=' + sID;
                    if($(oAnchorElm.parentNode).hasClass(oUi.cfg.activeCls)) {
                        oUi.active = sID;
                    }
                }
            });
            
            var sDirectAccess = box.getURLHashParts().tab;
            if(sDirectAccess) {
                oUi.open(sDirectAccess);
                oUi.active = sDirectAccess;
            }
            
            oUi.animated = !!oDatas.animated;
            if(oUi.animated) {
                setBindings(oUi);
            }
            
            oUi.enable();
            
            oUi.boxPublish('init');
        },
        
        boxDestroy: function() {
            this.disable();
        },
        
        disable: function() {
            if(this.disabled !== true) {
                this.rootElm.unbind('click');
                var sId = this.id,
                    sSuffix = sId + '>' + this.boxName + '.' + sId;
                box.unsubscribe(
                    'open@' + sSuffix,
                    'close@' + sSuffix
                );
                this.disabled = true;
            }
            return this;
        },
        
        enable: function() {
            if(this.disabled !== false) {
                this.rootElm.click(clickOnLinksList(this));
                this.disabled = false;
            }
            return this;
        },
        
        open: function(id) {
            if(!this.waiting && id && id != this.active && box.getDoc().getElementById(id)) {
                this.waiting = id;
                this.close();
                $('a[href$="#tab=' + id + '"]', this.rootElm).parent().addClass(this.cfg.activeCls);
                if(!this.animated) {
                    openTab(this);
                }
            }
        },
        
        close: function() {
            if(this.active) {
                $('a[href$="#tab=' + this.active + '"]', this.rootElm).parent().removeClass(this.cfg.activeCls);
                if(this.animated) {
                    this.phase = 'close';
                    this.boxPublish('beforeclose');
                } else {
                    closeTab(this);
                }
            }
        },
        
        animate: function(styles, duration) {
            var oUi = this;
            if(typeof styles == 'object' && !isNaN(duration)) {
                var id = oUi.phase == 'open' ? oUi.waiting : oUi.active;
                $('#' + id).animate(styles, duration, function() {
                    oUi.boxPublish(oUi.phase);
                });
            }
        }
    };
    
    return UiTabs;
});

box.get('ui').addConfig('accordion', {
    activeCls: 'on'
}).addConstructor('accordion', function($, box) {
    var clickOnElement, UiAccordion;
    
    clickOnElement = function(e, accordion) {
        var t = e.target;
        while(t != accordion.element[0]) {
            if(t.nodeName.toLowerCase() == accordion.clickable) {
                e.preventDefault();
                accordion[$(t).hasClass(accordion.cfg.activeCls) ? 'close' : 'open'](t);
                break;
            }
            t = t.parentNode;
        }
        t = null;
    };
    
    UiAccordion = function(sID, oDefaultCfg) {
        this.id = sID;
        this.cfg = oDefaultCfg;
    };
    UiAccordion.prototype= {
        boxCreate: function(datas) {
            var that = this;
            
            that.element = $(datas.rootElm);
            that.clickable = datas.clickable || 'dt';
            that.unique = datas.unique === false ? false : true;
            
            if(datas.addLinks) {
                $(that.clickable, that.element).each(function(i, elm) {
                    var active = $(elm).html('<a href="#">' + elm.innerHTML + '</a>').hasClass(that.cfg.activeCls);
                    if(active) {
                        that.active = elm;
                    }
                });
            } else {
                $(that.clickable, that.element).each(function(i, elm) {
                    var active = $(elm).hasClass(that.cfg.activeCls);
                    if(active) {
                        that.active = elm;
                    }
                });
            }
            
            that.enable();
        },
        
        boxDestroy: function() {
            this.disable();
        },
        
        disable: function() {
            if(this.disabled !== true) {
                this.element.unbind('click');
                this.disabled = true;
            }
            return this;
        },
        
        enable: function() {
            var that = this;
            if(that.disabled !== false) {
                that.element.click(function(e) {
                    clickOnElement(e, that);
                });
                that.disabled = false;
            }
            return that;
        },
        
        open: function(elm) {
            if(elm.nodeType) {
                if(this.unique) {
                    this.close();
                }
                this.active = $(elm).addClass(this.cfg.activeCls).next().addClass(this.cfg.activeCls).end();
                this.boxPublish('open');
            }
        },
        
        close: function(elm) {
            if((elm && elm.nodeType) || this.active) {
                $(elm || this.active).removeClass(this.cfg.activeCls).next().removeClass(this.cfg.activeCls);
                this.active = null;
                this.boxPublish('close');
            }
        }
    };
    
    return UiAccordion;
});

box.get('ui').addConfig('form', {
    webbox: false,
    focusCls: 'focus',
    checkedCls: 'checked',
    selectedCls: 'selected',
    hoverCls: 'hover',
    
    fauxSelectCls: 'fauxSelect',
    
    fauxOptions: '<div id="{$fauxOptionsId}" style="position:absolute; top:-10000px; left:-10000px"></div>',
    fauxOptionsId: 'fauxOptions',
    fauxOptionsMaxHeight: 200,
    fauxOptionsScrollbarOffset: 0
}).addConstructor('form', function($, box) {
    var FORM_INIT_PHASE = 1,
        W = box.getWin(),
        D = box.getDoc(),
        makeField,
        fields = {},
        
        types = {
            'checkbox': 'checkbox',
            'hidden': 'text',
            'password': 'text',
            'radio': 'radio',
            'select-one': 'select',
            'text': 'text',
            'textarea': 'text'
        },
        
        patterns = {
            empty: /^\s*$/,
            email: /^\s*[\w-]+(\.[\w-]+)*@([\w-]+\.)+[A-Za-z]{2,7}\s*$/
        },
        
        rules = {
            empty: function(value) {
                return patterns.empty.test(value);
            },
            email: function(value) {
                return patterns.email.test(value);
            }
        };
    
    box.addFormPatterns = function(datas) {
        for(var p in datas) {
            if(datas.hasOwnProperty(p)) {
                (function(pattern, key) {
                    patterns[key] = pattern;
                    rules[key] = function(value) {
                        return pattern.test(value);
                    };
                })(datas[p], p);
            }
        }
    };
    
    /**
     * Common form management methods
     */
    var reExtractFieldName, extractFieldName, getFieldCacheId, counterForCommonRoot,
        validateForm, bindFormSubmit, unbindFormSubmit;
    
    // only for webbox platform
    reExtractFieldName = /(ctl|brandlayout|mainbody)[0-9]+[$_]/g;
    extractFieldName = function(name) {
        return name.replace(reExtractFieldName, '');
    };
    
    getFieldCacheId = function(fieldName, formName) {
        return formName + '.' + fieldName;
    };
    counterForCommonRoot = 0;
    
    // form submission
    validateForm = function(e) {
        var id = $(this).getBoxDatas('form'),
            form = box.get('ui:form.' + id);
        if(form && form.disabled !== true) {
            if(form.mustValidateRules) {
                if(!form.isValid()) {
                    e.preventDefault();
                    form.boxPublish('submit', { valid: false, domEvt: e });
                } else {
                    form.disable();
                    form.boxPublish('submit', { valid: true, domEvt: e });
                }
            } else {
                form.disable();
                form.boxPublish('submit', { domEvt: e });
            }
        } else {
            e.preventDefault();
        }
    };
    
    bindFormSubmit = function(form) {
        if(form.submitBtn) {
            form.submitBtn.bind('click.boxValidation', validateForm);
        } else {
            form.dom.bind('submit.boxValidation', validateForm);
        }
    };
    
    unbindFormSubmit = function(form) {
        if(form.submitBtn) {
            form.submitBtn.unbind('click.boxValidation');
        } else {
            form.dom.unbind('submit.boxValidation');
        }
    };
    
    
    /**
     * UiForm
     */
    var UiForm = function(sID, oDefaultCfg) {
        this.id = sID;
        this.cfg = oDefaultCfg;
    };
    UiForm.prototype = {
        boxCreate: function(datas) {
            var that = this;
            
            that.dom = $(datas.rootElm).setBoxDatas({ form: that.id });
            that.fields = [];
            that.cfg.webbox = datas.webbox === true;
            that.submitBtn = datas.submit !== undefined ? that.dom.find(datas.submit) : null;
            if(that.submitBtn && 1 == that.submitBtn.length) { // do not check for webbox here (diags)
                that.submitBtn.setBoxDatas({ form: that.id });
                if(that.submitBtn.outerHTML().indexOf('doPostBack') > -1) {
                    var n = that.submitBtn[0].href.match(/'([^']+)'/);
                    that.submitName = n && n[1];
                    that.submitHref = that.submitBtn.attr('href');
                    that.submitBtn.attr('href', '#');
                }
            }
            bindFormSubmit(that);
            
            $('input, select, textarea', that.dom).each(function(i, elm) {
                if(elm.id && elm.name && elm.type && types[elm.type]) {
                    var bWebbox = that.cfg.webbox,
                        type = types[elm.type],
                        name;
                    if('radio' == type) {
                        name = bWebbox ? extractFieldName(elm.name) : elm.name;
                    } else {
                        name = bWebbox ? extractFieldName(elm.id) : elm.id;
                    }
                    var id = getFieldCacheId(name, that.id);
                    
                    // check for common root ids (Alsy diags)
                    if(bWebbox && 'radio' != type && fields[id]) {
                        ++counterForCommonRoot;
                        name = name + counterForCommonRoot;
                        id = id + counterForCommonRoot;
                        elm.id = name;
                    }
                    
                    if(!fields[id]) {
                        if('radio' == type) {
                            if(that.dom[0].nodeName.toLowerCase() == 'form') {
                                elm = that.dom[0].elements[elm.name];
                            } else {
                                elm = D.forms[0].elements[elm.name];
                            }
                        }
                        fields[id] = new makeField[type]($(elm), type, name, that.id);
                        that.fields.push(id);
                    }
                }
            });
            
            that.enable();
        },
        
        boxDestroy: function() {
            if(this.submitHref) {
                this.submitBtn.attr('href', this.submitHref);
            }
            unbindFormSubmit(this);
            this.clearErrors().removeValidation().removeReplacement();
            this.eachField(function(field) {
                delete fields[field.form + '.' + field.name];
            });
        },
        
        disable: function() {
            this.disabled = true;
        },
        
        enable: function() {
            this.disabled = false;
        },
        
        getElement: function() {
            return this.dom;
        },
        
        field: function(name) {
            return fields[getFieldCacheId(name, this.id)] || null;
        },
        
        eachField: function(fn) {
            var i = this.fields.length, l = i - 1;
            while(i--) {
                if(false === fn(fields[this.fields[l - i]])) {
                    break;
                }
            }
            return this;
        },
        
        submit: function() {
            if(this.dom[0].tagName === 'FORM') {
                this.dom[0].submit();
            } else if(this.submitName && box.getGlobal().__doPostBack) {
                box.getGlobal().__doPostBack(this.submitName, '');
            }
        },
        
        mustValidate: function(rules) {
            if(!this.mustValidateRules) {
                var msg = rules(this);
                if('string' == typeof msg) {
                    this.msg = msg;
                }
                this.mustValidateRules = true;
            }
            return this;
        },
        
        removeValidation: function() {
            this.eachField(function(field) {
                if(field.rule) {
                    field.removeValidation();
                }
            });
            return this;
        },
        
        getErrors: function() {
            var i = 0, errors = {};
            this.eachField(function(field) {
                if(field.error) {
                    errors[field.name] = field.error;
                    ++i;
                }
            });
            return (i ? errors : null);
        },
        
        setErrors: function(errors) {
            if('object' == typeof errors) {
                var id;
                for(var name in errors) {
                    id = getFieldCacheId(name, this.id);
                    if(errors.hasOwnProperty(name) && fields[id]) {
                        fields[id].setError(errors[name]);
                    }
                }
            }
            return this;
        },
        
        clearErrors: function() {
            this.eachField(function(field) {
                field.clearError();
            });
            this.boxPublish('submit', { valid: true });
            return this;
        },
        
        isValid: function(noBroadcast) {
            this.validate(noBroadcast === box.get('const:NOTIFY_OFF') ? box.get('const:NOTIFY_OFF') : undefined);
            var valid = true;
            this.eachField(function(field) {
                if(typeof field.error == 'string') {
                    return (valid = false);
                }
            });
            return valid;
        },
        
        validate: function(noBroadcast) {
            this.eachField(function(field) {
                if(undefined !== field.rule) {
                    field.validate(noBroadcast);
                }
            });
            return this;
        },
        
        addReplacement: function(options) {
            this.eachField(function(field) {
                if(undefined !== field.addReplacement) {
                    field.addReplacement(options);
                }
            });
            return this;
        },
        
        removeReplacement: function() {
            this.eachField(function(field) {
                if(undefined !== field.removeReplacement) {
                    field.removeReplacement();
                }
            });
            return this;
        }
    };
    
    
    /**
     * Common field management methods
     */
    var getFieldLabel, getFieldValidationEventName, validateField, bindFieldRule, unbindFieldRule,
        getFieldChangeEventName, changeField, bindFieldChange, unbindFieldChange,
        focusBlurField, bindFieldFocusBlur, unbindFieldFocusBlur, disableField, enableField;
    
    getFieldLabel = function(field) {
        if(field.jquery) {
            field = $(field);
        }
        var label = field.next('label');
        if(!label.length) {
            label = field.prev('label');
            if(!label.length && field.parent().length) {
                label = field.parent('label');
                if(!label.length) {
                    label = getFieldLabel(field.parent());
                }
            }
        }
        return label;
    };
    
    getFieldValidationEventName = function(type) {
        var evt;
        switch(type) {
            case 'checkbox':
            case 'radio':
                evt = 'click.boxValidation';
                break;
            case 'select':
                evt = 'change.boxValidation';
                break;
            case 'text':
                evt = 'blur.validation';
        }
        return evt;
    };
    
    validateField = function(e) {
        var id = $(this).getBoxDatas('id');
        if(id && fields[id]) {
            fields[id].validate();
        }
    };
    
    bindFieldRule = function(field) {
        field.dom.bind(getFieldValidationEventName(field.type), validateField);
    };
    
    unbindFieldRule = function(field) {
        field.dom.unbind(getFieldValidationEventName(field.type));
    };
    
    getFieldChangeEventName = function(type) {
        var evt;
        switch(type) {
            case 'checkbox':
            case 'radio':
            case 'select':
                evt = 'click.boxChange';
                break;
            case 'text':
                evt = 'change.boxChange';
        }
        return evt;
    };
    
    changeField = function(e) {
        var id = $(this).getBoxDatas('id');
        var field = id && fields[id];
        if(field) {
            var type = field.type;
            if('checkbox' == type || 'radio' == type) {
                field[this.checked ? 'check' : 'uncheck'](extractFieldName(this.id));
            } else if('select' == field.type) {
                if(field.getIndex() != field.current) {
                    field.setIndex(field.getIndex());
                }
            } else {
                field.boxPublish('change');
            }
        }
    };
    
    bindFieldChange = function(field) {
        field.dom.bind(getFieldChangeEventName(field.type), changeField);
    };
    
    unbindFieldChange = function(field) {
        field.dom.unbind(getFieldChangeEventName(field.type));
    };
    
    disableField = function(field) {
        unbindFieldChange(field);
        if('select' == typeof field.type) {
            unbindSelectKeyNav(field);
        }
        field.dom.each(function(i, elm) {
            elm.disabled = true;
        });
        field.boxPublish('disable');
    };
    
    enableField = function(field, init) {
        bindFieldChange(field);
        if('select' == field.type) {
            bindSelectKeyNav(field);
        }
        if(init != FORM_INIT_PHASE) {
            field.dom.each(function(i, elm) {
                elm.disabled = false;
            });
        }
        field.boxPublish(init == FORM_INIT_PHASE ? 'init' : 'enable');
    };
    
    focusBlurField = function(e) {
        var id = $(this).getBoxDatas('id'),
            field = id && fields[id],
            cfg;
        if(field) {
            cfg = field.getCfg();
            if('focus' == e.type) {
                if('radio' == field.type || 'checkbox' == field.type) {
                    field.getLabel(extractFieldName(this.id)).addClass(cfg.focusCls);
                } else if('select' == field.type) {
                    field.getReplaced().addClass(cfg.focusCls);
                    // bug IE6, when clicking on a label, select the first option
                    if(box.ie6 && field.current != field.getIndex()) {
                        field.dom[0].selectedIndex = field.current;
                    }
                }
            } else {
                if('radio' == field.type || 'checkbox' == field.type) {
                    field.getLabel(extractFieldName(this.id)).removeClass(cfg.focusCls);
                } else if('select' == field.type) {
                    field.getReplaced().removeClass(cfg.focusCls);
                }
            }
        }
    };
    
    bindFieldFocusBlur = function(field) {
        field.dom.bind('focus.boxReplacement', focusBlurField).bind('blur.boxReplacement', focusBlurField);
    };
    
    unbindFieldFocusBlur = function(field) {
        field.dom.unbind('.boxReplacement');
    };
    
    
    /**
     * Field (base constructor)
     */
    var Field = function(elm, type, name, form) {
        this.initialize(elm, type, name, form);
    };
    Field.prototype = {
        boxName: 'ui:field',
        
        boxGetName: function() {
            return this.boxName + '.' + this.name;
        },
        
        boxPublish: function(sType, oDatas) {
            box.publish({ type: sType, label: this.boxGetName(), source: this, data: oDatas });
        },
        
        initialize: function(elm, type, name, form) {
            this.dom = elm;
            this.dom.setBoxDatas({ id: getFieldCacheId(name, form) });
            this.type = type;
            this.name = name;
            this.form = form;
            this.error = null;
            this.enable(FORM_INIT_PHASE);
        },
        
        getForm: function() {
            return box.get('ui:form.' + this.form);
        },
        
        getCfg: function() {
            return this.getForm().cfg;
        },
        
        getElement: function() {
            return this.dom;
        },
        
        getLabel: function() {
            return getFieldLabel(this.dom);
        },
        
        getValue: function() {
            return (this.dom[0].value || null);
        },
        
        setValue: function(value) {
            this.dom[0].value = value;
            return this;
        },
        
        isDisabled: function() {
            return this.dom[0].disabled;
        },
        
        disable: function() {
            disableField(this);
            return this;
        },
        
        enable: function(init) {
            enableField(this, init);
            return this;
        },
        
        mustValidate: function(rule) {
            this.rule = rule;
            bindFieldRule(this);
            return this;
        },
        
        removeValidation: function() {
            this.rule = null;
            unbindFieldRule(this);
            return this;
        },
        
        isValid: function(noBroadcast) {
            this.validate(noBroadcast === box.get('const:NOTIFY_OFF') ? box.get('const:NOTIFY_OFF') : undefined);
            return typeof this.error != 'string';
        },
        
        validate: function(noBroadcast) {
            if(this.rule) {
                var r = this.rule(this);
                if('string' == typeof r) {
                    this.setError(r, noBroadcast);
                } else {
                    this.clearError();
                }
            }
            return this;
        },
        
        getError: function() {
            return this.error;
        },
        
        setError: function(error, noBroadcast) {
            if('string' == typeof error) {
                this.error = error;
                if(noBroadcast !== box.get('const:NOTIFY_OFF')) {
                    this.boxPublish('error', { message: error });
                }
            }
            return this;
        },
        
        clearError: function(noBroadcast) {
            this.error = null;
            if(noBroadcast !== box.get('const:NOTIFY_OFF')) {
                this.boxPublish('valid');
            }
            return this;
        },
        
        isReplaced: function() {
            return this.dom.eq(0).getBoxDatas('mode') === 'replaced';
        }
    };
    
    
    /**
     * TextField
     */
    var TextField = function(elm, type, name, form) {
        this.initialize(elm, type, name, form);
    };
    box.inherit(TextField, Field);
    box.extend(TextField, {
        boxName: 'ui:field.text',
        
        isDefault: function() {
            return (this.dom[0].value == this.dom[0].defaultValue);
        },
        
        clearValue: function() {
            this.dom[0].value = '';
            return this;
        },
        
        isEmpty: function() {
            return rules.empty(this.dom[0].value);
        },
        
        isMatching: function(pattern) {
            return (rules[pattern] ? rules[pattern](this.dom[0].value) : null);
        },
        
        isEqualTo: function(value) {
            return (this.dom[0].value == value);
        }
    });
    
    
    /**
     * CheckboxField
     */
    var CheckboxField = function(elm, type, name, form) {
        this.initialize(elm, type, name, form);
    };
    box.inherit(CheckboxField, Field);
    box.extend(CheckboxField, {
        boxName: 'ui:field.checkbox',
        
        isChecked: function() {
            return this.dom[0].checked;
        },
        
        check: function() {
            this.dom[0].checked = true;
            if(this.isReplaced()) {
                this.getLabel().addClass(this.getCfg().checkedCls);
            }
            this.boxPublish('change');
            return this;
        },
        
        uncheck: function() {
            this.dom[0].checked = false;
            if(this.isReplaced()) {
                this.getLabel().removeClass(this.getCfg().checkedCls);
            }
            this.boxPublish('change');
            return this;
        },
        
        addReplacement: function() {
            this.dom.setBoxDatas({ mode: 'replaced' });
            if(this.isChecked()) {
                this.getLabel().addClass(this.getCfg().checkedCls);
            }
            bindFieldFocusBlur(this);
            this.boxPublish('replace');
            return this;
        },
        
        removeReplacement: function() {
            var cfg = this.getCfg();
            this.dom.clearBoxDatas('mode');
            this.getLabel().removeClass(cfg.checkedCls).removeClass(cfg.focusCls);
            unbindFieldFocusBlur(this);
            return this;
        }
    });
    
    
    /**
     * RadiosGroup
     */
    var RadiosGroup = function(elm, type, name, form) {
        this.initialize(elm, type, name, form);
    };
    box.inherit(RadiosGroup, Field);
    box.extend(RadiosGroup, {
        boxName: 'ui:field.radio',
        
        initialize: function(elm, type, name, form) {
            var that = this;
            
            that.dom = elm;
            that.type = type;
            that.name = name;
            that.form = form;
            that.error = null;
            that.length = that.dom.length;
            that.map = {};
            that.current = null;
            that.each(function(field, i) {
                that.map[extractFieldName(field.id)] = i;
                if(field.checked) {
                    that.current = extractFieldName(field.id);
                }
                $(field).setBoxDatas({ id: getFieldCacheId(name, form) });
            });
            that.enable(FORM_INIT_PHASE);
        },
        
        each: function(fn) {
            var i = this.length, l = i - 1;
            while(i--) {
                if(fn(this.dom[l - i], l - i)) {
                    break;
                }
            }
            return this;
        },
        
        getChecked: function() {
            return this.current ? this.dom[this.map[this.current]] : null;
        },
        
        getElement: function(id) {
            var f = null;
            if('string' == typeof id) {
                if(undefined !== this.map[id]) {
                    return this.dom[this.map[id]];
                }
            } else if(typeof id == 'number') {
                if(id >= 0 && id < this.length) {
                    return this.dom[id];
                }
            } else {
                f = this.getChecked();
            }
            return f;
        },
        
        getElements: function() {
            return this.dom;
        },
        
        getLabel: function(id) {
            var field = this.getElement(id);
            return (field && getFieldLabel($(field)));
        },
        
        getLabels: function() {
            return getFieldLabel(this.dom);
        },
        
        getValue: function(id) {
            if(undefined !== id) {
                var field = this.getElement(id);
                return ((field && field.value) ? field.value : null);
            } else {
                var current = this.getChecked();
                return (current && current.value);
            }
        },
        
        setValue: function(value, id) {
            if(undefined !== id) {
                var field = this.getElement(id);
                if(field) {
                    field.value = value;
                }
            } else {
                var current = this.getChecked();
                if(current) {
                    current.value = value;
                }
            }
            return this;
        },
        
        isChecked: function(id) {
            var ok = false;
            if(undefined !== id) {
                var field = this.getElement(id);
                ok = (!!field && field.checked);
            } else {
                ok = !!this.current;
            }
            return ok;
        },
        
        check: function(id) {
            if(undefined !== id) {
                var field = this.getElement(id),
                    cfg;
                if(field && id != this.current) {
                    cfg = this.getCfg();
                    field.checked = true;
                    if(this.isReplaced()) {
                        if(this.current) {
                            this.getLabel(this.current).removeClass(cfg.checkedCls);
                        }
                        this.getLabel(id).addClass(cfg.checkedCls);
                    }
                    this.current = id;
                    this.boxPublish('change');
                }
            }
            return this;
        },
        
        uncheck: function(id) {
            if(this.current) {
                var field = this.getElement(undefined !== id ? id : this.current);
                if(field && field.checked) {
                    field.checked = false;
                    if(this.isReplaced()) {
                        this.getLabel(this.current).removeClass(this.getCfg().checkedCls);
                    }
                    this.current = null;
                    this.boxPublish('disable');
                }
            }
            return this;
        },
        
        addReplacement: function() {
            this.dom.setBoxDatas({ mode: 'replaced' });
            if(this.isChecked()) {
                this.getLabel(this.current).addClass(this.getCfg().checkedCls);
            }
            bindFieldFocusBlur(this);
            this.boxPublish('replace');
            return this;
        },
        
        removeReplacement: function() {
            var cfg = this.getCfg();
            this.dom.clearBoxDatas('mode');
            this.getLabels().removeClass(cfg.checkedCls).removeClass(cfg.focusCls);
            unbindFieldFocusBlur(this);
            return this;
        }
    });
    
    
    /**
     * SelectField
     */
    var fauxOptions, openedFauxSelect,
        getFauxOptions, getFauxOptionIndex, manageFauxSelectState, openFauxOptions, closeFauxOptions,
        clickOnFauxSelect, bindFauxSelectClick, unbindFauxSelectClick,
        clickOnFauxOptions, bindFauxOptionsClick, unbindFauxOptionsClick,
        keyUpOnFauxSelect, keyDownOnFauxSelect, bindSelectKeyNav, unbindSelectKeyNav,
        mouseOverOptionsIE6, mouseOutOptionsIE6;
    
    getFauxOptions = function(select) {
        var options = select.getOptions();
        var selected = select.getIndex();
        var i = options.length, l = i - 1, cls, html = '';
        while(i--) {
            cls = (l - i) == selected ? ' ' + select.getCfg().selectedCls : '';
            html += '<li class="box[i=' + (l - i) + ']' + cls + '">' + (options[l - i].text || '&nbsp;') + '</li>';
        }
        return html;
    };
    
    getFauxOptionIndex = function(option) {
        return option.className.match(/i=(\d+)/)[1];
    };
    
    mouseOverOptionsIE6 = function(cls) {
        return function() {
            $(this).addClass(cls);
        };
    };
    
    mouseOutOptionsIE6 = function(cls) {
        return function() {
            $(this).removeClass(cls);
        };
    };
    
    openFauxOptions = function(select) {
        select.opened = true;
        openedFauxSelect = getFieldCacheId(select.name, select.form);
        
        select.boxPublish('beforeopen');
        
        box.get('ui:panel.fauxOptions').element.html(getFauxOptions(select));
        box.get('ui:panel.fauxOptions').show();
        
        var cfg = select.getCfg(),
            fauxSelect = select.getReplaced(),
            fauxSelectPos = fauxSelect.getXY(),
            fauxSelectPaddingBox = fauxSelect.getSize('padding-box'),
            fauxSelectBorderBox = fauxSelect.getSize('border-box');
        
        // set width before computing height
        fauxOptions.width(fauxSelectPaddingBox.width);
        
        var fauxOptionsPaddingBox = fauxOptions.getSize('padding-box'),
            fauxOptionsHeight = Math.min(fauxOptionsPaddingBox.height, cfg.fauxOptionsMaxHeight);
        
        // set width before computing position
        fauxOptions.height(fauxOptionsHeight);
        var fauxOptionsBorderBox = fauxOptions.getSize('border-box');
        
        var wSize = $(W).getSize();
        var wOffset = $(W).getScroll();
        
        var top = fauxSelectPos.top + fauxSelectBorderBox.height;
        var reverse = false;
        if(top + fauxOptionsBorderBox.height > wOffset.top + wSize.height) {
            var tmp = fauxSelectPos.top - fauxOptionsBorderBox.height;
            if(tmp >= wOffset.top) {
                top = tmp;
                reverse = true;
            }
        }
        
        box.get('ui:mask.fauxOptions').dom.insertStyles = {width: wSize.width, height: 'document:content-box'};
        box.get('ui:mask.fauxOptions').show();
        box.get('ui:mask.fauxOptions').element.click(function() {
            closeFauxOptions(fields[openedFauxSelect]);
        });
        
        fauxOptions.css({
            top: top,
            left: fauxSelectPos.left,
            height: fauxOptionsHeight
        });
        
        box.get('ui:scroll.fauxOptions').bar.parent().css('height', fauxOptionsHeight - (2 * cfg.fauxOptionsScrollbarOffset));
        box.get('ui:scroll.fauxOptions').wrapper.width(fauxSelectPaddingBox.width);
        box.get('ui:scroll.fauxOptions').compute().moveToElement('#' + select.form + select.name + select.getIndex());
        if(box.ie6) {
            fauxOptions.find('li').mouseover(mouseOverOptionsIE6(cfg.hoverCls)).mouseout(mouseOutOptionsIE6(cfg.hoverCls));
        }
        select.boxPublish('open', { reverse: reverse });
    };
    
    closeFauxOptions = function(select) {
        select.opened = false;
        fauxOptions.css({left: '-10000px', height: 'auto'});
        box.get('ui:mask.fauxOptions').element.unbind('click');
        box.get('ui:mask.fauxOptions').hide();
        if(box.ie6) {
            fauxOptions.find('li').unbind('mouseover mouseout');
        }
        box.get('ui:scroll.fauxOptions').disable().reposition();
        box.get('ui:panel.fauxOptions').hide();
    };
    
    manageFauxSelectState = function(select) {
        if(select.isReplaced()) {
            if(select.opened) {
                closeFauxOptions(select);
            } else {
                openFauxOptions(select);
            }
        }
    };
    
    clickOnFauxSelect = function(e) {
        var id = $(this).prev().getBoxDatas('id');
        var select = id && fields[id];
        if(select) {
            select.dom[0].focus();
            manageFauxSelectState(select);
        }
    };
    
    bindFauxSelectClick = function(select) {
        select.getReplaced().click(clickOnFauxSelect);
    };
    
    unbindFauxSelectClick = function(select) {
        select.getReplaced().unbind('click');
    };
    
    clickOnFauxOptions = function(e) {
        var select = fields[openedFauxSelect];
        if(select && e.target.nodeName.toLowerCase() == 'li') {
            select.setIndex(getFauxOptionIndex(e.target));
            closeFauxOptions(select);
            select.dom[0].focus();
        }
    };
    
    bindFauxOptionsClick = function(select) {
        fauxOptions.click(clickOnFauxOptions);
    };
    
    unbindFauxOptionsClick = function(select) {
        fauxOptions.unbind('click');
    };
    
    keyUpOnFauxSelect = function(e) {
        var id = $(this).getBoxDatas('id');
        var select = id && fields[id];
        if(select) {
            var k = e.which;
            if(e.altKey && (k == 38 || k == 40)) {
                manageFauxSelectState(select);
                return;
            }
            var i = select.getIndex();
            switch(k) {
                case 13:
                case 27:
                    select.setIndex(i);
                    if(select.isReplaced()) {
                        closeFauxOptions(select);
                    }
                    break;
                case 34:
                case 35:
                    select.setIndex(select.dom[0].options.length - 1);
                    break;
                case 33:
                case 36:
                    select.setIndex(0);
                    break;
                case 37:
                case 38:
                    i = (i == select.current) ? --i : i;
                    if(i < 0) {
                        i = 0;
                    }
                    select.setIndex(i);
                    break;
                case 39:
                case 40:
                    i = (i == select.current) ? ++i : i;
                    if(i >= select.dom[0].options.length) {
                        i = select.dom[0].options.length - 1;
                    }
                    select.setIndex(i);
                    break;
                default:
                    select.setIndex(i);
            }
        }
    };
    
    keyDownOnFauxSelect = function(e) {
        var id = $(this).getBoxDatas('id');
        var select = id && fields[id];
        if(select && select.isReplaced() && 9 == e.which) {
            closeFauxOptions(select);
        }
    };
    
    bindSelectKeyNav = function(select) {
        select.dom.bind('keyup.boxKeyNav', keyUpOnFauxSelect).bind('keydown.boxKeyNav', keyDownOnFauxSelect);
    };
    
    unbindSelectKeyNav = function(select) {
        select.dom.unbind('.boxKeyNav');
    };
    
    box.subscribe('endmove>draggable.scroll.fauxOptions', function(e) {
        fields[openedFauxSelect].getElement()[0].focus();
    });
    
    var SelectField = function(elm, type, name, form) {
        this.initialize(elm, type, name, form);
    };
    box.inherit(SelectField, Field);
    box.extend(SelectField, {
        boxName: 'ui:field.select',
        
        initialize: function(elm, type, name, form) {
            this.dom = elm;
            this.dom.setBoxDatas({ id: getFieldCacheId(name, form) });
            this.type = type;
            this.name = name;
            this.form = form;
            this.error = null;
            this.length = this.dom[0].options ? this.dom[0].options.length : 0;
            this.current = this.dom[0].selectedIndex;
            this.enable(FORM_INIT_PHASE);
        },
        
        hasIndex: function(i) {
            return (!isNaN(i) && i >= 0 && i < this.length);
        },
        
        getIndex: function() {
            return this.dom[0].selectedIndex;
        },
        
        setIndex: function(i) {
            if(this.hasIndex(i) && i != this.current) {
                this.dom[0].selectedIndex = i;
                if(this.isReplaced()) {
                    this.getReplaced('span').html(this.getText() || '&nbsp;');
                    if(this.opened) {
                        var opts = fauxOptions.find('li'),
                            cfg = this.getCfg();
                        opts.eq(this.current).removeClass(cfg.selectedCls);
                        opts.eq(i).addClass(cfg.selectedCls);
                        if(!box.get('ui:scroll.fauxOptions').disabled) {
                            box.get('ui:scroll.fauxOptions').moveToElement(opts.eq(i));
                        }
                        opts = null;
                    }
                    if(this.rule) {
                        this.validate();
                    }
                }
                this.current = i;
                this.boxPublish('change');
            }
            return this;
        },
        
        getValue: function(i) {
            i = undefined !== i ? i : this.getIndex();
            if(this.hasIndex(i)) {
                return this.dom[0].options[i].value || null;
            }
            return null;
        },
        
        setValue: function(value, i) {
            i = undefined !== i ? i : this.getIndex();
            if(this.hasIndex(i)) {
                this.dom[0].options[i].value = value;
            }
            return this;
        },
        
        getText: function(i) {
            i = undefined !== i ? i : this.getIndex();
            if(this.hasIndex(i)) {
                return this.dom[0].options[i].text || null;
            }
            return null;
        },
        
        setText: function(text, i) {
            i = undefined !== i ? i : this.getIndex();
            if(this.hasIndex(i)) {
                this.dom[0].options[i].text = text;
                if(i == this.current && this.isReplaced()) {
                    this.getReplaced('span').html(text || '&nbsp;');
                }
            }
            return this;
        },
        
        getOption: function(i) {
            i = undefined !== i ? i : this.getIndex();
            if(this.hasIndex(i)) {
                return {'text': this.getText(i), 'value': this.getValue(i), 'selected': i == this.getIndex()};
            }
            return null;
        },
        
        setOption: function(option, i) {
            if('object' == typeof option) {
                i = undefined !== i ? i : this.getIndex();
                if(this.hasIndex(i)) {
                    this.dom[0].options[i].value = option.value;
                    this.dom[0].options[i].text = option.text;
                }
            }
            return this;
        },
        
        getOptions: function() {
            var options = [], i = this.length, l = i - 1;
            while(i--) {
                options[l - i] = this.getOption(l - i);
            }
            return options;
        },
        
        setOptions: function(options, clear) {
            if('object' == typeof options && options.length) {
                if(clear) {
                    this.dom[0].options.length = 0;
                }
                var i = options.length, l = i - 1, opt;
                while(i--) {
                    opt = options[l - i];
                    if(opt.selected) {
                        this.current = i;
                    }
                    this.dom[0].options[this.dom[0].options.length] = new Option(opt.text, opt.value, opt.selected);
                }
                this.length = this.dom[0].options.length;
                // @todo broadcast an 'update' event?
            }
            return this;
        },
        
        addReplacement: function() {
            this.dom.setBoxDatas({ mode: 'replaced' });
            var cfg = this.getCfg(),
                id = this.form + this.name + 'REP',
                html = '<div id="' + id + '" class="' + cfg.fauxSelectCls + '"><div><span id="' + id + 'Inner">' + (this.getText() || '&nbsp;') + '</span></div></div>';
            $(html).insertAfter(this.dom);
            bindFauxSelectClick(this);
            bindFieldFocusBlur(this);
            if(!fauxOptions && !box.get('ui:panel.fauxOptions') && !box.get('ui:mask.fauxOptionsMask')) {
                fauxOptions = $(cfg.fauxOptions.replace('{$fauxOptionsId}', cfg.fauxOptionsId)).appendTo(D.body).mousedown(bindFauxOptionsClick);
                box.get('ui').create('scroll.fauxOptions', {
                    rootElm: fauxOptions
                });
                box.get('ui').create('mask.fauxOptions', {
                    html: '<div id="boxFauxOptionsMask" style="position:absolute; top:0; left:0;"></div>',
                    insertTarget: fauxOptions,
                    insertMethod: 'insertBefore'
                });
                box.get('ui').create('panel.fauxOptions', {
                    html: '<ul></ul>',
                    insertTarget: box.get('ui:scroll.fauxOptions').wrapper
                });
            }
            if(box.ie6) {
                this.dom.bind('mousewheel', function(e) {
                    box.wheelEventForScroll(e, box.get('ui:scroll.fauxOptions'));
                });
            }
            this.boxPublish('replace');
            return this;
        },
        
        removeReplacement: function() {
            this.dom.clearBoxDatas('mode');
            unbindFauxSelectClick(this);
            unbindFieldFocusBlur(this);
            this.getReplaced().remove();
            return this;
        },
        
        getReplaced: function(selector) {
            return $('#' + this.form + this.name + 'REP ' + (selector || ''));
        }
    });
    
    makeField = {
        checkbox: CheckboxField,
        radio: RadiosGroup,
        select: SelectField,
        text: TextField
    };
    
    return UiForm;
});


