var SMBCms_version = '3.4b5';

// -- NOTE --
// All initializing code is below the Hash library

/**
 * Hash library function. Creates a new Hash object, and uses passed arguments
 * to populate the Hash.
 * Examples:
 * new Hash('key1', 'value1', 'key2', 'value2', 3, 'value3', 4, 'value4', 5, 6);
 * new Hash({'key1': 'value1', 'key2': 'value2'});
 * new Hash(new Array('val1', 'val2', 'val3'));
 * new Hash(existing_hash_object);
 */
function Hash()
{
    this.length = 0;
    this.numericLength = 0; 
    this.elementData = [];
    
    // If the first passed argument is a hash...
    if (arguments[0] && arguments[0].elementData)
    {
        this.length = arguments[0].length;
        this.numericLength = arguments[0].numericLength;
        this.elementData = arguments[0].elementData;
        return;
    }
    
    // Load hash data from an existing array, if it was passed
    if (typeof(arguments[0]) == 'object')
    {
        for (var i in arguments[0])
        {
            this.set(i, arguments[0][i]);
        }
        return;
    }
    
    // For regular Hash operations - load hash object from passed parameters
    for (var i = 0; i < arguments.length; i += 2)
    {
        if (typeof(arguments[i + 1]) != 'undefined')
        {
            this.set(arguments[i], arguments[i+1]);
        }
    }
};


/**
 * Hash prototype - get. Gets a Hash value that is associated with the passed in_key.
 */
Hash.prototype.get = function(in_key)
{
    return this.elementData[in_key];
};


/**
 * Hash prototype - set. Sets a Hash value that is paired with the passed in_key.
 */
Hash.prototype.set = function(in_key, in_value)
{
    // Allow "auto" to be a valid key, it will be converted to an integer here
    in_key = (in_key == 'auto' ? this.numericLength + 1 : in_key);
    
    if (typeof(in_value) != 'undefined')
    {
        if (typeof(this.elementData[in_key]) == 'undefined')
        {
            ++this.length;
            if (parseInt(in_key) == in_key)
            {
                ++this.numericLength;
            }
        }

        return this.elementData[in_key] = in_value;
    }

    return false;
};


/**
 * Hash prototype - remove. Removes an element from the Hash associated with the passed in_key.
 */
Hash.prototype.remove = function(in_key)
{
    var tmp_value;
    if (typeof(this.elementData[in_key]) != 'undefined')
    {
        this.length--;
        if (in_key == parseInt(in_key)) 
        {
            this.numericLength--;
        }

        tmp_value = this.elementData[in_key];
        delete this.elementData[in_key];
    }

    return tmp_value;
};


/**
 * Hash prototype - size. Returns the number of elements in the Hash.
 */
Hash.prototype.size = function()
{
    return this.length;
};


/**
 * Hash prototype - has. Checks to see if a key has a value in the Hash.
 */
Hash.prototype.has = function(in_key)
{
    return typeof(this.elementData[in_key]) != 'undefined';
};


/**
 * Hash prototype - find. Searches the Hash for a key that corresponds to the passed
 * value. 
 */
Hash.prototype.find = function(in_obj)
{
    for (var tmp_key in this.elementData) 
    {
        if (this.elementData[tmp_key] == in_obj) 
        {
            return tmp_key;
        }
    }
    return null;
};


/**
 * Hash prototype - merge. Merges the passed Hash into the current Hash.
 */
Hash.prototype.merge = function(in_hash)
{
    for (var tmp_key in in_hash.elementData) 
    {
        if (typeof(this.elementData[tmp_key]) == 'undefined') 
        {
            ++this.length;
            if (tmp_key == parseInt(tmp_key)) 
            {
                ++this.numericLength;
            }
        }

        this.elementData[tmp_key] = in_hash.elementData[tmp_key];
    }
};


/**
 * Hash prototype - compare. Compares the passed Hash with the current Hash.
 * Returns false if any elements are different, or true if all are the same.
 */
Hash.prototype.compare = function(in_hash)
{
    if (this.length != in_hash.length) 
    {
        return false;
    }

    for (var tmp_key in this.elementData) 
    {
        if (this.elementData[tmp_key] != in_hash.elementData[tmp_key]) 
        {
            return false;
        }
    }
    
    return true;
};


/**
 * "hashify" arrays. Converts an array to a Hash object.
 */
function hashify (in_array)
{
    // Loop through array and convert all array members to Hashes
    if (typeof(in_array) == 'object')
    {
        for (var i in in_array)
        {
            if (typeof(in_array[i]) == 'object')
            {
                in_array[i] = hashify(in_array[i]);
            }
        }
        
        // Make the array a hash
        in_array = new Hash(in_array);
    }
    
    return in_array;
};



// domLib code from Dan Allen's dom Library - www.mojavelinux.com
// domLib is released under the LGPL and can be re-released under the GPL.
// domLib version: 0.6
// -- Browsers --
var domLib_userAgent = navigator.userAgent.toLowerCase();
var domLib_isMac = navigator.appVersion.indexOf('Mac') != -1;
var domLib_isOpera = domLib_userAgent.indexOf('opera') != -1;
var domLib_isOpera7 = (domLib_userAgent.indexOf('opera/7') != -1 || domLib_userAgent.indexOf('opera 7') != -1);
var domLib_isSafari = domLib_userAgent.indexOf('safari') != -1;
var domLib_isKonq = domLib_userAgent.indexOf('konqueror') != -1;
// Both konqueror and safari use the khtml rendering engine
var domLib_isKHTML = (domLib_isKonq || domLib_isSafari);
var domLib_isIE = (!domLib_isKHTML && !domLib_isOpera && (domLib_userAgent.indexOf('msie 5') != -1 || domLib_userAgent.indexOf('msie 6') != -1));
var domLib_isIE5up = domLib_isIE;
var domLib_isIE50 = (domLib_isIE && domLib_userAgent.indexOf('msie 5.0') != -1);
var domLib_isIE55 = (domLib_isIE && domLib_userAgent.indexOf('msie 5.5') != -1);
var domLib_isIE5 = (domLib_isIE50 || domLib_isIE55);
// safari and konq may use string "khtml, like gecko", so check for destinctive /
var domLib_isGecko = domLib_userAgent.indexOf('gecko/') != -1;
var domLib_isMacIE = (domLib_isIE && domLib_isMac);
var domLib_isIE55up = domLib_isIE5up && !domLib_isIE50 && !domLib_isMacIE;
var domLib_isIE6up = domLib_isIE55up && !domLib_isIE55;

// -- Abilities --
var domLib_standardsMode = (document.compatMode && document.compatMode == 'CSS1Compat');
var domLib_useLibrary = (domLib_isOpera7 || domLib_isKonq || domLib_isIE5up || domLib_isGecko || domLib_isKHTML);
var domLib_hasBrokenTimeout = (domLib_isMacIE || (domLib_isKonq && domLib_userAgent.match(/konqueror\/3.([2-9])/) == null));
var domLib_canFade = (domLib_isGecko || domLib_isIE || domLib_isSafari);
var domLib_canDrawOverSelect = (domLib_isGecko || domLib_isOpera || domLib_isMac);

// -- Event Variables --
var domLib_eventTarget = domLib_isIE ? 'srcElement' : 'currentTarget';
var domLib_eventButton = domLib_isIE ? 'button' : 'which';
var domLib_eventTo = domLib_isIE ? 'toElement' : 'relatedTarget';
var domLib_stylePointer = domLib_isIE ? 'hand' : 'pointer';
// NOTE: a bug exists in Opera that prevents maxWidth from being set to 'none', so we make it huge
var domLib_styleNoMaxWidth = domLib_isOpera ? '1000000px' : 'none';
var domLib_hidePosition = '-1000px';
var domLib_scrollbarWidth = 14;
var domLib_autoId = 1;
var domLib_zIndex = 100;

// -- Detect mouse button codes --
var domLib_buttonLeft = (domLib_isGecko || domLib_isOpera || domLib_isIE || domLib_isSafari ? 1 : -1);
var domLib_buttonRight = (domLib_isGecko ? 3 : 2);

// -- Detection --
var domLib_obscuringElements;

// -- Timeouts --
var domLib_timeoutStateId = 0;
var domLib_timeoutStates = new Hash();

// -- SMBCms style constants --
var SMBCms_style_out = '';
var SMBCms_style_rollover = '_rollover';
var SMBCms_style_rolloverPressed = '_rollover_pressed';
var SMBCms_style_menuopenOut = '_menuopen';
var SMBCms_style_menuopenRollover = '_menuopen_rollover';
var SMBCms_style_menuopenPressed = '_menuopen_pressed';

// -- Plugins --
var SMBCms_actionPlugins = new Hash();
var SMBCms_transitionPlugins = new Hash();
var FloatAPI;

// -- Initialize other public variables --
// TRUE if global menu initilization has been done, FALSE otherwise
var SMBCms_globalInitDone = false;
// X,Y position of mouse on the screen.
var SMBCms_mousePosition = new Hash();
// Hash of SMBCms objects
var SMBCmsObjects = new Hash();
// Initialize last mouseover element
var SMBCms_lastMouseoverElement = '';
var SMBCms_lastMouseoverElementOwner = false;
// Active event handlers that should be cleaned on unload
var SMBCms_activeEventHandlers = [];

// -- Remove event handlers on unload (fixes IE memory leaks) --
// This code was inspired by IE7
if (window.attachEvent && domLib_isIE)
{
    window.attachEvent("onunload", function()
    {
        // Loop through all set event handlers
        while (SMBCms_activeEventHandlers.length)
        {
            var obj = SMBCms_activeEventHandlers.pop();
            try
            {
                obj[0].detachEvent(obj[1], obj[2]);
            }
            catch (e)
            {
            }
        }
    });
}

// -- Create global items container stylesheet object --
document.write('<style>'+
    'div.SMBCms_items_container {'+
        'position: absolute; top: 0px; left: 0px; visibility: hidden;'+
        'overflow: visible; margin: 0px; padding: 0px;'+
        'border: 0px transparent none; background-color: transparent;'+
        
    '}'+
    '</style>');

/**
 * Specially escapes a string so that it can be put inside of another string to
 * be evaluated. The quoteType should be either ' or ".
 * NOTE: This function is not used in this source file but it is used in some
 * plugins and is still very useful.
 */
function evalEscapeString (str, quoteType)
{
    // Set quote type, defaults to " if not already set
    var quoteType = quoteType ? quoteType : '"';
    // Escape and return string
    return 'unescape('+quoteType+escape(str)+quoteType+')';
};

/**
 * Builds one function object from the two functions passed. The function parameters of
 * the first function take precedence over the parameters of the second function.
 */
function SMBCms_buildFunction (function1, function2)
{
    // Initialize function body and parameters
    var function_body = '';
    var function_params = '';
    // Check that the first function is set; change the function
    // into a string; and parse the function.
    if (function1 && (data = SMBCms_parseFunction(function1 + '')))
    {
        function_body += data[1];
        function_params += data[0]; 
    }
    if (function2 && (data = SMBCms_parseFunction(function2 + '')))
    {
        function_body += data[1];
        function_params += (function_params && data[0] ? ',' : '') + data[0];
    }
    // Create final function
    eval('var retFunc = function ('+function_params+') {'+function_body+'};');
    // Return composite
    return retFunc;
};

/**
 * Parses a function and returns an array of [0]=function parameters, [1]=body
 */
function SMBCms_parseFunction (parseFunction)
{
    var data = ['',''];
    if (parseFunction.match(/\s*function\s*\(([^\)]*)\)\s*\{([^$]*)\}\s*/))
    {
        data[0] = RegExp.$1;
        data[1] = RegExp.$2;
    }
    else
    {
        data[1] = parseFunction;
    }
    return data;
};

/**
 * Merge two arrays. Returns the resulting array. Elements in the second array will NOT
 * replace elements in the first array, unless the third argument is "false".
 */
function SMBCms_mergeArrays (baseArray, mergeArray, override, provideIncrementalKeys)
{
    for (var i in mergeArray)
    {
        // Try to make the key into an integer
        if (parseInt(i) == i)
        {
            i = parseInt(i);
        }
        // If we can, put key from the merge array into
        // the base array.
        if (typeof(baseArray[i]) == 'undefined' || override)
        {
            baseArray[i] = mergeArray[i];
        }
        else if (provideIncrementalKeys && typeof(i) == 'number')
        {
            // The key exists in the base array and we cannot override it
            // The key is numeric, and provideIncrementalKeys is TRUE
            // So, push this element to the end of the base array.
            baseArray[baseArray.length] = mergeArray[i];
        }
    }
    return baseArray;
};

/**
 * Clones an array
 */
function SMBCms_cloneArray (ary)
{
    // Clone this array
    ary = SMBCms_mergeArrays({}, ary);
    // Clone all sub-arrays
    for (var index in ary)
    {
        if (typeof(ary[index]) == 'object')
        {
            ary[index] = SMBCms_cloneArray(ary[index]);
        }
    }
    // Return cloned array
    return ary;
};

/**
 * Set a timeout, compatible with all browsers (sometimes timeout functionality 
 * broken).
 */
function domLib_setTimeout(in_function, in_timeout, in_args)
{
    if (typeof(in_args) == 'undefined')
    {
        in_args = [];
    }
    
    // Make sure in_function is a function
    if (typeof(in_function) == 'string')
    {
        eval('var in_function = function () { '+in_function+' }');
    }

    if (in_timeout <= 0)
    {
        in_function(in_args);
        return 0;
    }

    // must make a copy of the arguments so that we release the reference
    var args = SMBCms_mergeArrays({}, in_args);

    if (!domLib_hasBrokenTimeout)
    {
        return setTimeout(function() { in_function(args); }, in_timeout);
    }
    else
    {
        var id = domLib_timeoutStateId++;
        var data = new Hash();
        data.set('function', in_function);
        data.set('args', args);
        domLib_timeoutStates.set(id, data);

        data.set('timeoutId', setTimeout('domLib_timeoutStates.get(' + id + ').get(\'function\')(domLib_timeoutStates.get(' + id + ').get(\'args\')); domLib_timeoutStates.remove(' + id + ');', in_timeout));
        return id;
    }
};

/**
 * Clear a timeout that was set with domLib_setTimeout
 */
function domLib_clearTimeout(in_id)
{
    if (!domLib_hasBrokenTimeout)
    {
        clearTimeout(in_id);
    }
    else
    {
        if (domLib_timeoutStates.has(in_id))
        {
            clearTimeout(domLib_timeoutStates.get(in_id).get('timeoutId'));
            domLib_timeoutStates.remove(in_id);
        }
    }
};

/**
 * Inspect a DOM object.
 *
 * Returns a Hash with the following values:
 *   'left': Left offset of the object from the main document.
 *   'top': Top offset of the object from the main document.
 *   'right': Right offset of the object from the main document.
 *   'bottom': Bottom offset of the object from the main document.
 *   'width': Actual width (in pixels) of the object. Useful if you
 *    haven't explicitly set the width/don't know what it is.
 *   'height': Actual height (in pixels) of the object. Useful if you
 *    haven't explicitly set the height/don't know what it is.
 *   'centerLeft': Offset from the main document to the center of the object
 *   on the X axis.
 *   'centerTop': Offset from the main document to the center of the object
 *   on the Y axis.
 *   'radius': 'height' if 'height' is greater than 'width', and vice-versa.
 */
function domLib_getOffsets (in_object, stopAt)
{
    stopAt = typeof(stopAt) == 'undefined' ? false : stopAt;
    
    var originalObject = in_object;
    var originalWidth = in_object.offsetWidth;
    var originalHeight = in_object.offsetHeight;
    var offsetLeft = 0;
    var offsetTop = 0;

    while (in_object)
    {
        offsetLeft += in_object.offsetLeft;
        offsetTop += in_object.offsetTop;
        in_object = in_object.offsetParent;
        if (stopAt == in_object)
        {   
            break;
        }
    }

    // MacIE misreports the offsets (even with margin: 0 in body{}), still not perfect
    if (domLib_isMacIE)
    {
        offsetLeft += 10;
        offsetTop += 10;
    }

    return new Hash(
        'left',         offsetLeft,
        'top',          offsetTop,
        'right',        offsetLeft + originalWidth,
        'bottom',       offsetTop + originalHeight,
        'height',       originalHeight,
        'width',        originalWidth,
        'leftCenter',   offsetLeft + originalWidth/2,
        'topCenter',    offsetTop + originalHeight/2,
        'radius',       Math.max(originalWidth, originalHeight) 
    );
};

/**
 * Check if the first argument is the child of the second
 * argument in the DOM. The first argument doesn't have to
 * be a direct child; it can be grand-child, great-grand-child,
 * etc.
 */
function domLib_isDescendantOf(in_object, in_ancestor)
{
    if (in_object == in_ancestor)
    {
        return true;
    }

    while (in_object != document.documentElement)
    {
        try
        {
            if ((tmp_object = in_object.offsetParent) && tmp_object == in_ancestor)
            {
                return true;
            }
            else if ((tmp_object = in_object.parentNode) == in_ancestor)
            {
                return true;
            }
            else
            {
                in_object = tmp_object;
            }
        }
        // in case we get some wierd error, just assume we haven't gone out yet
        catch(e)
        {
            return true;
        }
    }

    return false;
};

/**
 * Gets the X, Y coordinates of the mouse for a particular event object.
 * Base code from PPK's article at evolt.
 */
function domLib_getEventPosition(in_eventObj)
{
    var eventPosition = new Hash('x', 0, 'y', 0);

    // Opera and Konq both offer event properties that give the
    // total offset from top of document to current scroll offset
    if (domLib_isKonq)
    {
        eventPosition.set('x', in_eventObj.x);
        eventPosition.set('y', in_eventObj.y);
    }
    // IE varies depending on standard compliance mode
    else if (domLib_isIE)
    {
        if (domLib_standardsMode)
        {
            eventPosition.set('x', in_eventObj.clientX + document.documentElement.scrollLeft);
            eventPosition.set('y', in_eventObj.clientY + document.documentElement.scrollTop);
        }
        // NOTE: just in case we fire too early, go ahead and check for document.body
        else if (document.body)
        {
            eventPosition.set('x', in_eventObj.clientX + document.body.scrollLeft);
            eventPosition.set('y', in_eventObj.clientY + document.body.scrollTop);
        }
    }
    else
    {
        eventPosition.set('x', in_eventObj.pageX);
        eventPosition.set('y', in_eventObj.pageY);
    }

    return eventPosition;
};

/**
 * Hides all SELECT objects that are overlapping the passed object.
 * Optionally shows the hidden select objects if the second argument
 * is true (boolean).
 */
function domLib_detectCollisions(in_object, in_recover)
{
    if (typeof(domLib_obscuringElements) == 'undefined')
    {
        // If we can draw over select objects, like we can in at least Firefox
        // and Opera, don't hide selects
        domLib_obscuringElements = domLib_canDrawOverSelect
            ? new Hash()
            : document.getElementsByTagName('select');
        // Get all <applet>s in document
        domLib_obscuringElements = SMBCms_mergeArrays(domLib_obscuringElements,
            document.getElementsByTagName('applet'), false, true);
        // Get all <object> tags in document. Flash.
        domLib_obscuringElements = SMBCms_mergeArrays(domLib_obscuringElements,
            document.getElementsByTagName('object'), false, true);
    }

    // If we're supposed to, unhide all hidden <applet>s, <select>s, and <object>s
    if (in_recover)
    {
        // Loop through all elements
        for (var cnt = 0; cnt < domLib_obscuringElements.length; ++cnt)
        {
            var thisObscuringElement = domLib_obscuringElements[cnt];

            // If this obscuring element does not have a Hash of objects
            // it's obscuring, create a new one
            if (!thisObscuringElement.hideList)
            {
                thisObscuringElement.hideList = new Hash();
            }

            // Remove the current object's ID from this obscuring element's hide list
            thisObscuringElement.hideList.remove(in_object.id);
            // If this element is not obscuring any objects, show it
            if (!thisObscuringElement.hideList.length)
            {
                domLib_obscuringElements[cnt].style.visibility = 'visible';
                if (domLib_isKonq)
                {
                    domLib_obscuringElements[cnt].style.display = '';
                }
            }
        }

        return;
    }

    // okay, we have an object, so hunt and destroy
    var objectOffsets = domLib_getOffsets(in_object);

    // Loop through all obscuring elements
    for (var cnt = 0; cnt < domLib_obscuringElements.length; ++cnt)
    {
        var thisObscuringElement = domLib_obscuringElements[cnt];

        // If the obscuring element is in the object, then don't hide this
        // obscuring element.
        // :WARNING: is this too costly?
        if (domLib_isDescendantOf(thisObscuringElement, in_object))
        {
            continue;
        }

        // If this obscuring element does not have a Hash of objects
        // it's obscuring, create a new one
        if (!thisObscuringElement.hideList)
        {
            thisObscuringElement.hideList = new Hash();
        }

        var selectOffsets = domLib_getOffsets(thisObscuringElement); 
        var center2centerDistance = Math.sqrt(Math.pow(selectOffsets.get('leftCenter') - objectOffsets.get('leftCenter'), 2) + Math.pow(selectOffsets.get('topCenter') - objectOffsets.get('topCenter'), 2));
        var radiusSum = selectOffsets.get('radius') + objectOffsets.get('radius');
        // the encompassing circles are overlapping, get in for a closer look
        if (center2centerDistance < radiusSum)
        {
            if (
                // DOM object is left of selects/applets/objects
                objectOffsets.get('right') < selectOffsets.get('left') ||
                // DOM object is right of selects/applets/objects
                objectOffsets.get('left') > selectOffsets.get('right') ||
                // DOM object is above selects/applets/objects
                objectOffsets.get('bottom') < selectOffsets.get('top') ||
                // DOM object is below selects/applets/objects
                objectOffsets.get('top') > selectOffsets.get('bottom'))
            {
                // Select/applet/object is not obscuring DOM object
                thisObscuringElement.hideList.remove(in_object.id);
                if (!thisObscuringElement.hideList.length)
                {
                    thisObscuringElement.style.visibility = 'visible';
                    if (domLib_isKonq)
                    {
                        thisObscuringElement.style.display = '';
                    }
                }
            }
            else
            {
                // Select/applet/object is obscuring DOM object, hide it now
                thisObscuringElement.hideList.set(in_object.id, true);
                thisObscuringElement.style.visibility = 'hidden';
                if (domLib_isKonq)
                {
                    thisObscuringElement.style.display = 'none';
                }
            }
        }
    }
};

/**
 * This returns a DOM object with the passed ID.
 */
function SMBCms_getElement (elementId)
{
    return document.all ? document.all[elementId] : document.getElementById(elementId);
};

/**
 * InterCaps a string. Removes dashes, and uppercases the letter
 * directly after. This is useful for CSS. Examples:
 * -khtml-opacity = KhtmlOpacity
 * -moz-opacity = MozOpacity
 * background-color = backgroundColor
 * opacity = opacity
 */
function interCap (str)
{
    while (str.match(/\-(.)/))
    {
        // Character directly after the dash
        var c = RegExp.$1;
        // Intercap it
        str = str.replace('-'+c, c.toUpperCase());
    }
    return str;
};

/**
 * Takes a CSS string and returns an array of key:value pairs.
 */
function parseCssString (cssString)
{
    var css = {};

    // Split string into chunks
    var chunks = cssString.split(/\s*\;\s*/);
    // Loop through chunks
    for (var k in chunks)
    {
        // v will be something like "border-color: red"
        var v = chunks[k];
        if (v.match(/^\s*([a-zA-Z0-9\-]*)\s*\:\s*(.*)$/))
        {
            css[RegExp.$1] = RegExp.$2;
        }
    }
    return css;
};

/**
 * Cancels event bubbling of the passed event object.
 */
function domLib_cancelBubble(in_event)
{
    if (in_event)
    {
        in_event.cancelBubble = true;
    }
};

/**
 * Attaches event listener to an object, and works across most (if not all) browsers.
 */
function SMBCms_attachEventListener (eventObject, eventName, eventHandler)
{
    // IE/Opera
    if (eventObject.attachEvent)
    {
        eventObject.attachEvent('on'+eventName, eventHandler);
        // IE has memory leaks when assigning event handlers so we
        // keep track of all assigned event handlers and destroy them later
        SMBCms_activeEventHandlers[SMBCms_activeEventHandlers.length] = [eventObject, 'on'+eventName, eventHandler];
    }
    // Gecko
    else if (eventObject.addEventListener)
    {
        eventObject.addEventListener(eventName, eventHandler, false);
    }
    // Other
    else
    {
        eventObject['on'+eventName] = SMBCms_buildFunction(eventHandler, eventObject['on'+eventName]);
    }
}

/**
 * The core SMBCms function/object/class/API.
 * The generated SMBCms object can be retrieved by a call to
 * getSMBCmsObject(containerId);
 */
function SMBCms (containerId, menuData)
{
    // Make this SMBCms object easily accessible
    SMBCmsObjects.set(containerId, this);
    
    // Make sure we have a valid container
    var container = SMBCms_getElement(containerId);
    if (!container)
    {
        container = document.createElement('div');
        container.id = containerId;
        document.body.appendChild(container);
    }
    
    // Save the container ID and container object
    this.container = container;
    this.containerId = containerId;
    // Intialize the item hash evaluation path cache
    this.hashEvalPathCache = new Hash();
    // Initialize menu
    this.initialize(menuData);
    
    // Return the new SMBCms object :)
    return this;
};

/**
 * Set attributes for a new menu.
 */
SMBCms.prototype.initialize = function (menuData)
{
    // Clone and save the menu data
    this.menuData = hashify(SMBCms_cloneArray(menuData));
    // Initialize the open menu data hash
    this.openMenuData = new Hash();
    // Initialize this menu's zIndex
    domLib_zIndex = (this.menuData.has('zIndex') ? this.menuData.get('zIndex') : domLib_zIndex);
    // Initialize the mouseover status hash
    this.mouseoverStatuses = new Hash();
    // Visibility of SUBITEM elements populated by the toggleVisibility
    // method.
    this.menuOpenStatuses = new Hash();
    // Timeout object for the menu cleaning loop
    this.cleanMenusLoopTimeout = false;
    // Timeout object for the timeout call while cleaning menus; this
    // checks if individual menus can be hidden.
    this.cleanMenuTimeout = false;
    // Used for the above timeout; the menu ID that is being checked.
    this.cleanMenuId = false;
    // First closeable key of menu
    this.firstCloseableKey = false;
    // Timeout ID Hash for showMenu timeouts.
    this.showTimeoutHash = new Hash();

    // Set 'defaultMeasurementUnit', and default to pixels
    this.menuData.set('defaultMeasurementUnit', this.menuData.has('defaultMeasurementUnit') ? this.menuData.get('defaultMeasurementUnit') : 'px');

    // Set "loading" message
    this.container.innerHTML = '<div style="visibility: visible;" id="SMBCms_loading_' + this.container.id + '">A SMBCms menu is loading; please wait!</div>';
    // Set container styles
    var styles = ['position', 'top', 'left', 'width', 'height'];
    for (var i = 0; styles[i]; ++i)
    {
        // Set CSS style
        // If the variable isn't set we may get an error?
        var style = styles[i];
        var value = this.menuData.get(style);
        if (i != 'position')
        {
            this.container.style[style] = this.formatUnit(value);
        }
        else
        {

            this.container.style[style] = value;
        }
    }
    // Set background color to transparent
    this.container.style.backgroundColor = 'transparent';
    // Set visibility and overflow to visible
    this.container.style.visibility = this.container.style.overflow = 'visible';
    // Set zIndex
    this.container.style.zIndex = domLib_zIndex++;
    
    // Make sure expand_menu and contract_menu are automatically set
    this.menuData.set('expand_menu', (this.menuData.has('expand_menu') ? this.menuData.get('expand_menu') : 'auto'));
    this.menuData.set('contract_menu', (this.menuData.has('contract_menu') ? this.menuData.get('contract_menu') : 'none'));
    
    // This variable will be used in global initialization (see block below)
    // It might be modified if there is a right or left-click context menu; if there
    // is, all menus should NOT be closed when the document is clicked.
    var click_closes_all_menus = true;
            
    // IS THIS A FLOATING MENU?
    // If it is, set it up to float
    // It's either floating menus or context menus; never both
    if (this.menuData.get('float') && FloatAPI)
    {
        this.floatHandler = new FloatAPI(container.id, this.menuData.get('top'),
            this.menuData.get('left'), this.menuData.get('floatSpeed'), true);
    }
    
    // IS THIS A CONTEXT MENU?
    // If it is, set code that will open the submenu...
    else if (this.menuData.get('expand_menu').match(/context\(([a-zA-Z\-]*)\,([a-zA-Z0-9\-\_]*)\)/))
    {
        // Get context menu options data
        // Get expand type
        var expandType = RegExp.$1;
        // Get context object
        var contextObjectId = RegExp.$2;
        
        // Now get the context object
        var contextObject;
        if (contextObjectId == 'document')
        {
            contextObject = document;
        }
        else
        {
            contextObject = SMBCms_getElement(contextObjectId);
            // Make sure contextObject is a valid object
            if (!contextObject)
            {
                contextObject = document;
            }
        }
        
        // Create the function code
        var code_end = // Cancel event bubble
                'domLib_cancelBubble(in_event);' +
                // NOTICE: we have to do this timeout or else
                // menus are immediately closed, why?
                'domLib_setTimeout(function()' +
                '{' +
                    // Get SMBCms object
                    // The next line needs to be an eval for Opera :/
                    'eval("var obj = getSMBCmsObject(\\"' + this.containerId + '\\");");' +
                    // Set menu at cursor...
                    'obj.setMenuAtCursor();' +
                    // Show submenu
                    'obj.showMenu(obj.menuData, "");' +
                    // Return false so the event doesn't bubble
                    'return false;' +
                '},10);' +
                'return false;' +
            '}';

        // Now get "event" handler name
        // MAC KEYS (boolean): altKey, ctrlKey, shiftKey, metaKey
        var contextEvent;
        var eventButton;
        var contextFunctionCode_oncontext = function (in_event) {};
        var modifierKeys = '';
        
        // We have to see how the menu is supposed to open.
        // On left-click?
        if (expandType == 'left-click')
        {
            contextEvent = 'mouseup';
            eventButton = domLib_buttonLeft;
        }
        // Is this a right-click/control-click context menu?
        else if (expandType == 'context')
        {
            contextEvent = (domLib_isIE ? 'contextmenu' : 'mouseup');
            eventButton = (domLib_isIE ? 0 : domLib_buttonRight);

            // If browser is on a Macintosh, we have to adapt for the control-clicking scheme.
            // I still can't get Safari or Opera to work, but Firefox does fine.
            if (domLib_isMac)
            {
                eventButton = domLib_buttonLeft;
                modifierKeys = '|| !in_event.ctrlKey';
            }
            
            // We need special code for oncontextmenu on IE
            eval('var contextFunctionCode_oncontext = function (in_event) { domLib_cancelBubble(in_event); }');
        }
        // For anything else, use mouseover
        else
        {
            contextEvent = 'mouseover';
        }

        // Set final event code
        eval('var contextFunctionCode = function (in_event) {' +
            // Check for event button?
            (eventButton || modifierKeys
                ? 'if (in_event[domLib_eventButton] != '+eventButton+modifierKeys+') { return; }'
                : ''
            ) +
            code_end
        );
        
        // Now set the submenu opener code
        SMBCms_attachEventListener(contextObject, contextEvent, contextFunctionCode);
        if (!domLib_isIE)
        {
            SMBCms_attachEventListener(contextObject, 'contextmenu', contextFunctionCode_oncontext);
        }

        // Set styles of top-level submenu
        this.menuData.set('position', 'absolute');
        container.style.position = 'absolute';
    }
    
    // If the global menu initializations haven't been done yet,
    // do them now.
    if (!SMBCms_globalInitDone)
    {
        // Set code to capture the mouse position onscreen
        var mousemoveCode = function (in_event)
        {
            // Make sure we have a valid event (mainly for IE)
            // It may not pass us the event object; instead it
            // sets the global variable "event".
            var in_event = in_event ? in_event : window.event;
            SMBCms_mousePosition = domLib_getEventPosition(in_event);
            
            // Loop through any plugin callback functions
            if (SMBCms_actionPlugins.has('document_mousemove'))
            {
                for (var index in SMBCms_actionPlugins.get('document_mousemove').elementData)
                {
                    SMBCms_actionPlugins.get('document_mousemove').get(index)(in_event);
                }
            }
        };
        
        // Set document.onmousedown and onblur code so all submenus in all menus are
        // hidden
        var code_mousedown = function (in_event)
        {
            // Sometimes errors occur if this function has not yet loaded (Mozilla)
            if (SMBCms_hideAllMenus)
            {
                SMBCms_hideAllMenus(true);
            }
            
            // Loop through any plugin callback functions
            if (SMBCms_actionPlugins.has('document_click'))
            {
                for (var index in SMBCms_actionPlugins.get('document_click').elementData)
                {
                    SMBCms_actionPlugins.get('document_click').get(index)(in_event);
                }
            }
        };
        var code_blur = function (in_event)
        {
            // Sometimes errors occur if this function has not yet loaded (Mozilla)
            if (SMBCms_hideAllMenus)
            {
                SMBCms_hideAllMenus();
            }
            
            // Loop through any plugin callback functions
            if (SMBCms_actionPlugins.has('document_blur'))
            {
                for (var index in SMBCms_actionPlugins.get('document_blur').elementData)
                {
                    SMBCms_actionPlugins.get('document_blur').get(index)(in_event);
                }
            }
        };
        
        // Now assign actions to objects
        SMBCms_attachEventListener(document, 'mousedown', code_mousedown);
        SMBCms_attachEventListener(document, 'blur', code_blur);
        SMBCms_attachEventListener(document, 'mousemove', mousemoveCode);
                
        // We've initialized SMBCms globally for all future menus and this
        // one; make sure it doesn't happen again
        SMBCms_globalInitDone = true;
    }
    
    // If we are supposed to, automatically show the base menu.
    if (this.menuData.get('expand_menu').match('auto'))
    {
        this.showMenu(this.menuData, "");
    }
    
    // Remove the loading message
    SMBCms_getElement('SMBCms_loading_' + this.container.id).style.visibility = 'hidden';
    SMBCms_getElement('SMBCms_loading_' + this.container.id).innerHTML = '';
};

/**
 * Set the menu on the cursor.
 */
SMBCms.prototype.setMenuAtCursor = function ()
{
    // Get positions
    var topPos = SMBCms_mousePosition.get('y') + parseInt(this.menuData.get('top'));
    var leftPos = SMBCms_mousePosition.get('x') + parseInt(this.menuData.get('left'));
    
    // Set positions for object
    SMBCms_getElement(this.containerId).style.top = topPos + 'px';
    SMBCms_getElement(this.containerId).style.left = leftPos + 'px';
};

/**
 * Build an item.
 */
SMBCms.prototype.buildMenu = function (parentItemHash)
{
    // Uncomment this to get build time
//  var start_time = (new Date()).getTime();

    // Get item ID
    var itemId = parentItemHash.get('itemId') || this.containerId;
        
    //
    // -- Build items container --
    //
    
    // Get the items hash
    var itemsHash = parentItemHash.get('items');
    
    // Get parent element ID where we will insert this node
    var parentItemsObj = SMBCms_getElement(
        parentItemHash.get('parentId')
            ? parentItemHash.get('parentId') + '_subitems'
            : this.containerId);
    
    // Create the item container
    var itemsContainer = document.createElement('div');
    itemsContainer.id = itemId + '_subitems';
    itemsContainer.className = 'SMBCms_items_container';
    
    // Set initial positions
    itemsContainer.style.top = this.formatUnit(itemsHash.get('top'));
    itemsContainer.style.left = this.formatUnit(itemsHash.get('left'));

    // Now add positions of parent item as margins
    // I don't really like doing it this way, because the parent elements MAY have
    // positions set in units other than pixels - doing it this way makes the menu potentially
    // look weird if the window is resized... but, is there any other way? Since we don't
    // just have to jump to the position of the parent item, we also have to jump to the
    // position of the parent item's background item, I don't see any other way...
    if (!itemsHash.has('useOldPositioning') && !this.menuData.has('useOldPositioning'))
    {
        var parentBgItemObj;
        if (parentBgItemObj = SMBCms_getElement(parentItemHash.get('parentId')+'_subitems_background'))
        {
            var parentItemObj = SMBCms_getElement(itemId+'_item_container');
            itemsContainer.style.marginTop = this.formatUnit(parentBgItemObj.offsetTop + parentItemObj.offsetTop);  
            itemsContainer.style.marginLeft = this.formatUnit(parentBgItemObj.offsetLeft + parentItemObj.offsetLeft);  
        }
    }

    // Set events for item container
    var getSMBCmsObjectCode = 'getSMBCmsObject(\''+this.containerId+'\')';
    eval('var onmouseoverCode = function () {'+getSMBCmsObjectCode+'.setMouseoverStatus(\''+itemId+'_subitems\', true, false, false);}');
    eval('var onmouseoutCode =  function () {'+getSMBCmsObjectCode+'.setMouseoverStatus(\''+itemId+'_subitems\', false, false, false);}');
    SMBCms_attachEventListener(itemsContainer, 'mousemove', onmouseoverCode);
    SMBCms_attachEventListener(itemsContainer, 'mouseout', onmouseoutCode);

    // Add HTML of all items to the subitem container
    var bg_id = itemId + '_subitems_background';
    var scrollarea_id = itemId + '_subitems_scrollarea';
    var top = '', left = '', width = '', height = '', className = '', css ='';
    var bgItemSet = false;
    if (itemsHash.has('background-item'))
    {
        bgItemSet = true;
        var bg_item = itemsHash.get('background-item');
        top = bg_item.has('top') ? 'top: ' + this.formatUnit(bg_item.get('top')) + ';' : ''; 
        left = bg_item.has('left') ? 'left: ' + this.formatUnit(bg_item.get('left')) + ';' : ''; 
        className = bg_item.has('class') ? ' class="'+bg_item.get('class')+'"' : ''; 
        css = bg_item.has('css') ? bg_item.get('css') + ';' : '';

        // Width and height operations
        if (bg_item.has('width'))
        {
            width = 'width: ' + this.formatUnit(bg_item.get('width')) + ';';
            itemsContainer.style.width = this.formatUnit(bg_item.get('width'));
        }
        if (bg_item.has('height'))
        {
            height = 'height: ' + this.formatUnit(bg_item.get('height')) + ';';
            itemsContainer.style.height = this.formatUnit(bg_item.get('height'));
        }
    }

    //
    // -- Build scroll area --
    // If there is are scroll area attributes set...
    var scrollarea_css = 'top:0px;left:0px;width:0px;height:0px;overflow:visible;';
    var scrollarea_innerhtml = '&nbsp;';
    if (itemsHash.has('scroll-area'))
    {
        // Get scroll config
        var scrollerItemsHash = itemsHash.get('scroll-area');
        
        // Get scrolling type
        // (not needed/used currently)
//      var scrollType = scrollerItemsHash.get('scroll-type');
        
        // Switch to scroll type
//      switch (scrollType)
//      {
            // -- Native scrolling --
//          case 'native':
//          default:
                scrollarea_css = 'overflow:auto;' +
                    'top:' + this.formatUnit(scrollerItemsHash.get('top')) + ';' +
                    'left:' + this.formatUnit(scrollerItemsHash.get('left')) + ';' +
                    'width:' + this.formatUnit(scrollerItemsHash.get('visible-width')) + ';' +
                    'height:' + this.formatUnit(scrollerItemsHash.get('visible-height')) + ';';
//              break;
//      }
    }

    // Build item container, background item, and scroller  
    SMBCms_preloadImages('pic/0.gif');
    var itemsContainerHTML =
        '<div id="'+bg_id+'" style="background-color:transparent;background-image:url(http://www.jportal.info/blank_img.gif);border-style:none;margin:0px;padding:0px;visibility:inherit;position:absolute;'+top+left+width+height+';z-index:'+(++domLib_zIndex)+';">' +
            '<table style="background-color:transparent;background-image;url(http://www.jportal.info/blank_img.gif);border-style:none;margin:0px;padding:0px;width:100%;height:100%;background-color:transparent;visibility:inherit;" cellpadding="0" cellspacing="0"><tbody><tr>' +
                '<td '+className+' style="visibility:inherit;line-height:1px;'+css+'">' +
                    '&nbsp;' +
                    '<div id="'+scrollarea_id+'" style="margin:0px;padding:0px;visibility:inherit;position:absolute;'+scrollarea_css+'">' +
                        scrollarea_innerhtml; // ... to be continued... at the end of this function         
    
    //
    // -- Build items --
    //
        
    // Last item positions
    var marginTop = 0, marginLeft = 0;
    var lastItemHash = new Hash('top', 0, 'left', 0);
    
    // Loop through all the items in the hash of items
    for (var itemIndex = 1; itemsHash.has(itemIndex); ++itemIndex)
    {
        // Get item ID
        var thisItemId = itemId + '_' + itemIndex;
        // Get item hash
        var itemHash = itemsHash.get(itemIndex);
        // Initialize item attributes
        this.initializeItemAttributes(itemHash, thisItemId);

        // Call any plugins needed...
        if (SMBCms_actionPlugins.has('item_init'))
        {
            for (var index in SMBCms_actionPlugins.get('item_init').elementData)
            {
                SMBCms_actionPlugins.get('item_init').get(index)(false, this, itemHash);
            }
        }
        
        // Make sure the item has top and left positions, calculate item offsets (as
        // margins) and set top and left positions to offsets if needed
        // Get top positions
        if (!itemHash.has('top'))
        {
            marginTop += parseFloat(lastItemHash.get('top'));
            itemHash.set('top', (itemHash.has('offsetTop') ? itemHash.get('offsetTop') : 0));
        }
        else
        {
            marginTop = 0;
        }

        // Get left positions
        if (!itemHash.has('left'))
        {
            marginLeft += parseFloat(lastItemHash.get('left'));
            itemHash.set('left', (itemHash.has('offsetLeft') ? itemHash.get('offsetLeft') : 0));
        }
        else
        {
            marginLeft = 0;
        }
        
        // Get cursor and correct it
        // If it is hand or pointer, make sure that in IE it's 'hand' and in
        // gecko browsers it's 'pointer'
        var cursor = (itemHash.has('cursor')
            ? 'cursor:' + (itemHash.get('cursor').match(/hand|pointer/)
                ? domLib_stylePointer
                : itemHash.get('cursor')) + ';'
            : '');

        // Get user-defined CSS
        var css = (itemHash.has('css') ? itemHash.get('css') + ';' : '') + cursor;
        
        // Get item class
        var itemClass = (itemHash.has('class') ? ' class="' + itemHash.get('class') + '"' : '');
            
        // Populate item CSS
        var div_css = 'visibility:inherit; position:absolute; margin:0px; padding:0px;'+
            'margin-top:'+this.formatUnit(marginTop)+'; margin-left:'+this.formatUnit(marginLeft)+'; '+
            'top:'+this.formatUnit(itemHash.get('top'))+';'+
            'left:'+this.formatUnit(itemHash.get('left'))+';'+
            'width:'+this.formatUnit(itemHash.get('width'))+';'+
            'height:'+this.formatUnit(itemHash.get('height'))+';';
            
        // Build event code
        var event_code = ' onmouseover="'+getSMBCmsObjectCode+'.mouseAction(event,\'mouseover\',\''+thisItemId+'\');"' +
            ' onmouseout="'+getSMBCmsObjectCode+'.mouseAction(event,\'mouseout\',\''+thisItemId+'\');"' + 
            ' onclick="'+getSMBCmsObjectCode+'.mouseAction(event,\'click\',\''+thisItemId+'\');"' +
            ' onmouseup="'+getSMBCmsObjectCode+'.mouseAction(event,\'mouseup\',\''+thisItemId+'\');"' +
            ' onmousedown="'+getSMBCmsObjectCode+'.mouseAction(event,\'mousedown\',\''+thisItemId+'\');"';
            
        // Create item
        var item_container_id = thisItemId+'_item_container';
        itemsContainerHTML +=
            // Build table/div
            '<div id="'+item_container_id+'" style="'+div_css+'" '+event_code+'><table style="width:100%;height:100%;background-color:transparent;" cellpadding="0" cellspacing="0"><tbody><tr><td id="'+thisItemId+'_item"'+itemClass+' style="'+css+'">' +
                itemHash.get('content') +
            '</td></tr></tbody></table></div>';
        
        // -- Save last item hash --
        lastItemHash = itemHash;
    }
    
    // Set innerHTML of items container
    itemsContainer.innerHTML = itemsContainerHTML + '</div></td></tr></tbody></table></div>';

    // Append items container to DOM
    parentItemsObj.appendChild(itemsContainer);
    
    // If the items container doesn't have a height/width set, try to get it automatically..
    // this is to prevent weird bugs where the onmouseout/onmouseover code of the items
    // container doesn't ever execute
    if (!bgItemSet)
    {
        var itemsContainer = SMBCms_getElement(itemId+'_subitems');

        // Get extreme item positions: (L)eft, (R)ight, (T)op, (B)ottom.
        var extremeL = 99999999999999;
        var extremeR = 0;
        var extremeT = extremeL;
        var extremeB = 0;
        for (var i = 1; itemsHash.has(i); ++i)
        {
            var itemOffsets = domLib_getOffsets(SMBCms_getElement(itemId+'_'+i+'_item_container'));

            extremeL = (itemOffsets.get('left') < extremeL) ? itemOffsets.get('left') : extremeL;  
            extremeR = (itemOffsets.get('right') > extremeR) ? itemOffsets.get('right') : extremeR;
            extremeT = (itemOffsets.get('top') < extremeT) ? itemOffsets.get('top') : extremeT;  
            extremeB = (itemOffsets.get('bottom') > extremeB) ? itemOffsets.get('bottom') : extremeB;
        }
        
        // Now set dimensions of item container and background item
        SMBCms_getElement(bg_id).style.height = itemsContainer.style.height = (extremeB - extremeT) + 'px';
        SMBCms_getElement(bg_id).style.width = itemsContainer.style.width = (extremeR - extremeL) + 'px';
    }
        
    // Uncomment to get build time
//  var stop_time = (new Date()).getTime();
//  alert('Build time: ' + (stop_time - start_time) + 'ms.');
};

/**
 * Rebuild an existing menu, from scratch
 */
SMBCms.prototype.rebuildMenu = function (newMenuData)
{
    this.initialize(newMenuData);
};

/**
 * Sets the mouseover status of an element. The mouseover status can be
 * retrieved using the getMouseoverStatus method.
 */
SMBCms.prototype.setMouseoverStatus = function (elementId, status, setAsLast, executeLastElementMouseout)
{
    // Set last mouseover to "false" since if we're over a different item,
    // we're not over the last one now. Also execute it's onmouseout code.
    if (executeLastElementMouseout && SMBCms_lastMouseoverElement && SMBCms_lastMouseoverElement != elementId)
    {
        // Set mouseover status to false
        SMBCms_lastMouseoverElementOwner.mouseoverStatuses.set(SMBCms_lastMouseoverElement, false);

        // Execute onmouseout code
        SMBCms_lastMouseoverElementOwner.mouseAction(0, 'mouseout', SMBCms_lastMouseoverElement);

        // Kill all menu cleaning threads
        for (var i = this.openMenuData.length - 1; this.openMenuData.has(i); i--)
        {
            var itemHash = this.openMenuData.get(i);
            if (itemHash.cleanThread)
            {
                domLib_clearTimeout(itemHash.cleanThread);
            }
        }
    }

    // Set last mouseover ID and element
    if (setAsLast)
    {
        SMBCms_lastMouseoverElement = elementId;
        SMBCms_lastMouseoverElementOwner = this;
    }
        
    // Set current mouseover status
    this.mouseoverStatuses.set(elementId, status);
};

/**
 * Returns the current mouseover status of the element with the passed
 * ID.
 */
SMBCms.prototype.getMouseoverStatus = function (elementId)
{
    return this.mouseoverStatuses.has(elementId) ? this.mouseoverStatuses.get(elementId) : false;
};

/**
 * Checks whether a menu can be automatically hidden or not. If the menu does not
 * have "rollout" in it's contract_menu data hash parameter, or if the
 * mouse is over the menu, then it returns false.
 */
SMBCms.prototype.canHideMenu = function (itemHash)
{
    // Now get the item ID that we use
    var itemId = itemHash.get('itemId');
    
    // Can the menu be closed at all? If contract_menu
    // is not set or "rollout" is not in it, it can't be
    var contractMenu = itemHash.has('contract_menu') ? itemHash.get('contract_menu') : false;
    if (!contractMenu && itemId != this.containerId && itemId)
    {
        return true;
    }
    if (contractMenu && (!contractMenu.match(/rollout/) || contractMenu.match(/none/)))
    {
        return false;
    }
    
    // Now we know that we can close this menu, but we shouldn't if the mouse is over
    // the items or subitems
    if (this.getMouseoverStatus(itemId))
    {
        return false;
    }
    
    // Is the mouse over the background item, or parent item container?
    if (itemHash.has('parentId') && this.getMouseoverStatus(itemHash.get('parentId') + '_subitems'))
    {
        return false;
    }
    
    // Look to see if any subitems are moused over...
    // NOTE: I want to remove this code but it breaks context menu examples. Is the
    // onmouseover event not bubbling..?
    if (itemHash.has('items') && this.menuData.get('expand_menu').match('context'))
    {
        for (var i = 1; itemHash.get('items').has(i); ++i)
        {
            if (!this.canHideMenu(itemHash.get('items').get(i)))
            {
                return false;
            }
        }
    }
    
    // We can hide this menu
    return true;
};

/**
 * Initialize an item's attributes, like itemId, parentItemId, etc.
 */
SMBCms.prototype.initializeItemAttributes = function (itemHash, itemId)
{
    // Remove container ID from item ID
    var bareItemId = itemId.replace(this.containerId + '_', '');
    // Get menu level
    itemHash.set('menuLevel', (bareItemId ? bareItemId.split('_').length : 0));
    // Get parent ID
    itemHash.set('parentId', (itemId.match(/^(.*)\_([0-9]+)$/) ? RegExp.$1 : ''));
    // Set item and container IDs in the item hash
    itemHash.set('itemId', itemId);

    // Set beginning styles
    itemHash.set('currentStyleContent', '');
    itemHash.set('currentStyleCSS', '');
    itemHash.set('currentStyleClass', '');
    
    // Set defaults for this item hash
    itemHash.set('expand_menu', (itemHash.has('expand_menu') ? itemHash.get('expand_menu') : 'rollover'));
    itemHash.set('contract_menu', (itemHash.has('contract_menu') ? itemHash.get('contract_menu') : 'rollout'));
    itemHash.set('closeDelay', (itemHash.has('closeDelay') ? itemHash.get('closeDelay') : 100));
};

/**
 * Get an item hash corresponding to the passed item ID.
 * Item ID is in this format: "1_1_2_3"
 */
SMBCms.prototype.getItemHash = function (itemId)
{
    // Get item ID
    var itemId = (this.containerId == itemId ? '' : itemId);

    // Remove container ID from item ID
    var bareItemId = itemId.replace(this.containerId + '_', '');

    // If this item hash evaluation path has not been cached,
    // create it.
    if (!this.hashEvalPathCache.has(bareItemId))
    {
        var evalCode = 'var itemHash = this.menuData';
        if (itemId != '')
        {
            var chunks = bareItemId.split('_');
            for (var i = 0; i < chunks.length; ++i)
            {
                evalCode += '.get("items")' + (parseInt(chunks[i]) == chunks[i] ? '.get('+chunks[i]+')' : '');
                if (parseInt(chunks[i]) != chunks[i])
                {
                    break;
                }
            }
        }
        evalCode += ';';
        
        // Save (cache) eval code
        this.hashEvalPathCache.set(bareItemId, evalCode);
    }
    
    // Evaluate cached eval code
    eval(this.hashEvalPathCache.get(bareItemId));
    
    // Return item hash
    return itemHash;
};

/**
 * Checks whether a menu is open or not.
 */
SMBCms.prototype.menuIsOpen = function (itemId)
{
    // Basically, we'll just look at the hash of 
    // visible elements populated by the toggleVisibility
    // method. We can't directly look at the CSS visibility
    // property because an element may be transitioning out,
    // in which case the menu is closed but still visible.
    return (this.menuOpenStatuses.has(itemId) ? this.menuOpenStatuses.get(itemId) : false);
};

/**
 * Hides menus on or above the level of the passed item hash.
 */
SMBCms.prototype.hideMenusOnLevel = function (itemHash)
{
    var menuLevel = itemHash.get('menuLevel');
    var itemId = itemHash.get('itemId');

    // If the item hash level is above 0, continue
    if (itemHash.get('menuLevel') > 0)
    {
        // Loop through the open menus
        for (var tmp_key = (this.openMenuData.size()-1); tmp_key >= 0; tmp_key--)
        {
            // Get menu data
            var tmp_itemHash = this.openMenuData.get(tmp_key);
            var tmp_menuLevel = tmp_itemHash.get('menuLevel');
            var tmp_itemId = tmp_itemHash.get('itemId');

            // If the open menu meets certain criteria, hide it
            if (tmp_menuLevel >= menuLevel && !tmp_itemId.match(itemId))
            {
                this.hideMenu(tmp_itemHash);
            }
        }
    }
};
 
/**
 * Shows a menu's submenu. Builds the submenu if needed.
 */
SMBCms.prototype.showMenu = function (itemHash, itemId)
{
    // Uncomment this to get show time
//  var start_time = (new Date()).getTime();

    // Close all menus from other SMBCms objects
    SMBCms_hideAllMenus(false, this.containerId);
    
    // Close all menus on/above this level
    this.hideMenusOnLevel(itemHash);
    
    // If there are no subitems, stop now
    if (!itemHash.has('items'))
    {
        return;
    }
    
    //  Set menu open status
    this.menuOpenStatuses.set(itemId, true);
    
    // Get key for this Item in the hash of open menus
    var thisMenuOpen = false;
    var thisArrayId = this.openMenuData.find(itemHash) || -1;
    if (thisArrayId != -1)
    {
        thisMenuOpen = true;
    }
    
    // Get Item Or Container ID
    var itemOrContainerId = itemId || this.containerId;

    // Build menu
    if (!SMBCms_getElement(itemOrContainerId + '_subitems'))
    {
        this.buildMenu(itemHash);
    }

    // Increase the z-index
    SMBCms_getElement(this.containerId).style.zIndex = domLib_zIndex++;
    SMBCms_getElement(itemOrContainerId + '_subitems').style.zIndex = domLib_zIndex++;
    
    // The menu has not been opened yet.
    // Assign it a new hash key.
    if (thisMenuOpen == false)
    {
        // Clear menu cleaning timeouts
        if (this.cleanMenuTimeout)
        {
            domLib_clearTimeout(this.cleanMenuTimeout);
            this.cleanMenuTimeout = false;
        }

        // Get new array ID
        thisArrayId = this.openMenuData.size();

        // Change style to open
        if (itemId)
        {
            this.setItemStyle(itemHash, SMBCms_style_menuopenOut);
        }
        else if (itemHash.get('contract_menu').match('rollout'))
        {
            // There is no item_id, meaning that this is the base menu
            // Set the mouseover status to true
            // The menu closes on rollout, meaning that
            // (a) the menu is a context menu or (b) the user
            // is a crackhead. Assume A - the first item
            // will likely be placed directly under the cursor,
            // and the onmouseover code will not be executed
            // right away - so the mouseover status will be
            // FALSE, and the menu will be closed right away.
            this.setMouseoverStatus(this.containerId + '_subitems', true, false, false);
        }

        // Put this item hash in the open menu data hash
        this.openMenuData.set(thisArrayId, itemHash);
    }
    
    // Reposition as necessary (REALLY necessary with cliptrans)
    // This is quite time-consuming unfortunately.
    if (itemHash.get('itemId'))
    {
        var itemsHash = this.getItemHash(itemHash.get('itemId').replace(/[0-9]$/, ''));
        if (!itemsHash.has('useOldPositioning') && !this.menuData.has('useOldPositioning'))
        {
            var parentBgItemObj;
            if (parentBgItemObj = SMBCms_getElement(itemHash.get('parentId')+'_subitems_background'))
            {
                var parentItemObj = SMBCms_getElement(itemId+'_item_container');
                var itemsContainer = SMBCms_getElement(itemOrContainerId + '_subitems');
                itemsContainer.style.marginTop = parentBgItemObj.offsetTop + parentItemObj.offsetTop + 'px';  
                itemsContainer.style.marginLeft = parentBgItemObj.offsetLeft + parentItemObj.offsetLeft + 'px';  
            }
        }
    }

    // Show menu
    this.setVisibility(itemHash.get('items'), itemOrContainerId + '_subitems', true);
        
    // Loop through all items   
    for (var i = 1; itemHash.get('items').has(i); ++i)
    {
        // Get item hash
        var subItemHash = itemHash.get('items').get(i);
        
        // Get subitem ID
        var subItemId = subItemHash.get('itemId');

        // If there are any subitems that have "expand_menu" set to "auto",
        // open them now.
        // Check if the expand_menu of the subitem is set to auto
        var expandMenu = subItemHash.get('expand_menu');
        if (expandMenu && expandMenu.match('auto'))
        {
            // Show subitem's subitems
            this.showMenu(subItemHash, subItemId);

            // Set mouseover status of subitem to true if 
            // the subitem's subitems close with rollout.
            var contractMenu = subItemHash.get('contract_menu');
            if (contractMenu && contractMenu.match('rollout'))
            {
                this.setMouseoverStatus(subItemId, true, true, false);                  
            }
        }
    }

    // Set loop for cleaning menus
    if (!this.cleanMenusLoopTimeout)
    {
        this.cleanMenusLoop();
    }
    
    // Try to get the first closeable openMenuData key
    if (this.firstCloseableKey === false)
    {
        this.getFirstCloseableKey();
    }

    // Uncomment this to get show time
//  var end_time = (new Date()).getTime();
//  alert("Time to show menu " + itemHash.get('itemId') + ": " + (end_time - start_time) + " milliseconds");
};

/**
 * "Checks" a click menu when opening it; basically looks to see if there
 * are any click menus open on the same level, if there are, close them
 * and open this  menu.
 */
SMBCms.prototype.checkClickMenu = function (itemId)
{
    // Get item hash
    var itemHash = this.getItemHash(itemId);
    
    // Get bare item ID
    var bareItemId = itemId.replace(this.containerId + '_', '');
    
    // Is there a menu open on this level?
    if (itemHash.get('menuLevel') > 0)
    {
        for (var i = (this.openMenuData.size() - 1); i > 0; i--)
        {
            var data = this.openMenuData.get(i);
            if (data && data.get('menuLevel') == itemHash.get('menuLevel') && data.get('itemId') != itemId)
            {
                // There is a menu open on this level. Does it
                // expand with onclick?
                if (data.get('expand_menu').match('click'))
                {
                    if (data.get('contract_menu').match('click'))
                    {
                        // Show the menu and break the loop
                        this.showMenu(itemHash, itemId);
                        break;
                    }
                    else if (data.get('contract_menu').match('rollout'))
                    {
                        // The current menu on this level closes on rollout.
                        // In this case, it should NOT be open now since we're
                        // not over the menu! So just close it (and all
                        // children) here
                        this.hideMenusOnLevel(itemHash);
                    }
                }
            }
        }
    }
};

/**
 * Start the menu cleaning loop; calls cleanMenus every X milliseconds.
 */
SMBCms.prototype.cleanMenusLoop = function ()
{
    // Set another timeout
    if ((this.openMenuData.size()-1) >= this.firstCloseableKey)
    {
        this.cleanMenusLoopTimeout = domLib_setTimeout(function (args)
        {
            args[0].cleanMenusLoopTimeout = false;
            args[0].cleanMenusLoop();
        }, 50, [this]);
    }
    this.cleanMenus();
};
    
/**
 * Clean menus - closes menus if possible.
 */
SMBCms.prototype.cleanMenus = function ()
{
    // Loop through all open menus
    // If any can be closed now, set a small timeout and try to close it later
    // if we try to close it now, we get the weird effect of menus never seeming to open
    // (try it - change itemHash.get('closeDelay') to 0)
    for (var i = this.openMenuData.length - 1; this.openMenuData.has(i); i--)
    {
        var itemHash = this.openMenuData.get(i);
        if (this.canHideMenu(itemHash))
        {
            itemHash.cleanThread = domLib_setTimeout(function (args)
            {
                var cItemHash = args[0].openMenuData.get(args[1]);
                if (cItemHash && args[0].canHideMenu(cItemHash))
                {
                    args[0].hideMenu(cItemHash);
                }
            }, itemHash.get('closeDelay'), [this, i]);
        }
        else
        {
            break;
        }
    }   
};

/**
 * Hides a submenu with the specified parent item hash.
 * Note that the passed hash must point to the LAST OPEN SUBMENU in the
 * openMenuData hash; if it doesn't, then nothing will happen.
 */
SMBCms.prototype.hideMenu = function (itemHash, setMouseoverStatus)
{
    // Make sure the item hash is valid
    if (!itemHash)
    {
        return;
    }

    // Make sure setMouseoverStatus is valid 
    if (typeof(setMouseoverStatus) == 'undefined')
    {
        setMouseoverStatus = true;
    }
    
    // Make sure passed item_id matches the item ID in the item hash
    if (itemHash != this.openMenuData.get(this.openMenuData.size() - 1))
    {
        return;
    }

    // Get item ID
    var itemId = itemHash.get('itemId');
    
    if (setMouseoverStatus)
    {
        // Set menuover status
        this.setMouseoverStatus(itemId, false, false, false);
    }

    // Change styles
    if (itemHash.get('itemId'))
    {
        this.setItemStyle(itemHash,
            (this.getMouseoverStatus(itemHash.get('itemId'))
                ? SMBCms_style_rollover
                : SMBCms_style_out));
    }
    
    // If this is the base menu and this is a context menu, move it offscreen
    if (!itemHash.get('itemId') && itemHash.get('expand_menu').match('context'))
    {
        SMBCms_getElement(this.containerId).style.top = '-10000px';
        SMBCms_getElement(this.containerId).style.left = '-10000px';
    }

    // Hide all the items
    this.setVisibility(itemHash.get('items'), (itemHash.get('itemId') || this.containerId) + '_subitems', false);

    this.openMenuData.remove(this.openMenuData.size() - 1);

    //  Set menu open status
    this.menuOpenStatuses.set(itemHash.get('itemId'), false);
};

/**
 * Hides all menus.
 */
SMBCms.prototype.hideAllSubmenus = function (calledOnClick)
{
    // Prevent errors from occuring...
    if (!this.openMenuData.has(this.firstCloseableKey))
    {
        return;
    }
    
    // Hide all item's submenus
    var childrenHidden = false;
    for (var i = (this.openMenuData.size() - 1); i >= this.firstCloseableKey; i--)
    {       
        if (!childrenHidden && calledOnClick && this.openMenuData.get(i).get('contract_menu').match('rollout'))
        {
            if (!this.menuData.get('expand_menu').match('context') || this.firstCloseableKey == 1)
            {
                return;
            }
        }
        
        // Make sure "none" is not found in the contract_menu string, if it is then stop here
        if (this.openMenuData.get(i).get('contract_menu').match('none'))
        {
            return;
        }
        
        // Set mouseover status of the item to false, since we know
        // we're over the document and not the item. Also, there are
        // some cases when SMBCms sets the mouseover status to true
        // automatically when the mouse is not over the item. This
        // fixes any bugs.
        this.setMouseoverStatus(this.openMenuData.get(i).get('itemId'), false, false, false);
        
        // Hide menu
        this.hideMenu(this.openMenuData.get(i));
        childrenHidden = true;
    }
};

/**
 * Get first closeable openMenuData key.
 */
SMBCms.prototype.getFirstCloseableKey = function ()
{
    // Get first closable ID of the menu
    var data = this.openMenuData.get(0);
    var firstId = 1;
    if (this.menuData.get('expand_menu').match('context'))
    {
        firstId = 0;
    }
    else if (data && data.has('contract_menu') && data.get('contract_menu').match(/rollout/))
    {
        firstId = 0;
    }
    this.firstCloseableKey = firstId;
};

/**
 * Sets the item's styles to '' (out), '_rollover', '_menuopen',
 * '_menuopen_rollover', '_rollover_pressed', or '_menuopen_pressed'
 */
SMBCms.prototype.setItemStyle = function (itemHash, new_style, forceStyle)
{
    // Get fallback style
    new_style.match(/^([\_a-zA-Z]*)\_([a-zA-Z]*)$/);
    var fallback_style = RegExp.$1;
    
    // Set current style...
    itemHash.set('currentStyle', new_style);

    // Get element
    var element = SMBCms_getElement(itemHash.get('itemId') + '_item');  

    // Change content
    var style = itemHash.has('content' + new_style) ? new_style : fallback_style; 
    if (itemHash.has('content' + style) && !(itemHash.get('currentStyleContent') == style && !forceStyle))
    {
        element.innerHTML = itemHash.get('content' + style);
        itemHash.set('currentStyleContent', style);
    }

    // Change class 
    var style = itemHash.has('class' + new_style) ? new_style : fallback_style; 
    if (itemHash.has('class' + style) && !(itemHash.get('currentStyleClass') == style && !forceStyle))
    {
        element.className = itemHash.get('class' + style);
        itemHash.set('currentStyleClass', style);
    }

    // Change CSS
    var style = itemHash.has('css' + new_style) ? new_style : fallback_style; 
    if (itemHash.has('css' + style) && !(itemHash.get('currentStyleCSS') == style && !forceStyle))
    {
        // Get array of CSS attribute:value pairs from CSS string   
        var css;
        if (!forceStyle && itemHash.has('css_cached_array' + style))
        {
            css = itemHash.get('css_cached_array' + style);
        }
        else
        {
            css = parseCssString(itemHash.get('css' + style));
            itemHash.set('css_cached_array' + style, css);
        }
        
        for (var k in css)
        {
            var intercapKey = interCap(k);
            try
            {
                // Set particular style
                if (element.style[intercapKey] != css[k])
                {
                    element.style[intercapKey] = css[k];
                }
            }
            catch (e)
            {
                // Catch errors caused by invalid CSS
                // Don't alert the user of errors, because errors caught with Opera
                // might not be caught in Mozilla/IE, etc
            }
        }
        itemHash.set('currentStyleCSS', style);
    }
};

/**
 * Toggles the visibility of the passed element.
 * Uses the passed hash for data and passed element ID
 * as the element that we change the visibility status of.
 * The third argument is a boolean describing whether we
 * are showing or hiding the item.
 * This is another revolutionary SMBCms invention :)
 */
SMBCms.prototype.setVisibility = function (itemsData, itemsId, visibility)
{   
    var element = SMBCms_getElement(itemsId);

    // If we are making the items visible, detect
    // collisions with select lists, applets, objects, etc
    if (visibility)
    {
        domLib_detectCollisions(SMBCms_getElement(itemsId), false);
        element.style.visibility = 'visible';
    }
    
    // Initially set "transitionsUsed" to false
    var transitionsUsed = false;
    
    // Check if we are supposed to do some transition
    for (var index in SMBCms_transitionPlugins.elementData)
    {
        if (itemsData.has(SMBCms_transitionPlugins.get(index)))
        {
            transitionsUsed = true;
            this[SMBCms_transitionPlugins.get(index)](itemsData, itemsId + '_background', visibility);
        }
    }

    // Don't do anything special, just show or hide the element
    if (!transitionsUsed)
    {
        // Call the transitionCompleted method to clean up after everything.
        // Doesn't matter that we didn't transition at all; the transitionCompleted
        // method just needs to be called after the visibility is done changing.
        this.transitionCompleted(element, visibility, itemsId);
    }
};

/** 
 * Perform actions when a transition is done.
 */
SMBCms.prototype.transitionCompleted = function (domObj, visibilityStatus, itemsId)
{
    // Get a new DOM object if we have the background item here
    while (!domObj.id.match(/_subitems$/))
    {
        domObj = domObj.offsetParent;
    }

    // Did the transition make the element invisible?
    if (!visibilityStatus)
    {
        domObj.style.visibility = 'hidden';

        // Unhide selects, applets, etc
        domLib_detectCollisions(domObj, true);
    }
    else
    {
        // Hide selects, applets, etc
        domLib_detectCollisions(domObj, false);

        // Refresh item and any applicable plugins
        domLib_setTimeout('var obj = getSMBCmsObject("'+this.containerId+'"); obj.reloadItems(obj.getItemHash("'+itemsId.replace(/subitems$/, '')+'"), "' + itemsId + '");', 10);
    }
};

/**
 * Reload a full items hash. Gets dimensional/locational properties
 * of the set of items and then
 */
SMBCms.prototype.reloadItems = function (itemsHash, itemsId)
{
    if (itemsHash.get(1))
    {
        // Make sure we have a correct itemsId
        itemsId = itemsHash.get(1).get('parentId') + '_subitems';

        // Get dimensional properties
        itemsHash.set('liveOffsets', new Hash());
        itemsHash.get('liveOffsets').set('first-item-offset-left', 0);
        itemsHash.get('liveOffsets').set('first-item-offset-top', 0);
        itemsHash.get('liveOffsets').set('items-total-height', 0);
        itemsHash.get('liveOffsets').set('items-total-width', 0);

        var firstItemObject = SMBCms_getElement(itemsHash.get(1).get('itemId') + '_item_container');

        if (firstItemObject)
        {
            var backgroundElement = SMBCms_getElement(itemsId + '_background');
            var initialOffsetLeft = firstItemObject.offsetLeft + backgroundElement.offsetLeft;
            var initialOffsetTop = firstItemObject.offsetTop + backgroundElement.offsetTop;
            itemsHash.get('liveOffsets').set('first-item-offset-left', initialOffsetLeft);
            itemsHash.get('liveOffsets').set('first-item-offset-top', initialOffsetTop);
            
            var itemsHeight = 0;
            var itemsWidth = 0;
            for (var i = 1; itemsHash.has(i); ++i)
            {
                var itemObject = SMBCms_getElement(itemsHash.get(i).get('itemId') + '_item_container');
                if ((itemObject.offsetLeft + itemObject.offsetWidth) > itemsWidth)
                {
                    itemsWidth = (itemObject.offsetLeft + itemObject.offsetWidth);
                }
                if ((itemObject.offsetTop + itemObject.offsetHeight) > itemsHeight)
                {
                    itemsHeight = (itemObject.offsetTop + itemObject.offsetHeight);
                }
            }

            itemsHash.get('liveOffsets').set('items-total-height', itemsHeight);
            itemsHash.get('liveOffsets').set('items-total-width', itemsWidth);
        }
        
        // Call any registered plugins...
        if (SMBCms_actionPlugins.has('item_show'))
        {
            for (var index in SMBCms_actionPlugins.get('item_show').elementData)
            {
                SMBCms_actionPlugins.get('item_show').get(index)(this, itemsHash, itemsId);
            }
        }
    }
};

/**
 * Reload an item. Should be used right after an item attribute is changed.
 */
SMBCms.prototype.reloadItem = function (itemHash)
{
    // If there is not a valid item ID, just return right away
    // If there is an item ID, it means that the item HAS been
    // built so it's possible that it's visible. However, if 
    // there is no item ID it's completely impossible that the
    // item is visible.
    if (!itemHash || !itemHash.has('itemId'))
    {
        return;
    }

    // Now update the current style of the item - remember,
    // the current style is held in the item Hash.
    this.setItemStyle(itemHash, itemHash.has('currentStyle') ? itemHash.get('currentStyle') : '', true);
};

/**
 * Modify attributes, and then reload the item.
 */
SMBCms.prototype.modifyMenuData = function (hashPath, newValue)
{
    // Evaluation path to hash
    var evalHashPath = 'this.menuData';
    var itemHashPath = 'this.menuData';
    // Attribute name to modify
    var attributeName = '';
    
    // Parse hashPath
    var chunks = hashPath.split('-');
    for (var i = 0; i < chunks.length; ++i)
    {
        var chunk = chunks[i];
        if (typeof(chunks[i+1]) == 'undefined')
        {
            attributeName = chunk;
        }
        else
        {
            evalHashPath += '.get("'+chunk+'")';
            if (chunk == 'items' || !isNaN(chunk))
            {
                itemHashPath += '.get("'+chunk+'")';
            }
        }
    }
    
    // Get hash to modify
    try
    {
        eval('var attributeHash = ' + evalHashPath);
    }
    catch (e)
    {
        alert('SMBCms error when modifying menu data ['+hashPath+'] of menu with ' +
            'container ID: ' + this.containerId +
            "\n\nError Message: " + e.message +
            "\n\nError Name: " + e.name +
            "\n\nError Line: " + (e.lineNumber ? e.lineNumber : e.number) +
            "\n\nError Stack: " + (e.stack ? e.stack : '[unknown]'));
    }

    // Get item hash
    eval('var itemHash = ' + itemHashPath);

    // Modify attribute
    attributeHash.set(attributeName, newValue);
    
    // If attribute is CSS, removed cached CSS array - see styling function
    if (attributeName.match(/^css(.*)$/))
    {
        attributeHash.remove('css_cached_array' + RegExp.$1);
    }
    
    // Remove first closeable key if the modified attribute was expand_menu or
    // contract_menu
    if (attributeName.match(/(expand|contract)\_menu/))
    {
        this.firstCloseableKey = false;
    }
    
    // Reload item
    this.reloadItem(itemHash);
};

/**
 * Return value of requested attribute.
 */
SMBCms.prototype.getMenuData = function (hashPath)
{
    // Evaluation path to hash
    var evalAttributePath = 'this.menuData';
    // Attribute name to modify
    var attributeName = '';
    
    // Parse hashPath
    var chunks = hashPath.split('-');
    for (var i in chunks)
    {
        evalAttributePath += '.get("'+chunks[i]+'")';
    }
    
    // Get hash to modify
    eval('var attribute = ' + evalAttributePath);
    return attribute;
};

/**
 * Performs actions needed by a SMBCms element.
 */
SMBCms.prototype.mouseAction = function (in_event, action, itemId)
{
    // Make sure we have a valid in_event (for IE)
    // Sometimes we CAN'T have a valid event, in that event just set it to "false"
    var in_event = (in_event || (in_event == 0 ? false : window.event));
    
    // Get item hash
    var itemHash = this.getItemHash(itemId);
    
    // Make sure the item hash is valid
    if (!itemHash)
    {
        return;
    }
        
    // Switch to get the action
    switch (action) {
        default:
            break;

        //
        // Mouseover code
        //
        case 'mouseover':
            // Do not execute mouseover code if we are in the element already
            if (this.mouseoutTimeout == itemId && !this.getMouseoverStatus(itemId))
            {
                this.mouseoutTimeout = false;
                this.setMouseoverStatus(itemId, true, true, true);
                return;
            }

            // -- Submenu opening/closing --
            if (itemHash.has('items'))
            {
                // Are we opening the menu on onclick, and is this the click event?
                if (itemHash.get('expand_menu').match('click'))
                {
                    // When the mouse rolls over the menu,
                    // check if the currently opened menu opens with a click.
                    // If it does, close that one and open this one.
                    this.checkClickMenu(itemId);
                }
        
                // Are we opening the menu on rollover?
                if (itemHash.get('expand_menu').match('rollover'))
                {
                    // Expand submenu
                    this.setShowMenuTimeout(itemHash);
                }
            }
            else if (!itemHash.has('isContainerItem'))
            {
                // Expand submenu - although technically it doesn't exist. The effect that we
                // want is achieved, though - all the submenus on/above this level are closed.
                this.showMenu(itemHash, itemId);
            }

            // Basic mouseover code
            this.setItemStyle(itemHash, (this.menuIsOpen(itemId)
                ? SMBCms_style_menuopenRollover
                : SMBCms_style_rollover));
    
            // Open parent menu (basically re-showing all the items, INCLUDING this one -
            // useful if the menu is fading or sliding out and the mouse moves over.)
            // Nice effect, eh?     
            if (itemId != this.containerId && itemHash.get('parentId') && !itemHash.get('expand_menu').match('auto'))
            {
                this.showMenu(this.getItemHash(itemHash.get('parentId')), itemHash.get('parentId'));
            }
            
            // Set mouseover status to true
            this.setMouseoverStatus(itemId, true, true, true);
            break;
        // -- End onmouseover --

        //
        // Click/Mouseup code
        //
        case 'click':       
            // Prevent the event from bubbling
            domLib_cancelBubble(in_event);

            // -- Submenu opening/closing --
            if (itemHash.has('items'))
            {
                // Are we opening the menu on onclick?
                if (itemHash.get('expand_menu').match('click'))
                {
                    // Is the menu opened or closed?
                    if (this.menuIsOpen(itemId) && itemHash.get('contract_menu').match('click'))
                    {
                        // Contract submenu
                        this.hideMenu(itemHash, false);
                    }
                    else
                    {           
                        // Expand submenu
                        this.setShowMenuTimeout(itemHash);
                    }
                }
                else if (itemHash.get('contract_menu').match('click'))
                {
                    // The menu does not open on onclick, but it does
                    // close on onclick
                    this.hideMenu(itemHash, false);
                }
            }
        case 'mouseup':
            // Prevent the event from bubbling
            domLib_cancelBubble(in_event);

            // Reset style of the item on onclick
            this.setItemStyle(itemHash, (this.menuIsOpen(itemId)
                ? SMBCms_style_menuopenRollover
                : SMBCms_style_rollover));

            break;
        // -- End onclick/onmouseup --

        //
        // Mouseout code
        //
        case 'mouseout':
            // Set mouseover status to false
            this.setMouseoverStatus(itemId, false, false, false);
                    
            // Stop executing the mouseout timeout
            if (this.mouseoutTimeoutId)
            {
                domLib_clearTimeout(this.mouseoutTimeoutId);
                this.mouseoutTimeoutId = false;
            }
            
            // Create mouseout code as a function
            var mouseout_code = function (args)
            {
                // Arguments:
                // 0 => SMBCms object
                // 1 => itemHash
                // 2 => itemId
                // 3 => in_event
                args[0].mouseoutTimeout = false;
                args[0].mouseoutTimeoutId = false;

                if (!args[0].getMouseoverStatus(args[2]))
                {
                    // Set mouseover status
                    args[0].setMouseoverStatus(args[2], false, false, false);
                    
                    // Set style of item
                    args[0].setItemStyle(args[1], (args[0].menuIsOpen(args[2])
                        ? SMBCms_style_menuopenOut
                        : SMBCms_style_out));

                    // Execute custom actions?
                    if (args[1].has('actions') && args[1].get('actions').has('onmouseout'))
                    {
                        eval(args[1].get('actions').get('onmouseout'));
                    }
                                    
                    // Call any plugins needed...
                    if (SMBCms_actionPlugins.has('item_mouseout'))
                    {
                        for (var index in SMBCms_actionPlugins.get('item_mouseout').elementData)
                        {
                            SMBCms_actionPlugins.get('item_mouseout').get(index)(args[3], this, args[1]);
                        }
                    }
                }
            };
            
            // If we have a valid event, we are being called "genuinely"
            // To make sure we haven't moused over an object inside the item (that will
            // first the mouseout code) we first set a 10-ms timeout. If the mouse is
            // REALLY off the item, the code will execute normally. If not, the mouseover
            // code will execute, and set the mouseover status back to true - then when
            // the mouseout code fires, we know not to execute it!
            if (in_event)
            {
                // Set timeout
                this.mouseoutTimeout = itemId;
                this.mouseoutTimeoutId = domLib_setTimeout(mouseout_code, 10, [this, itemHash, itemId, in_event]);
            }
            else
            {
                // Execute normally
                mouseout_code([this, itemHash, itemId, in_event]);
            }
            break;
        // -- End mouseout --

        //
        // Mousedown code
        //
        case 'mousedown':
//          alert(in_event[domLib_eventButton]);
        
            // Prevent the event from bubbling
            domLib_cancelBubble(in_event);

            // Set item style
            this.setItemStyle(itemHash, (this.menuIsOpen(itemId)
                ? SMBCms_style_menuopenPressed
                : SMBCms_style_rolloverPressed));
            break;
        // -- End mousedown --
    }

    if (!action.match('mouseout'))
    {
        // Perform custom actions defined in the item hash
        if (itemHash.has('actions') && itemHash.get('actions').has('on'+action))
        {
            eval(itemHash.get('actions').get('on'+action));
        }
        
        // Call any plugins needed...
        if (SMBCms_actionPlugins.has('item_' + action))
        {
            for (var index in SMBCms_actionPlugins.get('item_' + action).elementData)
            {
                SMBCms_actionPlugins.get('item_' + action).get(index)(in_event, this, itemHash);
            }
        }
    }
};

/**
 * Set a timeout to open a submenu.
 */
SMBCms.prototype.setShowMenuTimeout = function (itemHash)
{
    // Set timeout...
    this.showTimeoutHash.set(this.showTimeoutHash.length, domLib_setTimeout(function(args)
    {
        // Stop any other showMenu timeouts
        for (var i = args[0].showTimeoutHash.length; i >= 0; i--)
        {
            domLib_clearTimeout(args[0].showTimeoutHash.get(i));
            args[0].showTimeoutHash.remove(i);
        }
        // Show menu
        args[0].showMenu(args[1], args[2]);
    }, (itemHash.has('openDelay') ? itemHash.get('openDelay') : 0), [this, itemHash, itemHash.get('itemId')]));
};

/**
 * Returns a formatted unit, which is unit+'px' if the variable doesn't have a
 * unit included with it.
 */
SMBCms.prototype.formatUnit = function (value)
{
    // Is value a number with no specified measurement unit?
    if (parseFloat(value) == value || parseInt(value) == value || typeof(value) == 'number')
    {
        return (value+'') + this.menuData.get('defaultMeasurementUnit');
    }
    // Measurement unit is specified
    else
    {
        return value;
    }
};

/**
 * Get a SMBCms object.
 */
function getSMBCmsObject (containerId)
{
    return SMBCmsObjects.has(containerId) ? SMBCmsObjects.get(containerId) : false; 
}

/**
 * Hide all submenus in all menus.
 * First argument should be true if called from document.onclick, false otherwise.
 * Second argument is the containerId of a SMBCms menu to NOT hide.
 */
function SMBCms_hideAllMenus (calledFromClick, except)
{
    // Loop through all menu objects
    for (var i in SMBCmsObjects.elementData)
    {
        if (i != except)
        {
            // Hide all submenus...
            getSMBCmsObject(i).hideAllSubmenus(calledFromClick);
        }
    }
}

/**
 * Preloads images. Assumes ALL arguments are image path names.
*/
function SMBCms_preloadImages ()
{
    // DOM div object where img elements will be placed
    var domObj = document.createElement('div');
    domObj.style.top = '-999999px';
    for (var i = 0; i < arguments.length; ++i)
    {
        domObj.innerHTML += '<div style="display:none; background-image: url(\''+arguments[i]+'\');">'
            + '<img src="'+arguments[i]+'" />'
            + '</div>';
    }
    document.body.appendChild(domObj);
};

/**
 * Register a plugin relating to executing on-menu-action code.
 *
 * Possible action types (pass to first argument as a string):
 * -- item_mouseover - Called when the mouse moves over the item
 * -- item_mouseout - Called when the mouse moves off the item
 * -- item_click - Called when the item is clicked on
 * -- item_mousedown - Called when the mouse is pressed over the item
 * -- item_mouseup - Called when the mouse is released over the item
 * -- document_mousemove (global mousemove event)
 * -- document_click (global click event)
 * -- item_init - Called right before the item is built and displayed for the first time
 * -- item_show - Called right after the item is made completely visible (or "refreshed")
 */
function SMBCms_registerPlugin_action (action, pluginCallback)
{
    if (!SMBCms_actionPlugins.has(action))
    {
        SMBCms_actionPlugins.set(action, new Hash());
    }
    SMBCms_actionPlugins.get(action).set('auto', pluginCallback);
};


/**
 * Register a plugin relating to visibility transitions.
 * Pass the name of the plugin name as the only argument; it is
 * used for calling the plugin and getting information about it.
 * See alphaTrans and clipTrans for more information.
 */
function SMBCms_registerPlugin_transition (pluginName)
{
    SMBCms_transitionPlugins.set('auto', pluginName);
};

var addEvent;
if (document.attachEvent) {
    addEvent = function(element, type, handler) {
    element.attachEvent("on" + type, handler);
    };
}
else if (document.addEventListener) {
    addEvent = function(element, type, handler) {
        element.addEventListener(type, handler, false);
        };
} else {
    addEvent = new Function; 
}
function setMenuCookie(id)
{
  var exp = new Date();
  var period = exp.getTime() + 5*60*1000; // 5 minutes
  exp.setTime (period);
  document.cookie = "menu_id=" + id + "; path=/; expires=" + exp.toGMTString();
}
