/**
 * LiveMenu, version 1.0
 *
 * Copyright (c) 2009 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 as main menus */
    mainMenuClassName: 'lm-menu',

    /* Css class name of 'ul' elements, which are considered as 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 set position of a submenu */
    right: 'lm-right', left: 'lm-left', up: 'lm-up', down: 'lm-down',

    /* A delay of the opening of a submenu (in milliseconds)*/
    showDelay: 80,

    /* A delay of the closing of a submenu (in milliseconds) */
    hideDelay: 400,

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

    /**
     * An effect that is used to show or hide a submenu. 
     * Can be 'plain', 'slide' or 'fade'
     */
    effect: 'plain',


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

    /* The duration of a submenu opening or closing (in milliseconds) */
    duration: 500,

    /* The maximum number of simultaneously hiding submenus */
    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);
    });
}

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

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

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

    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,
        ulNodes = this.domNode.getElementsByTagName('ul'),
        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 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() {
    var children = this.domNode.childNodes;

    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 the 'visibleSubs' array */
removeFromVisibleSubs: function (sub) {
    var vs = this.visibleSubs, vsUpdated = [];
    for (var i=0; i<vs.length; i++) if (vs[i] != sub) vsUpdated.push(vs[i]);
    this.visibleSubs = vsUpdated;
},
/**
 * Force hiding submenus to hide 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();
}

}

/* 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.hideTimer = null;
    this.showTimer = null;

    this.isShowing = false;
    this.isHiding = false;
    this.isGoingToHide = 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, showEventType = this.menu.config.showEventType;

    var _this = this;
    e.add(this.opener,  showEventType, function (e) { _this.show(e) }, true);
    e.add(this.opener,  showEventType, 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 (showEventType == '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 opening or closing, 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 m = this.menu;
    if (this.isGoingToHide) {
        clearTimeout(this.hideTimer); this.hideTimer = null;
        this.isGoingToHide = false;
    } else if (this.isHiding) {
        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 'showEventType' configuration
    //parameter value is 'click'
    if (this.isHiding || this.isGoingToHide) 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; 

    var lastShownSub = m.visibleSubs[m.visibleSubs.length-1];

    //If the submenu has child submenus open, do not hide this submenu
    if (lastShownSub != this) return;

    this.isGoingToHide = 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 (m.config.beforeShow) m.config.beforeShow.call(this);

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

    this.isShowing = true;

    liveMenu.Effect.In(this, m.config.effect, function () {
        this.isShowing = false;

        var m = this.menu;

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

        if (m.subToShowNext) {
            m.subToShowNext.doShow();
            m.subToShowNext = null;
        }
    });
},
/* Hides a submenu */
doHide: function () {
    if (this.hideTimer) {
        clearTimeout(this.hideTimer); this.hideTimer = null;
    }
    var m = this.menu;

    this.isShowing = false;
    this.isGoingToHide = false;
    this.isHiding = true;

    m.parseHidingSubs();

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

    liveMenu.Effect.Out(this, function () {
        this.isHiding = false;

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

        m.removeFromVisibleSubs(this);

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

        if (hideNext) this.parentSub.doHide();
    });
},
/* 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.isGoingToHide = 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 (but not children) of the parent submenu to hide
 * without effects, and hides a sibling submenu, which is not in the process of
 * hiding
 */
parseVisibleNotAncestors: function () {
    var vnas  = this.getVisibleNotAncestors();

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

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

    for (var i=0; i<vnas.length; i++) {
        vna = vnas[i];
        vnaParent = vna.parentSub || this.menu;
        if (vnaParent != parent) {
            vna.hideWithoutEffect();
        } else if (!vna.isHiding) {
            subToHide = vna;
        }
    }
    if (subToHide) subToHide.doHide();
},
/* Gets the position of a submenu out of 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 = {

/* An array of effect objects */
effects: [],

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.submenu);
        } else {
            var p = (now - e.startOn)/(e.finishOn - e.startOn);
            e.render(this.Transitions[e.transition](p));
        }
    }
},
/* Shows a submenu with effect 'effectName' */
In: function (submenu, effectName, callback) {
    if (this.effects[submenu.id] == null)
        this.effects[submenu.id] = new this[effectName](submenu);

    submenu.container.style.zIndex = this.zIndex++;
    
    if (effectName == 'plain') {
        this.effects[submenu.id].render('in');
        callback.call(submenu);
    } else {
        this.loop(this.effects[submenu.id], 'in', callback);
    }
},
/* Hides a submenu with the effect */
Out: function (submenu, callback) {
    if (this.effects[submenu.id].type == 'plain') {
        this.effects[submenu.id].render('out');
        callback.call(submenu);
    } else {
        this.loop(this.effects[submenu.id], 'out', callback);
    }
},
/* 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 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 at 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.container = submenu.container;
    liveMenu.Effect.setContainerPos(submenu);
}
liveMenu.Effect.plain.prototype = {
    render: function(direction) {
        this.container.style.visibility = direction == 'in'
            ? 'visible' : 'hidden';
    }
}

/* 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 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 && this.direction == 'out') {
            containerStyle.visibility = 'hidden';
        }
    }
}

}

/* 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.dimension = 'Width'; this.axis = 'left';
        this.initCoord = parseInt(submenu.domNode.style.left);
    } else {
        this.dimension = 'Height'; this.axis = 'top';
        this.initCoord = parseInt(submenu.domNode.style.top);
    }
}
liveMenu.Effect.slide.prototype = {

/**
 * Renders 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 pos = progress, sub = this.submenu.domNode,
            subHeightOrWidth = sub['offset'+this.dimension],
            coord = this.direction == 'in' 
                ? Math.floor(subHeightOrWidth * pos)
                : subHeightOrWidth - Math.floor(subHeightOrWidth * pos);

        sub.style[this.axis] = this.initCoord < 0
            ? (this.initCoord+coord)+'px'
            : (this.initCoord-coord)+'px';

        if (pos === 1.0 && this.direction == 'out')
            this.submenu.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;
}

}

