



/*
 * Copyright 2003-2007 inxire GmbH. All rights reserved.
 * ------------------------------------------------------
 * Version: $Id: menuDropdown.jsp,v 1.3 2008/04/16 13:45:10 hkeller Exp $
 *
 * Handling of drop-down and context menus
 */




/* Global reference to the currently open menu */
var currentMenu     = null;
var currentCheckbox = null;   // automaticly checked by context menu, uncheck on close
/** Close-Timer for the currently open menu */
var menuTimeout = null;

/* List of init functions, filled during page reading */
var initFunctions = new Array();

// Super class expansion map: className -> list of super classes (excluding publicobject)
var superClassMap = new Object();
superClassMap['PROCESSINFO'] = ' CATEGORY';
superClassMap['GROUPTEMPLATE'] = ' DIRECTORYGROUP DIRECTORYOBJECT';
superClassMap['WASTEBASKETENTRY'] = ' CATEGORY';
superClassMap['LINKEDARTICLE'] = ' CATEGORY';
superClassMap['DIRECTORYGROUP'] = ' DIRECTORYOBJECT';
superClassMap['WIKI'] = ' FOLDER';
superClassMap['INTERMEDIASOURCE'] = ' CATEGORY';
superClassMap['ADMINISTRATIONGROUP'] = ' DIRECTORYGROUP DIRECTORYOBJECT';
superClassMap['INTERMEDIAIMAGE'] = ' INTERMEDIASOURCE CATEGORY';
superClassMap['SURVEYPROPERTYBUNDLE'] = ' PROPERTYBUNDLE APPLICATIONOBJECT';
superClassMap['PROPERTYBUNDLE'] = ' APPLICATIONOBJECT';
superClassMap['DIRECTORYUSER'] = ' DIRECTORYOBJECT';
superClassMap['SEARCHPROPERTYBUNDLE'] = ' PROPERTYBUNDLE APPLICATIONOBJECT';
superClassMap['PROCESSPROTECTED'] = ' CATEGORY';
superClassMap['NEWSCHANNEL'] = ' CATEGORY';
superClassMap['PROCESSCONTAINER'] = ' CATEGORY';
superClassMap['SENTTOSUBSCRIBER'] = ' CATEGORY';
superClassMap['WORKGROUP'] = ' DIRECTORYGROUP DIRECTORYOBJECT';
superClassMap['TOPIC'] = ' FOLDER';
superClassMap['RFC822MESSAGE'] = ' DOCUMENT';
superClassMap['EXTENDEDGROUP'] = ' DIRECTORYGROUP DIRECTORYOBJECT';
superClassMap['FORUM'] = ' FOLDER';
superClassMap['TOPICROOT'] = ' FOLDER';
superClassMap['PRIMARYUSERPROFILE'] = ' USERPROFILE';



/* Show menu with given id and optionally save current checkbox */
function showMenu(menuId, checkbox) 
{
  // we only support DOM browser
  if (!document.getElementById || !menuId) return;

  if (window.isDebug > 1) logMessage("showMenu(" + menuId + ", " + (checkbox ? checkbox.value : "") + ") ...");

  // clear menu timeout
  clearMenuTimeout();
  hideCurrentMenu(checkbox);   // do not touch new checkbox(!)

  // save current item (optionally)
  if (checkbox) {
    currentCheckbox = checkbox;
  }
  else {
    currentCheckbox = null;
  }
  
  // make menu visible
  var menu = document.getElementById(menuId);
  menu.style.visibility = "visible";

  // mark as open window
  currentMenu     = menu;
}


/* Hide menu with given id */
function hideMenu(menuId) 
{
  // we only support DOM browser
  if (!document.getElementById || !menuId) return;

  if (window.isDebug > 1) logMessage("hideMenu(" + menuId + "), currentCheckbox: " + (currentCheckbox ? currentCheckbox.value : "") + " ...");

  var menu = document.getElementById(menuId);
  menu.style.visibility = "hidden";
  
  if (menu == currentMenu) {
    if (currentCheckbox && currentCheckbox.checked) {
      currentCheckbox.checked = false;
      if (currentCheckbox.form && currentCheckbox.form.adjustMenus) currentCheckbox.form.adjustMenus();
    }
    currentCheckbox = null;
    currentMenu     = null;
  }
}


/*
 * Hide current menu and clear current checkbox
 * @param newCheckbox  protected checkbox, that will not be unchecked
 */
function hideCurrentMenu(newCheckbox) 
{
  // we only support DOM browser
  if (!document.getElementById) return;

  if (window.isDebug > 1) logMessage("hideCurrentMenu(): " + getNodeDescription(currentMenu) + ", " + (currentCheckbox ? currentCheckbox.value : "") + " ...");

  if (currentMenu) {
    currentMenu.style.visibility = "hidden";
    if (currentCheckbox && currentCheckbox.checked && currentCheckbox != newCheckbox) {
      currentCheckbox.checked = false;
      if (currentCheckbox.form && currentCheckbox.form.adjustMenus) currentCheckbox.form.adjustMenus();
    }
    currentCheckbox = null;
    currentMenu     = null;
  }
}

/* Clear current checkbox property */
function clearCurrentCheckbox() 
{
  currentCheckbox = null;
}


/* Show menu if not current, hide otherwise */
function toggleMenu(menuId) 
{
  // we only support DOM browser
  if (!document.getElementById || !menuId) return;
  var menu = document.getElementById(menuId);

  if (menu == currentMenu) {
    // System.err.println((debugCounter++) + " hideMenu " + menuId);
    hideMenu(menuId);
  }
  else {
    // System.err.println((debugCounter++) + " showMenu " + menuId);
    showMenu(menuId);
  }
}


/* Switch to this menu if any other menu is open */
function switchMenu(menuId) 
{
  // we only support DOM browser
  if (!document.getElementById || !menuId) return;
  var menu = document.getElementById(menuId);

  if (currentMenu && currentMenu != menu) 
  {
    var currentMenuList = findAncestorOrSelf(currentMenu, 'menuList', null);
    var menuList        = findAncestorOrSelf(menu, 'menuList', null);
  
    if (menuList != null && menuList == currentMenuList)
    {
      // System.err.println((debugCounter++) + " showMenu " + menuId);
      showMenu(menuId);
    }
  }
}


/* Set timeout for menu with given id or the current menu, if no id given */
function setMenuTimeout(menuId) 
{
  // clear old timeout
  if (menuTimeout) clearTimeout(menuTimeout);

  // set timeout for visibility
  if (menuId) {
    menuTimeout = setTimeout("hideMenu('"+menuId+"')", 500);
  }
  else {
    menuTimeout = setTimeout("hideCurrentMenu()", 500);
  }
}


/* Clear menu timeout  */
function clearMenuTimeout() 
{
  // clear menu timeout
  if (menuTimeout) {
    clearTimeout(menuTimeout);
    menuTimeout = null;
  }
}


/*
 * Register menu. The menu may be registered at any time during page buildup,
 * but before the call ot initMenus().
 * 
 * All registered menus will be initialized later during initMenus() call
 * to allow other initializations to take place first.
 */
function registerMenu(menuId, actuatorId, offsetLeft, offsetTop)
{
  // register body.onload and body.onunload trigger with first call
  if (initFunctions.length == 0)
  {
    addOnloadTrigger("initMenus();");
  }

  var code = "initializeMenu('" + menuId + "', '" + actuatorId + "', " + offsetLeft + ", " + offsetTop + ")";
  // alert("Register menu(" + initFunctions.length + "): \n" + code);
  initFunctions[initFunctions.length] = new Function(code);
}


/*
 * Initializes all menus. This functions repositions all menus 
 * and register the event handler.
 *
 * @see initializeMenu(menuId, actuatorId, offsetLeft, offsetTo)
 */
function initMenus() {
  for (var i=0; i<initFunctions.length; i++) {
    // alert("execute function " + i + ":\n" + initFunctions[i]);
    initFunctions[i]();
  }

  // addOnclickTrigger(document, "hideCurrentMenu();");
  addOnmousedownTrigger(document, "hideCurrentMenu();");
}


/*
 * Initialize drop-down menu. During initialization the drop-down
 * menus are re-adjusted and the event handlers are set.
 *
 * All drop-down menus must be contained in an HTML element with CSS class
 * "menuList", because this is the position reference. The 'filesMenu' has
 * the following structure, for example:
 * <div class="menuList">
 *   <div class="singleMenuBox">
 *     <a href="#" id="filesActuator" class="actuator">Menu Label</a>
 *     <div id="filesMenu" class="menu">
 *       <div [id="filesMenu_copy"]><a href="javascript:...">Copy</a></div>
 *       ...
 *       <div><hr class="menuSeparator"/></div>
 *       ...
 *    </div>
 *    ...
 * </div>
 */
function initializeMenu(menuId, actuatorId, offsetLeft, offsetTop) 
{
  // we only support DOM browser
  if (!document.getElementById || !menuId || !actuatorId) return;

  var menu     = document.getElementById(menuId);
  var actuator = document.getElementById(actuatorId);

  // alert("menu: " + menu + "\nactuator: " + actuator + "\nhasChildNodes? " + menu.hasChildNodes());

  if (menu == null || actuator == null || !menu.hasChildNodes()) return;

  // search for parent node with class "singleMenuBox"
  var singleMenuBox = actuator;
  while (singleMenuBox.parentNode && singleMenuBox.className != 'singleMenuBox')
  {
    singleMenuBox = singleMenuBox.parentNode;
  }

  // search for parent node with class "menuList"
  var menuList = actuator;
  while (menuList.parentNode && menuList.className != 'menuList')
  {
    menuList = menuList.parentNode;
  }

  // showNodeProperties(menu, "Menu (unchanged)");
  // showNodeProperties(actuator, "Actuator");
  // showNodeProperties(menuList, "Menu List");
/*
  alert("Menu properties: "
        + "\n offsetTop: "  + offsetTop 
        + "\n offsetLeft: " + offsetLeft 
        + "\n menuList.offsetTop: " + menuList.offsetTop 
        + "\n menuList.offsetLeft: " + menuList.offsetLeft 
        + "\n menuList.offsetWidth: " + menuList.offsetWidth 
        + "\n menuList.offsetHeight: " + menuList.offsetHeight 
        + "\n singleMenuBox.offsetTop: " + singleMenuBox.offsetTop 
        + "\n singleMenuBox.offsetLeft: " + singleMenuBox.offsetLeft 
        + "\n singleMenuBox.offsetWidth: " + singleMenuBox.offsetWidth 
        + "\n singleMenuBox.offsetHeight: " + singleMenuBox.offsetHeight 
        + "\n actuator.offsetTop: " + actuator.offsetTop 
        + "\n actuator.offsetLeft: " + actuator.offsetLeft 
        + "\n actuator.offsetWidth: " + actuator.offsetWidth 
        + "\n actuator.offsetHeight: " + actuator.offsetHeight 
        + "\n menu.offsetTop: " + menu.offsetTop 
        + "\n menu.offsetLeft: " + menu.offsetLeft 
        + "\n menu.offsetWidth: " + menu.offsetWidth 
        + "\n menu.offsetHeight: " + menu.offsetHeight 
        + "\n visibility: " + menu.style.visibility 
        + "\n z-index: " + menu.style.zIndex);
*/

  /*
   * Re-adjust offset. Now menu will fit exactly below actuator box.
   */
  // menu.style.left = (offsetLeft + actuator.offsetLeft - menu.offsetLeft) + "px";
  menu.style.left = (offsetLeft + singleMenuBox.offsetLeft - menu.offsetLeft) + "px";
  menu.style.top  = (offsetTop  + menuList.offsetHeight) + "px";

  // showNodeProperties(menu, "Menu (moved)");
/*
  alert("Menu properties: "
        + "\n menu.offsetTop: " + menu.offsetTop 
        + "\n menu.offsetLeft: " + menu.offsetLeft 
        + "\n menu.offsetWidth: " + menu.offsetWidth 
        + "\n menu.offsetHeight: " + menu.offsetHeight 
        + "\n visibility: " + menu.style.visibility 
        + "\n z-index: " + menu.style.zIndex);
*/

  /*
   * Define event handler
   */

  var automatic = false;   // activate to use onmouseover/onmouseout trigger
                           // Warning: DO NOT use link on activator with automatic == false!

  if (automatic)
  {
    actuator.onmouseover = new Function("e", "showMenu('" + menuId + "');");
    actuator.onmouseout  = new Function("e", "setMenuTimeout('" + menuId + "');");
    menu.onmouseover     = function (e) 
    {
      // var target = getTargetNode(e);
      // debugCounter++;
      // System.err.println("onmouseover " + (debugCounter) + ": " + target.tagName + "#" + target.id + "." + target.className);
      clearMenuTimeout();
    }
    menu.onmouseout      = function (e)
    {
      // var target = getTargetNode(e);
      // debugCounter++;
      // System.err.println("onmouseout " + (debugCounter) + ": " + target.tagName + "#" + target.id + "." + target.className);
      setMenuTimeout(menuId);
    }
  }
  else 
  {
    actuator.onclick     = new Function("e", "toggleMenu('" + menuId + "'); stopBubbling(e); return stopDefaultAction(e)");
    actuator.onmouseover = new Function("e", "switchMenu('" + menuId + "'); stopBubbling(e); return stopDefaultAction(e)");
  }
  actuator.onmousedown = new Function("e", "stopBubbling(e); return true;");  // prevent menu close
  menu.onmousedown = new Function("e", "stopBubbling(e); return true;");      // prevent menu close
}


/**
 * Utility function to count the number of active entries in given menu.
 * Warning: This method just counts the number of visible child nodes
 */
function countMenuEntries(menuId) 
{
  // we only support DOM browser
  if (!document.getElementById || !menuId ) return 0;
  
  var menu = document.getElementById(menuId);
  var cc = 0;
  if (menu)
  {
    var childNodes = menu.childNodes;
    for (var i=0; i<childNodes.length; i++)
    {
      var child = childNodes[i];
      if (child && child.nodeType == 1 && child.style.display != 'none') cc++;
    }
  }
  return cc;
} 
    

/*
 * Context menu handler
 * **********************************************************************
 *
 * Context menues are shown at the current mouse position, where the
 * menu is adjusted to fit into the browser window.
 */


/*
 * Show context menu with given id.
 * @param  e         dom event
 * @param  menuId    html ID of context menu
 * @param  checkbox  optional checkbox node, will be checked on menu show and
 *                   unchecked on menu hide
 */
function showContextMenu(e, menuId, checkbox)
{
  if (window.isDebug > 1) logMessage("showContextMenu(" + menuId + ", " + (checkbox ? checkbox.value : "") + ") ...");

  // we only support DOM browser
  if (!document.getElementById || !menuId) return true;

  // check current item and adjust menu before calculation position (optionally)
  // Warning: if new checkbox is equal to currentCheckbox, prevent uncheck during hideCurrentMenu()
  var setCheckMark = (checkbox && (checkbox.checked == false || (currentMenu && currentCheckbox == checkbox)));
  if (setCheckMark) {
    checkbox.checked = true;
    if (checkbox.form && checkbox.form.adjustMenus) checkbox.form.adjustMenus();
  }

  // adjust menu position
  var menu = document.getElementById(menuId);

  var x = xPosition(e);
  var y = yPosition(e);
  var offset = 2;        // small skip to get correct event target on mouse-up

  var bodyWidth  = xWindowSize(window);
  var bodyHeight = yWindowSize(window);
  // IE6: use 'document.documentElement' (=HTML element) instead of 'document.body' for scroll infos
  var scrollLeft   = (document.documentElement && document.documentElement.scrollLeft) ? document.documentElement.scrollLeft : document.body.scrollLeft;
  var scrollTop    = (document.documentElement && document.documentElement.scrollTop) ? document.documentElement.scrollTop  : document.body.scrollTop;
/*
  alert( "Event position : " + x + ", " + y 
       + "\nbody : " + bodyWidth + " x " + bodyHeight 
       + "\ndocument.body : " + document.body.offsetWidth + " x " + document.body.offsetHeight + " (" + document.body.clientWidth + " x " + document.body.clientHeight + ")"
       + "\ndocumentElement : " + document.documentElement.offsetWidth + " x " + document.documentElement.offsetHeight + " (" + document.documentElement.clientWidth + " x " + document.documentElement.clientHeight + ")"
       + "\nwindow : " + window.innerWidth + " x " + window.innerHeight
       + "\nscroll : " + scrollLeft + ", " + scrollTop
       + "\nbody.scroll : " + document.body.scrollLeft + ", " + document.body.scrollTop
       + "\ndocumentElement.scroll : " + document.documentElement.scrollLeft + ", " + document.documentElement.scrollTop
       + "\noffset : " + document.body.offsetLeft + ", " + document.body.offsetTop
       + "\nmenuobj : " + menuobj.offsetWidth + " x " + menuobj.offsetHeight);
*/
  // if the horizontal distance isn't enough to accomodate the width of the context menu
  if (scrollLeft + bodyWidth < x + menu.offsetWidth + offset) {
    // move the horizontal position of the menu to the left as appropriate
    menu.style.left = (x - menu.offsetWidth - offset) + 'px';
  }
  else {
    // position the horizontal position of the menu where the mouse was clicked
    menu.style.left = x + offset + 'px';
  }

  // same concept with the vertical position
  if (scrollTop + bodyHeight < y + menu.offsetHeight) {
    menu.style.top = (scrollTop + bodyHeight - menu.offsetHeight) + 'px';
  }
  else {
    menu.style.top = y + 'px';
  }

  // show the menu and optionally activate checkbox
  showMenu(menuId, (setCheckMark ? checkbox : null));

  //if (window.isDebug) logMessage("... menu (" + menuId + ") shown");

  // return false to disable default action
  return stopDefaultAction(e);
}


/*
 * Register context menu. The menu may be registered at any time during page 
 * buildup, but before the call ot initMenus().
 * 
 * All registered menus will be initialized later during initMenus() call
 * to allow other initializations to take place first.
 *
 * @param menuId      html ID of menu element
 * @param actuatorId  html ID of an optional actuator. The actuator takes 
 *                    the oncontextmenu handler
 * @param eventFunc   optional event function to be set at actuator element.
 *                    The given value should be a source code fragment (string),
 *                    where the variable 'e' denotes the event object.
 */
function registerContextMenu(menuId, actuatorId, eventFunc)
{
  // if (window.isDebug) logMessage("registerContextMenu ...");

  // register body.onload and body.onunload trigger with first call
  if (initFunctions.length == 0)
  {
    addOnloadTrigger("initMenus();");
  }

  // do some escaping on event function
  if (eventFunc) {
    // alert(eventFunc);
    eventFunc = eventFunc.replace(/["'\\]/g, "\\$&");
    // alert(eventFunc);
  }
  else {
    eventFunc = '';
  }
  
  if (!actuatorId) actuatorId = '';
  
  var code = "initializeContextMenu('" + menuId + "', '" + actuatorId + "', '" + eventFunc + "')";
  // alert("Register menu(" + initFunctions.length + "): \n" + code);
  initFunctions[initFunctions.length] = new Function(code);
}


/*
 * Initialize context menu. Only the event handler is initialized
 * for context menus.
 *
 * @param menuId      html ID of menu element
 * @param actuatorId  html ID of an optional actuator. The actuator takes the
 *                    oncontextmenu handler. 
 * @param eventFunc   optional event function to be set at actuator element.
 *                    The given value should be a source code fragment (string),
 *                    where the variable 'e' denotes the event object.
 */
function initializeContextMenu(menuId, actuatorId, eventFunc) 
{
  // if (window.isDebug) logMessage("initializeContextMenu(" + menuId + ", " + actuatorId + ") ...");

  // we only support DOM browser
  if (!document.getElementById || !menuId) return;

  // initialize menu events
  var menu = document.getElementById(menuId);
  if (menu == null || !menu.hasChildNodes()) return;

  menu.onmousedown = new Function("e", "stopBubbling(e); return true;");      // prevent menu close

  // alert("menu: " + menu + "#" + menuId + "\nhasChildNodes? " + menu.hasChildNodes());

  // initialize actuator (optional)
  if (actuatorId)
  {
    var actuator = document.getElementById(actuatorId);
    if (actuator == null) return;
    
    // alert("menu: " + menu + "\nactuator: " + actuator + "\nhasChildNodes? " + menu.hasChildNodes());
    
    if (eventFunc)
    {
      // alert("initializeContextMenu: " + eventFunc);
      actuator.oncontextmenu = new Function("e", eventFunc);
      if (window.opera) actuator.onmouseup = new Function("e", "if (e.button != 2) return true; " + eventFunc);    // workaround for Opera
    }
    else
    {
      actuator.oncontextmenu = new Function("e", "return showContextMenu(e, '" + menuId + "');");
      if (window.opera) actuator.onmouseup = new Function("e", "if (e.button != 2) return true; return showContextMenu(e, '" + menuId + "');");
    }
  }
}


/*
 * Activation and Deactivation
 * **********************************************************************
 *
 * A menu entry can be enabled/disabled or shown/hide.
 */

/*
 * Show menu entry with given html ID. If the doShow flag is given and set
 * to 'false' the entry may although be hidden.
 *
 * @param  entryId  html ID of DIV or A surrounding a single entry
 * @param  doShow   if false, the entry will be hidden (optional), defaults to 'true'
 */
function showMenuEntry(entryId, doShow) 
{
  if (doShow == false) { hideMenuEntry(entryId); return; }

  if (!document.getElementById || !entryId) return;
  
  var elem = document.getElementById(entryId);
  if (!elem) return;

  if (elem.style.display != '')
  {
    elem.style.display = '';
  }
}


/*
 * Hide menu entry with given html ID.
 *
 * The ID may point to a single entry or some DIV surrounding
 * a set of entries.
 */
function hideMenuEntry(entryId) 
{
  if (!document.getElementById || !entryId) return;
  
  var elem = document.getElementById(entryId);
  if (!elem) return;

  if (elem.style.display != 'none')
  {
    elem.style.display = 'none';
  }
}


/*
 * Modify given icon URL to reflect the given state. This method
 * may be useful to find the 'disabled' or 'over' icon from the base icon URL.
 *
 * For example the icon '/icon/my.gif' will be converted to
 * '/icon/my_disabled.gif' for the 'disabled' state.
 *
 * If the icon URL can not be parsed, the original URL will be returned.
 */
function findStateIcon(iconSrc, state) {
  if (!iconSrc || !state) return null;

  var idx = iconSrc.search( /[.](png|gif)$/i );   // TODO: allow query parameters
  // alert("iconSrc: " + iconSrc + "\nidx: " + idx);
  if ( idx > 0) {
    var newSrc = iconSrc.substring(0, idx) + '_' + state + iconSrc.substring(idx);
    // alert("iconSrc: " + iconSrc + "\nnewSrc: " + newSrc);
    return newSrc;
  }
  // return original src URL
  return iconSrc;
}


/*
 * Change icon state of given menu entryicon object.
 * @param  icon   the html DOM object of some icon
 * @param  state  new state, will be suffixed to image 
 *                source, e.g. 'my.gif' -> 'my_over.gif'.
 *                Use null state to go back to original source.
 *
 * The given ID must point to the DIV or A surrounding a single entry.
 */
function changeMenuIconState(icon, state) 
{
  // disable anchor element
  if (icon && icon.src) {
    var src     = icon.src;
    var origSrc = icon.getAttribute('origSrc');
    if (state) {
      if (!origSrc) {
        origSrc = src;
        icon.setAttribute('origSrc', src);   // save original source URL
      }
      icon.src = findStateIcon(origSrc, state);    // e.g. 'my.gif' -> 'my_over.gif'
    }
    else if (origSrc) {            // unable to reset source, if no original value
      icon.src = origSrc;
    }
  }
}


/*
 * Find anchor element in menu entry
 * @param  entryId  the htmlId of the menu entry
 * @return  the first A-descendant found below the given entry or the entry itself
 */
function findMenuEntryAnchor(entryId) 
{
  if (!document.getElementById || !entryId) return;
  
  var elem = document.getElementById(entryId);
  if (!elem) return null;

  if (elem.tagName != 'A') {
    // find first A child
    elem = findDescendant(elem, null, 'A', null, null, 1);
  }
  return elem;
}


/*
 * Enable single entry with given html ID. If the doEnable flag is given
 * and set to 'false', the entry may although be disabled.
 *
 * @param  entryId  html ID of DIV or A surrounding a single entry
 * @param  doEnable if false, the entry will be disabled (optional), defaults to 'true'
 */
function enableMenuEntry(entryId, doEnable) 
{
  if (doEnable == false) { disableMenuEntry(entryId); return; }

  var elem = findMenuEntryAnchor(entryId);
  if (!elem) return;

  // enable anchor element
  if (elem.className == 'disabled')
  {
    elem.className = '';
    // elem.onclick   = null;
    var savedHref = elem.getAttribute('savedHref');    // Opera does not like 'noHref'
    if (savedHref) {
      //alert("Restoring old href: " + savedHref + "\nhref: " + elem.href);
      elem.href = savedHref;
    }

    // search for icon
    var icon = findDescendant(elem, null, 'IMG', null, null, 1);
    changeMenuIconState(icon, null);
  }
}


/*
 * Disable single entry with given html ID.
 *
 * The given ID must point to the DIV or A surrounding a single entry.
 */
function disableMenuEntry(entryId) 
{
  var elem = findMenuEntryAnchor(entryId);
  if (!elem) return;

  // disable anchor element
  if (elem.className != 'disabled')
  {
    elem.className = 'disabled';
    // elem.onclick   = window.stopDefaultAction;
    var href = elem.href;
    if (href) {
      elem.setAttribute('savedHref', href);   // save old value
      // elem.href = '#';
      elem.removeAttribute('href');
      //alert("Saving old href: " + href + "\nelem.getAttribute('savedHref'): " + elem.getAttribute("savedHref") + "\nelem.href: " + elem.href);
    }
    
    // search for icon
    var icon = findDescendant(elem, null, 'IMG', null, null, 1);
    changeMenuIconState(icon, 'disabled');
  }
}


/*
 * Dynamic menu activation/deactivation
 * **********************************************************************
 *
 * The leading checkbox of any listing my be annotated with meta data using
 * the custom attribute 'itemProps'. This attribute must contain a space-separated
 * list of properties, where the last property is either the content MIME type
 * or the iFS class name, for example:
 *
 *   <input type="checkbox" ... itemProps="versioned image/png">
 *   <input type="checkbox" ... itemProps="plain FORUM">
 *
 * These meta data are evaluated by a item listener of the menu to determine, 
 * which menu entry fit to the current selection. The decision is based on
 * two custom attributes attached to each menu entry:
 *
 *  a) itemCount: allowed number of checked items: -1 = any, 0 = no, 1 = exactly one, 2 = one or more
 *  b) propExp:   space-separated list of regular expressions. **All** regular
 *                expressions must match the properties of **all** selected items,
 *                or the entry will be disabled
 *
 * TODO: implement NOT operator for 'propExp', i.e. a regular expression that must not match
 */


/*
 * Register new menu listener on given form item. The menu entries are dynamicly
 * activated/deactivated depending on selection state of given checkbox item.
 * During onload trigger, the function initializeItemListerner(menuId, itemCode)
 * is called to install the listener with the HTML form.
 * 
 * @param menuId         HTML ID of menu element
 * @param itemCode       some JavaScript code evaluated to retrieve checkbox items
 * @param hideDisabled   if true, disabled entries are hidden (display: none)
 */
function registerItemListener(menuId, itemCode, hideDisabled) 
{
  // register body.onload and body.onunload trigger with first call
  if (initFunctions.length == 0)
  {
    addOnloadTrigger("initMenus();");
  }

  var code = "initializeItemListener('" + menuId + "', '" + itemCode + "', " + hideDisabled + ")";
  // alert("Register menu(" + initFunctions.length + "): \n" + code);
  initFunctions[initFunctions.length] = new Function(code);
}


/*
 * Initialize the item listener as onclick or onkeydown event on HTML form element.
 *
 * Warning:
 * 1) Moz, IE and Opera fires the onclick event automaticly after any key press !!
 *    Moz and IE: only if no 'onkeypress' handler is defined.
 *    Opera: fires both events, 'onkeypress' and 'onclick', in this order.
 * 2) form.onchange does not work in IE but in most other browsers (fires during mouse up)
 *    item.onchange works in IE, but fires when the item looses fokus (in complience with W3C)
 * 
 * @param menuId         HTML ID of menu element
 * @param itemCode       some JavaScript code evaluated to retrieve checkbox items
 * @param hideDisabled   if true, disabled entries are hidden (display: none)
 */
function initializeItemListener(menuId, itemCode, hideDisabled) 
{
  var menu  = document.getElementById(menuId);
  var items = eval(itemCode);   // may be none, single element or array of elements
  //alert("initializeItemListener(): " + menuId +  "\nitemCode: " + itemCode + "\nitems: " + items);
  if (!menu) return;
  
  if (items)              // need at least one item to define form trigger
  {
    var form = (items.length) ? items[0].form : items.form;
    //alert("form: " + form);
  
    var handler = function() {
      adjustMenu(menu, items, hideDisabled);
    }
  
    // define form method to be called by 'select all' and 'context menu open'
    var oldHandler = form.adjustMenus;
    if (oldHandler) {
      // we do not return any value
      form.adjustMenus = function() { oldHandler(); handler(); };
    }
    else {
      form.adjustMenus = function() { handler(); }
    }
  
    //alert(form.adjustMenus);
    
    // register event handler
    // TODO: use form.onchange, form.onkeypress, item.onclick or actuator.onclick ?
    //for (var i=0; i<items.length; i++) items[i].onclick = handler;
    var oldTrigger = form.onclick;
    if (oldTrigger) {
      // return the value of the old Trigger
      form.onclick = function(e) { handler(); return oldTrigger(e); };
    }
    else {
      form.onclick = function(e) { handler(); return true;}
    }
    if (window.isDebug) {
      //logMessage(getNodeDescription(form) + ":\r\n" + form.innerHTML);
      //if (window.isDebug) logMessage("form.onclick: \r\n" + form.onclick);
    }
  }

  // do once during body.onload
  adjustMenu(menu, items, hideDisabled);
}

/*
 * Adjust all menu entries found in child list
 * @param menu           HTML DOM element of menu
 * @param items          array of checkboxes, single checkbox or null/undefined
 * @param hideDisabled   if true, disabled entries are hidden (display: none)
 * @param currItemCount  number of currently checked items, optional
 */
function adjustMenu(menu, items, hideDisabled, currItemCount) 
{
  //alert("adjustMenu: " + menu.id + ", " + items);
  if (!menu) return;
  
  // cache number of checked elements
  if (!(currItemCount >= 0))
  {
    if (items && items.length)
    {
      currItemCount = 0;
      for (var i=0; i<items.length; i++) if (items[i].checked) currItemCount++;
    }
    else
    {
      currItemCount = (items && items.checked) ? 1 : 0;
    }
  }
  
  // process menu entries
  var childNodes = menu.childNodes;
  for (var i=0; i<childNodes.length; i++) 
  {
    var child = childNodes[i];
    if (child && child.id)             // each entry must have a unique ID
    {
      var res = adjustMenuEntry(child, items, hideDisabled, currItemCount);
      if (res && child.className == "menuGroup")    // do we allow entry grouping ??
      {
        adjustMenu(child, items, hideDisabled, currItemCount);
      }
    }
  }
  
  //re-adjust separators
  if (hideDisabled) adjustSeparators(menu);
}


/*
 * Adjust single menu entry
 * @param entry          HTML DOM element of entry (DIV)
 * @param items          array of checkboxes, single checkbox or null/undefined
 * @param hideDisabled   if true, disabled entries are hidden (display: none)
 * @param currItemCount  number of currently checked items, mandatory
 */
function adjustMenuEntry(entry, items, hideDisabled, currItemCount)
{
  //alert("adjustMenuEntry: " + items);
  if (!entry || !entry.id) return false;
  
  // retrieve custom attributes from menu entry
  var count = entry.getAttribute("itemCount");  // -1: any, 0: no, 1: exactly one, 2: one or more
  var exp   = entry.getAttribute("propExp");    // e.g. "(DOCUMENT|FOLDER)"
  if (!count && !exp) return false;     // need at least one meta attribute
  //alert("adjustMenuEntry(" + entry.id + "): " + count + ", " + exp);

  var enable = true;

  // check selection count
  if (    (count == 0 && currItemCount != 0) 
       || (count == 1 && currItemCount != 1)
       || (count >  1 && currItemCount == 0) ) 
  {
    enable = false;
  }
  else if (exp)
  {
    // retrieve array of regular expressions
    var regExpArray = entry.regExpArray;         // first try cached value
    if (!regExpArray)
    {
      regExpArray = convertToRegExpArray( exp );
      entry.regExpArray = regExpArray;             // cache regular expressions
    }

    if (items)
    {
      var len = (items.length) ? items.length : 1;  // may be single element or array of
      for (var i=0; i<len; i++)                     // check all selected form items
      {
        var item = (items.length) ? items[i] : items;
        if (item && item.checked)
        {
          var props = item.getAttribute("itemProps");
          // alert(item.value + ": " + props);
          if (!props)
          {
            enable = false;
            break;
          }
          else
          {
            var allprops = item.expandedProps;
            if (!allprops)
            {
              allprops = expandProps( props );
              item.expandedProps = allprops;          // cache expanded list
            }
    
            for (var j=0; j<regExpArray.length; j++)  // check all properties required by menu entry
            {
              var regExp = regExpArray[j];
              var match  = (allprops.search(regExp) >= 0);
              if ( (regExp.matchInverted && match) || (!regExp.matchInverted && !match)) {
                enable = false;
                break;
              }
            }
          }
        }  // end if (item && item.checked) 
        if (!enable) break;
      }  // end for (var i=0; ...
    }
  }

  // activate/deactivate entry
  if (hideDisabled)
  {
    showMenuEntry(entry.id, enable);
  }
  else
  {
    enableMenuEntry(entry.id, enable);
  }
  return enable;
}


/**
 * Convert space-separated list of regular expressions into array of
 * RegExp objects. If the regular expression starts with the '!' sign, the
 * custom property 'matchInverted' is set and the '!' removed from string
 * before calculating the regular expression.
 * @param  exp  space-separted list of regular expressions
 * @return an array of RegExp objects
 */
function convertToRegExpArray( exp )
{
  var regExpArray = new Array();
  if (!exp) return regExpArray;
  var words = exp.split(' ');
  for (var i=0; i<words.length; i++)
  {
    var word = words[i];
    if (word)
    {
      var inverted = (word.length > 1 && word.charAt(0) == '!');     // invert regular expression, i.e. it must NOT match
      if (inverted) word = word.substring(1);
      var regExpString = word.replace(/([+?])/gi, "\\$1");
      var regExp       = new RegExp("\\b"+regExpString+"\\b","i");   // e.g. /\b(DOCUMENT|FOLDER)\b/i
      regExp.matchInverted = inverted;                              // save in custom property
      regExpArray[regExpArray.length] = regExp;
    }
  }
  return regExpArray;
}


/*
 * Expand properties using class hierarchy found in superClassMap
 * @param props  some space-separated property list ending with MIME type or class name
 */
function expandProps(props) 
{
  if (!props) return props;
  
  var idx = props.lastIndexOf(' ');
  var className = (idx >= 0) ? props.substring(idx+1) : props;  // MIME type or class name

  var superNames = superClassMap[className];
  //alert("class: " + className + " super class: " + superNames);
  if (superNames) {
    props += superNames;   // includes leading space
  }
  else if (className.indexOf('/') >= 0) {     // any MIME type expands to document
    props += ' ' + 'DOCUMENT';
  }
  return props;
}


/**
 * Adjust separators in such a way, that only the first of consecutive separators
 * is visible and no separator is shown either at the beginning or the end of
 * the list.
 */
function adjustSeparators(menu)
{
  var lastSep = null;        // last separator element
  var cc      = 0;           // number of visible entries after last separator
  var childNodes = menu.childNodes;
  for (var i=0; i<childNodes.length; i++) 
  {
    var child = childNodes[i];
    if (child.nodeType != 1) continue;      // process only elements
    if (child.tagName == 'HR') 
    {
      if (cc > 0) {
        if (child.style.display != '') child.style.display = '';          // enable separator
        lastSep = child;
        cc = 0;
      }
      else {
        if (child.style.display != 'none') child.style.display = 'none';  // disable separator
      }
    }
    else
    {
      var visible = (child.style.display != 'none');
      if (visible) cc++;
    }
  }
  // disable last separator, if at end of list
  if (lastSep && cc == 0) 
  {
    if (lastSep.style.display != 'none') lastSep.style.display = 'none';
  }
}
