/**
 * LiveMenu, version 1.1
 *
 * Copyright (c) 2009-2010 Sergey Golubev
 *
 * LiveMenu is freely distributable under the terms of the MIT License.
 * For details, see http://livemenu.sourceforge.net
 */

var liveMenu = {};

liveMenu.defaultConfig = {
    /* Css class name of 'ul' elements, which are considered to be main menus */
    mainMenuClassName: 'lm-menu',

    /* Css class name of 'ul' elements, which are considered to be submenus */
    submenuClassName: 'lm-submenu',

    /* Css class name of a submenu container */
    containerClassName: 'lm-container',

    /* Css class names of horizontal and vertical submenus */
    horizontalClassName: 'lm-horizontal', verticalClassName: 'lm-vertical',

    /* Css class names, which determine position of a submenu */
    right: 'lm-right', left: 'lm-left', up: 'lm-up', down: 'lm-down',

    /* A delay in showing the submenus (in milliseconds)*/
    showDelay: 80,

    /* A delay in hiding a submenu (in milliseconds) */
    hideDelay: 500,

    /**
     * An event type at which a submenu should be shown. 
     * Can be 'mouseenter' or 'click'
     */
    showOn: 'mouseenter',

    /**
     * An effect that is used when showing or hiding a submenu. 
     * Can be 'plain', 'slide', 'fade' or smooth
     */
    effect: 'plain',

    /**
     * Defines the way the submenus are being hidden: simultaneously or 
     * consecutively. Can be simultaneous or consecutive.
     */
    mode: 'simultaneous',


    /**
     * The following configuration options make sense only if the 'effect'
     * option is not set to 'plain' 
     */

    /* The duration of showing or closing the submenus (in milliseconds) */
    duration: 400,

    /**
     * The maximum number of simultaneously hiding sibling submenus. Makes
     * sense only if the 'consecutive' mode is used, but regardless the mode,
     * '0' value make the submenus hide without effects. 
     */
    maxHidingSubmenus: 3,

    /* A transition algorithm. Can be 'linear' or 'sinoidal'. */
    transition: 'sinoidal'
}

liveMenu.isReady = false; //True if the DOM is loaded

liveMenu.subsCount = 0; //Used for submenu IDs generation

liveMenu.isKonqueror = navigator.userAgent.indexOf('Konqueror') != -1;

/* Initializes the menus after the DOM is loaded */
liveMenu.initOnLoad = function (menuId, config) {
	if (document.addEventListener) {
		document.addEventListener("DOMContentLoaded", function() {
            liveMenu.isReady = true;
            new liveMenu.Menu(menuId, config);
		}, false);
	} else if (document.attachEvent) {
		document.attachEvent("onreadystatechange", function() {
			if (document.readyState === "complete") {
                liveMenu.isReady = true;
                new liveMenu.Menu(menuId, config);
            }
		});
	}

    liveMenu.event.add(window, "load", function () { 
        if (!liveMenu.isReady) new liveMenu.Menu(menuId, config);
    });
}

/* The main menu constructor */
liveMenu.Menu = function (menuId, config) {
    var X = liveMenu.Utils;

    this.config = X.merge(liveMenu.defaultConfig, config);
    if (this.config.showOn == 'click')
        this.config.showDelay = 0;

    if (this.config.effect == 'plain' || this.config.maxHidingSubmenus == 0)
        this.config.mode = 'consecutive';

    this.id = menuId;
    this.domNode = document.getElementById(menuId);

    this.orientation = this.getOrientation();

    this.submenus = {};
    this.visibleSubs = [];
    this.stopHidingOn = null;

    var initSubNodes = this.getInitSubNodes();
    this.setSubIDs(initSubNodes);

    var convertedSubNodes = this.convertMenuTree(initSubNodes);
    this.initializeSubs(convertedSubNodes);
}

liveMenu.Menu.prototype = {

/* Gets initial submenu DOM nodes */
getInitSubNodes: function () {
    var X = liveMenu.Utils, cfg = this.config;
	if (this.domNode)
		var ulNodes = this.domNode.getElementsByTagName('ul');
	else
		var ulNodes = []; 
	var initSubNodes = [];

    for (var i=0, l=ulNodes.length; i<l; i++)
    if (X.hasClass(ulNodes[i], cfg.submenuClassName))
        initSubNodes.push(ulNodes[i]);

    return initSubNodes;
},
/* Generates and sets submenu IDs */
setSubIDs: function (initSubNodes) {
    for (var i=0, l=initSubNodes.length; i<l; i++) {
        var initSub = initSubNodes[i];

        initSub.id = 'submenu'+(++liveMenu.subsCount);

        initSub.parentNode.id = 
            'submenu'+liveMenu.subsCount+'_opener';
    }
},
/**
 * Converts an initial menu tree into the set of separate submenu nodes. 
 * For example: a submenu like:
 * <ul class="submenu">
 *  <li>item1</li>
 *  <li>item2<ul class="submenu">...</ul></li>
 * </ul>
 * Converts into:
 * <div class="container">
 *  <ul class="submenu">
 *   <li>item1</li>
 *   <li>item2</li>
 *  </ul>
 * </div>
 */
convertMenuTree: function(initSubNodes) {
    var initSub, container, sub, children, childSub,
        convertedSubNodes = [], i, j, l;

    for (i=0, l=initSubNodes.length; i<l; i++) {
        initSub = initSubNodes[i];
        sub = initSub.cloneNode(true);
        children = sub.childNodes;

        for (j=0; j<children.length; j++)
        if (children[j].tagName == 'LI') {
            if (childSub = children[j].getElementsByTagName('ul')[0])
                childSub.parentNode.removeChild(childSub);
        }

        container = document.createElement('div');
        container.className = this.config.containerClassName;
        container.appendChild(sub);
        sub.style.display = 'block';

        //Konqueror doesn't set css 'opacity' property correctly on nodes that 
        //consists only of elements with css 'float' property set to 'left'. 
        //So, horizontal submenus are not shown correctly using some effects.
        //To fix it, add an empty text node to the container of a submenu:
        if (liveMenu.isKonqueror) {
            var X = liveMenu.Utils;
            if ((this.config.effect == 'fade' || this.config.effect == 'smooth')
                && X.hasClass(sub, this.config.horizontalClassName))
            {
                container.appendChild(document.createTextNode('\u00a0'));
            }
        }

        document.body.appendChild(container);

        convertedSubNodes.push(sub);
    }

    this.removeInitSubNodes();

    return convertedSubNodes;
},
/* Initializes submenu objects */
initializeSubs: function(convertedSubNodes) {
    for (var i=0, l=convertedSubNodes.length; i<l; i++) {
        var sub = convertedSubNodes[i];
        this.submenus[sub.id] = new liveMenu.Submenu(sub, this);
    }
},
/* Removes initial submenu nodes */
removeInitSubNodes: function() {
	if (this.domNode)
	    var children = this.domNode.childNodes;
	else
		var children = "";
		
    for (var i=0; i<children.length; i++)
    if (children[i].tagName == 'LI') {
        var childSub = children[i].getElementsByTagName('ul')[0];
        if (childSub) childSub.parentNode.removeChild(childSub);
    }
},
/* Removes a submenu from 'this.visibleSubs' array */
removeFromVisibleSubs: function (sub) {
    var X = liveMenu.Utils;
    this.visibleSubs.splice(X.indexOf(sub, this.visibleSubs), 1);
},
/**
 * Forces the submenus in the process of hiding to hide immediately (without 
 * effects) if the limit of simultaneous hiding submenus is exceeded
 */
parseHidingSubs: function () {
    var limit = this.config.maxHidingSubmenus, hidingSubs = [],
        visibleSubs = this.visibleSubs;

    for (var i=0; i<visibleSubs.length; i++)
        if (visibleSubs[i].isHiding)
            hidingSubs.push(visibleSubs[i]);

    var numSubsToHide = hidingSubs.length - limit;

    if (numSubsToHide > 0)
        for (i=0; i<numSubsToHide; i++)
            hidingSubs[i].hideWithoutEffect();
},
/* Gets the group of submenus which should be shown. */
getGroupToShow: function (submenu) {
    var ancestors = submenu.getAncestors();
    var group = [submenu];
    for (var i in ancestors) {
        if (ancestors[i].isHiding) {
            group.unshift(ancestors[i]);
        }
    }
    return group;
},
/* Gets the group of submenus which should be hidden. */
getGroupToHide: function (submenu) {
    var ancestors = submenu.getAncestors();
    var group = [submenu];
    for (var i=0; ancestors[i] && ancestors[i] != this.stopHidingOn; i++) {
        group.unshift(ancestors[i]);
    }
    return group;
},
/* Gets the orientation of a submenu from its node className value */
getOrientation: function (submenuNode) {
    var X = liveMenu.Utils, cfg = this.config,
        node = submenuNode || this.domNode;
    if (X.hasClass(node, cfg.horizontalClassName)) return 'horizontal';
    if (X.hasClass(node, cfg.verticalClassName))  return 'vertical';
    return null;
}

}

/* A submenu constructor */
liveMenu.Submenu = function (domNode, menuObj) {
    this.id = domNode.id;
    this.menu = menuObj;
    this.domNode = domNode;
    this.container = domNode.parentNode;
    this.opener = document.getElementById(this.id + '_opener');
    this.parentSub = this.menu.submenus[this.opener.parentNode.id];
    this.position = this.getPosition();
    this.orientation = menuObj.getOrientation(domNode);

    this.hideTimer = null;
    this.showTimer = null;

    this.isShowing = false;
    this.isHiding = false;
    this.isSetToHide = false;

    this.addEventListeners();
}

liveMenu.Submenu.prototype = {

/* Adds all the necessary event listeners to a submenu node and its opener */
addEventListeners: function() {
    var e = liveMenu.event, showOn = this.menu.config.showOn;

    var _this = this;
    e.add(this.opener,  showOn, function (e) { _this.show(e) }, true);
    e.add(this.opener,  showOn, function (e) { _this.cancelHide(e) }, true);
    e.add(this.opener,  'mouseleave',  function (e) { _this.hide(e) });
    e.add(this.domNode, 'mouseleave',  function (e) { _this.hide(e) });

    var children = this.domNode.childNodes;
    for (var i=0; i<children.length; i++)
    if (children[i].tagName =='LI')
        e.add(children[i], 'mouseenter', function (e) { _this.cancelHide(e) });

    if (showOn == 'click') {
        var anchors = this.opener.getElementsByTagName('A');
        for (i=0; i<anchors.length; i++)
            e.add(anchors[i], 'click', function (e) { e.preventDefault() });
    }
},
/* The mouseover event listener, which is responsible for submenu showing */
show: function (e) {
    var parentSub = this.parentSub, m = this.menu;

    //If the parent submenu is in the process of showing or hiding, remember 
    //to show the submenu as soon as its parent is opened.
    if (parentSub && (parentSub.isShowing || parentSub.isHiding)) {
        m.subToShowNext = this;
        return;
    }

    var _this = this, showDelay = m.config.showDelay;
    this.showTimer = 
        setTimeout(function() { _this.doShow(false) }, showDelay);
},
/* The mouseover event listener, which cancels hiding of a submenu */
cancelHide: function (e) {
    if (!this.isVisible()) return; //show() is going to handle this event

    var ancestors = this.getAncestors();
    for (var i=0; i<ancestors.length; i++) {
        var sub = ancestors[i];
        clearTimeout(sub.hideTimer); sub.hideTimer = null;
        sub.isSetToHide = false;
    }
    var m = this.menu;
    if (this.isSetToHide) {
        clearTimeout(this.hideTimer); this.hideTimer = null;
        this.isSetToHide = false;
        m.stopHidingOn = this;
    } else if (this.isHiding) {
        m.stopHidingOn = this.parentSub;
        this.isHiding = false;
        this.doShow(true);
    } else if (m.stopHidingOn != this) {
        var lastShownSub = m.visibleSubs[m.visibleSubs.length-1];
        if (lastShownSub != this) m.stopHidingOn = this;
    }

    e.stopImmediatePropagation();
},
/* The mouseout event listener, which is responsible for submenu hiding */
hide: function(e) {
    //The following condition is possible if 'showOn' configuration
    //parameter value is 'click'
    if (this.isHiding || this.isSetToHide) return;

    //The submenu is hidden? Prevent it from showing on delay.
    if (!this.isVisible()) {
        this.menu.subToShowNext = null;
        clearTimeout(this.showTimer);
        return;
    }

    var m = this.menu;

    //Prevent the queue of hiding submenu from stopping
    m.stopHidingOn = null; 


    if (m.config.mode == 'consecutive') {
        //If the submenu has child submenus open, do not hide this submenu
        var lastShownSub = m.visibleSubs[m.visibleSubs.length-1];
        if (lastShownSub != this) return;
    }

    this.isSetToHide = true;

    var _this = this;
    this.hideTimer = 
        setTimeout(function () { _this.doHide() }, m.config.hideDelay);

    //Prevent the hide handler on the parent submenu from triggering if the 
    //mouse pointer was on the submenu opener
    e.stopPropagation();
},
/* Shows a submenu */
doShow: function (isVisible) {
    this.parseVisibleNotAncestors();

    var m = this.menu;

    if (isVisible) m.removeFromVisibleSubs(this);
    m.visibleSubs.push(this);

    var subsToShow = m.config.mode == 'simultaneous' ? m.getGroupToShow(this) 
                                                     : [this];
    for (var i in subsToShow) {
        subsToShow[i].isShowing = true;
        subsToShow[i].isHiding = false;
        if (m.config.beforeShow) m.config.beforeShow.call(subsToShow[i]);
    }
    liveMenu.Effect.In(subsToShow, m.config.effect, function () {
        var submenu, skipIt = false, m;
        //'this' is an effect object (consecutive mode)?
        if (!this.subIDs) {
            submenu = this.submenu;
            submenu.isShowing = false;
            skipIt = true;
            m = submenu.menu;
            if (m.config.afterShow) m.config.afterShow.call(submenu);
        } else {
            //'this' is a group of effect objects (simultaneous mode)
            m = this.effects[this.subIDs[0]].submenu.menu;
            for (var subId in this.effects) {
                submenu = this.effects[subId].submenu;
                submenu.isShowing = false;
                if (m.config.afterShow) m.config.afterShow.call(submenu);
            }
        }

        if (m.subToShowNext && 
            (skipIt || m.subToShowNext.parentSub.id in this.effects))
        {
            m.subToShowNext.doShow();
            m.subToShowNext = null;
        }
    });
},
/* Hides a submenu */
doHide: function () {
    var m = this.menu, subsToHide, forcePlainEffect;

    if (m.config.mode == 'simultaneous') {
        subsToHide = m.getGroupToHide(this);
        forcePlainEffect = false;
    } else {
        subsToHide = [this];
        forcePlainEffect = m.config.maxHidingSubmenus == 0 ? true : false;
    }

    for (var i in subsToHide) {
        clearTimeout(subsToHide[i].hideTimer); subsToHide[i].hideTimer = null;
        subsToHide[i].isHiding = true;
        subsToHide[i].isSetToHide = false;
        subsToHide[i].isShowing = false;
        if (m.config.beforeHide) m.config.beforeHide.call(subsToHide[i]);
    }

    if (m.config.mode != 'simultaneous' && !forcePlainEffect) {
        m.parseHidingSubs();
    }

    liveMenu.Effect.Out(subsToHide, function () {
        var m, submenu, subID;

        //'this' is a group of effect objects (simultaneous mode)?
        if (this.subIDs) {
            m = this.effects[this.subIDs[0]].submenu.menu;
            for (subID in this.effects) {
                submenu = this.effects[subID].submenu;
                submenu.isHiding = false;

                m.removeFromVisibleSubs(submenu);

                if (m.config.afterHide) m.config.afterHide.call(submenu);
            }
            return;
        }
        //'this' is an effect object (consecutive mode)
        submenu = this.submenu;
        submenu.isHiding = false;
        m = submenu.menu;

        if (submenu.parentSub) {
            var lastShownSub = m.visibleSubs[m.visibleSubs.length-1]
            if (lastShownSub == submenu &&
                m.stopHidingOn != submenu.parentSub &&
                !submenu.getVisibleNotAncestors().length)
            {
                var hideNext = true;
            }
        }

        m.removeFromVisibleSubs(submenu);

        if (m.config.afterHide) m.config.afterHide.call(submenu);

        if (hideNext) submenu.parentSub.doHide();
    }, forcePlainEffect);
},
/* Forces a submenu to hide without effects */
hideWithoutEffect: function () {
    if (this.hideTimer) {
        clearTimeout(this.hideTimer); this.hideTimer = null;
    }

    liveMenu.Effect.destroy(this);

    this.container.style.visibility = 'hidden';

    this.isShowing = false;
    this.isHiding = false;
    this.isSetToHide = false;

    var m = this.menu;

    m.subToShowNext = null;

    if (m.config.beforeHide) m.config.beforeHide.call(this);

    m.removeFromVisibleSubs(this);

    if (m.config.afterHide) m.config.afterHide.call(this);
},
/**
 * Forces all descendants of the parent submenu (except current submenu), which
 * are not in the process of hiding to hide
 */
parseVisibleNotAncestors: function () {
    var vnas  = this.getVisibleNotAncestors();

    if (!vnas.length) return;

    var m = this.menu;
    if (m.config.mode == 'simultaneous') {
        var X = liveMenu.Utils,
            lastShownSub = m.visibleSubs[m.visibleSubs.length-1];

        if (!lastShownSub.isHiding && X.indexOf(lastShownSub, vnas) != -1)
            lastShownSub.doHide();

        for (var i=0; i<vnas.length; i++)
            if (!vnas[i].isHiding)
                vnas[i].doHide();
        return;
    }

    if (vnas.length === 1) {
        if (!vnas[0].isHiding) 
            vnas[0].doHide();
        return;
    }

    var parent = this.parentSub || m, vna, vnaParent, subToHide; 

    for (var i=0; i<vnas.length; i++) {
        vna = vnas[i];
        vnaParent = vna.parentSub || m;
        if (vnaParent != parent) {
            vna.hideWithoutEffect();
        } else if (!vna.isHiding) {
            subToHide = vna;
        }
    }
    if (subToHide) subToHide.doHide();
},
/* Gets the position of a submenu from its node className value */
getPosition: function () {
    var X = liveMenu.Utils, cfg = this.menu.config,
        sub = this.domNode;
    if (X.hasClass(sub, cfg.right)) return 'right';
    if (X.hasClass(sub, cfg.down))  return 'down';
    if (X.hasClass(sub, cfg.up))    return 'up';
    if (X.hasClass(sub, cfg.left))  return 'left';
    return null;
},
/* Gets ancestor submenus of the current submenu */
getAncestors: function () {
    var ancestorSubs = [];
    var parent = this.parentSub;
    while (parent != null) {
        ancestorSubs.push(parent);
        parent = parent.parentSub;
    }
    return ancestorSubs;
},
/* Gets visible submenus, which are not ancestors of the current submenu */
getVisibleNotAncestors: function () {
    var X = liveMenu.Utils;
    var vnas = [];
    var ancestorSubs = this.getAncestors();
    var visibleSubs = this.menu.visibleSubs;

    ancestorSubs.push(this);

    for (var i=0; i<visibleSubs.length; i++)
        if (X.indexOf(visibleSubs[i], ancestorSubs) == -1)
            vnas.push(visibleSubs[i]);

    return vnas;
},
/* Checks if a submenu is visible */
isVisible: function () {
    return this.container.style.visibility == 'visible';
}

}

liveMenu.Effect = {

/* The effect objects storage */
effects: {},

/* An array of the 'liveMenu.Effect.group' objects */
groups: [],

zIndex: 100,

Transitions: {
    linear: function (pos) { return pos },
    sinoidal: function (pos) { return (-Math.cos(pos*Math.PI)/2) + .5 }
},

/**
 * Starts effect rendering and calculates the progress of it. Passes the
 * progress value to the render function of the effect object.
 */
loop: function (effectObj, direction, callback) {
    var e = effectObj;
    if (direction) {
        e.direction = direction;
        e.callback = callback;

        if (e.intervalId) { clearInterval(e.intervalId); e.intervalId = null; }

        var now = (new Date()).getTime();
        e.startOn = e.finishOn ? (2*now - e.finishOn) : now;
        e.finishOn = e.startOn + e.duration;

        e.render(null);

        e.intervalId = 
            setInterval(function () { liveMenu.Effect.loop(e) }, e.interval);
    } else {
        var now = (new Date()).getTime();
        if (now >= e.finishOn) {
            clearInterval(e.intervalId); e.intervalId = null;
            e.finishOn = e.startOn = null;
            e.render(1.0);
            e.callback.call(e);
        } else {
            var p = (now - e.startOn)/(e.finishOn - e.startOn);
            e.render(this.Transitions[e.transition](p));
        }
    }
},
/* Gets the index of a group object in 'this.groups' array by the group ID  */
getGroupIndex: function (groupID) {
    for (var i in this.groups) if (this.groups[i].id == groupID) return i;
    return null;
},
/* Makes up an array of the submenu IDs in a group, and returns it */
getSubIDs: function (groupOfSubs) {
    var subIDs = [];
    for (var i=0; i<groupOfSubs.length; i++) {
        subIDs.push(groupOfSubs[i].id);
    }
    return subIDs;
},
/* Gets the string representation of an array of the submenus IDs */
getGrpID: function (subIDs) {
    return subIDs.join(' ')+' ';
},
/* Shows a submenu with effect 'effectName' */
In: function (subsToShow, effectName, callback) {
    var m = subsToShow[0].menu, i;
    this.zIndex++;
    for (i in subsToShow) subsToShow[i].container.style.zIndex = this.zIndex;

    if (m.config.mode == 'simultaneous') {
        var X = liveMenu.Utils, grp,
            grpSubIDs = this.getSubIDs(subsToShow),
            grpID = this.getGrpID(grpSubIDs);
        for (i in this.groups) {
            grp = this.groups[i];
            if (grpID.indexOf(grp.id) == -1 && 
                X.indexOf(grpSubIDs[grpSubIDs.length-1], grp.subIDs) != -1)
            { 
                grp.divide(grpSubIDs[grpSubIDs.length-1]);
                break;
            }
        }

        var needToCreateNewGroup = true;
        for (i in this.groups) {
            grp = this.groups[i];
            if (grpID.indexOf(grp.id) != -1) {
                this.loop(grp, 'in', callback);
                needToCreateNewGroup = false;
            }
        }
        if (needToCreateNewGroup) {
            this.groups.push(new this.group(grpID, grpSubIDs, subsToShow, effectName));
            this.loop(this.groups[this.groups.length-1], 'in', callback);
        }
    } else {
        var submenu = subsToShow[0];
        if (this.effects[submenu.id] == null)
            this.effects[submenu.id] = new this[effectName](submenu);

        if (effectName == 'plain') {
            this.effects[submenu.id].render('in');
            callback.call(this.effects[submenu.id]);
        } else {
            this.loop(this.effects[submenu.id], 'in', callback);
        }
    }
},
/* Hides a submenu with the effect */
Out: function (subsToHide, callback, forcePlainEffect) {
    var m = subsToHide[0].menu;
    if (m.config.mode == 'consecutive') {
        var submenu = subsToHide[0];
        if (forcePlainEffect) {
            this.destroy(submenu);
            this.effects[submenu.id] = new this.plain(submenu);
        }
        var e = this.effects[submenu.id];
        if (e.type == 'plain') {
            e.render('out');
            callback.call(e);
            if (forcePlainEffect) this.destroy(submenu);
        } else {
            this.loop(this.effects[submenu.id], 'out', callback);
        }
        return;
    }

    var X = liveMenu.Utils, grp,
        group = subsToHide,
        grpSubIDs = this.getSubIDs(group),
        grpID = this.getGrpID(grpSubIDs),

        grpIntersect = [];

    for (var grpIndex=0, l=this.groups.length; grpIndex < l; grpIndex++) {
        grp = this.groups[grpIndex];
        if (grpID != grp.id) {
            //If the current group of subs(grp) contains the group 
            //given(group) and it is showing now, divide it into two groups:
            if (grp.direction == 'in' && grp.id.indexOf(grpID) != -1) {
                var first = grpSubIDs[0],
                    divSubIndex = X.indexOf(first, grp.subIDs)-1;
                grp.divide(grp.subIDs[divSubIndex]);
            }
            else {
                intersect = this.getGroupsIntersection(grpSubIDs, grp.subIDs);
                //The group given intersects with the current group?
                if (intersect.length) {
                    var g1 = grp.subIDs, g2 = grpSubIDs;
                    //If the current group contains submenus which are 
                    //ancestors to all submenus of the group given, 
                    //remove them from the current group
                    if (X.indexOf(g1[g1.length-1], g2) != -1 && 
                        X.indexOf(g1[0], g2) == -1)
                    {
                        grp.divide(g1[X.indexOf(g2[0], g1)-1]);
                        grp = this.groups[this.groups.length-1];
                    }
                    //Make the current group begin to hide
                    this.loop(grp, 'out', callback);

                    grpIntersect = grpIntersect.concat(intersect);
                }
            }
        }
    }
    //Remove the common submenus from the group given
    for (var i in grpIntersect) {
        var key = X.indexOf(grpIntersect[i], grpSubIDs);
        if (key != -1) {
            group.splice(key, 1);
            grpSubIDs = this.getSubIDs(group);
        }
    }

    if (!group.length) return;

    grpID = this.getGrpID(grpSubIDs);

    if (!(grpIndex = this.getGroupIndex(grpID))) {
        this.groups.push(new this.group(grpID, grpSubIDs, group));
        grpIndex = this.groups.length-1;
    }
    this.loop(this.groups[grpIndex], 'out', callback);
},
/* Gets tha array of common submenus between two groups */
getGroupsIntersection: function (g1IDs, g2IDs) {
    var X = liveMenu.Utils,
        start = X.indexOf(g1IDs[0], g2IDs), intersect = [];
    if (start != -1) {
        intersect = g2IDs.slice(start, start+g1IDs.length);
    } else {
        start = X.indexOf(g2IDs[0], g1IDs);
        if (start != -1) {
            intersect = g1IDs.slice(start, start+g2IDs.length);
        }
    }
    return intersect;
},
/* Destroys the effect object */
destroy: function (submenu) {
    var effect = this.effects[submenu.id];

    if (effect && effect.intervalId) clearInterval(effect.intervalId);

    this.effects[submenu.id] = null;
},
/* Places a submenu container to on its target position */
setContainerPos: function (submenu) {
    var containerStyle = submenu.container.style;
    var targetCoords = this.getTargetCoords(submenu);
    containerStyle.left = targetCoords.left+'px';
    containerStyle.top = targetCoords.top+'px';
},
/* Gets target coordinates of a submenu */
getTargetCoords: function(subObj) {
    var X = liveMenu.Utils, o = subObj.opener;

    switch (subObj.position) {
        case 'right': return {
            left: X.getOffsetPos(o, 'Left') + o.offsetWidth,
            top: X.getOffsetPos(o, 'Top')
        };
        case 'down': return {
            left: X.getOffsetPos(o, 'Left'),
            top: X.getOffsetPos(o, 'Top') + o.offsetHeight
        };
        case 'left': return {
            left: X.getOffsetPos(o, 'Left') - subObj.domNode.offsetWidth,
            top: X.getOffsetPos(o, 'Top')
        };
        case 'up': return {
            left: X.getOffsetPos(o, 'Left'),
            top: X.getOffsetPos(o, 'Top') - subObj.domNode.offsetHeight
        }
    }
},
/* Places a submenu node on its initial position (for sliding effects) */
setSubInitPos: function (submenu) {
    var sub = submenu.domNode;
    switch (submenu.position) {
        case 'right': sub.style.left = -sub.offsetWidth+'px'; return;
        case 'down': sub.style.top = -sub.offsetHeight+'px'; return;
        case 'left': sub.style.left = sub.offsetWidth+'px'; return;
        case 'up': sub.style.top = sub.offsetHeight+'px'; return;
    }
}

}

/* The 'plain' effect constructor */
liveMenu.Effect.plain = function (submenu) {
    this.type = 'plain';
    this.submenu = submenu;
    this.container = submenu.container;
    liveMenu.Effect.setContainerPos(submenu);
}
liveMenu.Effect.plain.prototype = {
    render: function(direction) {
        this.container.style.visibility = direction == 'in'
            ? 'visible' : 'hidden';
    }
}

/* The 'group' object constructor */
liveMenu.Effect.group = function (id, submenuIDs, submenus, effectObjOrName) {
    var cfg = submenus[0].menu.config;
    this.subIDs = submenuIDs;
    this.id = id;
    this.duration = cfg.duration;
    this.transition = cfg.transition;
    this.interval = 20;

    if (typeof(effectObjOrName) == 'object') {
        this.effects = effectObjOrName;
    } else {
        var effectName = effectObjOrName,
            effects = liveMenu.Effect.effects;

        this.effects = {};
        
        for (var i=0; i<submenus.length; i++) {
            var sub = submenus[i];
            if (!(sub.id in effects)) {
                effects[sub.id] = new liveMenu.Effect[effectName](sub);
            }
            this.effects[sub.id] = effects[sub.id];
        }
    }
}
liveMenu.Effect.group.prototype = {

/**
 * Renders all the submenus in the group depending on the progress value 
 * received from liveMenu.Effect.loop() function using
 */
render: function(progress) {
    var subID, effects = this.effects;
    if (progress == null) {
        for (subID in effects) {
            effects[subID].direction = this.direction;
        }
    }
    for (subID in effects) if (effects[subID]) {
        effects[subID].render(progress);
    }
    if (progress == 1.0) {
        var grpIndex = liveMenu.Effect.getGroupIndex(this.id);
        liveMenu.Effect.groups.splice(grpIndex, 1);
    }
},
/* Divides the group. The submenu with id 'submenuID' is the separator */
divide: function(submenuID) {
    var groupPart1 = [], groupPart2 = [], effectsPart1 = {}, effectsPart2 = {},
        groupPart = groupPart1, effectsPart = effectsPart1;

    for (var subID in this.effects) {
        groupPart.push(this.effects[subID].submenu); 
        effectsPart[subID] = this.effects[subID];
        if (subID == submenuID) {
            groupPart = groupPart2;
            effectsPart = effectsPart2;
        }
    }

    var grpIndex = liveMenu.Effect.getGroupIndex(this.id),
        subIDs1 = liveMenu.Effect.getSubIDs(groupPart1),
        subIDs2 = liveMenu.Effect.getSubIDs(groupPart2),
        grp1ID = liveMenu.Effect.getGrpID(subIDs1),
        grp2ID = liveMenu.Effect.getGrpID(subIDs2);

    if (this.direction == 'in') {
        this.subIDs = subIDs1; this.id = grp1ID;
        this.effects = effectsPart1;
    } else {
        this.subIDs = subIDs2; this.id = grp2ID;
        this.effects = effectsPart2;
    }

    var e = liveMenu.Effect;
    e.groups.push(this);
    e.groups.splice(grpIndex, 1);

    e.groups.push(this.direction == 'in' ?
        new e.group(grp2ID, subIDs2, groupPart2, effectsPart2) :
        new e.group(grp1ID, subIDs1, groupPart1, effectsPart1)
    );

    e.groups[e.groups.length-1].finishOn = this.finishOn;
}

}

/* The 'fade' effect constructor */
liveMenu.Effect.fade = function (submenu) {
    var cfg = submenu.menu.config;
    this.submenu = submenu;
    this.duration = cfg.duration;
    this.transition = cfg.transition;
    this.interval = 100;

    liveMenu.Effect.setContainerPos(submenu);

    var containerStyle = submenu.container.style;
    containerStyle.opacity = '0.0';
    containerStyle.zoom = 1;
}
liveMenu.Effect.fade.prototype = {

/**
 * Renders the submenu using the effect, depending on the progress value 
 * received from liveMenu.Effect.loop() function
 */
render: function (progress) {
    var containerStyle = this.submenu.container.style;
    if (progress == null) {
        if (!this.submenu.isVisible()) {
            containerStyle.visibility = 'visible';
            containerStyle.filter = 'alpha(opacity: 0)';
        }
    } else {
        var opacity = this.direction == 'in' ? progress : 1.0 - progress;
        opacity = opacity.toFixed(1);

        containerStyle.opacity = opacity;
        containerStyle.filter = 'alpha(opacity='+opacity*100+')';

        if (progress === 1.0) {
            if (this.direction == 'out')
                containerStyle.visibility = 'hidden';
            else
                containerStyle.filter = '';
        }
    }
}

}

/* The 'smooth' effect constructor */
liveMenu.Effect.smooth = function (submenu) {
    liveMenu.Effect.slide.call(this, submenu);
    var containerStyle = submenu.container.style;
    containerStyle.opacity = '0.0';
    containerStyle.zoom = 1;
    containerStyle.visibility = 'visible';
    containerStyle.filter = 'alpha(opacity: 0)';
}

liveMenu.Effect.smooth.prototype = {

render: function (progress) {
    liveMenu.Effect.slide.prototype.render.call(this, progress);
    if (progress == null) {
        this.prevProgress = 0;
    } else if (progress >= this.prevProgress+0.1 || progress == 1) {
        this.prevProgress = progress;
        liveMenu.Effect.fade.prototype.render.call(this, progress);
    }
}

}

/* The 'slide' effect constructor */
liveMenu.Effect.slide = function (submenu) {
    var cfg = submenu.menu.config;
    this.submenu = submenu;
    this.duration = cfg.duration;
    this.transition = cfg.transition;
    this.interval = 20;

    liveMenu.Effect.setContainerPos(submenu);

    liveMenu.Effect.setSubInitPos(submenu);

    if (submenu.position == 'left' || submenu.position == 'right') {
        this.condData = {
            initCoord: parseInt(submenu.domNode.style.left),
            x1: submenu.position == 'left' ? -submenu.container.offsetWidth : submenu.opener.offsetWidth,
            x2: 'up',
            x3: 'offsetTop',
            x4: 'offsetHeight',
            x5: 'down',
            x6: 'Top',
            x7: 'Left',
            x8: 'top',
            x9: 'left',
            x10: 'horizontal',
            x11: 'offsetWidth'
        };
    } else {
        this.condData = {
            initCoord: parseInt(submenu.domNode.style.top),
            x1: submenu.position == 'up' ? -submenu.container.offsetHeight : submenu.opener.offsetHeight,
            x2: 'left',
            x3: 'offsetLeft',
            x4: 'offsetWidth',
            x5: 'right',
            x6: 'Left',
            x7: 'Top',
            x8: 'left',
            x9: 'top',
            x10: 'vertical',
            x11: 'offsetHeight'
        };
    }
}
liveMenu.Effect.slide.prototype = {

/**
 * Renders the submenu using the effect, depending on the progress value 
 * received from liveMenu.Effect.loop() function
 */
render: function (progress) {
    if (progress == null) {
        var containerStyle = this.submenu.container.style;
        if (!this.submenu.isVisible()) containerStyle.visibility = 'visible';
    } else {
        var X = liveMenu.Utils,
            d = this.condData,
            submenu = this.submenu, subNode = submenu.domNode,
            opener = submenu.opener, container = submenu.container,
            parentSub = submenu.parentSub,

            coord = this.direction == 'in'
                ? Math.floor(subNode[d.x11] * progress)
                : subNode[d.x11] - Math.floor(subNode[d.x11] * progress);

        if (parentSub) {
            var pNode = parentSub.domNode, c = true;
            if (parentSub.position == d.x2)
                c = pNode[d.x3] <= pNode[d.x4]-opener[d.x3]-opener[d.x4];
            else if (parentSub.position == d.x5)
                c = pNode[d.x3]*-1 <= opener[d.x3];
            if (c) {
                var p = X.getOffsetPos(opener, d.x6);
                container.style[d.x8] = p+'px';
            } else {
                var ancestorSub = parentSub.parentSub;
                if (ancestorSub && ancestorSub.orientation == d.x10) {
                    var aNode = ancestorSub.domNode, aPos = X.getOffsetPos(aNode, d.x6);
                    container.style[d.x8] = parentSub.position == d.x2 ?
                        (aPos - aNode[d.x4])+'px' :
                        (aPos + aNode[d.x4])+'px';
                }
            }
            container.style[d.x9] = (X.getOffsetPos(opener, d.x7) + d.x1) + 'px';
        }

        subNode.style[d.x9] = d.initCoord < 0
            ? (d.initCoord+coord)+'px'
            : (d.initCoord-coord)+'px';

        if (progress === 1.0 && this.direction == 'out')
            container.style.visibility = 'hidden';
    }
}

}

/* Some functions for managing events */
liveMenu.event = {
    /* An array of elements the event handlers attached to */
    elements: [],

    /**
     * A collection of the event handlers with the structure:
     * { 'element index 1': { 
     *      'event type 1': ['handler1', 'handler2',...],
     *      'event type 2': ['handler1', 'handler2',...],
     *      ... 
     *   },
     *   'element index 2': { ... },
     *   ...
     * }
     */
    handlers: {},

    /* Adds event handlers */
    add: function (elem, evType, fn, addFirst) {
        var X = liveMenu.Utils, elemIndex = X.indexOf(elem, this.elements);

        if (elemIndex === -1) {
            this.elements.push(elem);
            elemIndex = this.elements.length-1;
            this.handlers[elemIndex] = {};
        }

        if (!this.handlers[elemIndex][evType]) {
            this.handlers[elemIndex][evType] = [];

            var originalEventType = evType == 'mouseenter' ? 'mouseover' : 
                evType == 'mouseleave' ? 'mouseout' : evType;

            var handler = function (event) {
                var e = new liveMenu.Event(
                    event || window.event, evType);
                liveMenu.event.handle.call(arguments.callee.elem, e); 
            }
            handler.elem = elem;

            if (elem.addEventListener)
                elem.addEventListener(originalEventType, handler, false);
            else if (elem.attachEvent)
                elem.attachEvent('on' + originalEventType, handler);
            else
                elem['on' + originalEventType] = handler;
        }
        elem = null;

        if (addFirst)
            this.handlers[elemIndex][evType].unshift(fn);
        else
            this.handlers[elemIndex][evType].push(fn);
    },
    /* Handles the events */
    handle: function (e) {
        var X = liveMenu.Utils, E = liveMenu.event,
            elemIndex = X.indexOf(this, E.elements),
            handlers = E.handlers[elemIndex][e.type];

        for (var i in handlers) {
            var handler = handlers[i];
            if (e.type == 'mouseenter' || e.type == 'mouseleave') {
                var parent = e.relatedTarget;
                while ( parent && parent != this )
                    parent = parent.parentNode;

                if (parent == this) return;
            }
            handler.call(this, e);

            if (e.isImmediatePropagationStopped) return;
        }
    }
}

/* A wrapper of the original event object */
liveMenu.Event = function (srcEvent, evType) {
    this.originalEvent = srcEvent;
    this.type = evType;

    if (srcEvent.relatedTarget)
        this.relatedTarget = srcEvent.relatedTarget;
    else if (srcEvent.fromElement)
        this.relatedTarget = srcEvent.fromElement == srcEvent.srcElement 
            ? srcEvent.toElement : srcEvent.fromElement;
}
liveMenu.Event.prototype = {
    stopPropagation: function () {
        var e = this.originalEvent;
        e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
    },
    stopImmediatePropagation: function () {
        this.isImmediatePropagationStopped = true;
        this.stopPropagation();
    },
    preventDefault: function () {
        var e = this.originalEvent;
        e.preventDefault ? e.preventDefault() : e.returnValue = false;
    }
}

/* A collection of utility functions */
liveMenu.Utils = {

merge: function (obj1, obj2) {
    if (!obj2) return obj1;
    for (var prop in obj1) if (!obj2.hasOwnProperty(prop)) {
        obj2[prop] = obj1[prop];
    }
    return obj2;
},
hasClass: function (el, className) {
    var pattern = new RegExp('(^|\\s)'+className+'(\\s|$)');
    if (el && el.className && pattern.test(el.className))
        return true;
    return false;
},
getOffsetPos: function (el, pos) {
    var res = 0;
    while (el != null) {
        res += el['offset'+pos];
        el = el.offsetParent;
    }
    return res;
},
indexOf: function(elt, arr) {
    if (Array.prototype.indexOf) return arr.indexOf(elt);
    for (var pos=0; pos<arr.length; pos++)
        if (arr[pos] === elt) return pos;
    return -1;
}

}

