/*
 * filename:      menu.js
 * project:       NUSTEP web application core libs
 * app:           ---
 * copyright:     NUSTEP s.r.o. 2002,2003,2004
 * author:        Svorad Stolc Jr. <stolc@nustep.net>, Cestmir Hybl Jr. <cestmir@nustep.net>
 * created:       2004/02
 * modified:      2005/02/04
 * version:       0.962
 * requirements:  JScript 5.0+
 * classes:
 *                - classMenu
 * description:
 *     Parametric DHTML menu component
 *       - single menu definition
 *       - multiple .show() calls for same menu definition with runtime parameter
 *         substitution for each menuitem url/code
 *       - runtime menuItem.enabled, .visible, .default property calculation based on .show()
 *         parameters
 *       - CSS skinable
 *       - programable time delay constants
 *
 * todo:
 *       - menuitem icons
 *       - accelerator key support (displaying, handling)
 */

function classMenu()
{
  // constructor
  this.delayedFunctionExecuteCallbackNextID = 0;
  this.element = null;
  this.event = null;
  this.items = [];
  this.menuDefs = {};
  this.styleDefs = {};

  this.reset();

  // register to global menu instances collection
  this.instanceIndex = classMenuInstances.length;
  classMenuInstances[this.instanceIndex] = this;
}

// private
classMenu.prototype.getMousePos = function()
{
  var result = {
    'x': null,
    'y': null
  };

  if (!this.event) return result;

  if (this.event.pageX != null)
    result.x = this.event.pageX;

  if ((result.x == null) && (document.body.scrollLeft || document.body.parentElement.scrollLeft))
    result.x = this.event.clientX + (document.body.scrollLeft || document.body.parentElement.scrollLeft);

  if (result.x == null)
    result.x = this.event.clientX;

  if (this.event.pageY != null)
    result.y = this.event.pageY;

  if ((result.y == null) && (document.body.scrollTop || document.body.parentElement.scrollTop))
    result.y = this.event.clientY + (document.body.scrollTop || document.body.parentElement.scrollTop);

  if (result.y == null)
    result.y = this.event.clientY;

  return result;
}

// private
classMenu.prototype.getClientRect = function()
{
  var result = {
    'left': 0,
    'top': 0,
    'width': 0,
    'height': 0
  };

  // Client rect size
  if (typeof(window.innerWidth) == 'number') {
    result.width = window.innerWidth;
    result.height = window.innerHeight;
  }
  else if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
    result.width = document.documentElement.clientWidth;
    result.height = document.documentElement.clientHeight;
  }
  else if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
    result.width = document.body.clientWidth;
    result.height = document.body.clientHeight;
  }

  // Client rect position
  if (typeof(window.pageYOffset) == 'number') {
    result.left = window.pageXOffset;
    result.top = window.pageYOffset;
  }
  else if (document.body && (document.body.scrollLeft || document.body.scrollTop)) {
    result.left = document.body.scrollLeft;
    result.top = document.body.scrollTop;
  }
  else if (document.documentElement && (document.documentElement.scrollLeft || document.documentElement.scrollTop)) {
    result.left = document.documentElement.scrollLeft;
    result.top = document.documentElement.scrollTop;
  }

  return result;
}

// private
classMenu.prototype.getElementAbsolutePosition = function(element)
{
  if (!element) return {'x': 0, 'y': 0};

  var result = {
    'x': element.offsetLeft,
    'y': element.offsetTop
  };

  var parent = element.offsetParent;
  while (parent) {
    result.x += parent.offsetLeft;
    result.y += parent.offsetTop;
    parent = parent.offsetParent;
  }

  return result;
}

// private
classMenu.prototype.getItemElement = function(AItemDef)
{
  if (!AItemDef['elementID'])
    return false;

  if (!AItemDef['element'])
    AItemDef['element'] = document.getElementById(AItemDef['elementID']);

  return AItemDef['element'];
}

// private
classMenu.prototype.blurFocusedElement = function()
{
  var element = document.getElementById('menu_' + this.instanceIndex + '_anchor');

  if (element) {
    var windowStatus = window.status;
    element.focus();
    window.status = windowStatus;
  }
}

// private
classMenu.prototype.delayedFunctionExecuteCallback = function(ACallbackID, ACallbackName)
{
  if (!this.delayedFunctionExecuteData[ACallbackName] ||
      (this.delayedFunctionExecuteData[ACallbackName]['callbackID'] != ACallbackID)) return;

  var code = '';
  for (var index in this.delayedFunctionExecuteData[ACallbackName]['params']) {
    if (code)
      code += ', ';
    code += 'this.delayedFunctionExecuteData[ACallbackName][\'params\'][' + index + ']';
  }
  code = this.delayedFunctionExecuteData[ACallbackName]['function'] + '(' + code + ');';

  eval(code);

  this.delayedFunctionExecuteData[ACallbackName] = null;
}

// private
classMenu.prototype.delayedFunctionExecuteDisable = function(ACallbackName)
{
  this.delayedFunctionExecuteData[ACallbackName] = null;
}

// private
classMenu.prototype.delayedFunctionExecuteDisableAll = function()
{
  this.delayedFunctionExecuteData = {};
}

// private
classMenu.prototype.delayedFunctionExecute = function(ACallbackName, AFunction, ATimeDelay)
{
  var callbackID = this.delayedFunctionExecuteCallbackNextID++;

  this.delayedFunctionExecuteData[ACallbackName] = {
    'callbackID': callbackID,
    'function':   AFunction,
    'params':     []
  };
  var params = this.delayedFunctionExecuteData[ACallbackName]['params'];

  for (var index = 3; index < arguments.length; index++)
    params[params.length] = arguments[index];

  setTimeout('classMenuInstances[' + this.instanceIndex + '].delayedFunctionExecuteCallback(' + callbackID + ', "' + ACallbackName + '");', ATimeDelay);
}

// private
classMenu.prototype.isSubmenu = function(AItemDef)
{
  return (AItemDef && AItemDef['submenu']);
}

// private
classMenu.prototype.hideWindowElementsCoveredBySubmenu = function(AMenuDef)
{
  if (!AMenuDef || !this.getItemElement(AMenuDef)) return;

  var tags = ['applet', 'iframe', 'select'];

  var pos = this.getElementAbsolutePosition(AMenuDef['element']);
  var EX1 = pos.x;
  var EX2 = AMenuDef['element'].offsetWidth + EX1;
  var EY1 = pos.y;
  var EY2 = AMenuDef['element'].offsetHeight + EY1;

  if (!AMenuDef['coveredWindowElements'])
    AMenuDef['coveredWindowElements'] = [];

  for (var i in tags) {
    var elements = document.getElementsByTagName(tags[i]);

    for (var j in elements) {
      var element = elements[j];

      pos = this.getElementAbsolutePosition(element);
      var CX1 = pos.x;
      var CX2 = element.offsetWidth + CX1;
      var CY1 = pos.y;
      var CY2 = element.offsetHeight + CY1;

      if (element.style &&
         (element.style.visibility != 'hidden') &&
         (CX1 < EX2) && (CX2 > EX1) && (CY1 < EY2) && (CY2 > EY1))
      {
        element.style.visibility = 'hidden';
        AMenuDef['coveredWindowElements'][AMenuDef['coveredWindowElements'].length] = element;
      }
    }
  }
}

// private
classMenu.prototype.showWindowElementsCoveredBySubmenu = function(AMenuDef)
{
  if (!AMenuDef || !AMenuDef['coveredWindowElements']) return;

  while (AMenuDef['coveredWindowElements'].length > 0) {
    var index = AMenuDef['coveredWindowElements'].length - 1
    var element = AMenuDef['coveredWindowElements'][index];

    if (element.style)
      element.style.visibility = 'visible';

    AMenuDef['coveredWindowElements'].length--;
  }
}

// private
classMenu.prototype.showSubmenu = function(AMenuDef)
{
  this.delayedFunctionExecuteDisable('showSubmenu');
  this.delayedFunctionExecuteDisable('hideAllSubmenus');

  if (this.styleDef['blurFocusedElement'])
    this.blurFocusedElement();

  //---------------------

  if (!AMenuDef || !this.getItemElement(AMenuDef) || AMenuDef['shown']) return;

  //---------------------

  if (AMenuDef['parent'])
    this.hideAllSubmenus(AMenuDef['parent']['parent']);

  this.shownSubmenuStack[this.shownSubmenuStack.length] = AMenuDef;
  this.expanded = true;

  //---------------------

  // Determine client area
  var clientRect = this.getClientRect();

  AMenuDef['element'].style.left = '0px';
  AMenuDef['element'].style.top = '0px';
  AMenuDef['element'].style.display = 'block';

  // Determine element size
  var elementWidth = AMenuDef['element'].offsetWidth;
  var elementHeight = AMenuDef['element'].offsetHeight;

  AMenuDef['element'].style.display = 'none';

  var elementOffsetX = null;
  var elementOffsetY = null;

  if (AMenuDef['parent'] && this.getItemElement(AMenuDef['parent']))
    with (AMenuDef['parent']['element']) {
      // Determine relative position according to given parent element
      var pos = this.getElementAbsolutePosition(AMenuDef['parent']['element']);
      var parentPos = this.getElementAbsolutePosition(this.parentElement);

      if ((pos['x'] != null) && (pos['y'] != null) && (parentPos['x'] != null) && (parentPos['y'] != null)) {
        elementOffsetX = pos['x'];
        elementOffsetY = pos['y'];

        if (AMenuDef['parent']['parent']['style']['horizontal'])
          elementOffsetY += offsetHeight;
        else
          elementOffsetX += offsetWidth;

        if (AMenuDef['parent']['parent']['style']['submenuOffsetX'] != null)
          elementOffsetX += AMenuDef['parent']['parent']['style']['submenuOffsetX'];

        if (AMenuDef['parent']['parent']['style']['submenuOffsetY'] != null)
          elementOffsetY += AMenuDef['parent']['parent']['style']['submenuOffsetY'];

        // Menurect always on screen - choosing proper menurect corner
        if ((elementOffsetX + elementWidth) > (clientRect.left + clientRect.width))
          if (AMenuDef['parent']['parent']['style']['horizontal'])
            elementOffsetX = (clientRect.left + clientRect.width) - elementWidth;
          else {
            elementOffsetX = pos['x'] - elementWidth;
            if (AMenuDef['parent']['parent']['style']['submenuOffsetX'] != null)
              elementOffsetX -= AMenuDef['parent']['parent']['style']['submenuOffsetX'];
          }

        if ((elementOffsetY + elementHeight) > (clientRect.top + clientRect.height))
          elementOffsetY = (clientRect.top + clientRect.height) - elementHeight;

        if (elementOffsetX < clientRect.left) elementOffsetX = clientRect.left;
        if (elementOffsetY < clientRect.top) elementOffsetY = clientRect.top;

        // Make position relative to parent element
        elementOffsetX -= parentPos['x'];
        elementOffsetY -= parentPos['y'];
      }
    }

  // Force change position by menu style
  if (AMenuDef['style']['offsetX'] != null)
    elementOffsetX = AMenuDef['style']['offsetX'];

  if (AMenuDef['style']['offsetY'] != null)
    elementOffsetY = AMenuDef['style']['offsetY'];

  // Force change position by given coordinates
  if (!AMenuDef['parent']) {
    if (this.offsetX != null)
      elementOffsetX = this.offsetX;

    if (this.offsetY != null)
      elementOffsetY = this.offsetY;
  }

  // If not defined, use mouse position
  if ((elementOffsetX == null) || (elementOffsetY == null)) {
    var mousePos = this.getMousePos();
    if ((mousePos.x != null) && (mousePos.y != null)) {
      elementOffsetX = mousePos.x;
      elementOffsetY = mousePos.y;

      if ((elementOffsetX + elementWidth) > (clientRect.left + clientRect.width))
        elementOffsetX -= elementWidth;

      if ((elementOffsetY + elementHeight) > (clientRect.top + clientRect.height))
        elementOffsetY -= elementHeight;

      if (elementOffsetX < clientRect.left) elementOffsetX = clientRect.left;
      if (elementOffsetY < clientRect.top) elementOffsetY = clientRect.top;
    }
  }

  // If still not defined, use zero position
  if (elementOffsetX == null)
    elementOffsetX = '0px';

  if (elementOffsetY == null)
    elementOffsetY = '0px';

  // Add metrics if necessary
  if (typeof(elementOffsetX) != 'string')
    elementOffsetX += 'px';

  if (typeof(elementOffsetY) != 'string')
    elementOffsetY += 'px';

  AMenuDef['element'].style.left = elementOffsetX;
  AMenuDef['element'].style.top = elementOffsetY;
  AMenuDef['element'].style.display = 'block';

  AMenuDef['shown'] = true;

  //---------------------

  this.hideWindowElementsCoveredBySubmenu(AMenuDef);
}

// private
classMenu.prototype.hideAllSubmenus = function(AMenuDef)
{
  if (AMenuDef && !AMenuDef['shown']) return;

  while (this.shownSubmenuStack.length > 0) {
    var index = this.shownSubmenuStack.length - 1

    if ((AMenuDef && (AMenuDef['itemIndex'] == this.shownSubmenuStack[index]['itemIndex'])) ||
        (!AMenuDef && this.shownSubmenuStack[index]['style']['nonCollapsible']))
      break;

    this.hideSubmenu(this.shownSubmenuStack[index]);
    this.shownSubmenuStack.length--;
  }
}

// private
classMenu.prototype.hideSubmenu = function(AMenuDef)
{
  if (!AMenuDef || !this.getItemElement(AMenuDef) || !AMenuDef['shown']) return;

  AMenuDef['element'].style.display = 'none';
  AMenuDef['shown'] = false;

  //---------------------

  this.showWindowElementsCoveredBySubmenu(AMenuDef);
}

// private
classMenu.prototype.calculateMenuStyle = function(AMenuDef)
{
  function updateStyle(ADestination, ASource)
  {
    if (ASource['cssContainer'] != null)
      ADestination['cssContainer'] = ASource['cssContainer'];

    if (ASource['cssItem'] != null)
      ADestination['cssItem'] = ASource['cssItem'];

    if (ASource['horizontal'] != null)
      ADestination['horizontal'] = ASource['horizontal'];

    if (ASource['offsetX'] != null)
      ADestination['offsetX'] = ASource['offsetX'];

    if (ASource['offsetY'] != null)
      ADestination['offsetY'] = ASource['offsetY'];

    if (ASource['submenuOffsetX'] != null)
      ADestination['submenuOffsetX'] = ASource['submenuOffsetX'];

    if (ASource['submenuOffsetY'] != null)
      ADestination['submenuOffsetY'] = ASource['submenuOffsetY'];

    if (ASource['nonCollapsible'] != null)
      ADestination['nonCollapsible'] = ASource['nonCollapsible'];

    if (ASource['showSubmenuDelay'] != null)
      ADestination['showSubmenuDelay'] = ASource['showSubmenuDelay'];

    if (ASource['hideAllSubmenusDelay'] != null)
      ADestination['hideAllSubmenusDelay'] = ASource['hideAllSubmenusDelay'];
  }

  var style = {};

  updateStyle(style, this.styleDef);

  if (this.styleDef && this.styleDef['levels']) {
    var levelStyle = null;

    for (var level in this.styleDef['levels']) {
      levelStyle = this.styleDef['levels'][level];
      if (level >= AMenuDef['level'])
        break;
    }

    if (levelStyle)
      updateStyle(style, levelStyle);
  }

  updateStyle(style, AMenuDef);

  AMenuDef['style'] = style;
}

// private
classMenu.prototype.calculateItemStyle = function(AItemDef)
{
  var css = '';

  if (AItemDef['parent']['style']['cssItem'] != null)
    css = AItemDef['parent']['style']['cssItem'];

  if (AItemDef['css'] != null)
    css = AItemDef['css'];

  AItemDef['style'] = {
    'css': css
  }
}

// private
classMenu.prototype.setElementCode = function(ACode)
{
  if (!document.body) return; // Trying to create element before page initialization is finished

  if (!this.element) {
    var element = document.createElement('div');

    /*
    if (this.parentElementID) {
      this.parentElement = document.getElementById(this.parentElementID);
      this.parentElement.innerHTML = ACode;
      return;
    }
    */

    if (this.parentElementID) {
      this.parentElement = document.getElementById(this.parentElementID);
      if (!this.parentElement)
        this.parentElement = document.body;
    }
    else
      this.parentElement = document.body;

    this.parentElement.appendChild(element);

    if (!this.parentElementID) {
      element.style.position = "absolute";
      element.style.zIndex = 10000;
      element.style.left = 0;
      element.style.top = 0;
    }
    element.style.width = '100%';

    this.element = element;
  }

  this.element.innerHTML = ACode;
}

// private
classMenu.prototype.documentEventOnKeyDown = function(AEvent)
{
  if (AEvent.keyCode == 27)
    this.collapse();
}

// private
classMenu.prototype.documentEventOnMouseDown = function(AEvent)
{
  if (!this.mouseOver)
    this.collapse();
}

// private
classMenu.prototype.submenuEventOnMouseOver = function(AEvent, AElement)
{
  this.mouseOver = true;

  this.delayedFunctionExecuteDisable('selectShownSubmenus');
  this.delayedFunctionExecuteDisable('collapse');
}

// private
classMenu.prototype.submenuEventOnMouseOut = function(AEvent, AElement)
{
  this.mouseOver = false;

  this.delayedFunctionExecute('selectShownSubmenus', 'this.selectShownSubmenus', (this.styleDef['selectShownSubmenusDelay'] != null ? this.styleDef['selectShownSubmenusDelay'] : 250));
  this.delayedFunctionExecute('collapse', 'this.collapse', (this.collapseDelay != null ? this.collapseDelay : (this.styleDef['collapseDelay'] != null ? this.styleDef['collapseDelay'] : 2000)));
}

// private
classMenu.prototype.itemEventOnMouseOver = function(AEvent, AElement)
{
  var item = this.getMenuItemByElement(AElement);
  this.selectItem(item);
  window.status = (item['description'] ? item['description'] : '');
}

// private
classMenu.prototype.itemEventOnMouseOut = function(AEvent, AElement)
{
  window.status = '';
}

// private
classMenu.prototype.itemEventOnClick = function(AEvent, AElement)
{
  var item = this.getMenuItemByElement(AElement);
  this.executeItem(item);
}

// private
classMenu.prototype.selectItem = function(AItemDef, ASubsequentCall)
{
  if (!AItemDef || !this.getItemElement(AItemDef)) return;

  if (AItemDef['parent']['parent'])
    this.selectItem(AItemDef['parent']['parent'], true);

  if (!AItemDef['selected']) {
    this.deselectItems(AItemDef['parent']);

    AItemDef['element'].className = 'selected';
    AItemDef['selected'] = true;
  }

  if (!ASubsequentCall) {
    this.collapseConcurrentMenus();

    var data = this.delayedFunctionExecuteData['hideAllSubmenus'];
    if (!data || (data['params'][0]['itemIndex'] != AItemDef['parent']['itemIndex']))
      this.delayedFunctionExecute('hideAllSubmenus', 'this.hideAllSubmenus', (AItemDef['parent']['style']['hideAllSubmenusDelay'] != null ? AItemDef['parent']['style']['hideAllSubmenusDelay'] : 1000), AItemDef['parent']);

    this.delayedFunctionExecuteDisable('showSubmenu');

    if (this.isSubmenu(AItemDef)) {
      this.deselectItems(AItemDef['submenu']);
      this.delayedFunctionExecute('showSubmenu', 'this.showSubmenu', (AItemDef['parent']['style']['showSubmenuDelay'] != null ? AItemDef['parent']['style']['showSubmenuDelay'] : 250), AItemDef['submenu']);
    }
  }
}

// private
classMenu.prototype.deselectItems = function(AMenuDef)
{
  if (AMenuDef)
    var menuDef = AMenuDef;
  else
    var menuDef = this.menuDef;

  if (!menuDef['shown']) return;

  for (var index in menuDef['items']) {
    var item = menuDef['items'][index];

    if (this.getItemElement(item) && item['selected']) {
      item['element'].className = 'deselected';
      item['selected'] = false;
    }

    if (this.isSubmenu(item))
      this.deselectItems(item['submenu']);
  }
}

// private
classMenu.prototype.selectShownSubmenus = function()
{
  this.deselectItems();

  if (this.shownSubmenuStack.length > 1)
    this.selectItem(this.shownSubmenuStack[this.shownSubmenuStack.length - 1]['parent']);
}

// private
classMenu.prototype.executeItem = function(AItemDef)
{
  if (AItemDef['code'] || AItemDef['url']) {
    var code = null;

    if (AItemDef['code'])
      code = this.substituteParamValues(AItemDef['code'], this.params);
    else
      if (AItemDef['url'])
        code = 'document.location.href = "' + this.substituteParamValues(AItemDef['url'], this.params) + '";';

    if (code)
      setTimeout(code, 50);

    // After this command whole menu could be hidden and reset
    this.collapse();
  }
  else
    if (this.getItemElement(AItemDef) && this.isSubmenu(AItemDef) && !AItemDef['submenu']['shown'])
      this.showSubmenu(AItemDef['submenu'])

}

// private
classMenu.prototype.inflateMenuDef = function(AFlatMenuDef, AStartIndex)
{
  if (AStartIndex)
    var startIndex = AStartIndex;
  else
    var startIndex = 0;

  if (!AFlatMenuDef[startIndex])
    return null;

  var level = AFlatMenuDef[startIndex][0];
  var items = [];
  var index;

  for (var i = startIndex; i < AFlatMenuDef.length; i++) {
    if (AFlatMenuDef[i][0] == level) {
      index = items.length;
      items[index] = {};

      if (AFlatMenuDef[i][1])
        items[index]['title'] = AFlatMenuDef[i][1];

      if (AFlatMenuDef[i][2])
        items[index]['url'] = AFlatMenuDef[i][2];

      if (AFlatMenuDef[i][3])
        items[index]['code'] = AFlatMenuDef[i][3];
    }
    else if ((AFlatMenuDef[i][0] > level) && (!items[index]['submenu']))
      items[index]['submenu'] = this.inflateMenuDef(AFlatMenuDef, i);
    else if (AFlatMenuDef[i][0] < level)
      break;
  }

  return {'items': items};
}

// public
classMenu.prototype.addMenu = function(AMenuName, AMenuDef, AIsFlat)
{
  if (AIsFlat)
    var menuDef = this.inflateMenuDef(AMenuDef);
  else
    var menuDef = AMenuDef;

  this.menuDefs[AMenuName] = menuDef;
  this.prepareMenuDef(menuDef);
}

// public
classMenu.prototype.addStyle = function(AStyleName, AStyleDef)
{
  this.styleDefs[AStyleName] = AStyleDef;
}

// private
classMenu.prototype.prepareMenuDef = function(AMenuDef, AParent, ALevel)
{
  AMenuDef['itemIndex'] = this.items.length;
  this.items[AMenuDef['itemIndex']] = AMenuDef;

  AMenuDef['parent'] = (AParent ? AParent : null);
  AMenuDef['level'] = (ALevel ? ALevel : 0);

  var hasItems = false;

  for (var index in AMenuDef['items']) {
    var item = AMenuDef['items'][index];

    item['itemIndex'] = this.items.length;
    this.items[item['itemIndex']] = item;

    item['parent'] = AMenuDef;
    item['level'] = AMenuDef['level'];
    item['isSeparator'] = (!item['title']);

    if (this.isSubmenu(item) && !this.prepareMenuDef(item['submenu'], item, item['level'] + 1))
      item['submenu'] = null;

    hasItems = true;
  }

  return hasItems;
}

// private
classMenu.prototype.getMenuItem = function(AIndex)
{
  return this.items[AIndex];
}

// private
classMenu.prototype.getMenuItemByElement = function(AElement)
{
  var index = AElement.getAttribute('mnItemIndex');

  if (index != null)
    return this.getMenuItem(index);
  else
    return null;
}

// private
classMenu.prototype.mergeParams = function(ADestination, ASource)
{
  for (var paramName in ASource)
    ADestination[paramName] = ASource[paramName];
}

// private
classMenu.prototype.substituteParamValues = function(AStr, AParams)
{
  for (var paramName in AParams) {
    AStr = AStr.replace(new RegExp('%' + paramName + '%', 'g'), AParams[paramName]);
  }
  return AStr;
}

// private
classMenu.prototype.evalProperty = function(AValue, AParams, ADefaultValue)
{
  var type = typeof(AValue);
  var params = AParams;
  return (type == 'string' ? (AValue == '0' ? false : eval(AValue)) : (type == 'undefined' ? ADefaultValue : !!AValue));
}

// private
classMenu.prototype.generateElementCode = function(AMenuDef)
{
  var code = '';

  AMenuDef['elementID'] = 'menu_' + this.instanceIndex + '_' + AMenuDef['itemIndex'];
  this.calculateMenuStyle(AMenuDef);

  for (var index in AMenuDef['items']) {
    var item = AMenuDef['items'][index];

    if (!this.evalProperty(item['visible'], this.params, true))
      continue; // hidden item

    this.calculateItemStyle(item);

    if (item['isSeparator'])
      var itemCode = '<div class="separator">&nbsp;</div>';
    else {
      var isDefault = this.evalProperty(item['default'], this.params, false);
      var isEnabled = this.evalProperty(item['enabled'], this.params, true) && (item['url'] || item['code'] || this.isSubmenu(item));

      item['elementID'] = 'menu_' + this.instanceIndex + '_' + item['itemIndex'];

      var itemCode =
        '<div class="content' +
          (isDefault ? ' default' : '') +
          (!isEnabled ? ' disabled' : '') +
        '">' + item['title'] + '</div>';

      itemCode =
        '<div class="' + (!this.isSubmenu(item) ? 'normal' : 'submenu') + '">' + itemCode + '</div>';

      itemCode =
        '<div ' +
          'id="' + item['elementID'] + '" ' +
          'mnItemIndex="' + item['itemIndex'] + '" ' +
          'class="deselected" ' +
          (isEnabled ?
            'onclick="classMenuInstances[' + this.instanceIndex + '].itemEventOnClick(event, this);" ' +
            'onmouseover="classMenuInstances[' + this.instanceIndex + '].itemEventOnMouseOver(event, this);" ' +
            'onmouseout="classMenuInstances[' + this.instanceIndex + '].itemEventOnMouseOut(event, this);" '
          : '') +
        '>' + itemCode + '</div>';

      if (this.isSubmenu(item))
        this.generateElementCode(item['submenu']);
    }

    if (code && AMenuDef['style']['horizontal'])
      code += '</td><td>';

    code += '<div class="' + item['style']['css'] + '">' + itemCode + '</div>';
  }

  code =
    '<div ' +
      'id="' + AMenuDef['elementID'] + '" ' +
      'mnItemIndex="' + AMenuDef['itemIndex'] + '" ' +
      'class="' + AMenuDef['style']['cssContainer'] + '" ' +
      'style="position: absolute; display: none;" ' +
      // (AMenuDef['level'] ? 'style="position: absolute; display: none; "' : '') +
      'onmouseover="classMenuInstances[' + this.instanceIndex + '].submenuEventOnMouseOver(event, this);" ' +
      'onmouseout="classMenuInstances[' + this.instanceIndex + '].submenuEventOnMouseOut(event, this);" ' +
    '>' +
    '<table border="0" cellpadding="0" cellspacing="0" class="container">' +
    '<tr><td>' + code + '</td></tr>' +
    '</table>' +
    '</div>';

  AMenuDef['elementCode'] = code;
}

// private
classMenu.prototype.resetItems = function()
{
  for (var index in this.items) {
    this.items[index]['element'] = null;
    this.items[index]['elementID'] = null;
    this.items[index]['elementCode'] = null;
    this.items[index]['style'] = null;
    this.items[index]['shown'] = null;
    this.items[index]['selected'] = null;
  }
}

// private
classMenu.prototype.getCode = function()
{
  this.resetItems();
  this.setElementCode('');

  this.generateElementCode(this.menuDef);

  var code = '';

  for (var index in this.items) {
    if (this.items[index]['elementCode'])
      code += this.items[index]['elementCode'];
  }

  code += '<a href="#" id="menu_' + this.instanceIndex + '_anchor"></a>';

  return code;
}

// public
classMenu.prototype.show = function(AMenuName, AStyleName, AParams, AEvent, AOffsetX, AOffsetY, AParentElementID)
{
  this.hide();
  this.collapseConcurrentMenus();

  // Setting global events
  this.origDocumentEventOnKeyDown = document.onkeydown;
  document.onkeydown = Function('AEvent', 'classMenuInstances[' + this.instanceIndex + '].documentEventOnKeyDown((AEvent ? AEvent : event));');

  this.origDocumentEventOnMouseDown = document.onmousedown;
  document.onmousedown = Function('AEvent', 'classMenuInstances[' + this.instanceIndex + '].documentEventOnMouseDown((AEvent ? AEvent : event));');

  this.event = AEvent;

  this.menuDef = this.menuDefs[AMenuName];
  this.menuName = AMenuName;

  this.styleDef = this.styleDefs[AStyleName];
  this.styleName = AStyleName;

  // menu instance params
  this.params = (this.menuDef.params ? this.menuDef.params : {});

  // implicit calculated params
  this.params['currentURL'] = escape(window.location).replace(/\//g, '%2F');

  // show() call params
  if (AParams)
    this.mergeParams(this.params, AParams);

  this.offsetX = AOffsetX;
  this.offsetY = AOffsetY;

  this.parentElementID = (AParentElementID ? AParentElementID : this.styleDef['levels'][0]['parentElementID']);

  this.setElementCode(this.getCode());
  this.showSubmenu(this.menuDef);

  this.delayedFunctionExecute('collapse', 'this.collapse', (this.collapseDelay != null ? this.collapseDelay : (this.styleDef['collapseDelay'] != null ? this.styleDef['collapseDelay'] : 2000)));

  this.shown = true;

  return false;
}

// public
classMenu.prototype.hide = function()
{
  if (!this.shown) return;

  // Restoring global events
  document.onkeydown = this.origDocumentEventOnKeyDown;
  document.onmousedown = this.origDocumentEventOnMouseDown;

  this.setElementCode('');

  this.reset();
  this.resetItems();
}

// Executes generic menuitem action without showing menu.
// public
classMenu.prototype.action = function(AMenuName, AActionName, AParams)
{
  var menuDef = this.menuDefs[AMenuName];

  // menu instance params
  var params = (menuDef.params ? menuDef.params : {});

  // implicit calculated params
  params['currentURL'] = escape(window.location).replace(/\//g, '%2F');

  // method call params
  this.mergeParams(params, AParams);

  var actionDef = menuDef.items[AActionName];
  if (actionDef && (actionDef['code'] || actionDef['url'])) {
    var code = null;

    if (actionDef['code'])
      code = this.substituteParamValues(actionDef['code'], params);
    else
      if (actionDef['url'])
        code = 'document.location.href = "' + this.substituteParamValues(actionDef['url'], params) + '";';

    if (code)
      eval(code);
  }
}

// public
classMenu.prototype.setCollapseDelay = function(ACollapseDelay)
{
  this.collapseDelay = ACollapseDelay;

  if (this.delayedFunctionExecuteData['collapse'] != null)
    this.delayedFunctionExecute('collapse', 'this.collapse', (this.collapseDelay != null ? this.collapseDelay : (this.styleDef['collapseDelay'] != null ? this.styleDef['collapseDelay'] : 2000)));
}

// public
classMenu.prototype.collapse = function()
{
  if (!this.shown) return;

  this.deselectItems();
  this.hideAllSubmenus();
  this.delayedFunctionExecuteDisableAll();

  if (this.shownSubmenuStack.length > 0)
    this.selectShownSubmenus();
  else
    this.hide();

  this.expanded = false;
}

// private
classMenu.prototype.collapseConcurrentMenus = function()
{
  for (var index in classMenuInstances) {
    if ((index != this.instanceIndex) && classMenuInstances[index].shown && classMenuInstances[index].expanded)
      classMenuInstances[index].collapse();
  }
}

// private
classMenu.prototype.reset = function()
{
  this.delayedFunctionExecuteDisableAll();

  this.menuDef = null;
  this.menuName = null;

  this.styleDef = null;
  this.styleName = null;

  this.params = null;

  this.offsetX = null;
  this.offsetY = null;

  this.parentElementID = null;
  this.parentElement = null;

  this.shownSubmenuStack = [];

  this.mouseOver = false;

  this.shown = false;
  this.expanded = false;

  this.collapseDelay = null;
}

// global menu instance collection
classMenuInstances = [];

// standard instance for context menus
contextMenu = new classMenu();
