/*
   Определение undefined для старых браузеров
*/
try {
    undefined
} catch (e) {
    window.undefined = void 0;
}

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


/// Функции call, apply
/////////////////////////////////////////////////////////////////////

(function() {

/**
 * Методы функций для поддержки старых браузеров.
 * Включает набор функций, отсутствующих в старых версиях браузеров (например, IE 5), но содержащихся в стандарте ECMAScript.
 * @class
 * @external
 * @name Function
 */
var F = Function.prototype;

if (!F.apply) {

    var ac = 0;

    /**
     * Применяет метод одного объекта в контексте другого. Возвращает результат выполнения функции.
     * @name apply
     * @function
     * @memberOf Function
     * @jsver 1.3
     * @param {Object} context Контекст выполнения функции
     * @param {Array} [args] Массив, элементы которого передаются функции как аргументы
     * @returns {Object} Результат выполнения функции
     *
     * @example
     * var foo = new Foo();
     * function bar() { ... }
     * bar.apply(foo, [1, 2, 3]);
     */
    F.apply = function(c, a) {
        var n = '__y5_apply__' + (ac++) + '__', r;

        c = c || window;
        c[n] = this;

        switch ((a || []).length) {
            case 0: r = c[n](); break;
            case 1: r = c[n](a[0]); break;
            case 2: r = c[n](a[0], a[1]); break;
            case 3: r = c[n](a[0], a[1], a[2]); break;
            default:
                var af = [];
                if (a) {
                    var l = a.length;
                    af = new Array(l);
                    for (var i = 0; i < l; i++) {
                        af[i] = 'a[' + i + ']';
                    }
                }

                r = eval('c.' + n + '(' + af.join(',') + ')');
        }

        if (typeof c.valueOf == 'function') {
            delete c[n];
        } else {
            c[n] = undefined;
        }

        return r;
    };

}

if (!F.call) {

    /**
     * Вызывает метод одного объекта в контексте другого. Возвращает результат выполнения функции.
     * @name call
     * @function
     * @memberOf Function
     * @jsver 1.3
     * @param {Object} context Контекст выполнения функции
     * @param {Object} [arg1] Аргумент вызываемой функции
     * @param {Object} [arg2]
     * @param {Object} [...]
     * @param {Object} [argN]
     * @returns {Object} Результат выполнения функции
     *
     * @example
     * var foo = new Foo();
     * function bar() { ... }
     * bar.call(foo, 1, 2, 3);
     */
    F.call = function(context) {
        return this.apply(context, Array.prototype.slice.apply(arguments, [1]));
    };

}

})();

(function() {

/// Функции для работы с массивами
/////////////////////////////////////////////////////////////////////

/**
 * Набор функций для работы с массивами.
 * Включает реализацию функций, которые отсутствуют в некоторых старых браузерах, но содержатся в стандарте ECMAScript.
 * @class
 * @external
 * @name Array
 */
var A = Array.prototype, ArrayFuncs = {

    /**
     * Добавляет элемент в конец массива.
     * Возвращает длину полученного массива.
     * Функция изменяет исходный массив.
     * @name push
     * @function
     * @memberOf Array
     * @jsver 1.2
     * @param {Object} element1 Элемент, добавляемый в конец массива
     * @param {Object} [element2]
     * @param {Object} [elementN]
     * @returns {Number} Длина полученного массива
     *
     * @example
     * var a = ["Mars", "Pluto", "Mercury"];
     * a.push("Venus");
     * // a == ["Mars", "Pluto", "Mercury", "Venus"]
     * // a.length == 4
     */
    push: function() {
        var args = arguments;
        for (var i = 0, l = args.length; i < l; i++) {
            this[this.length] = args[i];
        }

        return this.length;
    },

    /**
     * Удаляет последний элемент массива.
     * Возвращает удаляемый элемент.
     * Функция изменяет исходный массив.
     * @name pop
     * @function
     * @memberOf Array
     * @jsver 1.2
     * @returns {Object} Удаленный элемент массива
     *
     * @example
     * var a = [1, 2, 3, 4];
     * a.pop();
     * // -> 4
     * // a == [1, 2, 3]
     *
     * var i = [];
     * typeof i.pop();
     * // -> "undefined"
     * // i == []
     */
    pop: function() {
        var element, length = this.length;

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

        return element;
    },

    /**
     * Добавляет элемент в начало массива. Возвращает длину полученного массива.
     * Функция изменяет исходный массив.
     * @name unshift
     * @function
     * @memberOf Array
     * @jsver 1.2
     * @param {Object} element1 Элемент, добавляемый в начало массива
     * @param {Object} [element2]
     * @param {Object} [elementN]
     * @returns {Number} Длина полученного массива
     *
     * @example
     * var a = ["Mars", "Pluto", "Mercury"];
     * a.unshift("Venus");
     * // a == ["Venus", "Mars", "Pluto", "Mercury"]
     * // a.length == 4
     */
    unshift: function() {
        var i, l,
            a  = arguments,
            al = a.length,
            tl = this.length,
            nl = al + tl;

        this.length = nl;

        for (i = tl - 1, l = 0; i >= l; i--) {
            this[i + al] = this[i];
        }

        for (i = 0, l = al; i < l; i++) {
            this[i] = a[i];
        }

        return this.length;
    },

    /**
     * Изменяет содержимое массива. Позволяет удалить часть элементов массива и одновременно добавить новые. Возвращает массив удаленных элементов.
     * Функция изменяет исходный массив.
     * @name splice
     * @function
     * @memberOf Array
     * @jsver 1.2
     * @param {Number} start Индекс, с которого изменяется массив
     * @param {Number} [deleteCount] Количество удаляемых элементов (если 0 или не указан, то элементы не удаляются)
     * @param {Object} [element1] Элемент, добавляемый в массив (если элемент не определен, то функция просто удаляет элементы из массива)
     * @param {Object} [element2]
     * @param {Object} [...]
     * @param {Object} [elementN]
     * @returns {Array} Удаленные элементы массива
     *
     * @example
     * var a = [1, 2, 3, 4, 5];
     * a.splice(1, 3);
     * // удаленные элементы -> [2, 3, 4]
     * // a == [1, 5]
     *
     * var i = [1, 2, 3, 4, 5];
     * i.splice(1, 2, 6, "foo");
     * // удаленные элементы -> [2, 3]
     * // i == [1, 6, "foo", 4, 5]
     */
    splice: function(start, deleteCount) {
        var i,
            args = arguments,
            l = this.length,
            removed = [];

        start = start || 0;
        deleteCount = 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 (args.length > 2) {
            var il;
            var arr = this.slice(start);
            this.length = start;

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

        return removed;
    },

    /**
     * Удаляет первый элемент массива. Возвращает удаляемый элемент.
     * Функция изменяет исходный массив.
     * @name shift
     * @function
     * @memberOf Array
     * @jsver 1.2
     * @returns {Object} Удаленный элемент массива
     *
     * @example
     * var a = [1, 2, 3, 4];
     * a.shift();
     * // -> 1
     * // a == [2, 3, 4]
     *
     * var i = [];
     * typeof i.shift();
     * // -> "undefined"
     * // i == []
     */
    shift: function() {
        var element;

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

        return element;
    },

    /**
     * Поиск элемента массива.
     * Ищет заданный элемент с начала массива и возвращает его индекс или -1, если необходимого элемента в массиве нет.
     * @name indexOf
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Object} searchElement Искомый элемент
     * @param {Number} [fromIndex] Индекс, с которого ищется элемент (по умолчанию - с первого элемента массива)
     * @returns {Number} Индекс искомого элемента
     *
     * @example
     * var a = [2, 5, 9];
     * var i = a.indexOf(2);
     * // i == 0
     * i = a.indexOf(7);
     * // i == -1
     */
    indexOf: function(elt /*, from*/) {
        var len = this.length >>> 0;

        var from = Number(arguments[1]) || 0;
        from = (from < 0)
             ? Math.ceil(from)
             : Math.floor(from);
        if (from < 0)
          from += len;

        for (; from < len; from++)
        {
          if (from in this &&
              this[from] === elt)
            return from;
        }
        return -1;
    },

    /**
     * Обратный поиск элемента массива.
     * Ищет заданный элемент с конца массива и возвращает его индекс или -1, если необходимого элемента в массиве нет.
     * @name lastIndexOf
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Object} searchElement Искомый элемент
     * @param {Number} [fromIndex] Индекс, с которого ищется элемент (по умолчанию - с последнего элемента массива)
     * @returns {Number} Индекс искомого элемента
     *
     * @example
     * var array = [2, 5, 9, 2];
     * var index = array.lastIndexOf(2);
     * // index == 3
     * index = array.lastIndexOf(7);
     * // index == -1
     * index = array.lastIndexOf(2, 3);
     * // index == 3
     * index = array.lastIndexOf(2, 2);
     * // index == 0
     * index = array.lastIndexOf(2, -2);
     * // index == 0
     * index = array.lastIndexOf(2, -1);
     * // index == 3
     */
    lastIndexOf: function(elt /*, from*/) {
        var len = this.length;

        var from = Number(arguments[1]);
        if (isNaN(from))
        {
          from = len - 1;
        }
        else
        {
          from = (from < 0)
               ? Math.ceil(from)
               : Math.floor(from);
          if (from < 0)
            from += len;
          else if (from >= len)
            from = len - 1;
        }

        for (; from > -1; from--)
        {
          if (from in this &&
              this[from] === elt)
            return from;
        }
        return -1;
    },

    /**
     * Тестирование каждого элемента массива при помощи функции проверки.
     * Результатом тестирования массива является true, если все функции проверки вернули true. Иначе результатом тестирования массива будет false.
     * @name every
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Function} callback Функция для проверки элемента массива (должна возвращать true или false)
     * @param {Object} [thisObject] Контекст выполнения функции проверки
     * @returns {Boolean} true - если все функции проверки вернули true, иначе - false
     *
     * @example
     * function isBigEnough(element, index, array) {
     *     return (element >= 10);
     * }
     *
     * var passed = [12, 5, 8, 130, 44].every(isBigEnough);
     * // passed == false
     * passed = [12, 54, 18, 130, 44].every(isBigEnough);
     * // passed == true
     */
    every: function(fun /*, thisp*/) {
        var len = this.length >>> 0;
        if (typeof fun != "function")
          throw new TypeError();

        var thisp = arguments[1];
        for (var i = 0; i < len; i++)
        {
          if (i in this &&
              !fun.call(thisp, this[i], i, this))
            return false;
        }

        return true;
    },

    /**
     * Фильтрация элементов массива.
     * Возвращает новый массив, который состоит из элементов, прошедших функцию проверки, т.е. удовлетворяющих определенным условиям.
     * Исходный массив не изменяется.
     * @name filter
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Function} callback Функция для проверки элемента массива (должна возвращать true или false)
     * @param {Object} [thisObject] Контекст выполнения функции проверки
     * @returns {Array} Массив
     *
     * @example
     * function isBigEnough(element, index, array) {
     *     return (element >= 10);
     * }
     * var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);
     * // filtered == [12, 130, 44]
     */
    filter: function(fun /*, thisp*/) {
        var len = this.length >>> 0;
        if (typeof fun != "function")
          throw new TypeError();

        var res = new Array();
        var thisp = arguments[1];
        for (var i = 0; i < len; i++)
        {
          if (i in this)
          {
            var val = this[i]; // in case fun mutates this
            if (fun.call(thisp, val, i, this))
              res.push(val);
          }
        }

        return res;
    },

    /**
     * Обработка каждого элемента массива заданной функцией.
     * @name forEach
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Function} callback Функция, выполняемая для каждого элемента
     * @param {Object} [thisObject] Контекст выполнения функции
     *
     * @example
     * function printElt(element, index, array) {
     *     print("[" + index + "] == " + element);
     * }
     * [2, 5, 9].forEach(printElt);
     * // Печатает:
     * // [0] == 2
     * // [1] == 5
     * // [2] == 9
     */
    forEach: function(fun /*, thisp*/) {
        var len = this.length >>> 0;
        if (typeof fun != "function")
          throw new TypeError();

        var thisp = arguments[1];
        for (var i = 0; i < len; i++)
        {
          if (i in this)
            fun.call(thisp, this[i], i, this);
        }
    },

    /**
     * Обработка каждого элемента массива заданной функцией.
     * Создает новый массив с результатами выполнения функции для каждого элемента массива.
     * Исходный массив не изменяется.
     * @name map
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Function} callback Функция, выполняемая для каждого элемента
     * @param {Object} [thisObject] Контекст выполнения функции
     * @returns {Array} Массив элементов
     *
     * @example
     * var numbers = [1, 4, 9];
     * var roots = numbers.map(Math.sqrt);
     * // roots == [1, 2, 3]
     * // numbers == [1, 4, 9]
     */
    map: function(fun /*, thisp*/) {
        var len = this.length >>> 0;
        if (typeof fun != "function")
          throw new TypeError();

        var res = new Array(len);
        var thisp = arguments[1];
        for (var i = 0; i < len; i++)
        {
          if (i in this)
            res[i] = fun.call(thisp, this[i], i, this);
        }

        return res;
    },

    /**
     * Тестирование каждого элемента массива при помощи функции проверки. Результатом тестирования массива является true, если хотя бы один элемент прошел успешную проверку. Иначе результатом тестирования массива будет false.
     * @name some
     * @function
     * @memberOf Array
     * @jsver 1.6
     * @param {Function} callback Функция, выполняемая для каждого элемента
     * @param {Object} [thisObject] Контекст выполнения функции
     *
     * @example
     * function isBigEnough(element, index, array) {
     *     return (element >= 10);
     * }
     * var passed = [2, 5, 8, 1, 4].some(isBigEnough);
     * // passed == false
     * passed = [12, 5, 8, 1, 4].some(isBigEnough);
     * // passed == true
     */
    some: function(fun /*, thisp*/) {
        var i = 0,
            len = this.length >>> 0;

        if (typeof fun != "function")
          throw new TypeError();

        var thisp = arguments[1];
        for (; i < len; i++)
        {
          if (i in this &&
              fun.call(thisp, this[i], i, this))
            return true;
        }

        return false;
    },

    /**
     * Обработка слева направо соседних элементов массива заданной функцией. Функция применяется одновременно к двум элементам массива и превращает их в одно значение.
     * Функция изменяет исходный массив.
     * @name reduce
     * @function
     * @memberOf Array
     * @jsver 1.8
     * @param {Function} callback Функция, выполняемая для каждого элемента
     * @param {Object} [initial] Начальное значение
     *
     * @example
     * [0, 1, 2, 3, 4].reduce(function(previousValue, currentValue, index, array) {
     *     return previousValue + currentValue;
     * });
     * // 10
     */
    reduce: function(fun /*, initial*/) {
        var len = this.length >>> 0;
        if (typeof fun != "function")
          throw new TypeError();

        // no value to return if no initial value and an empty array
        if (len == 0 && arguments.length == 1)
          throw new TypeError();

        var i = 0;
        if (arguments.length >= 2)
        {
          var rv = arguments[1];
        }
        else
        {
          do
          {
            if (i in this)
            {
              rv = this[i++];
              break;
            }

            // if array contains no values, no initial value to return
            if (++i >= len)
              throw new TypeError();
          }
          while (true);
        }

        for (; i < len; i++)
        {
          if (i in this)
            rv = fun.call(null, rv, this[i], i, this);
        }

        return rv;
    },

    /**
     * Обработка cправа налево соседних элементов массива заданной функцией. Функция применяется одновременно к двум элементам массива и превращает их в одно значение.
     * Функция изменяет исходный массив.
     * @name reduceRight
     * @function
     * @memberOf Array
     * @jsver 1.8
     * @param {Function} callback Функция, выполняемая для каждого элемента
     * @param {Object} [initial] Начальное значение
     *
     * @example
     * [0, 1, 2, 3, 4].reduceRight(function(previousValue, currentValue, index, array) {
     *     return previousValue + currentValue;
     * });
     * // 10
     */
    reduceRight: function(fun /*, initial*/) {
        var len = this.length >>> 0;
        if (typeof fun != "function")
          throw new TypeError();

        // no value to return if no initial value, empty array
        if (len == 0 && arguments.length == 1)
          throw new TypeError();

        var i = len - 1;
        if (arguments.length >= 2)
        {
          var rv = arguments[1];
        }
        else
        {
          do
          {
            if (i in this)
            {
              rv = this[i--];
              break;
            }

            // if array contains no values, no initial value to return
            if (--i < 0)
              throw new TypeError();
          }
          while (true);
        }

        for (; i >= 0; i--)
        {
          if (i in this)
            rv = fun.call(null, rv, this[i], i, this);
        }

        return rv;
    }
};

for (var name in ArrayFuncs) {
    if (!A[name]) {
        A[name] = ArrayFuncs[name];
    }
}

(function() {   

    var make_generic = function(static_holder, dynamic_holder, method_name) {
        var dynamic_method = dynamic_holder[method_name];

        if (typeof dynamic_method != "function" && static_holder[method_name]) {
            return false;
        }   

        static_holder[method_name] = function() {
            return dynamic_method.call.apply(dynamic_method, arguments);
        }   

        return static_holder[method_name];
    };  

      //создаём дженерики - чтобы работать с коллекциями, похожими на массив, не конвертируя их в массив
      //например, Array.forEach(document.getElementsByTagName('*'), function() { ... });
     ["every", "filter", "forEach", "indexOf", "lastIndexOf", "map", "reduce", "reduceRight", "some"].forEach(function($1) {
        make_generic(Array, Array.prototype, $1);
     }); 

})();


var StringPrototype = String.prototype,
    Space = '';

/// Исправляем поведение indexOf в IE 5
if (Space.indexOf(Space) != 0) {

    StringPrototype.indexOfBug = StringPrototype.indexOf;

    StringPrototype.indexOf = function(needle) {
        if (this.toString() == Space && needle === Space) {
            return 0;
        }

        return this.indexOfBug(needle);
    };

    StringPrototype.lastIndexOfBug = StringPrototype.lastIndexOf;

    StringPrototype.lastIndexOf = function(needle) {
        var pos = this.lastIndexOfBug(needle);
        if (needle === Space) {
            pos++;
        }

        return pos;
    };

}

/// Исправление String.replace (Safari1.x/IE5.0).
// IE7/IE8.js - copyright 2004-2008, Dean Edwards
if (Space.replace(/^/, String)) {
    var GLOBAL = /(g|gi)$/,
        _replace = StringPrototype.replace;

    StringPrototype.replace = function(expression, replacement) {
        if (typeof replacement == 'function') { // Safari doesn't like functions
            var regexp, global, match, string = this, result = Space;

            if (expression && expression.constructor == RegExp) {
                regexp = expression;
                global = regexp.global;
                if (global == null) {
                    global = GLOBAL.test(regexp);
                }
                // we have to convert global RexpExps for exec() to work consistently
                if (global) {
                    regexp = new RegExp(regexp.source); // non-global
                }
            } else {
                regexp = new RegExp(rescape(expression));
            }

            while (string && (match = regexp.exec(string))) {
                result += string.slice(0, match.index) + replacement.apply(this, match);
                string = string.slice(match.index + match[0].length);
                if (!global) {
                    break;
                }
            }
            return result + string;
        }
        return _replace.apply(this, arguments);
    };
}

var fromCharCode = String.fromCharCode;

if (!StringPrototype.charCodeAt) {

    StringPrototype.charCodeAt = function(index) {
        var k = 0,
            enc = escape(this).match(/(%[\da-fA-F]{2}|%u[\da-fA-F]{4}|.)/g);

        for (var i = 0, l = enc.length; i < l; i++) {
            var symb = enc[i];
            if (!symb) {
                continue;
            }

            if (symb.indexOf('%u') == 0) {
                symb = parseInt(symb.replace('%u', Space), 16);
            } else if (symb.indexOf('%') == 0) {
                symb = parseInt(symb.replace('%', Space), 16);
            } else {
                var begin = 0, end = 256, cur = 0, curChar = Space;

                while (end - begin > 1) {
                    cur = (end + begin) >> 1; // div 2
                    curChar = fromCharCode(cur);
                    if (curChar > symb) {
                        end = cur;
                    } else if (curChar < symb) {
                        begin = cur;
                    } else {
                        symb = cur;
                        break;
                    }
                }
            }

            if (k == index) {
                return symb;
            }
            k++;
        }

        return NaN;
    };
}

// ie 5.0
if (!window.encodeURIComponent) {

    var hexDigit = '0123456789ABCDEF'.split('');

    function dec2hex (n) {
        return hexDigit[n >> 4] + hexDigit[n & 15];
    }

    function utf8_and_URI_encode (string, encodeURI) {
        string = string.toString().replace(/\r\n/g, "\n");

        var text = '',
            c,
            n = 0,
            strlen = string.length,
            // символы, которые не преобразуются encodeURIComponent
            nonEscapeCharacters = "!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";

        if (encodeURI) {
            // encodeURI не преобразует еще 12 символов
            nonEscapeCharacters += '#$&+,-/:;=?@';
        }

        for (; n < strlen; n++) {
            c = string.charCodeAt(n);
            if (c < 128 ) {
                if (nonEscapeCharacters.indexOf(string.charAt(n)) != -1) {
                    text += fromCharCode(c);
                } else {
                    text += '%'+dec2hex(c);
                }

            // utf-8 encode
            } else if ((c > 127) && (c < 2048)) {
                text += escape(fromCharCode((c >> 6) | 192) + fromCharCode((c & 63) | 128));

            } else {
                text += escape(fromCharCode((c >> 12) | 224) + fromCharCode(((c >> 6) & 63) | 128) + fromCharCode((c & 63) | 128));
            }
        }

        return text;
    };

    window.encodeURIComponent = utf8_and_URI_encode;

    window.encodeURI = function (string) {
        return utf8_and_URI_encode(string, true);
    }
}

if (!window.decodeURIComponent) {
    var mask21 = 6,  // parseInt('110', 2)
        mask22 = 2,  // parseInt('10', 2)
        mask31 = 14, // parseInt('1110', 2)
        throwMsg = 'malformed URI sequence';

    function utf8_and_URI_decode (text) {
        text = unescape(text.toString());
        var string = '',
            strlen = text.length,
            i = 0,
            s,
            c = 0,
            c1 = 0,
            c2 = 0;

        while (i < strlen) {
            c = text.charCodeAt(i);
            if (c < 128) {
                s = fromCharCode(c);
                i++;
            } else if ((c > 191) && (c < 224)) {
                c1 = text.charCodeAt(i + 1);
                if (!((c >> 5) == mask21 && (c1 >> 6) == mask22)) {
                    throw throwMsg;
                }
                s = fromCharCode(((c & 31) << 6) | (c1 & 63));
                i += 2;
            } else {
                c1 = text.charCodeAt(i + 1);
                c2 = text.charCodeAt(i + 2);
                if (!((c >> 4) == mask31 && (c1 >> 6) == mask22 && (c2 >> 6) == mask22)) {
                    throw throwMsg;
                }
                s = fromCharCode(((c & 15) << 12) | ((c1 & 63) << 6) | (c2 & 63));
                i += 3;
            }
            string += s;
        }

        return string;
    }

    window.decodeURIComponent = window.decodeURI = utf8_and_URI_decode;
}


/// Node
/////////////////////////////////////////////////////////////////////

if (!window.Node) {
    window.Node = {};
}

if (!Node.ELEMENT_NODE) {

// DOM level 2 ECMAScript Language Binding
/*
    var types = {
        ELEMENT_NODE: 1,
        ATTRIBUTE_NODE: 2,
        TEXT_NODE: 3,
        CDATA_SECTION_NODE: 4,
        ENTITY_REFERENCE_NODE: 5,
        ENTITY_NODE: 6,
        PROCESSING_INSTRUCTION_NODE: 7,
        COMMENT_NODE: 8,
        DOCUMENT_NODE: 9,
        DOCUMENT_TYPE_NODE: 10,
        DOCUMENT_FRAGMENT_NODE: 11,
        NOTATION_NODE: 12
    };

    for (var type in types) {
        Node[type] = types[type];
    }

    delete types;
*/

    (
     'ELEMENT,ATTRIBUTE,TEXT,CDATA_SECTION,ENTITY_REFERENCE,ENTITY,' +
     'PROCESSING_INSTRUCTION,COMMENT,DOCUMENT,DOCUMENT_TYPE,DOCUMENT_FRAGMENT,NOTATION'
    ).split(',').forEach(function(type, num) { Node[type + '_NODE'] = num + 1 });

}

})();
/**
 * Ядро фреймворка.
 * Содержит определения часто используемых переменных (версия браузера, тип операционной системы и т.п.) и набор функций для загрузки модулей. 
 * 
 * @class
 * @static
 * @name y5
 *
 * @example
 * Для загрузки модулей библиотек можно использовать удобный механизм динамической загрузки.
 * Для этого используется метод y5.require.
 *
 * y5.require('Dom', function() {
 *     // здесь можно использовать функции y5.Dom
 * });
 *
 * y5.require('URL', 'Classes', function() {
 *     // здесь можно использовать функции y5.URL и y5.Classes
 * });
 */
var y5 = (function() {

    var
        // Список загруженных модулей
        // y5:Arrays, y5:Dom, ...
        Modules = {},
        
        /**
         * Массив заблокированных модулей
         * @name BlockModules
         * @memberOf y5
         * @type Array
         * @private
         */
        BlockModules = [],

        // Список требуемых модулей
        ModulesRequired = {},

        // Правило для вычисления названия библиотеки и имени модуля
        // y5:Arrays или {y5}.Arrays (устарело)
        ModuleRegexp = /^(\{([^\}]+)\}\.|([^:]+):)?(.+)$/,

        // Список библиотек
        // y5, Friends, ...
        // Формат: {y5:
        //   {
        //     path: [путь к библиотеке],
        //     charset: [кодировка файлов модулей],
        //     query: [параметры URL, добавляемые к запросу модуля]
        //   }
        // }
        Namespaces = {},

        // Имя библиотеки по умолчанию
        NamespaceDefault = 'y5',

        // Очередь модулей
        // [y5:Arrays, y5:Dom, ...]
        Query = [],

        // Список скриптов страницы
        Scripts = document.getElementsByTagName('script'),

        moduleLoadedNotifier,
        moduleRequiredNotifier,
        namespaceAddedNotifier;

    function runCallback(callback) {
        if (typeof callback == 'function') {
            callback();
        }
    }

    function isModuleLoaded(module) {
        return !!Modules[module];
    }

    function isModuleNotLoaded(module) {
        return !Modules[module];
    }

    function isModulesLoaded(modules) {
        return modules.every(isModuleLoaded);
    }
    
    function isModuleBlocked (module) {
        for (var i=0,j=BlockModules.length; i<j; i++) {
            if (module.indexOf(BlockModules[i]) == 0) {
                return true;
            }
        }
        return false;
    }

    function addModuleLoaded(module) {
        Modules[module] = 1;

        if (!moduleLoadedNotifier) {
            moduleLoadedNotifier = y5.Events.notify('y5:moduleLoaded', y5, false);
        }
        moduleLoadedNotifier.dispatch(module);
    }

    function isModuleRequired(module) {
        return !!ModulesRequired[module];
    }

    function addModuleRequired(module) {
        ModulesRequired[module] = 1;

        if (!moduleRequiredNotifier) {
            moduleRequiredNotifier = y5.Events.notify('y5:moduleRequired', y5, false);
        }
        moduleRequiredNotifier.dispatch(module);
    }

    function removeModuleRequired(module) {
        delete ModulesRequired[module];
    }

    function addNamespace(namespace, data) {
        Namespaces[namespace] = data;
        if (!namespaceAddedNotifier) {
            namespaceAddedNotifier = y5.Events.notify('y5:namespaceAdded', y5, false);
        }
        namespaceAddedNotifier.dispatch(data);
        callQuery();
    }

    function getNamespaceData(namespace) {
        return Namespaces[namespace];
    }

    function getModuleData(module) {
        var data = module.match(ModuleRegexp);
        // TODO: exception
        return [
            data[3] || data[2] || NamespaceDefault,
            data[4]
        ];
    }

    function getModuleNameCanonical(module) {
        var data = getModuleData(module);
        return data[0] + ':' + data[1];
    }

    function getModuleURL(module, type) {
        var data = getModuleData(module);
        var namespace = getNamespaceData(data[0]);

        if (!namespace) {
            return false;
        }

        return [
            (
                namespace.path +
                data[1].replace(/\./g, '/') +
                '.' + (type || 'js') +
                namespace.query
            ),
            namespace.charset
        ];
    }

    /**
     * Загружает модули по необходимости.
     * @private
     */
    function loadModules(modules) {
        var i = 0,
            l = modules.length,
            module;
        for (; i < l; i++) {
            module = modules[i];
            if (isModuleLoaded(module) || isModuleRequired(module) || isModuleBlocked(module)) {
                continue;
            }

            var data = getModuleURL(module);
            if (data) {
                addModuleRequired(module);
                y5.Loader.loadScript(data[0], data[1]);
            }
        }
    }

    /**
     * Помещает в очередь список требуемых модулей и callback-функцию.
     * @private
     */
    function pushQuery(modules, callback) {
        modules = modules.filter(isModuleNotLoaded);

        if (modules.length == 0) {
            runCallback(callback);
            return false;
        }

        Query.push({
            modules: modules,
            callback: callback
        });

        return modules;
    }

    /**
     * Запускает callback-функции из очереди для загруженных модулей.
     * @private
     */
    function callQuery() {
        for (var i = 0; i < Query.length; i++) {
            var item = Query[i];
            if (isModulesLoaded(item.modules)) {
                Query.splice(i, 1);
                runCallback(item.callback);
                i--;
            } else {
                loadModules(item.modules);
            }
        }
    }

    /**
     * Возвращает данные скрипта по имени файла.
     * @param {String} file Имя файла
     * @param {String} [charset] Кодировка файла (если указано, то переопределяет кодировку указанную в скрипте)
     * @returns {Object} Объект вида {path: 'http://www.yandex.ru/foo/', query: '?build=1', charset: 'utf-8'}
     * @private
     */
    function getData(file, charset) {
        for (var i = 0, l = Scripts.length; i < l; i++) {
            var script = Scripts[i],
                src = script.getAttribute('src');

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

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

                return base;
            }
        }
        return null;
    }

    function registerNamespaceByData(namespace, data, charset) {
        if (typeof data == 'string') {
            data = {path: data, charset: charset};
        }

        // дополняем путь слэшем, если забыт
        if (data.path.lastIndexOf('/') != data.path.length - 1) {
            data.path += '/';
        }

        // устанавливаем кодировку по умолчанию
        if (!data.charset) {
            data.charset = 'utf-8';
        }

        // устанавливаем параметры запроса по умолчанию
        if (!data.query) {
            data.query = '';
        }

        addNamespace(namespace, data);
    }

    function getNamespaceItem(namespace, name) {
        try {
            return Namespaces[namespace][name];
        } catch (e) {
            return null;
        }
    }

    var obj = {
        /**
         * Текущая версия фреймворка.
         * @name y5.version
         * @memberOf y5
         * @type Number
         */
        version: 1.5,

        /**
         * Логическая переменная. Устанавливается в true, когда происходит событие dom:loaded.
         * @name y5.domloaded
         * @memberOf y5
         * @type Boolean
         */
        domloaded: false,

        /**
         * Загружает требуемые модули и выполняет callback-функцию после загрузки модулей.
         * @name y5.require
         * @memberOf y5
         * @function
         * @param {String | Array} modules "foo.bar"
         * @param {Function} callback Функция, выполняемая в момент загрузки
         */
        require: function(/* module1, module2, ..., moduleN [ , callback ] */) {
            var args = arguments,
                modules = [],
                callback = y5.VOID,
                Types = y5.Types;

            for (var i = 0, l = args.length; i < l; i++) {
                var arg = args[i];
                switch (Types.type(arg)) {
                    case Types.ARRAY:
                        modules = modules.concat(arg);
                        break;

                    case Types.STRING:
                        modules.push(arg);
                        break;

                    case Types.FUNCTION:
                        callback = arg;
                        break;
                }
            }

            modules = modules.map(getModuleNameCanonical);
            modules = pushQuery(modules, callback);

            if (modules) {
                loadModules(modules);
            }
        },

        /**
         * Функция вызывается модулем после окончания загрузки всех зависимых модулей.
         * @name y5.loaded
         * @memberOf y5
         * @function
         * @param {String} module Имя модуля (например, "foo.bar")
         */
        loaded: function(module) {
            module = getModuleNameCanonical(module);
            removeModuleRequired(module);
            addModuleLoaded(module);
            callQuery();
        },

        /**
         * Регистрирует новую библиотеку модулей по имени файла.
         * @name y5.registerNamespace
         * @memberOf y5
         * @function
         * @param {String} namespace Имя, под которым регистрируется библиотека
         * @param {String} filename Строка, которая входит в имя файла библиотеки
         * @param {String} [charset] Кодировка файлов библиотеки
         */
        registerNamespace: function(namespace, file, charset) {
            var timer = null,
                counter = 0;

            function get() {
                if (counter < 1000) {
                    var data = getData(file, charset);
                    if (data) {
                        window.clearTimeout(timer);
                        registerNamespaceByData(namespace, data);
                        return true;
                    }
                    counter++;
                }
                return false;
            }

            if (!get()) {
                timer = window.setInterval(get, 1);
            }
        },

        /**
         * Регистрирует новую библиотеку модулей по URL или объекту с данными о библиотеке.
         * @name y5.registerNamespaceByData
         * @memberOf y5
         * @function
         * @param {String} namespace Имя, под которым регистрируется библиотека
         * @param {Object | String} data Данные вида: {path: 'http://www.yandex.ru/foo/', query: '?build=1', charset: 'utf-8'}
         * или путь к библиотеке.
         * @param {String} [charset] Кодировка файлов библиотеки
         */
        registerNamespaceByData: registerNamespaceByData,

        /**
         * Возвращает путь к файлам библиотеки.
         * @name y5.namespacePath
         * @memberOf y5
         * @function
         * @param {String} namespace Имя библиотеки
         * @returns {String | null} Путь
         *
         * @example
         * y5.namespacePath("Foo");
         * // -> "http://www.yandex.ru/foo/"
         */
        namespacePath: function(namespace) {
            return getNamespaceItem(namespace, 'path');
        },

        /**
         * Возвращает кодировку файлов библиотеки.
         * @name y5.namespaceCharset
         * @memberOf y5
         * @function
         * @param {String} namespace Имя библиотеки
         * @returns {String | null} Кодировка
         *
         * @example
         * y5.namespaceCharset("Foo");
         * // -> "utf-8"
         */
        namespaceCharset: function(namespace) {
            return getNamespaceItem(namespace, 'charset');
        },

        /**
         * Возвращает URL модуля.
         * @name y5.moduleURL
         * @memberOf y5
         * @function
         * @param {String} module Имя модуля (например, "foo.bar")
         * @param {String} ext Расширение имени файла модуля (по умолчанию - "js")
         * @returns {String} URL модуля
         *
         * @example
         * y5.moduleURL("Dom");
         * // -> [base URL]/Dom.js
         *
         * y5.moduleURL("Widget.Window", "css");
         * // -> [base URL]/Widget/Window.css
         */
        moduleURL: function(module, ext) {
            return getModuleURL(module, ext)[0];
        },

        /**
         * Возвращает каноническое (полное) имя модуля.
         * @name y5.moduleName
         * @memberOf y5
         * @function
         * @param {String} module Имя модуля (например, "foo.bar")
         * @returns {String} Имя модуля
         *
         * @example
         * y5.moduleName("foo.bar");
         * // -> "y5:foo.bar"
         */
        moduleName: getModuleNameCanonical,

        /**
         * Возвращает название библиотеки модуля.
         * @name y5.moduleNamespace
         * @memberOf y5
         * @function
         * @param {String} module Имя модуля (например, "foo.bar")
         * @returns {String} Namespace модуля
         *
         * @example
         * y5.moduleNamespace("foo.bar");
         * // -> "y5"
         */
        moduleNamespace: function(module) {
            return getModuleData(module)[0];
        },

        /**
         * Возвращает объект по имени модуля.
         * @name y5.moduleObject
         * @memberOf y5
         * @function
         * @param {String} module Имя модуля (например, "foo.bar")
         * @returns {Object | null} Объект
         *
         * @example
         * y5.moduleNamespace("foo.bar");
         * // -> y5.foo.bar
         */
        moduleObject: function(module) {
            var obj = window;
            var classes = getModuleNameCanonical(module).split(/[:\.]/g);

            for (var i = 0, l = classes.length; i < l; i++) {
                obj = obj[classes[i]];
                if (typeof obj == y5.UNDEF) {
                    return null;
                }
            }

            return obj;
        },
        
        /**
         * Блокирует загрузку дочерних модулей для указанного модуля или библиотеки.
         * @name y5.blockLoad
         * @memberOf y5
         * @function 
         * @param {String} module Имя модуля (например, "foo.bar") или библиотеки
         * @param {Boolean} withModule Блокировать также загрузку указанного модуля
         * @returns  {Boolean}
         */
        blockLoad: function (module, withModule) {
            // не неймспейс
            if (module.indexOf(':') != (module.length-1)) {
                module = getModuleNameCanonical(module);
                // если блокируем только дочерние модули, то добавляем в конец имени точку
                if (!withModule) {
                    module += '.';
                }
            }
            
            // если модуля нет в списке 
            if (module && BlockModules.indexOf(module) == -1) {
                BlockModules.push(module);
                return true;
            }
            return false;
        },
        
        /**
         * Разблокирует загрузку дочерних модулей для указанного модуля или библиотеки.
         * @name y5.unblockLoad
         * @memberOf y5
         * @function 
         * @param {String} module Имя модуля (например, "foo.bar") или библиотеки
         * @returns {Boolean}
         */
        unblockLoad: function (module) {
            // не неймспейс
            if (module.indexOf(':') != (module.length-1)) {
                module = getModuleNameCanonical(module);
            }
            
            if (module) {
                var index = BlockModules.indexOf(module);
                if (index == -1) {
                    index = BlockModules.indexOf(module + '.');
                }
                if (index != -1) {
                    BlockModules.splice(index, 1);
                    return true;
                }
            }
            return false;
        }
    };

    /**
     * Возвращает данные скрипта по имени файла.
     * @param {String} file Имя файла
     * @param {String} [charset] Кодировка файла (если указана, то переопределяет кодировку, указанную в скрипте)
     * @returns {Object} Объект вида {path: 'http://www.yandex.ru/foo/', query: '?build=1', charset: 'utf-8'}
     * @name getBase
     * @memberOf y5
     * @function
     * @deprecated
     */
    obj.getBase = getData;
    obj.getBaseAndSetAlias = obj.registerNamespace;
    obj.setAlias = registerNamespaceByData;
    obj.constructURL = obj.moduleURL;
    obj.getAlias = obj.moduleNamespace;
    obj.charsets = {};
    obj.setAliasCharset = function() {};

    return obj;

})();

/// Vars
/////////////////////////////////////////////////////////////////////

/**
 * @class Содержит глобальные переменные.
 * @static
 * @name y5.Vars
 */
y5.Vars = {
    DEBUG: false,

    /**
     * Строка 'undefined'.
     * @name y5.Vars.UNDEF
     * @memberOf y5.Vars
     * @constant
     * @type String
     */

    /**
     * @name y5.UNDEF
     * @link y5.Vars.UNDEF
     * @memberOf y5
     * @type String
     */
    UNDEF: 'undefined',

    /**
     * Всегда возвращает значение false.
     * @name y5.Vars.FALSE
     * @memberOf y5.Vars
     * @function
     * @type Function
     * @returns {Boolean} false
     */

    /**
     * @name y5.FALSE
     * @link y5.Vars.FALSE
     * @memberOf y5
     * @type Function
     */
    FALSE: function() { return false; },

    /**
     * Всегда возвращает значение true.
     * @name y5.Vars.TRUE
     * @memberOf y5.Vars
     * @function
     * @type Function
     * @returns {Boolean} true
     */

    /**
     * @name y5.TRUE
     * @link y5.Vars.TRUE
     * @memberOf y5
     * @type Function
     */
    TRUE: function() { return true; },

    /**
     * Всегда возвращает значение null.
     * @name y5.Vars.NULL
     * @memberOf y5.Vars
     * @function
     * @type Function
     * @returns {Object} null
     */

    /**
     * @name y5.NULL
     * @link y5.Vars.NULL
     * @memberOf y5
     * @type Function
     */
    NULL: function() { return null; },

    /**
     * Всегда возвращает значение undefined.
     * @name y5.Vars.VOID
     * @memberOf y5.Vars
     * @function
     * @type Function
     * @returns {undefined}
     */

    /**
     * @name y5.VOID
     * @link y5.Vars.VOID
     * @memberOf y5
     * @type Function
     */
    VOID: function() { }

};

/// Browser
/////////////////////////////////////////////////////////////////////

y5.Browser = {
    get: function(nav) {
        var agt = nav.userAgent.toLowerCase(),
            result = {};

        function getVer(is_this, regex) {
            if (is_this) {
                var f = agt.match(regex);
                return f ? parseFloat(f[1]) : 0;
            }
            return 0;
        }

        function contains(needle) {
            return agt.indexOf(needle) != -1;
        }

        /**
         * Определяет операционную систему пользователя. Возвращает true, если операционная система пользователя - MS Windows.
         * @name y5.Vars.is_win
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_win
         * @link y5.Vars.is_win
         * @memberOf y5
         * @type Boolean
         */
        result.is_win       = contains('windows');

        /**
         * Определяет операционную систему пользователя. Возвращает true, если операционная система пользователя - Mac OS.
         * @name y5.Vars.is_mac
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_mac
         * @link y5.Vars.is_mac
         * @memberOf y5
         * @type Boolean
         */
        result.is_mac       = contains('mac');
        
        /**
         * Определяет операционную систему пользователя. Возвращает true, если операционная система пользователя - GNU/Linux.
         * @name y5.Vars.is_linux
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_linux
         * @link y5.Vars.is_linux
         * @memberOf y5
         * @type Boolean
         */
        result.is_linux       = contains('linux');

        /**
         * Определяет тип браузера. Возвращает true для браузера Safari.
         * @name y5.Vars.is_safari
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_safari
         * @link y5.Vars.is_safari
         * @memberOf y5
         * @type Boolean
         */
        result.is_safari    = contains('safari');

        /**
         * Определяет тип браузера. Возвращает true для браузера Safari на iPhone и iPod Touch.
         * @name y5.Vars.is_iphone
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_iphone
         * @link y5.Vars.is_iphone
         * @memberOf y5
         * @type Boolean
         */
        result.is_iphone    = result.is_safari && contains('iphone');

        /**
         * Определяет тип браузера. Возвращает true для браузера Opera.
         * @name y5.Vars.is_opera
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_opera
         * @link y5.Vars.is_opera
         * @memberOf y5
         * @type Boolean
         */
        result.is_opera     = contains('opera');

        /**
         * Определяет тип браузера. Возвращает true для браузера Konqueror.
         * @name y5.Vars.is_konq
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_konq
         * @link y5.Vars.is_konq
         * @memberOf y5
         * @type Boolean
         */
        result.is_konq      = contains('konqueror');

        /**
         * Определяет тип браузера. Возвращает true для браузеров семейства MS Internet Explorer.
         * @name y5.Vars.is_ie
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie
         * @link y5.Vars.is_ie
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie        = !result.is_opera && contains('msie');

        /**
         * Определяет тип движка браузера. Возвращает true для браузеров, основанных на KHTML.
         * @name y5.Vars.is_khtml
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_khtml
         * @link y5.Vars.is_khtml
         * @memberOf y5
         * @type Boolean
         */
        result.is_khtml     = !result.is_safari && contains('khtml');

        /**
         * Определяет тип движка браузера. Возвращает true, если в браузере используется движок Gecko.
         * @name y5.Vars.is_gecko
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_gecko
         * @link y5.Vars.is_gecko
         * @memberOf y5
         * @type Boolean
         */
        result.is_gecko     = contains('gecko/');

        /**
         * Определяет версию браузера семейства MS Internet Explorer. Возвращает номер версии.
         * @name y5.Vars.ie_ver
         * @memberOf y5.Vars
         * @constant
         * @type Number
         */

        /**
         * @name y5.ie_ver
         * @link y5.Vars.ie_ver
         * @memberOf y5
         * @type Number
         */
        result.ie_ver       = getVer(result.is_ie, /msie (\d+\.\d)/);

        /**
         * Определяет версию движка рендеринга страниц для браузеров, основанных на коде проекта Gecko. Возвращает номер версии.
         * @name y5.Vars.gecko_ver
         * @memberOf y5.Vars
         * @constant
         * @type Number
         */

        /**
         * @name y5.gecko_ver
         * @link y5.Vars.gecko_ver
         * @memberOf y5
         * @type Number
         */
        result.gecko_ver    = getVer(result.is_gecko, /rv:(\d+\.\d)/);

        /**
         * Определяет версию браузера Opera. Возвращает номер версии.
         * @name y5.Vars.opera_ver
         * @memberOf y5.Vars
         * @constant
         * @type Number
         */

        /**
         * @name y5.opera_ver
         * @link y5.Vars.opera_ver
         * @memberOf y5
         * @type Number
         */
        result.opera_ver    = getVer(result.is_opera, /opera[\/ ](\d+\.\d)/);

        /**
         * Определяет версию браузера Safari. Возвращает номер версии.
         * @name y5.Vars.safari_ver
         * @memberOf y5.Vars
         * @constant
         * @type Number
         */

        /**
         * @name y5.safari_ver
         * @link y5.Vars.safari_ver
         * @memberOf y5
         * @type Number
         */
        result.safari_ver   = getVer(result.is_safari, /safari\/(\d+)/);

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 5.0.
         * @name y5.Vars.is_ie5
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie5
         * @link y5.Vars.is_ie5
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie5       = result.ie_ver == 5.0;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 5.5.
         * @name y5.Vars.is_ie55
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie55
         * @link y5.Vars.is_ie55
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie55      = result.ie_ver == 5.5;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 5.0 и последующих версий.
         * @name y5.Vars.is_ie5up
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie5up
         * @link y5.Vars.is_ie5up
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie5up     = result.ie_ver > 4.9;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 5.5 и последующих версий.
         * @name y5.Vars.is_ie55up
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie55up
         * @link y5.Vars.is_ie55up
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie55up    = result.ie_ver > 5.4;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 6.0 и последующих версий.
         * @name y5.Vars.is_ie6up
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie6up
         * @link y5.Vars.is_ie6up
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie6up     = result.ie_ver > 5.9;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 7.0 и последующих версий.
         * @name y5.Vars.is_ie7up
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie7up
         * @link y5.Vars.is_ie7up
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie7up     = result.ie_ver > 6.9;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии ниже 6.0 (не включая 6.0).
         * @name y5.Vars.is_ie6down
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie6down
         * @link y5.Vars.is_ie6down
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie6down   = result.is_ie && result.ie_ver < 6.0;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии ниже 7.0 (не включая 7.0).
         * @name y5.Vars.is_ie7down
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie7down
         * @link y5.Vars.is_ie7down
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie7down   = result.is_ie && result.ie_ver < 7.0;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии ниже 8.0 (не включая 8.0).
         * @name y5.Vars.is_ie8down
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie7down
         * @link y5.Vars.is_ie8down
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie8down = result.is_ie && result.ie_ver < 8.0;

        /**
         * Определяет версию браузера. Возвращает true для браузеров MS Internet Explorer версии 8.
         * @name y5.Vars.is_ie8
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.is_ie55
         * @link y5.Vars.is_ie8
         * @memberOf y5
         * @type Boolean
         */
        result.is_ie8 = result.is_ie && result.ie_ver == 8;


        /**
         * Определяет доступность в браузере cookie. Устанавливается в true, если доступ к cookie возможен,  или в false, если cookie запрещены.
         * @name y5.Vars.cookieEnabled
         * @memberOf y5.Vars
         * @constant
         * @type Boolean
         */

        /**
         * @name y5.cookieEnabled
         * @link y5.Vars.cookieEnabled
         * @memberOf y5
         * @type Boolean
         */
        result.cookieEnabled = nav.cookieEnabled;

        return result;
    }
};

/// Копируем переменные браузера в Vars, затем в y5
(function () {
    var i;

    // Определение браузера
    var browser = y5.Browser.get(window.navigator);
    for (i in browser) {
        y5.Vars[i] = browser[i];
    }

    // Копируем переменные в y5
    for (i in y5.Vars) {
        y5[i] = y5.Vars[i];
    }

})();

/// Loader
/////////////////////////////////////////////////////////////////////

/**
 * @class Набор функций для загрузки дополнительных JavaScript-файлов и CSS-стилей на страницу.
 * @static
 * @name y5.Loader
 */
y5.Loader = (function() {

    function setElementAttributes(element, attributes) {
        for (var type in attributes) {
            var value = attributes[type];
            if (value) {
                element.setAttribute(type, value);
            }
        }
    }

    function insertElement(container, element) {
        container.insertBefore(element, container.firstChild);
    }

    var loadObject;
    if (y5.is_opera && y5.opera_ver < 8) {

        loadObject = function(name, attributes) {
            if (!document.body) {
                return null;
            }

            var span = document.createElement('span');
            span.style.display = 'none';
            span.innerHTML = '<' + name + '><\/' + name + '>';

            var element = span.getElementsByTagName(name).item(0);
            setElementAttributes(element, attributes);
            insertElement(document.body, span);

            return element;
        }

    } else {

        var head = document.getElementsByTagName('head')[0];

        loadObject = function(name, attributes) {
            var element = document.createElement(name);
            setElementAttributes(element, attributes);
            insertElement(head, element);

            return element;
        };

    }

    function createScript(attributes, callback) {
        var tries = 10, timer = null;

        function tryCreateScript() {
            if (!--tries) {
                window.clearTimeout(timer);
                return false;
            }

            var script = loadObject('script', attributes);

            if (script) {
                if (typeof callback == 'function') {
                    callback(script);
                }
                window.clearTimeout(timer);
                return true;
            }

            return false;
        }

        if (!tryCreateScript()) {
            timer = window.setInterval(tryCreateScript, 10);
        }
    }

    return {
        /**
         * Загружает файл, содержащий JavaScript
         * @name y5.Loader.loadScript
         * @memberOf y5.Loader
         * @function
         * @param {String} src Путь к скрипту
         * @param {String} [charset] Кодировка скрипта
         * @param {String} [callback] Функция, выполняемая по окончании загрузки
         * @param {String} [id] ID создаваемого элемента DOM
         */
        loadScript: function(src, charset, callback, id) {
            createScript({src: src, charset: charset, type: 'text/javascript', id: id}, callback);
        },

        /**
         * Загружает объект.
         * @name y5.Loader.loadObject
         * @memberOf y5.Loader
         * @param {String} name Имя элемента
         * @param {String} attributes Список атрибутов
         * @private
         *
         * @example
         * y5.Loader.loadObject('script', {src: 'script.js'});
         * // -> &lt;script src="script.js">&lt;/script>
         */
        loadObject: loadObject
    };

})();

/// Scripts
/////////////////////////////////////////////////////////////////////

/**
 * Создает элемент script.
 * @class
 * @static
 * @deprecated y5.Loader
 */
y5.Scripts = {
    /**
     * Создает элемент script.
     * @memberOf y5.Scripts
     * @deprecated y5.Loader.loadScript
     */
    createScript: function(src, charset, callback) {
        y5.Loader.loadScript(src, charset, callback);
    }
};

/// Console
/////////////////////////////////////////////////////////////////////

(function() {

var VOID = y5.VOID;

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

})();

/// Exception
/////////////////////////////////////////////////////////////////////

/**
 * Создает экземпляр исключения
 * @class Класс Exception
 * @name y5.Exception
 * @param {String} message Сообщение
 * @param {String} method Название метода
 * @param {String} module Название модуля
 * @example
 * try {
 *     // JavaScript code
 * } catch (e) {
 *     throw new y5.Exception(e, 'myMethod', 'myModule');
 * }
 */
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();
(function() {

var UNDEF = y5.UNDEF;

function checkDomNode(obj, type) {
    return (obj && obj.nodeType && obj.nodeType == type) || false;
}

/**
 * @class Модуль для работы с типами объектов. Содержит определения констант, соответствующих разным типам объектов, и набор функций для выяснения типа объекта.
 * @name y5.Types
 * @static
 */
y5.Types = {
    /// consts

    // standard

    /**
     * Константа, обозначающая тип «неопределенный». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.UNDEF
     * @memberOf y5.Types
     * @constant
     * @link y5.Types.UNDEFINED
     * @type Number
     */
    UNDEF:          1 << 0,

    /**
     * Константа, обозначающая тип «неопределенный». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.UNDEFINED
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    UNDEFINED:      1 << 0,

    /**
     * Константа, обозначающая тип «объект». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.OBJECT
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    OBJECT:         1 << 1,

    /**
     * Константа, обозначающая тип «функция». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.FUNCTION
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    FUNCTION:       1 << 2,

    /**
     * Константа, обозначающая тип «число». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.NUMBER
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    NUMBER:         1 << 3,

    /**
     * Константа, обозначающая тип «строка». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.STRING
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    STRING:         1 << 4,

    /**
     * Константа, обозначающая «логический» тип. Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.BOOLEAN
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    BOOLEAN:        1 << 5,

    // user
    /**
     * Константа, обозначающая тип «дата». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.DATE
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    DATE:           1 << 10,

    /**
     * Константа, обозначающая тип «регулярное выражение». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.REGEXP
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    REGEXP:         1 << 11,

    /**
     * Константа, обозначающая тип «массив». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.ARRAY
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    ARRAY:          1 << 12,

    /**
     * Тип null.
     * @name y5.Types.NULL
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    NULL:           1 << 13,

    /**
     * Константа, обозначающая тип «событие». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.EVENT
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    EVENT:          1 << 14,

    /**
     * Константа, обозначающая тип «узел документа». Возвращает цифровой код, соответствующий данному типу.
     * @name y5.Types.NODE
     * @memberOf y5.Types
     * @constant
     * @type Number
     */
    NODE:           1 << 15,

    TYPES: {
        'undefined': 1 << 0,
        'object':    1 << 1,
        'function':  1 << 2,
        'number':    1 << 3,
        'string':    1 << 4,
        'boolean':   1 << 5
    },

    /// methods

    /**
     * Возвращает числовой код типа объекта – одну из констант y5.Types.
     * @name y5.Types.type
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект
     * @returns {Number} Константа y5.Types
     *
     * @example
     * y5.Types.type(1);
     * // -> y5.Types.NUMBER
     *
     * y5.Types.type(new Date);
     * // -> y5.Types.DATE
     *
     * y5.Types.type([1, 2, 3]);
     * // -> y5.Types.ARRAY
     *
     * y5.Types.type(document);
     * // -> y5.Types.NODE
     */
    type: function(obj) {
        var type = this.TYPES[typeof obj];

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

        // DOM NODE
        if (type == this.OBJECT) {
            if (obj.nodeName || this.document(obj) /* IE 5.0 */) {
                return this.NODE;
            }
        }

        // ARRAY, REGEXP, DATE
        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;
            }
        }

        // EVENT
        if (this.event(obj)) {
            return this.EVENT;
        }

        return type;
    },

    /**
     * Проверяет, является ли тип объекта одним из типов, заданных параметром mask. В случае успеха проверки возвращает true.
     * @name y5.Types.test
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @param {Number} mask Маска для проверки (задается в виде TYPE1 | TYPE2 | ...)
     * @returns {Boolean} Результат проверки
     *
     * @example
     * // проверить, что тип объекта Date или RegExp
     * y5.Types.test(obj, y5.Types.DATE | y5.Types.REGEXP);
     */
    test: function(obj, mask) {
        return !!(this.type(obj) & mask);
    },

    // standard

    /**
     * Проверяет, является ли объект определенным. В случае успеха проверки возвращает true.
     * @name y5.Types.def
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    def: function(obj) {
        return typeof obj != UNDEF;
    },

    /**
     * Проверяет, является ли объект неопределенным. В случае успеха проверки возвращает true.
     * @name y5.Types.undef
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    undef: function(obj) {
        return typeof obj == UNDEF;
    },

    /**
     * Проверяет, принадлежит ли объект к типу Object. В случае успеха проверки возвращает true.
     * @name y5.Types.object
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    object: function(obj) {
        return typeof obj == 'object';
    },

    /**
     * Проверяет, является ли объект функцией. В случае успеха проверки возвращает true.
     * @name y5.Types.func
     * @memberOf y5.Types
     * @function
     * @param {Object} object Рбъект для проверки
     * @returns {Boolean} Результат проверки
     */
    func: function(obj) {
        return typeof obj == 'function';
    },

    /**
     * Проверяет, является ли объект числом. В случае успеха проверки возвращает true.
     * @name y5.Types.number
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    number: function(obj) {
        return typeof obj == 'number';
    },

    /**
     * Проверяет, является ли объект строкой. В случае успеха проверки возвращает true.
     * @name y5.Types.string
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    string: function(obj) {
        return typeof obj == 'string';
    },

    /**
     * Проверяет, принадлежит ли объект к типу Boolean. В случае успеха проверки возвращает true.
     * @name y5.Types.bool
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    bool: function(obj) {
        return typeof obj == 'boolean';
    },

    /**
     * Проверяет, является ли объект null. В случае успеха проверки возвращает true.
     * @name y5.Types.nul
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    nul: function(obj) {
        return obj === null;
    },

    // user

    /**
     * Проверяет, является ли объект массивом. В случае успеха проверки возвращает true.
     * @name y5.Types.array
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    array: function(obj) {
        return obj instanceof Array;
    },

    /**
     * Проверяет, является ли объект регулярным выражением. В случае успеха проверки возвращает true.
     * @name y5.Types.regexp
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    regexp: function(obj) {
        return obj instanceof RegExp;
    },

    /**
     * Проверяет, является ли объект датой. В случае успеха проверки возвращает true.
     * @name y5.Types.date
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    date: function(obj) {
        return obj instanceof Date;
    },

    /**
     * Проверяет, является ли объект событием. В случае успеха проверки возвращает true.
     * @name y5.Types.event
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    event: function(obj) {
        // Правильно: obj instanceof Event
        // Для IE
        return obj && typeof obj.type != UNDEF && typeof (obj.stopPropagation || obj.cancelBubble) != UNDEF;
    },

    // dom

    /**
     * Проверяет, является ли объект элементом DOM. В случае успеха проверки возвращает true.
     * @name y5.Types.element
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    element: function(obj) {
        return checkDomNode(obj, Node.ELEMENT_NODE);
    },

    /**
     * Проверяет, является ли объект атрибутом DOM. В случае успеха проверки возвращает true.
     * @name y5.Types.attribute
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    attribute: function(obj) {
        return checkDomNode(obj, Node.ATTRIBUTE_NODE);
    },

    /**
     * Проверяет, является ли объект текстовым узлом DOM. В случае успеха проверки возвращает true.
     * @name y5.Types.text
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    text: function(obj) {
        return checkDomNode(obj, Node.TEXT_NODE);
    },

    /**
     * Проверяет, является ли объект документом. В случае успеха проверки возвращает true.
     * @name y5.Types.document
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    document: function(obj) {
        // для старых браузеров IE 5.0
        return (obj && typeof obj.documentElement != UNDEF) || false;

        // правильно
        // return checkDomNode(obj, Node.DOCUMENT_NODE);
    },

    /**
     * Проверяет, является ли объект узлом комментария. В случае успеха проверки возвращает true.
     * @name y5.Types.comment
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    comment: function(obj) {
        return checkDomNode(obj, Node.COMMENT_NODE);
    },

    /**
     * Проверяет, является ли объект узлом DOM. В случае успеха проверки возвращает true.
     * @name y5.Types.node
     * @memberOf y5.Types
     * @function
     * @param {Object} object Объект для проверки
     * @returns {Boolean} Результат проверки
     */
    node: function(obj) {
        return (obj && typeof obj.nodeType != UNDEF);
    }
};

})();/**
 * @class Модуль для реализации механизма &laquo;сборки мусора&raquo; &mdash; освобождения памяти от отработавших объектов, которые больше не будут востребованы приложением.
 * @name y5.GC
 * @static
 */
y5.GC = {
    /**
     * Массив объектов предназначенныз для удаления
     * @type Array
     * @name y5.GC.data
     * @memberOf y5.GC
     * @private
     */
    data: [],

    /**
     * Регистрирует объект в сборщике мусора. Возвращает зарегистрированный объект.
     * @name y5.GC.collect
     * @memberOf y5.GC
     * @function
     * @param {Object} obj объект
     * @returns {Object} сам объект
     */
    collect: function(obj) {
        this.data.push(obj);
        return obj;
    },

    /**
     * Удаляет объект из коллекции сборщика мусора.
     * @name y5.GC.remove
     * @memberOf y5.GC
     * @function
     * @param {Object} obj объект
     */
    remove: function(obj) {
        var i = this.data.indexOf(obj);
        if (i != -1) {
            this.destruct(i);
            this.data.splice(i, 1);
        }
    },

    /**
     * Вызывает деструктор сохраненного объекта.
     * @name y5.GC.destruct
     * @memberOf y5.GC
     * @function
     * @private
     */
    destruct: function(i) {
        var obj = this.data[i];
        if (obj) {
            if (typeof obj.cleanup == 'function') {
                obj.cleanup();
            } else if (typeof obj.destruct == 'function') {
                obj.destruct();
            }
        }
        this.data[i] = null;
    },

    /**
     * Удаляет все сохраненные объекты.
     * @name y5.GC.cleanup
     * @memberOf y5.GC
     * @function
     * @private
     */
    cleanup: function() {
        for (var i = this.data.length - 1; i >= 0; i--) {
            this.destruct(i);
        }
        this.data.length = 0;

        if (y5.is_ie && CollectGarbage) {
            CollectGarbage();
        }
    }
};
(function() {

var Types = y5.Types,
    UNDEF = y5.UNDEF,
    GC    = y5.GC;

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

/// Обрабатываем не-DOM типы событий
var typeDOMAttrModified = 'DOMAttrModified',
    typePropertyChange = 'propertychange',
    typeDOMMouseScroll = 'DOMMouseScroll',
    typeMouseWheel = 'mousewheel',
    specificTypes = {};

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;
}

/////////////////////////////////////////////////////////////////////

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

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

if (y5.is_ie) {

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

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

    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 doc = document.documentElement,
            body = document.body;
        e.pageX = e.clientX + (doc.scrollLeft || body.scrollLeft);
        e.pageY = e.clientY + (doc.scrollTop || body.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) {

    mouseButtonsListener = {L: [0, 65535, 1 /* safari.ver <= 420 */], M: [2], R: [3]};

    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) {
        mouseButtonsListener = {L: [1], M: [2], R: [3]};
    }

} else {

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

if (y5.is_ie || y5.is_konq) {
    mouseButtonsListener = {L: [1], M: [4], R: [2]};
}

function normalizeEvent(e) {
    var button;
    try {
        // ff1 на этот момент говорит [Exception... "Illegal operation on WrappedNative prototype object"  nsresult: "0x8057000c (NS_ERROR_XPC_BAD_OP_ON_WN_PROTO)" . Поэтому оборачиваем в try
        // видимо у MutationEvent нельзя обращаться к каким-то свойствам
        button = e.button;
    } catch (e) {}
    if (typeof button != UNDEF) {
        e.buttonL = mouseButtonsListener.L.indexOf(button) != -1;
        e.buttonM = mouseButtonsListener.M.indexOf(button) != -1;
        e.buttonR = mouseButtonsListener.R.indexOf(button) != -1;
    } else {
        e.buttonL = e.buttonM = e.buttonR = false;
    }

    normalizeEventSpecific(e);

    return e;
}

/////////////////////////////////////////////////////////////////////

/**
 * Создает обработчик DOM-события.
 * @constructor
 * @class Обеспечивает поддержку работы с событиями. Позволяет устанавливать обработчики на различные события браузера и DOM.
 * @name y5.AEventListener
 * @param {String} type Тип события: click, load...
 * @param {Function} listener Callback-функция на возникновение события
 * @param {Element} element Объект, на котором возникает событие
 * @param {Boolean} add Начать "слушать" событие немедленно
 * @param {Object} context Контекст, в котором выполняется callback. По-умолчанию, element
 * @param {Boolean} runOnce Обработать событие один раз и удалить обработчик. По умолчанию, false.
 *
 * @example
 * new y5.AEventListener("click", callback, link, true);
 */
y5.AEventListener = function(type, listener, element, add, context, runOnce) {
    this.type = normalizeType(type);
    this.element = element || document;
    this.blocked = false;
    this.added = false;
    this.runOnce = runOnce || false;
    
    var _this = this;
    this.listener = function(evt) {
        var e = typeof evt != UNDEF ? normalizeEvent(evt) : {};

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

        execListenerWithContext(listener, context, e, _this.element);
        if (_this.runOnce) {
            _this.cleanup();
        }
    };

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

    GC.collect(this);
};

y5.AEventListener.prototype = {
    /**
     * Включает отслеживание установленного ранее события.
     * @name y5.AEventListener.add
     * @memberOf y5.AEventListener
     * @function
     */
    add: function() {
        if (this.added) return;

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

    /**
     * Добавляет обработчик к элементу
     * @name y5.AEventListener._add
     * @memberOf y5.AEventListener
     * @function
     * @private
     */
    _add: function() {
        this.element.addEventListener(this.type, this.listener, false);
    },

    /**
     * Выключает отслеживание события.
     * @name y5.AEventListener.remove
     * @memberOf y5.AEventListener
     * @function
     */
    remove: function() {
        if (!this.added) return;

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

    /**
     * Удаляет обработчик с элемента
     * @name y5.AEventListener._remove
     * @memberOf y5.AEventListener
     * @function
     * @private
     */
    _remove: function() {
        this.element.removeEventListener(this.type, this.listener, false);
    },

    /**
     * Блокирует событие. Перестает выполняться установленный обработчик события и отменяется стандартная реакция интерфейсных элементов на данное событие.
     * @name y5.AEventListener.block
     * @memberOf y5.AEventListener
     * @function
     */
    block: function() {
        this.blocked = true;
    },

    /**
     * Разблокирует событие. Установленный обработчик события начинает выполняться снова и восстанавливается стандартная реакция интерфейсных элементов на данное событие.
     * @name y5.AEventListener.unblock
     * @memberOf y5.AEventListener
     * @function
     */
    unblock: function() {
        this.blocked = false;
    },

    /**
     * Удаляет обработчик события, ссылки на элемент и callback-функцию.
     * @name y5.AEventListener.cleanup
     * @memberOf y5.AEventListener
     * @function
     * @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: /./
    },

    // Коды клавиш мыши
    mouseButtonsEvent = [0, 1, 2];

if (y5.is_safari) {
    mouseButtonsEvent = [0, 2, 3];
}

if (y5.is_safari || y5.is_opera) {
    // Создание KeyEvents вызывает исключение
    // Safari 3.0.4 (523.12.9)
    // Opera 9.5b1
    delete eventTypes['Key'];
}

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

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

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

    GC.collect(this);
};

y5.Event.prototype = {
    /**
     * Инициализация.
     * @name y5.Event.init
     * @memberOf y5.Event
     * @function
     * @private
     */
    init: function() {
        for (var type in eventTypes) {
            if (eventTypes[type].test(this.type)) {
                this.eventType = type;
                break;
            }
        }
    },

    /**
     * Инициирует возникновение подготовленного ранее события.
     * @name y5.Event.dispatch
     * @memberOf y5.Event
     * @function
     * @param {Object} params параметры
     *
     * @example
     * var E = new y5.Event("click", link, false);
     * // ...
     * E.dispatch();
     */
    dispatch: function(params, canBubble, cancelable) {
        if (typeof params == UNDEF) {
            params = this.params;
        }

        canBubble = canBubble || true;
        cancelable = cancelable || true;

        // create
        var event = document.createEvent(this.eventType + 'Events');

        // init (по типу события)
        switch (this.eventType) {
            case 'Mouse':
                event.initMouseEvent(
                    this.type,
                    canBubble,
                    cancelable,
                    document.defaultView,
                    params.detail    || 0,
                    params.screenX   || 0,
                    params.screenY   || 0,
                    params.clientX   || 0,
                    params.clientY   || 0,
                    params.ctrlKey   || false,
                    params.altKey    || false,
                    params.shiftKey  || false,
                    params.metaKey   || false,
                    mouseButtonsEvent[params.button || 0 /* default left */],
                    null
                );
                break;

            case 'Key':
                event.initKeyEvent(
                    this.type,
                    canBubble,
                    cancelable,
                    document.defaultView,
                    params.ctrlKey   || false,
                    params.altKey    || false,
                    params.shiftKey  || false,
                    params.metaKey   || false,
                    params.keyCode   || 0,
                    params.charCode  || 0
                );
                break;

            default:
                event.initEvent(this.type, canBubble, cancelable);
                break;
        }

        // dispatch
        return this.element.dispatchEvent(event);
    },

    /**
     * Удаляет ссылку на элемент
     * @name y5.Event.cleanup
     * @memberOf y5.Event
     * @function
     * @private
     */
    cleanup: function() {
        this.element = null;
    }
};

var Event = y5.Event;

if (document.createEventObject) { // IE

    mouseButtonsEvent = [1, 4, 2];

    function setEvent(event, params) {
        for (var name in params) {
            try {
                var value;
                switch (name) {
                    case 'button':
                        value = mouseButtonsEvent[params.button || 0 /* default left */];
                        break;

                    default:
                        value = params[name];
                        break;
                }
                event[name] = value;
            } catch(e) {}
        }
        return event;
    }

    Event.prototype.init = y5.NULL;

    Event.prototype.dispatch = function(params) {
        if (typeof params == UNDEF) {
            params = this.params;
        }

        var event = setEvent(document.createEventObject(), params);
        return this.element.fireEvent('on' + this.type, event);
    };

} else if (!document.createEvent) {

    Event.prototype.init = y5.NULL;

    Event.prototype.dispatch = function(params) {
        try {
            return this.element[this.type](params);
        } catch (e) {
            y5.Console.warn('Browser is too old', ['Event']);
            return false;
        }
    };
}

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


/// Observer & Notifier
/////////////////////////////////////////////////////////////////////
(function() {

var ObserverManager = {
    // список постоянных событий
    notifiers: {},
    observers: {},

    dispatchNotify: function(id, params, permanent) {
        var result = true,
            observers = this.observers[id];

        if (observers) {
            // надо делать копию массива обработчиков, чтобы при удалении не ехали индексы и не было пропусков.
            var cloneObservers = [].concat(observers),
                i = 0,
                length = cloneObservers.length;
                
            // проходимся по массиву и вызываем обработчики
            for (; i<length; i++) {
                if (!cloneObservers[i].added) continue;
                result &= this.runListener(cloneObservers[i], params);
                // если обработчик одноразовый, то вызываем cleanup
                // непосредственно из массива удалять обработчик по его индексу (i) нельзя, т.к. если до этого уже был кто-то удален, он не совпадет  
                if (cloneObservers[i].runOnce) {
                    cloneObservers[i].cleanup();
                }
            }
        }

        if (permanent) {
            var notifiers = this.notifiers;
            if (!notifiers[id]) {
                notifiers[id] = [];
            }
            notifiers[id].push(params);
        }

        return result;
    },

    runListener: function(observer, params) {
        var result = true;

        result &= execListenerWithContext(observer.listener, observer.context, params);
        y5.Console.log('Observer listener: ', observer, ['Notifier']);

        return result;
    },

    addObserver: function(observer) {
        var id = observer.id,
            observers = this.observers;

        if (!observers[id]) {
            observers[id] = [];
        }
        observers[id].push(observer);

        // запускаем выполнение для "постоянных" событий
        this.fireNotify(observer);
    },

    removeObserver: function(observer) {
        var observers = this.observers[observer.id],
            index;

        if (observers) {
            for (var i=0, length = observers.length; i < length; i++) {
                if (observers[i] === observer) {
                    observers.splice(i, 1);
                    break;
                }
            }
        }
    },
    
    fireNotify: function(observer) {
        var notifiers = this.notifiers[observer.id];

        if (notifiers) {
            for (var i = 0, l = notifiers.length; i < l; i++) {
                this.runListener(observer, notifiers[i]);
            }
        }
    },

    generateId: function(type, element) {
        var id = element;
        if (!element || typeof element == 'object') {
            id = y5.Utils.getUniqueId(element || y5);
        }
        return type + '_' + id;
    }
};

/**
 * Создает объект-слушатель, реагирующий на пользовательское событие заданного типа.
 * @class Класс для реализации совместно с классом y5.Notifier механизма пользовательских событий. Позволяет расширить стандартный набор событий браузера новыми, проблемно-ориентированными типами. Класс y5.Observer отвечает за создание слушателей — объектов, отслеживающих возникновение пользовательских событий.
 * @constructor
 * @name y5.Observer
 * @param {String} type Тип события
 * @param {Function} listener Callback-функция
 * @param {Object} object Объект, на котором возникает событие
 * @param {Boolean} add Начать слушать немедленно
 * @param {Object} context Контекст выполнения функции listener
 * @param {Boolean} runOnce Обработать событие один раз и удалить обработчик. По умолчанию, false.
 *
 * @example
 * new y5.Observer("customEvent", callback, object, true);
 */
y5.Observer = function(type, listener, element, add, context, runOnce) {
    this.id = ObserverManager.generateId(type, element);
    this.added = false;
    this.listener = listener;
    this.context = context;
    this.runOnce = runOnce || false;
    
    if (add) {
        this.add();
    }

    GC.collect(this);
};

y5.Observer.prototype = {
    /**
     * Включает (активирует) объект-слушатель.
     * @name y5.Observer.add
     * @memberOf y5.Observer
     * @function
     */
    add: function() {
        if (!this.added) {
            ObserverManager.addObserver(this);
            this.added = true;

            y5.Console.log('Observer added: ', this, ['Observer']);
        }
    },

    /**
     * Выключает (деактивирует) объект-слушатель.
     * @name y5.Observer.remove
     * @memberOf y5.Observer
     * @function
     */
    remove: function() {
        if (this.added) {
            ObserverManager.removeObserver(this);
            this.added = false;

            y5.Console.log('Observer removed: ', this, ['Observer']);
        }
    },

    /**
     * Удаляет обработчик события, ссылки на элемент и callback-функцию.
     * @name y5.Observer.cleanup
     * @memberOf y5.Observer
     * @function
     * @private
     */
    cleanup: function() {
        this.remove();
        this.context = null;
        this.listener = null;
    }
};

var ObserverProto = y5.Observer.prototype;

/**
 * @memberOf y5.Observer
 * @name y5.Observer.start
 * @function
 * @deprecated y5.Observer.add
 */
ObserverProto.start = ObserverProto.add;

/**
 * @memberOf y5.Observer
 * @name y5.Observer.stop
 * @function
 * @deprecated y5.Observer.remove
 */
ObserverProto.stop = ObserverProto.remove;

/**
 * Подготовливает пользовательское событие и, если параметр dispatch равен true, инициирует его возникновение.
 * @class Класс для реализации совместно с классом y5.Observer механизма пользовательских событий. Позволяет расширить стандартный набор событий браузера новыми, проблемно-ориентированными типами. Класс y5.Notifier отвечает за инициализацию событий и отправку сообщений объектам-слушателям, созданным при помощи класса y5.Observer.
 * @constructor
 * @name y5.Notifier
 * @param {String} type Тип события
 * @param {Object} object Объект, на котором возникает событие
 * @param {Boolean} [dispatch] Послать сообщение немедленно (true - по умолчанию)
 * @param {Object} [params] Параметры сообщения
 *
 * @example
 * new y5.Notifier("customEvent", object, true, {foo: 'bar'});
 */
y5.Notifier = function(type, element, dispatch, params) {
    this.id = ObserverManager.generateId(type, element);
    this.params = params;

    y5.Console.log('Notifier new: ', this, ['Notifier']);

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

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

y5.Notifier.prototype = {
    /**
     * Инициирует возникновение события - отправляет сообщение. Возвращает true, если общий результат выполнения функций объекта-слушателя равен true.
     * @name y5.Notifier.dispatch
     * @memberOf y5.Notifier
     * @function
     * @param {Object} [params] Параметры, передаваемые функции слушателя
     * @param {Boolean} [permanent] "Постоянное" событие
     * @returns {Boolean} Общий результат выполнения функций слушателя
     *
     * @example
     * var notify = new y5.Notifier("customEvent", object, false, {foo: 'bar'});
     *
     * // передать слушателю новые параметры
     * notify.dispatch({foo: 'yadda'});
     *
     * // передать слушателю параметры заданные при создании
     * notify.dispatch();
     */
    dispatch: function(params, permanent) {
        if (typeof params == UNDEF) {
            params = this.params;
        }

        y5.Console.group('Notifier dispatch: ', [this, params], ['Notifier']);
        var result = ObserverManager.dispatchNotify(this.id, params, permanent);
        y5.Console.groupEnd();

        return result;
    }
};

/**
 * Инициирует возникновение пользовательского события. Возвращает true, если общий результат выполнения функций объекта-слушателя равен true.
 * @class Класс для создания пользовательского события. Упрощенный вариант y5.Notifiler.
 * @name y5.Notify
 * @param {String} type Тип события
 * @param {Object} object Объект, на котором возникает событие
 * @param {Object} [params] Параметры, передаваемые функции слушателя
 * @param {Boolean} [permanent] "Постоянное" событие
 * @returns {Boolean} Общий результат выполнения функций слушателя
 *
 * @example
 * y5.Notify("customEvent", object, {foo: 'bar'});
 */
y5.Notify = function(type, element, params, permanent) {
    y5.Console.group('Notifier dispatch: ', [this, params], ['Notifier']);
    var id = ObserverManager.generateId(type, element);
    var result = ObserverManager.dispatchNotify(id, params, permanent);
    y5.Console.groupEnd();

    return result;
};

})();

/// Events Factory
/////////////////////////////////////////////////////////////////////

/**
 * Создает один объект-слушатель.
 * @function
 * @private
 */
function observeEvent (type, listener, element, add, context, runOnce) {
    var obj = isCustom(type) ? y5.Observer : AEventListener;
    return new obj(type, listener, element, add, context, runOnce);
}

function isCustom (type) {
    return type.indexOf(':') != -1;
}

/**
 * @class Набор функций для инициализации стандартных событий браузера, изменения их свойств, а также для создания слушателей — объектов, отслеживающих возникновение данных событий.
 * @static
 * @name y5.Events
 */
y5.Events = {
    /**
     * Создает один или несколько объектов-слушателей.
     * @name y5.Events.observe
     * @memberOf y5.Events
     * @function
     * @param {String | Array} types Тип события: click, load...
     * @param {Function} listener Callback-функция на возникновение события
     * @param {Element | Object} element Объект, на котором возникает событие
     * @param {Boolean} [add] Начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] Контекст выполнения функции listener
     * @param {Boolean} [runOnce] Обработать событие один раз и удалить обработчик. По умолчанию, false.
     * @returns {y5.AEventListener | y5.Observer | Array}
     *
     * @example
     * y5.Events.observe("click", callback, element, true);
     */
    observe: function(types, listener, element, add, context, runOnce) {
        if (!element) {
            return {add: y5.NULL, remove: y5.NULL};
        }

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

                for (; i < length; i++) {
                    events[i] = observeEvent(types[i], listener, element, add, context, runOnce);
                }
                
                return events;

            case Types.STRING:
                return observeEvent(types, listener, element, add, context, runOnce);
        }

        return null;
    },
    
    /**
     * Создает один или несколько одноразовых объектов-слушателей, обработчики удаляются после наступления события.
     * @name y5.Events.observeOnce
     * @memberOf y5.Events
     * @function
     * @param {String | Array} types Тип события: click, load...
     * @param {Function} listener Callback-функция на возникновение события
     * @param {Element | Object} element Объект, на котором возникает событие
     * @param {Boolean} [add] Начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] Контекст выполнения функции listener
     * @returns {y5.AEventListener | y5.Observer | Array}
     *
     * @example
     * y5.Events.observe("click", callback, element, true);
     */
    observeOnce: function (types, listener, element, add, context) {
        return this.observe(types, listener, element, add, context, true);
    },

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

    /**
     * Создает объект-слушатель, реагирующий на изменение определенного свойства элемента (например, на изменение содержимого поля ввода).
     * @name y5.Events.observeProperty
     * @memberOf y5.Events
     * @function
     * @param {String} property Свойство
     * @param {Function} listener Callback-функция
     * @param {Element} element Элемент
     * @param {Boolean} [add] Начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] Контекст выполнения функции listener
     * @param {Boolean} [runOnce] Обработать событие один раз и удалить обработчик. По умолчанию, false.
     * @returns {y5.AEventListener}
     * @note Не работает в Safari 3.0.4, Firefox 1.0
     *
     * @example
     * y5.Events.observeProperty("value", callback, element, true);
     */
    observeProperty: function(property, listener, element, add, context, runOnce) {
        // Внимание! Не используем контекст для listenerAttr,
        // только для listener
        function listenerAttr(evt) {
            if (evt.attrName == property) {
                execListenerWithContext(listener, context, evt, element);
            }
        }
        return new AEventListener(typeDOMAttrModified, listenerAttr, element, add, null, runOnce);
    },
    
    /**
     * Одноразовый слушатель изменения свойств элемента. После наступления события обработчик удаляется.
     * @name y5.Events.observePropertyOnce
     * @memberOf y5.Events
     * @function
     * @param {String} property Свойство
     * @param {Function} listener Callback-функция
     * @param {Element} element Элемент
     * @param {Boolean} [add] Начать слушать немедленно (false - по умолчанию)
     * @param {Object} [context] Контекст выполнения функции listener
     * @returns {y5.AEventListener}
     * @note Не работает в Safari 3.0.4, Firefox 1.0
     *
     * @example
     * y5.Events.observeProperty("value", callback, element, true);
     */
    observePropertyOnce: 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, null, true);
    }
};

var Events = y5.Events;

// aliases

/**
 * Создает один или несколько объектов-слушателей.
 * @name y5.on
 * @see y5.Events.observe
 * @memberOf y5
 * @function
 */
y5.on = Events.observe;

/**
 * Создает событие и инициирует его возникновение.
 * @name y5.fire
 * @see y5.Events.notify
 * @memberOf y5
 * @function
 */
y5.fire = Events.notify;

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

/**
 * @memberOf y5.Events
 * @name y5.Events.make
 * @function
 * @deprecated y5.Events.notify
 */
Events.make = Events.notify;

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


/// dom:loaded notify
/////////////////////////////////////////////////////////////////////
(function() {

var timer, type = 'load';

function notifyDomLoaded() {
    if (y5.domloaded) {
        return;
    }

    // устанавливаем глобальный флаг
    y5.domloaded = true;

    if (timer) {
        window.clearInterval(timer);
    }

    // обнуляем эту функцию, чтобы вызов был только один раз
    notifyDomLoaded = y5.VOID;

    // посылаем сообщение и делаем его постоянным
    y5.Notify('dom:loaded', y5, null, true);
}

if (document.addEventListener) {

    if (y5.is_safari || y5.is_khtml) {
        var loadedRegex = /loaded|complete/;

        timer = window.setInterval(function() {
            if (loadedRegex.test(document.readyState)) {
                notifyDomLoaded();
            }
        }, 0);
    } else if (y5.is_opera && y5.opera_ver < 9) {
        ;
    } else {
        type = 'DOMContentLoaded';
    }

} else {
    var doc = document;

    function isSet(property) {
        return typeof doc[property] != 'undefined';
    }

    function poll() {
        if (doc.body !== null && doc.getElementsByTagName) {
            if (isSet('readyState') && (/loaded|complete/).test(doc.readyState)) {
                notifyDomLoaded();
            }
            if (isSet('fileSize')) {
                try {
                    doc.documentElement.doScroll('left');
                    notifyDomLoaded();
                } catch (e) {}
            }
        }
    }

    timer = window.setInterval(poll, 10);
}

y5.Events.observe(type, notifyDomLoaded, window, true);

})();

/// Init GC
/////////////////////////////////////////////////////////////////////

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

})();(function() {

var Types = y5.Types,
    NOT_OBJECT = Types.NODE | Types.BOOLEAN | Types.NUMBER | Types.STRING | Types.EVENT | Types.REGEXP | Types.FUNCTION | Types.NULL;

/**
 * @class Набор полезных функций-утилит общего назначения.
 * @static
 * @name y5.Utils
 */
y5.Utils = {
    /**
     * Счетчик генерируемых ID.
     * @name y5.Utils.counterId
     * @memberOf y5.Utils
     * @type Number
     * @private
     */
    counterId: 0,

    /**
     * Генерирует случайный ID. Возвращает результат в виде строки символов.
     * @name y5.Utils.generateId
     * @memberOf y5.Utils
     * @function
     * @param {String} [prefix] Префикс (по умолчанию = '')
     * @returns {String} Случайный ID
     */
    generateId: function(prefix) {
        return (prefix || '') + ((new Date()).getTime() + Math.round(Math.random() * 10000));
    },

    /**
     * Генерирует уникальный ID. Возвращает результат в виде строки символов.
     * @name y5.Utils.generateUniqueId
     * @memberOf y5.Utils
     * @function
     * @returns {String} Уникальный ID
     *
     * @example
     * y5.Utils.generateUniqueId();
     * // -> 'y5__id45'
     */
    generateUniqueId: function() {
        return 'y5__id' + (++this.counterId);
    },

    /**
     * Генерирует уникальный ID и присваивает его свойству uniqueID переданного объекта.
     * Если свойство uniqueID объекта существует, то значение этого свойства не изменяется. Возвращает значение uniqueID в виде строки символов.
     * @name y5.Utils.getUniqueId
     * @memberOf y5.Utils
     * @function
     * @param {Object} object Объект
     * @returns {String} Уникальный ID
     */
    getUniqueId: function(object) {
        if (object === document) {
            return this.documentID;
        }
        return object.uniqueID || this.setUniqueId(object);
    },

    /**
     * Присваивает свойству uniqueID объекта переданное значение. Если параметр uniqueID не определен, то генерируется новый уникальный идентификатор и полученное значение присваивается свойству uniqueID объекта. Возвращает значение свойства uniqueID в виде строки символов.
     * @name y5.Utils.setUniqueId
     * @memberOf y5.Utils
     * @function
     * @param {Object} object Объект
     * @param {String} [uniqueID] Уникальный ID (если неопределен, то генерируется новый)
     * @returns {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);
    },

    /**
     * Сравнивает два объекта. Возвращает true, если оба объекта на самом деле - это один и тот же объект.
     * @name y5.Utils.isEqual
     * @memberOf y5.Utils
     * @function
     * @param {Object} first Объект
     * @param {Object} second Объект
     * @returns {Boolean} Результат сравнения
     */
    isEqual: function(first, second) {
        return this.getUniqueId(first) == this.getUniqueId(second);
    },

    /**
     * Решает проблему браузера IE, связанную с некорректной отрисовкой тега select и объекта Flash, в результате которой эти визуальные элементы перекрывают любые другие элементы страницы, даже находящиеся на  слоях выше select или Flash. Проблема решается путем создания элемента iframe и «подкладывания» его под элемент (обычно div), который необходимо корректно отобразить поверх select или Flash.
     * @name y5.Utils.fakeFrame
     * @memberOf y5.Utils
     * @type Object
     */
    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.Elements.css(obj, 'z-index') - 2;
            this.frame.style.zIndex = y5.Elements.css(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.Elements.css(obj, 'z-index') - 2;
                    this.frame.style.zIndex = y5.Elements.css(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
     * @name y5.Utils.hexDigit
     * @memberOf y5.Utils
     * @type Array
     */
    hexDigit: '0123456789ABCDEF'.split(''),

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

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

    /**
     * Поочередно копирует свойства нескольких объектов в новый. Возвращает объект, получившийся в результате слияния исходных.
     * @name y5.Utils.objectCopy
     * @memberOf y5.Utils
     * @function
     * @param {Object} object1 Первый объект
     * @param {Object} object2 Второй объект
     * @param {Object} objectN n-объект
     * @returns {Object} Объект, получившийся в результате слияния объектов
     *
     * @example
     * y5.Utils.objectCopy({a: 1, b: 3}, {b: 2, c: 3});
     * // -> {a: 1, b: 2, c: 3}
     */
    objectCopy: function(destination) {
        var key,
            result = {},
            argsCnt = arguments.length;

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

        if (argsCnt == 1) {
            return result;

        } else if (argsCnt == 2) {
            var source = arguments[1],
                src,
                data;
            for (key in source) {
                src = source[key];
                data = {};

                if (Types.test(src, NOT_OBJECT)) {
                    data = src;
                // Date
                } else if (Types.date(src)) {
                    data = new Date(src);
                // Array
                } else if (Types.array(src)) {
                    data = [].concat(src);
                // Object
                } else {
                    if (Types.def(result[key])) {
                        data = result[key];
                    }
                    data = this.objectCopy(data, src);
                }

                result[key] = data;
            }

        } else {
            key=1;
            for (; key<argsCnt; key++) {
                result = this.objectCopy(result, arguments[key]);
            }
        }
        return result;
    },

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

        var key,
            destProto = destination.prototype,
            sourceProto = source.prototype;

        destProto[source_name] = source;

        for (key in sourceProto) {
            // если такого свойства нет в destination, то копируем его из source
            if (Types.undef(destProto[key])) {
                destProto[key] = sourceProto[key];

            // если свойство в destination и в source - объекты, то объединяем их  через objectCopy    
            } else if (Types.object(destProto[key]) && Types.object(sourceProto[key])) {
                destProto[key] = this.objectCopy(sourceProto[key], destProto[key]);
            }
            /*
             // если свойство есть, то ничего не делаем
             } else {}
            */
        }
    },

    /**
     * Запускает выполнение метода указанного объекта с периодичностью, заданной в параметре timeout. Возвращает уникальный ID таймера.
     * @name y5.Utils.setTimeout
     * @memberOf y5.Utils
     * @function
     * @param {Function} method Метод
     * @param {Number} timeout Таймаут (мсек)
     * @param {Object} [context] Объект, в контексте которого запускается функция (по умолчанию null)
     * @param {Object} [arg1] Аргумент вызываемой функции
     * @param {Object} [arg2]
     * @param {Object} [...]
     * @param {Object} [argN]
     * @returns {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 y5.Utils.formatNumber
     * @memberOf y5.Utils
     * @function
     * @param {Number|String} num Число
     * @param {String} [exponent_delimiter] Разделитель порядков. По умолчанию: пробел
     * @param {String} [float_delimiter] Разделитель дробной части. По умолчанию: точка
     * @returns {String} Отформатированное число
     * @example
     * y5.Utils.formatNumber('10000', ' ');
     * // -> '10 000'
     *
     * y5.Utils.formatNumber('-10000.02', ',', '.');
     * // -> '-10,000.02'
     *
     */
    formatNumber: function (num, exponent_delimiter, float_delimiter) {
        num = parseFloat(num.toString(), 10);
        if (isNaN(num)) {
            return;
        }

        exponent_delimiter = Types.string(exponent_delimiter) ? exponent_delimiter : ' ';
        float_delimiter = float_delimiter ? float_delimiter : '.';

        // определяем знак
        var sgn = num < 0 ? '-' : '',
        // отделяем дробную часть
            strNum = num.toString(),
            pointIndex = strNum.indexOf('.'),
            floatPart = 0;
        if (pointIndex != -1) {
            floatPart = strNum.substr(pointIndex + 1);
        }

        // берем число без знака и отсекаем дробную часть
        num = Math.floor(Math.abs(num)).toString();

        var startIndex = num.length % 3,
            result = num.substr(0, startIndex),
            cnt = Math.floor(num.length / 3),
            i = 0;
        for (; i < cnt; i++) {
            result += exponent_delimiter + num.substr(3 * i + startIndex, 3);
        }

        // если количество цифр кратно 3, то убираем первый exponent_delimiter
        if (startIndex == 0) {
            result = result.substr(1);
        }

        if (floatPart) {
            result += float_delimiter + floatPart;
        }

        return sgn + result;
    }
};

y5.Utils.documentID = y5.Utils.generateId('y5__');

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

y5.loaded('Utils');

})();/// Init
/////////////////////////////////////////////////////////////////////

// Устанавливаем namespace y5
y5.registerNamespace('y5', 'y5.js');
y5.loaded('Types');
y5.loaded('Events');

if (/y5debug/.test(location.search + document.cookie)) {
    y5.require('Debug');
}/**
 * Кэш данных.
 * @class Организация кэширования активно используемых данных. Включает набор функций для реализации механизма кэширования и работы с закэшированными данными.
 * @name y5.Cache
 *
 * @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 = {
    /**
     * Получает ранее сохраненные данные из кэша. Возвращает запрошенные данные, если они находятся в кэше. Если запрошенных данных в кэше нет, возвращает undefined.
     * @name y5.Cache.get
     * @memberOf y5.Cache
     * @function
     * @param {String} key Уникальный ключ
     * @returns {Object | undefined} Данные из кэша
     */
    get: function(key) {
        return this.data[key];
    },

    /**
     * Записывает данные в кэш. Возвращает данные, помещенные в кэш.
     * @name y5.Cache.set
     * @memberOf y5.Cache
     * @function
     * @param {String} key Уникальный ключ
     * @param {Object} value Объект
     * @returns {Object} Объект, помещенный в кэш
     */
    set: function(key, value) {
        return this.data[key] = value;
    },

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

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

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

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

var EMPTY = '',
    SPACE = ' ',
    trimRegexp = /(^[\s\xA0]+|[\s\xA0]+$)/g,
    isVoidRegexp = /^[\s\xA0]*$/,
    normalizeSpaceRegexp = /[\s\xA0]{2,}/g,
    escapeRegexpRe = /([\|\!\[\]\^\$\(\)\{\}\+\=\?\.\*\\])/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+);|.)/gi,
    unescapeHTMLHash = {lt: '<', gt: '>', quot: '"', apos: "'", amp: '&'},
    unescapeHTMLReplacer = function(s, c, d) {
        return unescapeHTMLHash[d] || (d ? String.fromCharCode(d.substring(1)) : c);
    },
    // y5.Strings.dasherize
    dasherizeRegexp = /[A-Z]+[a-z]+/g,
    dasherizeFunction = function(s) {
            return '-' + s.toLowerCase()
    },
    // y5.Strings.camelize
    camelizeFunction = function(item, i) {
        if (i != 0) {
            return y5.Strings.capitalize(item);
        }
        return item;
    };

/**
 * Преобразует объект для y5.Strings.printf
 * @param {String} Формат
 * @param {Object} Данные для преобразования
 * @returns {String} Результирующая строка
 * @private
 */
function convertPrintf(format, data) {
    data = data.toString();

    var match = /^%(0?)(\d+)d$/.exec(format);
    if (match) {
        var padchar = match[1] || SPACE,
            padding = parseInt(match[2], 10) - data.length;
        return y5.Strings.repeat(padchar, padding) + data;
    }
    return data;
}

/**
 * @class Набор функций для работы со строками. Включает реализацию дополнительных (отсутствующих в стандарте JavaScript) функций, упрощающих работу со строковыми данными.
 * @static
 * @name y5.Strings
 */
y5.Strings = {
    /**
     * Проверяет строку на пустоту. Возвращает true, если строка пустая.
     * @name y5.Strings.isEmpty
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка для проверки
     * @returns {Boolean} Результат проверки
     */
    isEmpty: function(str) {
        return (str == EMPTY);
    },

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

    /**
     * Проверяет, содержит ли строка заданную подстроку. Возвращает true, если вхождение заданной подстроки было найдено.
     * @name y5.Strings.contains
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @param {String} needle Строка для поиска
     * @param {Boolean} ci Поиск без учета регистра
     * @returns {Boolean} Результат проверки
     * @example
     * y5.Strings.contains("foobarbaz", "bar")
     * // -> true
     *
     * y5.Strings.contains("FooBarBaz", "bar")
     * // -> false
     *
     * y5.Strings.contains("FooBarBaz", "bar", true)
     * // -> true
     */
    contains: function(str, needle, ci) {
        if (ci) {
            str = str.toUpperCase();
            needle = needle.toUpperCase();
        }
        return str.indexOf(needle) !== -1;
    },

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

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

    /**
     * Сравнивает две строки.
     * Возвращает отрицательное число, если str1  меньше, чем str2; положительное число, если str1 больше, чем str2, и 0 если строки равны.
     * @param {String} str1 Строка 1
     * @param {String} str2 Строка 2
     * @param {Boolean} ci Сравнение без учета регистра
     * @return {Number} Результат сравнения
     * @example
     * y5.Strings.compare('Style', 'styLe');
     * // -1
     *
     * y5.Strings.compare('style', 'StyLe', true);
     * // 0
     *
     * var fruits = ['banana', 'orange', 'lemon', 'grapefruit', 'cherry'];
     * fruits.sort(y5.Strings.compare).join(',');
     * // banana,cherry,grapefruit,lemon,orange
     */
    compare: function (str1, str2, ci) {
        if (ci) {
            str1 = str1.toLowerCase();
            str2 = str2.toLowerCase();
        }
        if (str1 == str2) {
            return 0;
        } else if (str1 < str2) {
            return -1;
        } else {
            return 1;
        }
    },

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

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

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

    /**
     * Определяет, какому символу соответствует заданный цифровой код. Возвращает символ по его коду.
     * @name y5.Strings.getCode
     * @memberOf y5.Strings
     * @function
     * @param {Number} code Код символа
     * @returns {String} Символ
     * @example
     * 65 -> "A"
     */
    getCode: function(code) {
        return String.fromCharCode(code);
    },

    /**
     * Подготавливает строку для безопасной вставки в HTML-код. Возвращает результат преобразования.
     * Все служебные HTML-символы в данной строке заменяются на их символьные аналоги. Данная функция часто используется для записи в innerHTML. Для обратного преобразования рекомендуется использовать функцию y5.Strings.unescapeHTML.
     * @name y5.Strings.escapeHTML
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {String} Преобразованная строка
     * @example
     * "&lt;foo>" -> "&amp;lt;foo&amp;gt;"
     */
    escapeHTML: function(str) {
        return str.replace(escapeHTMLRe, escapeHTMLReplacer);
    },

    /**
     * Заменяет символьные аналоги служебных HTML-символов на реальные значения. Возвращает результат преобразования.
     * Функция, обратная escapeHTML.
     * @name y5.Strings.unescapeHTML
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {String} Преобразованная строка
     * @example
     * "&amp;lt;foo&amp;gt;" -> "&lt;foo>"
     */
    unescapeHTML: function(str) {
        return str.replace(unescapeHTMLRe, unescapeHTMLReplacer);
    },

    /**
     * Выбирает из элемента DOM текстовое содержимое. Возвращает строку с полученным текстом.
     * @name y5.Strings.stripTags
     * @memberOf y5.Strings
     * @function
     * @param {String | HTMLElement} element Элемент
     * @returns {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, EMPTY);
    },

    /**
     * Заменяет в строке все вхождения букв Ё и ё на Е и е соответственно. Возвращает результат преобразования.
     * @name y5.Strings.IoToIe
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {String} Преобразованная строка
     * @example
     * "ёж" -> "еж"
     */
    IoToIe: function(str) {
        return str.replace(/[\u0451\u0401]/g, '\u0435');
    },

    /**
     * Подбирает любому числу правильную словоформу. Возвращает число и заданное слово в соответствующем данному числу склонении.
     * @name y5.Strings.plural
     * @memberOf y5.Strings
     * @function
     * @param {Number} number Число
     * @param {Array} forms Список форм слова (формы: 1, 2, 5, 0)
     * @param {Boolean} withoutNumber Преобразовать и вернуть только слово
     * @returns {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 писем'
     *
     * y5.Strings.plural(5, forms, true);
     * // -> 'писем'
     *
     * var forms = ['письмо', 'письма', 'писем'];
     * y5.Strings.plural(0, forms);
     * // -> '0 писем'
     *
     * @todo сделать формат вывода
     */
    plural: function(num, forms, withoutNumber) {
        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;
                    }
                }
            }
        }

        if (withoutNumber) {
            return forms[res];            
        } else {
            return num + SPACE + forms[res];
        }
    },

    /**
     * Позволяет выводить число с разными формами слова.
     * @param {Number} number Число
     * @param {Array} forms Список форм слова (формы: 1, 5, 2, 0)
     * @returns {String} Число с формой слова
     * @deprecated y5.Strings.plural
     */
    conversion: function(num, words) {
        return this.plural(num, [words[0], words[2], words[1], words[3]]);
    },

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

    /**
     * Переводит слова, разделенные дефисами, в форму camelCase. Возвращает результат преобразования.
     * @name y5.Strings.camelize
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {String} Преобразованная строка
     * @example
     * "foo-bar-baz" -> "fooBarBaz"
     */
    camelize: function(str) {
        return str.split('-').map(camelizeFunction).join(EMPTY);
    },

    /**
     * Разделяет дефисами слова в форме camelCase. Возвращает результат преобразования.
     * @name y5.Strings.dasherize
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {String} Преобразованная строка
     * @example
     * y5.Strings.dasherize("fooBarBaz");
     * // "foo-bar-baz"
     */
    dasherize: function (str) {
        return str.replace(dasherizeRegexp, dasherizeFunction);
    },

    /**
     * Повторяет строку заданное число раз. Возвращает строку, состоящую из заданного количества повторений исходной строки.
     * @name y5.Strings.repeat
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @param {Number} count Число повторений
     * @returns {String} Строка, продублированная number раз
     * @example
     * y5.Strings.repeat("foobar", 3);
     * // -> "foobarfoobarfoobar"
     *
     * y5.Strings.repeat("foo", 0);
     * // -> ""
     */
    repeat: function(str, count) {
        if (count < 1) {
            return EMPTY;
        }

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

    /**
     * Заменяет все символы перевода в строке на теги &lt;br&gt;. Возвращает преобразованную строку.
     * @name y5.Strings.nl2br
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @param {Boolean} xhtml XHTML-совместимый вывод. true -> "&lt;br />", иначе - "&lt;br>"
     * @returns {String} HTML-строка
     * @example
     * "foo\nbar" -> "foo&lt;br>bar"
     */
    nl2br: function(str, xhtml) {
        return str.replace(NLRegexp, xhtml ? '<br />' : '<br>');
    },

    /**
     * Преобразовывает текст в HTML (экранирует все HYML-символы и заменяет перевод строк на &lt;br&gt;)
     * @name y5.Strings.text2html
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {String} HTML-строка
     */
    text2html: function(str) {
        return this.nl2br(this.escapeHTML(str))
    },

    /**
     * Преобразует строку в массив слов. Возвращает получившийся массив.
     * @name y5.Strings.words
     * @memberOf y5.Strings
     * @function
     * @param {String} string Строка
     * @returns {Array} Массив слов
     * @example
     * y5.Strings.words('foo bar');
     * // -> ['foo', 'bar']
     *
     * y5.Strings.words('');
     * // -> []
     */
    words: function(str) {
        return str.match(wordsRegexp) || [];
    },

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

    /**
     * Реализует поддержку форматированного вывода данных. Формат соответствует популярному формату функции printf языка программирования C. Возвращает отформатированную по заданному шаблону строку.
     * @name y5.Strings.printf
     * @memberOf y5.Strings
     * @function
     * @param {String} format Строка формата вывода
     * @param {Object | Array} arg1 аргумент 1 (если массив, то он используется в качестве списка аргументов)
     * @param {Object} arg2 аргумент 2
     * @param {Object} argN аргумент n
     * @returns {String} Результат
     */
    printf: function(str, data) {
        var values = data;
        var args = arguments, length = args.length;
        if (length > 2) {
            values = [];
            for (var i = 1; i < length; i++) {
                values.push(args[i]);
            }
        } else if (typeof data != 'object') {
            values = [data];
        }

        var k = 0;
        function replacer(token) {
            var value = values[k]; k++;
            return convertPrintf(token, typeof value != y5.UNDEF ? value : EMPTY);
        }

        return str.replace(/%(s|\d*d)/g, replacer).replace(/%%/g, '%');
    },

    /**
     * Создает пустую строку. Возвращает строку с нулевой длиной.
     * @name y5.Strings.EMPTY
     * @constant
     * @type String
     */
    EMPTY: EMPTY,

    /**
     * Создает строку, состоящую из одного пробела. Возвращает полученную строку.
     * @name y5.Strings.SPACE
     * @constant
     * @type String
     */
    SPACE: SPACE,

    /**
     * Создает строку, состоящую из одного неразрывного пробела. Возвращает полученную строку.
     * @name y5.Strings.NBSP
     * @constant
     * @type String
     */
    NBSP: '\u00A0'
};

var Strings = y5.Strings;

/// aliases

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

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

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

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

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

})();

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

var UNDEF = y5.UNDEF,
    Types = y5.Types,
    urlRegexp = /^((((\w+):)\/\/)?(([\w\-\.]+\.\w+|localhost)(\:(\d+))?))?(\/?[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
    validURLRegexp = /^(([\w]+:)?\/\/)?(([\d\w]|%[a-fA-f\d]{2,2})+(:([\d\w]|%[a-fA-f\d]{2,2})+)?@)?([\d\w][-\d\w]{0,253}[\d\w]\.)+[\w]{2,4}(:[\d]+)?(\/([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)*(\?(&?([-+_~.\d\w]|%[a-fA-f\d]{2,2})=?)*)?(#([-+_~.\d\w]|%[a-fA-f\d]{2,2})*)?$/,
    winHex = 'E9F6F3EAE5EDE3F8F9E7F5FAF4FBE2E0EFF0EEEBE4E6FDFFF7F1ECE8F2FCE1FEB8C9D6D3CAC5CDC3D8D9C7D5DAD4DBC2C0CFD0CECBC4C6DDDFD7D1CCC8D2DCC1DEA8'.match(/../g),
    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'.match(/.{6}/g),
    win2utfTable = {},
    encode = encodeURIComponent,
    fileExt = ['.xml', '.html', '.jpg', '.gif', '.png', '.xhtml', '.php', '.xsl', '.py', '.pl'],
    i = 0, l = winHex.length,
    // Символы ; , / ? : @ & = + $ не кодируются и не раскодируются через encodeURI,
    // в pathname их надо обрабатывать отдельно
    reservedCharactersRegexp = /%3B|%2C|%2F|%3F|%3A|%40|%26|%3D|%2B|%24|%23/gi;

for (; i < l; i++) {
    win2utfTable[winHex[i]] = utfHex[i];
}

function win2utf(token, code) {
    return win2utfTable[code] || token;
}

function decodeWin(value) {
    return value.replace(/%([A-Fa-f0-9]{2})/g, win2utf);
}

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

    try {
        var str = decodeURIComponent(val);
        if (str == undefined) {
            throw 'malformed URI sequence';
        }
        return str;
    } catch (e) {
        try {
            return decodeURIComponent(decodeWin(val));
        } catch (e) {
            return unescape(val);
        }
    }
}

function decodeURI_win (value) {
    try {
        var str = decodeURI(value);
        if (str == undefined) {
            throw 'malformed URI sequence';
        }
        return str;
    } catch (e) {
        try {
            return decodeURI(decodeWin(value));
        } catch (e) {
            return unescape(value);
        }
    }
}

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

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

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

    return result;
}

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

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

    return query;
}

/**
 * Создает экземпляр класса для работы с URL и его параметрами.
 * @constructor
 * @name y5.URL
 * @class Класс для работы с URL и его частями. Содержит набор методов для упрощения операций, связанных с обработкой параметров, переданных в URL, и использованием полученных данных внутри приложения.
 * @param {String} href URL
 *
 * @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) {
    href = Types.def(href) ? href.toString() : window.location.href;
    // поиск запроса в url
    var match = href.match(urlRegexp);


    if (!match) {
        throw new y5.Exception('This is not an url', 'constructor', 'URL');
    }
    this.Host  = match[6] || '';
    this.Path = match[9] || '';

    if (match[6]) {
        // чтобы отличить yandex.ru от index.html, проверяем хост на распространенные расширения файлов
        var ext = match[6].substring(match[6].lastIndexOf('.')),
            extIndex = fileExt.indexOf(ext);
        // если расширение совпало, значит хост - путь до файла
        if (extIndex != -1) {
            this.Path = match[6];
            this.Host = '';    
        }
    }
    
    this.Href  = match[0];
    this.Proto = match[4] || '';
    this.Port  = match[8] || 0;
    this.Path  = decodeURI_win(this.Path);
    this.Query = parseQuery(match[11] || '');
    this.Hash  = decode(match[13] || '');
};

y5.URL.prototype = {
    /**
     * Выполняет переход по URL согласно текущему состоянию объекта y5.URL.
     * @name y5.URL.go
     * @memberOf y5.URL
     * @function
     */
    go: function() {
        window.location.href = this.toString();
    },

    /**
     * Формирует URL со всеми параметрами из содержимого объекта. Возвращает получившийся URL в виде строки.
     * @name y5.URL.toString
     * @memberOf y5.URL
     * @function
     * @returns {String} URL
     */
    toString: function() {
        var result = '';

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

        // path
        if (this.Path) {
            if (this.Host && this.Path.indexOf('/') != 0) {
                result += '/';
            }
            /*
            pathname ведет себя следующим образом:
             - для урл сначала делается decodeURI, потом encodeURI
               /"quote"/ -> /%22quote%22/
               /%22quote%22/ -> /%22quote%22/
             - но не преобразовываются как сами зарезервированные символы - ; , / ? : @ & = + $, так и их их url-кодированные значения,
              это приводит к тому, что переданное значение www.yandex.ru/url/f%3Dx%2F2/index.xml преобразуется в www.yandex.ru/url/f=x/2/index.xml, чего допустить нельзя
             - пути f=2 и f%3D2 равнозначны, а f%3Dx%2F2 и f=x/2 - нет

             Поэтому разбиваем pathname некодируемыми символами и кодируем получившиеся части отдельно
             т.к. /f%3Dx%2F2/comments должно остаться непреобразованным
             */

            // тут можно использовать this.Path.split(reservedCharactersRegexp),
            // но IE не возвращает сепаратор в результирующий массив
            // см. http://msdn.microsoft.com/en-us/library/microsoft.jscript.stringprototype.split.aspx

            // обнуляем регулярку
            reservedCharactersRegexp.lastIndex = 0;
            var match, lastFindedIndex = 0, chunks = [];
            while (match = reservedCharactersRegexp.exec(this.Path)) {
                if (reservedCharactersRegexp.lastIndex > lastFindedIndex) {
                    if (match[0]) {
                        chunks.push(this.Path.slice(lastFindedIndex, match.index));
                        chunks.push(match[0]);
                        lastFindedIndex = match.index + match[0].length;
                    }
                }
            }
            if (lastFindedIndex !== this.Path.length) {
                chunks.push(this.Path.slice(lastFindedIndex));
            }

            chunks[0] = encodeURI(chunks[0]);

            // собираем обратно строку
            result += chunks.reduce(function(result, current, index){
                if (!(index % 2)) {
                    current = encodeURI(current);
                }
                return result + current;
            });
        }

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

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

        return result;
    },

    /**
     * Создает новый объект y5.URL из данного. Возвращает копию исходного объекта.
     * @name y5.URL.clone
     * @memberOf y5.URL
     * @function
     * @returns {y5.URL} Копия объекта
     */
    clone: function() {
        return new y5.URL(this.toString());
    },

/// getters/setters

    /**
     * Устанавливает или получает текущее значение протокола URL. Если вызывается без параметра, то возвращает текущее значение протокола. Если вызывается с параметром, то заменяет текущее значение протокола на заданное в параметре и возвращает исходный объект с измененным значением протокола.
     * @name y5.URL.proto
     * @memberOf y5.URL
     * @function
     * @param {String} [string] Новый протокол
     * @returns {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() {
        /* if (typeof value != UNDEF) {
            this.Proto = value;
            return this;
        }

        return this.Proto; */
    },

    /**
     * Устанавливает или получает текущее значение хоста URL. Если вызывается без параметра, то возвращает текущее значение хоста. Если вызывается с параметром, то заменяет текущее значение хоста на заданное в параметре и возвращает исходный объект с измененным значением хоста.
     * @name y5.URL.host
     * @memberOf y5.URL
     * @function
     * @param {String} [string] Новый хост
     * @returns {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() {
        /* if (typeof value != UNDEF) {
            this.Host = value;
            return this;
        }

        return this.Host; */
    },

    /**
     * Устанавливает или получает текущее значение порта URL. Если вызывается без параметра, то возвращает текущее значение порта. Если вызывается с параметром, то заменяет текущее значение порта на заданное в параметре и возвращает исходный объект с измененным значением порта.
     * @name y5.URL.port
     * @memberOf y5.URL
     * @function
     * @param {Number} [string] Новый порт
     * @returns {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() {
        /* if (typeof value != UNDEF) {
            this.Port = value;
            return this;
        }

        return this.Port; */
    },

    /**
     * Устанавливает или получает текущее значение пути URL (содержимого URL без хоста и набора параметров). Если вызывается без параметра, то возвращает текущее значение пути. Если вызывается с параметром, то заменяет текущее значение пути на заданное в параметре и возвращает исходный объект с измененным значением пути.
     * @name y5.URL.path
     * @memberOf y5.URL
     * @function
     * @param {String} [string] Новый путь
     * @returns {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() {
        /* if (typeof value != UNDEF) {
            this.Path = value;
            return this;
        }

        return this.Path; */
    },

    /**
     * Устанавливает или получает текущее значение якоря URL. Если вызывается без параметра, то возвращает текущее значение якоря. Если вызывается с параметром, то устанавливает заданный в параметре якорь и возвращает исходный объект с добавленным якорем.
     * @name y5.URL.hash
     * @memberOf y5.URL
     * @function
     * @param {String} [string] Новый якорь
     * @returns {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() {
        /* if (typeof value != UNDEF) {
            this.Hash = value;
            return this;
        }

        return this.Hash; */
    },

    /**
     * Устанавливает или получает текущее значение строки запроса URL. Если вызывается без параметра, то возвращает текущее значение запроса. Если вызывается с параметром, то заменяет текущее значение запроса на заданное в параметре и возвращает исходный объект с измененным значением запроса.
     * @name y5.URL.query
     * @memberOf y5.URL
     * @function
     * @param {Object | String} [query] Новые параметры запроса
     * @returns {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 (Types.def(value)) {
            this.clearQuery();

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

            return this;
        }

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

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

    /**
     * Разбирает URL на составляющие и возвращает список имен параметров запроса в виде массива. Если URL не содержит параметров, то возвращается пустой массив.
     * @name y5.URL.queryKeys
     * @memberOf y5.URL
     * @function
     * @returns {Array} Список ключей
     * @example
     * var url = new y5.URL("http://www.yandex.ru/yandsearch?text=test&amp;foo=bar");
     * url.queryKeys(); // -> ['text', 'foo']
     */
    queryKeys: function() {
        var result = [];
        for (var name in this.Query) {
            result.push(name);
        }
        return result.sort();
    },

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

    /**
     * Добавляет параметр с именем name и значением value к URL. Возвращает исходный объект с добавленными данными.
     * @name y5.URL.addParam
     * @memberOf y5.URL
     * @function
     * @param {String} name Название параметра
     * @param {String} value Значение параметра
     * @returns {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test");
     * url.addParam("stype", "www");
     * url.toString(); // -> "http://ya.ru/?text=test&amp;stype=www"
     */
    addParam: function(name, value) {
        addParam(this.Query, name, value);

        return this;
    },

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

        return this;
    },

    /**
     * Удаляет набор параметров, переданных в виде массива имен из URL. Возвращает измененный исходный объект.
     * @name y5.URL.removeParams
     * @memberOf y5.URL
     * @function
     * @param {Array} params Удаляемый параметр запроса
     * @returns {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&amp;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 на набор параметров, переданных в виде объекта params. Возвращает измененный исходный объект.
     * @name y5.URL.replaceParams
     * @memberOf y5.URL
     * @function
     * @param {Object} params Заменяемый параметр запроса
     * @returns {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&amp;stype=www");
     * url.replaceParams({stype: "images"});
     * url.toString(); // -> "http://ya.ru/?text=test&amp;stype=images"
     */
    replaceParams: function(params) {
        var list = [];
        for (var name in params) {
            list.push(name);
        }
        this.removeParams(list);
        this.addParams(params);

        return this;
    },

    /**
     * Удаляет все параметры из URL. Возвращает измененный исходный объект.
     * @name y5.URL.clearQuery
     * @memberOf y5.URL
     * @function
     * @returns {this}
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&amp;stype=www");
     * url.clearQuery();
     * url.toString(); // -> "http://ya.ru/"
     */
    clearQuery: function() {
        this.Query = {};
        return this;
    },

    /**
     * Находит параметр с заданным именем и возвращает его значение. Если параметра с заданным именем в URL найти не удалось, то возвращается null. Если в URL содержится несколько параметров с заданным именем, то возвращается значение первого из них.
     * @name y5.URL.getParam
     * @memberOf y5.URL
     * @function
     * @param {String} name Имя параметра
     * @returns {String | null} Значение параметра запроса
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&amp;text=www");
     *
     * // параметр присутствует в запросе
     * url.getParam("text"); // -> "test"
     *
     * // параметр отсутствует в запросе
     * url.getParam("foo"); // -> null
     */
    getParam: function(name) {
        var params = this.Query[name];
        return params ? params[0] : null;
    },

    /**
     * Находит все параметры с заданным именем и возвращает массив их значений. Если ни одного параметра с заданным именем найти не удалось, то возвращается пустой массив.
     * @name y5.URL.getParams
     * @memberOf y5.URL
     * @function
     * @param {String} name Имя параметра
     * @returns {Array | []} Список значений параметра
     * @example
     * var url = new y5.URL("http://ya.ru/?text=test&amp;text=www");
     *
     * // параметры присутствуют в запросе
     * url.getParams("text"); // -> ["test", "www"]
     *
     * // параметры отсутствуют в запросе
     * url.getParams("foo"); // -> []
     */
    getParams: function(name) {
        return this.Query[name] || [];
    }
};

var URL = y5.URL,
    URLProto = URL.prototype;

// Создаем getters/setters
var funcs = {proto: 'Proto', host: 'Host', port: 'Port', path: 'Path', hash: 'Hash'};
for (i in funcs) {
    URLProto[i] = (function(property) {
        return function(value) {
            if (Types.def(value)) {
                this[property] = value;
                return this;
            }
            return this[property];
        };
    })(funcs[i]);
}

/// aliases

/**
 * @memberOf y5.URL
 * @name y5.URL.get
 * @see y5.URL.toString
 * @function
 */
URLProto.get = URLProto.toString;

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

/**
 * Проверяем, что данный url валиден
 * @param {String} url URL
 * @returns {Boolean} Результат проверки
 * @name y5.URL.isValid
 * @memberOf y5.URL
 * @function
 * @static
 */
URL.isValid = function (url) {
    return validURLRegexp.test(url);    
}

y5.loaded('URL');

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

/**
 * Механизм шаблонизации. Замена вхождений на их значение.
 * @param {Object} obj Объект данных
 * @param {String} type '#' - выводим с преобразованием для HTML, '$' - выводим как есть
 * @param {String} property Имя свойства
 * @returns {String} Значение элемента данных
 * @private
 */
function replace(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));
}

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

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

    /**
     * Обрабатывает шаблон и вычисляет значения выражений шаблона — производит замену включений на соответствующие этим включениям значения данных. Возвращает сформированную согласно шаблону строку.
     * @name y5.Template.evaluate
     * @memberOf y5.Template
     * @function
     * @param {Object} obj объект данных
     * @returns {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 = {};
        }

        function _replace(str, type, property) {
            return replace(obj, type, property);
        }

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

    /**
     * Обрабатывает шаблон и вычисляет значения выражений шаблона для массива данных. При этом  заданный шаблон применяется к каждому из элементов массива по отдельности. Возвращает результирующую строку, сформированную согласно примененным шаблонам.
     * @name y5.Template.evaluateArray
     * @memberOf y5.Template
     * @function
     * @param {Array} obj Объект данных
     * @param {String} [div] Разделитель выводимых строк (по умолчанию - пустая строка)
     * @returns {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 || '');
    }
};

/**
 * Объединяет все шаги по работе с шаблонами в один вызов. Позволяет определить шаблон, задать значения данных и инициировать обработку шаблона. Возвращает результирующую строку, сформированную согласно примененным шаблонам.
 * @name y5.T
 * @memberOf y5.Template
 * @function
 * @see y5.Template.evaluate
 * @param {String} template Строка-шаблон
 * @param {Object | Args} obj Объект данных или все аргументы после строки шаблона
 * @returns {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.loaded('Template');

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

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

    /**
     * Разбивает строку по пробелам.
     * Если имя - RegExp, то возвращает массив, содержащий элемент.
     * @param {String | RegExp} names Имена классов
     * @returns {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 Имя класса
     * @returns {RegExp} Правило для нахождения класса
     * @private
     */
    rName = function(name, nocache) {
        var _name = '',
            flags = '',
            source = '';

        if (typeof name == 'string') {
            source = _name = Strings.escapeRegexp(name);
        } else {
            source = name.source;
            _name = name.toString();
            // остальные флаги не нужны, т.к. не имеют смысла для поиска класса
            flags += name.ignoreCase?'i':'';
        }

        if (!nocache) {
            if (cacheRegexp.empty(_name)) {
                return cacheRegexp.set(_name, new RegExp('(^|\\s+)' + source + '(\\s+|$)', flags));
            }
            return cacheRegexp.get(_name);
        }
        return new RegExp('(^|\\s+)' + source + '(\\s+|$)', flags);
    };

/**
 * @class Набор функций для работы с классами CSS.
 * Включает реализацию функций, упрощающих работу с CSS-классами HTML-элементов страницы.
 * @static
 * @name y5.Classes
 * @memberOf y5
 */
y5.Classes = {
    /**
     * Проверяет элемент на наличие у него заданного CSS-класса. Имя класса может быть задано строкой или регулярным выражением. Метод возвращает true, если HTML-элемент имеет требуемый класс.
     * @name y5.Classes.test
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String | RegExp} name Имя класса
     * @param {Boolean} [nocache] Не кэшировать результат проверки
     * @returns {Boolean} Результат проверки
     *
     * @example
     * // &lt;div class="one two"/&gt;
     * y5.Classes.test(element, 'two'); // -> true
     */
    test: function(elem, name, nocache) {
        checkElem('test', elem, name);

        if (name == '*') {
            return true;
        }
        try {
            if (!nocache) {
                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;
    },

    /**
     * Присваивает элементу новое имя CSS-класса вместо существующего. Метод возвращает true, если класс был успешно установлен, или false, если HTML-элемент страницы уже имеет класс с данным именем.
     * @name y5.Classes.set
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String} name Имя класса
     * @returns {Boolean} true, если класс был установлен
     *
     * @example
     * // &lt;div class="one"/>
     * y5.Classes.set(element, 'two'); // -> true
     * // &lt;div class="two"/>
     *
     * // &lt;div class="one"/>
     * y5.Classes.set(element, 'one'); // -> false
     * // &lt;div class="one"/>
     */
    set: function(elem, name) {
        checkElem('set', elem, name);
        if (elem.className != name) {
            elem.className = name;
            return true;
        }
        return false;
    },

    /**
     * Добавляет новые CSS-классы заданному элементу. В зависимости от типа передаваемых в параметре name данных, можно добавлять один или несколько классов одновременно. Если в качестве параметра name передается строка, то к существующим классам будет добавлен один новый класс с именем, соответствующим данной строке. Если в качестве параметра name передается массив, то к существующим классам будет добавлено несколько новых классов с именами, соответствующими элементам массива. Метод возвращает массив, содержащий названия добавленных классов.
     * @name y5.Classes.add
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String | Array} name Имя класса
     * @returns {Array} Добавленные классы
     *
     * @example
     * // &lt;div class="one"/>
     * y5.Classes.add(element, 'two'); // -> ['two']
     * // &lt;div class="one two"/>
     *
     * // &lt;div class="one"/>
     * y5.Classes.add(element, ['foo', 'bar']); // -> ['foo', 'bar']
     * // &lt;div class="one foo bar"/>
     *
     * // &lt;div class="one"/>
     * y5.Classes.add(element, 'one foo bar'); // -> ['one', 'foo', 'bar']
     * // &lt;div class="one foo bar"/>
     */
    add: function(elem, names) {
        var addons = split(names).filter(
            function(name) {
                return !this.test(elem, name)
            },
            this
        );

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

        return addons;
    },

    /**
     * Удаляет CSS-классы у заданного элемента. В зависимости от типа передаваемых в параметре name данных, можно удалять один или несколько классов одновременно. Если в качестве параметра name передается строка, то удаляется один класс с именем, соответствующим переданной строке. Если в качестве параметра name передается массив, то будет удалено несколько классов с именами, соответствующими элементам массива. Метод возвращает массив, содержащий названия удаленных классов.
     * @name y5.Classes.remove
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String | Array} name Имя класса
     * @returns {Array} Удаленные классы
     *
     * @example
     * // &lt;div class="one two"/>
     * y5.Classes.remove(element, 'two'); // -> ['two']
     * // &lt;div class="one"/>
     */
    remove: function(elem, names) {
        var className = elem.className;
        var removed = [];

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

        elem.className = Strings.normalizeSpace(className);

        return removed;
    },

    /**
     * Заменяет один CSS-класс на другой у заданного элемента. Имя заменяемого класса может быть задано строкой или регулярным выражением. Метод возвращает true, если класс был успешно заменен.
     * @name y5.Classes.replace
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String | RegExp} find Заменяемое имя класса
     * @param {String} replace Новое имя класса
     * @returns {Boolean} true, если класс find был заменен на replace
     *
     * @example
     * // &lt;div class="one two"/>
     * y5.Classes.replace(element, 'two', 'three'); // -> true
     * // &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'));
            return true;
        }
        return false;
    },

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

    /**
     * Устанавливает/сбрасывает CSS-класс у заданного элемента. Класс будет сброшен (удален), если HTML-элемент уже имеет класс с заданным именем. Класс будет установлен (добавлен), если у HTML-элемента нет класса с заданным именем. Метод возвращает true в случае установки CSS-класса или false в случае его сброса.
     * @name y5.Classes.toggle
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String} name Имя класса
     * @returns {Boolean} true, если класс был добавлен
     *
     * @example
     * // &lt;div class="one two"/>
     * y5.Classes.toggle(element, 'two'); // -> false
     * // &lt;div class="one"/>
     *
     * // &lt;div class="one"/>
     * y5.Classes.toggle(element, 'two'); // -> true
     * // &lt;div class="one two"/>
     */
    toggle: function (elem, name) {
        var added = !this.test(elem, name);
        this.assign(elem, name, added);
        return added;
    },

    /**
     * Переключает CSS-класс у заданного HTML-элемента страницы с одного значения на другое. В качестве параметров передаются два имени классов, и HTML-элементу присваивается тот, который отличается от текущего. Метод возвращает имя CSS-класса, на который был переключен исходный класс.
     * @name y5.Classes.swap
     * @memberOf y5.Classes
     * @function
     * @param {HTMLElement} element Элемент
     * @param {String} name1 Имя класса
     * @param {String} name2 Имя класса
     * @returns {String} Класс, который заменил собой искомый
     *
     * @example
     * // &lt;div class="one"/>
     * y5.Classes.swap(element, 'two', 'one'); // -> 'two'
     * // &lt;div class="two"/>
     *
     * // &lt;div class="two"/>
     * y5.Classes.swap(element, 'two', 'one'); // -> 'one'
     * // &lt;div class="one"/>
     */
    swap: function (elem, one, two) {
        if (this.test(elem, one)) {
            this.replace(elem, one, two);
            return two;
        } else if (this.test(elem, two)) {
            this.replace(elem, two, one);
        } else {
            this.add(elem, one);
        }
        return one;
    }
};

y5.loaded('Classes');

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

var Strings = y5.Strings,
    Types = y5.Types,
    cssTextSplitRegexp = /\s*;\s*/g,
    styleSplitRegexp = /\s*:\s*/,
    isHTML = /[<>\s]/,
    pxTest = /\d+px/,
    pxExcept = /z-?index|font-?weight|opacity|zoom|line-?height/i;

/**
 * Устанавливает значение CSS-свойства элемента DOM.
 * @function
 * @private
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'margin-left'
 * @param {String} value Значение
 * @returns {Element}
 */
function setPropertyValue (element, property, value) {
    element.style[Strings.camelize(property)] = value;
    return element;
}

/**
 * Возвращает вычисленное значение CSS-свойства элемента DOM.
 * @function
 * @private
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'margin-left'
 * @returns {String} Значение свойства, например '5px'
 */
function getPropertyValue (element, property) {
    return y5.Elements.getStyle(element).getPropertyValue(Strings.dasherize(property));
}

/**
 * @class Модуль для создания и работы с элементами DOM
 * @static
 * @name y5.Elements
 */
y5.Elements = {
    /**
     * Создает элемент DOM в соответствии с заданными параметрами
     * @name y5.Elements.create
     * @memberOf y5.Elements
     * @function
     * @param {String} tagName Имя элемента или html-разметка
     * @param {Object} [attributes] Хэш атрибутов вида {name:'...', ...}
     * @param {String} [innerHTML] HTML-код
     * @returns {Element} Элемент
     */
    create: function(tagName, attributes, innerHTML) {
        var element;
        // deprecated: tagName - объект вида {tagName:'...', attributes: {name:'...', ...}}
        if (!Types.string(tagName)) {
            attributes = tagName.attributes;
            tagName = tagName.tagName;
        }

        // если имя тэга
        if (!isHTML.test(tagName)) {
            if (Strings.compare(tagName, 'style', true) == 0) {
                // да, этот код не содержит workaround для создания в IE тега STYLE с атрибутом NAME
                element = document.createElement('div');
                element.innerHTML = '<p>x<\/p><style>' + (innerHTML || attributes.innerHTML || '') + '<\/style>';
                element = element.childNodes[1];
                innerHTML = undefined;
                delete attributes.innerHTML;

            } else {
                // для избежания ошибок от создания неправильных тэгов
                try {
                    if (y5.is_ie && attributes && attributes.name) {
                        element = document.createElement('<' + tagName + ' name="' + attributes.name + '"/>');
                        delete attributes.name;
                    } else {
                        element = document.createElement(tagName);
                    }
                } catch (e) {}
            }
        }

        // если первый аргумент - html-разметка
        if (!element) {
            element = document.createElement('div');

            // если создаем stylе, то для IE6/IE7 надо добавить в начало тэг <p>
            if (Strings.startsWith(tagName, '<style', true)) {
                element.innerHTML = '<p>x<\/p>' + tagName;
                element = element.childNodes[1];

            } else {
                element.innerHTML = tagName;
                element = element.firstChild;
            }
        }

        if (Types.object(attributes)) {
            this.setAttributes(element, attributes);
        }

        if (Types.def(innerHTML)) {
            this.setHTML(element, innerHTML);
        }
        
        return element;
    },

    /**
     * Устанавливает набор атрибутов элементу DOM.
     * @name y5.Elements.setAttributes
     * @memberOf y5.Elements
     * @function
     * @param {Element} element Элемент DOM
     * @param {Object} [attributes] Список атрибутов, вида {name:'test', ...}
     */
    setAttributes: function(element, attributes) {
        if (!attributes) return;
        
        var name,
            value,
            styles,
            stylesLength,
            style,
            i;
            

        for (name in attributes) {
            value = attributes[name];

            switch (name) {
                case 'style':
                case 'cssText':
                    if (element.style.cssText && !(value.indexOf('opacity') != -1 && y5.is_ie)) {
                        element.style.cssText = value;
                    } else {
                        styles = value.split(cssTextSplitRegexp);
                        stylesLength = styles.length;
                        for (i = 0; i < stylesLength; i++) {
                            style = styles[i].split(styleSplitRegexp);
                            setPropertyValue(element, style[0], style[1]);
                        }
                    }
                    break;

                case 'class':
                case 'className':
                    element.className = value;
                    break;

                case 'innerHTML':
                    element.innerHTML = value;
                    break;

                default:
                    element.setAttribute(name, value);
            }
        }
    },

    /**
     * Формирует содержимое элемента DOM из строки HTML-разметки.
     * @name y5.Elements.setHTML
     * @memberOf y5.Elements
     * @function
     * @param {Element} element Элемент DOM
     * @param {String} html HTML-код
     * @returns {Element} Элемент
     */
    setHTML: function(element, html) {
        element.innerHTML = html;
        return element;
    },

    /**
     * <p>Метод позволяет работать с CSS-свойствами DOM-элемента.</p>
     * <p>Имена свойств можно задавать как в CSS-стиле (например margin-top), так и в JavaScript-стиле (marginTop).</p>
     * <p>При установке свойств к значениям имеющим тип number, автоматически прибавляется постфикс "px".</p>
     * <p>При чтении свойств значения имеющие постфикс "px" приводятся к типу number.</p>
     * <p>Логика работы зависит от переданных аргументов:</p>
     * <p>y5.Elements.css(element, propertyName) - возвращает значение CSS-свойства.</p>
     * <p>y5.Elements.css(element, propertyName, propertyValue) - устанавливает значение CSS-свойства.</p>
     * <p>y5.Elements.css(element, priopertyHash) - устанавливает CSS-свойства перечисленные в хеше.</p>
     * @function
     * @name y5.Elements.css
     * @memberOf y5.Elements
     * @param {HTMLElement} element DOM-элемент, для которого осуществляется работа с CSS-свойствами.
     * @param {String | Object} [property] Имя свойства или хеш значений.
     * @param {String | Number} [propertyValue] Значение.
     * @returns {String | Number} Значение CSS-свойства.
     * @emample
     * // Установка нескольких CSS-свойств.
     * y5.Elements.css(element, {
     *      top: 10,
     *      left: "10%",
     *      'margin-left': "1em",
     *      zIndex: 100
     * });
     * // Установка одного CSS-свойства.
     * y5.Elements.css(element, "left", 10);
     * // Получение значения CSS-свойства.
     * var left = y5.Elements.css(element, "left"); -> 10
     */
    css: function (element, property, value) {
        var propertyHash = property,
            prop;

        if (Types.string(property)) {
            // если нет value, то возвращаем значение свойства элемента
            if (Types.undef(value)) {
                value = getPropertyValue(element, property);
                // opera fix
                if (property == 'opacity') {
                    return parseFloat(value, 10);
                }
                return pxTest.test(value) ? parseInt(value, 10) : value;
            } else {
                propertyHash = {};
                propertyHash[property] = value;
            }
        }

        for (prop in propertyHash) {
            setPropertyValue(
                element,
                prop,
                Types.number(value = propertyHash[prop]) && !pxExcept.test(prop) ? value + "px" : value
            );
        }
    },

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

var Elements = y5.Elements;

/**
 * @name createElement
 * @memberOf y5.Elements
 * @function
 * @deprecated y5.Elements.create
 */
Elements.createElement = Elements.create;

/**
 * @name setElementAttributes
 * @memberOf y5.Elements
 * @function
 * @deprecated y5.Elements.setAttributes
 */
Elements.setElementAttributes = Elements.setAttributes;

/**
 * @name createElementWithName
 * @memberOf y5.Elements
 * @function
 * @deprecated y5.Elements.createWithName
 */
Elements.createElementWithName = Elements.createWithName;

/**
 * @name createElementFromHTML
 * @memberOf y5.Elements
 * @function
 * @deprecated y5.Elements.createFromHTML
 */
Elements.createElementFromHTML = Elements.create;

/**
 * Создает элемент DOM с заданным параметром name.
 * @name y5.Elements.createWithName
 * @memberOf y5.Elements
 * @function
 * @param {String} tag Имя элемента (имя тега)
 * @param {String} name Атрибут name элемента
 * @returns {Element} Элемент
 * @deprecated y5.Elements.create
 */
Elements.createWithName = function(tag, name) {
    return Elements.create(tag, {name: name});
};

/**
 * Создает элемент DOM из строки HTML-разметки.
 * @name y5.Elements.createFromHTML
 * @memberOf y5.Elements
 * @function
 * @param {String} html Текст HTML
 * @returns {Element} Первый элемент из списка созданных
 * @deprecated y5.Elements.create
 */
Elements.createFromHTML = Elements.create;

/**
 * Возвращает вычисленное значение CSS-свойства элемента DOM.
 * @name y5.Elements.getPropertyValue
 * @memberOf y5.Elements
 * @function
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'margin-left'
 * @returns {String} Значение свойства, например '5px'
 * @deprecated y5.Elements.css
 */
Elements.getPropertyValue = Elements.css;

/**
 * Возвращает вычисленное целое значение CSS-свойства элемента.
 * @name y5.Elements.getPropertyValuePx
 * @memberOf y5.Elements
 * @function
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'margin-left'
 * @returns {String} Значение свойства, например 5
 * @deprecated y5.Elements.css
 */
Elements.getPropertyValuePx = Elements.css;

/**
 * Возвращает вычисленное числовое значение CSS-свойства элемента DOM.
 * @name y5.Elements.getPropertyValueFloat
 * @memberOf y5.Elements
 * @function
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'opacity'
 * @returns {String} Значение свойства, например 0.5
 * @deprecated y5.Elements.css
 */
Elements.getPropertyValueFloat = Elements.css;

/**
 * Устанавливает значение CSS-свойства элемента DOM.
 * @name y5.Elements.setPropertyValue
 * @memberOf y5.Elements
 * @function
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'margin-left'
 * @param {String} value Значение
 * @deprecated y5.Elements.css
 */
Elements.setPropertyValue = Elements.css;

/**
 * Устанавливает значение CSS-свойства элемента DOM, предварительно добавив к нему суффикс 'px'.
 * @name y5.Elements.setPropertyValuePx
 * @memberOf y5.Elements
 * @function
 * @param {Element} element Элемент
 * @param {String} propname Свойство, например 'margin-left'
 * @param {Number} value Значение
 * @deprecated y5.Elements.css
 */
Elements.setPropertyValuePx = Elements.css;

// IE
if (Types.undef(document.defaultView)) {

    Elements.getStyle = function(element) {
        return element.currentStyle || element.runtimeStyle;
    };

   getPropertyValue = function(element, propname) {
        var name = Strings.camelize(propname),
            value;
        switch (name) {
            case 'opacity':
                value = 100;
                try {
                    value = element.filters['DXImageTransform.Microsoft.Alpha'].opacity;
                } catch (e) {
                    try {
                        value = element.filters('alpha').opacity;
                    } catch(e) { }
                }
                return (value / 100).toString();

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

            // возвращаем пиксельные значения размеров
            case 'width':
            case 'height':
            case 'top':
            case 'right':
            case 'bottom':
            case 'left':
                var set = false;
                if (!element.style[name]) {
                    element.style[name] = Elements.getStyle(element)[name];
                    set = true;
                }

                value = element.style['pixel' + Strings.capitalize(name)];

                if (set) {
                    element.style[name] = null;
                }

                return value;
        }

        return Elements.getStyle(element)[name];
    };

    function cutAlphaFilter(filter){
        return filter.replace(/alpha\s*\([^\)]*\)/ig, '');
    }

    var setPropertyValueOld = setPropertyValue;

    setPropertyValue = function(element, propname, value) {
        switch (propname) {
            case 'opacity':
                var filter = getPropertyValue(element, 'filter');
                var style = element.style;
                if (value == 1) {
                    filter = cutAlphaFilter(filter);
                    if (filter) {
                        style.filter = filter;
                    } else {
                        style.removeAttribute('filter')
                    }
                    return element;
                } else if (value < 0.00001) {
                    value = 0;
                }
                // fix opacity bug
                if (!style.zoom) {
                    style.zoom = 1;
                }
                style.filter = cutAlphaFilter(filter) + 'alpha(opacity=' + (value * 100) + ')';
                break;

            default:
                setPropertyValueOld(element, propname, value);
        }

        return element;
    };
}

y5.loaded('Elements');

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

var Types = y5.Types,
    Classes = y5.Classes,
    Strings = y5.Strings,
    Elements = y5.Elements,
    UNDEF = y5.UNDEF,
    Asterix = '*',
    ParentNode = 'parentNode',
    PreviousSibling = 'previousSibling',
    NextSibling = 'nextSibling';

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

/* Функция преобразования единиц измерения */
function unitConvert(value, element, func, length) {
    element = element || y5.Dom.getBody();

    var elem = Elements.create('span', {style: 'position:absolute;display:block;visibility:hidden;width:100' + length});
    var node = element.appendChild(elem);
    var result = func(value, node.clientWidth, 100);
    element.removeChild(node);

    return result;
}

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

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

function elements2array(elements, limit) {
    var length = elements.length;

    if (typeof limit != UNDEF) {
        length = Math.min(limit, length);
    }

    var nodes = new Array(length),
        i = 0;

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

    return nodes;
}

function getElementsByTagName (context, tagName) {
    if (tagName === Asterix) {
        return getAll(context);

    } else if (Types.string(tagName)) {
        return elements2array(context.getElementsByTagName(tagName));

    } else {
        var elements = [],
            i = 0,
            j = tagName.length;
        for (; i<j; i++) {
            if (tagName[i] === Asterix) {
                return getAll(context);
            }
            elements = elements.concat(getElementsByTagName(context, tagName[i]));
        }
        return elements;
    }
}

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

var getAll;
if (y5.is_ie6down) {
    getAll = function(context) {
        return context.all;
    };
} else {
    getAll = function(context) {
        return context.getElementsByTagName(Asterix);
    };
}

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


/// Функции выбора элементов
/////////////////////////////////////////////////////////////////////

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

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

    /**
     * Возвращает список потомков по имени и классу.
 	 * @name getDescendants
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @param {Number} [limit] Лимит количества найденных элементов
     * @returns {Array} Результат поиска
     */
    getDescendants: function(context, tagName, className, limit) {
        return this.filterElements(getElementsByTagName(context, tagName || Asterix), Asterix, className, limit);
    },

    /**
     * Возвращает список потомков узла и/или сам узел по имени и классу.
 	 * @name getDescendantsOrSelf
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @param {Number} [limit] Лимит количества найденных элементов
     * @returns {Array} Результат поиска
     */
    getDescendantsOrSelf: function(context, tagName, className, limit) {
        return this.filterElements(getElementsByTagName(context, tagName || Asterix), tagName, className, limit, context);
    },

    /**
     * Возвращает первого потомка по имени и классу.
 	 * @name getDescendant
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getDescendant: function(context, tagName, className) {
        return getFirstFromSet(this.getDescendants(context, tagName, className, 1));
    },

    /**
     * Возвращает первого потомка по имени и классу.
 	 * @name getDescendantOrSelf
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getDescendantOrSelf: function(context, tagName, className) {
        return getFirstFromSet(this.getDescendantsOrSelf(context, tagName, className, 1));
    },

    /**
     * Возвращает элементы-предки по имени и классу.
 	 * @name getAncestors
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Array} Результат поиска
     */
    getAncestors: function(context, tagName, className) {
        return this.getElementsByType(context, tagName, className, ParentNode);
    },

    /**
     * Возвращает элементы-предки узла и/или сам узел по имени и классу.
 	 * @name getAncestorsOrSelf
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Array} Результат поиска
     */
    getAncestorsOrSelf: function(context, tagName, className) {
        return this.getElementsByType(context, tagName, className, ParentNode, context);
    },

    /**
     * Возвращает первый элемент-родитель по имени и классу.
 	 * @name getAncestor
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getAncestor: function(context, tagName, className) {
        return this.getElementByType(context, tagName, className, ParentNode);
    },

    /**
     * Возвращает первый элемент-родитель узла и/или сам узел по имени и классу.
 	 * @name getAncestorOrSelf
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getAncestorOrSelf: function(context, tagName, className) {
        return this.getElementByType(context, tagName, className, ParentNode, context);
    },

    /**
     * Возвращает дочерние элементы по имени и классу.
 	 * @name getChildren
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Array} Результат поиска
     */
    getChildren: function(context, tagName, className, limit) {
        return this.filterElements(context.childNodes, tagName, className, limit);
    },

    /**
     * Возвращает первый дочерний элемент по имени и классу.
 	 * @name getChild
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getChild: function(context, tagName, className) {
        return getFirstFromSet(this.getChildren(context, tagName, className, 1));
    },

    /**
     * Возвращает предыдущие элементы по имени и классу.
 	 * @name getPreceding
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getPreceding: function(context, tagName, className) {
        return this.getElementsByType(context, tagName, className, PreviousSibling);
    },

    /**
     * Возвращает первый предыдущий элемент по имени и классу.
 	 * @name getPrev
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getPrev: function(context, tagName, className) {
        return this.getElementByType(context, tagName, className, PreviousSibling);
    },

    /**
     * Возвращает следующие элементы по имени и классу.
 	 * @name getFollowing
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getFollowing: function(context, tagName, className) {
        return this.getElementsByType(context, tagName, className, NextSibling);
    },

    /**
     * Возвращает первый следующий элемент по имени и классу.
 	 * @name getNext
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Element | null} Результат поиска
     */
    getNext: function(context, tagName, className) {
        return this.getElementByType(context, tagName, className, NextSibling);
    },

    /// частные случаи

    /**
     * Возвращает потомков с заданным именем и классом.
     * @param {Array | String} tagName Имя элемента ('*' - любое имя)
     * @param {String} className Имя класса
     * @param {Element} [context] Контекст поиска (если не указан, то используется document)
     * @param {Number} [limit] Ограничить количество элементов
     * @returns {Array of Elements} Набор узлов документа
     * @deprecated y5.Dom.getDescendants
     */
    getElementsByTagNameAndClass: function(tagName, className, context, limit) {
        return this.getDescendants(context || document, tagName, className, limit);
    },

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

    /**
     * Возвращает первого потомка с заданным именем.
     * @param {Array | String} tagName Имя элемента ('*' - любое имя)
     * @param {Element} [context] Контекст поиска (если не указан, то используется document)
     * @returns {Element} Элемент
     * @deprecated y5.Dom.getDescendants
     */
    getElementsByTagName: function(tagName, context, limit) {
        return this.getDescendants(context || document, tagName, Asterix, limit);
    },

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

    /**
     * Возвращает потомков с заданным классом.
 	 * @name getElementsByClass
 	 * @memberOf y5.Dom
 	 * @function
     * @param {String} className Имя класса
     * @param {Element} [context] Контекст поиска (если не указан, то используется document)
     * @param {Number} [limit] Ограничить количество элементов
     * @returns {Array of Elements} Набор узлов документа
     */
    getElementsByClass: function(className, context, limit) {
        return this.getDescendants(context || document, Asterix, className, limit);
    },

    /**
     * Возвращает первого потомка с заданным классом.
     * Эквивалентно вызову getElementsByClass с параметром limit == 1.
 	 * @name getElementByClass
 	 * @memberOf y5.Dom
 	 * @function
     * @param {String} className Имя класса
     * @param {Element} [context] Контекст поиска (если не указан, то используется document)
     * @returns {Element} Узел документа
     */
    getElementByClass: function(className, context) {
        return getFirstFromSet(this.getElementsByClass(className, context, 1));
    },

    /**
     * Возвращает первого предка с заданным именем.
     * @param {Element} context Контекст поиска
     * @param {Array | String} [tagName] Имя элемента ('*' - любое имя)
     * @returns {Element} Узел документа
     * @deprecated y5.Dom.getAncestorOrSelf
     */
    getParentByTagName: function(context, tagName) {
        return this.getAncestorOrSelf(context, tagName, Asterix);
    },

    /**
     * Возвращает первого предка с заданным именем класса.
     * @param {String} className Имя класса
     * @param {Element} context Контекст поиска
     * @returns {Element} Узел документа
     * @deprecated y5.Dom.getAncestorOrSelf
     */
    getParentByClass: function(className, context) {
        return this.getAncestorOrSelf(context, Asterix, className);
    },


/// Тесты и фильтры
/////////////////////////////////////////////////////////////////////

    /**
     * Фильтрует список элементов по имени тега и класса.
 	 * @name filterElements
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Array of Elements} elements Список элементов
     * @param {Array | String} [tagName] Имя (имена) элемента для проверки ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @param {Number} [limit] Лимит количества найденных элементов
     * @returns {Array of Elements} Результат фильтрации
     * @optimize
     */
    filterElements: function(elements, tagName, className, limit, selfNode) {
        var element, result = [], i = 0, j = 0;
        limit = limit || -1;

        if (selfNode) {
            if (this.testElement(selfNode, tagName, className)) {
                result[j++] = selfNode;
            }
        }

        while (j != limit && (element = elements[i++])) {
            if (this.testElement(element, tagName, className)) {
                result[j++] = element;
            }
        }

        return result;
    },

    /*
    filterByClass: function(elements, className, limit) {
        return this.filterElements(elements, Asterix, className, limit);
    },

    filterByTagName: function(elements, tagName, limit) {
        return this.filterElements(elements, tagName, Asterix, limit);
    },
    */

    /**
     * Проверяет элемент на имя и класс.
 	 * @name testElement
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} element Проверяемый элемент
     * @param {Array | String} [tagName] Имя (имена) элемента для проверки ('*' - любое имя)
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Boolean} Результат проверки
     */
    testElement: function(element, tagName, className) {
        return (
            this.testTagName(element, tagName) &&
            this.testClassName(element, className)
        );
    },

    /**
     * Проверяет имя элемента.
 	 * @name testTagName
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} element Элемент для проверки
     * @param {Array | String} [tagName] Имя (имена) элемента для проверки ('*' - любое имя)
     * @returns {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 || !element.tagName) {
            return false;
        }

        if ((tagName || Asterix) == Asterix) {
            return true;
        }

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

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

        return false;
    },

    /**
     * Проверяет имя класса элемента.
 	 * @name testClassName
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} element Элемент для проверки
     * @param {String | RegExp} [className] Имя класса ('*' - любое имя)
     * @returns {Boolean} Результат проверки
     *
     * @example
     * // &lt;div class="foo"/>
     * y5.Dom.testClassName(div, 'foo');
     * // -> true
     * y5.Dom.testClassName(div, 'bar');
     * // -> false
     * y5.Dom.testClassName(div, '*');
     * // -> true
     */
    testClassName: function(element, className) {
        return Classes.test(element, className || Asterix);
    },

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


/// Функции удаления, замены узлов
/////////////////////////////////////////////////////////////////////

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

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

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

    /**
     * Заменяет один элемент на другой.
 	 * @name replaceNode
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} element Заменяемый элемент
     * @param {Element | String} [newElement] Новый элемент или строка HTML
     * @todo генерировать уникальный ID
     */
    replaceNode: function(element, newElement) {
        switch (typeof newElement) {
            case 'string':
                if (element.outerHTML) {
                    if (newElement.indexOf('<') == 0) {
                        var spanId = '__outer_span__';
                        element.outerHTML = '<span id="' + spanId + '">&#160;</span>' + newElement;

                        var span = y5.$(spanId);
                        span.parentNode.removeChild(span);
                    } else {
                        element.outerHTML = newElement;
                    }
                } else {
                    var replaced;

                    // для FF 1.0, вставка пустого элемента
                    if (Strings.normalize(newElement) == '') {
                        replaced = document.createTextNode(newElement)
                    } else {
                        var range = element.ownerDocument.createRange();
                        range.selectNodeContents(element);
                        replaced = range.createContextualFragment(newElement);
                    }
                    element.parentNode.replaceChild(replaced, element);
                }
                break;

            default:
                element.parentNode.replaceChild(newElement, element);
                break;
        }
    },

    /**
     * Возвращает текстовое содержимое элемента.
 	 * @name textContent
 	 * @memberOf y5.Dom
 	 * @function
     * @function
     * @param {Element} element Элемент
     * @returns {String} Текст
     */
    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'), '');
            }
        }
    })(),


/// Функции вставки узлов
/////////////////////////////////////////////////////////////////////

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

    /**
     * Вставляет новый элемент после указанного.
 	 * @name insertAfter
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} newElem Элемент для вставки
     * @param {Element} refElem Элемент после которого будет вставлен новый элемент
     * @returns {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);
    },


/// Функции для работы со стилями и свойствами элементов
/////////////////////////////////////////////////////////////////////

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

    /**
     * Возвращает левое и верхнее смещение одного элемента относительно другого.
     * Если второй аргумент не указан, то смещение считается от document.
 	 * @name getOffset
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} targetElement Элемент, для которого считается смещение
     * @param {Element} [relativeElement] Элемент, относительно которого считается смещение
     * @returns {Array} [левое смещение, верхнее смещение]
     */
    getOffset: function(targetElement, relativeElement) {
        // функция используется в Safari и Opera
        var elementX = 0,
            elementY = 0,
            relativeElementOffset = [0,0],
            body = this.getBody(),
            position, pStatic, pRelative, inlineElement;

        if (!relativeElement || !Types.element(relativeElement)) {
            relativeElement = document;
        }

        /*
         * если считаем в opera смещение для inline-элемента,
         * то надо вместо целевого элемента подсовывать фейковый span и считать для него offset,
         * т.к. для многострочного элемента offsetLeft будет неверным.
         */ 
        if (y5.is_opera && Elements.css(targetElement, 'display') == 'inline') {
            // если есть margin-left, то фейковый элемент будет смещен влево на это значение
            elementX = Elements.css(targetElement, 'margin-left');
            inlineElement = Elements.create('span');
            this.insertBefore(inlineElement, targetElement);
            targetElement = inlineElement;
        }

        while (targetElement !== null && targetElement !== relativeElement) {
            // offset added
            elementX += targetElement.offsetLeft || 0;
            elementY += targetElement.offsetTop || 0;

            if (!(y5.is_konq || (y5.is_opera && y5.opera_ver > 8.6))) {
                position = Elements.css(targetElement, 'position');
                pStatic = position == 'static';
                pRelative = position == 'relative';

                if (pStatic || (!y5.is_opera && pRelative)) {
                    elementX += Elements.css(targetElement, 'border-left-width');
                    elementY += Elements.css(targetElement, 'border-top-width');

                    if (y5.is_ie && targetElement !== body) {
                        elementX += Elements.css(targetElement, 'margin-left');
                        elementY += Elements.css(targetElement, 'margin-top');
                    }
                }
            }

            // offsetParent указывает на первый внешний блочный элемент с position:relative, от которого отсчитываются координаты текущего элемента.
            targetElement = targetElement.offsetParent;
        }

        if (Types.element(inlineElement)) {
            this.removeNode(inlineElement);
        }

        // если предок не document и не offsetParent, то считаем offset у предка
        if (relativeElement !== document && targetElement !== relativeElement) {
            relativeElementOffset = this.getOffset(relativeElement);
        }

        return [elementX-relativeElementOffset[0], elementY-relativeElementOffset[1]];
    },

    /**
     * Возвращает верхнее смещение одного элемента относительно другого.
     * Если второй аргумент не указан, то смещение считается от document.
 	 * @name offsetTop
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} targetElement Элемент, для которого считается смещение
     * @param {Element} [relativeElement] Элемент, относительно которого считается смещение
     * @returns {Number} Верхнее смещение
     */
    offsetTop: function(targetElement, relativeElement) {
        return this.getOffset(targetElement, relativeElement)[1];
    },

    /**
     * Возвращает левое смещение одного элемента относительно другого.
     * Если второй аргумент не указан, то смещение считается от document.
 	 * @name offsetLeft
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} targetElement Элемент, для которого считается смещение
     * @param {Element} [relativeElement] Элемент, относительно которого считается смещение
     * @returns {Number} Левое смещение
     */
    offsetLeft: function(targetElement, relativeElement) {
        return this.getOffset(targetElement, relativeElement)[0];
    },
    
    getDimensions: function(element) {
        var display = Elements.css(element, 'display');
        // если элемент не скрыт
        if (display != 'none' && display != null) {
            return [element.offsetWidth, element.offsetHeight];
        }
        
        // для элементов c display=none ширина и высота равна 0
        var style = element.style,
            originalVisibility = style.visibility,
            originalPosition = style.position,
            originalDisplay = style.display,
            dimensions;
        
        style.visibility = 'hidden';
        style.position = 'absolute';
        style.display = 'block';
        
        dimensions = [element.offsetWidth, element.offsetHeight];
        
        style.display = originalDisplay;
        style.position = originalPosition;
        style.visibility = originalVisibility;
        
        return dimensions;
    },
    
    getWidth: function (element) {
        return this.getDimensions(element)[0];
    },
    
    getHeight: function (element) {
        return this.getDimensions(element)[1];
    },

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

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

    /**
     * Возвращает вычисленные стили элемента.
     * @param {Element} element Элемент
     * @returns {Object} Стили
     * @deprecated y5.Elements.getStyle
     */
    getStyle: function(element) {
        return Elements.getStyle(element);
    },

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

    /**
     * Возвращает вычисленное числовое значение CSS-свойства элемента.
     * @param {Element} element Элемент
     * @param {String} propname Свойство, например 'margin-left'
     * @returns {String} Значение свойства, например '5'
     * @deprecated y5.Elements.getPropertyValuePx
     */
    getPropertyValuePx: function(element, propname) {
        return Elements.getPropertyValuePx(element, propname);
    },

    /**
     * Преобразовывает значение свойства элемента из em в px.
 	 * @name em2px
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Number} em Значение в em
     * @param {Element} element Элемент (должен быть видимым).
     * @returns {Number} Результат в px
     * @todo перенести в y5.CSS
     */
    em2px: function(em, element) {
        return unitConvert(em, element, toPxConvert, 'em');
    },

    /**
     * Преобразовывает значение свойства элемента из px в em.
 	 * @name px2em
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Number} px Значение в px
     * @param {Element} element Элемент (должен быть видимым).
     * @returns {Number} Результат в em
     * @todo перенести в y5.CSS
     */
    px2em: function(px, element) {
        return unitConvert(px, element, fromPxConvert, 'em');
    },

    /**
     * Преобразовывает значение свойства элемента из unit (em, %, in,...) в px.
 	 * @name unit2px
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Number} length Значение в unit
     * @param {Element} element Элемент (должен быть видимым).
     * @returns {Number} Результат в px
     * @todo перенести в y5.CSS
     */
    unit2px: function(unit, element, type) {
        return unitConvert(unit, element, toPxConvert, type);
    },

    /**
     * Преобразовывает значение свойства элемента из px в unit (em, %, in,...).
 	 * @name px2unit
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Number} px Значение в px
     * @param {Element} element Элемент (должен быть видимым).
     * @returns {Number} Результат в unit
     * @todo перенести в y5.CSS
     */
    px2unit: function(px, element, type) {
        return unitConvert(px, element, fromPxConvert, type);
    },


/// private
/////////////////////////////////////////////////////////////////////

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

        return null;
    },

    /**
     * Возвращает список элементов заданного типа.
 	 * @name getElementsByType
 	 * @memberOf y5.Dom
 	 * @function
     * @param {Element} context Контекст поиска
     * @param {String} tagName Имя элемента ('*' - любое имя)
     * @param {String} className Имя класса ('*' - любое имя)
     * @param {Boolean} type Тип элемента: nextSibling, previousSibling, firstChild, parentNode...
     * @returns {Array} Результат поиска
     * @private
     */
    getElementsByType: function(context, tagName, className, type, selfNode) {
        var result = [];

        context = selfNode || context[type];
        while (context) {
            if (this.testElement(context, tagName, className)) {
                result.push(context);
            }
            context = context[type];
        }

        // для определенных типов меняем последовательность,
        // чтобы узлы в массиве шли в том же порядке, как в документе
        if (type == PreviousSibling || type == ParentNode) {
            return result.reverse();
        }

        return result;
    },

    /**
     * Возвращает элементы по запросу XPath.
 	 * @name getElementsByXPath
 	 * @memberOf y5.Dom
 	 * @function
     * @param {String} expression Выражение XPath
     * @param {Element} [context] Контекст запроса (если не указан, то document)
     * @returns {Array of Elements} Набор узлов документа
     * @private
     */
    getElementsByXPath: function(expression, context) {
        var evaluator = new XPathEvaluator();
        var query = evaluator.evaluate(expression, context, 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.
 	 * @name getElementByXPath
 	 * @memberOf y5.Dom
 	 * @function
     * @param {String} expression Выражение XPath
     * @param {Element} [context] Контекст запроса (если не указан, то document)
     * @returns {Array of Elements} Набор узлов документа
     * @private
     */
    getElementByXPath: function(expression, context) {
        var evaluator = new XPathEvaluator();
        var query = evaluator.evaluate(expression, context, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

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

var Dom = y5.Dom;

if (Dom.XPathSupport) {

    function tagNameForXPath(tagName) {
    	switch (Types.type(tagName)) {
    	    case Types.STRING:
    	        return tagName.toLowerCase();

    	    case Types.ARRAY:
                var i = 0,
                    length = tagName.length,
                    exprs = [],
                    name;

                if (length == 1) {
                    return tagName[0].toLowerCase();
                }

                for (; i < length; i++) {
                    name = tagName[i];
                    if (name != Asterix) {
                        exprs.push("name()='" + name.toLowerCase() + "'");
                        exprs.push("name()='" + name.toUpperCase() + "'");
                    } else {
                        return Asterix;
                    }
                }
	
                return Asterix + '[' + exprs.join(' or ') + ']';
    	}
    	
    	return Asterix;
    }

    function classNameForXPath(className) {
        if (className && className != Asterix) {
            return "[contains(concat(' ',@class,' '),' " + className + " ')]";
        }
        return '';
    }

    Dom.getByAxis = function(context, axis, tagName, className, limit) {
        var expression = axis + tagNameForXPath(tagName);

        if (typeof className == 'string') {
            expression += classNameForXPath(className);
            if (limit) {
                expression += '[position()<=' + limit + ']';
            }
            return this.getElementsByXPath(expression, context);
        } else {
            return this.filterElements(this.getElementsByXPath(expression, context), Asterix, className, limit);
        }
    };

    var axis = {
        getDescendants: 'descendant',
        getDescendantsOrSelf: 'descendant-or-self',
        getAncestors: 'ancestor',
        getAncestorsOrSelf: 'ancestor-or-self',
        getChildren: 'child',
        getFollowing: 'following-sibling',
        getPreceding: 'preceding-sibling'
    };

    for (var i in axis) {
        Dom[i] = (function(a) {
            return function(context, tagName, className, limit) {
                return this.getByAxis(context, a + '::', tagName, className, limit);
            };
        })(axis[i]);
    }
}

if (document.getElementsByClassName) {
    Dom.__getElementsByClass = Dom.getElementsByClass;
    Dom.getElementsByClass = function(className, context, limit) {
        if (Types.regexp(className)) {
            return this.__getElementsByClass(className, context, limit);
        }
        return elements2array((context || document).getElementsByClassName(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(targetElement, relativeElement) {
        if (targetElement === document) {
            return [0, 0];
        }

        if (targetElement === document.body) {
            return [
                document.body.clientLeft + document.documentElement.clientLeft,
                document.body.clientTop + document.documentElement.clientTop
            ];
        }

        if (!Types.element(relativeElement)) {
            relativeElement = document;
        }

        var targetElementOffset,
            relativeElementOffset = (relativeElement === document || relativeElement === document.body ?
                // IE in quirks and standards modes has body and html borders
                {
                    left: document.body.clientLeft + document.documentElement.clientLeft,
                    top: document.body.clientTop + document.documentElement.clientTop
                } :
                relativeElement.getBoundingClientRect()
            );

        /*
         * для inline элементов, которые располагаются на нескольких строках,
         * надо брать координаты первого текстового прямоугольника, т.к. getBoundingClientRect
         * возвращает координаты объединенной области, а это минимальные x и y
         */ 
        if (Elements.css(targetElement, 'display') == 'inline') {
            var rects = targetElement.getClientRects();
            targetElementOffset = {left: rects[0].left, top: rects[0].top};

        } else {
            targetElementOffset = targetElement.getBoundingClientRect();
        }

        // FF returns float value => Math.round
        // IE adds border width => -document.documentElement.clientLeft and Top
        return [
            Math.round(targetElementOffset.left - relativeElementOffset.left + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft)),
            Math.round(targetElementOffset.top - relativeElementOffset.top + Math.max(document.documentElement.scrollTop,  document.body.scrollTop))
        ];
    };

// getBoxObjectFor is deprecated in ff3
} else if (document.getBoxObjectFor) { // Gecko 1.0.1+

    Dom.getOffset = function(targetElement, relativeElement) {
        if (targetElement === document) {
            return [0, 0];
        }

        if (!Types.element(relativeElement)) {
            relativeElement = document;
        }

        var relativeElementOffset = {x:0,y:0},
            targetElementOffset = document.getBoxObjectFor(targetElement);

        if (relativeElement !== document) {
            relativeElementOffset = document.getBoxObjectFor(relativeElement);
        }

        return [targetElementOffset.x-relativeElementOffset.x, targetElementOffset.y-relativeElementOffset.y];
    };

}

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

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

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

}

/// aliases

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

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

/**
 * @name getNextElement
 * @memberOf y5.Dom
 * @function
 * @deprecated y5.Dom.getNext
 */
Dom.getNextElement = Dom.getNext;

/**
 * @name getPreviousElement
 * @memberOf y5.Dom
 * @function
 * @deprecated y5.Dom.getPrev
 */
Dom.getPreviousElement = Dom.getPrev;

/**
 * @name deleteNode
 * @memberOf y5.Dom
 * @function
 * @deprecated y5.Dom.removeNode
 */
Dom.deleteNode = Dom.removeNode;

/**
 * @name getOffsset
 * @memberOf y5.Dom
 * @function
 * @deprecated y5.Dom.getOffset
 */
Dom.getOffsset = Dom.getOffset;

/**
 * @name innerText
 * @memberOf y5.Dom
 * @function
 * @deprecated y5.Dom.textContent
 */
Dom.innerText = Dom.textContent;

y5.loaded('Dom');

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

var GC = y5.GC;

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


// TimerObserver
/////////////////////////////////////////////////////////////////////

/**
 * Создает объект-таймер, вызывающий заданную функцию с заданной периодичностью.
 * @class Класс, реализующий механизм вызова заданных функций по таймеру.
 * @constructor
 * @name y5.TimerObserver
 * @param {Function} listener Callback-функция
 * @param {Number} period Периодичность запуска функции (в секундах)
 * @param {Boolean} add Начать слушать немедленно (по умолчанию - false)
 * @param {Object} context Контекст выполнения функции listener
 *
 * @example
 * function callback(observer) {
 *     // ...
 *     // останавливаем таймер после 10 запусков
 *     if (observer.tick == 10) {
 *         observer.remove();
 *     }
 * }
 * new y5.TimerObserver(callback, 5, true);
 */
y5.TimerObserver = function(listener, period, add, context) {
    this.period = period * 1000;
    this.added = false;
    this.tick = 0;
    this.timersCount = 10;
    this.timers = new Array(this.timersCount);
    this.context = context;
    this.listener = listener;

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

    GC.collect(this);
};

y5.TimerObserver.prototype = {
    /**
     * Количество вызовов функции.
     * @name y5.TimerObserver.tick
     * @memberOf y5.TimerObserver
     * @type Number
     */


    /**
     * Включает (активирует) таймер.
     * @name y5.TimerObserver.add
     * @memberOf y5.TimerObserver
     * @function
     */
    add: function() {
        if (this.added) {
            return;
        }

        this.tick = 0;
        this.added = true;
        this.setTimer();
    },

    /**
     * Выключает (деактивирует) таймер.
     * @name y5.TimerObserver.remove
     * @memberOf y5.TimerObserver
     * @function
     */
    remove: function() {
        if (!this.added) {
            return;
        }

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

    /**
     * @private
     */
    execListener: function(startTime, i) {
        if (!this.added) {
            return;
        }

        window.clearTimeout(this.timers[i]);

        var endTime = new Date().getTime();
        var diff = startTime - endTime;
        var skip = diff > 0;

        this.tick++;
        execListenerWithContext(this.listener, this.context, this, this.tick, skip);

        if (this.tick % this.timersCount == 0) {
            this.setTimer();
        }
    },

    /**
     * @private
     */
    setTimer: function() {
        this.clearTimer();

        var period = 0;
        var time = new Date().getTime();
        for (var i = 0; i < this.timersCount; i++) {
            period += this.period;
            var startTime = time + period;

            this.timers[i] = y5.Utils.setTimeout(this.execListener, period, this, startTime, i);
        }
    },

    /**
     * @private
     */
    clearTimer: function() {
        for (var i = 0; i < this.timersCount; i++) {
            window.clearTimeout(this.timers[i]);
        }
    },

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


/// InputObserver
/////////////////////////////////////////////////////////////////////

/**
 * Создает объект-слушатель.
 * @class Класс для создания объектов-слушателей, отслеживающих изменения элементов ввода. Предназначен для создания интерактивных (реагирующих на заполнение пользователем полей ввода) HTML-форм.
 * @constructor
 * @name y5.InputObserver
 * @param {Function} listener Callback-функция
 * @param {Element} element Элемент
 * @param {Boolean} [add] Начать слушать немедленно (по умолчанию - false)
 * @param {Object} [context] Контекст выполнения функции listener
 *
 * @example
 * new y5.InputObserver(callback, element, true);
 */
y5.InputObserver = function(listener, element, add, context) {
    this.element = element;
    this.listener = listener;
    this.context = context;
    this.added = false;

    // init
    this.setInitState();
    this.timer = new y5.TimerObserver(this.execListener, .1, this.added, this);

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

    GC.collect(this);
};

y5.InputObserver.prototype = {
    /**
     * Включает (активирует) объект-слушатель.
     * @name y5.InputObserver.add
     * @memberOf y5.InputObserver
     * @function
     */
    add: function() {
        this.timer.add();
        this.added = true;
    },

    /**
     * Выключает (деактивирует) объект-слушатель.
     * @name y5.InputObserver.remove
     * @memberOf y5.InputObserver
     * @function
     */
    remove: function() {
        this.timer.remove();
        this.added = false;
    },

    /**
     * @private
     */
    execListener: function(timer, skip) {
        if (/* TEST: !skip && */this.isChanged()) {
            execListenerWithContext(this.listener, this.context, this.element);
            this.state = this.getState();
        }
    },

    /**
     * Запрашивает текущее состояние элемента ввода. Возвращает объект состояния.
     * @name y5.InputObserver.getState
     * @memberOf y5.InputObserver
     * @function
     * @returns {Object} Объект состояния
     */
    getState: function() {
        var element = this.element;

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

    /**
     * Устанавливает текущее состояние элемента в качестве начального.
     * @name y5.InputObserver.setInitState
     * @memberOf y5.InputObserver
     * @function
     */
    setInitState: function() {
        this.state = this.getState();
        this.initState = this.getState();
    },

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

    /**
     * Проверяет, изменилось ли состояние элемента ввода по сравнению с начальным. Если состояние изменилось, то возвращается true, иначе - false.
     * @name y5.InputObserver.isInitChanged
     * @memberOf y5.InputObserver
     * @function
     * @returns {Boolean} Изменение текущего состояния элемента в сравнении с this.initState
     */
    isInitChanged: function() {
        return this.stateChanged(this.initState);
    },

    /**
     * Сравнивает состояния элемента ввода.
     * @private
     * @param {Object} state Сравниваемое состояние
     * @returns {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
     */
    cleanup: function() {
        this.remove();
        this.context = null;
        this.listener = null;
        this.element = null;
    }
};


/// Events Factory
/////////////////////////////////////////////////////////////////////

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

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

y5.loaded('EventsExt');

});/**
 * @deprecated y5.Events
 */

y5.require('Events', function() {

var data = [];

y5.CallBacks = {
    add: function(type, callback, object, execute, context) {
        if (typeof execute == y5.UNDEF) {
            execute = true;
        }
        var observer = new y5.Observer('y5:' + type, callback, object, execute, context);
        data.push(observer);
        return observer;
    },

    remove: function(observer) {
        var pos = data.indexOf(observer);
        if (pos != -1) {
            data[pos].remove();
        }
    },

    dispatch: function(type, object, params) {
        return y5.Notify('y5:' + type, object, params);
    },

    Listener: y5.NULL
};

y5.loaded('CallBacks');

});(function() {

/**
 * Возвращает заголовок для установки cookie.
 * @see y5.Cookies.set
 * @private
 */
function getHeader(name, value, time, domain, path) {
    var kookie = [];

    // имя и значение
    kookie.push(name + '=' + encodeURIComponent(value));

    // устанавливаем время жизни
    if (typeof time == 'number') {
        var expire = new Date();

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

    // домен
    kookie.push('domain=' + (domain || window.location.hostname));

    // путь
    kookie.push('path=' + (path || '/'));

    return kookie.join(';');
}

/**
 * Устанавливает cookie.
 * @see y5.Cookies.set
 * @private
 */
function setHeader(name, value, time, domain, path) {
    document.cookie = getHeader(name, value, time, domain, path);
}

/**
 * Функции для работы с cookie: установить, получить значение, удалить.
 * @class Содержит функции для работы с cookie.
 * @static
 * @name y5.Cookies
 */
y5.Cookies = {

    /**
     * Устанавливает cookie.
     * @name y5.Cookies.set
     * @memberOf y5.Cookies
     * @function
     * @param {String} name Имя
     * @param {String} value Значение
     * @param {Number} [time] Время жизни (в часах), если не указано, то cookie устанавливается на сессию
     * @param {String} [domain] Домен (если не указан, то текущий)
     * @param {String} [path] Путь (если не указан, то /)
     *
     * @example
     * // сессионная cookie
     * y5.Cookies.set('foo', 'bar');
     *
     * // на два часа
     * y5.Cookies.set('foo', 'bar', 2);
     *
     * // сессионная на домен *.yandex.ru
     * y5.Cookies.set('foo', 'bar', null, '.yandex.ru');
     *
     * // сессионная на домен *.yandex.ru и путь
     * y5.Cookies.set('foo', 'bar', null, '.yandex.ru', '/path');
     */
    set: function(name, value, time, domain, path) {
        // set cookie if value not null & not empty
        if (value !== null && value !== '') {
            setHeader(name, value, time, domain, path);
        }
    },

    /**
     * Возвращает значение cookie по имени.
     * @name y5.Cookies.get
     * @memberOf y5.Cookies
     * @function
     * @param {String} name Имя
     * @returns {String | null} Значение cookie
     *
     * @example
     * y5.Cookies.set('foo');
     * // -> 'bar'
     */
    get: function(name) {
        var res = document.cookie.match(new RegExp(name + '=([^;]*)'));
        return (res && res[1] ? decodeURIComponent(res[1]) : null);
    },

    /**
     * Удаляет cookie.
     * @name y5.Cookies.remove
     * @memberOf y5.Cookies
     * @function
     * @param {String} name Имя
     * @param {String} [domain] Домен
     * @param {String} [path] Путь
     *
     * @example
     * y5.Cookies.remove('foo');
     */
    remove: function(name, domain, path) {
        // set expire time to one year later
        setHeader(name, '', -365 * 24, domain, path);
    }
};

var Cookies = y5.Cookies;

/// aliases

/**
 * @name setCookie
 * @memberOf y5.Cookies
 * @function
 * @deprecated y5.Cookies.set
 */
Cookies.setCookie = Cookies.set;

/**
 * @name getCookie
 * @memberOf y5.Cookies
 * @function
 * @deprecated y5.Cookies.get
 */
Cookies.getCookie = Cookies.get;

/**
 * @name delCookie
 * @memberOf y5.Cookies
 * @function
 * @deprecated y5.Cookies.remove
 */
Cookies.delCookie = Cookies.remove;

})();

y5.loaded('Cookies');y5.require('Events', 'Utils', 'Cache', 'Classes', 'Dom', function() {

/*
 *   Счётчик компонентов.
 *   Увеличивается при запросе на создание нового компонента и уменьшается при его создании.
 */
var componentsCounter = 0,
    componentsCounterProperty = y5.Utils.generateId('_y5_Components'),
    Notify = y5.Notify;

function componentsInc() {
    componentsCounter++;
}

function componentsDec() {
    if (--componentsCounter == 0) {
        Notify('y5:allComponentsCreated', y5.Components);
    }
}

/**
 * Модуль для создания компонентов - интерактивных элементов интерфейса страницы (календарь, слайдер и т.п.).
 * Компоненты инициализируются автоматически при загрузке страницы и помечаются специальными классами вида "{LibraryNamespace}-c-{ComponentName}", например "y5-c-Calendar".
 *
 * @class Содержит функции для создания компонентов из исходного кода документа.
 * @static
 * @name y5.Components
 * @memberOf y5
 */

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

    /**
     * Инициализирует механизм поддержки компонентов. Запускает поиск и установку компонентов, содержащихся в документе. 
 	 * @name y5.Components.init
 	 * @memberOf y5.Components
 	 * @function
     * @param {Element} element Элемент документа, с которого начинается поиск компонентов.
     */
    init: function(element) {
        element = element || y5.Dom.getBody();

        var elements = y5.Dom.getDescendants(element, this.tagName, this.classNameRegex);

        if (y5.Classes.test(element, this.classNameRegex)) {
            elements.push(element);
        }

        this.createComponents(elements);
    },

    /**
     * Создает набор компонентов из списка элементов документа.
 	 * @name y5.Components.createComponents
 	 * @memberOf y5.Components
 	 * @function
     * @param {Array} elements Список элементов
     */
    createComponents: function(elements) {
        for (var i = 0, l = elements.length; i < l; i++) {
            this.prepareComponent(elements[i]);
        }
    },

    /**
     * @param {Element} element Элемент компонента.
     * @private
     */
    prepareComponent: function(element) {
        if (!element) {
            return;
        }

        var params = this.getParams(element),
            locations = this.getModules(element.className),
            location,
            i = 0,
            l = locations.length;

        element[componentsCounterProperty] = l;

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

    /**
     * Проверяет, что для элемента уже были созданы компоненты.
     * @param {Element} element Элемент компонента
     * @private
     */
    checkPrepare: function(element, location) {
        var key = y5.Utils.getUniqueId(element) + '-' + location;
        if (this.cache.empty(key)) {
            this.cache.set(key, true);
            return false;
        }
        return true;
    },

    /**
     * Создает компонент.
     * @param {String} location
     * @param {Element} element Элемент
     * @param {Object} params Параметры
     * @private
     */
    createComponent: function(location, element, params) {
        function createComponent() {
            var Component = y5.moduleObject(location);
            if (Component == null) {
                y5.Console.error('Component init failed %s', y5.moduleName(location), ['y5', 'Components']);
                componentsDec();
                return;
            }

            function run() {
                var comp;
                if (!Component.createFromTag) {
                    comp = new Component(element, params);
                } else {
                    comp = Component.createFromTag(element, params);
                }
                y5.GC.collect(comp);
                element[componentsCounterProperty]--;
                /**
                 * Событие о создании компонента.
                 * @event
                 * @param {Object} params Параметры компонента.
                 * @param {String} params.name Имя компонента.
                 * @param {Element} params.element Элемент, на котором создан компонент.
                 * @param {Object} params.instance Созданный компонентs.
                 */
                Notify('y5:componentCreated', element, {
                    name: y5.moduleName(location),
                    element: element,
                    instance: comp
                });
                if (element[componentsCounterProperty] == 0) {
                    /**
                     * Событие о создании всех компонентов на элементе.
                     * @event
                     */
                    Notify('y5:allComponentsCreated', element);    
                }

                y5.Console.log('Component created %s', y5.moduleName(location), ['y5', 'Components']);
                componentsDec();
            }

            window.setTimeout(run, 0);
        }

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

    /**
     * Возвращает имена модулей, найденных в именах классов.
     * @param {String} className
     * @returns {Array} Список модулей
     * @private
     */
    getModules: function(className) {
        var classNames = className.match(this.getClassNameRegex),
            length = classNames.length,
            result = new Array(length);

        for (var i = 0; i < length;  i++) {
            result[i] = classNames[i].replace(this.className, ':').replace(/-/g, '.');
        }

        return result;
    },

    /**
     * Возвращает параметры, заданные в элементе компонента.
     * Для передачи параметров используется атрибут 'onclick', например:
     * onclick="return {foo: 'bar'}" возвращает "{foo: 'bar'}".
     * @param {Element} element Элемент
     * @returns {Object} Объект
     * @private
     */
    getParams: function(element) {
        try {
            return element.onclick ? element.onclick() : {};
        } catch (e) {
            return null;
        }
    },

    /**
     * @private
     */
    getName: function(className, prefix) {
        return className.match(new RegExp(prefix + '([\\w-]+)', ''))[1].replace(/-/g, '.');
    }
};

var is_init = false;
function init() {
    if (!is_init) {
        is_init = true;
        y5.Components.init();
    }
}

/* слушаем оба события и запускаем init на первое */
new y5.Observer('y5:srcload', init, y5, true);
new y5.Observer('dom:loaded', init, y5, true);

y5.loaded('Components');

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

/**
 * Класс, реализующий работу со слушателями "горячих клавиш".
 * @class Содержит функции для работы со слушателями "горячих клавиш".
 * @public
 * @name y5.ShortCutListener
 * @memberOf y5
 * @param {Number} masks Маски комбинаций клавиш
 * @param {Function} callback Функция обработки нажатия клавиш
 * @param {Node} node Целевой элемент
 * @param {Object} options Параметры
 */
function ShortCutListener(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();

    y5.GC.collect(this);
}

ShortCutListener.prototype = {
    /// Public

    /**
     * Добавляет комбинацию клавиш в список активных.
     * @name y5.ShortCutListener.add
     * @memberOf y5.ShortCutListener
     * @public
     * @function
     */
    add: function() {
        this.enable(true);
    },

    /**
     * Удаляет комбинацию клавиш из списка активных.
     * @name y5.ShortCutListener.remove
     * @memberOf y5.ShortCutListener
     * @public
     * @function
     */
    remove: function() {
        this.enable(false);
    },

    /**
     * Проверяет активность комбинации.
     * @name y5.ShortCutListener.isEnable
     * @memberOf y5.ShortCutListener
     * @public
     * @function
     */
    isEnable: function() {
        return this.enabled;
    },

    /**
     * Включает/выключает состояние активности.
     * @name y5.ShortCutListener.enable
     * @memberOf y5.ShortCutListener
     * @public
     * @function
     * @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;
    },

    /**
     * Очищает содержимое этого объекта.
     * @name y5.ShortCutListener.cleanup
     * @memberOf y5.ShortCutListener
     * @public
     * @function
     */
    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 = [];

var KEYS = {
    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,
    PGUP: 33,
    PAGE_DOWN: 34,
    PGDN: 34,
    END: 35,
    HOME: 36,
    LEFT_ARROW: 37,
    LEFT: 37,
    UP_ARROW: 38,
    UP: 38,
    RIGHT_ARROW: 39,
    RIGHT: 39,
    DOWN_ARROW: 40,
    DOWN: 40,
    INSERT: 45,
    INS: 45,
    DELETE: 46,
    DEL: 46,
    LEFT_WINDOW: 91,
    RIGHT_WINDOW: 92,
    SELECT: 93,
    PLUS: y5.is_ie || y5.is_safari ? 187 : 61,
    PLUS_NUM: 107,
    MINUS: y5.is_ie || y5.is_safari ? 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
};

function convertKey(key) {
    if (y5.Types.object(key)) {
        return key;
    }

    var tokens = key.replace(/\s+/g, '').split(/\+/g);
    key = {};

    tokens.forEach(function(tok) {
        tok = tok.toUpperCase();
        switch (tok) {
            case 'ALT':
                key.alt = true;
                break;

            case 'SHFT': case 'SHIFT':
                key.shift = true;
                break;

            case 'CTL': case 'CTRL':
                key.ctrl = true;
                break;

            default:
                var num = KEYS[tok];
                if (num) {
                    key.key = num;
                } else {
                    key.ch = tok;
                }
        }
    });

    return key;
}

function convertKeys(keys) {
    if (!y5.Types.array(keys)) {
        keys = [keys];
    }

    return keys.map(convertKey);
}

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

    return options;
}

function removeFromList(element, list) {
    var index = list.lastIndexOf(element);
    if (index !== -1) {
        y5.GC.remove(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(convertKeys(keys), type), listener, node || document, options));
}

/**
 * @class Набор функций для обработки клавиатурных сокращений. Позволяет реализовать в приложении поддержку «горячих клавиш».
 * @static
 * @name y5.ShortCut
 *
 * @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);
 *
 * Примеры сочетаний:
 * 'h'
 * 'ctrl+h'
 * ['ctrl+h','alt+b']
 * {ctrl: true, ch: 'h'}
 * {ctrl: true, key: y5.ShortCut.ESC}
 * [{ctrl: true, ch: 'h'}, {alt: true, ch: 'b'}]
 * ['ctl+h', {alt: true, ch: 'b'}]
 * 'alt+f1'
 * 'ctrl + alt + shift + h'
 *
 * Пример вызова:
 * 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 = {
    /**
     * Устанавливает обработчик клавиатурного сокращения на нажатие клавиши (keydown). Возвращает экземпляр класса ShortCutListener.
     * @name y5.ShortCut.down
     * @memberOf y5.ShortCut
     * @function
     * @param {Array of Keys} keys Список сочетаний клавиш
     * @param {Function} callback Функция обработки нажатия клавиш
     * @param {Node} node Целевой элемент
     * @param {Object} options Параметры
     * @returns {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);
    },

    /**
     * Устанавливает обработчик клавиатурного сокращения на нажатие и отпускание клавиши (keypress). Возвращает экземпляр класса ShortCutListener.
     * @name y5.ShortCut.press
     * @memberOf y5.ShortCut
     * @function
     * @param {Array of Keys} keys Список сочетаний клавиш
     * @param {Function} callback Функция обработки нажатия клавиш
     * @param {Node} node Целевой элемент
     * @param {Object} options Параметры
     * @returns {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);
    },

    /**
     * Удаляет обработчик клавиатурных сокращений. В случае успеха возвращает true.
     * @name y5.ShortCut.remove
     * @memberOf y5.ShortCut
     * @function
     * @param {Object} shortcut
     * @returns {Boolean}
     */
    remove: function(shortcut) {
        return this.removeDown(shortcut) || this.removePress(shortcut);
    },

    /**
     * Выключает обработку конкретного клавиатурного сокращения, которое было установлено на нажатие клавиши (keydown).
     * @name y5.ShortCut.removeDown
     * @memberOf y5.ShortCut
     * @function
     * @param {Object} shortcut
     * @returns {Boolean}
     */
    removeDown: function(shortcut) {
        return removeFromList(shortcut, listDown);
    },

    /**
     * Выключает обработку конкретного клавиатурного сокращения, которое было установлено на нажатие и отпускание клавиши (keypress).
     * @name y5.ShortCut.removePress
     * @memberOf y5.ShortCut
     * @function
     * @param {Object} shortcut
     * @returns {Boolean}
     */
    removePress: function(shortcut) {
        return removeFromList(shortcut, listPress);
    }
};

for (var name in KEYS) {
    y5.ShortCut[name] = KEYS[name];
}

var AEventListener = y5.AEventListener;

/// Init

var commonDownListener = keyDownListener;

if (y5.is_ie) {

    commonDownListener = function(e) {
        if (!e.repeat) {
            keyDownListener(e);
        }
        keyPressListener(e);
    };

} else if (y5.is_safari) {

    var keyUp, press = false;
    keyUp = new AEventListener('keyup', function() { press = false; keyUp.remove(); }, document, false);

    commonDownListener = function(e) {
        if (!press) {
            keyUp.add();
            keyDownListener(e);
        }
        press = true;

        keyPressListener(e);
    };

} else {

    new AEventListener('keypress', keyPressListener, document, true);

}

new AEventListener('keydown', commonDownListener, document, true);

y5.loaded('ShortCuts');

});y5.require('Dom', function(){
var Ext = {};
/*
 * Ext JS Library 2.1
 * Copyright(c) 2006-2008, Ext JS, LLC.
 * licensing@extjs.com
 *
 * http://extjs.com/license
 */

/*
 * This is code is also distributed under MIT license for use
 * with jQuery and prototype JavaScript libraries.
 */
/**
 * @class Ext.DomQuery
 * @private
Provides high performance selector/xpath processing by compiling queries into reusable functions. New pseudo classes and matchers can be plugged. It works on HTML and XML documents (if a content node is passed in).
<p>
DomQuery supports most of the <a href="http://www.w3.org/TR/2005/WD-css3-selectors-20051215/#selectors">CSS3 selectors spec</a>, along with some custom selectors and basic XPath.</p>

<p>
All selectors, attribute filters and pseudos below can be combined infinitely in any order. For example "div.foo:nth-child(odd)[@foo=bar].bar:first" would be a perfectly valid selector. Node filters are processed in the order in which they appear, which allows you to optimize your queries for your document structure.
</p>
<h4>Element Selectors:</h4>
<ul class="list">
    <li> <b>*</b> any element</li>
    <li> <b>E</b> an element with the tag E</li>
    <li> <b>E F</b> All descendent elements of E that have the tag F</li>
    <li> <b>E > F</b> or <b>E/F</b> all direct children elements of E that have the tag F</li>
    <li> <b>E + F</b> all elements with the tag F that are immediately preceded by an element with the tag E</li>
    <li> <b>E ~ F</b> all elements with the tag F that are preceded by a sibling element with the tag E</li>
</ul>
<h4>Attribute Selectors:</h4>
<p>The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.</p>
<ul class="list">
    <li> <b>E[foo]</b> has an attribute "foo"</li>
    <li> <b>E[foo=bar]</b> has an attribute "foo" that equals "bar"</li>
    <li> <b>E[foo^=bar]</b> has an attribute "foo" that starts with "bar"</li>
    <li> <b>E[foo$=bar]</b> has an attribute "foo" that ends with "bar"</li>
    <li> <b>E[foo*=bar]</b> has an attribute "foo" that contains the substring "bar"</li>
    <li> <b>E[foo%=2]</b> has an attribute "foo" that is evenly divisible by 2</li>
    <li> <b>E[foo!=bar]</b> has an attribute "foo" that does not equal "bar"</li>
</ul>
<h4>Pseudo Classes:</h4>
<ul class="list">
    <li> <b>E:first-child</b> E is the first child of its parent</li>
    <li> <b>E:last-child</b> E is the last child of its parent</li>
    <li> <b>E:nth-child(<i>n</i>)</b> E is the <i>n</i>th child of its parent (1 based as per the spec)</li>
    <li> <b>E:nth-child(odd)</b> E is an odd child of its parent</li>
    <li> <b>E:nth-child(even)</b> E is an even child of its parent</li>
    <li> <b>E:only-child</b> E is the only child of its parent</li>
    <li> <b>E:checked</b> E is an element that is has a checked attribute that is true (e.g. a radio or checkbox) </li>
    <li> <b>E:first</b> the first E in the resultset</li>
    <li> <b>E:last</b> the last E in the resultset</li>
    <li> <b>E:nth(<i>n</i>)</b> the <i>n</i>th E in the resultset (1 based)</li>
    <li> <b>E:odd</b> shortcut for :nth-child(odd)</li>
    <li> <b>E:even</b> shortcut for :nth-child(even)</li>
    <li> <b>E:contains(foo)</b> E's innerHTML contains the substring "foo"</li>
    <li> <b>E:nodeValue(foo)</b> E contains a textNode with a nodeValue that equals "foo"</li>
    <li> <b>E:not(S)</b> an E element that does not match simple selector S</li>
    <li> <b>E:has(S)</b> an E element that has a descendent that matches simple selector S</li>
    <li> <b>E:next(S)</b> an E element whose next sibling matches simple selector S</li>
    <li> <b>E:prev(S)</b> an E element whose previous sibling matches simple selector S</li>
</ul>
<h4>CSS Value Selectors:</h4>
<ul class="list">
    <li> <b>E{display=none}</b> css value "display" that equals "none"</li>
    <li> <b>E{display^=none}</b> css value "display" that starts with "none"</li>
    <li> <b>E{display$=none}</b> css value "display" that ends with "none"</li>
    <li> <b>E{display*=none}</b> css value "display" that contains the substring "none"</li>
    <li> <b>E{display%=2}</b> css value "display" that is evenly divisible by 2</li>
    <li> <b>E{display!=none}</b> css value "display" that does not equal "none"</li>
</ul>
 * @singleton
 */
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
         * @returns {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).
         * @returns {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).
         * @returns {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
         * @returns {String}
         */
        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. Returns the defaultValue, or 0 if none is specified.
         * @param {String} selector The selector/xpath query
         * @param {Node} root (optional) The start of the query (defaults to document).
         * @param {Number} defaultValue
         * @returns {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
         * @returns {Boolean}
         */
        is : function(el, ss){
            if(typeof el == "string"){
                el = document.getElementById(el);
            }
            var isArray = Ext.isArray(el);
            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
         * @returns {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: /^\:([\w-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/,
                select: 'n = byPseudo(n, "{1}", "{2}");'
            },{
                re: /^(?:([\[\{])(?:@)?([\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);
            },

            "any" : function(c, selectors){
                var ss = selectors.split('|');
                var r = [], ri = -1, s;
                for(var i = 0, ci; ci = c[i]; i++){
                    for(var j = 0; s = ss[j]; j++){
                        if(Ext.DomQuery.is(ci, s)){
                            r[++ri] = ci;
                            break;
                        }
                    }
                }
                return r;
            },

            "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;
            }
        }
    };
}();

var select,
    isElement = y5.Types.element,
    Dom = y5.Dom;

// safari
if (document.querySelectorAll) {
    select = function (selector, root) {
        root = isElement(root) ? root : document;
        var nodes = [],
            result;

        // если родной querySelector не поддерживает какой-либо селектор, то ищем через собственную реализацию 
        try {
            result = root.querySelectorAll(selector);
            if (result) {
                // делаем массив из NodeList
                nodes = Array.prototype.slice.call(result);
            }
        } catch (e) {
            nodes = Ext.DomQuery.select(selector, root);
        }

        return nodes;
    }
} else {
    select = Ext.DomQuery.select;
}

/**
 * Возвращает список элементов по css-селектору.
 * @param {String} selector css-селектор
 * @param {Element} [root] контекст поиска (если не указан, то используется document)
 * @returns {Array} массив найденных элементов
 * @name y5.cssQuery
 * @function
 * @example
 * var nodes = y5.cssQuery('div.foo a');
 */
y5.cssQuery = select;

/// aliases

/**
 * @name y5.$$
 * @see y5.cssQuery
 * @memberOf y5
 * @function
 * @param {String} selector css-селектор
 * @param {Element} [root] контекст поиска (если не указан, то используется document)
 * @returns {Array} массив найденных элементов
 */
y5.$$ = select;

/**
 * @name y5.Dom.querySelectorAll
 * @see y5.cssQuery
 * @memberOf y5.Dom
 * @function
 * @param {String} selector css-селектор
 * @param {Element} [root] контекст поиска (если не указан, то используется document)
 * @returns {Array} массив найденных элементов
 */
Dom.querySelectorAll = select;

/**
 * Возвращает первый элемент по css-селектору.
 * @name y5.Dom.querySelector
 * @memberOf y5.Dom
 * @function
 * @param {String} selector css-селектор
 * @param {Element} [root] контекст поиска (если не указан, то используется document)
 * @returns {Element} Найденный элемент
 */
if (document.querySelector) {
    Dom.querySelector = function (selector, root) {
        root = isElement(root) ? root : document;
        return root.querySelector(selector);
    }
    
} else {
    Dom.querySelector = function (selector, root) {
        return select(selector, root)[0];
    }
}

y5.loaded('cssQuery');
});y5.require('URL', 'cssQuery', function() {

var formElements = ['input', 'textarea', 'select', 'button'];

function formToQuery(form, tags) {
    var url = y5.Url('');
    var elements = y5.cssQuery((tags || formElements).join(', '), form);

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

        if (element.disabled) {
            continue;
        }

        var tagName = element.tagName.toLowerCase(),
            name = element.name;

        switch (tagName) {
            case 'input':
                if (checkInputValue(element)) {
                    url.addParam(name, element.value);
                }
                break;

            case 'select':
                    if (!name.length) {
                        continue;
                    }

                    if (element.selectedIndex < 0) {
                        continue;
                    }

                    var opts = element.options;
                    if (element.multiple) {
                        for (var j = 0, lj = opts.length; j < lj; j++) {
                            if (opts[j].selected) {
                                url.addParam(name, opts[j].value);
                            }
                        }
                    } else {
                        url.addParam(name, opts[element.selectedIndex].value);
                    }

                    break;

            default:
                url.addParam(name, element.value);
                break;
        }
    }

    return url.query();
}

function checkInputValue(element) {
    var type = element.type;
    if (type == 'checkbox' || type == 'radio') {
        return element.checked;
    }
    return true;
}

/**
 * @class Набор функций  для работы с группой элементов HTML-формы.
 * @static
 * @name y5.FormCollector
 */
y5.FormCollector = {

    /**
     * Формирует строку из значений элементов формы в формате GET-запроса, т.е. "foo=bar&amp;bar=foo"
     * @name y5.FormCollector.collectTags
     * @memberOf y5.FormCollector
     * @function
     * @param {Element} container Контейнер элементов формы
     * @param {cssQuery} tags Список элементов в формате cssQuery
     * @returns {String} Строка запроса
     */
    collectTags: function(container, tags) {
        return formToQuery(container, tags);
    },

    /**
     * Формирует массив, содержащий набор полей формы.
     * @name y5.FormCollector.getTagsArray
     * @memberOf y5.FormCollector
     * @function
     * @param {Element} container Контейнер элементов формы
     * @param {cssQuery} tags Список элементов в формате cssQuery (необязательный, по умолчанию ['input', 'textarea', 'select', 'button'])
     * @returns {Array} Массив элементов
     */
    getTagsArray: function(container, tags) {
        return  y5.cssQuery((tags || formElements).join(', '), container);
    }
};

y5.loaded('FormCollector');

});
/**
 * Загружает документ с CSS-стилями.
 * @name y5.Loader.loadStyle
 * @memberOf y5.Loader
 * @function
 * @param {String} href Путь к документу
 * @param {String} [id] ID создаваемого элемента DOM
 */
y5.Loader.loadStyle = function(href, id) {
    return this.loadObject('link', {href: href, rel: 'stylesheet', type: 'text/css', id: id});
};

/**
 * @class Набор функций для добавления в документ CSS-стилей, расположенных во внешних файлах или динамически сформированных программным путем.
 * @static 
 * @name y5.Styles
 */
y5.Styles = {
    /**
     * Загружает документ со стилями. Адрес документа задается в виде стандартного URL.
     * @name y5.Styles.createStyle
     * @memberOf y5.Styles
     * @function
     * @param {String} href Путь к документу
     * @param {String} [id] ID создаваемого элемента DOM
     */
    createStyle: function(href, id) {
        return y5.Loader.loadStyle(href, id);
    },

    /**
     * Загружает модуль как документ со стилями. Адрес модуля соответствует адресации, принятой в y5
     * @name y5.Styles.loadModule
     * @memberOf y5.Styles
     * @function
     * @param {String} module Путь к документу
     * @param {String} [id] ID создаваемого элемента DOM
     */
    loadModule: function(module, id) {
        return this.createStyle(y5.moduleURL(module, 'css'), id);
    },

    /**
     * Встраивает набор CSS-стилей в HTML-документ.
     * @name y5.Styles.createInline
     * @memberOf y5.Styles
     * @function
     * @param {String} stylesText Код стилей
     */
    createInline: function(stylesText) {
        var style = document.createElement('div');
        style.innerHTML = '<p>x<\/p><style>' + stylesText + '<\/style>';
        document.body.appendChild(style.childNodes[1]);
    }
};

y5.loaded('Styles');y5.require('Strings', function() {

var stringTestRegexp = /[\x00-\x1f\"\\]/,
    stringReplaceRegexp = /[\x00-\x1f\"\\]/g,
    compositeKeyTestRegexp = /^(.+)\[(.*)\]$/,
    specialChars = {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    },
    Types = y5.Types,
    Strings = y5.Strings;

/**
 * Функция преобразования специальных и не-ASCII символов
 * в escape-последовательности для представления в JSON.
 * @param {String} char символ для преобразования
 * @returns {String} escape-последовательность
 * @private
 */
function replaceChars(ch) {
    var chr = specialChars[ch];
    if (chr) return chr;

    chr = ch.charCodeAt();
    return '\\u00' + Math.floor(chr / 16).toString(16) + (chr % 16).toString(16);
}

/**
 * Функция форматирования числа в двузначную строку.
 * @param {Number} number Число
 * @returns {String} Строка
 * @private
 *
 * @example
 * 1 -> '01'
 * 10 -> '10'
 */
function formatDateItem(num) {
    if (num < 0) {
        num = 0;
    }
    return num < 10 ? '0' + num : num;
}

/**
 * @class Набор функций для работы с данными в формате JSON.
 * @static
 * @name y5.JSON
 * @memberOf y5
 */
y5.JSON = {
    /**
     * Преобразовывает (сериализует) JavaScript-объект в JSON-строку. Возвращает JSON-строку.
     * @name y5.JSON.encode
     * @memberOf y5.JSON
     * @function
     * @param {Object} object Объект для сериализации
     * @returns {String} Сериализованный объект
     *
     * @example
     * y5.JSON.encode({a: 1, b: true, c: 'foo'});
     * // -> '{"a":1,"b":true,"c":"foo"}'
     * y5.JSON.encode([1, 2, 3]);
     * // -> '[1,2,3]'
     */
    encode: function(obj) {
        switch (Types.type(obj)) {
            case Types.NUMBER:
                return this.fromNumber(obj);

            case Types.BOOLEAN:
                return this.fromBoolean(obj);

            case Types.STRING:
                return this.fromString(obj);

            case Types.DATE:
                return this.fromDate(obj);

            case Types.ARRAY:
                return this.fromArray(obj);

            case Types.OBJECT:
                return this.fromObject(obj);

            default:
                return 'null';
        }
    },

    /**
     * Преобразовывает JSON-строку в JavaScript-объект. Возвращает объект, полученный из JSON.
     * @name y5.JSON.decode
     * @memberOf y5.JSON
     * @function
     * @param {String} string Строка JSON
     * @returns {Object} Объект
     * 
     * @example
     * y5.JSON.decode('{"a":1,"b":true,"c":"foo"}');
     * // -> {a: 1, b: true, c: "foo"} 
     */
    decode: function(s) {
        return eval('(' + s + ')');
    },
    
    /**
     * Возвращает JSON-строку, полученную из URL
     * @param {String} query Строка запроса
     * @returns {String} Строка JSON
     * 
     * @example
     * y5.JSON.fromQuery('?test=1&amp;&amp;test=2');
     * // -> {"test":"2"}
     * 
     * y5.JSON.fromQuery('test=foo&amp;foo[14]=3&amp;foo[]=4&amp;foo[]=5&amp;foo[test]=2test1=text');
     * // -> {"test":"foo","foo":{14:"3",15:"4",16:"5","test":"2test1=text"}}
     */
    fromQuery: function (query) {
        query = query.toString() || '';
        // выбирезаем запрос после ?
        var queryIndex = query.indexOf('?');
        if (queryIndex != -1) {
            query = query.substr(queryIndex+1);
        }
        
        if (query) {
            query = query.split('&');
            var i=0,
                queryLength = query.length,
                result = {},
                compositeKey,
                compositeIndex,
                compositeObject,
                value,
                key,
                vars;

            for (;i<queryLength;i++) {
                if (!query[i]) continue;
                queryIndex = query[i].indexOf('=');
                if (queryIndex == -1) {
                    key = query[i];
                    value = '';    
                } else {
                    key = query[i].substring(0, queryIndex);
                    value = query[i].substr(queryIndex+1) || '';
                }
                
                // проверяем элементы типа foo[]
                compositeKey = key.match(compositeKeyTestRegexp);
                
                if (compositeKey && compositeKey[1]) {
                    // если такого элемента нет, то надо его создать
                    if (!Types.object(result[compositeKey[1]])) {
                        result[compositeKey[1]] = {};
                        //фейковый параметр для того, чтобы работало foo[13]=13,foo[test]=test,foo[]=14 
                        result[compositeKey[1]].__length = 0;
                    }
                    compositeObject = result[compositeKey[1]];
                    
                    // если указан индекс объекта foo[index] 
                    if (compositeKey[2]) {
                        compositeIndex = parseInt(compositeKey[2], 10);
                        // если индекс числовой
                        if (!isNaN(compositeIndex)) {
                            compositeObject[compositeIndex] = value;
                            if (compositeIndex > compositeObject.__length) {
                                compositeObject.__length = compositeIndex+1;
                            }
                                
                        } else if (Types.string(compositeKey[2])) {
                            compositeObject[compositeKey[2]] = value;
                        }
                        
                    // если индекс пустой, т.е. foo[], то как бы делаем push
                    } else {
                        compositeObject[compositeObject.__length++] = value;
                    }        
                            
                } else {
                    result[key] = value;    
                }
            }
            
            // удаляем __length
            for (i in result) {
                if (result[i] && result[i].__length) {
                    delete result[i].__length;
                }
            }
            
            return this.encode(result);
        }
        return 'null';
    },
    
    /**
     * Преобразует JavaScript-объект в строку запроса
     * @param {Object} object Объект
     * @returns {String} Строка запроса
     * 
     * @example
     * y5.JSON.toQuery({"test":"foo","foo":{14:"3",15:"4",16:"5","test":"2test1=text"}});
     * // -> test=foo&amp;foo[14]=3&amp;foo[15]=4&amp;foo[16]=5&amp;foo[test]=2test1=text
     * 
     * y5.JSON.toQuery({array:[1,2,3]});
     * // -> array[]=1&amp;array[]=2&amp;array[]=3
     * 
     * y5.JSON.toQuery({dt:new Date(2008,06,13,14,15,03)});
     * // -> dt=2008-07-13T10:15:03Z
     */
    toQuery: function (object) {
        var query = '',
            i,
            delimeter = '',
            index,length,obj;

        object = Types.object(object)?object:this.decode(object);
            
        for (i in object) {
            obj = object[i];
            switch (Types.type(obj)) {
                case Types.BOOLEAN:
                    query += delimeter + i+'='+this.fromBoolean(obj);
                    delimeter = '&';
                    break;
    
                case Types.DATE:
                    query += delimeter + i+'='+this.fromDate(obj).replace('"', '').replace('"', '');
                    delimeter = '&';
                    break;
    
                case Types.ARRAY:
                    for (index=0,length=obj.length; index<length; index++) {
                        query += delimeter + i + '[]=' + obj[index];
                        delimeter = '&';
                    }
                    break;
    
                case Types.OBJECT:
                    for (index in obj) {
                       query += delimeter + i + '[' + index + ']=' + obj[index];
                        delimeter = '&';
                    }
                    break;
    
                default:
                    query += delimeter + i+'='+obj;
                    delimeter = '&';
                    break;
            }
        }
        return query;
    },
    
    /**
     * Преобразовывает JSON-строку в формат, пригодный для вставки в HTML-код страницы. Возвращает строку, в которой все служебные HTML-символы заменены на символьные примитивы. Полезно для визуального контроля содержимого JSON при отладке приложения.
     * @name y5.JSON.toHTML
     * @memberOf y5.JSON
     * @function
     * @param {Object} object Объект для сериализации
     * @returns {String} Сериализованный объект
     *
     * @example
     * y5.JSON.toHTML({a: 'foo&amp;bar'});
     * // -> '{&amp;quot;a&amp;quot;:&amp;quot;foo&amp;amp;bar&amp;quot;}'
     */
    toHTML: function(obj) {
        return Strings.escapeHTML(this.encode(obj));
    },

    /**
     * Возвращает строковое представление числа для JSON.
     * @param {Number} object Объект для сериализации
     * @returns {String} Сериализованный объект
     * @private
     */
    fromNumber: function(obj) {
        return isFinite(obj) ? String(obj) : 'null';
    },

    /**
     * Возвращает строковое представление булевого объекта для JSON.
     * @param {Boolean} object Объект для сериализации
     * @returns {String} Сериализованный объект
     * @private
     */
    fromBoolean: function(obj) {
        return String(obj);
    },

    /**
     * Возвращает строку для JSON.
     * Специальные и не-ASCII символы заменяются escape-последовательностями.
     * @param {String} object Объект для сериализации
     * @returns {String} Сериализованный объект
     * @private
     */
    fromString: function(obj) {
        if (stringTestRegexp.test(obj)) {
            try {
                obj = obj.replace(stringReplaceRegexp, replaceChars);
            } catch (e) {}
        }

        return '"' + obj + '"';
    },

    /**
     * Возвращает строковое представление даты для JSON.
     * @param {Date} object Объект для сериализации
     * @returns {String} Сериализованный объект
     * @private
     */
    fromDate: function(obj) {
        return '"' + obj.getUTCFullYear()         + '-' +
            formatDateItem(obj.getUTCMonth() + 1) + '-' +
            formatDateItem(obj.getUTCDate())      + 'T' +
            formatDateItem(obj.getUTCHours())     + ':' +
            formatDateItem(obj.getUTCMinutes())   + ':' +
            formatDateItem(obj.getUTCSeconds())   + 'Z"';
    },

    /**
     * Возвращает строковое представление массива для JSON.
     * @param {Array} object Объект для сериализации
     * @returns {String} Сериализованный объект
     * @private
     */
    fromArray: function(obj) {
        return '[' + obj.map(function(item) { return this.encode(item) }, this).join(',') + ']';
    },

    /**
     * Возвращает JSON-строку.
     * @param {Object} object Объект для сериализации
     * @returns {String} Сериализованный объект
     * @private
     */
    fromObject: function(obj) {
        var list = [];

        for (var i in obj) {
            if (i == Number(i)) {
                i = Number(i);
            }
            list.push(this.encode(i) + ':' + this.encode(obj[i]));
        }

        return '{' + list.join(',') + '}';
    }
};

var JSON = y5.JSON;

/// aliases

/**
 * @name y5.JSON.toObject
 * @function
 * @see y5.JSON.decode
 */
JSON.toObject = JSON.decode;

/**
 * @name y5.JSON.toString
 * @function
 * @see y5.JSON.encode
 */
JSON.toString = JSON.encode;

y5.loaded('JSON');

});y5.require('Strings', 'Events', 'URL', 'Utils', function() {

var URL = y5.URL,
    Utils = y5.Utils,
    Types = y5.Types,
    VOID = y5.VOID,
    moduleName = 'Request',
    states = ['uninitialized', 'loading', 'loaded', 'interactive', 'complete'];

function onExceptionDefault(e) {
    y5.Console.error(moduleName, [e], [moduleName]);
}

function trimText() {
    return y5.Strings.trim(this.responseText);
}

function responseText() {
    return this.responseText;
}

function jsonDecode() {
    if (y5.JSON) {
        return y5.JSON.decode(this.responseText);
    }
    throw new y5.Exception('y5:JSON module required', jsonDecode, 'Request');
}

/**
 * Создает базовый экземпляр класса для реализации на его основе Ajax-запросов.
 * @name y5.Request
 * @class Набор базовых методов для реализации на их основе Ajax-запросов с использованием разных методов передачи данных (транспортов): XML, Script, Iframe.
 * @constructor
 * @param {String | y5.URL} url URL запроса
 * @param {Object} params Параметры запроса
 *
 * @example
 * // Пример запроса (на примере y5.Request.XML).
 * var req = new y5.Request.XML("test.xml");
 * req.onload = function(req) { alert(req.responseText) };
 * req.onexception = function(e) { alert(e.message) };
 * req.send();
 *
 * // Запрос, обработчики которого находятся в объекте
 * var obj = {
 *     errorMessage: 'Ошибка загрузки',
 *     onload: function(req) { alert(req.responseText) },
 *     onerror: function() { alert(this.errorMessage) }
 * };
 *
 * var req = new y5.Request.XML("test.xml", { callbackObject: obj });
 * req.send();
 *
 *
 * Типы событий на выполнение запроса:
 *
 * loading: возникает при отправке запроса
 * load: возникает при успешном завершении запроса и получении запрашиваемых данных
 * error: ошибка при выполнении запроса
 * abort: возникает при прерывании запроса с помощью функции abort()
 * exception: ошибка связанная с работой приложения
 * complete: возникает всегда по завершении запроса, если не возникло исключения
 * {код ответа сервера}: например '200'. Если сервер вернул определенный статус и существует
 * функция такого вида, то она будет вызвана
 *
 *
 * Функции, реагирующие на события можно привязывать разными способами:
 *
 * 1) Непосредственное назначение функции экземпляру объекта (имя функции -- on{тип события}).
 * 2) Передача в виде параметров при создании экземпляра класса ({onload: function() {}})
 * 3) Навешивание функций на события. При возникновении события посылается сообщение
 *    типа request:{тип события} для ID указанного в параметрах при создании экземпляра.
 *
 *
 * Объект, возвращаемый при получении запроса имеет следующие свойства:
 *
 * responseText: тело ответа
 * text(): функция возвращает responseText с удаленными начальными и концевыми пробельными символами
 * getResponseHeader(header): возвращает заголовок ответа
 * status: код ответа (если статус в диапазоне от 200 до 299, то возникает событие load, иначе error)
 */
function Request(url, params) {
    this.req = null;
    this.url = url instanceof URL ? url : new URL(url);

    var context;
    if (params) {
        // получаем ссылку на контекст выполнения обработчиков для того, чтобы не копировать его через objectCopy
        context = params.callbackContext;
        params.callbackContext = null;
    }

    // параметры
    this.params = Utils.objectCopy(
        Utils.objectCopy({}, this.defaultParams),
        params
    );

    if (Types.object(context)) {
        this.params.callbackContext = context;
    }

    this.params.method = this.params.method.toLowerCase();
}

Request.prototype = {
    /**
     * Параметры по умолчанию.
     * @memberOf y5.Request
     * @name y5.Request.params
     * @private
     * @type Object
     *
     * @example
     * // параметры по умолчанию
     * {
     *     // идентификатор запроса
     *     id: null,
     *
     *     // метод запроса
     *     method: 'get',
     *
     *     // функция выполняемая при исключении
     *     onexception: onExceptionDefault,
     *
     *     // контекст выполнения функций callback
     *     callbackContext: null,
     *
     *     // объект содержащий функции callback
     *     callbackObject: null
     * }
     */
    defaultParams: {
        // идентификатор запроса
        id: null,

        // метод запроса
        method: 'get',

        // функция выполняемая при исключении
        onexception: onExceptionDefault,

        // контекст выполнения функций callback
        callbackContext: null,

        // объект содержащий функции callback
        callbackObject: null
    },

    /**
     * Прерывает выполнение запроса. Возникает событие abort.
     * @memberOf y5.Request
     * @name y5.Request.abort
     * @function
     */
    abort: VOID,

    /**
     * Отправляет запрос. В качестве аргумента может быть передана строка в виде параметров GET или объект JavaScript.
     * @memberOf y5.Request
     * @name y5.Request.send
     * @function
     * @param {String | Object} [query] Данные запроса в виде запроса GET или объекта
     *
     * @example
     * req.send('foo=1&amp;bar=2');
     * req.send({foo: 1, bar: 2});
     */
    send: function(query) {
        try {
            this.init();
            y5.Console.log(this.toString() + ' ' + this.params.method.toUpperCase(), [this, query], [moduleName, this.toString()]);

            if (query && y5.Types.object(query) && !query.submit) {
                query = y5.Url('').replaceParams(query).query();
            }

            this._send(query);
        } catch (e) {
            this.dispatch('exception', e);
        }
    },

    /**
     * @private
     */
    init: function() {
        if (this.isInit) return;

        if (!this.id) {
            this.id = Utils.generateUniqueId();
        }
        this._init();

        this.isInit = true;
    },

    /**
     * @private
     */
    _init: VOID,

    /**
     * @private
     */
    end: function() {
        this._end();

        // clean up to avoid memleak
        this.req = null;
        this.isInit = false;

        this.dispatch('end');
    },

    /**
     * @private
     */
    _end: VOID,

    /**
     * @private
     */
    dispatch: function(status, args) {
        var params = this.params,
            name = 'on' + status,
            callbackContext = params.callbackContext || this,
            callbackObject = params.callbackObject;

        args = args || this.req;

        // выполняем функцию
        if (Types.func(this[name])) {
            this[name].call(callbackContext, args);
        }

        // выполняем функцию, если задана в параметрах
        if (Types.func(params[name])) {
            params[name].call(callbackContext, args);
        }

        // выполняем функцию объекта содержащего функции callback
        if (callbackObject && Types.func(callbackObject[name])) {
            callbackObject[name](args);
        }

        // посылаем сообщение
        y5.Notify('request:' + status, params.id || this, args);
        
        // глобальное отслеживание запросов
        y5.Notify('request:' + status, y5, [this, args]);
    },

    /**
     * @private
     */
    onStateChange: function() {
        try {
            y5;
        } catch (e) {
            // обрыв запроса при закрытии страницы
            return;
        }

        var stateNum = this.req.readyState,
            complete = stateNum == 4;

        if (complete) {
            var status = 0;
            try {
                status = this.req.status;
            } catch (e) {}

            if (y5.is_ie && status == 1223) {
                // http://dev.jquery.com/ticket/1450
                status = 204
            }

            if (!Types.func(this['on' + status])) {
                status = (status >= 200 && status < 300) ? 'load' : 'error';
            }

            var isText = Types.string(this.req.responseText);
            this.req.text = isText ? trimText : responseText;
            this.req.json = isText ? jsonDecode : y5.NULL;

            // выполняем функцию по окончании загрузки
            this.dispatch(status);
        }

        // выполняем функцию на изменение состояния загрузки
        this.dispatch(states[stateNum]);

        if (complete) {
            this.end();
        }
    },

    /**
     * Возвращает название класса.
     * @memberOf y5.Request
     * @name y5.Request.toString
     * @function
     * @returns {String} Название класса
     */
    toString: function() {
        return moduleName;
    }
};

y5.Get = {};
y5.Post = {};
y5.Load = {};

/**
 * Функция для расширения возможностей конкретных Ajax-классов.
 * Добавляет функции: y5.Get.*, y5.Post.*, y5.Load.*.
 * @private
 */
Request.ext = function(type, obj, notExt) {
    if (Types.undef(notExt)) {
        Utils.objectExtends(obj, Request, moduleName);
    }

    var module = moduleName + '.' + type;

    y5.Get[type] = function(url, params, query) {
        var xhr = new obj(url, Utils.objectCopy(params || {}, {method: 'get'}));
        xhr.send(query);
        return xhr;
    };

    y5.Post[type] = function(url, params, query) {
        var xhr = new obj(url, Utils.objectCopy(params || {}, {method: 'post'}));
        xhr.send(query);
        return xhr;
    };

    y5.Load[type] = function(container, url, params, query) {
        var xhr = new obj(url, params);
        xhr.onload = function(req) {
            if (Types.string(container)) {
                container = document.getElementById(container);
            }
            container.innerHTML = req.responseText;
        };
        xhr.send(query);
        return xhr;
    };

    obj.prototype.toString = function() {
        return module;
    };

    Request[type] = obj;

    y5.loaded(module);
};

y5.Request = Request;
y5.loaded(moduleName);

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

var UNDEF = y5.UNDEF;

/**
 * Создает экземпляр класса для выполнения Ajax-запросов с помощью объекта XMLHttpRequest.
 * @class Класс для выполнения Ajax-запроса с помощью объекта XMLHttpRequest. Использование XMLHttpRequest позволяет загружать внешние документы и использовать функции DOM для доступа к их содержимому. Одним из ограничений загрузки данных при помощи XMLHttpRequest является невозможность выполнения запросов между разными доменами. Для междоменных запросов следует использовать y5.Request.Script или y5.Request.Iframe.
 * При использовании XMLHttpRequest на сервер отправляются дополнительные заголовки:
 * X-Requested-With: XMLHttpRequest
 * Accept: text/javascript,text/html,application/xml,text/xml,text/plain,&#42;/&#42;
 * @constructor
 * @name y5.Request.XML
 * @extends y5.Request
 */
function RequestXML(url, params) {
    this.Request(url, params);
}

RequestXML.prototype = {
    /**
     * Параметры по умолчанию.
     * @memberOf y5.Request.XML
     * @name y5.Request.XML.params
     * @type Object
     * @private
     * @example
     * // параметры по умолчанию
     * {
     *     async: true,
     *     contentType: 'application/x-www-form-urlencoded',
     *     encoding: 'UTF-8',
     *     headers: {
     *         'X-Requested-With': 'XMLHttpRequest',
     *         'Accept': 'text/javascript,text/html,application/xml,text/xml,text/plain,&#42;/&#42;'
     *     }
     * }
     */
    defaultParams: {
        async: true,

        contentType: 'application/x-www-form-urlencoded',

        encoding: 'UTF-8',

        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'Accept': 'text/javascript,text/html,application/xml,text/xml,text/plain,*/*'
        }
    },

    abort: function() {
        try {
            this.req.abort();
        } catch (e) {}

        this.dispatch('abort');
    },

    _send: function(query) {
        var _this = this,
            params = this.params,
            method = params.method,
            async = params.async,
            get = method == 'get',
            post = method == 'post',
            url = this.url;

        if (typeof XMLHttpRequest != UNDEF) {
            this.req = new XMLHttpRequest();
        } else if (typeof ActiveXObject != UNDEF) {
            this.req = new ActiveXObject('Microsoft.XMLHTTP');
            /*
            var objects = ['MSXML2.XMLHTTP.3.0', 'Microsoft.XMLHTTP'];
            for (var i = 0, l = objects.length; i < l; i++) {
                if ((this.req = new ActiveXObject(objects[i]))) {
                    break;
                }
            }
            */
        } else {
            return false;
        }

        if (get && query) {
            url = url.clone();
            url.query(query);
            query = null;
        }

        if (post && !query) {
            url = url.clone();
            query = url.query();
            url.clearQuery();
        }

        // toUpperCase для Opera 8
        this.req.open(method.toUpperCase(), url, async);

        if (async) {
            this.req.onreadystatechange = function() {
                _this.onStateChange();
            };
        }

        var headers = y5.Utils.objectCopy({}, params.headers);
        if (post) {
            var encoding = params.encoding;
            headers['Content-Type'] = params.contentType + (encoding ? '; charset=' + encoding : '');
            if (y5.is_gecko && y5.gecko_ver < 1.8) {
                headers['Connection'] = 'close'; // Mozilla Bug #246651
            }
        }
        for (var name in headers) {
            this.req.setRequestHeader(name, headers[name]);
        }

        this.req.send(query);

        if (!async) {
            this.onStateChange();
        }
    }
};

if (y5.is_ie7down) {

    RequestXML.prototype.onStateChange = function() {

        var req = this.req;

        if (this.req.readyState == 4) {
            this.req = {
                status: req.status,
                readyState: req.readyState,
                responseText: req.responseText,
                responseXML: req.responseXML
            };

            this.req.getResponseHeader = function(name) {
                return req.getResponseHeader(name);
            };

            this.req.abort = function() {
                return req.abort();
            };
        }
        this.Request.prototype.onStateChange.apply(this);
    };
}

y5.Request.ext('XML', RequestXML);

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

/**
 * Создает экземпляр класса для выполнения Ajax-запросов методом загрузки скрипта.
 * @class Класс для выполнения Ajax-запроса с помощью загрузки скрипта. Для отправки запроса используется возможность динамически добавлять элемент script в тело документа. Чтобы получить ответ, в загруженном скрипте должен присутствовать вызов функции y5.Request.Script.onload
 * @constructor
 * @name y5.Request.Script
 * @extends y5.Request
 *
 * @example
 * // в вызываемом скрипте
 * var xhr = new y5.Request.Script('comments.js');
 * xhr.onload = function(req) { comments.innerHTML = req.responseText };
 * xhr.onerror = function(req) { comments.innerHTML = 'Ошибка загрузки комментариев' };
 * xhr.onloading = function() { spinner.show() };
 * xhr.oncomplete = function() { spinner.hide() };
 * xhr.send({id: post_id});
 *
 * // в скрипте ответе comments.js
 * y5.Request.Script.onload('y5__id1', ...);
 *
 * // Для того, чтобы клиент получил ответ, необходимо в вызываемом скрипте выполнить функцию <xref module="y5.Request.Script" method="onload"/>(key, responseText, [headers]) или <xref module="y5.Request.Script" method="onerror"/>.
 */
function RequestScript(url, params) {
    this.Request(url, params);

    // в одну строку, чтобы была удалена при оптимизации
    if (this.params.method == 'post') { var moduleName = this.toString(); y5.Console.warn(moduleName + ': POST not supported', [moduleName]); }
}

RequestScript.prototype = {
    /**
     * Параметры по умолчанию.
     * @memberOf y5.Request.Script
     * @name y5.Request.Script.params
     * @type Object
     * @private
     * @example
     * // параметры по умолчанию
     * {
     *     encoding: 'UTF-8',
     *     key: 'requestid'
     * }
     */
    defaultParams: {
        encoding: 'UTF-8',
        key: 'requestid'
    },

    abort: function() {
        this.dispatch('abort');
        this.end();
    },

    /**
     * @private
     */
    _init: function() {
        this.observer = new y5.Observer(':onMessage', this._load, this.id, true, this);
    },

    /**
     * @private
     */
    _end: function() {
        if (this.observer) {
            this.observer.cleanup();
            delete this.observer;
        }
    },

    /**
     * @private
     */
    _send: function(query) {
        var url = this.url.clone(),
            params = this.params,
            requestid = {};

        // замещаем параметры запроса
        if (query) {
            url.query(query);
        }

        // добавляем requestid
        requestid[params.key] = this.id;
        url.addParams(requestid);

        y5.Loader.loadScript(url, params.encoding);

        this.dispatch('loading');
    },

    /**
     * @private
     */
    _load: function(req) {
        try {
            this.req = req;
            this.onStateChange();
        } catch (e) {
            this.dispatch('exception', e);
        }
    }
};

function notify(key, data, headers, status) {
    headers = headers || {};

    for (var name in headers) {
        headers[name.toLowerCase()] = headers[name];
    }

    var getResponseHeader = function(name) {
        return headers[name.toLowerCase()];
    };

    var httpStatus = getResponseHeader('status');
    if (httpStatus) {
        status = parseInt(httpStatus, 10);
    }

    var req = {
        responseText: data,
        status: status,
        readyState: 4,
        getResponseHeader: getResponseHeader
    };

    y5.Notify(':onMessage', key, req);
}

/**
 * Функция, которую необходимо вызвать в загружаемом скрипте, чтобы объект запроса получил ответ, и возникло событие load.
 * @memberOf y5.Request.Script
 * @name onload
 * @function
 * @static
 * @param {ID} key Значение параметра requestid, переданное при запросе скрипта
 * @param {Object} data Данные
 * @param {Object} [headers] Заголовки ответа (по умолчанию {status: 200})
 *
 * @example
 * y5.Request.Script.onload('y5__id1', {id: 15, title: '...', body: '...'});
 */
RequestScript.onload = function(key, data, headers) {
    notify(key, data, headers, 200);
};

/**
 * Функция, которую необходимо вызвать в загружаемом скрипте, чтобы объект запроса получил ответ и возникло событие error.
 * @memberOf y5.Request.Script
 * @name onerror
 * @function
 * @static
 * @param {ID} key Значение параметра requestid, переданное при запросе скрипта
 * @param {Object} data Данные
 * @param {Object} [headers] Заголовки ответа (по умолчанию {status: 500})
 *
 * @example
 * y5.Request.Script.onerror('y5__id1', {id: 15, title: '...', body: '...'});
 */
RequestScript.onerror = function(key, data, headers) {
    notify(key, data, headers, 500);
};

// compatibility
if (!y5.AjaxJS) {
    y5.AjaxJS = {onload: RequestScript.onload};
}

y5.Request.ext('Script', RequestScript);

});y5.require('Elements', 'Template', 'Request', function() {

var Events = y5.Events,
    Elements = y5.Elements,
    Utils = y5.Utils,
    elStyle = 'visibility:hidden;position:absolute;left:0;top:0;width:0;height:0;';

function iframeName(id) {
    return 't_' + id;
}

function iframeDocument(iframe) {
    return iframe.contentWindow.document;
}

function insertElement(el) {
    var body = document.body;
    return body.insertBefore(el, body.firstChild);
}

function createIframe(id) {
    var el = Elements.create('iframe', {name: iframeName(id), src: 'about:blank', style: elStyle});
    el = insertElement(el);

    if (y5.is_ie6down || (y5.is_opera && y5.opera_ver >= 9)) {
        var doc = iframeDocument(el);
        doc.open();
        doc.write('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><span></span>');
        doc.close();
    }

    return el;
}

var templateHidden;
function createForm(id, url, query) {
    var el, formContent = [];

    url.query(query);
    url.queryKeys().forEach(
        function(name) {
            url.getParams(name).forEach(
                function(value) {
                    formContent.push({name: name, value: value});
                }
            );
        }
    );
    url.clearQuery();

    el = Elements.create('form', {target: iframeName(id), action: url, method: 'post', style: elStyle});

    if (!templateHidden) {
        templateHidden = new y5.Template('<input type="hidden" name="#{name}" value="#{value}"/>');
    }
    el.innerHTML = templateHidden.evaluateArray(formContent);

    return insertElement(el);
}

function removeElement(el) {
    if (el) {
        el.parentNode.removeChild(el);
        el = undefined;
        return true;
    }
    return false;
}

function getMetaInfo(doc) {
    var meta = {},
        metaTags = doc.getElementsByTagName('meta');

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

        meta[(el.httpEquiv || el.name).toLowerCase()] = el.content;
    }

    return meta;
}

function formSubmit(form, action, target, method) {
    // save
    var formAction = form.action,
        formTarget = form.target,
        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);
}

function makeObserver() {
    this.observer = Events.observe('load', this._load, this.iframe, true, this);
}

/**
 * Создает экземпляр класса для выполнения Ajax-запросов путем загрузки данных в iframe.
 * @class Класс для выполнения Ajax-запроса с помощью загрузки данных в iframe. Для  отправки запроса используется возможность динамически добавлять элемент iframe в тело документа. При загрузке документа в iframe, тег body должен содержать тело ответа. Теги meta могут содержать заголовки ответа.
 * @constructor
 * @name y5.Request.Iframe
 * @extends y5.Request
 */
function RequestIframe(url, params) {
    this.Request(url, params);
}

RequestIframe.prototype = {
    abort: function() {
        // TODO
        this.end();
        this.dispatch('abort');
    },

    /**
     * @private
     */
    _init: function() {
        this.iframe = createIframe(this.id);

        // Opera отсылает событие load при загрузке фейкового фрема, поэтому надо ставить timeout
        if (y5.is_opera/* && y5.opera_ver < 9*/) {
            Utils.setTimeout(function() { makeObserver.call(this); }, 0, this);
        } else {
            makeObserver.call(this);
        }
    },

    /**
     * @private
     */
    _end: function() {
        this.observer.cleanup();
        delete this.observer;

        // сохраняем ссылку на iframe, чтобы чуть позже удалить его
        var iframe = this.iframe;
        window.setTimeout(function() { removeElement(iframe); }, 500);
    },

    /**
     * Посылает запрос.
     * @memberOf y5.Request.Iframe
     * @name y5.Request.Iframe.send
     * @function
     * @param {String | Object | Form} [query] Данные запроса в виде строки запроса GET или объекта
     * @private
     * @example
     * req.send(form);
     */
    _send: function(query) {
        // see opera hack
        if (!this.observer) {
            Utils.setTimeout(function() { this._send(query); }, 0, this);
            return;
        }

        var params = this.params,
            url = this.url.clone();
        // если в send() передана форма и она имеет метод submit. Так делает y5.Requst.Form.
        if (query && query.submit) {
            formSubmit(query, url, iframeName(this.id), params.method);

        // иначе создаем форму и отправляем ее
        } else {
            switch (params.method) {
                case 'post':
                    var form = createForm(this.id, url, query);
                    form.submit();
                    removeElement(form);
                    break;

                default:
                    if (query) {
                        url.query(query);
                    }
                    this.iframe.src = url;
                    break;
            }
        }

        this.dispatch('loading');
    },

    /**
     * @private
     */
    _load: function() {
        try {
            var doc = iframeDocument(this.iframe),
                meta = getMetaInfo(doc),
                getResponseHeader = function(name) {
                    return meta[name.toLowerCase()];
                },
                status = getResponseHeader('status'),
                body = doc.getElementsByTagName('body')[0].innerHTML;

            this.req = {
                status: status ? parseInt(status, 10) : 200,
                readyState: 4,
                responseText: body,
                getResponseHeader: getResponseHeader
            };
            this.onStateChange();
        } catch (e) {
            this.dispatch('exception', e);
        }
    }
};

y5.Request.ext('Iframe', RequestIframe);

});y5.require('Utils', 'Request', 'FormCollector', function() {

var Request = y5.Request,
    Utils = y5.Utils;

/**
 * Создает экземпляр класса y5.Request.Form
 * @class Класс для отправки данных в форме
 * @constructor
 * @name y5.Request.Form
 * @extends y5.Request
 *
 * @example
 * var req = new y5.Request.Form("test.xml");
 * req.onload = function(req) { alert(req.responseText) };
 * req.onerror = function(e) { alert(e.message) };
 * req.send(form);
 */
function RequestForm(url, params) {
    var context;
    if (params) {
        // получаем ссылку на контекст выполнения обработчиков для того, чтобы не копировать его через objectCopy
        context = params.callbackContext;
        delete params.callbackContext;
    }

    this.params = Utils.objectCopy(
        Utils.objectCopy({}, this.defaultParams),
        params
    );

    this.transport = 'Request.' + this.params.transport;

    if (y5.Types.object(context)) {
        this.params.callbackContext = context;
    }

    this.require(function() {
        this.module = new Request[this.params.transport](url, this.params);
    });
}

RequestForm.prototype = {
    /**
     * Параметры по умолчанию.
     * @memberOf y5.Request.Form
     * @name y5.Request.Form.params
     * @type Object
     * @private
     * @example
     * // параметры по умолчанию
     * {
     *     method: 'post',
     *     transport: 'Iframe'
     * }
     */
    defaultParams: {
        method: 'post',
        transport: 'Iframe'
    },

    abort: function() {
        this.require(function() {
            this.module.abort();
        });
    },

    send: function(form) {
        switch (this.params.transport) {
            case 'Iframe':
                break;

            default:
                form = y5.FormCollector.collectTags(form);
                break;
        }

        this.require(function() {
            // копируем обработчики
            for (var func in this) {
                if (func.indexOf('on') == 0) {
                    this.module[func] = this[func];
                }
            }
            this.module.send(form);
        });
    },

    /**
     * @private
     */
    require: function(callback) {
        var _this = this;

        y5.require(this.transport, function() { callback.apply(_this) });
    }
};

Request.ext('Form', RequestForm, true);

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

var VOID = y5.VOID;

/**
 * Base class for performing Ajax requests.
 * @constructor
 * @deprecated
 * @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) {
    // hack
    if (y5.AjaxForm && (ajax instanceof y5.AjaxForm)) {
        body = ajax.form;
        ajax = new y5.Request.Form(url, {method: method || 'post'});
    } else {
        ajax.url = new y5.URL(url);
        ajax.params.method = (method || 'get').toLowerCase();
    }
    ajax.onload = function(req) { (responseSuccess || VOID)(req.responseText, trace) };
    ajax.onerror = function(err) { (responseError || VOID)(trace, err) };

    this.ajax = ajax;
    this.body = body || null;
};

y5.Ajax.prototype = {
    /**
     * Send request.
     */
    send: function() {
        this.ajax.send(this.body);
    },

    /**
     * Abort request.
     * @alias y5.Ajax.abort
     */
    abort: function () {
        this.ajax.abort();
    }
};

y5.loaded('Ajax');

});y5.require('Request.Form', function() {
    /**
     * @deprecated
     */
    y5.AjaxForm = function(form) {
        this.form = form;
    };

    y5.loaded('AjaxForm');
});y5.require('Request.Iframe', function() {
    /**
     * @deprecated
     */
    y5.AjaxIframe = y5.Request.Iframe;

    y5.loaded('AjaxIframe');
});y5.require('Request.Script', function() {
    /**
     * @deprecated
     */
    y5.AjaxJS = function(id) {
        this.id = id;
        this.Request();
    };
    y5.AjaxJS.prototype = y5.Request.Script.prototype;
    y5.AjaxJS.onload = y5.Request.Script.onload;

    y5.loaded('AjaxJS');
});
