window.$e = window.$e || {};
$e.DEBUG = 1;
$e.isIE = !!navigator.userAgent.match(/\smsie\s[678]/i);
$e.isIE67 = !!navigator.userAgent.match(/\smsie\s[67]/i);
$e.isIE8 = !!navigator.userAgent.match(/\smsie\s8/i);
$e.useFixForIE = false;
$e.doc = document;
$e.de = document.documentElement;

function l() {
    if ($e.DEBUG && window.console && !$e.isIE) {
        console.log.apply(console, arguments);
    } else if ($e.DEBUG && window.opera && opera.postError) {
        opera.postError.apply(opera, arguments);
    } else {
        if (status.length > 100) {
            status = '';
        }
        status += '    LOG: ' + Array.prototype.slice.call(arguments).join(', ');
    }
}

function timer(comment) {
	var me = arguments.callee;
	var now = (new Date()).getTime();
	if (!me.log) {
		me.log = [];
	}

	me.log.push([now, comment || 'mark']);
}

timer.dump = function(comment) {
	if (comment) {
		timer(comment);
	}

	if (this.log) {
		var prev = 0;
		for (var i = 0; i < this.log.length; i++) {
			var item = this.log[i];
			l((prev ? "+" + Math.round(item[0] - prev, 3) + "ms: " : "Start: ") + item[1]);
			prev = item[0];
		}

		delete this.log;
	}
}


function assert(cond) {
    if (!cond) {
        var wh = assert.caller ?
            assert.caller.toString().substring(0, 60) + '...' :
            'unknown';

        throw new Error("Assertion failed at " + wh);
    }
}

function wrap(callback, comment) {
    return function () {
        try {
            return callback.apply(this, arguments);
        } catch (e) {
            logJSError(comment ? "Error in " + comment + ': ' + jsErrorToString(e) : jsErrorToString(e));
            throw e;
        }
    }
}

function logJSError(e) {
    return ;
    l("Logging error to server: " + e);
    e = e.length > 1500 ? e.substr(0, 1500) + '...' : e;
    (new Image()).src = window.jsConfig.reportPath + '&text=' + encodeURIComponent(e);
}

function jsErrorToString(e)
{
	var etext  = e.name + ' [' + e.message + ']';

    if (e.fileName) { etext += '\nFile "' + e.fileName + '"'; }
    if (e.lineNumber) { etext += '\nLine ' + e.lineNumber; }

    etext += '\nUA: ' + navigator.userAgent;

    if (e.stack) { etext += '\nStack: ' + e.stack; }
    etext += "\nLocation: " + location.href;

	return etext;
}


if ($e.de.getBoundingClientRect) {
    $e.getPosition = function (node) {
        var box = node.getBoundingClientRect();
        var view = this.getViewportPosition();
        return {
            x: Math.round(box.left) + view.x,
            y: Math.round(box.top) + view.y
        };
    }
} else {
    // FF2
    $e.getPosition = function(node) {
        var left = 0, top = 0, body = document.body;

        // Count scroll, but IE8 (some versions) already counts it
        if (!this.useFixForIE8) {
            for (var n = node.parentNode; n && n != body; n = n.parentNode) {
                left -= n.scrollLeft;
                top -= n.scrollTop;
            }
        }

        // And offsets
        for (; node && node != body; node = node.offsetParent) {
            left += node.offsetLeft /*  + node.offsetParent.clientLeft */;
            top += node.offsetTop /*  + node.offsetParent.clientTop */;
        }

        return { x:left, y:top };
    }
}

$e.getViewportPosition = function () {
    var de = document.documentElement;
    return {
        x: 'pageXOffset' in window ? window.pageXOffset : de.scrollLeft,
        y: 'pageYOffset' in window ? window.pageYOffset : de.scrollTop,
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    };
}

$e.scrollPageY = function(newPos) {
    this.de.scrollTop = newPos;
    // WebKit uses body for scrolling
    if (!this.de.scrollTop) {
        this.doc.body.scrollTop = newPos;
    }
}

$e.create = function (name, properties) {
    var p = properties || {};
    var e = document.createElement(name);
    var style = 'style' in p ? p.style : '';
    delete p.style;
    var child = p.child;
    delete p.child;

    for (var i in p) {
        e[i] = p[i];
    }

    style && (e.style.cssText = style);
    child && e.appendChild(child);
    return e;
}


function findId(id) {
	return document.getElementById(id);
}

function findTag(name, context) {
	var n = (context || document).getElementsByTagName(name);
	return n ? n[0] : null;
}

/*
* Find first element by class name
*/
function find(klass, root, tagHint) {
    if (root.getElementsByClassName) {
        var e = root.getElementsByClassName(klass);
        return e ? e[0] : null;
    }

    if (root.querySelector) {
        // IE 8
        return root.querySelector('.' + klass);
    }

    // IE7-
    var tags = root.getElementsByTagName(tagHint || '*');
    var tagsCount = tags.length;
    var re = new RegExp('(^|\\s)' + klass + '(\\s|$)');
    for (var i = 0; i < tagsCount; i++) {
        if (re.test(tags[i].className)) {
            return tags[i];
        }
    }

    return null;
}

function findAll(klass, root, tagHint) {
    if (root.getElementsByClassName) {
        return root.getElementsByClassName(klass);
        //return e ? e[0] : null;
    }

    if (root.querySelectorAll) {
        // IE 8
        return root.querySelectorAll('.' + klass);
    }

    // IE7-
    var tags = root.getElementsByTagName(tagHint || '*');
    var tagsCount = tags.length;
    var re = new RegExp('(^|\\s)' + klass + '(\\s|$)');
    var found = [];
    for (var i = 0; i < tagsCount; i++) {
        if (re.test(tags[i].className)) {
            found.push(tags[i]);
        }
    }

    return found;
}

function findParent(elm, klass) {
	var re = classRe(klass);

    for(;elm.parentNode && elm != document.body; elm = elm.parentNode) {
        if (re.test(elm.className)) {
            return elm;
        }
    }

    return null;
}



function parentTag(elm, name, stopAt) {
	name = name.toUpperCase();

    for(;elm.parentNode && elm != document.body && elm != stopAt; elm = elm.parentNode) {
        if (elm.nodeName == name) {
            return elm;
        }
    }

    return null;
}

function findRightTag(node, name) {
    name = name.toUpperCase();
    for ( ;node && node.nodeName != name; node = node.nextSibling) {};
    return node;
}

function findLeftTag(node, name) {
    name = name.toUpperCase();
    for ( ;node && node.nodeName != name; node = node.previousSibling) {};
    return node;
}





function classRe(klass) {
    var r = classRe.cache[klass] ? classRe.cache[klass] : (classRe.cache[klass] = new RegExp('(^|\\s)' + klass + '(\\s|$)', 'g'));
    r.lastIndex = 0;
    return r;
}
classRe.cache = {};

function replaceClass(elm, oldKlass, newKlass) {
    // Cache regexps
    elm.className = elm.className.replace(classRe(oldKlass), '$1' + newKlass + '$2');
};

function hasClass(elm, klass) {
    return (classRe(klass).test(elm.className));
};

function addClass(elm, klass) {
	if (!hasClass(elm, klass)) {
		elm.className += (elm.className === '' ? '' : ' ') + klass;
	}
};

function removeClass(elm, klass) {
    elm.className = elm.className.replace(classRe(klass), '$1').replace(/\s+$/, '');
};

function hasAttr(node, attr) {
    return node.attributes[attr] !== undefined;
};

function getAttr(node, attr) {
    var attrs = node.attributes[attr];
    return attrs ? attrs.nodeValue : undefined;
};


function addHandlers(node, h) {
	for (var type in h) {
		addEvent(node, type, h[type]);
	}
}

function addEvent(obj, type, fn) {
    // We'll capture all errors this way
    var fn = wrap(fn, type + '_handler');

    if (obj.addEventListener) {
		obj.addEventListener(type, fn, false);
    } else if (obj.attachEvent) {
		obj.attachEvent("on"+type, fn);
    } else {
        throw new Error('Events attaching are not supported in this browser');
    }

    return [addEvent.MAGIC, obj, type, fn];
}

addEvent.MAGIC = 0xf565;

function removeEvent(handle) {
	assert(handle.length == 4);
	assert(handle[0] == addEvent.MAGIC);
	if (obj.removeEventListener) {
		obj.removeEventListener(handle[1], handle[2], handle[3]);
	} else {
		obj.detachEvent("on" + handle[1], handle[2]);
	}
}

function preventEvent(e) {
	e.returnValue = false;
	e.preventDefault && e.preventDefault();
}


$e.isInside = function(node, parent) {
	if (parent.compareDocumentPosition) {
		return !!(parent.compareDocumentPosition(node) & 0x10); // DOCUMENT_POSITION_CONTAINED_BY
	} else if (parent.contains) {
		// IE
		return parent.contains(node);
	} else {
		throw new Exception('Cannot use isInside()');
	}
}


/**
* @return Whether scrolling took place
*/
$e.makeVisible = function (item, scroller, margin) {
    if (!item) { return; };
    var itemTop = Math.max(0, item.offsetTop - (margin || 0));
    var itemBottom = itemTop + item.offsetHeight + (margin || 0);

    if (itemTop < scroller.scrollTop) {
        scroller.scrollTop = itemTop;
        return true;
    } else if (itemBottom > scroller.scrollTop + scroller.clientHeight) {
        scroller.scrollTop = itemBottom - scroller.clientHeight;
        return true;
    }

    return false;
}

$e.makeVisibleHor = function (item, scroller, margin) {
    if (!item) { return; };
    var itemLeft = Math.max(0, item.offsetLeft - (margin || 0));
    var itemRight = itemLeft + item.offsetWidth + (margin || 0);

    if (itemLeft < scroller.scrollLeft) {
        scroller.scrollLeft = itemLeft;
        return true;
    } else if (itemRight > scroller.scrollLeft + scroller.clientWidth) {
        scroller.scrollLeft = itemRight - scroller.clientWidth;
        return true;
    }

    return false;
}

$e.clearSelection = function() {
    var sel ;
    if(document.selection && document.selection.empty) {
        document.selection.empty() ;
    } else if (window.getSelection) {
        sel=window.getSelection();
        if(sel && sel.removeAllRanges) {
            sel.removeAllRanges();
        }
    }
}





var keyCodes = {
    KEY_ENTER: 13,
    KEY_ESCAPE: 27,
    KEY_SPACE: 32,
    KEY_UP: 38,
    KEY_DOWN: 40,
    KEY_LEFT: 37,
    KEY_RIGHT: 39,
    KEY_END: 35,
    KEY_HOME: 36,
    KEY_PGUP: 33,
    KEY_PGDN: 34
};

/**
* Fixes auto-repeat events and keydown/keypress events across different browsers:
*
* Opera doesn't send keyDown for auto-repeat
* Mozilla sends keyPress for special keys
* IE and other browsers store key code in different fields of Event object
*
* onKeydown is called for every key pressed, and auto-repeats. It holds key code in e.keyCodeFixed.
*        onKeydown can be prevented for special keys.
* onKeypress is called for normal keys, auto-repeats and holds character code in e.whichFixed.
*       onKeypress event can be prevented for character keys only.
*/
$e.addKeyListener = function(elm, handlers) {
    var keyDownCode = 0, afterKeydown = false, preventKeypress = false;
    function isSpecial(e) {
        // IE/Chrome don't send keypress for special keys
        // Opera & FF do, but they have e.which === 0
        // old WebKit sets pseudo-ascii code (63xxx) in e.which

        // Also some keys are not treat as special: Break (3), Scroll Lock (145), NumLock (144), Bs(8),
        // Enter (13),
        if ((e.which || e.keyCode) < 32) { return true; }
        return (e.which === 0 || (e.which > 63200 && e.which < 63300));
    }

    addEvent(elm, 'keydown', function(ev) {
        var e = ev || window.event;
        keyDownCode = e.keyCode;
        afterKeydown = true;
        e.keyCodeFixed = e.keyCode;
        handlers.keydown && handlers.keydown(e);
        preventKeypress = (e.returnValue === false);
    });

    addEvent(elm, 'keypress', function(ev) {
        var e = ev || window.event;

        // Fix auto-repeat for Opera
        if (!afterKeydown && keyDownCode && handlers.keydown) {
            e.keyCodeFixed = keyDownCode;
            handlers.keydown(e);
            preventKeypress = (e.returnValue === false);
        }

        afterKeydown = false;
        if (preventKeypress) {
            preventEvent(e);
            preventKeypress = false;
        }

        e.whichFixed = e.which || e.keyCode;
        if (e.keyCodeFixed) { delete e.keyCodeFixed; }
        if (!isSpecial(e) && handlers.keypress) {
            handlers.keypress(e);
        }
    });

    addEvent(elm, 'keyup', function(ev) {
        var e = ev || window.event;
        if (e.keyCode == keyDownCode) {
            keyDownCode = 0;
        }

        e.keyCodeFixed = e.keyCode;
        handlers.keyup && handlers.keyup(e);
    });
}

$e.throttle = function(cbk, interval) {
    var lastCall = 0, to = null;
    function runCallback() { lastCall = (new Date()).getTime(); cbk(); };
    return function() {
        var t = (new Date).getTime();
        if (t  - lastCall < interval) {
            clearTimeout(to);
            to = setTimeout(runCallback, lastCall + interval - t);
        } else {
            clearTimeout(to);
            lastCall = t;
            cbk();
        }
    }
}

$e.trim = function(val) {
    return val.replace(/^\s+|\s+$/g, '');
}

/*
*   HTML to text & text to HTML conversions
*/
$e.getInnerText = function(node) {
    // Actually, innerText and textContent are different, e.g. for option tags
    return 'innerText' in node ? node.innerText :
        ('textContent' in node ? node.textContent : $e.htmlToText(node.innerHTML));
}

$e.setInnerText = function(node, text) {
    if ('innerText' in node) { node.innerText = text; return; }

    if (node.childNodes.length) {
        while (node.lastChild) {
            node.removeChild(node.lastChild);
        }
    }
    node.appendChild(node.ownerDocument.createTextNode(text));
}

var h2tTable = {'amp': '&', 'lt': '<', 'gt': '>', 'quot': '"', 'apos': "'", 'raquo': '»', 'laquo': '»' };
$e.htmlToText = function (txt) {
    // Fixes innerText for really old browsers
    txt = txt.replace(/<[!\[\/a-zA-Z]+?>/g, '').replace(/&([a-zA-Z]+);?/g, function(m, m1) { return h2tTable[m1] ? h2tTable[m1] : '' }).
            replace(/&#(\d+);?/g, function(m, m1){ return String.fromCharCode(parseInt(m1, 10)); });
    return txt;
}

var t2hTable = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#039;'};
$e.h = $e.html = function(text) {
    return text.replace(/[<>&'"]/g, function(m) { return t2hTable[m] });
}



$e.arrayCache = function(limit) {
    var store = {}, list = [];

    return {
        get: function(key) {
            return key in store ? store[key] : null;
        },
        put: function(key,value) {
            if (!key in store) {
                if (list.length >= limit) {
                    var firstKey = list.shift();
                    delete store[firstKey];
                }
                list.push(key);
            }
            store[key] = value;
        }
    };
}

$e.uuid = 1000;
$e.dataCache = {};
$e.uniqueId = function() {
    return $e.uuid++;
}

$e.id = function(node) {
    return node['$e_id'] ? node['$e_id'] : (node['$e_id'] = $e.uuid++);
}

$e.data = function (elm, key) {
    if (elm.$eId && $e.dataCache[elm.$eId]) {
        return $e.dataCache[elm.$eId][key];
    }

    return undefined;
}

$e.addFocusListener = function(root, type /* focusin/focusout */, fn) {
    if (root.addEventListener) {
        root.addEventListener(type == 'focusin' ? 'focus' : 'blur', fn, true);
    } else {    // IE
        root.attachEvent('on' + type, function () {
            fn(window.event);
        });
    }
};


// Animation
$e.animate = function(callback, options) {
    options = options || {};
    var timeStep = options.timeStep || 16;
    var fullDuration = options.fullDuration || 200;
    var value = options.start || 0;
    var from = options.from || 0;
    var to = options.to || 1;
    assert(value >= from && value <= to);
    var interval = null;
    var startValue, endValue, startTime, endTime;
    var tickFn = wrap(onTick);

    function isRunning() { return !!interval; }
    function getTarget() { return interval ? (endValue - value) : 0; }

    function setBounds(newFrom, newTo) {
    	assert(newTo >= newFrom);
    	if (newFrom > from) {
    		endValue = Math.max(newFrom, endValue);
    		value = Math.max(newFrom, value);
    	}

    	if (newTo < to) {
    		endValue = Math.min(newTo, endValue);
    		value = Math.min(newTo, value);
    	}

    	from = newFrom;
    	to = newTo;
    }

    function stop() {
        clearInterval(interval);
        interval = null;
    }

    function jump (pos) {
        assert(pos >= from && pos <= to);
        value = pos;
        stop();
        callback && callback.call(i, pos, 1);
    }

    function goTo (target, duration) {
        assert(target >= from && target <= to);
        if (target == value) {
            jump(target);
            return;
        }

        startValue = value;
        endValue = target;
        startTime = (new Date()).getTime();
        // keep constant velocity
        duration = duration || (Math.abs((endValue - startValue) / (from - to)) * fullDuration);
        endTime = startTime + duration;
        assert(Math.abs(endTime - startTime) > 1e-4);

        if (!interval) {
            interval = setInterval(tickFn, timeStep);
        }
    }

    function set(val) {
        assert(!interval);
        assert(val >= from && val <= to);
        value = val;
    }

    function onTick() {
    	if (!interval) { return; } // IE bug
        var pos = ((new Date()).getTime() - startTime) / (endTime - startTime);
        pos = Math.min(1, pos);
        assert(pos >= 0 && pos <= 1);
        value = (endValue - startValue) * pos + startValue;

        if (pos == 1) {
            stop();
        }

        callback && callback.call(i, value, pos);
    }

    var i = {
        stop: stop,// Stop without calling anything
        jump: jump, // Jump immediately, stop animation, calling a callback
        goTo: goTo, // Start animation to target point or jump() if we are there
        set: set,    // Just change internal value
        isRunning: function() { return !!interval },
        getTarget: getTarget,
        getValue: function() { return value },
        setBounds: setBounds
    };

    return i;
}
/*
* Combobox - supports 2 types of input widgets:
*   1. Custom select - is created from SELECT.js-select
*   2. Autocomplete list - created from INPUT
*
* Options:
*   tokenizerFn -
*   cancelRequestFn -
*   sendRequestFn = fn(callback(response))
*
*   throttleInterval -
*   autosubmit - Submit form on selecting element by click or ENTER key
*   autocompleteEmpty
*   emptyText
*
* Response object (passed to callback by sendRequestFn()):
*   result: false/'list'/'html'
*   error: 'text'
*   html: html code for list
*   data: data array
*   headerHtml, footerHtml
*/
$e.form = $e.form || {};
//~ (function($e) {


$e.form.attachCombobox = function (elm, options) {

        function attachToSelect(elm) {
            var code = [];
            var elementsCount = elm.options.length;
            var defaultSelectedValue = '';
            var defaultSelectedText = '';

            // Grab options from original SELECT element
            for (var i = 0; i < elementsCount; i++) {
                var o = elm.options[i];
                var isSelected = o.selected ? ' class="selected"' : '';
                code.push('<li data-value="'+ $e.html(o.value) +'"' + isSelected + '>' + $e.html(o.text) + '</li>');

                if (o.defaultSelected) {
                    defaultSelectedValue = o.value;
                    defaultSelectedText = o.text;
                }
            }

            if (!elementsCount) {
                code.push('<li data-value="" class="unselectable list-message"><em>' + msgEmptyText + '</em></li>');
            }

            // Create HTML structure
            listHtmlCode = code.join('');

            // Read-only input element
            if (elm.selectedIndex >= 0) {
                var value = elm.options[elm.selectedIndex].value;
                var text = elm.options[elm.selectedIndex].text;
            } else {
                value = text = '';
            }

            var value = selectedOption ? getAttr(selectedOption, 'data-value') : '';
            inputElement = $e.create('input', { type: 'text', autocomplete: 'off', value: text, defaultValue: text,
                    readOnly: true, name: 'dummyInput', id: elm.id });
            hiddenInput = $e.create('input', { type: 'hidden', value: value, name: elm.name, defaultValue: value });

            // Now, remove SELECT element
            elm.parentNode.insertBefore(hiddenInput, elm);
            elm.parentNode.insertBefore(inputElement, elm);
            elm.parentNode.removeChild(elm);
            replaceClass(wrapper, 'f-wrap-select', 'f-wrap f-fake-select f-icon-right collapsed');
        }

        function attachToInput(elm) {
            inputElement = elm;
            inputElement.autocomplete = 'off';
            listHtmlCode = null;
            addClass(wrapper, 'collapsed');
        }

        function createListNode(listCode) {
            if (!listWrapper) {

                listHtmlCode = null;    // Free memory
                scrollDiv = listElement = $e.create('ul', { className: 'vlist', innerHTML: listCode });
                listHeader = $e.create('div', { className: 'list-header' });
                listFooter = $e.create('div', { className: 'list-footer' });

                // Decorations
                var d1 = $e.create('ins', { className: 'li-top with-pe' }), d2 = $e.create('ins', { className: 'li-middle with-pe'}),
                    d3 = $e.create('ins', { className: 'li-bottom with-pe'});

                var borderDiv = $e.create('div',  { className: 'list-border', child: listHeader });
                borderDiv.appendChild(listElement);
                borderDiv.appendChild(listFooter);
                borderDiv.appendChild(d1);borderDiv.appendChild(d2);borderDiv.appendChild(d3);
                listWrapper = $e.create('div', { className: 'f-select-window h', child: borderDiv });
                selectedOption = find('selected', listWrapper, 'li');

                assert(!!listElement);
                wrapper.appendChild(listWrapper);

                listIface = attachListable(listElement, selectedOption, {
                    scrollNode: scrollDiv,
                    extendedKeySet: !isACInput,
                    allowLeaveList: isACInput,
                    onChange: onListChange,
                    onSelect: onListSelect,
                    pageSize: pageSize
                });

                attachSimpleDropdown(inputElement, listWrapper, {
                    onFocusIn: onFocusIn,
                    onFocusOut: onFocusOut
                });

                $e.addKeyListener(inputElement, {
                    keydown: onKeydown,
                    keyup: onKeyup
                });
            }
        }

        function updateACList(htmlData) {
            selectedOption = null;
            if (!listElement) {
                createListNode(htmlData.html);
            } else {
                listElement.innerHTML = htmlData.html;
                listHeader.innerHTML = htmlData.header || '';
                listFooter.innerHTML = htmlData.footer || '';
            }

            listIface.setSelection(selectedOption);
        }

        function buildList(data) {
            var code = [], len = data.length;

            if (!len) {
                return '<li class="list-message unselectable"><em>' + msgEmptyText + '</em></li>';
            }

            for (var i = 0; i < len; i++) {
                code.push('<li>' + $e.html(data[i]) + '</li>');
            }

            return code.join('');
        }

        var isACInput = elm.tagName != 'SELECT';
        var o = options || {};
        var tokenizerFn = o.tokenizerFn || tokenizer;
        var wrapper = findParent(elm, isACInput ? 'f-wrap' : 'f-wrap-select');
        var listHtmlCode = null;
        var selectedOption = null, listWrapper = null, scrollDiv = null,
                listHeader = null, listFooter = null,
                listElement = null, inputElement = null, hiddenInput = null;
        var pageSize = 10;
        var listVisible = false;
        var isFocused = $e.isIE ? false : (document.activeElement == elm);
        isFocused && onFocusIn();
        var throttleInterval = o.throttleInterval || 400;
        var acRequestThrottler;  // = $e.throttle(throttleInterval, sendACRequest);
        var acIsInRequest = false;
        var acListKeyword = null;
        var acCache = $e.arrayCache(30);
        var acAutoSubmit = 'autosubmit' in o ? o.autosubmit : false;
        var msgEmptyText = o.emptyText || 'Список пуст';
        var listIface = null;

        // Various bug fixes
        // var lastKeyboardEvent = 0, lastMDEvent = 0, preventBlur = false, dontCallAc = false;


        assert(!!wrapper);
        isACInput ? attachToInput(elm) : attachToSelect(elm);
        elm = null;

        addEvent(inputElement, 'focus', onInputFocus);
        if (!isACInput) {
            addEvent(inputElement, 'mousedown', onInputClick);
        }

        function onInputClick(ev) {
            !isACInput && toggleList();
        }

        function onInputFocus() {
            if (listIface) {
                return;
            }

            onFocusIn();
        }

        function onFocusIn() {
            if (isFocused) {
                return;
            }

            addClass(wrapper, 'focused');
            isFocused = true;
        }

        function onFocusOut() {
            isFocused = false;
            removeClass(wrapper, 'focused');
            hideList();
        }

        // Keyboard navigation
        function onKeydown(ev) {
            var e = ev || window.event;
            var code = e.keyCodeFixed;

            createListNode(listHtmlCode);   // Needed
            //~ dontCallAc = false;

            if (e.altKey && code == keyCodes.KEY_DOWN) {
                if (isACInput) {
                    checkACList(true);
                } else {
                    showList();
                }
                preventEvent(e);
                return;
            }

            if (e.altKey || e.ctrlKey || e.shiftKey) {
                return;
            }

            if (!listVisible && isACInput) {
                // Dont scroll through hidden list
                if (code == keyCodes.KEY_DOWN || code == keyCodes.KEY_UP ||
                    code == keyCodes.KEY_PGDN || code == keyCodes.KEY_PGUP) {

                        checkACList(true);
                        preventEvent(e);
                }
                return;
            }

            switch (code) {
                case keyCodes.KEY_SPACE:
                    if (!isACInput) {
                        toggleList();
                        preventEvent(e);
                    }
                    break;

                case keyCodes.KEY_ESCAPE:
                    hideList();
                    preventEvent(e);
                    //~ dontCallAc = true;
                    break;

                default:
                    if (listIface.navigate(code, e)) {
                        //isACInput && (code != keyCodes.KEY_ENTER) && checkACList(true);
                        preventEvent(e);
                    }
            }
        }

        function onKeyup(ev) {
            isACInput && checkACList();
        }

        function onListSelect(selected) {
            changeTo(selected);
            hideList();
            var keyword = tokenizerFn(inputElement.value);
            acListKeyword = keyword;
        }

        function onListChange(selected) {
            !isACInput && changeTo(selected);
        }


        /** For autocomplete input, call checkACList(true) instead */
        function showList() {
            if (acIsInRequest) {
                return;
            }

            if (!listVisible) {
                createListNode(isACInput ? '' : listHtmlCode); // Create list if necessary
                listVisible = true;
                //listWrapper.style.display = 'block';
                listIface.preventMouseMove();
                replaceClass(wrapper, 'collapsed', 'expanded');
                listIface.setSelection(selectedOption);

                //~ checkIfVisible();
                var pos = $e.getPosition(listWrapper);
                scrollToViewport(pos.y - inputElement.offsetHeight, pos.y + listWrapper.offsetHeight);
            }
        }

        function checkACList(showIfHidden) {
            var keyword = tokenizerFn(inputElement.value);

            if (keyword === '' && !o.autocompleteEmpty) {
                hideList();
                acListKeyword = keyword;
                return;
            }

            if (keyword === acListKeyword) {
                if (showIfHidden) {
                    showList();
                }
                return;
            }

            acListKeyword = keyword;

            var cached;
            if (null !== (cachedList = acCache.get(keyword))) {
                updateACList(cachedList);
                showList();
                return;
            }

            // Cancel request sent
            acIsInRequest && o.cancelRequestFn && o.cancelRequestFn();
            hideList();
            acListKeyword = keyword;
            acRequestThrottler || (acRequestThrottler = $e.throttle(sendACRequest, throttleInterval));
            acRequestThrottler();
        }

        function sendACRequest() {
            if (!isFocused) {
                return;
            }
            o.sendRequestFn(acListKeyword, onRequestDone);
            acIsInRequest = true;
        }

        function onRequestDone(resp) {
            acIsInRequest = false;
            if (!isFocused) {
                return;
            }

            if (resp.result === false) {
                // Error — build error message list
                //var html = buildMessageList('Ошибка при отправке запроса');
                return;
            }

            if (resp.keyword != acListKeyword) {
                return;
            }

            if (resp.result == 'list') {
                var htmlCode = buildList(resp.data);
            } else {
                var htmlCode = resp.html;
            }

            var listHtml = {
                html: htmlCode,
                header: resp.headerHtml,
                footer: resp.footerHtml
            };

            acCache.put(acListKeyword, listHtml);
            updateACList(listHtml);
            showList();
        }

        function hideList() {
            if (listVisible) {
                //listWrapper.style.display = '';
                listVisible = false;
                replaceClass(wrapper, 'expanded', 'collapsed');
            }
            if (isACInput) {
                selectedOption = null;
                //~ highlight(null);
                //~ selectedOption = null;
            }
        }

        function toggleList() {
            listVisible ? hideList() : showList();
        }

        function changeTo(node) {
            if (node == selectedOption || !node) {
                return;
            }

            selectedOption = node;

            if (isACInput) {
                var val;
                if (hasAttr(node, 'data-value')) {
                    inputElement.value = getAttr(node, 'data-value');
                } else {
                    inputElement.value = $e.getInnerText(node);
                }
            } else {
                inputElement.value = $e.getInnerText(node);
                hiddenInput.value = getAttr(node, 'data-value');
            }
        }

        function tokenizer(value) {
            value = value.toLowerCase().replace(/\s+/g, ' ');
            value = value.replace(/^\s+/, '');
            return value;
        }
    };


//~ })($e);

function scrollToViewport(top, bottom) {
    //var pos = $e.getPosition(elm);
    var view = $e.getViewportPosition();
    //var bottom = pos.y + elm.offsetHeight;
    if (top < view.y) {
       $e.scrollPageY(top - 10);
    } else if (bottom > view.y + view.height) {
        var scrollTo = bottom - view.height;
        scrollTo = Math.min(scrollTo, top);
        $e.scrollPageY(scrollTo);
    }
}


function updateCheckboxState(elm) {
    var key = '$e_wrapper', wrapper;
    if (!elm[key]) {
        wrapper = elm[key] = findParent(elm, 'f-wrap-checkbox');
    } else {
        wrapper = elm[key];
    }
    elm.checked ? addClass(wrapper, 'checked') : removeClass(wrapper, 'checked');
}

function updateRadioState(elm) {
    var key = '$e_wrapper', wrapper;
    if (!elm[key]) {
        wrapper = elm[key] = findParent(elm, 'f-wrap-radio');
    } else {
        wrapper = elm[key];
    }
    elm.checked ? addClass(wrapper, 'checked') : removeClass(wrapper, 'checked');
}


/** TODO: temporary */
function attachFileInput(inp) {
    var wrapper = findParent(inp, 'f-wrap-file');
    var replaceHtml = '<span class="f-wrap-combo f-width-100"><span class="f-wrap"><span class="back with-pe"><span class="f-js-filename"></span></span><span class="bottom with-pe"></span></span>' +
        '<span class="f-wrap-button"><span class="back with-pe"><span class="pseudo-button" type="button" value="Выбрать файл">Выбрать файл</span></span><span class="bottom with-pe"></span><span class="file-selector"></span>' +
        '</span>';
    var maxLen = 33;
    var emptyText = inp.getAttribute('placeholder') || 'Файл не выбран';
    assert(!!wrapper);
    var input = inp; // .cloneNode();
    inp = null;
    wrapper.innerHTML = replaceHtml;
    var inputHolder = find('file-selector', wrapper, 'span');
    var filenameHolder = find('f-js-filename', wrapper, 'span');
    // var selectBtn = find('js-select-file-btn', wrapper, 'input');
    inputHolder.appendChild(input);
    addEvent(input, 'change', updateFilename);
    addEvent(input, 'focus', function() { addClass(wrapper, 'focused'); })
    addEvent(input, 'blur', function() { removeClass(wrapper, 'focused'); })
    // addEvent(selectBtn, 'click', function() { input.click(); l('pass'); });
    updateFilename();
    addClass(wrapper, 'f-wrap-js-file');




    function updateFilename(ev) {
        var name = input.value.replace(/^(?:.*[\/\\])?([^\/\\]+)$/, '$1');
        if (name == '') {
            $e.setInnerText(filenameHolder, emptyText);
            filenameHolder.title = '';
            removeClass(wrapper, 'selected');
        } else {
            $e.setInnerText(filenameHolder, truncate(name, maxLen));
            filenameHolder.title = name.length > maxLen ? name : '';
            addClass(wrapper, 'selected');
        }
    }

    function truncate(text, max) {
        assert(max >= 10);
        if (text.length > max) {
            var endLength = 11;
            text = text.substr(0, max - endLength) + '…' + text.substr(text.length - endLength, endLength);
        }
        return text;
    }
}

/*
function attachRadioButton(elm) {
    var wrapper = findParent(elm, 'f-wrap-radio');
    var me = attachRadioButton;
    function updateState() {
        if (elm.form && elm.name) {
            var siblings = elm.form.elements[elm.name];
            for (var i = 0, l = siblings.length; i < l; i++) {
                var obj = me.findNeighbour(siblings[i]);
                obj && obj.refresh();
            }
        } else {
            updateSelfState();
        }
    }

    function updateSelfState() {
        elm.checked ? addClass(wrapper, 'checked') : removeClass(wrapper, 'checked');
    }

    if (wrapper) {
        addClass(wrapper, 'f-wrap-js-radio');
        elm.checked && addClass(wrapper, 'checked');
        addEvent(elm, 'click', updateState);
        me.addNeighbour(elm, self);
    }

    var self = { refresh: updateSelfState }
    return self;
}
*/
/*
attachRadioButton.addNeighbour = function (elm, object) {
    attachRadioButton.neighbours || (attachRadioButton.neighbours = {});
    elm.$eId || (elm.$eId = $e.uniqueId());
    attachRadioButton.neighbours[elm.$eId] = object;
}

attachRadioButton.findNeighbour = function(elm) {
    return elm.$eId ? attachRadioButton.neighbours[elm.$eId] : undefined;
}
*/

$e.form.attachPlaceholders = function() {
    // Check for native placeholders?
    var hasNative = 'placeholder' in document.createElement('input');
    if (hasNative) {
        // l("Use native placeholders");
        addClass(document.body, 'with-native-placeholders');
        return;
    }

    // timer('Placeholders start');
    // Fake placeholders
    $e.addFocusListener(document.body, 'focusin', function(ev) { var e = ev || window.event; showOrHide(e.target || e.srcElement, false); });
    $e.addFocusListener(document.body, 'focusout', function(ev) { var e = ev || window.event; showOrHide(e.target || e.srcElement, true); });

    try { var focused = document.activeElement; } catch(e) { /* l("IE: weird error"); */ }
    if (focused) { showOrHide(focused, false); }
    addClass(document.body, 'with-placeholders');
    var phKey = '$e_placeholder';

    var labs = document.querySelectorAll ?
                document.querySelectorAll('label.f-placeholder') :
                document.getElementsByTagName('label');

    for (var i = labs.length - 1; i >= 0; i--) {
        var label = labs[i];
        if (hasClass(label, 'f-placeholder')) {
            var f =  label.htmlFor, elm = f ? document.getElementById(f) : null, ph = elm.getAttribute('placeholder');
            if (elm && ph) {
                elm[phKey] = label;
                $e.setInnerText(label, ph);
                label.style.display = (elm.value === '') ? 'block' : '';
            }
        }
    }

    if (document.querySelectorAll) {
        var elements = [document.querySelectorAll('input[placeholder],textarea[placeholder]')];
    } else {
        var elements = [document.getElementsByTagName('input'), document.getElementsByTagName('textarea')];
    }

    for (var i = 0, len = elements.length; i < len; i++) {
        for (var j = 0, len2 = elements[i].length; j < len2; j++) {
            var el = elements[i][j], text = el.getAttribute('placeholder');
            if (!text || el[phKey]) { continue; }
            if (!el.id) { el.id = '$e_input_' + $e.uniqueId(); };
            var label = $e.create('label', { className: 'f-placeholder', htmlFor: el.id });
            $e.setInnerText(label, text);
            el.parentNode.insertBefore(label, el);
            el[phKey] = label;
            label.style.display = (el.value === '') ? 'block' : '';
        }
    }

    // timer.dump('Placeholders end');


    function showOrHide(elm, bShow) {
        if (elm.nodeName != 'INPUT' && elm.nodeName != 'TEXTAREA') { return; }

        if (!(phKey in elm)) {
            return;
            //findLabel(elm);
        }
        elm[phKey] && (elm[phKey].style.display = (bShow && elm.value === '') ? 'block' : '');
    }
}


/*
    TODO: track created fake selects ans refresh their internal state on form reset
    ?? copy disabled property
*/

function start() {
    function createFormElements() {
        var inputs = document.getElementsByTagName('input');
        var selects = document.getElementsByTagName('select');
        var forms = {}; // List of forms with controls (to handle reset event)

        // timer('Start');
        for (var i = selects.length - 1; i >= 0; i--) {
            // Check for class
            if (!hasClass(selects[i], 'js-select')) {
                continue;
            }

            $e.form.attachCombobox(selects[i]);
        }
        // timer.dump('Replaced selects');
        for (var i = inputs.length - 1; i >= 0; i--) {
            // Check for class
            var inp = inputs[i];
            if (inp.type == 'text' && hasClass(inp, 'js-autocomplete')) {
                $e.form.attachCombobox(inp, {
                    sendRequestFn: function(key, callback) {
                        var list = [];
                        for (var i = 0; i < 7; i++) {
                            list.push(key + rand() + rand() + rand());
                        }
                        setTimeout(function() {
                            callback({
                                result: 'list',
                                data: list,
                                keyword: key,
                                footerHtml: '<small>Total: 8 found</small>'
                            });
                        }, 500);
                    }
                });
            } else if (inp.type == 'checkbox' && hasClass(inp, 'js-checkbox')) {
                updateCheckboxState(inp);
                addEvent(inp, 'click', onCheckboxClick);
                if (inp.form) { forms[$e.id(inp.form)] = inp.form; }

            } else if (inp.type == 'radio' && hasClass(inp, 'js-radio')) {
                updateRadioState(inp);
                addEvent(inp, 'click', onRadioClick);
                if (inp.form) { forms[$e.id(inp.form)] = inp.form; }

                //attachRadioButton(inp);
            } else if (inp.type == 'file' && hasClass(inp, 'js-file')) {
                attachFileInput(inp);
            }
        }

        for (var i in forms) {
            var form = forms[i];
            addEvent(form, 'reset', onFormReset);
        }

        // timer.dump('Replaced inputs');
        addClass(document.body, 'with-js-controls');


        function onFormReset(ev) {
            var e = ev || window.event;
            var form = e.target || e.srcElement;

            // Need time to change controls state
            setTimeout(function() {
                // Update checkboxes & radios states
                var inputs = form.getElementsByTagName('input');
                for (var i = 0, l = inputs.length; i < l; i++) {
                    var inp = inputs[i];
                    if (inp.type == 'checkbox' && hasClass(inp, 'js-checkbox')) {
                        updateCheckboxState(inp);
                    } else if (inp.type == 'radio' && hasClass(inp, 'js-radio')) {
                        updateRadioState(inp);
                    } else if (inp.type == 'file' && hasClass(inp, 'js-file')) {
                        // Trigger change event
                        if (document.createEvent) {
                            var newEv = document.createEvent("HTMLEvents");
                            newEv.initEvent('change', false, false);
                            inp.dispatchEvent(newEv);
                        } else {
                            inp.fireEvent('onchange');
                        }
                    }
                }
            }, 50);
        }

        function getId(elm) {
            var k = '$e_id';
            if (!elm[k]) {
                elm[k] = $e.uniqueId;
            }
            return elm[k];
        }


        function onCheckboxClick(ev) {
            var e = ev || window.event;
            var t = e.target || e.srcElement;
            assert(t.nodeName == 'INPUT');
            updateCheckboxState(t);
        }

        function onRadioClick(ev) {
            var e = ev || window.event;
            var elm = e.target || e.srcElement;
            assert(elm.nodeName == 'INPUT');
            if (elm.form && elm.name) {
                var siblings = elm.form.elements[elm.name];
                for (var i = 0, l = siblings.length; i < l; i++) {
                    var obj = siblings[i];
                    // var obj = findRadioNeighbour(siblings[i]);
                    obj && updateRadioState(obj);
                }
            } else {
                updateRadioState(elm);
            }
        }

        /* function saveRadioNeightbours(elm) {
            attachRadioButton.neighbours || (attachRadioButton.neighbours = {});
            elm.$eId || (elm.$eId = $e.uniqueId());
            attachRadioButton.neighbours[elm.$eId] = object;
        } */

    }

    function rand() {
        var elms = ['na', 'ni', 'no', 'nu', 'ta', 'to', 'te', 'ki', 'ka', 'mi', 'mu',
            'ryu', 'ro', 're', 'ri', 'ra', 'a', 'o', 'i', 'se', 'si', 'shi', 'sho', 'sha', 'hi',
            'n'
        ];

        var r = Math.round(Math.random() * elms.length);
        return elms[r];
    }

    createFormElements();
    $e.form.attachPlaceholders();
    testDropdowns();
}

function testDropdowns() {
    var drops = findAll('f-with-dropdown', document, 'div');
    for (var i = 0; i < drops.length; i++) {
        var drop = drops[i];
        if (hasClass(drop, 'f-with-simple-dropdown')) {
            var list = find('f-dropdown', drop, 'div');
            var inp = findTag('input', drop);
            attachSimpleDropdown(inp, list, {
                onFocusIn: function(ev) { var t = ev.target || ev.srcElement, p = findParent(t, 'f-wrap'); addClass(p, 'expanded'); },
                onFocusOut: function(ev) { var t = ev.target || ev.srcElement, p = findParent(t, 'f-wrap'); removeClass(p, 'expanded'); }
            });
        } else {
            addDD(drop);
        }
    }

    function addDD(drop) {
        var listId = drop.getAttribute('data-dropdown-id');
        var list = findId(listId);
        var inp = findTag('input', drop);
        var created=  false;
        attachDropdown(inp, list, {
            onFocusIn: function(ev) { showDD(); },
            onFocusOut: function(ev) { list.style.display = ''; }
        });

        function showDD() {
            if (!created) {
                created = true;
                document.body.appendChild(list);
            }

            var pos = $e.getPosition(inp);
            list.style.position = 'absolute';
            list.style.display = 'block';
            list.style.top = pos.y + inp.offsetHeight + 'px';
            list.style.left = pos.x + 'px';
            list.style.minWidth = inp.offsetWidth + 'px';

        }
    }
}




/**
*   Options:
*
*   onFocusIn
*   onFocusOut
*   onFocusReturn
*
*/
function attachDropdown(focusNode, dropdownNode, options) {
    var o = options || {};
    var lastMD = 0;
    var lastFocus = 0; // IE brings events in wrong order
    var tmpNode = focusNode.ownerDocument.activeElement;
    var innerFocus = (tmpNode == focusNode || $e.isInside(tmpNode, dropdownNode)) ? tmpNode : null;
    var willReturnFocus = false;
    var focusLostTo = null;


    // Record dropdown click to prevent focus losing
    addEvent(dropdownNode, 'mousedown', function() { lastMD = (new Date()).getTime(); });
    addEvent(focusNode, 'focus', function(ev) {

        // Clear focus lost timeout
        clearTimeout(focusLostTo);
        focusLostTo = null;

        lastFocus = (new Date()).getTime();

        var wasFocused = !!innerFocus;
        innerFocus = focusNode;

        if (!wasFocused && o.onFocusIn) {
            o.onFocusIn(ev || window.event);
        }
    });


    $e.addFocusListener(dropdownNode, 'focusin', function(ev) {
        var e = (ev || window.event), t = e.target || e.srcElement;
        if (t) {

            // Clear focus lost timeout
            clearTimeout(focusLostTo);
            focusLostTo = null;

            lastFocus = (new Date()).getTime();

            var wasFocused = !!innerFocus;
            innerFocus = t;

            if (!wasFocused) {
                o.onFocusIn && o.onFocusIn(ev || window.event);
            }
        }
    });



    addEvent(focusNode, 'blur', function(ev) {
        onBlur(ev || window.event);
    });

    $e.addFocusListener(dropdownNode, 'focusout', function(ev) {
        var e = ev || window.event;

        // Let's wait what happens
        onBlur(e);
    });

    function onBlur(ev) {

        if (!innerFocus) {
            return;
        }

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

        if (t - lastMD < 50 || t - lastFocus < 50) {
            // gain focus back, if user clicked at dropdown area
            setTimeout(function() {
                if (innerFocus) {
                    innerFocus.focus();
                }
            }, 50);

            o.onFocusReturn && o.onFocusReturn(innerFocus);
        } else {

            clearTimeout(focusLostTo);
            focusLostTo = setTimeout(function() {
                var wasFocused = !!innerFocus;
                innerFocus = null;

                if (wasFocused) {
                    o.onFocusOut && o.onFocusOut(ev);
                }
            }, 50);
        }
    }
}

/**
* Dropdown list w/o inner focusable elements
*
* Options:
*   onFocusIn, onFocusOut, onFocusNotLost
*/
function attachSimpleDropdown(focusNode, dropdownNode, options) {
    var o = options || {};
    var lastMD = 0;
    var focusReturnTime = 0;
    var focused = (focusNode.ownerDocument.activeElement == focusNode);

    // Record dropdown click to prevent focus losing
    addEvent(dropdownNode, 'mousedown', function() { lastMD = (new Date()).getTime(); });

    addEvent(focusNode, 'focus', function(ev) {
        if ((new Date()).getTime() - focusReturnTime < 150) {
            return; // We returned focus, do nothing
        }

        var wasFocused = focused;
        focused = true;
        if (!wasFocused && o.onFocusIn) {
            o.onFocusIn(ev || window.event);
        }
    });

    addEvent(focusNode, 'blur', function(ev) {
        if (!focused) {
            return;
        }

        var t = (new Date()).getTime();
        if (t - lastMD < 50) {
            // gain focus back
            focusReturnTime = t;
            setTimeout(function(){
                if (focused) {
                    focusNode.focus();
                }
            }, 0);

            o.onFocusNotLost && o.onFocusNotLost();
        } else {
            focused = false;
            o.onFocusOut && o.onFocusOut(ev || window.event);
        }
    });
}

function initListable(node, onSelect, options) {
    addEvent(node, 'click', showList);
    //addEvent(node, 'focus', showList);
    //addEvent(node, 'blur', hideList);
    var selected = null;

    $e.addKeyListener(node, {
        keydown: function(ev) {
            var code = ev.keyCodeFixed;
            if (iface) {
                if (iface.navigate(code, ev)) {
                    preventEvent(ev);
                } else {
                    // l('navigate failed');
                }
            }
        }
    });

    var initd = false, iface = null, listDiv = null;

    function showList() {
        if (!initd) {
            initd = true;
            var wrap = findParent(node, 'f-wrap-button');
            var html = '';
            for (var op in options) {
                html += '<li data-value="' + op +'"' + (Math.random() >= 0 ? ' class="unselectable"' : '') +'>' + options[op] + '</li>';
            }


            var ul = $e.create('ul', { className: 'list vlist', innerHTML: html });
            listDiv = $e.create('div', { className: 'list-window', child: ul });
            //addClass(ul.firstChild, 'selected');
            wrap.appendChild(listDiv);
            selected = null; //ul.firstChild;

            iface = attachListable(ul, selected, { scrollNode: ul, onChange: onChange, onSelect: hideList, allowLeaveList: true, extendedKeySet: true });
            attachSimpleDropdown(node, listDiv, {
                onFocusOut: hideList
            });
        } else {
            listDiv.style.display = '';
        }

        iface.setSelection(selected);
        node.focus();
    }

    function hideList() {
        listDiv.style.display = 'none';
    }

    function onChange(newNode) {
        selected = newNode;
        onSelect(newNode ? newNode.getAttribute('data-value') : '(none)');
        if (newNode) {
            node.value = newNode.innerHTML;
        }
    }
}

/**
* options:
*
* isHorizontal
* scrollNode
* onChange(selected)
* onChange
* onSelect
* onScroll()
* extendedKeySet
* allowLeaveList
*/
function attachListable(listNode, selected, options) {
    var o = options || {}, lastKeyboardEvent = 0;
    var pageSize = o.pageSize || 10;
    var eks = o.extendedKeySet;

    addEvent(listNode, 'mousemove', function(ev) {
        var e = ev || window.event, t = e.target || e.srcElement, item = t;

        // Prevent fake mousemove on scrolling in Chrome
        var time = (new Date()).getTime();
        if (time - lastKeyboardEvent < 150 /* Chrome waits for 100ms */ ) {
            return;
        }

        while (item != document.body && item != listNode && item.parentNode != listNode) {
            item = item.parentNode;
        }

        if (item.parentNode != listNode || item == selected) { return; }
        if (hasClass(item, 'unselectable')) { return; }

        highlight(item);
        // o.onChange && o.onChange(selected);
    });


    addEvent(listNode, 'mousedown', function(ev){
        var e = ev || window.event, item = e.target || e.srcElement;
        if ((e.which && e.which > 1) || e.button && e.button > 1) {
            // Not a left btn
            return;
        }

        while (item != document.body && item != listNode && item.parentNode != listNode) {
            item = item.parentNode;
        }

        if (item.parentNode != listNode) {
            return;
        }

        if (hasClass(item, 'unselectable')) { return; }
        iface.setSelection(item);
        o.onChange && o.onChange(selected);
        o.onSelect && o.onSelect(selected);
    });


    function keyboardNavigate(item, steps) {
        var stepsLeft = Math.abs(steps), stepsDone = 0;

        if (!item && stepsLeft > 0) {
            if (steps > 0) {
                // Find first list item
                for (var item = listNode.firstChild; item; item = item.nextSibling) {
                    if (item.nodeType == 1 /* Element */ && !hasClass(item, 'unselectable')) {
                        break;
                    }
                }
            } else {
               // Find last list item
                for (var item = listNode.lastChild; item; item = item.previousSibling) {
                    if (item.nodeType == 1 /* Element */ && !hasClass(item, 'unselectable')) {
                        break;
                    }
                }
            }

            stepsLeft --;
            stepsDone ++;
        }

        if (!item) {
            // Nothing changed, fail
            return;
        }

        var current = item, lastGood = item;

        // Don't run on pgUp/pgDn
        if (stepsDone == 0) {

            // If we have a starting point
            for (; current && stepsLeft > 0;)
            {
                current = (steps > 0) ? current.nextSibling : current.previousSibling;
                if (!current) {
                    break;
                }

                if (current.nodeType != 1 || hasClass(current, 'unselectable')) {
                    continue;
                }

                lastGood = current;
                stepsLeft --;
                stepsDone ++;

            }
        }

        // If we failed to find good one
        if (!current) {
            // If we cannot leave list, stop there
            // Also, to leave list using PgUp/Dn you should have edge item selected
            if (!o.allowLeaveList || stepsDone > 1) {
                current = lastGood;
            }
        }

        // If selection changed, update interface
        if (current != selected) {
            iface.setSelection(current);
            o.onChange && o.onChange(selected);
        }
    }

    function highlight(item) {
        if (selected != item) {
            if (selected) { removeClass(selected, 'selected'); }
            selected = item;
            if (selected) { addClass(selected, 'selected'); }
        };
    }

    var iface = {

        setSelection: function(item /* may be null */) {
            highlight(item);

            // Check if visible
            if (o.scrollNode && selected) {
                o.isHorizontal ?
                    $e.makeVisibleHor(selected, o.scrollNode) : $e.makeVisible(selected, o.scrollNode);
            }
        },

        getSelection: function() { return selected; },
        preventMouseMove: function() { lastKeyboardEvent = (new Date()).getTime(); },

        navigate: function(keyCode, ev) {
            lastKeyboardEvent = (new Date()).getTime();
            var e = ev || {};

            switch (keyCode) {
                case keyCodes.KEY_LEFT:
                    if (!eks) { break; }
                    // fallthrough
                case keyCodes.KEY_UP:
                    keyboardNavigate(selected, -1);
                    return true;

                case keyCodes.KEY_PGUP:
                    keyboardNavigate(selected, -pageSize);
                    return true;

                case keyCodes.KEY_HOME:
                    if (!eks) { break; }
                    keyboardNavigate(null, 1);
                    return true;

                case keyCodes.KEY_RIGHT:
                    if (!eks) { break; }
                    // fallthrough
                case keyCodes.KEY_DOWN:
                    keyboardNavigate(selected, 1);
                    return true;

                case keyCodes.KEY_PGDN:
                    keyboardNavigate(selected, pageSize);
                    return true;

                case keyCodes.KEY_END:
                    if (!eks) { break; }
                    keyboardNavigate(null, -1);
                    return true;

                case keyCodes.KEY_ENTER:
                    if (o.onSelect && selected) {
                        o.onSelect(selected);
                        return true;
                    }

                    break;
            }

            return false;
        }
    };

    return iface;
}
var isFirstMenuClick = true;
var initialActiveItem = null;

function transformMenu(li, ev) {

    if (hasClass(li, 'active')) {
        // Ignore
        return true;
    }

    // Check for submenu click
    var t = ev.target || ev.srcElement;
    var tParent = parentTag(t, 'ul');
    if (hasClass(tParent, 'menu-2-front')) {
        return true;
    }

    // Check child count
    var list = find('menu-2-hidden', li, 'ul');
    if (!list) { return true; }
    var children = list.getElementsByTagName('li');
    if (children.length < 2) {
        return true;
    }

    // Remove current active element
    var ul = parentTag(li, 'ul');
    var active = find('active', ul, 'li');
    active && removeClass(active, 'active');

    if (isFirstMenuClick) {
        isFirstMenuClick = false;
        initialActiveItem = active;
    }

    addClass(li, 'active');

    // Expand
    var expandBtn = find('dropdown-button', li, 'ins');
    if (expandBtn.focus) { expandBtn.focus(); };
    expandTopMenu(expandBtn);

    return false;
};

// Don;t try to understand
function restoreMenuState(li) {
    if (!isFirstMenuClick) {
        var ul = parentTag(li, 'ul');
        var active = ul && find('active', ul, 'li');
        active && removeClass(active, 'active');
        initialActiveItem && addClass(initialActiveItem, 'active');
    };
};


function expandTopMenu(obj) {
    var li = parentTag(obj, 'li');
    if (!li || hasClass(li, 'expanded')) {
        return;
    }

    if (!li.listableInstance) {
        var ul = find('menu-2-hidden', li, 'ul');
        var selected = find('selected', ul, 'li');
        var listable = li.listableInstance = attachListable(ul, selected, { onSelect: function(selected) {
            var a = findTag('a', selected);
            if (a) { location.href = a.href; }
        }});
        $e.addKeyListener(li, {
            keydown: function(ev) {
                if (listable.navigate(ev.keyCodeFixed, ev)) { preventEvent(ev); };
                if (ev.keyCodeFixed == keyCodes.KEY_ESCAPE) { obj.blur(); };
            }
        });
        attachSimpleDropdown(obj, li, {
            onFocusOut: function() { removeClass(li, 'expanded'); if (!isFirstMenuClick) { restoreMenuState(li); }; }
        });

    } else {
        li.listableInstance.setSelection(null);
    }

    addClass(li, 'expanded');
}


function expandSearchButton(obj, ev) {

    if (ev.which && ev.which > 1 || ev.button && ev.button > 1) {
        return;
    }

    var wrap = findParent(obj, 'f-wrap-button');
    if (!wrap || hasClass(wrap, 'expanded')) {
        return;
    }

    if (!wrap.listableInstance) {
        var list = find('values-list', wrap, 'ul');
        var hidden = find('hidden-input', wrap, 'input');
        var text = find('value', wrap, 'span');
        var selected = find('selected', list, 'li');

        var listable = wrap.listableInstance = attachListable(list, selected, { onChange: function(selected) {
                var val = selected.getAttribute('data-value');
                hidden.value = val;
                text.innerHTML = selected.innerHTML;
            },
            onSelect: function() { closeList(); }
        });

        $e.addKeyListener(wrap, {
            keydown: function(ev) {
                if (listable.navigate(ev.keyCodeFixed, ev)) { preventEvent(ev); };
                if (ev.keyCodeFixed == keyCodes.KEY_ESCAPE) { closeList(); };
            }
        });

        attachSimpleDropdown(obj, wrap, {
            onFocusOut: function() { closeList(); }
        });
    }

    obj.focus();
    addClass(wrap, 'expanded');

    function closeList() {
        removeClass(wrap, 'expanded');
    };
}

function sendFeedbackForm(input) {
    var form = input.form, el = form.elements;
    setFormError('');

    var text = el.text.value;

    // Validate
    if (!text.length) {
        setFormError("Пожалуйста, введите текст вашего сообщения.");
        el.text.focus();
        return;
    }

    if (text.length < 16) {
        setFormError("Пожалуйста, напишите чуть больше текста.");
        el.text.focus();
        return;
    }

    if (el.author.value.length < 3) {
        setFormError("Пожалуйста, укажите ваше имя или адрес e-mail.");
        el.author.focus();
        return;
    }

    // Send feedback
    form.action = '/save-feedback.php';
    el.sig.value = 'valid';
    el.url.value = location.href;
    el.sendBtn.disabled = true;
    form.submit();

    function setFormError(txt) {
        var errorDiv = find('js-error-area', form, 'div');
        if (!errorDiv) {
            errorDiv = $e.create('div', {className: 'js-error-area error-area'});
            var wrapDiv = $e.create('div', { className: 'f-row', child: errorDiv });
            form.insertBefore(wrapDiv, form.firstChild);
        }

        errorDiv.innerHTML = txt;
        if (!txt) {
            errorDiv.style.display = 'none';
        } else {
            errorDiv.style.display = 'block';
        }
    }
}
function attachSlideshow(root, options) {
	var
        scene = find('scene', root, 'div'),
        thumbArea = find('thumbs', root, 'div'),
        thumbScroller = find('scroller', thumbArea, 'div'),
        thumbList = findTag('ul', thumbArea),
        selectedLi = null,
        selectedA = null,
        firstLi = findTag('li', thumbList),
        firstA = findTag('a', selectedLi),
        backDiv = null,
        midImage = null,
        frontImage = find('front-image', scene, 'img'),
		backText = null,
        backAnim,
        midAnim,
        frontAnim,
        heightAnim,
        haSpeed = 250,
        minHeight = 450,
        stageHeight = scene.scrollHeight,
		imageObjects = {},
		imagesLoaded = {},
        imageHeight = {},   // For IE
        imageWidth = {},
		loadingSrc = null,
		loadingTitle = null,
        btnLeft = null, btnRight = null, btnVisible = false,
		backstageError = '<span class="back-error">Ошибка при загрузке картинки. ' +
			'<br>Вы можете попробовать обновить страницу.</span>',
		backstageLoading = '<span class="back-loading">Загрузка&hellip;</span>';


    // attachScroller()
    highlight(firstLi, firstA);
    root.tabIndex = 0;
    scene.style.height = Math.max(minHeight, stageHeight) + 'px';
    scene.style.overflow = 'hidden';
    $e.addKeyListener(root, { keydown: onKeyDown } );

    addEvent(thumbArea, 'click', function (ev) {
		var e = ev || window.event, t = e.target || e.srcElement, node = parentTag(t, 'li');
        if (!node) { return; }

		loadImage(node);
        preventEvent(e);
	});

    addEvent(scene, 'click', function(ev) { var e = ev || window.event;
        if (e.which && e.which > 1 || e.button && e.button > 1) {
            return;
        }
        //thumbArea.focus && thumbArea.focus();
        preventEvent(ev); $e.clearSelection(); navigate(selectedLi.nextSibling, 1);
    });

    // Chrome is too slow sometimes to calculate real width
    setTimeout(checkButtonsVisibility, 50);

    function onKeyDown(ev) {
        var code = ev.keyCodeFixed;
        if (ev.altKey || ev.shiftKey || ev.ctrlKey || ev.metaKey) {
            return;
        }

        switch (code) {
            case keyCodes.KEY_LEFT:
                navigate(selectedLi.previousSibling, -1);
                preventEvent(ev);
                break;

            case keyCodes.KEY_RIGHT:
                navigate(selectedLi.nextSibling, 1);
                preventEvent(ev);
                break;

            case keyCodes.KEY_HOME:
                navigate(thumbList.firstChild, 1);
                preventEvent(ev);
                break;

            case keyCodes.KEY_END:
                navigate(thumbList.lastChild, -1);
                preventEvent(ev);
                break;
        }
    }

    function navigate(node, dir) {
        for (; node && node.nodeType != 1; node = dir > 0 ? node.nextSibling : node.previousSibling) { }
        node && loadImage(node);
    }

    function loadImage(node) {
        if (!node || node == selectedLi) { return; }

        var a = findTag('a', node);
        if (!a || !a.href) { return; }

        var src = a.href;
        highlight(node, a);

        if (src == loadingSrc) { return; }

        loadingSrc = src;
        loadingTitle = a.title;

        if (!backDiv) { initScene(loadingSrc, loadingTitle); }
        checkButtonsVisibility();

        frontAnim.goTo(0);

        if (imagesLoaded[src]) {
			backAnim.goTo(0);
            setImage(midImage, loadingSrc, loadingTitle);
			midAnim.goTo(1);
            if (imageHeight[src]) {
                heightAnim.goTo(Math.max(minHeight, imageHeight[src]), haSpeed);
            }
		} else {
			midAnim.goTo(0);
			backText.innerHTML = backstageLoading;
			backAnim.goTo(1);
		}

		if (!imagesLoaded[src] && !imageObjects[src]) {
			startLoading(src);
		}
    }

    function highlight(node, a) {
        if (node != selectedLi) {
            if (selectedLi) { removeClass(selectedLi, 'selected'); }
            selectedLi = node;
            if (selectedLi) {
                addClass(selectedLi, 'selected');
                $e.makeVisibleHor(selectedLi, thumbScroller, 80);
            }

            selectedA = a;
        }
    }

    function startLoading(src) {
		assert(!imagesLoaded[src]);
		assert(!imageObjects[src]);

		var i = new Image();
		i.onload = wrap(function() { onImageLoad(src) }, 'image_onload');
		i.onerror = wrap(function() { onImageError(src) }, 'image_onerror');

        imageObjects[src] = i;
		i.src = src;
	}


    function initScene(midSrc, midTitle) {
        midImage = $e.create('img', { src: midSrc, title: midTitle, className: 'mid-image', style: 'visibility: hidden;' });
        var midDiv = $e.create('div', { className: 'mid-stage', child: midImage });
        scene.insertBefore(midDiv, scene.firstChild);

        backText = $e.create('div', { className: 'back-text' });
        backDiv = $e.create('div', { className: 'backstage', child: backText });
        scene.insertBefore(backDiv, scene.firstChild);

        backAnim = $e.animate(function (val) {
            setOpacity(backDiv, val);
        }, { start: 0, fullDuration: $e.isIE67 ? 50 : 300 });

        midAnim = $e.animate(function (val) {
            setOpacity(midImage, val);
            if (val == 1) {
                onMidShown();
            }
        }, { start: 0, fullDuration: $e.isIE67 ? 900 : 900 });

        frontAnim = $e.animate(function (val) {
            setOpacity(frontImage, val);
        }, { start: 1, fullDuration: $e.isIE67 ? 850 : 900 });

        heightAnim = $e.animate(function(val) {
            scene.style.height = Math.round(val) + 'px';
        }, { start: stageHeight, fullDuration: 2000, from: 0, to: 99999 });
    }

	function onMidShown() {
		// Switch with front
		frontAnim.jump(0);
		setImage(frontImage, loadingSrc, loadingTitle);
		frontAnim.jump(1);
		midAnim.jump(0);
	}

    function onImageLoad(src) {

		imagesLoaded[src] = true;
        imageHeight[src] = imageObjects[src].height;
        imageWidth[src] = imageObjects[src].width;
		delete imageObjects[src];

		if (src == loadingSrc) {
			// midAnim.jump(0);
			setImage(midImage, loadingSrc, loadingTitle);

            heightAnim.goTo(Math.max(minHeight, imageHeight[src]), haSpeed);

			backAnim.goTo(0);
			midAnim.goTo(1);
		}
	}

	function onImageError(src) {

		delete imageObjects[src];

		// If we were waiting for this one
		if (src == loadingSrc) {
			backText.innerHTML = backstageError;
			backAnim.goTo(1);
		}
	}

    function setImage(img, src, alt) {
        $e.isIE67 && (img.style.display = 'none');

        if ($e.isIE67 && imageHeight[src] && imageWidth[src]) {
            img.height = imageHeight[src];
            img.width = imageWidth[src];
        }

        img.src = src;
        img.title = alt;
        img.alt = alt;

        $e.isIE67 && (img.style.display = '');
    }

    function setOpacity(obj, value) {
		assert(value >= 0 && value <= 1);
        var s = obj.style;
        if (value == 0) {
            s.visibility = 'hidden';
            return;
        } else if (s.visibility != 'visible') {
            s.visibility = 'visible';
        }

        if (obj.filters && obj.filters.length > 0) {
            obj.filters.item("alpha").opacity = Math.floor(value * 100);
        } else if ('opacity' in s) {
            s.opacity = value;
        }
	}


    function checkButtonsVisibility() {
        if (thumbScroller.scrollWidth > thumbScroller.clientWidth) {
            if (!btnVisible) {
                if (!btnLeft) { createButtons(); }
                btnLeft.style.display = '';
                btnRight.style.display = '';
                btnVisible = true;
            }
        } else {
            if (btnVisible) {
                btnLeft.style.display = 'none';
                btnRight.style.display = 'none';
                btnVisible = false;
            }
        }
    }

    function createButtons() {
        btnLeft = $e.create('button', { /* type: 'button', */ className: 'btn-left', innerHTML: '&laquo;',
            onclick: function() { navigate(selectedLi.previousSibling, -1); $e.clearSelection(); return false; },
            ondblclick: fixIEClick
            });
        btnRight = $e.create('button', { /* type: 'button', */ className: 'btn-right', innerHTML: '&raquo;',
            onclick: function() { navigate(selectedLi.nextSibling, 1);$e.clearSelection(); return false; /* prevent selection */ },
            ondblclick: fixIEClick
            });
        thumbScroller.appendChild(btnLeft);
        thumbScroller.appendChild(btnRight);
    }

    // Prevent selection on double click
    function fixIEClick(ev) {
        if ($e.isIE67) {
            this.onclick(ev);
        }
    }
}

/**
* Finds all areas of the page containing lightboxed images, and creates a slideshow widget from them
*
* Image patterns:
*   <img src="{thumbnailSource}" alt="" title="" data-ami-mbgrp="{title}" style="{width,height}" width="{w}" height="{h}" border="0" align="" data-ami-mbpopup="{fullImageSrc}" data-ami-mbdescr="{descr}">
*
*   <a href="javascript:show_picture('show_pic.php','{fullSizeSrc}','',{fullSizeW},{fullSizeH});"><img style="WIDTH: 134px; HEIGHT: 100px" title="Смог в Сочи " border="0" alt="" src="{thumbnailSrc}" width="{w}" height="{h}"></a>
*
*/
function createSlideshows() {
    // timer('Start create slideshow');
    var areas = findAll('b-with-slideshow', document, 'div');
    for (var i = 0; i < areas.length; i++) {

        // Find all images and create a slideshow
        createSlideshowInArea(areas[i]);
    }
    // timer.dump('End create slideshow');


    fixAlternateRows();
    fixMenuItems();
}

function createSlideshowInArea(area) {

    var imgs = area.getElementsByTagName('img'),
        data = [],
        jsRe = /^\s*javascript:\s*show_picture\s*\(\s*["']show_pic\.php["']\s*,\s*['"]([^'"]+)['"]/;

    var validImgPath = /\.(jpg|jpeg|png|gif)(#|[?]|$)/;
    var noSlideshowClassRe = /(^|\s)no-slideshow(\s|$)/;

    for (var j = 0; j < imgs.length; j++) {
        var img = imgs[j], fullSrc, row;
        fullSrc = img.getAttribute('data-ami-mbpopup');

        // Skip those img who have class = no-slideshow
        if (noSlideshowClassRe.test(img.className)) {
            continue;
        }

        if (fullSrc && validImgPath.test(fullSrc)) {
            data.push({
                node: img,
                fullSrcHtml: fullSrc,
                thumbSrcHtml: img.src,
                titleHtml: $e.html(img.getAttribute('data-ami-mbdescr') || img.title || '')
            });
        } else {
            // try to find javascript link
            var a = parentTag(img, 'a', area), matches;
            if (a && a.href && (matches = jsRe.exec(a.href)) && matches[1] && validImgPath.test(matches[1])) {
                data.push({
                    node: a,
                    fullSrcHtml: matches[1],
                    thumbSrcHtml: $e.html(img.src),
                    titleHtml: $e.html(a.title || img.title || img.alt || '')
                });
            }
        }
    }

    if (data.length) {
        // Create and init slideshow widget
        var codeTpl =
            '<div class="widget-picviewer clear"><div class="scene">' +
                '<img class="front-image" src="{fullSrc1}" alt="{title1}">' +
            '</div>' +
            '<div class="thumbs"><div class="scroller"><ul class="ilist scroller-list">' +
                '{thumbs}' +
            '</ul></div></div>' +
            '</div>';

        var listItemTpl = '<li><a href="{fullSrc}" title="{title}"><img src="{thumbSrc}" alt="{title}"></a></li>';

        var listCode = '';
        for (var i = 0; i < data.length; i++) {
            var item = data[i];
            listCode += listItemTpl.replace(/\{fullSrc\}/g, item.fullSrcHtml).
                                    replace(/\{title\}/g, item.titleHtml).
                                    replace(/\{thumbSrc\}/g, item.thumbSrcHtml);
        }

        var code = codeTpl.replace(/\{fullSrc1\}/g, data[0].fullSrcHtml).
                            replace(/\{title1\}/g, data[0].titleHtml).
                            replace(/\{thumbs\}/g, listCode);

        var slideshowDiv = $e.create('div', {  className: 'widget-picviewer clear', innerHTML: code });

        var
            fallbackImg = find('article-fallback-image', area, 'div'),
            insertPoint = find('article-slideshow-placeholder', area, 'div') || area;

        // Insert slideshow
        insertPoint.insertBefore(slideshowDiv, insertPoint.firstChild);

        // Remove article fallback image if present
        if (fallbackImg) {
            fallbackImg.parentNode.removeChild(fallbackImg);
        }

        // Remove original images
        for (var i = 0; i < data.length; i++) {
            var node = data[i].node;
            node.parentNode.removeChild(node);
        }

        // Init s/s
        attachSlideshow(slideshowDiv);
    }
}


function initPicViewer() {
    var viewers = findAll('widget-picviewer', document.body, 'div');
    for (var i = 0; i < viewers.length; i++) {
        attachSlideshow(viewers[i]);
    }
}

function fixAlternateRows() {
    var tables = document.getElementsByTagName('table');
    for (var i = 0; i < tables.length; i++) {
        if (hasClass(tables[i], 'alternate-rows')) {
            var trs = tables[i].getElementsByTagName('tr');
            for (var j = 2; j < trs.length; j+=2) {
                addClass(trs[j], 'odd');
            }
        }
    }
}


function fixMenuItems() {
    var menu = document.getElementById('top-menu-list');
    if (!menu) { return; };
    // Find active section

    var active = null;
    for (var ch = menu.firstChild; ch; ch = ch.nextSibling) {
        if (ch.nodeName == 'LI' && hasClass(ch, 'active')) {
            active = ch;
            break;
        }
    }

    if (!active) {
        return;
    };

    // Get submenu list
    var submenu = find('menu-2-hidden', active, 'UL');
    var links = submenu ? submenu.getElementsByTagName('A') : [];

    var url = location.pathname;
    var subtitle = '', matchLevel = 0;
    for (i = 0; i < links.length; i++) {
        var path = links[i].href.replace(/^https?:\/\/[^\/]+/, '').replace(/[?#].*$/, '');
        if (!path) { continue; };
        if (url.substr(0, path.length) == path && path.length > matchLevel) {
            matchLevel = path.length;
            subtitle = $e.getInnerText(links[i]);
        }
    }

    // Ok, we failed, let's try other way
    var breadcrumbDiv = find('header-with-ruler', document, 'div');
    if (breadcrumbDiv && !subtitle) {
        var html = breadcrumbDiv.innerHTML;
        html = html.replace(/<a[^>]*>|<\/a>|<\/?b[^>]*>/gi, '').replace(/\s*<nobr>(\s|&nbsp;)*\/(\s|&nbsp;)*<\/nobr>\s*/gi, '/');
        var parts = html.split('/');
        var html = parts.pop();
        html = $e.trim(html);
        if (html && html.length < 50) {
            subtitle = $e.htmlToText(html);
        }
    }

    if (subtitle) {
        // Add a subtitle
        var subitem = find('active-subitem', active, 'span');
        $e.setInnerText(subitem, subtitle);
    }
}

