if (!Function.prototype.apply) {

    function __ApplyCallbackName__() {
        return '__apply__' + new Date().getTime() + '__' + Math.floor(Math.random() * 88998899) + '__';
    }

    Function.prototype.apply = function (context, args) {
        var args_func = [];
        if (args) {
            var l = args.length;
            args_func = new Array(l);
            for (var i = 0; i < l; i++) {
                args_func[i] = 'arguments[1][' + i + ']';
            }
        }

        context = context || window;

        var callback_name = __ApplyCallbackName__();
        context[callback_name] = this;

        var ret = eval('context.' + callback_name + '(' + args_func.join(',') + ')');
        eval('context.' + callback_name + ' = null');
        return ret;
    }

}

if (!Function.prototype.call) {
    Function.prototype.call = function (context) {
        return this.apply(context, Array.prototype.slice.apply(arguments, [1]));
    }
}

(function () {

var A = Array.prototype;

if (!A.push) {

    A.push = function () {
        for (var i = 0, l = arguments.length; i < l; i++) {
            this[this.length] = arguments[i];
        }

        return this.length;
    }

}

if (!A.splice) {

    A.splice = function (start, deleteCount) {
        var i;
        var l = this.length;
        var removed = [];

        start = parseInt(start) || 0;
        deleteCount = parseInt(deleteCount) || 0;

        start = start < 0 ? Math.max(l + start, 0) : Math.min(start, l);
        deleteCount = Math.min(Math.max(deleteCount, 0), l - start);

        if (deleteCount) {
            removed = this.slice(start, start + deleteCount);

            l -= deleteCount;
            for (i = start; i < l; i++) {
                this[i] = this[i + deleteCount];
            }
            this.length = l;
        }

        if (arguments.length > 2) {
            var il;
            var arr = this.slice(start);
            this.length = start;

            for (i = 2, il = arguments.length; i < il; i++) {
                this.push(arguments[i]);
            }
            for (i = 0, il = arr.length; i < il; i++) {
                this.push(arr[i]);
            }
        }

        return removed;
    }

}

if (!A.pop) {

    A.pop = function () {
        var element, length = this.length;

        if (length != 0) {
            element = this[length - 1];
            this.length--;
        }

        return element;
    }

}

if (!A.shift) {

    A.shift = function () {
        var element;

        if (this.length !== 0) {
            element = this[0];
            this.splice(0, 1);
        }

        return element;
    }

}

if (!A.indexOf) {

    A.indexOf = function (searchElement, fromIndex) {
        fromIndex = fromIndex || 0;

        for (var length = this.length; fromIndex < length; fromIndex++) {
            if (this[fromIndex] == searchElement) {
                return fromIndex;
            }
        }

        return -1;
    }

}

if (!A.lastIndexOf) {

    A.lastIndexOf = function (searchElement, fromIndex) {
        var length = this.length;

        fromIndex = fromIndex || length - 1;

        if (fromIndex < 0) {
            fromIndex += length;
        }

        for (; fromIndex >= 0; fromIndex--) {
            if (this[fromIndex] == searchElement) {
                return fromIndex;
            }
        }

        return -1;
    }

}

if (!A.every) {

    A.every = function (callback, thisObject) {
        thisObject = thisObject || window;

        var index = 0, length = this.length;

        for (; index < length; index++) {
            if (!callback.apply(thisObject, [this[index], index, this])) {
                break;
            }
        }

        return (index == length);
    }

}

if (!A.filter) {

    A.filter = function (callback, thisObject) {
        thisObject = thisObject || window;

        var length = this.length, count = 0, filtered = new Array(length);

        for (var index = 0; index < length; index++) {
            if (callback.apply(thisObject, [this[index], index, this])) {
                filtered[count++] = this[index];
            }
        }

        filtered.length = count;

        return filtered;
    }

}

if (!A.forEach) {

    A.forEach = function (callback, thisObject) {
        thisObject = thisObject || window;

        for (var index = 0, length = this.length; index < length; index++) {
            callback.apply(thisObject, [this[index], index, this]);
        }
    }

}

if (!A.map) {

    A.map = function (callback, thisObject) {
        thisObject = thisObject || window;

        var index = 0, length = this.length, map = new Array(length);

        for (; index < length; index++) {
            map[index] = callback.apply(thisObject, [this[index], index, this]);
        }

        return map;
    }

}

if (!A.some) {

    A.some = function (callback, thisObject) {
        thisObject = thisObject || window;

        var index = 0, length = this.length;
        for (; index < length; index++) {
            if (callback.apply(thisObject, [this[index], index, this])) {
                break;
            }
        }

        return (index != length);
    }

}

})();

if (typeof decodeURIComponent == 'undefined') {
    decodeURIComponent = unescape;
}

if (typeof encodeURIComponent == 'undefined') {
    encodeURIComponent = escape;
}

if (!String.charCodeAt) {
    /*
        Такая реализация работает только с первыми 256 символами
    */
    String.prototype.charCodeAt = function (index) {
        var c = this.charAt(index);
        var begin = 0, end = 256, cur = 0, curChar = '';

        while (end - begin > 1) {
            cur = (end + begin) >> 1; // div 2
            curChar = String.fromCharCode(cur);
            if (curChar > c) {
                end = cur;
            } else if (curChar < c) {
                begin = cur;
            } else {
                return cur;
            }
        }
    }
}

/**
 * y5 core object contains methods for dynamic loading of scripts.
 *
 * Для загрузки скриптов можно использовать удобный (в большинстве случаев) механизм динамической загрузки.
 * Для этого используем метод:
 *     y5.require(moduleNames, listener);
 *     moduleNames — строка или массив строк с именами модулей для загрузки
 *     listener — обработчик на загрузку модулей
 *
 * <strong>Пример</strong>
 * &lt;script src="http://img.yandex.net/js/ver/y5.js"&gt;&lt;/script&gt; &lt;!-- подключим ядро --&gt;
 * &lt;script&gt;
 *     function onload1() {
 *         // тут пишем то, что хотим сделать сразу после загрузки модуля Events
 *     }
 *     y5.require('Events', onload1); // грузим модуль Events с обработчиком загрузки onload1
 *     function onload2() {
 *         // тут пишем то, что хотим сделать сразу после загрузки модулей Events и Dom
 *     }
 *     y5.require(['Events', 'Dom'], onload2); // грузим модули Events и Dom с обработчиком загрузки onload2
 * &lt;/script&gt;
 *
 * <strong>Tests:</strong>
 *   <a href="../tests/jsunit/testRunner.html?testPage=../Suite.html&amp;autorun=true">Все тесты JSUnit</a>
 */
var y5 = {

    aliases: {},
    aliasTries: {},
    expectedAliases: {},
    scripts: document.getElementsByTagName('script'),
    aliasRegexp: /^\{([^\}]+)\}\/?/,

    /**
     * This method loads a script file and executes listeners after script finishes loading.
     * @param {String} String like "foo.bar". Loads file bar.js from subfolder foo.
     * @param {Function} Function to execute after script finishes loading.
     * @method
     */
    require: function (url, listener) {
        this.Loader.require(url, listener);
    },

    /**
     * Scripts must execute this method (y5.loaded) after all initialization functions.
     * @param {String} String like "foo.bar". Loads file bar.js from subfolder foo.
     * @method
     */
    loaded: function (url) {
        this.Loader.loaded(url);
    },

    /**
     * Returns alias base for file in src attribute
     * @param {String} file name
     * @return {Object} like {path: 'http://www.yandex.ru/foo/', query: '?build=1', charset: 'utf-8'}
     * @method
     */
    getBase: function (file) {
        for (var i = 0, l = this.scripts.length; i < l; i++) {
            var script = this.scripts[i],
                src = script.getAttribute('src');

            if (src && src.lastIndexOf(file) >= 0) {
                var base = {
                    path: src.substring(0, src.lastIndexOf('/') + 1),
                    charset: script.getAttribute('charset') || 'utf-8'
                };

                var query = src.lastIndexOf('?');
                if (query >= 0) {
                    base.query = src.substring(query, src.length);
                }

                return base;
            }
        }
        return null;
    },

    /**
     * Sets the alias as path to file
     * @param {String} alias
     * @param {String} file name
     * @method
     */
    getBaseAndSetAlias: function (alias, file) {
        var base = this.getBase(file);

        if (base) {
            this.setAlias(alias, base);
        } else {
            var tries = this.aliasTries[alias];

            if (tries) {
                this.aliasTries[alias] = 1;
            } else if (tries >= 1000) {
                return;
            }

            // timeout for FireFox to prevent script execution
            // before script tag is added to the DOM.
            var _this = this;
            window.setTimeout(function()
            {
                _this.getBaseAndSetAlias(alias, file);
                _this.aliasTries[alias]++;
            }, 1);
        }
    },

    /**
     * Sets the alias
     * @param {String} Alias
     * @param {String} Path
     * @method
     */
    setAlias: function (name, value) {
        if (typeof value == 'string') {
            value = {path: value};
        }

        this.aliases[name] = value;

        // maybe some scripts are waiting for this alias
        if (this.expectedAliases[name]) {
            var listener;
            while ((listener = this.expectedAliases[name].shift())) {
                listener();
            }
        }
    },

    /**
     * Returns the alias
     * @param {String} location
     * @method
     */
    getAlias: function (location) {
        var alias = this.aliasRegexp.exec(location);
        return (alias ? alias[1] : null);
    },

    /**
     * Creates URLs from strings.
     * @param {String} String, like "foo.bar"
     * @param {String} File extension, like "js"
     * @return {String} URL, like http://www.yandex.ru/foo/bar.js
     */
    constructURL: function (name, type) {
        name = name.replace(/\./g, '/');
        type = type || 'js';

        var alias = this.aliasRegexp.exec(name);
        if (alias) {
            var base = this.aliases[alias[1]];
            var path = base.path;

            // дополняем путь слешем, если забыт
            if (path && path.lastIndexOf('/') != path.length - 1) {
                path += '/';
            }
            name = name.replace(alias[0], path);
            type += base.query || '';
        }

        return name + '.' + type;
    },

    /**
     * @deprecated
     */
    charsets: {},

    /**
     * @deprecated
     */
    setAliasCharset: function() {}
};

/**
 * Contains global variables used for browser identification.
 * @alias y5.Vars
 * @deprecated
 */
y5.Vars = new function () {
    this.UNDEF = 'undefined';
    this.DEBUG = false;

    // detect browser
    var nav           = window.navigator;
    var agt           = nav.userAgent.toLowerCase();
    var is_major      = parseInt(nav.appVersion, 10);
    var is_minor      = parseFloat(nav.appVersion);

    this.is_win       = (agt.indexOf('windows') != -1);
    this.is_mac       = (agt.indexOf('mac') != -1);

    this.is_safari    = (agt.indexOf('safari') != -1 || agt.indexOf('khtml') != -1);
    this.is_konq      = (agt.indexOf('konqueror') != -1 || agt.indexOf('khtml') != -1);
    this.is_opera     = (agt.indexOf('opera') != -1);
    this.is_opera_7   = (agt.indexOf('opera') != -1 && is_major < 8);
    this.is_gecko     = (agt.indexOf('gecko') != -1) && !this.is_safari;
    this.is_ie        = ((agt.indexOf('msie') != -1) && (agt.indexOf('opera') == -1));

    this.is_ie5       = (this.is_ie && (/msie 5\.0/.test(agt)));
    this.is_ie55      = (this.is_ie && (/msie 5\.5/.test(agt)));
    this.is_ie5up     = (this.is_ie && (/msie [56789]/.test(agt)));
    this.is_ie6up     = (this.is_ie && (/msie [6789]/.test(agt)));
    this.is_ie55up    = (this.is_ie55 || this.is_ie6up);
    this.is_ie6down   = (this.is_ie && !this.is_ie6up);
    this.is_ie7up     = (this.is_ie && (/msie [789]/.test(agt)));
    this.is_ie7down   = (this.is_ie && !this.is_ie7up);

    this.is_ff10      = (!this.is_ie && (/firefox\/1\.0\.\d?/.test(agt)));

    this.opera_ver    = parseFloat(agt.substr(agt.indexOf('opera') + 6));
    this.opera_ver    = isNaN(this.opera_ver) ? 10 : this.opera_ver;
    this.safari_ver   = (this.is_safari ? parseFloat(agt.substr(agt.indexOf('safari') + 7)) : 0);
    this.gecko_ver = this.is_gecko ? (
        (function() {
            var f = agt.match(/rv:(\d\.\d)/);
            return f ? f[1] : 0;
        })()
    ) : 0;

    this.FALSE        = function() { return false; };
    this.TRUE         = function() { return true; };
    this.NULL         = function() { return null; };
};

// Исправляем поведение indexOf в IE 5
if (y5.Vars.is_ie5) {

    String.prototype.indexOfBug = String.prototype.indexOf;

    String.prototype.indexOf = function(needle) {
        if (this.toString() === '' && needle === '') {
            return 0;
        }

        return this.indexOfBug(needle);
    };

    String.prototype.lastIndexOfBug = String.prototype.lastIndexOf;

    String.prototype.lastIndexOf = function(needle) {
        var pos = this.lastIndexOfBug(needle);
        if (needle === '') {
            pos++;
        }

        return pos;
    };

}

// copy vars to y5
for (var i in y5.Vars) {
    y5[i] = y5.Vars[i];
}

/**
 * Test objects
 */
y5.Types = {
    /// consts

    // dom
    ELEMENT_NODE:   1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE:      3,
    COMMENT_NODE:   8,
    DOCUMENT_NODE:  9,

    // standard
    UNDEF:          0,
    UNDEFINED:      0,
    OBJECT:         101,
    FUNCTION:       102,
    NUMBER:         103,
    STRING:         104,
    BOOLEAN:        105,

    // user
    ARRAY:          1001,
    REGEXP:         1002,
    DATE:           1003,
    NULL:           1004,
    EVENT:          1005,

    types: {
        'undefined': 0,
        'object':    101,
        'function':  102,
        'number':    103,
        'string':    104,
        'boolean':   105
    },

    /// methods

    type: function (obj) {
        var type = this.types[typeof obj];

        if (obj === null) {
            return this.NULL;
        }

        if (type == this.OBJECT && obj.nodeName) {
            return obj.nodeType;
        }

        if (type == this.OBJECT || type == this.FUNCTION) {
            switch(obj.constructor) {
                case Array:
                    return this.ARRAY;

                case RegExp:
                    return this.REGEXP;

                case Date:
                    return this.DATE;
            }
            // TODO: EVENT
        }

        return type;
    },

    // standard

    def: function (obj) {
        return typeof obj !== 'undefined';
    },

    undef: function (obj) {
        return typeof obj === 'undefined';
    },

    object: function (obj) {
        return typeof obj === 'object';
    },

    func: function (obj) {
        return typeof obj === 'function';
    },

    number: function (obj) {
        return typeof obj === 'number';
    },

    string: function (obj) {
        return typeof obj === 'string';
    },

    bool: function (obj) {
        return typeof obj === 'boolean';
    },

    // user

    array: function (obj) {
        return (obj instanceof Array);
    },

    regexp: function (obj) {
        return (obj instanceof RegExp);
    },

    date: function (obj) {
        return (obj instanceof Date);
    },

    event: function (obj) {
        return (obj !== null && typeof obj.type != 'undefined' && typeof obj.cancelBubble != 'undefined');
    },

    // dom

    element: function (obj) {
        return this.testDomNode(obj, this.ELEMENT_NODE);
    },

    attribute: function (obj) {
        return this.testDomNode(obj, this.ATTRIBUTE_NODE);
    },

    text: function (obj) {
        return this.testDomNode(obj, this.TEXT_NODE);
    },

    document: function (obj) {
        return this.testDomNode(obj, this.DOCUMENT_NODE);
    },

    comment: function (obj) {
        return this.testDomNode(obj, this.COMMENT_NODE);
    },

    node: function (obj) {
        return (obj !== null && typeof obj.nodeType != 'undefined');
    },

    /// private

    testDomNode: function(obj, type) {
        return (obj.nodeType && obj.nodeType == type);
    }
};


/**
 * Creates the SCRIPT tags.
 * @alias y5.Scripts
 */
y5.Scripts = new function () {
    /*
        Если head не определен в теле документа,
        во всех браузерах, кроме Opera он создается автоматически,
        а Opera нужно помочь.
    */
    try {
        if(!document.getElementsByTagName('head')[0]) {
            document.getElementsByTagName('html')[0].appendChild(document.createElement('head'));
        }
    } catch (e) {}

    this.head = document.getElementsByTagName('head');

    this.setScriptAttributes = function (script, src, charset) {
        script.setAttribute('type', 'text/javascript');
        script.setAttribute('src', src);
        if (charset) {
            script.setAttribute('charset', charset);
        }
    };

    if (y5.is_ie) {
        this.insertScript = function (script, container) {
            container.insertBefore(script, container.firstChild);
        }
    } else {
        this.insertScript = function (script, container) {
            container.appendChild(script);
        }
    }

    if (y5.is_opera_7) {
        this.appendScript = function (src, charset) {
            if (!document.body) {
                return null;
            }
            var span = document.createElement('span');
            span.style.display = 'none';
            span.innerHTML = '<script><\/script>';
            var script = span.getElementsByTagName('script').item(0);
            this.setScriptAttributes(script, src, charset);
            this.insertScript(span, document.body);
            return script;
        }
    } else if (y5.is_konq) {
        this.appendScript = function (src, charset) {
            if (!document.body) {
                return null;
            }
            var span = document.body.appendChild(document.createElement('span'));
            span.style.display = 'none';
            var script = document.createElement('script');
            this.setScriptAttributes(script, src, charset);
            this.insertScript(script, span);
            return script;
        }
    } else {
        this.appendScript = function (src, charset) {
            var script = document.createElement('script');
            this.setScriptAttributes(script, src, charset);
            this.insertScript(script, this.head.item(0));
            return script;
        }
    }

    /**
     * This method creates the SCRIPT tag.
     * @alias y5.Scripts.createScript
     * @param {String} src
     * @param {String} Charset
     * @param {Function} Listerner for script creation.
     */
    this.createScript = function (src, charset, listener, tries) {
        tries = tries || 0;
        listener = (typeof listener == 'function' ? listener : y5.NULL);
        var script = this.appendScript(src, charset);
        if (script) {
            listener(script);
        } else if (tries < 10) {
            var _this = this;
            window.setTimeout(function () {_this.createScript(src, charset, listener, ++tries)}, 10);
        }
    };
};

/**
 * Dynamically loads scripts.
 * @alias y5.Loader
 */
y5.Loader = new function () {
    this.scripts = {};

    var _this = this;

    function startLoad(url, listener, charset) {
        if (!_this.scripts[url]) {
            _this.scripts[url] = createScriptFake(listener);
            y5.Scripts.createScript(y5.constructURL(url), charset);
        } else {
            if (_this.scripts[url].ready) {
                listener();
            } else {
                _this.scripts[url].listeners.push(listener);
            }
        }
    }

    function waitForAlias(alias, url, listener) {
        if (!y5.expectedAliases[alias]) {
            y5.expectedAliases[alias] = [];
        }
        y5.expectedAliases[alias].push(function () { require(url, listener); });
    }

    function require(url, listener) {
        var alias = y5.getAlias(url);
        if (!alias) {
            url = '{y5}.' + url;
            alias = 'y5';
        }

        var base = y5.aliases[alias]
        if (!base) {
            // if alias is not defined, wait until it appears
            waitForAlias(alias, url, listener);
        } else {
            startLoad(url, listener, base.charset);
        }
    }

    function requireList(urls, listener) {
        var counter = 0,
            length = urls.length;

        function callBack() {
            // increase files counter
            counter++;
            // exec listener if all loaded
            if (counter == length && typeof listener == 'function') {
                listener();
            }
        }

        for (var i = 0; i < length; i++) {
            require(urls[i], callBack);
        }
    }

    function createScriptFake(listener) {
        return {
            listeners: [listener],
            ready: false
        }
    }

    /**
     * Scripts must execute this method (y5.loaded) after all initialization functions
     * @alias y5.Loader.loaded
     * @param {String} String like "foo.bar". Loads file bar.js from subfolder foo.
     */
    this.loaded = function (url) {
        if (!y5.getAlias(url)) {
            url = '{y5}.' + url;
        }

        if (!this.scripts[url]) {
            this.scripts[url] = createScriptFake(y5.NULL);
        }

        var listener;
        while ((listener = this.scripts[url].listeners.shift())) {
            listener();
        }

        this.scripts[url].ready = true;
    };

    /**
     * This method loads the script file and executes listeners after script finishes loading.
     * @alias y5.Loader.require
     * @param {String} String like "foo.bar". Loads file bar.js from subfolder foo.
     * @param {Function} Function to execute after script finishes loading.
     */
    this.require = function (urls, listener) {
        urls = typeof urls == 'string' ? [urls] : urls;
        requireList(urls, listener);
    };
};

// Set alias 'y5' from path to y5.js file.
y5.getBaseAndSetAlias('y5', 'y5.js');

// Файла Arrays.js больше не существует,
// но чтобы не искать всех кто на него ссылался,
// мы скажем что он уже загружен
y5.loaded('Arrays');

y5.Console = {
    log:      y5.NULL,
    info:     y5.NULL,
    warn:     y5.NULL,
    error:    y5.NULL,
    trace:    y5.NULL,
    dir:      y5.NULL,
    dirxml:   y5.NULL,
    group:    y5.NULL,
    groupEnd: y5.NULL
};

/**
 * Exception
 * @alias y5.Exception
 */
y5.Exception = function (message, method, module) {
    if (!y5.DEBUG) {
        return true;
    }

    var ret = 'y5.' + module + '.' + method + ': ' + message;
    var e = new Error(ret);

    if (e.stack) {
        e.message += "\nStack:\t" + e.stack.replace(/\n/ig, '\n\t');
    }

    return e;
};

y5.Exception.prototype = new Error();

// If you want to debug something, add the y5debug=on parameter to the URL.
// To turn debugging off, add the y5debug=off parameter.

if (/y5debug/.test(window.location.search) || /y5debug/.test(document.cookie)) {
    y5.require('Debug');
}/**
 * @name Utils
 * @fileOverview
 * Функции-утилиты
 */

/**
 * Различные функции-утилиты
 * @class
 * @static
 */
y5.Utils = {
    /**
     * Счетчик генерируемых ID.
     * @private
     */
    counterId: 0,

    /**
     * Генерирует случайный ID.
     * @param {String} [prefix] префикс (по умолчанию = '')
     * @return {String} случайный ID
     */
    generateId: function(prefix) {
        return (prefix || '') + ((new Date()).getTime() + Math.round(Math.random() * 10000));
    },

    /**
     * Генерирует уникальный ID.
     * @return {String} уникальный ID
     *
     * @example
     * y5.Utils.generateUniqueId();
     * // -> 'y5__id45'
     */
    generateUniqueId: function() {
        return 'y5__id' + (++this.counterId);
    },

    /**
     * Генерирует уникальный ID и присваевает его свойству uniqueID переданного объекта.
     * Если свойство uniqueID объекта существует, то просто возвращается значение этого свойства.
     * @param {Object} object объект
     * @return {String} уникальный ID
     */
    getUniqueId: function(object) {
        return object.uniqueID || this.setUniqueId(object);
    },

    /**
     * Устанавливает значение свойства uniqueID переданного объекта.
     * @param {Object} object объект
     * @param {String} [uniqueID] уникальный ID (если неопределен, то генерируется новый)
     * @return {String} уникальный ID
     *
     * @example
     * y5.Utils.setUniqueId(object, 'foo');
     * // -> 'foo'
     *
     * y5.Utils.setUniqueId(object);
     * // -> 'y5__id46'
     */
    setUniqueId: function(object, uniqueID) {
        if (typeof uniqueID == y5.UNDEF) {
            uniqueID = this.generateUniqueId();
        }
        return (object.uniqueID = uniqueID);
    },

    /**
     * Сравнивает два объекта.
     * Положительный результат в случае, если объект один и тот же.
     * @param {Object} first объект
     * @param {Object} second объект
     * @return {Boolean} результат сравнения
     */
    isEqual: function(first, second) {
        return this.getUniqueId(first) == this.getUniqueId(second);
    },

    /**
     * Создает iframe для IE, для подкладывания под div, чтобы избежать артефактов
     * с Flash или select.
     */
    fakeFrame: {
        frame: null,
        transparentFrame: null,

        init: function (obj) {
            if (!y5.is_ie7down) {
                return;
            }
            this.frame = y5.Dom.$('fakeFrame');
            if (!this.frame) {
                this.create(obj);
            }

            return [this.frame, this.transparentFrame];
        },

        create: function (obj) {
            this.transparentFrame = document.createElement('<iframe id="fakeTransparentFrame" src="about:blank" frameborder="0" tabindex="-1" style="filter:Alpha(opacity=1); position: absolute;">');
            this.frame = document.createElement('<iframe id="fakeFrame" src="about:blank" frameborder="0" tabindex="-1" allowtransparency="true" style="FILTER: chroma(color=#FFFFFF); position: absolute;">');
            this.transparentFrame.style.zIndex = y5.Dom.getPropertyValue(obj, 'z-index') - 2;
            this.frame.style.zIndex = y5.Dom.getPropertyValue(obj, 'z-index') - 1;
            this.frame.style.display = this.transparentFrame.style.display = 'none';
            y5.Dom.getBody().appendChild(this.transparentFrame);
            y5.Dom.getBody().appendChild(this.frame);
        },

        adjust: function (obj) {
            if (!this.frame) {
                return;
            }

            if (obj.offsetHeight > 0) {
                if (this.transparentFrame.style.zIndex == -2) {
                    this.transparentFrame.style.zIndex = y5.Dom.getPropertyValue(obj, 'z-index') - 2;
                    this.frame.style.zIndex = y5.Dom.getPropertyValue(obj, 'z-index') - 1;
                }
                this.frame.style.width = this.transparentFrame.style.width = obj.offsetWidth + 'px';
                this.frame.style.height = this.transparentFrame.style.height = obj.offsetHeight + 'px';
                this.frame.style.left = this.transparentFrame.style.left = obj.offsetLeft + 'px';
                this.frame.style.top = this.transparentFrame.style.top = obj.offsetTop + 'px';
            }
            this.frame.style.display = this.transparentFrame.style.display = obj.style.display;
        }
    },

    /**
     * @private
     */
    hexDigit: '0123456789ABCDEF'.split(''),

    /**
     * Преобразует десятичное число в двузначное 16-тиричное.
     * @param {Number} number число
     * @return {String} 16-ричное число
     *
     * @example
     * y5.Utils.dec2hex(10);
     * // -> '0A'
     */
    dec2hex: function(n) {
        return this.hexDigit[n >> 4] + this.hexDigit[n & 15];
    },

    /**
     * Преобразует 16-тиричное число в десятичное.
     * @param {String} number 16-ричное число
     * @return {Number} десятичное число
     *
     * @example
     * y5.Utils.hex2dec('A');
     * // -> 10
     */
    hex2dec: function(n) {
        return parseInt(n, 16);
    },

    /**
     * Поочередно копирует свойства двух объектов.
     * @param {Object} destination первый объект
     * @param {Object} source второй объект
     * @return {Object} объект получившийся в результате слияния двух объектов
     *
     * @example
     * y5.Utils.objectCopy({a: 1, b: 3}, {b: 2, c: 3});
     * // -> {a: 1, b: 2, c: 3}
     */
    objectCopy: function(destination, source) {
        var key, result = new Object();

        for (key in destination) {
            result[key] = destination[key];
        }

        if (source) {
            for (key in source) {
                var src = source[key];
                if (src && y5.Types.object(src) &&
                    !(
                      y5.Types.node(src) ||
                      y5.Types.event(src)
                    )
                    /* exclude dom nodes/events */
                ) {
                    if (y5.Types.undef(result[key])) {
                        result[key] = new Object();
                    } else if (y5.Types.array(src)) {
                        result[key] = src;
                        continue;
                    }
                    result[key] = this.objectCopy(result[key], src);
                } else {
                    result[key] = src;
                }
            }
        }

        return result;
    },

    /**
     * Расширяет функции прототипа первого объекта, функциями прототипа второго объекта.
     * @param {Object} destination расширяемый объект
     * @param {Object} source объект с функциями расширения
     * @param {Object} sourceName имя объекта расширения
     */
    objectExtends: function(destination, source, source_name) {
        var key;
        source_name = source_name || source.toString().match(/function\s*([^\(]+)\(/)[1];

        var destination_copy = new Object();
        for (key in destination.prototype) {
            destination_copy[key] = destination.prototype[key];
        }

        destination.prototype[source_name] = source;
        for (key in source.prototype) {
            destination.prototype[key] = source.prototype[key];
        }

        for (key in destination_copy) {
            if (y5.Types.object(destination.prototype[key])) {
                destination.prototype[key] = this.objectCopy(destination.prototype[key], destination_copy[key]);
            } else {
                destination.prototype[key] = destination_copy[key];
            }
        }
    },

    /**
     * Выполняет метод объекта через заданный таймаут.
     * @param {Function} method метод
     * @param {Number} timeout таймаут (мсек)
     * @param {Object} [context] объект в контексте которого запускается функция (по умолчанию null)
     * @param {Object} [arg1] аргумент вызываемой функции
     * @param {Object} [arg2]
     * @param {Object} [...]
     * @param {Object} [argN]
     * @return {ID} ID таймера
     */
    setTimeout: function(func, timeout, context /* args */) {
        var args = [];

        for (var i = 3, l = arguments.length; i < l; i++) {
            args.push(arguments[i]);
        }

        function call() {
            func.apply(context, args);
        }

        return window.setTimeout(call, timeout);
    }
};

/**
 * @name getUniqueID
 * @memberof y5.Utils
 * @function
 * @deprecated y5.Utils.getUniqueId
 */
y5.Utils.getUniqueID = y5.Utils.getUniqueId;

y5.loaded('Utils');
/**
 * Кеш данных.
 *
 * @constructor
 * @class
 *
 * @example
 * var cache = new y5.Cache();
 * cache.set("foo", "Bar");
 * cache.get("foo");
 * // -> "Bar"
 *
 * cache.test("foo");
 * // -> true
 *
 * cache.empty("baz");
 * // -> true
 *
 * cache.remove("foo");
 * cache.test("foo");
 * // -> false
 *
 * @todo добавить время жизни
 */
y5.Cache = function() {
    // объект для хранения данных
    this.data = {};
};

y5.Cache.prototype = {
    /**
     * Возвращает данные из кеша.
     * @param {String} key уникальный ключ
     * @return {Object | undefined} данные из кеша
     */
    get: function(key) {
        return this.data[key];
    },

    /**
     * Помещает данные в кеш.
     * @param {String} key уникальный ключ
     * @param {Object} value объект
     * @return {Object} value
     */
    set: function(key, value) {
        return (this.data[key] = value);
    },

    /**
     * Проверяет, что данные содержатся в кеше.
     * @param {String} key уникальный ключ
     * @return {Boolean} результат проверки
     */
    test: function(key) {
        return (typeof(this.data[key]) !== y5.UNDEF);
    },

    /**
     * Проверяет, что данные отсутствуют в кеше.
     * @param {String} key уникальный ключ
     * @return {Boolean} результат проверки
     */
    empty: function(key) {
        return (typeof(this.data[key]) === y5.UNDEF);
    },

    /**
     * Удаляет данные из кеша.
     * @param {String} key уникальный ключ
     */
    remove: function(key) {
        delete this.data[key];
    }
};

y5.loaded('Cache');(function() {

var trimRegexp = /(^[\s\xA0]+|[\s\xA0]+$)/g,
    isVoidRegexp = /^[\s\xA0]*$/,
    normalizeSpaceRegexp = /[\s\xA0]{2,}/g,
    escapeRegexp = /([\|\!\[\]\^\$\(\)\{\}\+\=\?\.\*\\])/g,
    stripTagsRegexp = /(<([^>]+)>)/ig,
    NLRegexp = /\r\n|\r|\n/g,
    wordsRegexp = /[^\s\xA0]+/g,
    escapeHTMLRe = /[&<>\"\']/g,
    escapeHTMLReplacer = function(c) {
        return '&#' + c.charCodeAt(0) + ';';
    },
    unescapeHTMLRe = /&(lt|gt|quot|apos|amp|#\d+);/g,
    unescapeHTMLReplacer = function(s, c) {
        switch (c) {
            case 'lt':   return '<';
            case 'gt':   return '>';
            case 'quot': return '"';
            case 'apos': return "'";
            case 'amp':  return '&';
        }

        return String.fromCharCode(c.substring(1));
    };

/**
 * @class Функции для работы со строками.
 * @static
 */
y5.Strings = {
    /**
     * Проверяет строку на пустоту.
     * @param {String} string строка для проверки
     * @return {Boolean} результат проверки
     */
    isEmpty: function(str) {
        return (str == this.EMPTY);
    },

    /**
     * Проверяет строку на пустоту или содержание только пробельных символов.
     * @param {String} string строка для проверки
     * @return {Boolean} результат проверки
     * @example
     * ' \n\t' -> true
     */
    isVoid: function(str) {
        return (!str || isVoidRegexp.test(str));
    },

    /**
     * Проверяет, что строка содержит подстроку.
     * @param {String} string строка
     * @param {String} needle строка для поиска
     * @return {Boolean} результат проверки
     * @example
     * y5.Strings.contains("foobarbaz", "bar")
     * // -> true
     *
     * y5.Strings.contains("FooBarBaz", "bar")
     * // -> false
     * @todo проверка без учета регистра
     */
    contains: function(str, needle) {
        return str.indexOf(needle) !== -1;
    },

    /**
     * Проверяет, что строка начинается с подстроки.
     * @param {String} string строка
     * @param {String} needle строка для поиска
     * @return {Boolean} результат проверки
     * @example
     * y5.Strings.startsWith("foobar", "foo")
     * // -> true
     *
     * y5.Strings.startsWith("Foobar", "foo")
     * // -> false
     * @todo проверка без учета регистра
     */
    startsWith: function(str, needle) {
        return str.indexOf(needle) === 0;
    },

    /**
     * Проверяет, что строка оканчивается подстрокой.
     * @param {String} string строка
     * @param {String} needle строка для поиска
     * @return {Boolean} результат проверки
     * @example
     * y5.Strings.endsWith("foobar", "bar")
     * // -> true
     *
     * y5.Strings.endsWith("FooBar", "bar")
     * // -> false
     * @todo проверка без учета регистра
     */
    endsWith: function(str, needle) {
        return str.lastIndexOf(needle) + needle.length === str.length;
    },

    /**
     * Удаляет начальные и конечные пробельные символы.
     * @param {String} string строка
     * @return {String} строка с удаленными пробелами
     * @example
     * "   foo  \n" -> "foo"
     */
    trim: function(str) {
        return str.replace(trimRegexp, this.EMPTY);
    },

    /**
     * Заменяет в строке множество пробельных символов на один пробел и удаляет начальные и конечные пробельные символы.
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "   foo  \t  bar \n" -> "foo bar"
     */
    normalizeSpace: function(str) {
        return this.trim(str.replace(normalizeSpaceRegexp, this.SPACE));
    },

    /**
     * Экранирует в строке все символы, которые являются специальными в регулярном выражении.
     * Используется для вставки любой строки в регулярное выражение.
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "{2}" -> "\{2\}"
     */
    escapeRegexp: function(text) {
        return text.replace(escapeRegexp, "\\$1");
    },

    /**
     * Возвращает символ по его коду.
     * @param {Number} code код символа
     * @return {String} символ
     * @example
     * 65 -> "A"
     */
    getCode: function(code) {
        return String.fromCharCode(code);
    },

    /**
     * Возвращает строку для вставки в HTML-код.
     * Используется для записи в innerHTML.
     * Для обратного преобразование используется y5.Strings.unescapeHTML.
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "&lt;foo>" -> "&amp;lt;foo&amp;gt;"
     */
    escapeHTML: function(str) {
        return str.replace(escapeHTMLRe, escapeHTMLReplacer);
    },

    /**
     * Функция обратная escapeHTML.
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "&amp;lt;foo&amp;gt;" -> "&lt;foo>"
     */
    unescapeHTML: function(str) {
        return str.replace(unescapeHTMLRe, unescapeHTMLReplacer);
    },

    /**
     * Выбирает из элемента текст.
     * @param {String | HTMLElement} element элемент
     * @return {String} текстовое содержимое элемента
     * @example
     * &lt;div>&lt;i>foo&lt;/i> &lt;b>bar&lt;/b>&lt;/div>
     * y5.Strings.stripTags(div);
     * // -> "foo bar"
     *
     * y5.Strings.stripTags('&lt;i>foo&lt;/i> &lt;b>bar&lt;/b>');
     * // -> "foo bar"
     */
    stripTags: function(elem) {
        return (typeof(elem) == 'string' ? elem : elem.innerHTML).replace(stripTagsRegexp, this.EMPTY);
    },

    /**
     * Заменяет [Ёё] на "e".
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "ёж" -> "еж"
     */
    IoToIe: function(str) {
        return str.replace(/[\u0451\u0401]/g, '\u0435');
    },

    /**
     * Позволяет выводить число с разными формами слова.
     * @param {Number} number число
     * @param {Array} forms список форм слова
     * @return {String} число с формой слова
     * @example
     * var forms = ['письмо', 'письма', 'писем', 'писем нет'];
     * y5.Strings.plural(0, forms);
     * // -> 'писем нет'
     *
     * y5.Strings.plural(1, forms);
     * // -> '1 письмо'
     *
     * y5.Strings.plural(2, forms);
     * // -> '2 письма'
     *
     * y5.Strings.plural(5, forms);
     * // -> '5 писем'
     *
     * var forms = ['письмо', 'письма', 'писем'];
     * y5.Strings.plural(0, forms);
     * // -> '0 писем'
     *
     * @todo сделать формат вывода
     */
    plural: function(num, forms) {
        var res = 2;
        var num_10 = num % 10;
        var num_100 = num % 100;

        if (num == 0) {
            // если задана форма для нулевого значения, то выводим только его
            if (forms[3]) {
                return forms[3];
            }
        } else {
            if (num_100 < 5 || num_100 > 20) {
                if (num_10 == 1) {
                    res = 0;
                } else {
                    if (num_10 >= 2 && num_10 <= 4) {
                        res = 1;
                    }
                }
            }
        }

        return num + ' ' + forms[res];
    },

    /**
     * @deprecated y5.Strings.plural
     */
    conversion: function(num, words) {
        return this.plural(num, [words[0], words[2], words[1], words[3]]);
    },

    /**
     * Переводит первый символ строки в верхний регистр.
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "foo" -> "Foo"
     */
    capitalize: function(str) {
        return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
    },

    /**
     * Переводит строку разделенную дефисами в форму camelCase.
     * @param {String} string строка
     * @return {String} преобразованная строка
     * @example
     * "foo-bar-baz" -> "fooBarBaz"
     */
    camelize: function(str) {
        return str.split('-').map(function(item, i) {
            if (i != 0) {
                return y5.Strings.capitalize(item);
            }
            return item;
        }).join(this.EMPTY);
    },

    /**
     * Повторяет строку.
     * @param {String} string строка
     * @return {String} строка повторенная n раз
     * @example
     * y5.Strings.repeat("foobar", 3);
     * // -> "foobarfoobarfoobar"
     *
     * y5.Strings.repeat("foo", 0);
     * // -> ""
     */
    repeat: function(str, count) {
        if (count < 1) {
            return this.EMPTY;
        }

        return (new Array(count + 1)).join(str);
    },

    /**
     * Заменяет символ перевода строки на тег &lt;br>
     * @param {String} string строка
     * @param {Boolean} xhtml true -> "<br />", иначе - "<br>"
     * @return {String} HTML
     * @example
     * "foo\nbar" -> "foo&lt;br>bar"
     */
    nl2br: function(str, xhtml) {
        return str.replace(NLRegexp, xhtml ? '<br />' : '<br>');
    },

    /**
     * Преобразовывает текст в HTML.
     * @param {String} string строка
     * @return {String} HTML
     */
    text2html: function(str) {
        return this.nl2br(this.escapeHTML(str))
    },

    /**
     * Возвращает массив слов из строки.
     * @param {String} string строка
     * @return {Array} массив слов
     * @example
     * y5.Strings.words('foo bar');
     * // -> ['foo', 'bar']
     *
     * y5.Strings.words('');
     * // -> []
     */
    words: function(str) {
        return str.match(wordsRegexp) || [];
    },

    /**
     * Возвращает количество слов в строке.
     * @param {String} string строка
     * @return {Number} число слов
     * @example
     * y5.Strings.wordsCount('foo bar');
     * // -> 2
     */
    wordsCount: function(str) {
        return this.words(str).length;
    },

    /**
     * Allows to do string conversions using printf syntax.
     *
     * %[padding][width]type
     * padding   - An optional padding specifier that says what character will be
     *             used for padding the results to the right string size. This may
     *             be a space character or a "0" (zero character).
     * width     - An optional number, a width specifier that says how many
     *             characters (minimum) this conversion should result in.
     * precision - An optional precision specifier that says how many decimal digits
     *             should be displayed for floating-point numbers. This option has
     *             no effect for other types than float.
     * type      - A type specifier that says what type the argument data should be
     *             treated as. Possible types:
     *
     * % - a literal percent character. No argument is required.
     * d - the argument is treated as an integer, and presented as a decimal number.
     * s - the argument is treated as and presented as a string.
     * @param {String} format Format
     * @param {Array} args Array of arguments
     * @return {String} Result string
     */
    printf: function(str, data) {
        if (typeof(data) != 'object') {
            data = [data];
        }
        var formats = str.match(/%s|%\d{0,2}d/g);
        if (formats) {
            for (var i = 0, l = formats.length; i < l; i++) {
                data[i] = typeof(data[i]) == y5.UNDEF ? this.EMPTY : data[i];
                str = str.replace(new RegExp(formats[i], this.EMPTY), this.convertData(formats[i], data[i]));
            }
        }
        return str.replace(/%%/g, '%');
    },

    /**
     * Converts data for y5.Strings.printf
     * @param {String} Format.
     * @param {Object} Data fo convertion.
     * @return {String} Result string.
     * @private
     */
    convertData: function(format, data) {
        data = data.toString();
        if (format == '%s' || format == '%d') {
            return data;
        }
        if (/%\d{2,2}d/.test(format)) {
            var symbol = format.substr(1, 1),
                length = format.substr(2, 1);
            while (data.length < length) {
                data = symbol + data;
            }
        }
        return data;
    },

    /**
     * Пустая строка.
     * @constant
     * @type String
     */
    EMPTY: '',

    /**
     * Пробел.
     * @constant
     * @type String
     */
    SPACE: ' ',

    /**
     * Неразрывный пробел.
     * @constant
     * @type String
     */
    NBSP: '\u00A0'
};

var Strings = y5.Strings;

if (y5.is_ie5) {

    Strings.escapeHTML = function(str) {
        return str.replace(/&/g, '&#38;').replace(/</g, '&#60;').replace(/>/g, '&#62;').replace(/\"/g, '&#34;').replace(/\'/g, '&#39;');
    };

    Strings.unescapeHTML = function(str) {
        return str.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
    };

}

/// aliases

/**
 * @name strip
 * @link y5.Strings.trim
 * @function
 */
Strings.strip = Strings.trim;

/**
 * @name times
 * @link y5.Strings.repeat
 * @function
 */
Strings.times = Strings.repeat;

/**
 * @name include
 * @link y5.Strings.contains
 * @function
 */
Strings.include = Strings.contains;

/**
 * @name isBlank
 * @link y5.Strings.isVoid
 * @function
 */
Strings.isBlank = Strings.isVoid;

/**
 * @name normalize
 * @link y5.Strings.normalizeSpace
 * @function
 */
Strings.normalize = Strings.normalizeSpace;

/**
 * @name stripHTML
 * @link y5.Strings.stripTags
 * @function
 * @deprecated
 */
Strings.stripHTML = Strings.stripTags;

})();

y5.loaded('Strings');
(function() {

var UNDEF = y5.UNDEF,
    Types = y5.Types,
    urlRegexp = /^((((\w+):)\/\/)(([\w\-\.]+)(\:(\d+))?))?(\/?[^\?#]*)?(\?([^\?#]*))?(#(.*))?$/,
    winHex = '%E9%F6%F3%EA%E5%ED%E3%F8%F9%E7%F5%FA%F4%FB%E2%E0%EF%F0%EE%EB%E4%E6%FD%FF%F7%F1%EC%E8%F2%FC%E1%FE%B8%C9%D6%D3%CA%C5%CD%C3%D8%D9%C7%D5%DA%D4%DB%C2%C0%CF%D0%CE%CB%C4%C6%DD%DF%D7%D1%CC%C8%D2%DC%C1%DE%A8',
    utfHex = '%D0%B9%D1%86%D1%83%D0%BA%D0%B5%D0%BD%D0%B3%D1%88%D1%89%D0%B7%D1%85%D1%8A%D1%84%D1%8B%D0%B2%D0%B0%D0%BF%D1%80%D0%BE%D0%BB%D0%B4%D0%B6%D1%8D%D1%8F%D1%87%D1%81%D0%BC%D0%B8%D1%82%D1%8C%D0%B1%D1%8E%D1%91%D0%99%D0%A6%D0%A3%D0%9A%D0%95%D0%9D%D0%93%D0%A8%D0%A9%D0%97%D0%A5%D0%AA%D0%A4%D0%AB%D0%92%D0%90%D0%9F%D0%A0%D0%9E%D0%9B%D0%94%D0%96%D0%AD%D0%AF%D0%A7%D0%A1%D0%9C%D0%98%D0%A2%D0%AC%D0%91%D0%AE%D0%81',

    win2utf = function(token) {
        var pos = winHex.indexOf(token);
        if (pos != -1) {
            return utfHex.substr(pos * 2, 6);
        }
        return token;
    },

    decodeWin = function(value) {
        return value.replace(/%[a-f0-9]{2}/ig, win2utf);
    },

    /**
     * Кодирует строку.
     * @param {String} string строка
     * @return {String} результат перекодировки
     * @private
     */
    encode = function(value) {
        return encodeURIComponent(value);
    },

    /**
     * Декодирует строку.
     * @param {String} string строка
     * @return {String} результат перекодировки
     * @private
     */
    decode = function(value) {
        var val = value.replace(/\+/g, '%20');
        try {
            return decodeURIComponent(val);
        } catch (e) {
            try {
                return decodeURIComponent(decodeWin(val));
            } catch (e) {
                return unescape(val);
            }
        }
    },

    /**
     * Добавляет параметр к запросу.
     * @param {Object} query объект запроса
     * @param {String} string имя параметра
     * @param {String} string значение параметра
     * @private
     */
    addParam = function(Query, name, value) {
        // make array
        if (typeof Query[name] == UNDEF) {
            Query[name] = new Array();
        }
        if (Types.array(value)) {
            Query[name] = Query[name].concat(value);
        } else {
            Query[name].push(value);
        }
    },

    getParamQuery = function(Query, name) {
        var list = Query[name];
        var len = list.length;
        var result = new Array(len);
        var enc_name = encode(name);

        for (var i = 0; i < len; i++) {
            result[i] = enc_name + '=' + encode(list[i]);
        }

        return result;
    };

/**
 * Класс для работы с URL и его частями.
 *
 * @constructor
 * @class
 * @param {String} href ссылка
 *
 * @example
 * var url = new y5.URL("http://www.yandex.ru/");
 * url.host(); // -> "www.yandex.ru"
 * url.port(); // -> "http"
 *
 * url.path("/yandsearch").query({text: 'test'});
 * url.go(); // -> "http://www.yandex.ru/yandsearch?text=test"
 *
 * // пустой URL
 * var url = new y5.URL("");
 * url.path("www.yandex.ru");
 * url.toString(); // -> "http://www.yandex.ru" (http по умолчанию)
 *
 * // по умолчанию
 * var url = new y5.URL();
 * url.toString() == window.location.href; // -> true
 */
y5.URL = function(href) {
    this.parse(typeof href != UNDEF ? href : window.location.href);
};

y5.URL.prototype = {
    /**
     * Прямой переход по ссылке.
     */
    go: function() {
        window.location.href = this.toString();
    },

    /**
     * Получить ссылку в виде строки.
     * @return {String} ссылка
     */
    toString: function() {
        var result = [];

        // proto & hostname
        if (this.Proto || this.Host) {
            if (this.Host) {
                result.push(this.Proto || 'http');
                result.push('://');
                result.push(this.Host);
                if (this.Port) {
                    result.push(':');
                    result.push(this.Port);
                }
            }
        }

        // path
        if (this.Path) {
            if (this.Host && this.Path.indexOf('/') != 0) {
                result.push('/');
            }
            result.push(this.Path);
        }

        // query
        var query = this.query();
        if (query) {
            result.push('?');
            result.push(query);
        }

        // hash
        if (this.Hash) {
            result.push('#');
            result.push(encode(this.Hash));
        }

        return result.join('');
    },

    /**
     * Создает новый объект y5.URL из данного.
     * @return {y5.URL} копия объекта
     */
    clone: function() {
        return new y5.URL(this.toString());
    },

    /**
     * Разбирает ссылку.
     * @param {String} string ссылка
     * @return {Boolean} результат разбора (true - успешно)
     * @private
     */
    parse: function(href) {
        // поиск запроса в url
        var match = href.match(urlRegexp);

        if (!match) {
            throw new y5.Exception('this is not an url', 'parseURL', 'URL');
            return false;
        }

        this.Href  = match[0];
        this.Proto = match[4];
        this.Host  = match[6];
        this.Port  = match[8] || 0;
        this.Path  = decode(match[9] || '');
        this.Query = this.parseQuery(match[11] || '');
        this.Hash  = decode(match[13] || '');

        //this.protoFull  = match[2];
        //this.protoOrig  = match[3];
        //this.Hostname   = match[5];
        //this.searchOrig = match[10] || '';
        //this.hashOrig   = match[12] || '';

        return true;
    },

    /**
     * Разбирает параметры запроса.
     * @param {String} string параметры запроса
     * @return {Object} результат в виде объекта
     * @private
     */
    parseQuery: function(value) {
        var query = {};
        var parts = value.replace(/\+/g, '%20').split('&');

        for (var i = 0, l = parts.length; i < l; i++) {
            var pair = parts[i].split('=');
            var name = pair.shift();
            if (name) {
                var value = pair.length > 1 ? pair.join('=') : (pair[0] || '');
                addParam(query, decode(name), decode(value));
            }
        }

        return query;
    },

/// getters/setters

    /**
     * Установить или получить протокол URL.
     * @param {String} [string] новый протокол
     * @return {String | this} протокол
     * @example
     * var url = new y5.URL("http://www.yandex.ru/");
     * url.proto(); // -> "http"
     *
     * url.proto("FOO");
     * url.toString(); // -> "FOO://www.yandex.ru/"
     */
    proto: function(value) {
        if (typeof value != UNDEF) {
            this.Proto = value;
            return this;
        }

        return this.Proto;
    },

    /**
     * Установить или получить хост URL.
     * @param {String} [string] новый хост
     * @return {String | this} хост
     * @example
     * var url = new y5.URL("http://www.yandex.ru/");
     * url.host(); // -> "www.yandex.ru"
     *
     * url.host("ya.ru");
     * url.toString(); // -> "http://ya.ru/"
     */
    host: function(value) {
        if (typeof value != UNDEF) {
            this.Host = value;
            return this;
        }

        return this.Host;
    },

    /**
     * Установить или получить порт URL.
     * @param {Number} [string] новый порт
     * @return {Number | this} порт
     * @example
     * var url = new y5.URL("http://www.yandex.ru/");
     * url.port(); // -> 0
     *
     * url.port(8080);
     * url.toString(); // -> "http://www.yandex.ru:8080/"
     */
    port: function(value) {
        if (typeof value != UNDEF) {
            this.Port = value;
            return this;
        }

        return this.Port;
    },

    /**
     * Установить или получить путь URL.
     * @param {String} [string] новый путь
     * @return {String | this} путь
     * @example
     * var url = new y5.URL("http://www.yandex.ru/");
     * url.path(); // -> "/"
     *
     * url.path("/yandsearch");
     * url.toString(); // -> "http://www.yandex.ru/yandsearch"
     */
    path: function(value) {
        if (typeof value != UNDEF) {
            this.Path = value;
            return this;
        }

        return this.Path;
    },

    /**
     * Установить или получить запрос URL.
     * @param {Object | String} [query] новые параметры запроса
     * @return {String | this} запрос
     * @example
     * var url = new y5.URL("http://www.yandex.ru/yandsearch?text=test");
     * url.query(); // -> "text=test"
     *
     * url.query({foo: "bar"});
     * url.toString(); // -> "http://www.yandex.ru/yandsearch?foo=bar"
     *
     * url.query("foo=bar");
     * url.toString(); // -> "http://www.yandex.ru/yandsearch?foo=bar"
     */
    query: function(value) {
        if (typeof value != UNDEF) {
            this.clearQuery();

            if (Types.string(value)) {
                this.Query = this.parseQuery(value);
            } else {
                this.replaceParams(value);
            }

            return this;
        }

        var result = new Array();

        for (var name in this.Query) {
            result = result.concat(getParamQuery(this.Query, name));
        }

        return result.join('&');
    },

    /**
     * Установить или получить якорь URL.
     * @param {String} [string] новый путь
     * @return {String | this} путь
     * @example
     * var url = new y5.URL("http://www.yandex.ru/#top");
     * url.hash(); // -> "top"
     *
     * url.hash("bottom");
     * url.toString(); // -> "http://www.yandex.ru/#bottom"
     */
    hash: function(value) {
        if (typeof value != UNDEF) {
            this.Hash = value;
            return this;
        }

        return this.Hash;
    },

/// Методы работы с параметрами

    /**
     * Добавляет параметр к запросу URL.
     * @param {Object} params новый параметр запроса
     * @return {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test");
     * url.addParam({stype: "www"});
     * url.toString(); // -> "http://ya.ru/?text=test&stype=www"
     */
    addParams: function(params) {
        for (var name in params) {
            addParam(this.Query, name, params[name]);
        }

        return this;
    },

    /**
     * Удаляет параметр из запроса URL.
     * @param {Array} params удаляемый параметр запроса
     * @return {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&stype=www");
     * url.removeParam(["stype"]);
     * url.toString(); // -> "http://ya.ru/?text=test"
     */
    removeParams: function(params) {
        for (var i = 0, l = params.length; i < l; i++) {
            delete this.Query[params[i]];
        }

        return this;
    },

    /**
     * Заменяет параметр в запросе URL.
     * @param {Object} params заменяемый параметр запроса
     * @return {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&stype=www");
     * url.replaceParam({stype: "images"});
     * url.toString(); // -> "http://ya.ru/?text=test&stype=images"
     */
    replaceParams: function(params) {
        var list = [];
        for (var name in params) {
            list.push(name);
        }
        this.removeParams(list);
        this.addParams(params);

        return this;
    },

    /**
     * Удаляет запрос из URL.
     * @return {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&stype=www");
     * url.clearQuery();
     * url.toString(); // -> "http://ya.ru/"
     */
    clearQuery: function() {
        this.Query = {};
        return this;
    },

    /**
     * Возвращает первый параметр из запроса URL.
     * @param {String} name имя параметра
     * @return {String | null} значение параметра запроса
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&text=www");
     * url.getParam("text"); // -> "test"
     * url.getParam("foo"); // -> null
     */
    getParam: function(name) {
        var params = this.Query[name];
        return params ? params[0] : null;
    },

    /**
     * Возвращает параметры из запроса URL.
     * @param {String} name имя параметра
     * @return {Array | []} список значений параметра
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&text=www");
     * url.getParams("text"); // -> ["test", "www"]
     * url.getParam("foo"); // -> []
     */
    getParams: function(name) {
        return this.Query[name] || [];
    }
};

/// aliases

/**
 * @name get
 * @link y5.URL.prototype.toString
 * @function
 */
y5.URL.prototype.get = y5.URL.prototype.toString;

})();

/**
 * Сокращенная функция для работы с URL
 * @param {String} href ссылка
 *
 * @example
 * y5.Url("http://www.yandex.ru/").path("/yandsearch").addParam({text: "test"}).go();
 * // -> "http://www.yandex.ru/yandsearch?text=test"
 */
y5.Url = function(href) {
    return new y5.URL(href);
};

y5.loaded('URL');/**
 * Класс для вывода по шаблону.
 * Функция вывода строки из шаблона, в котором вхождения вида #{name}
 * и/или ${name} заменяются на соответствующие параметры объекта.
 * Параметр вида #{name} выводится с преобразованием строки для вывода
 * в HTML, ${name} - как есть.
 *
 * @constructor
 * @class
 * @param {String} template строка-шаблон
 *
 * @example
 * var T = new y5.Template("Класс: #{name}");
 */
y5.Template = function(template) {
    this.template = template;
};

y5.Template.prototype = {
    // Правило для поиска подстановок
    pattern: /([#$])\{([^}]+)\}/g,

    /**
     * Вычисление выражения шаблона.
     * @param {Object} obj объект данных
     * @return {String} преобразованная строка
     *
     * @example
     * var T = new y5.Template("Класс: #{name}");
     * T.evaluate({name: "Template"});
     * // -> "Класс: Template"
     */
    evaluate: function(obj) {
        if (obj == null) {
            y5.Console.warn('y5.Template: object is null or undefined', 'Template');
            obj = {};
        }

        return this._evaluate(obj);
    },

    /**
     * Вычисление выражения шаблона (данные заданы массивом).
     * @param {Array} obj объект данных
     * @param {String} [div] разделитель выводимых строк (по умолчанию пустая строка)
     * @return {String} преобразованная строка
     *
     * @example
     * var T = new y5.Template("Класс: #{name}");
     * T.evaluateArray([{name: "Template"}, {name: "Classes"}], "\n");
     * // -> "Класс: Template\nКласс: Classes"
     */
    evaluateArray: function(arr, div) {
        var result = new Array(arr.length);

        for (var i = 0, l = arr.length; i < l; i++) {
            result[i] = this._evaluate(arr[i]);
        }

        return result.join(div || '');
    },

    /**
     * Вычисление выражения шаблона.
     * Отдельная функция введена для решения проблем совместимости со IE 5 (см. ниже)
     * @param {Object} obj объект данных
     * @return {String} преобразованная строка
     * @private
     */
    _evaluate: function(obj) {
        var _this = this;
        function replace(str, type, property) {
            return _this._replace(obj, type, property);
        }

        return this.template.replace(this.pattern, replace);
    },

    /**
     * Замена вхождений на их значение.
     * @param {Object} obj объект данных
     * @param {String} type '#' - выводим с преобразованием для HTML, '$' - выводим как есть
     * @param {String} property имя свойства
     * @return {String} значение элемента данных
     * @private
     */
    _replace: function(obj, type, property) {
        var value = obj[property];

        if (value == null) {
            y5.Console.warn('y5.Template: unknown property ' + property, 'Template');
            return '';
        }

        // Свойство с $ выводим как есть
        if (type == '$') {
            return value;
        }

        // Свойство с # выводим с преобразованием для HTML
        // type == '#'
        return y5.Strings.escapeHTML(String(value));
    }
};

if (y5.is_ie5) {

    y5.Template.prototype.pattern = /([#$])\{([^}]+)\}/;

    y5.Template.prototype._evaluate = function(obj) {
        var ret = [], found, str = this.template;

        while (true) {
            var found = this.pattern.exec(str);
            if (!found) break;

            ret.push(str.substring(0, found.index));
            ret.push(this._replace(obj, found[1], found[2]));
            str = str.substr(found.index + found[0].length);
        }

        if (str) {
            ret.push(str);
        }

        return ret.join('');
    };

}

/**
 * Сокращенная функция для вычисления выражения шаблона
 * @param {String} template строка-шаблон
 * @param {Object | Args} obj объект данных или все аргументы после строки шаблона
 * @return {String} преобразованная строка
 *
 * @example
 * // вместо
 * var T = new y5.Template("Класс: #{name}");
 * T.evaluate({name: "Template"});
 * // -> "Класс: Template"
 *
 * // используем
 * y5.T("Класс: #{name}", {name: "Template"});
 * // -> "Класс: Template"
 *
 * y5.T("Класс: #{1}", "Template");
 * // -> "Класс: Template"
 */
y5.T = function(template, obj) {
    // Если передан примитивный тип, то считаем, что параметры для
    // подстановки берем из аргументов
    var args = typeof(obj) != 'object' ? arguments : obj;

    return new y5.Template(template).evaluate(args);
};

y5.require(['Strings'],
    function() {
        y5.loaded('Template');
    }
);y5.require(['Cache', 'Strings'], function() {

var
    cache = true,
    cacheRegexp = new y5.Cache(),
    cacheTest = new y5.Cache(),
    Strings = y5.Strings,

    /**
     * Разбивает строку по пробелам.
     * Если имя - RegExp, то возвращает массив из этого элемента.
     * @param {String | RegExp} names имена классов
     * @return {Array} список классов
     * @private
     */
    split = function(names) {
        if (typeof names == 'string') {
            return names.split(' ');
        }

        // regexp
        if (typeof names.source != y5.UNDEF) {
            return [names];
        }

        return names;
    },

    /**
     * Проверяет переданный элемент и имя класса.
     * @param {String} method метод
     * @param {HTMLElement} element элемент для проверки
     * @param {String | RegExp} name имя класса
     * @private
     */
    checkElem = function(method, elem, name) {
        if (typeof elem == y5.UNDEF || elem == null) {
            throw new y5.Exception('object required', method, 'Classes');
        }
        if (!name || (typeof name != 'string' && !name.source)) {
            throw new y5.Exception('class name required', method, 'Classes');
        }
    },

    /**
     * Возвращает RegExp для работы с классами.
     * @param {String | RegExp} name имя класса
     * @return {RegExp} правило для нахождения класса
     * @private
     */
    rName = function(name, nocache) {
        var _name = '';
        var _ic = false;

        if (typeof name == 'string') {
            _name = Strings.escapeRegexp(name);
        } else {
            _name = name.source;
            _ic = name.ignoreCase;
        }

        if (!nocache && cache) {
            if (cacheRegexp.empty(_name)) {
                return cacheRegexp.set(_name, new RegExp('(^|\\s+)' + _name + '(\\s+|$)', (_ic ? 'i' : '')));
            }
            return cacheRegexp.get(_name);
        }
        return new RegExp('(^|\\s+)' + _name + '(\\s+|$)', (_ic ? 'i' : ''));
    };

/**
 * @class Функции для работы с CSS-классом элемента.
 * @static
 */
y5.Classes = {
    /**
     * Проверяет элемент на наличие CSS-класса.
     * @param {HTMLElement} element элемент
     * @param {String | RegExp} name имя класса
     * @param {Boolean} [nocache] не кешировать результат проверки
     * @return {Boolean} результат проверки
     * @example
     * &lt;div class="one two"/>
     * y5.Classes.test(element, 'two');
     * // -> true
     */
    test: function(elem, name, nocache) {
        checkElem('test', elem, name);

        if (name == '*') {
            return true;
        }
        try {
            if (!nocache && cache) {
                var n = name + ' ' + elem.className;
                if (cacheTest.empty(n)) {
                    return cacheTest.set(n, rName(name).test(elem.className));
                }
                return cacheTest.get(n);
            }
            return rName(name, nocache).test(elem.className);
        } catch (e) {}

        return false;
    },

    /**
     * Устанавливает элементу новое имя класса.
     * @param {HTMLElement} element элемент
     * @param {String} name имя класса
     * @example
     * &lt;div class="one"/>
     * y5.Classes.set(element, 'two');
     * // -> &lt;div class="two"/>
     */
    set: function(elem, name) {
        checkElem('set', elem, name);
        if (elem.className != name) {
            elem.className = name;
        }
    },

    /**
     * Добавляет элементу новые CSS-классы.
     * @param {HTMLElement} element элемент
     * @param {String | Array} name имя класса
     * @example
     * &lt;div class="one"/>
     * y5.Classes.add(element, 'two');
     * // -> &lt;div class="one two"/>
     */
    add: function(elem, names) {
        var _this = this;

        var addons = split(names).filter(
            function (name) {
                return !_this.test(elem, name);
            }
        ).join(' ');

        if (addons) {
            elem.className += ' ' + addons;
        }
    },

    /**
     * Удаляет у элемента CSS-классы.
     * @param {HTMLElement} element элемент
     * @param {String | Array} name имя класса
     * @example
     * &lt;div class="one two"/>
     * y5.Classes.remove(element, 'two');
     * // -> &lt;div class="one"/>
     */
    remove: function(elem, names) {
        var className = elem.className;

        split(names).forEach(
            function(name) {
                while (rName(name).test(className)) {
                    className = className.replace(rName(name), ' ');
                }
            }
        );

        elem.className = Strings.normalizeSpace(className);
    },

    /**
     * Заменяет у элемента один CSS-класс на другой.
     * @param {HTMLElement} element элемент
     * @param {String | RegExp} find заменяемое имя класса
     * @param {String} replace новое имя класса
     * @example
     * &lt;div class="one two"/>
     * y5.Classes.replace(element, 'two', 'three');
     * // -> &lt;div class="one three"/>
     */
    replace: function (elem, find, replace) {
        if (this.test(elem, find)) {
            elem.className = Strings.normalizeSpace(elem.className.replace(rName(find, true), '$1' + replace + '$2'));
        }
    },

    /**
     * Добавляет или удаляет у элемента CSS-класс в зависимости от условия.
     * @param {HTMLElement} element элемент
     * @param {String | Array} name имя класса
     * @param {Boolean} test если значение равно true, то добавляется новый класс, иначе - удаляется
     * @example
     * &lt;div class="one"/>
     * y5.Classes.assign(element, 'two', true);
     * // -> &lt;div class="one two"/>
     *
     * &lt;div class="one two"/>
     * y5.Classes.assign(element, 'two', false);
     * // -> &lt;div class="one"/>
     */
    assign: function (elem, name, test) {
        if (test) {
            this.add(elem, name);
        } else {
            this.remove(elem, name);
        }
    },

    /**
     * Удаляет или добавляет у элемента CSS-класс в зависимости от того, установлен класс или нет.
     * @param {HTMLElement} element элемент
     * @param {String} name имя класса
     * @example
     * &lt;div class="one two"/>
     * y5.Classes.toggle(element, 'two');
     * // -> &lt;div class="one"/>
     *
     * &lt;div class="one"/>
     * y5.Classes.toggle(element, 'two');
     * // -> &lt;div class="one two"/>
     */
    toggle: function (elem, name) {
        this.assign(elem, name, !this.test(elem, name));
    },

    /**
     * Заменяет один CSS-класс другим, в зависимости от того, какой присутствует.
     * @param {HTMLElement} element элемент
     * @param {String} one имя класса
     * @param {String} two имя класса
     * @example
     * &lt;div class="one"/>
     * y5.Classes.swap(element, 'two', 'one');
     * // -> &lt;div class="two"/>
     *
     * &lt;div class="two"/>
     * y5.Classes.swap(element, 'two', 'one');
     * // -> &lt;div class="one"/>
     */
    swap: function (elem, one, two) {
        if (this.test(elem, one)) {
            this.replace(elem, one, two);
        } else if (this.test(elem, two)) {
            this.replace(elem, two, one);
        } else {
            this.add(elem, one);
        }
    }
};

y5.loaded('Classes');

});
/**
 * Функции для создания элементов DOM.
 * @class
 * @static
 */
y5.Elements = {
    /**
     * Создает элемент, используя параметры объекта.
     * @param {Object} object объект вида {tagName:'...', attributes: {name:'...', ...}}
     * @return {Element} элемент
     */
    createElement: function(e) {
        var element,
            attributes = e.attributes,
            tagName = e.tagName;

        if (attributes && attributes.name) {
            element = this.createElementWithName(tagName, attributes.name);
        } else {
            element = document.createElement(tagName);
        }
        if (attributes) {
            this.setElementAttributes(element, attributes);
        }
        return element;
    },

    /**
     * Устанавливает атрибуты элементу.
     * @param {Element} element
     * @param {Object} список атрибутов, вида {name:'test', ...}
     */
    setElementAttributes: function(element, attributes) {
        for (var name in attributes) {
            element.setAttribute(name, attributes[name]);
        }
    },

    /**
     * Создает элемент с заданным параметром name.
     * Обход проблемы IE.
     * @param {String} tagName имя элемента (имя тега)
     * @param {String} name параметр name элемента
     * @return {Element} элемент
     */
    createElementWithName: function(tag, name) {
        if (y5.is_ie) {
            return document.createElement('<' + tag + ' name="' + name + '"/>');
        }

        var element = document.createElement(tag);
        element.name = name;

        return element;
    },

    /**
     * Создает элемент из строки разметки.
     * @param {String} html текст HTML
     * @return {Element} первый элемент из списка созданных
     */
    createElementFromHTML: function(html) {
        var tmpDiv = document.createElement('div');
        tmpDiv.innerHTML = html;
        return tmpDiv.firstChild;
    }
};

y5.loaded('Elements');
y5.require(['Classes', 'Strings'], function() {

var Types = y5.Types,
    Classes = y5.Classes;

function domGetById(id) {
    return document.getElementById(id);
}

function elementsFilterByClassName(elements, className, limit) {
    limit = limit || elements.length;
    var result = new Array(limit), element, i = 0, j = 0;
    while (j < limit && (element = elements[i++])) {
        if (Classes.test(element, className)) {
            result[j++] = element;
        }
    }
    result.length = j;
    return result;
}

/* ems convert function */
function emConvert(value, element, func) {
    element = element || y5.Dom.getBody();

    var elm = element.appendChild(document.createElement('span'));
    elm.style.cssText = 'position:absolute;display:block;visibility:hidden;width:100em';

    var result = func(value, elm.clientWidth, 100);
    element.removeChild(elm);

    return result;
}

function em2pxConvert(value, width, num) {
    return value * width / num;
}

function px2emConvert(value, width, num) {
    return value / (width / num);
}

function elements2array(elements) {
    var length = elements.length,
        nodes = new Array(length);

    for (var i = 0; i < length; i++) {
        nodes[i] = elements[i];
    }

    return nodes;
}

function getFirstFromSet(elements) {
    if (typeof elements[0] != y5.UNDEF) {
        return elements[0];
    }
    return null;
}

/**
 * Функции для работы с DOM.
 * @class
 * @static
 */
y5.Dom = {
    /**
     * Браузер поддерживает запросы с помощью XPath.
     */
    XPathSupport: typeof XPathEvaluator !== y5.UNDEF,

    /**
     * Возвращает элемент body документа.
     * @return {HTMLElement} элемент body
     */
    getBody: function() {
        return this.body || (this.body = document.body || this.getElementByTagName('body'));
    },

    /**
     * Возвращает элемент html документа.
     * @return {HTMLElement} элемент html
     */
    getHtml: function() {
        return this.html || (this.html = document.documentElement || this.getElementByTagName('html'));
    },

    /**
     * Проверяет имя элемента.
     * @param {Element} element элемент для проверки
     * @param {String | Array} tagName имя/имена для проверки
     * @return {Boolean} результат проверки
     *
     * @example
     * &lt;div />
     * y5.Dom.testTagName(div, 'div');
     * // -> true
     * y5.Dom.testTagName(div, 'span');
     * // -> false
     * y5.Dom.testTagName(div, ['div', 'span']);
     * // -> true
     */
    testTagName: function(element, tagName) {
        if (!element || !tagName || !element.tagName) {
            return false;
        }

        if (tagName === '*') {
            return true;
        }

        var elTagName = element.tagName.toLowerCase();
        if (typeof tagName === 'string') {
            return elTagName === tagName.toLowerCase();
        }

        for (var i = 0, l = tagName.length; i < l; i++) {
            if (elTagName === tagName[i].toLowerCase()) {
                return true;
            }
        }

        return false;
    },

    /**
     * Возвращает первого потомка с заданным именем.
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {Element} [context] контекст поиска (если не указан, то используется document)
     * @return {Element} элемент
     */
    getElementsByTagName: function(tagName, context) {
        return elements2array((context || document).getElementsByTagName(tagName));
    },

    /**
     * Возвращает первого потомка с заданным именем.
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {Element} [context] контекст поиска (если не указан, то используется document)
     * @return {Element} элемент
     */
    getElementByTagName: function(tagName, context) {
        return getFirstFromSet((context || document).getElementsByTagName(tagName));
    },

    /**
     * Возвращает потомков с заданным именем и классом.
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса
     * @param {Element} [context] контекст поиска (если не указан, то используется document)
     * @param {Number} [limit] ограничить количество элементов
     * @return {Array of Elements} набор узлов документа
     */
    getElementsByTagNameAndClass: function(tagName, className, context, limit) {
        tagName = tagName || '*';
        context = context || document;
        var elements = (tagName === '*' && context.all ? context.all : context.getElementsByTagName(tagName));
        return elementsFilterByClassName(elements, className, limit);
    },

    /**
     * Возвращает первого потомка с заданным именем и классом.
     * Эквивалентно вызову getElementsByTagNameAndClass с параметром limit == 1.
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса
     * @param {Element} [context] контекст поиска (если не указан, то используется document)
     * @return {Element} узел документа
     */
    getElementByTagNameAndClass: function(tagName, className, context) {
        return getFirstFromSet(this.getElementsByTagNameAndClass(tagName, className, context, 1));
    },

    /**
     * Возвращает потомков с заданным классом.
     * @param {String} className имя класса
     * @param {Element} [context] контекст поиска (если не указан, то используется document)
     * @param {Number} [limit] ограничить количество элементов
     * @return {Array of Elements} набор узлов документа
     */
    getElementsByClass: function(className, context, limit) {
        return this.getElementsByTagNameAndClass('*', className, context, limit);
    },

    /**
     * Возвращает первого потомка с заданным классом.
     * Эквивалентно вызову getElementsByClass с параметром limit == 1.
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса
     * @param {Element} [context] контекст поиска (если не указан, то используется document)
     * @return {Element} узел документа
     */
    getElementByClass: function(className, context) {
        return getFirstFromSet(this.getElementsByClass(className, context, 1));
    },

    /**
     * Возвращает первого предка с заданным именем.
     * @param {Element} context контекст поиска
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @return {Element} узел документа
     */
    getParentByTagName: function(context, tagName) {
        while (context) {
            if (this.testTagName(context, tagName)) {
                return context;
            }
            context = context.parentNode;
        }
        return null;
    },

    /**
     * Возвращает первого предка с заданным именем класса.
     * @param {Element} context контекст поиска
     * @param {String} className имя класса
     * @return {Element} узел документа
     */
    getParentByClass: function(className, context) {
        while (context) {
            if (Classes.test(context, className)) {
                return context;
            }
            context = context.parentNode;
        }
        return null;
    },

    /**
     * Проверяет, что элемент является дочерним.
     * @param {Element} childNode проверяемый элемент
     * @param {Element} [parentNode] предполагаемый родитель (если не указан, то используется document)
     * @return {Boolean} результат проверки
     */
    isChild: function(childNode, parentNode) {
        if (parentNode == document) {
            return true;
        }
        while (childNode) {
            if (childNode === parentNode) {
                return true;
            }
            childNode = childNode.parentNode;
        }
        return false;
    },

    /**
     * Удаляет узел из документа.
     * @param {Node} node узел
     */
    removeNode: function(node) {
        if (node && node.parentNode) {
            node.parentNode.removeChild(node);
        }
    },

    /**
     * Удаляет всех потомков узла.
     * @param {Node} node узел
     */
    clearNode: function(node) {
        if (!node) {
            return null;
        }
        while (node.firstChild) {
            node.removeChild(node.firstChild);
        }
        return node;
    },

    /**
     * Удаляет узел из документа и добавляет всех его потомков к его родителю.
     * @param {Node} node узел
     */
    cutNode: function(node) {
        var parent = node.parentNode;
        while (node.firstChild) {
            parent.appendChild(node.firstChild);
        }
        this.removeNode(node);
    },

    /**
     * Возвращает первый предыдущий элемент по имени и классу.
     * @param {Element} context контекст поиска
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса ('*' - любое имя)
     * @return {Element | null} результат поиска
     */
    getPreviousElement: function(context, tagName, className) {
        return this.getElement(context, tagName, className, 'previousSibling');
    },

    /**
     * Возвращает первый следующий элемент по имени и классу.
     * @param {Element} context контекст поиска
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса ('*' - любое имя)
     * @return {Element | null} результат поиска
     */
    getNextElement: function(context, tagName, className) {
        return this.getElement(context, tagName, className, 'nextSibling');
    },

    /**
     * Возвращает первый элемент-потомок по имени и классу.
     * @param {Element} context контекст поиска
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса ('*' - любое имя)
     * @return {Element | null} результат поиска
     * @todo рефакторинг
     */
    firstDescendant: function(context, tagName, className) {
        context = context.firstChild;

        while (context) {
            if (this.testElement(context, tagName, className)) {
                return context;
            }
            context = context.nextSibling;
        }

        return null;
    },

    /**
     * Вставляет новый элемент перед указанным.
     * Обертка для DOM-функции insertBefore.
     * @param {Element} newElem элемент для вставки
     * @param {Element} refElem элемент перед которым будет вставлен новый элемент
     * @return {Node} вставленный элемент
     * @example
     * // - parent
     * //    - refElem
     * y5.Dom.insertBefore(newElem, refElem);
     * // - parent
     * //    - newElem*
     * //    - refElem
     */
    insertBefore: function(newElem, refElem) {
        return refElem.parentNode.insertBefore(newElem, refElem);
    },

    /**
     * Вставляет новый элемент после указанного.
     * @param {Element} newElem элемент для вставки
     * @param {Element} refElem элемент после которого будет вставлен новый элемент
     * @return {Node} вставленный элемент
     * @example
     * // - parent
     * //    - refElem
     * y5.Dom.insertAfter(newElem, refElem);
     * // - parent
     * //    - refElem
     * //    - newElem*
     */
    insertAfter: function(newElem, refElem) {
        var next = refElem.nextSibling;
        if (next) {
            return this.insertBefore(newElem, next);
        }

        return refElem.parentNode.appendChild(newElem);
    },

    /**
     * Возвращяет массив дочерних элементов.
     * @param {Element} context контекстный элемент
     * @return {Array} дочерние элементы
     */
    childNodes: function(context) {
        return elements2array(context.childNodes);
    },

    /**
     * Проверяет элемент на имя и класс.
     * @param {Element} element проверяемый элемент
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса ('*' - любое имя)
     * @return {Boolean} результат проверки
     */
    testElement: function(element, tagName, className) {
        return (
                Types.element(element) &&
                this.testTagName(element, tagName || '*') &&
                Classes.test(element, className || '*')
        );
    },

    /**
     * Returns textContent of passed element.
     * @param {Object} DOM node.
     * @return {String} innerText.
     */
    textContent: (function() {
        var span = document.createElement('span');
        if (y5.gecko_ver > 1.7 && Types.def(span.textContent)) {
            return function(element) {
                return element.textContent;
            }
        } else if (Types.def(span.innerText)) {
            return function(element) {
                return element.innerText;
            }
        } else {
            return function(element) {
                // using RegExp for IE 5
                return element.innerHTML.replace(new RegExp('<.*?>', 'g'), '');
            }
        }
    })(),

    /**
     * Возвращает размеры видимой области документа.
     * @return {Array} [ширина, высота]
     */
    viewPort: function() {
        var body = this.getBody();
        return [body.clientWidth, body.clientHeight];
    },

    /**
     * Возвращает левое и верхнее смещение элемента относительно предка.
     * @param {Element} element элемент
     * @param {Element} [parent] предок
     * @return {Array} [ширина, высота]
     */
    getOffset: function(element, parent) {
        var x = 0, y = 0;
        parent = parent || document;

        while (element !== null && element !== parent) {
            // offset added
            x += element.offsetLeft || 0;
            y += element.offsetTop || 0;

            if (this.getPropertyValue(element, 'position') == 'static') {
                // border added
                x += this.getPropertyValuePx(element, 'border-left-width');
                y += this.getPropertyValuePx(element, 'border-top-width');
            }

            element = element.offsetParent;
        }

        return [x, y];
    },

    /**
     * Возвращает верхнее смещение элемента относительно предка.
     * @param {Element} element элемент
     * @param {Element} [parent] предок
     * @return {Number} смещение
     */
    offsetTop: function(element, parent) {
        return this.getOffset(element, parent)[1];
    },

    /**
     * Возвращает левое смещение элемента относительно предка.
     * @param {Element} element элемент
     * @param {Element} [parent] предок
     * @return {Number} смещение
     */
    offsetLeft: function(element, parent) {
        return this.getOffset(element, parent)[0];
    },

    /**
     * Возвращает позицию горизонтальной прокрутки страницы.
     * @return {Number} позиция прокрутки
     */
    getPageScrollX: function() {
        return this.getBody().scrollLeft || this.getHtml().scrollLeft;
    },

    /**
     * Возвращает позицию вертикальной прокрутки страницы.
     * @return {Number} позиция прокрутки
     */
    getPageScrollY: function() {
        return this.getBody().scrollTop || this.getHtml().scrollTop;
    },

    /**
     * Возвращает вычисленные стили элемента.
     * @param {Element} element элемент
     * @return {Object} стили
     * @todo перенести в y5.CSS
     */
    getStyle: function(element) {
        return document.defaultView.getComputedStyle(element, null);
    },

    /**
     * Возвращает вычисленное значение CSS-свойства элемента.
     * @param {Element} element элемент
     * @param {String} propname свойство, например 'margin-left'
     * @return {String} значение свойства, например '5px'
     * @todo перенести в y5.CSS
     */
    getPropertyValue: function(element, propname) {
        return this.getStyle(element).getPropertyValue(propname);
    },

    /**
     * Возвращает вычисленное числовое значение CSS-свойства элемента.
     * @param {Element} element элемент
     * @param {String} propname свойство, например 'margin-left'
     * @return {String} значение свойства, например 5
     * @todo перенести в y5.CSS
     */
    getPropertyValuePx: function(element, propname) {
        var value = parseInt(this.getPropertyValue(element, propname), 10);
        if (!isNaN(value)) {
            return value;
        }
        return 0;
    },

    /**
     * Преобразовывает значение свойства элемента из em в px.
     * @param {Number} em значение в em
     * @param {Element} element элемент (НЕ 'display: none').
     * @return {Number} результат в px
     * @todo перенести в y5.CSS
     */
    em2px: function(em, element) {
        return emConvert(em, element, em2pxConvert);
    },

    /**
     * Преобразовывает значение свойства элемента из px в em.
     * @param {Number} px значение в px
     * @param {Element} element элемент (НЕ 'display: none').
     * @return {Number} результат в em
     * @todo перенести в y5.CSS
     */
    px2em: function(px, element) {
        return emConvert(px, element, px2emConvert);
    },

    /// private

    /**
     * Возвращает первый элемент заданного типа.
     * @param {Element} context контекст поиска
     * @param {String} tagName имя элемента ('*' - любое имя)
     * @param {String} className имя класса ('*' - любое имя)
     * @param {Boolean} type тип элемента: nextSibling, previousSibling, firstChild...
     * @return {Element | null} результат поиска
     * @private
     */
    getElement: function(context, tagName, className, type) {
        while ((context = context[type])) {
            if (this.testElement(context, tagName, className)) {
                return context;
            }
        }

        return null;
    },

    /**
     * Возвращает элементы по запросу XPath.
     * @param {String} expression выражение XPath
     * @param {Element} [context] контекст запроса (если не указан, то document)
     * @return {Array of Elements} набор узлов документа
     * @private
     */
    getElementsByXPath: function(expression, context) {
        var evaluator = new XPathEvaluator();
        var query = evaluator.evaluate(expression, context || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        var length = query.snapshotLength;
        var result = new Array(length);
        for (var i = 0; i < length; i++) {
            result[i] = query.snapshotItem(i);
        }
        return result;
    },

    /**
     * Возвращает первый элемент по запросу XPath.
     * @param {String} expression выражение XPath
     * @param {Element} [context] контекст запроса (если не указан, то document)
     * @return {Array of Elements} набор узлов документа
     * @private
     */
    getElementByXPath: function(expression, context) {
        var evaluator = new XPathEvaluator();
        var query = evaluator.evaluate(expression, context || document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

        return query != null ? query.singleNodeValue : null;
    }
};

var Dom = y5.Dom;

if (Dom.XPathSupport) {

    Dom.getParentByClass = function(className, context) {
        var elements = null;
        var expression = 'ancestor-or-self::*';
        if (typeof className == 'string') {
            if (className && className != '*') {
                expression += "[contains(concat(' ', @class, ' '), ' " + className + " ')]";
            }
            return this.getElementByXPath(expression, context);
        } else {
            elements = this.getElementsByXPath(expression + '[1]', context);
            elements = elementsFilterByClassName(elements, className);
        }
        return getFirstFromSet(elements);
    };

    Dom.getElementsByTagName = function(tagName, context) {
        return this.getElementsByXPath('descendant::' + (tagName || '*'), context);
    };

    Dom.getElementByTagName = function(tagName, context) {
        return this.getElementByXPath('descendant::' + (tagName || '*'), context);
    };

    Dom.getElementsByTagNameAndClass = function(tagName, className, context, limit) {
        var expression = 'descendant::' + (tagName || '*');
        if (typeof className == 'string') {
            if (className && className != '*') {
                expression += "[contains(concat(' ', @class, ' '), ' " + className + " ')]";
            }
            if (limit) {
                expression += '[position() <= ' + limit + ']';
            }
            return this.getElementsByXPath(expression, context);
        } else {
            return elementsFilterByClassName(this.getElementsByXPath(expression, context), className, limit);
        }
    };

}

if (window.innerHeight) { // all except Explorer

    Dom.viewPort = function() {
        return [window.innerWidth, window.innerHeight];
    };

} else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode

    Dom.viewPort = function() {
        var e = document.documentElement;
        return [e.clientWidth, e.clientHeight];
    };

}

// http://msdn2.microsoft.com/en-us/library/ms536433.aspx
// http://developer.mozilla.org/en/docs/DOM:element.getBoundingClientRect
if (document.documentElement && document.documentElement.getBoundingClientRect) { // msie 5+, ff3

    Dom.getOffset = function(element) {
        if (element === document) {
            return [0, 0];
        }
        var box = element.getBoundingClientRect();
        // FF returns float value => Math.round
        // IE adds border width => -document.documentElement.clientLeft and Top
        return [
            Math.round(box.left - document.documentElement.clientLeft + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft)),
            Math.round(box.top - document.documentElement.clientTop + Math.max(document.documentElement.scrollTop,  document.body.scrollTop))
        ];
    };
	
// getBoxObjectFor is deprecated in ff3
} else if (document.getBoxObjectFor) { // Gecko 1.0.1+

    Dom.getOffset = function(element) {
        if (element === document) {
            return [0, 0];
        }
        var box = document.getBoxObjectFor(element);
        return [box.x, box.y];
    };

}

if (typeof window.pageXOffset == 'number') {

    Dom.getPageScrollX = function() {
        return window.pageXOffset;
    };

    Dom.getPageScrollY = function() {
        return window.pageYOffset;
    };

}

if (typeof document.defaultView == y5.UNDEF) {

    Dom.getStyle = function(element) {
        return element.currentStyle;
    };

    Dom.getPropertyValue = function(element, propname) {
        var splits = propname.split(/-/g);
        for (var i = 1, l = splits.length; i < l; i++) {
            splits[i] = y5.Strings.capitalize(splits[i]);
        }

        var name = splits.join('');
        switch (name) {
            case 'opacity':
                var value = 100;
                try {
                    value = element.filters['DXImageTransform.Microsoft.Alpha'].opacity;
                } catch (e) {
                    try {
                        value = element.filters('alpha').opacity;
                    } catch(e) { }
                }
                return value / 100;

            case 'float':
                name = 'styleFloat';
                break;
        }

        return element.currentStyle[name];
    };

}

/// aliases

/**
 * @name $
 * @link y5.Dom.$
 * @memberof y5
 * @function
 * @param {String} id
 * @return {Element} элемент DOM
 */
y5.$ = domGetById;

/**
 * Сокращение для document.getElementById.
 * @name $
 * @memberof y5.Dom
 * @function
 * @param {String} id
 * @return {Element} элемент DOM
 */
Dom.$ = domGetById;

/**
 * @name getNextElement
 * @memberof y5.Dom
 * @function
 * @deprecated y5.Dom.getFollowing
 */
Dom.deleteNode = Dom.removeNode;

/**
 * @deprecated
 */
Dom.getOffsset = Dom.getOffset;

/**
 * @deprecated
 */
Dom.innerText = Dom.textContent;

y5.loaded('Dom');

});
y5.require('Dom', function() {

var Types = y5.Types,

    // функция для преобразования свойств Event
    normalizeEventSpecific,

    // коды клавиш мыши
    mouseButtons = {L: [0, 65535], M: [1], R: [2]},

    specificTypes = {};

if (y5.is_ie) {

    function IE_preventDefault() {
        this.returnValue = false;
    }

    function IE_stopPropagation() {
        this.cancelBubble = true;
    }

    mouseButtons = {L: [1], M: [4], R: [2]};

    normalizeEventSpecific = function(e) {
        e.timeStamp = new Date().getTime();
        e.charCode = e.type == 'keypress' ? e.keyCode : 0;
        e.isChar = e.charCode > 0;
        e.target = e.srcElement;
        e.metaKey = e.altKey;
        e.attrName = e.propertyName == 'className' ? 'class' : e.propertyName;
        e.preventDefault = IE_preventDefault;
        e.stopPropagation = IE_stopPropagation;

        var root = y5.Dom.getBody();
        e.pageX = e.clientX + root.scrollLeft;
        e.pageY = e.clientY + root.scrollTop;

        switch (e.type) {
            case 'mouseout':
                e.relatedTarget = e.toElement;
                break;

            case 'mouseover':
                e.relatedTarget = e.fromElement;
                break;
        }

        // mouse scroll details
        e.scrollDetail = 0;
        if (e.wheelDelta) {
            e.scrollDetail = -e.wheelDelta / 40;
        }
    };

} else if (y5.is_safari) {

    mouseButtons = {L: [0, 65535], M: [2], R: [3]};
    if (y5.safari_ver <= 420) {
        mouseButtons.L = [1];
    }

    normalizeEventSpecific = function(e) {
        // TODO! раскрыть смысл
        if (!Types.func(e.preventDefault)) {
            e.preventDefault = y5.NULL;
        }

        if (!Types.func(e.stopPropagation)) {
            e.stopPropagation = y5.NULL;
        }

        // Если событие произошло на текстовом узле (TEXT_NODE == 3, CDATA == 4)
        if (e.target && (e.target.nodeType == 3 || e.target.nodeType == 4)) {
            e.target = e.target.parentNode;
        }

        if (e.wheelDelta) {
            e.scrollDetail = -e.wheelDelta / 400;
        }
    };

} else if (y5.is_opera) {

    normalizeEventSpecific = function(e) {
        // mouse scroll details
        e.scrollDetail = 0;
        if (e.wheelDelta) {
            e.scrollDetail = e.wheelDelta / 40;
        }
        if (y5.opera_ver >= 9.2) {
            e.scrollDetail *= -1;
        }
    };

    if (y5.opera_ver < 8) {
        mouseButtons = {L: [1], M: [2], R: [3]};
    }

} else {

    normalizeEventSpecific = function(e) {
        // mouse scroll details
        e.scrollDetail = e.detail;
    };
}

function normalizeEvent(e) {
    var button = e.button;
    if (typeof button != y5.UNDEF) {
        e.buttonL = mouseButtons.L.indexOf(button) != -1;
        e.buttonM = mouseButtons.M.indexOf(button) != -1;
        e.buttonR = mouseButtons.R.indexOf(button) != -1;
    } else {
        e.buttonL = e.buttonM = e.buttonR = false;
    }

    normalizeEventSpecific(e);

    return e;
}

/// Обрабатываем не-DOM типы событий

var typeDOMAttrModified = 'DOMAttrModified',
    typePropertyChange = 'propertychange',
    typeDOMMouseScroll = 'DOMMouseScroll',
    typeMouseWheel = 'mousewheel';

if (document.attachEvent) { // IE
    specificTypes[typeDOMAttrModified] = typePropertyChange;
} else { // DOM
    specificTypes[typePropertyChange] = typeDOMAttrModified;
}

if (y5.is_ie || y5.is_opera || y5.is_safari) {
    // браузеры с событием колеса мыши 'mouseweel'
    specificTypes[typeDOMMouseScroll] = typeMouseWheel;
} else {
    specificTypes[typeMouseWheel] = typeDOMMouseScroll;
}

function normalizeType(type) {
    return specificTypes[type] || type;
}


// вызов функции с контекстом
function execListenerWithContext(listener, context, evt, element) {
    context ? listener.call(context, evt, element) : listener(evt, element);
}

/**
 * Вспомогательный класс. Обрабатывает объект Event, для преобразования к общему виду.
 * @private
 */
y5.HandleEvent = {
    normalizeEvent: normalizeEvent
};

/**
 * Удаляет установленные обработчики событий.
 * @private
 */
var EventListenerCleaner = {
    _data: [],

    add: function(listener) {
        if (listener.type == 'unload') {
            return;
        }
        this._data[this._data.length] = listener;
    },

    cleanup: function() {
        for (var i = 0, l = this._data.length; i < l; ++i) {
            this._data[i].cleanup();
            this._data[i] = null;
        }
    }
};

/**
 * Слушатель событий.
 * Позволяет устанавливать обработчик на различные события браузера и DOM.
 * @constructor
 * @param {String} type тип события: click, load...
 * @param {Function} listener callback-функция на возникновение события
 * @param {Element | Object} element объект на котором возникает событие
 * @param {Boolean} add начать слушать событие немедленно
 * @param {Object} context контекст, в котором выполняется callback
 *
 * @example
 * new y5.AEventListener("click", callback, link, true);
 */
y5.AEventListener = function(type, listener, element, add, context) {
    this.type = normalizeType(type);
    this.element = element || document;
    this.blocked = false;
    this.added = false;

    var _this = this;
    this.listener = function(evt) {
        var e = typeof evt != y5.UNDEF ? normalizeEvent(evt) : {};

        if (_this.blocked) {
            e.stopPropagation();
            e.preventDefault();
            return;
        }

        execListenerWithContext(listener, context, e, _this.element);
    };

    if (add) {
        this.add();
    }

    if (EventListenerCleaner) {
        EventListenerCleaner.add(this);
    }
};

y5.AEventListener.prototype = {
    /**
     * Включает слушатель.
     */
    add: function() {
        if (this.added) return;

        this._add();
        this.added = true;
    },

    /**
     * @private
     */
    _add: function() {
        this.element.addEventListener(this.type, this.listener, false);
    },

    /**
     * Выключает слушатель.
     */
    remove: function() {
        if (!this.added) return;

        this._remove();
        this.added = false;
    },

    /**
     * @private
     */
    _remove: function() {
        this.element.removeEventListener(this.type, this.listener, false);
    },

    /**
     * Блокирует слушатель: не выполняет функцию callback, отменяет действие
     * элемента на данное событие по умолчанию и продолжение события.
     */
    block: function() {
        this.blocked = true;
    },

    /**
     * Разблокирует слушатель.
     */
    unblock: function() {
        this.blocked = false;
    },

    /**
     * @private
     */
    cleanup: function() {
        this.remove();
        this.element = null;
        this.listener = null;
    }
};

var AEventListener = y5.AEventListener;

if (document.attachEvent) { // IE

    AEventListener.prototype._add = function() {
        this.element.attachEvent('on' + this.type, this.listener);
    };

    AEventListener.prototype._remove = function() {
        this.element.detachEvent('on' + this.type, this.listener);
    };

}

/// Создание событий

// Типы событий (w3)
var eventTypes = {
        Mouse: /^mouse|click/,
        Key: /^key/,
        Mutation: /^DOM/,
        HTML: /./
    },

    setEvent = function(event, evt) {
        for (var i in evt) {
            try {
                event[i] = evt[i];
            } catch(e) {}
        }
        return event;
    };

/**
 * Позволяет делать симуляцию различных событий.
 * @constructor
 * @param {String} [type] тип события: click, mousedown... (если не указан, то используется click)
 * @param {Object} [element] целевой элемент события (если не указан, то используется document)
 * @param {Boolean} [dispatch] отправить событие (если не указано, то событие возникает сразу)
 * @param {Object} [event] параметры события
 *
 * @example
 * new y5.Event("click", link);
 */
y5.Event = function(type, element, dispatch, event) {
    this.type = normalizeType(type || 'click');
    this.element = element || document;

    if (typeof dispatch == y5.UNDEF) {
        dispatch = true;
    }

    this.event = setEvent(this.init(), event || {});

    if (dispatch) {
        this.dispatchEvent();
    }
};

y5.Event.prototype = {
    /**
     * Инициализация.
     * @private
     */
    init: function() {
        for (var type in eventTypes) {
            if (eventTypes[type].test(this.type)) {
                return document.createEvent(type + 'Events');
            }
        }
        return null;
    },

    /**
     * Отправка события.
     *
     * @example
     * var E = new y5.Event("click", link, false);
     * // ...
     * E.dispatch();
     */
    dispatch: function() {
        this.event.initEvent(this.type, true, true);
        this.element.dispatchEvent(this.event);
    }
};

var Event = y5.Event;

if (document.createEventObject) { // IE

    Event.prototype.init = function() {
        return document.createEventObject();
    };

    Event.prototype.dispatch = function() {
        this.element.fireEvent('on' + this.type, this.event);
    };

}

/**
 * @name dispatchEvent
 * @link y5.Event.dispatch
 * @function
 * @deprecated
 */
Event.prototype.dispatchEvent = Event.prototype.dispatch;


/**
 * Менеджер изменения полей ввода.
 * @private
 */

var InputObserverManager = {};

(function() {

    var listeners = [];
    var timer = null;

    function runListeners() {
        if (listeners.length == 0) {
            killTimer();
            return;
        }

        var start = new Date().getTime();

        for (var i = 0, l = listeners.length; i < l; i++) {
            listeners[i]();
        }

        // время работы callback-функций
        var time = new Date().getTime() - start + 1;

        // запускаем таймер в зависимости от скорости работы callback
        setTimer(time < 150 ? (Math.floor(Math.log(time)) || 1) * 100 : 1000);
    }

    function runListenersAsync() {
        window.setTimeout(runListeners, 0);
    }

    function setTimer(msec) {
        timer = window.setTimeout(runListenersAsync, msec);
    }

    function killTimer(msec) {
        timer = null;
    }

    InputObserverManager.addListener = function(listener) {
        listeners.push(listener);
        if (!timer) {
            setTimer(100);
        }
    };

    InputObserverManager.removeListener = function(listener) {
        var pos = listeners.indexOf(listener);
        if (pos != -1) {
            listeners.splice(pos, 1);
        }
    };

})();

/**
 * Слушатель изменения элемента ввода.
 * @class
 * @constructor
 * @param {Function} listener callback-функция
 * @param {Element} element элемент
 * @param {Boolean} [add] начать слушать немедленно (false - по умолчанию)
 * @param {Object} [context] контекст выполнения функции listener
 *
 * @example
 * new y5.InputObserver("value", callback, element, true);
 */
y5.InputObserver = function(listener, element, add, context) {
    this.element = element;
    this.state = this.getState();
    this.initState = this.getState();
    this.added = false;

    var _this = this;
    this.listener = function() {
        if (_this.isChanged()) {
            execListenerWithContext(listener, context, element);
            _this.state = _this.getState();
        }
    };

    if (add) {
        this.add();
    }
};

y5.InputObserver.prototype = {
    /**
     * Добавляет слушатель.
     */
    add: function() {
        if (!this.added) {
            InputObserverManager.addListener(this.listener);
            this.added = true;
        }
    },

    /**
     * Удаляет слушатель.
     */
    remove: function() {
        if (this.added) {
            InputObserverManager.removeListener(this.listener);
            this.added = false;
        }
    },

    /**
     * Получает текущее состояние элемента ввода.
     * @return {Object} объект состояния
     */
    getState: function() {
        var element = this.element;

        return {
            value: element.value,
            checked: element.checked,
            selected: element.selected,
            selectedIndex: element.selectedIndex
        };
    },

    /**
     * Возвращает изменение состояния ввода в сравнении с предыдущим.
     * @private
     * @return {Boolean} изменение текущего состояния элемента в сравнении с this.state
     * @note Изменение this.state происходит при каждом выполнении функции callback.
     */
    isChanged: function() {
        return this.stateChanged(this.state);
    },

    /**
     * Возвращает изменение состояния ввода в сравнении с начальным.
     * @return {Boolean} изменение текущего состояния элемента в сравнении с this.initState
     */
    isInitChanged: function() {
        return this.stateChanged(this.initState);
    },

    /**
     * Сравнивает состояния элемента ввода.
     * @private
     * @param {Object} state сравниваемое состояние
     * @return {Boolean} изменение текущего состояния элемента в сравнении с state
     */
    stateChanged: function(state) {
        var element = this.element;

        return (
            state.checked != element.checked ||
            state.selected != element.selected ||
            state.selectedIndex != element.selectedIndex ||
            state.value != element.value
        );
    }
};


/**
 * Тайм-менеджер.
 * @private
 */

var TimerObserverManager = {};

(function() {

    // период запуска таймера
    var time = 100;

    // время таймера
    var timeCount = 0;
    var listeners = [];
    var periods = [];
    var timer = null;

    function runListeners() {
        if (listeners.length == 0) {
            killTimer();
            return;
        }

        timeCount += time;

        for (var i = 0, l = listeners.length; i < l; i++) {
            var period = periods[i];
            var hit = timeCount / period;
            var diff = hit - Math.floor(hit);
            if (diff >= 0 && diff < (time - 1) / period) {
                listeners[i]();
            }
        }
    }

    function setTimer() {
        timer = window.setInterval(runListeners, time);
    }

    function killTimer() {
        window.clearTimeout(timer);
        timer = null;
        timeCount = 0;
    }

    TimerObserverManager.addListener = function(listener, period) {
        listeners.push(listener);
        periods.push(period * 1000);
        if (!timer) {
            setTimer();
        }
    };

    TimerObserverManager.removeListener = function(listener) {
        var pos = listeners.indexOf(listener);
        if (pos != -1) {
            listeners.splice(pos, 1);
            periods.splice(pos, 1);
        }
    };

})();

/**
 * Вызов функции по таймеру.
 * @class
 * @constructor
 * @param {Function} listener callback-функция
 * @param {Number} period периодичность запуска функции (в секундах)
 * @param {Boolean} add начать слушать немедленно
 * @param {Object} context контекст выполнения функции listener
 *
 * @example
 * function callback(observer) {
 *     // ...
 *     // останавливаем таймер после 10 запусков
 *     if (observer.tick == 10) {
 *         observer.remove();
 *     }
 * }
 * new y5.TimerObserver(callback, true);
 */
y5.TimerObserver = function(listener, period, add, context) {
    this.period = period;
    this.added = false;
    this.tick = 1;

    var _this = this;
    this.listener = function() {
        execListenerWithContext(listener, context, _this);
        _this.tick++;
    };

    if (add) {
        this.add();
    }
};

y5.TimerObserver.prototype = {
    /**
     * Включает наблюдатель таймера.
     */
    add: function() {
        if (!this.added) {
            TimerObserverManager.addListener(this.listener, this.period);
            this.added = true;
        }
    },

    /**
     * Выключает наблюдатель таймера.
     * @param {Boolean} [reset] сбрасывает счетчик тиков
     */
    remove: function(reset) {
        if (this.added) {
            TimerObserverManager.removeListener(this.listener);
            if (reset) {
                this.tick = 1;
            }
            this.added = false;
        }
    }
};


/**
 * Функции для создания слушателей событий, изменения свойств и создания событий.
 * @class
 * @static
 */
y5.Events = {
    /**
     * Создает один или несколько объектов слушателей (y5.AEventListener).
     * @param {String | Array} type тип события: click, load...
     * @param {Function} listener callback-функция на возникновение события
     * @param {Element | Object} element объект на котором возникает событие
     * @param {Boolean} [add] начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] контекст выполнения функции listener
     * @return {y5.AEventListener | Array of y5.AEventListener}
     *
     * @example
     * y5.Events.observe("click", callback, element, true);
     */
    observe: function(type, listener, element, add, context) {
        if (!element) {
            return {add: y5.NULL, remove: y5.NULL};
        }

        switch (Types.type(type)) {
            case Types.ARRAY:
                var length = type.length,
                    events = new Array(length);

                for (var i = 0; i < length; i++) {
                    events[i] = new AEventListener(type[i], listener, element, add, context);
                }

                return events;

            case Types.STRING:
                return new AEventListener(type, listener, element, add, context);
        }

        return null;
    },

    /**
     * Слушатель изменения свойств элемента.
     * @param {String} property свойство
     * @param {Function} listener callback-функция
     * @param {Element} element элемент
     * @param {Boolean} [add] начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] контекст выполнения функции listener
     * @return {y5.AEventListener}
     *
     * @example
     * y5.Events.observeProperty("value", callback, element, true);
     */
    observeProperty: function(property, listener, element, add, context) {
        // Внимание! Не используем контекст для listenerAttr,
        // только для listener
        function listenerAttr(evt) {
            if (evt.attrName == property) {
                execListenerWithContext(listener, context, evt, element);
            }
        }
        return new AEventListener(typeDOMAttrModified, listenerAttr, element, add);
    },

    /**
     * Слушатель изменения элемента ввода.
     * @param {Function} listener callback-функция
     * @param {Element} element элемент
     * @param {Boolean} [add] начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] контекст выполнения функции listener
     * @return {y5.InputObserver}
     *
     * @example
     * y5.Events.observeInput("value", callback, element, true);
     */
    observeInput: function(listener, element, add, context) {
        return new y5.InputObserver(listener, element, add, context);
    },

    /**
     * Вызов функции по таймеру.
     * @param {Function} listener callback-функция
     * @param {Number} period периодичность запуска функции (в секундах)
     * @param {Boolean} [add] начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] контекст выполнения функции listener
     * @return {y5.TimerObserver}
     *
     * @example
     * function callback(observer) {
     *     // ...
     *     // останавливаем таймер после 10 запусков
     *     if (observer.tick == 10) {
     *         observer.remove();
     *     }
     * }
     * y5.Events.observeTimer(callback, 1, true);
     */
    observeTimer: function(listener, period, add, context) {
        return new y5.TimerObserver(listener, period, add, context);
    },

    /**
     * Создает событие на элементе.
     * @param {String} [type] тип события: click, mousedown... (если не указан, то используется click)
     * @param {Object} [element] целевой элемент события (если не указан, то используется document)
     * @param {Boolean} [dispatch] отправить событие (если не указано, то событие возникает сразу)
     * @param {Object} [event] параметры события
     * @return {y5.Event}
     *
     * @example
     * // создать событие "mousedown" на элементе
     * y5.Events.make("mousedown", element);
     *
     * // создать событие "click" на документе
     * y5.Events.make();
     *
     * // создать событие "click" на элементе
     * var evt = y5.Events.make("mousedown", element, false);
     * // ...
     * // выполнить его позже
     * // evt.dispatch()
     */
    make: function(type, element, dispatch, event) {
        return new y5.Event(type, element, dispatch, event);
    }
};

var Events = y5.Events;

// aliases

/**
 * @name observe
 * @link y5.Events.observe
 * @function
 * @deprecated
 */
Events.create = Events.observe;

/**
 * @name PropertyListener
 * @link y5.Events.observeProperty
 * @function
 * @deprecated
 */
Events.PropertyListener = Events.observeProperty;

// init cleanups
// В старых gecko-браузерах вызывает крах
if (y5.is_gecko && y5.gecko_ver < 1.8) {
    EventListenerCleaner = false;
} else {
    // Register the cleanup handler on page unload.
    new AEventListener('unload', EventListenerCleaner.cleanup, window, true, EventListenerCleaner);
}

y5.loaded('Events');

});/**
 * This object allows you to create soft links between objects (DOM nodes or JavaScript objects).
 * Пусть нам нужно на один инпут повесить два компонента: автокомплит и валидацию.
 *
 * &lt;div class="y5-c-Components-AutoComplete Friends-c-Validator"&gt;
 *     &lt;input type="text" /&gt;
 * &lt;/div&gt;
 *
 * Оба компонента при инициализации (createFromTag) получат DIV в переменной element.
 * Если автокомплит изменил значение и об этом следует сообщить валидатору
 * в коде автокомплита следует написать: y5.CallBacks.dispatch('change', element, event);
 * Где event - это объект с данными, которые можно передать тому, кто слушает это событие.
 *
 * В коде валидатора:
 * var listener = y5.CallBacks.add('change', funtion(event){...}, element);
 * Listener обладает методами remove и add, что позволяет динамически прекращать слушать событие.
 *
 * Функция из второго параметра выполнится при вызове dispatch.
 * @alias y5.CallBacks
 */

y5.CallBacks = new function () {
    this.srcload = false;
    this.listeners = {};
    this.eventHistory = {};

    /**
     * Returns object ID.
     * @alias y5.CallBacks.getId
     * @param {Object} DOM node or string "*"
     * @return {String} DOM node ID or "*"
     * @private
     */
    this.getId = function (object) {
        if (typeof(object) == 'string') return object;
        return y5.Utils.getUniqueId(object);
    };

    /**
     * Allows to listen for object events.
     * @alias y5.CallBacks.add
     * @param {String} Event type.
     * @param {Function} Event listener.
     * @param {Object} Which object to listen.
     * @param {Boolean} Starts the listener immediately (default is "true").
     * @param {Object} This object will be in "this" variable for listener (optional).
     * @return {Object} Listener y5.CallBacks.Listener.
     */
     this.add = function (type, listener, object, execute, context) {
        object = object || this;
        var id = this.getId(object);

        if (id == this.uniqueID && type == 'srcload' && this.srcload) {
            this.execListener(listener, context);
        }

        var key = id + type;
        if (typeof(this.listeners[key]) == y5.UNDEF) {
            this.listeners[key] = [];
        }
        var listeners = this.listeners[key];
        return (listeners[listeners.length] = new y5.CallBacks.Listener(listener, execute, key, context));
    };

    /**
     * Allows to listen for object events and execute the listener if event was dispatched before.
     * @alias y5.CallBacks.addWithIncludingHistory
     * @param {String} Event type.
     * @param {Function} Event listener.
     * @param {Object} Which object to listen.
     * @param {Boolean} Starts the listener immediately (default is "true").
     * @param {Object} This object will be in "this" variable for listener (optional).
     * @return {Object} Listener y5.CallBacks.Listener.
     */
     this.addWithIncludingHistory = function (type, listener, object, execute, context) {
        var newListener = this.add(type, listener, object, execute, context);
        newListener.includingHistory = true;
        if (execute){
            newListener.start();
        }
        return newListener;
    };

    /**
     * Allows to dispatch object event.
     * @alias y5.CallBacks.dispatch
     * @param {String} Event.
     * @param {Object} Object which dispatches an event.
     * @param {Object} Event object (can be used for passing data to listeners).
     */

    this.dispatch = function (type, object, event) {
        y5.Console.group('Type: ', type + ', object: ', object, this.execListener.tags.concat(type));

        var id = this.getId(object || this);

        if (id == this.uniqueID && type == 'srcload' && !this.srcload && !this.srcLoad()) {
            return;
        }

        var result = true;

        result &= this.execListeners(id, type, event);
        result &= this.execListeners('*', type, event);
        result &= this.execListeners(id, '*', event);
        result &= this.execListeners('*', '*', event);

        y5.Console.groupEnd();

        // add event to history
        var key = id + type;
        if (typeof(this.eventHistory[key]) == y5.UNDEF){
            this.eventHistory[key] = [];
        }
        this.eventHistory[key].push(event);

        return result;
    }

    /**
     * Runs list of listeners by key.
     * @alias y5.CallBacks.execListeners
     * @param {String} Object ID (from y5.CallBacks.getId method).
     * @param {String} Event type.
     * @param {Object} Event object (can be used for passing data to listeners).
     * @private
     */

    this.execListeners = function (id, type, event) {
        var result = true;
        var key = id + type;
        var listeners = this.listeners[key];
        if (typeof(listeners) == y5.UNDEF) {
            return result;
        }

        // сделаем локальную копию массива
        var listenersCopy = listeners.slice(0);
        for (var i = 0, l = listenersCopy.length; i < l; i++) {
            var listener = listenersCopy[i];
            // у тех, что не надо исполнять execute == false; у тех, что уже удалены execute == null
            if (listener.execute) {
                result &= this.execListener(listener.listener, listener.context, event, id, type);
            }
        }

        return result;
    };

    /**
     * Runs listener.
     * @alias y5.CallBacks.execListener
     * @param {Function} Listener.
     * @param {Object} This object will be in "this" variable for listener.
     * @param {Object} Event object (can be used for passing data to listeners).
     * @private
     */
    this.execListener = function (listener, context, event, id, type) {
        if (typeof(context) == 'object') {
            y5.Console.log(context, this.execListener.tags);
            return listener.call(context, event);
        } else {
            y5.Console.log('Listener: ', listener, this.execListener.tags.concat(type));
            y5.Console.log('Event: ', event, this.execListener.tags.concat(type));
            return listener(event);
        }
    };
    this.execListener.tags = ['y5', 'CallBacks', 'Dispatch'];

    /**
     * Sends event history to listener
     * @alias y5.CallBacks.dispatchEventHistoryForListener
     * @param {Object} y5.CallBacks.Listener
     */
    this.dispatchEventHistoryForListener = function (listener) {
        var events = this.eventHistory[listener.key];
        if (!events) {
            return;
        }
        for (var i = 0, l = events.length; i < l; i++) {
            if (listener.execute) {
                this.execListener(listener.listener, listener.context, events[i]);
            }
        }
    }

    /**
     * Dispatches event on object from object.parentNode property.
     * @alias y5.CallBacks.dispatchParent
     * @param {String} Event type.
     * @param {Object} Object which is the parent for event dispatcher.
     * @param {Object} Event object (can be used for passing data to listeners).
     * @private
     */
    this.dispatchParent = function (type, object, event) {
        if (typeof(object.parentNode) != y5.UNDEF) {
            this.dispatch(type, object.parentNode, event);
        }
    };

    /**
     * Removes listener.
     * @alias y5.CallBacks.remove
     * @param {Object} Listener.
     */
    this.remove = function (listener) {
        var listenersByKey = this.listeners[listener.key];
        var i = listenersByKey.indexOf(listener);
        if (i != -1) {
            listenersByKey.splice(i, 1);
        }
        listener = listener.context = listener.listener = listener.execute = null;
    };

    /**
     * Code for IE. Defers dispatching of the "scrload" event until body node closes in IE.
     * For all browsers create y5.CallBacks.Destroyer object.
     * @alias y5.CallBacks.srcLoad
     * @private
     */
    this.srcLoad = function () {
        var _this = this;
        if (!y5.is_ie || document.readyState == 'interactive' || document.readyState == 'complete') {
            return srcLoaded();
        }

        function srcLoaded () {
            new y5.CallBacks.Destroyer();
            return _this.srcload = true;
        }

        function srcLoad () {
            if (document.readyState == 'interactive' || document.readyState == 'complete') {
                srcLoaded();
                _this.dispatch('srcload');
            }
        }
        document.onreadystatechange = srcLoad;
        return false;
    };
};

/**
 * Destroyer object. Sends the "unload" event when user wants to leave the page.
 * Objects can listen to this event and clean up themselves to prevent memory leaks.
 * To listen to this event, use the code below.
 *
 * function cleaner(){
 *     // some code for cleaning object links and destroying objects.
 * }
 * y5.CallBacks.add('unload', cleaner);
 * @alias y5.CallBacks.Destroyer
 */
y5.CallBacks.Destroyer = function () {
    y5.Events.create('unload', destroy, window, true);

    function destroy () {
        y5.CallBacks.dispatch('unload');

        // clear event history
        for (var key in this.eventHistory) {
            this.eventHistory[key].splice(0, this.eventHistory[key].length);
            delete this.eventHistory[key];
        }
        this.eventHistory = null;
    }
};

/**
 * Listener object. When you add a function listener you will receive this object.
 * This object allows you to manipulate the function listener.
 * @alias y5.CallBacks.Listener
 */
y5.CallBacks.Listener = function (listener, execute, key, context) {
    this.listener = listener;
    this.context = context;
    this.key = key;
    this.execute = (typeof(execute) != y5.UNDEF ? execute : true);
    this.includingHistory = 0;
};
y5.CallBacks.Listener.prototype = {
    /**
     * Starts listen for event.
     * @alias y5.CallBacks.Listener.add
     */
    add: function () {
        this.execute = true;
        // если указано - учитывать историю
        if (this.includingHistory) {
            // выполняем историю для этого обработчика
            y5.CallBacks.dispatchEventHistoryForListener(this);
            // сбрасываем флаг учета истории
            this.includingHistory = false;
        }
    },

    /**
     * Stops listen for event.
     * @alias y5.CallBacks.Listener.remove
     */
    remove: function () {
        this.execute = false;
    }
};

/**
 * Start listen for event.
 * @alias y5.CallBacks.Listener.start
 * @deprecated
 */
y5.CallBacks.Listener.prototype.start = y5.CallBacks.Listener.prototype.add;

/**
 * Stop listen for event.
 * @alias y5.CallBacks.Listener.stop
 * @deprecated
 */
y5.CallBacks.Listener.prototype.stop = y5.CallBacks.Listener.prototype.remove;

y5.require(['Utils', 'Events'], function (){ y5.loaded('CallBacks') });
/**
 * Get, set and delete cookies.
 * @alias y5.Cookies
 */
y5.Cookies = new function () {

    /**
     * Set cookie.
     * @alias y5.Cookies.setCookie
     * @param {String} Name.
     * @param {String} Value.
     * @param {Date} Expire (hours).
     * @param {String} Domain (default - current domain).
     * @param {String} Path (default /).
     */
    this.setCookie = function (name, value, time, domain, path) {
        // set cookie if value not null & not empty
        if (value !== null && value !== '') {
            this.setHeader(name, value, time, domain, path);
        }
    };

    /**
     * Get cookie value.
     * @alias y5.Cookies.getCookie
     * @param {String} Name.
     * @return {String} Value.
     */
    this.getCookie = function (name) {
        var res = document.cookie.match(new RegExp(name + '=([^;]*)'));
        return (res && res[1] ? decodeURIComponent(res[1]) : null);
    };

    /**
     * Delete cookie.
     * @alias y5.Cookies.delCookie
     * @param {String} Name.
     * @param {String} Domain (default - current domain).
     */
    this.delCookie = function (name, domain, path) {
        // set expire time to one year later
        this.setHeader(name, '', -(24 * 365), domain, path);
    };

    /**
     * Get header for cookie set.
     * @alias y5.Cookies.getHeader
     * @private
     */
    this.getHeader = function(name, value, time, domain, path) {
        var kukki = new Array();

        kukki.push(name + '=' + encodeURIComponent(value));

        // set expire time if defined
        if (typeof(time) == 'number') {
            var expire = new Date();

            // set expire time
            expire.setTime(expire.getTime() + (time * 3600000));
            kukki.push('expires=' + expire.toGMTString());
        }

        // domain
        kukki.push('domain=' + (domain || window.location.hostname));

        // path
        kukki.push('path=' + (path || '/'));

        return kukki.join(';');
    };

    this.setHeader = function(name, value, time, domain, path) {
        // save cookie
        document.cookie = this.getHeader(name, value, time, domain, path);
    };
};

y5.loaded('Cookies');/**
 * Base object for component creation.
 * Looks for DOM nodes "div", "code" and "form" that have "{ProjectName}-c-{ComponentName}" in their class name and initializes suitable components.
 * Подключать файлы, хранящие собственно функциональность компонента, можно автоматически
 * (по наличии html-кода компонента на странице).
 * Сбор компонентов и подключение нужных файлов начинается при срабатывании события srcload.<br/>
 * <strong>Пример</strong>
 * &lt;html&gt;
 *     ...
 *     &lt;script src="http://img.yandex.net/js/ver/y5.js"&gt;&lt;/script&gt; &lt;!-- подключим ядро --&gt;
 *     ...
 *     &lt;body&gt;
 *     ...
 *     &lt;div class="y5-c-MyComponent"&gt;...&lt;/div&gt;  &lt;!-- описываем html-часть компонента --&gt;
 *     ...
 *     &lt;script&gt;
 *         function onload(){
 *             y5.CallBacks.dispatch('srcload'); // инициализируем событие srcload
 *         }
 *         // грузим модули CallBacks и Components с обработчиком загрузки onload
 *         y5.require(['CallBacks', 'Components'], onload);
 *     &lt;/script&gt;
 *     &lt;!--Важно: инициализация события srcload должна произойти в самом конце html-страницы, перед закрытием body. --&gt;
 *     &lt;/body&gt;
 * &lt;/html&gt;
 * @alias y5.Components
 */

y5.Components = {
    className: '-c-',
    classNameRegex: /\w+-c-[\w\-]+/,
    getClassNameRegex: /\w+-c-[\w\-]+/g,
    tagName: ['code', 'div', 'form'],

    /**
     * Looks for DOM nodes "div", "code" and "form" that have "{ProjectName}-c-{ComponentName}" in their class name and initializes suitable components.
     * @alias y5.Components.init
     * @param {Object} DOM node. Look for components in the children of this DOM node.
     */
    init: function(element) {
        element = element || y5.Dom.getBody();
        var elements = [];
        if (y5.Classes.test(element, this.classNameRegex)) {
            elements.push(element);
        }
        for (var i = 0, l = this.tagName.length; i < l; i++) {
           elements = elements.concat(y5.Dom.getElementsByTagNameAndClass(this.tagName[i], this.classNameRegex, element));
        }
        this.createComponents(elements);
    },

    /**
     * Creates components from DOM node by class name.
     * @alias y5.Components.createComponents
     * @param {Object} DOM node.
     */
    createComponents: function(components) {
        for (var i = 0, l = components.length; i < l; i++) {
            this.prepareComponent(components[i]);
        }
    },

    checkPrepare: function(component, location) {
        var key = y5.Utils.getUniqueId(component) + '-' + location;
        if (this.cache.empty(key)) {
            this.cache.set(key, true);
            return false;
        }
        return true;
    },

    prepareComponent: function(component) {
        if (!component) {
            return;
        }

        var params = this.getParams(component);
        var locations = this.getLocations(component.className, this.className);

        for (var i = 0, l = locations.length; i < l; i++) {
            var location = locations[i];
            if (!this.checkPrepare(component, location)) {
                this.createComponent(location, component, params);
            }
        }
    },

    /**
     * Creates component from DOM node by class name.
     * @alias y5.Components.createComponent
     * @param {String} Location.
     * @param {Object} DOM node.
     * @param {Object} Params for initialization.
     */
    createComponent: function(location, component, params) {
        var _this = this;
        function createComponent() {
            var Component = _this.getComponent(location);
            if (Component == null) {
                return;
            }
            function createComponent() {
                if (!Component.createFromTag) {
                    new Component(component, params)
                } else {
                    Component.createFromTag(component, params);
                }
                y5.Console.log('Call "createFromTag" in %s', location, ['y5', 'Components']);
                y5.Components.Watcher.decrement();
            }
            window.setTimeout(createComponent, 0);
        }

        y5.Console.log('Start init %s', location, ['y5', 'Components']);
        y5.Components.Watcher.increment();
        y5.require(location, createComponent);
    },

    /**
     * Returns component location from class name.
     * "y5-c-Components-ComponentName" converts to "{y5}.Components.ComponentName"
     * "-c-" is separator between alias and path.
     * @alias y5.Components.getLocation
     * @param {String} Class name.
     * @param {String} Separator between alias and path.
     */
    getLocation: function(string, separator) {
        var index = string.indexOf(this.className);
        var alias = string.substr(0, index);
        var path = string.substr(index + 3);
        return '{' + alias + '}\.' + path.replace(/-/g, '.');
    },

    /**
     * Returns component location from class name.
     * "y5-c-Components-ComponentName1 y5-c-Components-ComponentName2"
     * converts to "[{y5}.Components.ComponentName1, {y5}.Components.ComponentName2]"
     * "-c-" is separator between alias and path.
     * @alias y5.Components.getLocations
     * @param {String} Class name.
     * @param {String} Separator between alias and path.
     */
    getLocations: function (string, separator) {
        var locations = this.getClassNames(string, separator);
        var l = locations.length;
        var result = new Array(l);
        for (var i = 0; i < l;  i++) {
            result[i] = this.getLocation(locations[i], separator);
        }
        return result;
    },

    /**
     * Returns component class names like "y5-c-Components-ComponentName".
     * @alias y5.Components.getClassNames
     * @param {String} Class name.
     * @param {String} Separator between alias and path.
     * @return {Array} Array of class names.
     */
    getClassNames: function(string, separator) {
        return string.match(this.getClassNameRegex);
    },

    /**
     * Returns component name from location omitting namespace (alias).
     * "{y5}.Components.Component" converts to "Components.Component"
     * @alias y5.Components.getComponentName
     * @param {String} Location.
     */
    getComponentName: function(location) {
        return location.replace('{' + this.getNameSpace(location) + '}.', '');
    },

    /**
     * Returns component namespace from (alias) location.
     * "{y5}.Components.Component" converts to "y5"
     * @alias y5.Components.getNameSpace
     * @param {String} Location.
     * @return {String} Alias.
     */
    getNameSpace: function (location) {
        return y5.getAlias(location);
    },

    /**
     * Returns component by location or null.
     * "{y5}.Components.Component" return object "window.y5.Components.Component"
     * @alias y5.Components.getComponent
     * @param {String} Location.
     * @return {Object} Object.
     */
    getComponent: function (location) {
        if (!location) {
            return null;
        }
        var nameSpace = this.getNameSpace(location);
        var name = this.getComponentName(location).split('.');
        var Component = window[nameSpace];
        if (typeof(Component) == y5.UNDEF) {
            y5.Console.error('No such namespace"' + nameSpace + '"', 'getComponent', 'Componemts', ['y5', 'Components']);
            return null;
        }
        var path = nameSpace;
        for (var i = 0, l = name.length; i < l; i++) {
            path += '.' + name[i];
            Component = Component[name[i]];
            if (typeof(Component) == y5.UNDEF) {
                y5.Console.error('No such compoment"' + path + '"', 'getComponent', 'Componemts', ['y5', 'Components']);
                return null;
            }
        }
        return Component;
    },

    /**
     * Returns component name omitting prefix from class name.
     * "Prefix-Name" converts to "Name"
     * "Prefix-Long-Name" converts to "Long.Name"
     * @alias y5.Components.getName
     * @param {String} Class name.
     * @param {String} Prefix.
     */
    getName: function (className, prefix) {
        return className.match(new RegExp(prefix + '([\\w-]+)', ''))[1].replace(/-/ig, '.');
    },

    /**
     * Returns object after word "return" in attribute "onclick".
     * Attribute "onclick" will be removed during execution.
     * onclick="return {foo: 'bar'}" returns "{foo: 'bar'}"
     * @alias y5.Components.getParams
     * @param {Object} DOM node.
     */
    getParams: function (object) {
        try {
            var params = object.onclick ? object.onclick() : {};
        } catch (e) {
            //throw new y5.Exception('Error getting params "' + object.onclick + '"', 'components', 'getParams');
        }
        object.removeAttribute('onclick');
        object.onclick = {};
        return params;
    }
};

y5.Components.Watcher = {
    counter: 0,

    increment: function() {
        this.counter++;
    },

    decrement: function() {
        if (--this.counter == 0) {
            y5.CallBacks.dispatch('allComponentsCreated', y5.Components);
        }
    }
};

y5.require(['Utils', 'Cache', 'Classes', 'Dom', 'CallBacks'],
    function() {
        y5.Components.cache = new y5.Cache();

        y5.loaded('Components');
        y5.CallBacks.add('srcload', function () { y5.Components.init() });
    }
);y5.require(['Dom', 'Events'], function() {

/**
 * Слушатель клавиатурного сокращения.
 * @class
 * @private
 *
 * @param {Number} masks маски клавиатурного сочетания
 * @param {Function} callback функция обработки нажатия клавиш
 * @param {Node} node целевой элемент
 * @param {Object} options параметры
 *
 * @private
 */
var ShortCutListener = function(masks, callback, node, options) {
    this.masks = masks;
    this.node = node;

    if (options.context) {
        this.callback = function(e, opts) {
            callback.apply(options.context, [e, opts]);
        }
    } else {
        this.callback = callback;
    }
    this.options = options;

    this.add();
};

ShortCutListener.prototype = {
    /// Public

    /**
     * Добавляет сокращение в список активных.
     */
    add: function() {
        this.enable(true);
    },

    /**
     * Удаляет сокращение из списка активных.
     */
    remove: function() {
        this.enable(false);
    },

    /**
     * Проверяет активность сокращения.
     */
    isEnable: function() {
        return this.enabled;
    },

    /**
     * Включает/выключает состояние активности.
     * @param {Boolean} enabled
     */
    enable: function(enabled) {
        this.enabled = enabled;
    },

    /// Private

    /**
     * Проверяет сокращение и вызывает callback, если нажато сокращение.
     * @param {Number} mask маска клавиатурного сочетания
     * @param {Boolean} isInput элемент ввода?
     * @param {Node} node элемент на котором произошло событие
     * @param {Event} event событие ввода
     * @private
     */
    check: function(mask, isInput, node, e) {
        if (!this.enabled) {
            return false;
        }

        // выходим, если сокращение запрещено на элементах ввода
        if (this.options.checkTarget && isInput) {
            return false;
        }

        // попали в маску?
        if (!this.checkMask(mask)) {
            return false;
        }

        // проверяем, если элемент на котором произошло событие
        // является дочерним по отношению к целевому элементу
        if (y5.Dom.isChild(node, this.node)) {
            if (this.options.preventDefault) {
                e.preventDefault();
            }

            this.callback(e, this.options);
            return true;
        }

        return false;
    },

    /**
     * Проверяет маску по списку масок объекта.
     * @param {Number} mask маска клавиатурного сочетания
     * @param {Boolean} результат проверки
     * @private
     */
    checkMask: function(mask) {
        for (var i = 0, l = this.masks.length; i < l; i++) {
            var m = this.masks[i];
            if (m == mask || m == 0) {
                return true;
            }
        }
        return false;
    },

    /**
     * Очищает содержимое этого объекта.
     */
    cleanup: function() {
        this.remove();
        this.node = null;
        this.options = null;
        this.callback = null;
    }
};

/// ShortCut

var maskUnicode = 1 << 16,
    maskCode = maskUnicode - 1,
    maskCtrl = maskUnicode,
    maskAlt = maskCtrl << 1,
    maskShift = maskAlt << 1,
    typeDown = 1,
    typePress = 2,
    default_options = {
        checkTarget: true,
        preventDefault: true,
        context: null,
        once: false
    },
    listDown = [],
    listPress = [];

function setDefaultOption(options) {
    for (var option in default_options) {
        if (typeof options[option] === y5.UNDEF) {
            options[option] = default_options[option];
        }
    }

    return options;
}

function cleanUpShortCut(shortcut) {
    shortcut.cleanup();
    delete shortcut;
}

function removeFromList(element, list) {
    var index = list.lastIndexOf(element);
    if (index !== -1) {
        cleanUpShortCut(list[index]);
        list = list.splice(index, 1);
        return true;
    }
    return false;
}

function getMask(set, type) {
    var code = 0, mask;

    if (set.key) {
        code = set.key;
    } else if (set.ch) {
        switch (type) {
            case typeDown:
                code = set.ch.toUpperCase();
                break;

            case typePress:
                code = set.ch.toLowerCase();
                break;
        }
        code = code.charCodeAt(0);
    }

    mask = maskCode & code;

    if (set.ctrl) {
        mask ^= maskCtrl;
    }

    if (set.alt) {
        mask ^= maskAlt;
    }

    if (set.shift) {
        mask ^= maskShift;
    }

    return mask;
}

function getMasks(keys, type) {
    var len = keys.length,
        masks = new Array(len);

    for (var i = 0; i < len; i++) {
        masks[i] = getMask(keys[i], type);
    }

    return masks;
}

function getSet(key, e) {
    return {key: key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey}
}

function getMaskEvent(e, type) {
    switch (type) {
        case typeDown:
            return getMask(getSet(e.keyCode, e));

        case typePress:
            var key = e.charCode ? e.charCode : e.keyCode;
            return getMask(getSet(key, e));
    }
    return 0;
}

function isInputTarget(elem) {
    if (!elem.tagName) {
        return false;
    }

    switch (elem.tagName.toLowerCase()) {
        case 'input': {
            switch (elem.type) {
                case 'text': case 'password': case 'file': case 'search':
                    return true;
            }
            break;
        }
        case 'textarea': {
            return true;
        }
    }

    return false;
}

function keyListener(e, type, list) {
    var i, l,
        node = e.target,
        mask = getMaskEvent(e, type),
        isInput = isInputTarget(node),
        tmpList = [],
        res = false;

    for (i = 0, l = list.length; i < l; i++) {
        if (list[i].isEnable()) {
            tmpList.push(list[i]);
        }
    }

    for (i = 0, l = tmpList.length; i < l; i++) {
        var shortcut = tmpList[i];
        var check = shortcut.check(mask, isInput, node, e);

        if (check && shortcut.options.once) {
            removeFromList(shortcut, list);
        }

        res = check || res;
    }

    return res;
}

function keyDownListener(e) {
    return keyListener(e, typeDown, listDown);
}

function keyPressListener(e) {
    // блокируем сочетания, на которые могут быть повешены браузерные сочетания
    if (!(e.ctrlKey || e.altKey)) {
        e.stopPropagation();
    }

    return keyListener(e, typePress, listPress);
}

function action(type, list, keys, listener, node, options) {
    switch (typeof options) {
        case 'object':
            break;

        case 'boolean':
            options = {checkTarget: options};
            break;

        case 'undefined':
            options = {};
            break;
    }

    options = setDefaultOption(options);

    return (list[list.length] = new ShortCutListener(getMasks(keys, type), listener, node || document, options));
}

/**
 * Функции для обработки клавиатурных сокращений
 * @class
 * @static
 *
 * @example
 * Предопределенные константы клавиш:
 *
 *   F1  BACKSPACE END          PLUS_NUM
 *   F2  TAB       HOME         MINUS
 *   F3  ENTER     LEFT_ARROW   MINUS_NUM
 *   F4  SHIFT     UP_ARROW     NUM_1
 *   F5  CTRL      RIGHT_ARROW  NUM_LOCK
 *   F6  ALT       DOWN_ARROW   SCROLL_LOCK
 *   F7  PAUSE     INSERT       SLASH
 *   F8  CAPS_LOCK DELETE       ASTERISK
 *   F9  ESC       PLUS         BS
 *   F10 SPACE     LEFT_WINDOW
 *   F11 PAGE_UP   RIGHT_WINDOW
 *   F12 PAGE_DOWN SELECT
 *
 * Вызов функций:
 * y5.ShortCut.down([Key, Key], Callback, Node, Options);
 *
 * Пример вызова:
 * y5.ShortCut.down([{ch: 'a', ctrl: true}], function() { alert('Ctrl+A'); }, document);
 * y5.ShortCut.down([{key: y5.ShortCut.ESC}], function() { alert('Esc'); }, document);
 *
 * Объект Key:
 * {
 *    key: code,              // числовой код клавиши
 *    ch: char,               // символ клавиши, например 'A'
 *    ctrl: (true | false),   // зажата клавиша Ctrl
 *    alt: (true | false),    // зажата клавиша Alt
 *    shift: (true | false),  // зажата клавиша Shift
 * }
 *
 * Если указано свойство key, то свойство ch игнорируется.
 * Свойства ctrl, alt, shift являются необязательными.
 *
 * Объект Options:
 * {
 *    checkTarget: (true | false),     // проверять целевой элемент (если true (по умолчанию),
 *                                     // то не вызывать Callback на полях ввода)
 *    preventDefault: (true | false),  // запрещать выполнять действие по умолчанию, которое
 *                                     // установлено для документа на нажатие клавиши (true по умолчанию)
 *    context: (null | context),       // контекст в котором будет выполнена функция Callback
 *                                     // (контекста по умолчанию нет)
 *    once: (false | true)             // обработать сочетание один раз, затем удалить (false по умолчанию)
 * }
 */
y5.ShortCut = {
    BS: 8,
    BACKSPACE: 8,
    TAB: 9,
    ENTER: 13,
    SHIFT: 16,
    CTRL: 17,
    ALT: 18,
    PAUSE: 19,
    CAPS_LOCK: 20,
    ESC: 27,
    SPACE: 32,
    PAGE_UP: 33,
    PAGE_DOWN: 34,
    END: 35,
    HOME: 36,
    LEFT_ARROW: 37,
    UP_ARROW: 38,
    RIGHT_ARROW: 39,
    DOWN_ARROW: 40,
    INSERT: 45,
    DELETE: 46,
    LEFT_WINDOW: 91,
    RIGHT_WINDOW: 92,
    SELECT: 93,
    PLUS: y5.is_ie ? 187 : 61,
    PLUS_NUM: 107,
    MINUS: y5.is_ie ? 189 : 109,
    MINUS_NUM: 109,
    NUM_1: 49,
    F1: 112,
    F2: 113,
    F3: 114,
    F4: 115,
    F5: 116,
    F6: 117,
    F7: 118,
    F8: 119,
    F9: 120,
    F10: 121,
    F11: 122,
    F12: 123,
    NUM_LOCK: 144,
    SCROLL_LOCK: 145,
    SLASH: 191,
    ASTERISK: 106,

    /**
     * Устанавливает сокращение на нажатие клавиши.
     *
     * @param {Array of Keys} keys список сочетаний клавиш
     * @param {Function} callback функция обработки нажатия клавиш
     * @param {Node} node целевой элемент
     * @param {Object} options параметры
     * @return {Object} экземпляр класса ShortCutListener
     *
     * @example
     * y5.ShortCut.down([{key: y5.ShortCut.ESC}], function() { alert('Esc'); }, document);
     */
    down: function(keys, callback, node, options) {
        return action(typeDown, listDown, keys, callback, node, options);
    },

    /**
     * Устанавливает сокращение на повтор нажатия клавиши.
     *
     * @param {Array of Keys} keys список сочетаний клавиш
     * @param {Function} callback функция обработки нажатия клавиш
     * @param {Node} node целевой элемент
     * @param {Object} options параметры
     * @return {Object} экземпляр класса ShortCutListener
     *
     * @example
     * y5.ShortCut.press([{key: y5.ShortCut.ESC}], function() { alert('Esc'); }, document);
     */
    press: function(keys, callback, node, options) {
        return action(typePress, listPress, keys, callback, node, options);
    },

    /**
     * Удаляет сокращение.
     * @param {Object} shortcut
     * @return {Boolean}
     */
    remove: function(shortcut) {
        return this.removeDown(shortcut) || this.removePress(shortcut);
    },

    /**
     * Удаляет сокращение из списка обработки нажатий.
     * @param {Object} shortcut
     * @return {Boolean}
     */
    removeDown: function(shortcut) {
        return removeFromList(shortcut, listDown);
    },

    /**
     * Удаляет сокращение из списка обработки повтора нажатий.
     * @param {Object} shortcut
     * @return {Boolean}
     */
    removePress: function(shortcut) {
        return removeFromList(shortcut, listPress);
    }
};

var ShortCut = y5.ShortCut;
var AEventListener = y5.AEventListener;

/// Init

if (y5.is_ie) {
    function keyPressListenerI(e) {
        if (!e.repeat) {
            keyDownListener(e);
        }
        keyPressListener(e);
    }
    new AEventListener('keydown', keyPressListenerI, document, true);
} else {
    new AEventListener('keypress', keyPressListener, document, true);
    new AEventListener('keydown', keyDownListener, document, true);
}

function cleanUpShortCuts() {
    listDown.forEach(cleanUpShortCut);
    listDown = null;

    listPress.forEach(cleanUpShortCut);
    listPress = null;
}

new AEventListener('unload', cleanUpShortCuts, window, true);

y5.loaded('ShortCuts');

});/*
 * Ext JS Library 1.1
 * Copyright(c) 2006-2007, Ext JS, LLC.
 * licensing@extjs.com
 *
 * http://www.extjs.com/license
 */
var Ext = {};
Ext.DomQuery = function(){
    var cache = {}, simpleCache = {}, valueCache = {};
    var nonSpace = /\S/;
    var trimRe = /^\s+|\s+$/g;
    var tplRe = /\{(\d+)\}/g;
    var modeRe = /^(\s?[\/>+~]\s?|\s|$)/;
    var tagTokenRe = /^(#)?([\w-\*]+)/;
    var nthRe = /(\d*)n\+?(\d*)/, nthRe2 = /\D/;

    function child(p, index){
        var i = 0;
        var n = p.firstChild;
        while(n){
            if(n.nodeType == 1){
               if(++i == index){
                   return n;
               }
            }
            n = n.nextSibling;
        }
        return null;
    };

    function next(n){
        while((n = n.nextSibling) && n.nodeType != 1);
        return n;
    };

    function prev(n){
        while((n = n.previousSibling) && n.nodeType != 1);
        return n;
    };

    function children(d){
        var n = d.firstChild, ni = -1;
        while(n){
            var nx = n.nextSibling;
            if(n.nodeType == 3 && !nonSpace.test(n.nodeValue)){
                d.removeChild(n);
            }else{
                n.nodeIndex = ++ni;
            }
            n = nx;
        }
        return this;
    };

    function byClassName(c, a, v){
        if(!v){
            return c;
        }
        var r = [], ri = -1, cn;
        for(var i = 0, ci; ci = c[i]; i++){
            if((' '+ci.className+' ').indexOf(v) != -1){
                r[++ri] = ci;
            }
        }
        return r;
    };

    function attrValue(n, attr){
        if(!n.tagName && typeof n.length != "undefined"){
            n = n[0];
        }
        if(!n){
            return null;
        }
        if(attr == "for"){
            return n.htmlFor;
        }
        if(attr == "class" || attr == "className"){
            return n.className;
        }
        return n.getAttribute(attr) || n[attr];

    };

    function getNodes(ns, mode, tagName){
        var result = [], ri = -1, cs;
        if(!ns){
            return result;
        }
        tagName = tagName || "*";
        if(typeof ns.getElementsByTagName != "undefined"){
            ns = [ns];
        }
        if(!mode){
            for(var i = 0, ni; ni = ns[i]; i++){
                cs = ni.getElementsByTagName(tagName);
                for(var j = 0, ci; ci = cs[j]; j++){
                    result[++ri] = ci;
                }
            }
        }else if(mode == "/" || mode == ">"){
            var utag = tagName.toUpperCase();
            for(var i = 0, ni, cn; ni = ns[i]; i++){
                cn = ni.children || ni.childNodes;
                for(var j = 0, cj; cj = cn[j]; j++){
                    if(cj.nodeName == utag || cj.nodeName == tagName  || tagName == '*'){
                        result[++ri] = cj;
                    }
                }
            }
        }else if(mode == "+"){
            var utag = tagName.toUpperCase();
            for(var i = 0, n; n = ns[i]; i++){
                while((n = n.nextSibling) && n.nodeType != 1);
                if(n && (n.nodeName == utag || n.nodeName == tagName || tagName == '*')){
                    result[++ri] = n;
                }
            }
        }else if(mode == "~"){
            for(var i = 0, n; n = ns[i]; i++){
                while((n = n.nextSibling) && (n.nodeType != 1 || (tagName == '*' || n.tagName.toLowerCase()!=tagName)));
                if(n){
                    result[++ri] = n;
                }
            }
        }
        return result;
    };

    function concat(a, b){
        if(b.slice){
            return a.concat(b);
        }
        for(var i = 0, l = b.length; i < l; i++){
            a[a.length] = b[i];
        }
        return a;
    }

    function byTag(cs, tagName){
        if(cs.tagName || cs == document){
            cs = [cs];
        }
        if(!tagName){
            return cs;
        }
        var r = [], ri = -1;
        tagName = tagName.toLowerCase();
        for(var i = 0, ci; ci = cs[i]; i++){
            if(ci.nodeType == 1 && ci.tagName.toLowerCase()==tagName){
                r[++ri] = ci;
            }
        }
        return r;
    };

    function byId(cs, attr, id){
        if(cs.tagName || cs == document){
            cs = [cs];
        }
        if(!id){
            return cs;
        }
        var r = [], ri = -1;
        for(var i = 0,ci; ci = cs[i]; i++){
            if(ci && ci.id == id){
                r[++ri] = ci;
                return r;
            }
        }
        return r;
    };

    function byAttribute(cs, attr, value, op, custom){
        var r = [], ri = -1, st = custom=="{";
        var f = Ext.DomQuery.operators[op];
        for(var i = 0, ci; ci = cs[i]; i++){
            var a;
            if(st){
                a = Ext.DomQuery.getStyle(ci, attr);
            }
            else if(attr == "class" || attr == "className"){
                a = ci.className;
            }else if(attr == "for"){
                a = ci.htmlFor;
            }else if(attr == "href"){
                a = ci.getAttribute("href", 2);
            }else{
                a = ci.getAttribute(attr);
            }
            if((f && f(a, value)) || (!f && a)){
                r[++ri] = ci;
            }
        }
        return r;
    };

    function byPseudo(cs, name, value){
        return Ext.DomQuery.pseudos[name](cs, value);
    };

    // This is for IE MSXML which does not support expandos.
    // IE runs the same speed using setAttribute, however FF slows way down
    // and Safari completely fails so they need to continue to use expandos.
    var isIE = window.ActiveXObject ? true : false;

    // this eval is stop the compressor from
    // renaming the variable to something shorter
    eval("var batch = 30803;");

    var key = 30803;

    function nodupIEXml(cs){
        var d = ++key;
        cs[0].setAttribute("_nodup", d);
        var r = [cs[0]];
        for(var i = 1, len = cs.length; i < len; i++){
            var c = cs[i];
            if(!c.getAttribute("_nodup") != d){
                c.setAttribute("_nodup", d);
                r[r.length] = c;
            }
        }
        for(var i = 0, len = cs.length; i < len; i++){
            cs[i].removeAttribute("_nodup");
        }
        return r;
    }

    function nodup(cs){
        if(!cs){
            return [];
        }
        var len = cs.length, c, i, r = cs, cj, ri = -1;
        if(!len || typeof cs.nodeType != "undefined" || len == 1){
            return cs;
        }
        if(isIE && typeof cs[0].selectSingleNode != "undefined"){
            return nodupIEXml(cs);
        }
        var d = ++key;
        cs[0]._nodup = d;
        for(i = 1; c = cs[i]; i++){
            if(c._nodup != d){
                c._nodup = d;
            }else{
                r = [];
                for(var j = 0; j < i; j++){
                    r[++ri] = cs[j];
                }
                for(j = i+1; cj = cs[j]; j++){
                    if(cj._nodup != d){
                        cj._nodup = d;
                        r[++ri] = cj;
                    }
                }
                return r;
            }
        }
        return r;
    }

    function quickDiffIEXml(c1, c2){
        var d = ++key;
        for(var i = 0, len = c1.length; i < len; i++){
            c1[i].setAttribute("_qdiff", d);
        }
        var r = [];
        for(var i = 0, len = c2.length; i < len; i++){
            if(c2[i].getAttribute("_qdiff") != d){
                r[r.length] = c2[i];
            }
        }
        for(var i = 0, len = c1.length; i < len; i++){
           c1[i].removeAttribute("_qdiff");
        }
        return r;
    }

    function quickDiff(c1, c2){
        var len1 = c1.length;
        if(!len1){
            return c2;
        }
        if(isIE && c1[0].selectSingleNode){
            return quickDiffIEXml(c1, c2);
        }
        var d = ++key;
        for(var i = 0; i < len1; i++){
            c1[i]._qdiff = d;
        }
        var r = [];
        for(var i = 0, len = c2.length; i < len; i++){
            if(c2[i]._qdiff != d){
                r[r.length] = c2[i];
            }
        }
        return r;
    }

    function quickId(ns, mode, root, id){
        if(ns == root){
           var d = root.ownerDocument || root;
           return d.getElementById(id);
        }
        ns = getNodes(ns, mode, "*");
        return byId(ns, null, id);
    }

    return {
        getStyle : function(el, name){
            return Ext.fly(el).getStyle(name);
        },
        /**
         * Compiles a selector/xpath query into a reusable function. The returned function
         * takes one parameter "root" (optional), which is the context node from where the query should start.
         * @param {String} selector The selector/xpath query
         * @param {String} type (optional) Either "select" (the default) or "simple" for a simple selector match
         * @return {Function}
         */
        compile : function(path, type){
            type = type || "select";

            var fn = ["var f = function(root){\n var mode; ++batch; var n = root || document;\n"];
            var q = path, mode, lq;
            var tk = Ext.DomQuery.matchers;
            var tklen = tk.length;
            var mm;

            // accept leading mode switch
            var lmode = q.match(modeRe);
            if(lmode && lmode[1]){
                fn[fn.length] = 'mode="'+lmode[1].replace(trimRe, "")+'";';
                q = q.replace(lmode[1], "");
            }
            // strip leading slashes
            while(path.substr(0, 1)=="/"){
                path = path.substr(1);
            }

            while(q && lq != q){
                lq = q;
                var tm = q.match(tagTokenRe);
                if(type == "select"){
                    if(tm){
                        if(tm[1] == "#"){
                            fn[fn.length] = 'n = quickId(n, mode, root, "'+tm[2]+'");';
                        }else{
                            fn[fn.length] = 'n = getNodes(n, mode, "'+tm[2]+'");';
                        }
                        q = q.replace(tm[0], "");
                    }else if(q.substr(0, 1) != '@'){
                        fn[fn.length] = 'n = getNodes(n, mode, "*");';
                    }
                }else{
                    if(tm){
                        if(tm[1] == "#"){
                            fn[fn.length] = 'n = byId(n, null, "'+tm[2]+'");';
                        }else{
                            fn[fn.length] = 'n = byTag(n, "'+tm[2]+'");';
                        }
                        q = q.replace(tm[0], "");
                    }
                }
                while(!(mm = q.match(modeRe))){
                    var matched = false;
                    for(var j = 0; j < tklen; j++){
                        var t = tk[j];
                        var m = q.match(t.re);
                        if(m){
                            fn[fn.length] = t.select.replace(tplRe, function(x, i){
                                                    return m[i];
                                                });
                            q = q.replace(m[0], "");
                            matched = true;
                            break;
                        }
                    }
                    // prevent infinite loop on bad selector
                    if(!matched){
                        throw 'Error parsing selector, parsing failed at "' + q + '"';
                    }
                }
                if(mm[1]){
                    fn[fn.length] = 'mode="'+mm[1].replace(trimRe, "")+'";';
                    q = q.replace(mm[1], "");
                }
            }
            fn[fn.length] = "return nodup(n);\n}";
            eval(fn.join(""));
            return f;
        },

        /**
         * Selects a group of elements.
         * @param {String} selector The selector/xpath query (can be a comma separated list of selectors)
         * @param {Node} root (optional) The start of the query (defaults to document).
         * @return {Array}
         */
        select : function(path, root, type){
            if(!root || root == document){
                root = document;
            }
            if(typeof root == "string"){
                root = document.getElementById(root);
            }
            var paths = path.split(",");
            var results = [];
            for(var i = 0, len = paths.length; i < len; i++){
                var p = paths[i].replace(trimRe, "");
                if(!cache[p]){
                    cache[p] = Ext.DomQuery.compile(p);
                    if(!cache[p]){
                        throw p + " is not a valid selector";
                    }
                }
                var result = cache[p](root);
                if(result && result != document){
                    results = results.concat(result);
                }
            }
            if(paths.length > 1){
                return nodup(results);
            }
            return results;
        },

        /**
         * Selects a single element.
         * @param {String} selector The selector/xpath query
         * @param {Node} root (optional) The start of the query (defaults to document).
         * @return {Element}
         */
        selectNode : function(path, root){
            return Ext.DomQuery.select(path, root)[0];
        },

        /**
         * Selects the value of a node, optionally replacing null with the defaultValue.
         * @param {String} selector The selector/xpath query
         * @param {Node} root (optional) The start of the query (defaults to document).
         * @param {String} defaultValue
         */
        selectValue : function(path, root, defaultValue){
            path = path.replace(trimRe, "");
            if(!valueCache[path]){
                valueCache[path] = Ext.DomQuery.compile(path, "select");
            }
            var n = valueCache[path](root);
            n = n[0] ? n[0] : n;
            var v = (n && n.firstChild ? n.firstChild.nodeValue : null);
            return ((v === null||v === undefined||v==='') ? defaultValue : v);
        },

        /**
         * Selects the value of a node, parsing integers and floats.
         * @param {String} selector The selector/xpath query
         * @param {Node} root (optional) The start of the query (defaults to document).
         * @param {Number} defaultValue
         * @return {Number}
         */
        selectNumber : function(path, root, defaultValue){
            var v = Ext.DomQuery.selectValue(path, root, defaultValue || 0);
            return parseFloat(v);
        },

        /**
         * Returns true if the passed element(s) match the passed simple selector (e.g. div.some-class or span:first-child)
         * @param {String/HTMLElement/Array} el An element id, element or array of elements
         * @param {String} selector The simple selector to test
         * @return {Boolean}
         */
        is : function(el, ss){
            if(typeof el == "string"){
                el = document.getElementById(el);
            }
            var isArray = (el instanceof Array);
            var result = Ext.DomQuery.filter(isArray ? el : [el], ss);
            return isArray ? (result.length == el.length) : (result.length > 0);
        },

        /**
         * Filters an array of elements to only include matches of a simple selector (e.g. div.some-class or span:first-child)
         * @param {Array} el An array of elements to filter
         * @param {String} selector The simple selector to test
         * @param {Boolean} nonMatches If true, it returns the elements that DON'T match
         * the selector instead of the ones that match
         * @return {Array}
         */
        filter : function(els, ss, nonMatches){
            ss = ss.replace(trimRe, "");
            if(!simpleCache[ss]){
                simpleCache[ss] = Ext.DomQuery.compile(ss, "simple");
            }
            var result = simpleCache[ss](els);
            return nonMatches ? quickDiff(result, els) : result;
        },

        /**
         * Collection of matching regular expressions and code snippets.
         */
        matchers : [{
                re: /^\.([\w-]+)/,
                select: 'n = byClassName(n, null, " {1} ");'
            }, {
                re: new RegExp("^\\:([\\w-]+)(?:\\(((?:[^\\s>\\/]*|.*?))\\))?", ''),
                select: 'n = byPseudo(n, "{1}", "{2}");'
            },{
                re: new RegExp("^(?:([\\[\\{])(?:@)?([\\w-]+)\\s?(?:(=|.=)\\s?['\"]?(.*?)[\"']?)?[\\]\\}])", ''),
                select: 'n = byAttribute(n, "{2}", "{4}", "{3}", "{1}");'
            }, {
                re: /^#([\w-]+)/,
                select: 'n = byId(n, null, "{1}");'
            },{
                re: /^@([\w-]+)/,
                select: 'return {firstChild:{nodeValue:attrValue(n, "{1}")}};'
            }
        ],

        /**
         * Collection of operator comparison functions. The default operators are =, !=, ^=, $=, *= and %=.
         * New operators can be added as long as the match the format <i>c</i>= where <i>c</i> is any character other than space, &gt; &lt;.
         */
        operators : {
            "=" : function(a, v){
                return a == v;
            },
            "!=" : function(a, v){
                return a != v;
            },
            "^=" : function(a, v){
                return a && a.substr(0, v.length) == v;
            },
            "$=" : function(a, v){
                return a && a.substr(a.length-v.length) == v;
            },
            "*=" : function(a, v){
                return a && a.indexOf(v) !== -1;
            },
            "%=" : function(a, v){
                return (a % v) == 0;
            },
            "|=" : function(a, v){
                return a && (a == v || a.substr(0, v.length+1) == v+'-');
            },
            "~=" : function(a, v){
                return a && (' '+a+' ').indexOf(' '+v+' ') != -1;
            }
        },

        /**
         * Collection of "pseudo class" processors. Each processor is passed the current nodeset (array)
         * and the argument (if any) supplied in the selector.
         */
        pseudos : {
            "first-child" : function(c){
                var r = [], ri = -1, n;
                for(var i = 0, ci; ci = n = c[i]; i++){
                    while((n = n.previousSibling) && n.nodeType != 1);
                    if(!n){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "last-child" : function(c){
                var r = [], ri = -1, n;
                for(var i = 0, ci; ci = n = c[i]; i++){
                    while((n = n.nextSibling) && n.nodeType != 1);
                    if(!n){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "nth-child" : function(c, a) {
                var r = [], ri = -1;
                var m = nthRe.exec(a == "even" && "2n" || a == "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a);
                var f = (m[1] || 1) - 0, l = m[2] - 0;
                for(var i = 0, n; n = c[i]; i++){
                    var pn = n.parentNode;
                    if (batch != pn._batch) {
                        var j = 0;
                        for(var cn = pn.firstChild; cn; cn = cn.nextSibling){
                            if(cn.nodeType == 1){
                               cn.nodeIndex = ++j;
                            }
                        }
                        pn._batch = batch;
                    }
                    if (f == 1) {
                        if (l == 0 || n.nodeIndex == l){
                            r[++ri] = n;
                        }
                    } else if ((n.nodeIndex + l) % f == 0){
                        r[++ri] = n;
                    }
                }

                return r;
            },

            "only-child" : function(c){
                var r = [], ri = -1;;
                for(var i = 0, ci; ci = c[i]; i++){
                    if(!prev(ci) && !next(ci)){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "empty" : function(c){
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    var cns = ci.childNodes, j = 0, cn, empty = true;
                    while(cn = cns[j]){
                        ++j;
                        if(cn.nodeType == 1 || cn.nodeType == 3){
                            empty = false;
                            break;
                        }
                    }
                    if(empty){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "contains" : function(c, v){
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    if((ci.textContent||ci.innerText||'').indexOf(v) != -1){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "nodeValue" : function(c, v){
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    if(ci.firstChild && ci.firstChild.nodeValue == v){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "checked" : function(c){
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    if(ci.checked == true){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "not" : function(c, ss){
                return Ext.DomQuery.filter(c, ss, true);
            },

            "odd" : function(c){
                return this["nth-child"](c, "odd");
            },

            "even" : function(c){
                return this["nth-child"](c, "even");
            },

            "nth" : function(c, a){
                return c[a-1] || [];
            },

            "first" : function(c){
                return c[0] || [];
            },

            "last" : function(c){
                return c[c.length-1] || [];
            },

            "has" : function(c, ss){
                var s = Ext.DomQuery.select;
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    if(s(ss, ci).length > 0){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "next" : function(c, ss){
                var is = Ext.DomQuery.is;
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    var n = next(ci);
                    if(n && is(n, ss)){
                        r[++ri] = ci;
                    }
                }
                return r;
            },

            "prev" : function(c, ss){
                var is = Ext.DomQuery.is;
                var r = [], ri = -1;
                for(var i = 0, ci; ci = c[i]; i++){
                    var n = prev(ci);
                    if(n && is(n, ss)){
                        r[++ri] = ci;
                    }
                }
                return r;
            }
        }
    };
}();

y5.cssQuery = Ext.DomQuery.select;
y5.loaded('cssQuery');
/**
 * Base class for performing Ajax requests.
 * @alias y5.Ajax
 * @constructor
 * @param {Object} Ajax object (AjaxIframe, AjaxJS ...)
 * @param {String} Request URL
 * @param {Function} Function handler for success
 * @param {Function} Function handler for error
 * @param {Object} Trace
 * @param {String} Request method (POST, GET)
 * @param {Object} Request body (for POST method only)
 */
y5.Ajax = function (ajax, url, responseSuccess, responseError, trace, method, body) {
    this.method = method || this.GET;
    this.body = body || '';
    this.url = url;
    try {
        this._checkCreation(ajax, responseSuccess, responseError);
    } catch (e) {
        responseError(e, trace);
        return;
    }
    this.trace = trace || null;
    this._prepareAjax();

    /**
     * Default time to wait for Ajax response is 10s.
     * @alias y5.Ajax.waitTime
     * @constant
     */
    this.waitTime = 90 * 1000;
    this.wait = null;
};
y5.Ajax.prototype = {

    /* Constants */

    GET: 'GET',

    /* Public methods */

    /**
     * Send request.
     * @alias y5.Ajax.send
     */
    send: function () {
        var _this = this;
        function abort () {
            _this.abort();
        };
        this.wait = window.setTimeout(abort, this.waitTime);
        this.ajax.open(this.method, this.url);
        this.ajax.send(this.body);
    },

    /**
     * Abort request.
     * @alias y5.Ajax.abort
     */
    abort: function () {
        this.ajax.abort();
        if (typeof(this.error) == 'function') {
            this.error('Failed to connect, timeout limit.', this.trace);
        }
    },

    /* Private methods */

    /**
     * Prepare all listeners.
     * @alias y5.Ajax._prepareAjax
     * @private
     */
    _prepareAjax: function () {
        var _this = this;
        this.ajax.onreadystatechange = function () {
            _this._onreadystatechange();
        }
    },

    /**
     * Check readystatechange and do actions if needed.
     * @alias y5.Ajax._onreadystatechange
     * @private
     */
    _onreadystatechange: function () {
        if (this.ajax.readyState != 4) {
            return;
        }

        if (this.wait) {
            window.clearTimeout(this.wait);
            this.wait = null;
        }

        if (this.ajax.status != 200) {
            if (typeof(this.error) == 'function') {
                this.error("Invalid response status", this.trace, this.ajax.status);
            }
        } else {
            if (typeof(this.response) == 'function') {
                this.response(this.ajax.responseText, this.trace);
            }
        }

        // cleanup
        this.ajax = null;
    },

    /**
     * Check Ajax creation.
     * @alias y5.Ajax._checkCreation
     * @param {Object} ajax
     * @param {Function} responseSuccess
     * @param {Function} responseError
     * @private
     */
    _checkCreation: function (ajax, responseSuccess, responseError) {
        if (ajax.open && ajax.send && ajax.onreadystatechange) {
            this.ajax = ajax;
        } else {
            throw 'Ajax object is not defined';
        }
        if (typeof(responseSuccess) == 'function') {
            this.response = responseSuccess;
        } else {
            throw 'Function for success response is not defined';
        }
        if (typeof(responseError) == 'function') {
            this.error = responseError;
        } else {
            throw 'Function for error response is not defined';
        }
    }
};

y5.loaded('Ajax');
/**
 * Allows sending forms without reloading a page.
 * Main puprose is to send files from "&lt;input type='file'/&gt;".
 * @alias y5.AjaxForm
 * @constructor
 * @param {Object} DOM node form.
 */
y5.AjaxForm = function (form) {
    this.form = form;
    this.iframe = null;
    this.method = null;
    this.url = null;
    this.responseText = null;
    this.readyState = this.UNINITIALIZED;
    this.event = null;
    this.responseBody = null;
    this.responseStream = null;
    this.responseXML = null;
    this.status = 200;
    this.statusText = '';
    this.iframe = null;
    this._checkIframe();
}
y5.AjaxForm.prototype = {

// Constants
    GET: 'GET',
    POST: 'POST',
    UNINITIALIZED: 0,
    LOADING: 1,
    LOADED: 2,
    INTERACTIVE: 3,
    COMPLETED: 4,

// Public methods

    /**
     * Prepares Ajax connection.
     * @alias y5.AjaxForm.open
     * @param {Object} Method.
     * @param {String} URL.
     */
    open: function (method, url) {
        this.method = method || this.POST;
        this.url = url || null;
        return true;
    },

    /**
     * Sends the request.
     * @alias y5.AjaxForm.send
     * @param {Object} Body.
     */
    send: function (body) {
        if (!this.url) {
            // TODO m.b. exception
            return false;
        }
        if (!this.iframe) {
            var _this = this;
            window.setTimeout(function () {_this.send(body)}, 200);
            return false;
        }
        this._readyStateChange(this.LOADING);
        y5.AjaxFormSubmit(this.form, this.url, this.iframe.name, this.method);
        // TODO m.b. exception
        return false;
    },

    /**
     * Aborts the request.
     * @alias y5.AjaxForm.abort
     */
    abort: function () {
        this._deleteIframe();
        this._readyStateChange(this.UNINITIALIZED);
    },

    /**
     * Should be redefined to look for readystatechange.
     * @alias y5.AjaxForm.onreadystatechange
     */
    onreadystatechange: function () {
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxForm.getAllResponseHeaders
     */
    getAllResponseHeaders: function () {
        return null;
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxForm.getResponseHeader
     * @param {String} Header name.
     */
    getResponseHeader: function (name) {
        return null;
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxForm.setRequestHeader
     * @param {String} Header name.
     * @param {String} Header value.
     */
    setRequestHeader: function (name, value) {
    },

// Private methods

    /**
     * Checks readystatechange and perform actions if needed.
     * @alias y5.AjaxForm._readyStateChange
     * @param {Number} Ready state.
     * @private
     */
    _readyStateChange: function (readyState) {
        this.readyState = readyState;
        if (typeof(this.onreadystatechange) == 'function') {
            this.onreadystatechange();
        }
    },

    /**
     * Process request.
     * @alias y5.AjaxForm._onload
     * @private
     */
    _onload: function () {
        this._readyStateChange(this.LOADED);
        this._readyStateChange(this.INTERACTIVE);
        try {
            this.responseText = this.iframe.contentWindow.document.body.innerHTML;
            // log to console
            y5.Console.group(this.iframe.contentWindow.document.location, ['Ajax', 'AjaxForm']);
            y5.Console.dirxml(this.iframe, ['Ajax', 'AjaxForm']);
            y5.Console.groupEnd(this.iframe.contentWindow.document.location, ['Ajax', 'AjaxForm']);
        } catch (e) {
            y5.Console.error(e);
        }

        if (!this.responseText) {
            this.responseText = null;
        }
        this._readyStateChange(this.COMPLETED);
        this._deleteIframe();
    },

    /**
     * Checks iframe for creation.
     * @alias y5.AjaxForm._checkIframe
     * @private
     */
    _checkIframe: function () {
        if (window.ff10AjaxIframe) {
            this._setEventToIframe(window.ff10AjaxIframe);
            return;
        }
        if (y5.is_ie && document.readyState != 'complete') {
            var _this = this;
            window.setTimeout(function () {_this._checkIframe()}, 100);
            return;
        }
        this._createIframe();
    },

    /**
     * Creates iframe.
     * @alias y5.AjaxForm._createIframe
     * @private
     */
    _createIframe: function () {
        var iframe = y5.Elements.createElement({tagName: 'iframe', attributes: {name: y5.Utils.generateId()}});
        iframe.style.visible = 'hidden';
        iframe.style.width = 0;
        iframe.style.height = 0;
        iframe.frameBorder = 'no';
        var _this = this;
        this.event = y5.Events.create('load', function () {_this._setEventToIframe(iframe)}, iframe, true);
        this.operaTimeout = window.setTimeout(function () {_this._setEventToIframe(iframe)}, 500);
        y5.Dom.getBody().appendChild(iframe);
    },

    /**
     * Sets onload event to iframe.
     * @alias y5.AjaxForm._setEventToIframe
     * @param {Object} DOM node iframe.
     * @private
     */
    _setEventToIframe: function (iframe) {
        window.clearTimeout(this.operaTimeout);
        if (this.event) {
            this.event.remove();
        }
        var _this = this;
        this.event = y5.Events.create('load', function () {_this._onload()}, iframe, true);
        this.iframe = iframe;
    },

    /**
     * Deletes iframe.
     * @alias y5.AjaxForm._deleteIframe
     * @private
     */
    _deleteIframe: function () {
        if (!this.iframe || window.ff10AjaxIframe) {
            this.event.remove();
            this.iframe.contentWindow.document.location.replace('about:blank');
            this.iframe = null;
            return;
        }
        this.event.remove();
        this.iframe.src = '';
        document.body.removeChild(this.iframe);
        this.iframe = null;
        this.form = null;
    }
};

/**
 * Allows to change form action, send form to iframe and set Back action.
 * @alias y5.AjaxFormSubmit
 * @constructor
 * @param {Object} DOM node form.
 * @param {String} URL for sending form.
 * @param {Object} DOM node iframe name.
 * @param {String} Method of sending "POST" or "GET".
 */
y5.AjaxFormSubmit = function (form, action, target, method) {
    // save
    var formAction = form.action;
    var formTarget = form.target;
    var formMethod = form.method;

    // new
    form.action = action;
    form.target = target;
    form.method = method;

    // submit
    form.submit();

    // restore
    /*
       восстанавливаем по таймауту, иначе webkit восстанавливает свойства
       до вызова submit
    */
    window.setTimeout(function() {
        form.action = formAction;
        form.target = formTarget;
        form.method = formMethod;
    }, 0);
}

y5.require(['CallBacks', 'Elements', 'Utils', 'Events', 'Dom'], function () { y5.loaded('AjaxForm') });/**
 * Ajax through iframe.
 * @alias y5.AjaxIframe
 * @constructor
 * @param {Object} history
 */

y5.AjaxIframe = function (history, extendedResponse) {
    this.iframe = null;
    this.method = null;
    this.url = null;
    this.responseText = null;
    this.readyState = this.UNINITIALIZED;
    this.event = null;
    this.responseBody = null;
    this.responseStream = null;
    this.responseXML = null;
    this.status = 200;
    this.statusText = '';
    this.history = history || false;
    this.iframe = null;
    this.extendedResponse = extendedResponse || false;
    this._checkIframe();
}
y5.AjaxIframe.prototype = {

    /* Constants */

    GET: 'GET',
    POST: 'POST',
    UNINITIALIZED: 0,
    LOADING: 1,
    LOADED: 2,
    INTERACTIVE: 3,
    COMPLETED: 4,

    /* Public methods */

    /**
     * Prepares Ajax connection.
     * @alias y5.AjaxIframe.open
     * @param {Object} Method.
     * @param {String} URL.
     */
    open: function (method, url) {
        this.method = method || this.GET;
        this.url = url || null;
        return true;
    },

    /**
     * Sends the request.
     * @alias y5.AjaxIframe.send
     * @param {Object} Body.
     */
    send: function (body) {
        if (!this.url) {
        // TODO m.b. exception
            return false;
        }
        if (!this.iframe) {
            var _this = this;
            window.setTimeout(function () {_this.send(body)}, 200);
            return false;
        }
        if (this.method == this.GET) {
            return this._sendGET(this.url);
        } else if (this.method == this.POST) {
            return this._sendPOST(this.url, body);
        }
        // TODO m.b. exception
        return false;
    },

    /**
     * Aborts the request.
     * @alias y5.AjaxIframe.abort
     */
    abort: function () {
        this._deleteIframe();
        this._readyStateChange(this.UNINITIALIZED);
    },

    /**
     * Should be redefined to look for readystatechange.
     * @alias y5.AjaxIframe.onreadystatechange
     */
    onreadystatechange: function () {
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxIframe.getAllResponseHeaders
     */
    getAllResponseHeaders: function () {
        return null;
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxIframe.getResponseHeader
     * @param {String} Header name.
     */
    getResponseHeader: function (name) {
        return null;
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxIframe.setRequestHeader
     * @param {String} Header name.
     * @param {String} Header value.
     */
    setRequestHeader: function (name, value) {
    },

    /* Private methods */

    /**
     * Checks readystatechange and performs actions if needed.
     * @alias y5.AjaxIframe._readyStateChange
     * @param {Number} Ready state.
     * @return {Object} "false" or result of y5.AjaxIframe.onreadystatechange
     * @private
     */
    _readyStateChange: function (readyState) {
        this.readyState = readyState;
        if (typeof(this.onreadystatechange) == 'function'){
            this.onreadystatechange();
        }
    },

    /**
     * Sends GET request by changing iframe location.
     * @alias y5.AjaxIframe._sendGET
     * @param {Object} Url.
     * @private
     */
    _sendGET: function (url) {
        try {
            if (this.history) {
                this.iframe.contentWindow.document.location.assign(url);
            } else {
                this.iframe.contentWindow.document.location.replace(url);
            }
        } catch (e) {
            this.iframe.src = url;
        }
        this._readyStateChange(this.LOADING);
        return true;
    },

    /**
     * Sends POST request by posting form to iframe.
     * @alias y5.AjaxIframe._sendPOST
     * @param {Object} url
     * @param {Object} body
     * @private
     */
    _sendPOST: function (url, body) {
        this.event.remove();
        var form = new y5.AjaxIframeForm(this.iframe, url, body);
        this.event.add();
        form.submit();
        return true;
    },

    /**
     * Process request.
     * @alias y5.AjaxIframe._onload
     * @private
     */
    _onload: function () {
        this._readyStateChange(this.LOADED);
        this._readyStateChange(this.INTERACTIVE);
        try {
            if (this.extendedResponse) {
                this.responseText = {
                    data : this.iframe.contentWindow.document.body.innerHTML,
                    location : this.iframe.contentWindow.document.location
                }
            } else {
                this.responseText = this.iframe.contentWindow.document.body.innerHTML;
            }
        } catch (e) {}

        y5.Console.group(this.iframe.contentWindow.document.location, ['Ajax', 'AjaxIframe']);
        y5.Console.dirxml(this.iframe, ['Ajax', 'AjaxIframe']);
        y5.Console.groupEnd(this.iframe.contentWindow.document.location, ['Ajax', 'AjaxIframe']);

        if (!this.responseText) {
            this.responseText = null;
        }
        this._readyStateChange(this.COMPLETED);
        if (!this.history) {
            this._deleteIframe();
        }
    },

    /**
     * Checks iframe for creation.
     * @alias y5.AjaxIframe._checkIframe
     * @private
     */
    _checkIframe: function () {
        if (window.ff10AjaxIframe) {
            this._setEventToIframe(window.ff10AjaxIframe);
            return;
        }
        if (!y5.is_ie) {
            this._createIframe();
        } else if (document.readyState != 'complete') {
            var _this = this;
            window.setTimeout(function (){_this._checkIframe()}, 100);
            return;
        } else {
            this._createIframe();
        }
    },

    /**
     * Creates iframe.
     * @alias y5.AjaxIframe._createIframe
     * @private
     */
    _createIframe: function () {
        var iframe = y5.Elements.createElement({tagName: 'iframe'});
        iframe.style.visible = 'hidden';
        iframe.style.width = 0;
        iframe.style.height = 0;
        iframe.frameBorder = 'no';
        var _this = this;
        this.event = y5.Events.create('load', function () {_this._setEventToIframe(iframe)}, iframe, true);
        this.operaTimeout = window.setTimeout(function () {_this._setEventToIframe(iframe)}, 500);
        document.body.appendChild(iframe);
        if (this.history) {
            this._prepareFirstBack(iframe);
            //window.setTimeout(function (){_this._prepareFirstBack(iframe)}, 0);
        }
    },

    /**
     * Creates a blank page to enable proper writing of history.
     * @alias y5.AjaxIframe._prepareFirstBack
     * @param {Object} iframe
     * @private
     */
    _prepareFirstBack: function (iframe) {
        if (y5.is_ie) {
            iframe.contentWindow.document.open();
            iframe.contentWindow.document.write('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1251"><title> ajax </title></head><body></body></html>');
            iframe.contentWindow.document.close();
        } else {
            try {
                iframe.contentWindow.document.location.assign('about:blank');
            } catch (e) {
                if (!y5.is_opera) {
                    iframe.contentWindow.document.location.href = 'about:blank';
                } else {
                    iframe.src = 'about:blank';
                }
            }
        }
    },

    /**
     * Sets onload event to iframe.
     * @alias y5.AjaxIframe._setEventToIframe
     * @param {Object} DOM node iframe.
     * @private
     */
    _setEventToIframe: function (iframe) {
        window.clearTimeout(this.operaTimeout);
        if (this.event) {
            this.event.remove();
        }
        var _this = this;
        this.event = y5.Events.create('load', function () {_this._onload()}, iframe, true);
        this.iframe = iframe;
    },

    /**
     * Deletes iframe.
     * @alias y5.AjaxIframe._deleteIframe
     * @private
     */
    _deleteIframe: function () {
        if (!this.iframe || window.ff10AjaxIframe) {
            this.event.remove();
            this.iframe.contentWindow.document.location.replace('about:blank');
            this.iframe = null;
            return;
        }
        this.event.remove();
        this.iframe.src = '';
        document.body.removeChild(this.iframe);
        this.iframe = null;
    }
};

/**
 * Form for POST requests.
 * @alias y5.AjaxIframeForm
 * @constructor
 * @param {Object} DOM node iframe.
 * @param {String} Url.
 * @param {Object} Body.
 */
y5.AjaxIframeForm = function (iframe, url, body) {
    this.iframe = iframe;
    try {
        var doc = iframe.contentWindow.document;
    } catch (e) {
        return;
    }
    this._fillDocument(doc);
    this.form = this._createForm(doc, url, body);
}

y5.AjaxIframeForm.prototype = {

    /* Public methods */

    /**
     * Submits form.
     * @alias y5.AjaxIframeForm.submit
     */
    submit: function () {
        if (this.form) {
            this.form.submit();
        }
    },

    /* Private methods */

    /**
     * Fills document, it allows to add form later.
     * @alias y5.AjaxIframeForm._fillDocument
     * @param {Object} Document element.
     * @private
     */
    _fillDocument: function (doc) {
        doc.open();
        doc.write('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1251"><title> ajax </title></head><body><span/></body></html>');
        doc.close();
    },

    /**
     * Creates form.
     * @alias y5.AjaxIframeForm._createForm
     * @param {Object} Document element.
     * @param {String} Url.
     * @param {Object} Body.
     * @private
     */
    _createForm: function (doc, url, body) {
        var form;
        try {
            form = doc.body.appendChild(doc.createElement('form'));
            form.method = 'POST';
            form.action = url;
            this._createFormBody(doc, form, body);
        } catch (e) {
            this.iframe.id = 'y5.iframeForAjax';
            form = document.body.appendChild(y5.Elements.createElement({tagName: 'form'}));
            form.method = 'POST';
            form.action = url;
            form.target = this.iframe.id;
            this._createFormBody(document, form, body);
        }
        return form;
    },

    /**
     * Creates list of hidden fields, depends on argument "body".
     * @alias y5.AjaxIframeForm._createFormBody
     * @param {Object} Document element.
     * @param {Object} Form.
     * @param {Object} Body.
     * @private
     */
    _createFormBody: function (doc, form, body) {
        function createInput(doc, form, name, value) {
            var element;
            try {
                element = doc.createElement('input');
                element.type = 'hidden';
                element.name = name;
                element.value = value;
            } catch (e) {
                element = doc.createElement('<input type="hidden" name="' + name + '" value="' + value + '">');
            }
            form.appendChild(element);
            return element;
        }

        if (typeof body == 'string') {
            var elements = body.split('&');
            for (var i in elements) {
                var pair = elements[i].split('=');
                createInput(doc, form, pair[0], pair[1]);
            }
        } else if (typeof body == 'object') {
            for (name in body) {
                createInput(doc, form, name, body[name]);
            }
        }
    }
};

y5.require(['Events', 'Elements'], function (){y5.loaded('AjaxIframe')});/**
 * Hash for awaiting Ajax responses
 * @alias y5.AjaxJSResponse
 */
y5.AjaxJSResponse = {};

/**
 * AJAX through JavaScript.
 * @alias y5.AjaxJS
 * @constructor
 * @param {Object} id.
 */
y5.AjaxJS = function (id) {
    this.key = id || this._getKey();
    this.src = null;
    this.method = this.GET;
    this.responseText = '';
    this.readyState = this.UNINITIALIZED;
    this.script = null;
    this.responseBody = null;
    this.responseStream = null;
    this.responseXML = null;
    this.status = 200;
    this.statusText = '';
    this.charset = y5.charsets['y5'];
};

/**
 * Method for calling in the loaded JS files.
 * @alias y5.AjaxJS.onload
 * @param {Object} Key.
 * @param {Object} Data.
 */
y5.AjaxJS.onload = function (key, data) {
    var response = y5.AjaxJSResponse[key];
    if (!response) {
        y5.Console.warn('Nobody is waiting for response.\n   requestid:\t' + key + '\n   data:\t' + data, ['Ajax', 'AjaxJS']);
        return;
    }
    response._onload(key, data);
    delete y5.AjaxJSResponse[key];
    delete response;
};

y5.AjaxJS.prototype = {

    /* Constants */

    GET: 'GET',
    UNINITIALIZED: 0,
    LOADING: 1,
    LOADED: 2,
    INTERACTIVE: 3,
    COMPLETED: 4,
    KEY_NAME: 'requestid',

    /* Public methods */

    /**
     * Prepare ajax connection.
     * @alias y5.AjaxJS.open
     * @param {Object} method
     * @param {Object} src
     */
    open: function (method, src) {
        this.src = src || null;
    },

    /**
     * Send the request.
     * @alias y5.AjaxJS.send
     */
    send: function () {
        if (!this.src) {
            return;
        }
        this._readyStateChange(this.LOADING);
        this._registerResponseWaiting(this.key);
        var delimiter = '?';
        if ((this.src.indexOf('?') != -1)) {
            delimiter = (this.src.lastIndexOf('?') == this.src.length - 1) ? '' : '&';
        }
        this.src += delimiter + this.KEY_NAME + '=' + this.key;

        var _this = this;
        function getScript (script) {
            _this.script = script;
        }
        y5.Scripts.createScript(this.src, this.charset, getScript);
    },

    /**
     * Abort the request.
     * @alias y5.AjaxJS.abort
     */
    abort: function () {
        this._deleteScript();
        this._readyStateChange(this.UNINITIALIZED);
        if (this.key) {
            delete (y5.AjaxJSResponse[this.key]);
        }
    },

    /**
     * Should be redefined to look for ready state change.
     * @alias y5.AjaxJS.onreadystatechange
     */
    onreadystatechange: function () {
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxJS.getAllResponseHeaders
     */
    getAllResponseHeaders: function () {
        return null;
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxJS.getResponseHeader
     * @param {String} Header name.
     */
    getResponseHeader: function (name) {
        return null;
    },

    /**
     * Needed for XmlHttpRequest compatibility.
     * @alias y5.AjaxJS.setRequestHeader
     * @param {String} Header name.
     * @param {String} Header value.
     */
    setRequestHeader: function (name, value) {
    },

    /* Private methods */

    /**
     * Delete Script.
     * @alias y5.AjaxJS._deleteScript
     * @private
     */
    _deleteScript: function () {
        if (this.script) {
            this.script.src = '';
            try {
				document.getElementsByTagName('head').item(0).removeChild(this.script);
			} catch (e) {};
            this.script = null;
        }
    },

    /**
     * Check readystatechange and perform actions if needed.
     * @alias y5.AjaxJS._readyStateChange
     * @param {Object} readyState
     * @private
     */
    _readyStateChange: function (readyState) {
        this.readyState = readyState;
        if (typeof(this.onreadystatechange) == 'function') {
            this.onreadystatechange();
        }
    },

    /**
     * Put responce to waiting list.
     * @memberOf y5.AjaxJS._registerResponseWaiting
     * @param {Object} key
     * @private
     */
    _registerResponseWaiting: function (key) {
        y5.AjaxJSResponse[this.key] = this;
    },

    /**
     * Process request.
     * @alias y5.AjaxJS._onload
     * @param {Object} key
     * @param {Object} data
     * @private
     */
    _onload: function (key, data) {
        this._readyStateChange(this.LOADED);
        this._readyStateChange(this.INTERACTIVE);
        this.responseText = data;
        this._readyStateChange(this.COMPLETED);

        y5.Console.group(key, ['Ajax', 'AjaxJS']);
        y5.Console.dirxml(this.script, ['Ajax', 'AjaxJS']);
        y5.Console.log(data, ['Ajax', 'AjaxJS']);
        y5.Console.groupEnd(key, ['Ajax', 'AjaxJS']);

        this._deleteScript();
    },

    /**
     * Returns a unique key.
     * @alias y5.AjaxJS._getKey
     * @private
     */
    _getKey: function () {
        return (new Date().getTime() + '' + Math.round(Math.random() * 1000000));
    }
};

y5.loaded('AjaxJS');y5.Styles = new function () {
    this.styles = [];

    var createStyle;

    this.createStyle = function (href, loadInstantly) {
        if (this.styles[href]) {
            return this.styles[href];
        }

        loadInstantly = loadInstantly || true;

        var style = {
            href: href,
            element: createStyle(),
            load: function () {
                this.element.href = this.href;
            }
        };

        if (loadInstantly) {
            style.load();
        }

        this.styles[href] = style;

        return style;
    };

    if (y5.is_opera_7) {
        createStyle = function () {
            var span = document.createElement('span');
            span.style.display = 'none';
            span.innerHTML = '<l'+'ink></'+'link>';
            var style = span.getElementsByTagName('link').item(0);
            setStyleAttributes(style);
            appendStyle(span, document.body);
            span = null;
            return style;
        }
    } else if (y5.is_konq) {
        createStyle = function () {
            var head = document.getElementsByTagName("head")[0];
            var style = document.createElement('link');
            setStyleAttributes(style);
            window.setTimeout(function() { appendStyle(style, head); }, 0);
            return style;
        }
    } else {
        createStyle = function () {
            var style = document.createElement('link');
            setStyleAttributes(style);
            appendStyle(style, document.getElementsByTagName('head').item(0));
            return style;
        }
    }

    function setStyleAttributes(style) {
        style.setAttribute('rel', 'stylesheet');
        style.setAttribute('type', 'text/css');
    }

    function appendStyle(style, container) {
        container.insertBefore(style, container.firstChild);
    }
};

y5.loaded('Styles');