ADxMenu

Towards smaller download

I have used this version of ADxMenu at the commercial web site I’m working on, where it has proven its abilities. It is tested in IE5+ and Mozilla Firebird 0.7 on Windows and it works like a charm. Opera 7.22 is still too-buggy and I’m ingnoring it for time being.

Rules for building HTML and CSS has changed once more, so bear with me.

Basic setup

Here is our plain and simple list:

    <div id="menu">
    
    <ul id="menuList">
    
    <li><a href="#">Main item</a></li>
    
    <li>
    
      <a href="#" class="submenu">Main menu item</a>
    
      <ul id="mmiMenu">
    
      <li><a href="#">Sub item</a></li>
    
      <li><a href="#" class="submenu">Sub item</a>
    
          <ul id="siMenu">...</ul>
    
      </li>
    
      </ul>
    
    </li>
    
    <li><a href="#">Main item</a></li>
    
    </ul>
    
    <div class="clearer"></div>
    
    </div>

We will float all items (LI elements) so I use enclosing div to avoid many problems when one wants to give border or background color to the main menu. After the menu, we have clearing div which is styled in CSS like this:

    div.clearer {
    
    	clear: both;
    
    	line-height: 0;
    
    	height: 0;
    
    }
    
    * html div.clearer {
    
    	font-size: 0;
    
    }

Second rule is only recognized by IE/Win. With first rule only, IE will still add small gap, so we need this one too. In Mozilla, if your layout has all floated columns, background of parent div (for those floated columns) will not be rendered if this is used, so we restrict it only to IE.

WCH script requires that each UL has its own ID, so this attribute is mandatory. Menu script needs (for IE) that each A element in the item which contains nested menu has class="submenu" so this is another mandatory rule.

If you need icons, give the appropriate IDs to list items, otherwise leave them be.

Styles…

Styling the menu has changed a lot. I will deal with it step by step (I admitt that all previous entries offered little help on this).

    #menu {
    
    	position: relative;
    
    	z-index: 10000;
    
    }

First off, make sure your div#menu is in the BODY z-index space (not contained in other positioned element) to avoid overlapping discussed in the previous post. We position it relatively, so that all calculations are done starting from this box.

Large z-index value is needed to place the menu above anything else, anywhere in the page.

    #menu ul {
    
    	margin: 0;
    
    	padding: 0;
    
    	border: 0;
    
    	list-style-type: none;
    
    }
    
    
    
    #menu li {
    
    	margin: 0;
    
    	padding: 0;
    
    	border: 0;
    
    	list-style-type: none;
    
    	float: left;
    
    	position: relative;
    
    }

These zeros are here to overcome differing indentation problem. IE and Opera use margin to indent the list while Gecko uses padding. We also remove the bullets, and as said before, float all list items to the left.

Each LI is relatively positioned, to define new coordinate system for submenu.

    #menu li ul {
    
    	visibility: hidden;
    
    	position: absolute;
    
    	z-index: 10;
    
    }
    
    
    
    #menu li ul li {
    
    	width: 100%;
    
    	display: block;
    
    	position: relative;
    
    }

At first load, all nested (sub) menus are hidden and positioned absolutely. z-index value is given because it is required for WCH to operate properly (if you remember, it will place iFrame one z-index step below the layer).

Submenu items are expanded to take full width of the list. Remember this - if you want to give padding to LI elements for one particular style, actual width in CSS2-compliant browsers will be: width you set on parent UL + your padding.

    #menu li:hover > ul {
    
    	visibility: visible;
    
    }

This is it! Definitive example of the power of CSS2 selectors. This is all that’s needed for menu to popup. Sadly, 99% of end-users will not benefit from this. So, read more.

    #menu {
    
    	color: #000;
    
    	margin: 0;
    
    	padding: 0;
    
    }
    
    
    
    #menu li {
    
    	width: auto;
    
    	min-width: 60px;
    
    }
    
    
    
    #menu li ul {
    
    	color: #000;
    
    	background-color: #fff;
    
    	border: 1px solid #ccc;
    
    	width: 150px;
    
    }

This is an example of basic styling of the list, just to make it visually obvious what is happening. Each submenu is given proper width (or you’ll get ugly rendering in Gecko).

    #menu a {
    
    	text-decoration: none;
    
    	color: #000;
    
    	padding: 5px;
    
    	display: block;
    
    }
    
    
    
    #menu li ul a {
    
    	padding: 4px 1px 5px 5px;
    
    }
    
    
    
    #menu li ul a.submenu {
    
    	padding: 4px 16px 5px 5px;
    
    	background-image: url(arrow.gif);
    
    	background-repeat: no-repeat;
    
    	background-position: 100% 7px;
    
    }
    
    #menu li ul a.submenu:hover {
    
    	background-position: 100% -16px;
    
    }

For IE’s script need, we have given class="submenu" to each A element in the list item with submenu. This comes handy as we need to insert pointing arrow. I used simple windows-like icon (transparent version of course), which has two parts.

Upper part is initially shown, while the lower part contains the highlighted version. Switching between the two is done using Pixy’s fast rollovers. Keep in mind that you need to compensate for the item’s height - it’s basically trial and error system, until you get it done.

    * html #menu a {
    
    	padding: 5px 10px;
    
    	width: .1em;
    
    	white-space: nowrap;
    
    }
    
    * html #menu li ul a {
    
    	padding: 4px 16px 5px 5px;
    
    	width: 100%;
    
    }
    
    * html #menu li ul {
    
    	width: 130px;
    
    }

IE will not make A element as wide as possible when display:block is used, so we need to give it 100% width. Since we also have padding, in IE6 this will expand the items for padding-left + padding-right value, so the actual UL must be shortened by such value.

But then, this will create problems in IE5.x because of its wrong box model which can be resolved using one of many variations of Tantek’s hack (not shown here, for simplicity sake).

That width: .1em bit is there to fix the clickable region for IE and white-space is there to stop each item from wrapping. This property is not supported in IE 5.0x, so you either set some intermediate width for it or ignore it (as IE 5.0x is declining in use). There is one stupid bug in IE 5.0x when you do font-weight: bold on a:hover, last word will dissappear. I first thought it is going below the visible portion, but it is not - it just vanishes. Pay attention to that.

And that’s it. You can see this in Examples section, as Basic style. Check out other examples for various make-up styles.

…and moves

As said before, CSS2 browsers does not need scripting. You can simply use the CSS only version and be happy playing with top and left properties for nested ULs. If you have the privilege to work in some intranet environment where only Gecko or something similar is used.

In real world, we have to deal with IE. And we also have to deal with small screen and sometimes rather long menus. And that usability issue can’t be solved with CSS. It never will be, since that is not CSS domain. So you might need scripting for CSS2 browsers too, at least for this.

Script uses X library and WCH script, so you must have them around. It also uses AttachEvent script, since both menu and WCH have function that should be executed on page load.

I have used » to signify line-break (in real code, you need to connect these lines and remove the marker).

From its begining, ADxMenu had support for multiple menus on the same page. On page load, we define one global array that will keep the pointers to starting, main UL.

    var ADXM_nIndex = 0;
    
    var ADXM_oMainMenu = new Array();

This function will initialize the menu and it should be executed on window.onload:

    function ADXM_InitializeMenu(sMenuID, sMenuLayout) {
    
      //	deny all non-DOM browsers
    
      if (!document.getElementById && !document.documentElement)
    
        return null;
    
    
    
      ADXM_oMainMenu[ADXM_nIndex] = xGetElementById(sMenuID);
    
      if (ADXM_oMainMenu[ADXM_nIndex]) {
    
        //  if layout is not passed, assume menu is horizontal
    
        if ( !xDef(sMenuLayout) || "H|V".indexOf(sMenuLayout) == -1 )
    
          sMenuLayout = "H";
    
    
    
        //  save menuID, and add some properties to use
    
        ADXM_oMainMenu[ADXM_nIndex].id = sMenuID;
    
        ADXM_oMainMenu[ADXM_nIndex].curmenu = null;
    
        ADXM_oMainMenu[ADXM_nIndex].main = true;
    
    
    
        //  finally, process menu items to give
    
        //  them behavior guidelines
    
        ADXM_ProcessItems(ADXM_oMainMenu[ADXM_nIndex], sMenuLayout, ADXM_nIndex);
    
    
    
        //  increase index for additional menus
    
        ADXM_nIndex++;
    
      }
    
    }

curmenu parameter is used by WCH when hiding all iFrames from top parent to last child. main parameter is used by IE 5.0x, since it uses old-school show/hide workaround for windowed controls.

After that, we goto item processing, and increment global menu index.

To processing function we pass menuLayout parameter, which can be “H” or “V”. As the code above shows, default is horizontal menu. This layout is used only for main menu - all submenus are vertical, as the processing function will show.

Processing functions are different for IE and rest of the crowd (which I assume are all CSS2 compliant). Lets dissect CSS2 script first, as it is much simpler.

Processing for CSS2 browsers
    //  array of this menu's items
    
    var aMenuLIs = ADXM_GetChildsByTagName(oMenuUL, "LI");
    
    for (var i=0;i<aMenuLIs.length;i++) {
    
      oMenuLI = aMenuLIs[i];
    
      oMenuLI.nIndex = nIndex;
    
      //  now position items and make room for features
    
      aUL = ADXM_GetChildsByTagName(oMenuLI, "UL");
    
      if (aUL.length)  {  //  has submenus
    
        ...
    
      }
    
    } //for

We first use custom function (common for all DOM browsers) to fetch all LI child elements for particular menu (UL passed as oMenuUL parameter). Each of them is processed in turn and we first save global menu index, and then check are there nested menus. If yes, then we have work to do:

    oUL = aUL[0];
    
    
    
    oMenuLI.submenu = oUL;
    
    oMenuLI.menuLayout = sMenuLayout;
    
    
    
    oMenuLI.onmouseover = function() {
    
      ADXM_SetMenuPos(this);
    
    };
    
    
    
    ADXM_ProcessItems(oUL, "V", nIndex);

We save the pointer to submenu on LI level, as well as layout. Then we add onmouseover function, which will basically position the submenu. As said before, we don’t need to do this using DOM - but since we also want to check for off-screen excursions, we can make life easier and use DOM for this too.

In the end, we recursivelly call the processing function (passing “V” as menu layout) to process the items in nested list.

Gecko browsers (and Opera too) don’t have problems with windowed controls (actually, Opera has problems with object element) so we don’t use WCH for it.

Processing for IE/Win

    var aMenuLIs = ADXM_GetChildsByTagName(oMenuUL, "LI");
    
    
    
    for (var j=0;j<aMenuLIs.length;j++) {
    
      oMenuLI = aMenuLIs[j];
    
      //  array of all A elements in the menu
    
      aMenuAs = ADXM_GetChildsByTagName(oMenuLI, "A");
    
    
    
      for (i=0;i<aMenuAs.length;i++) {
    
        //	if this one has no submenu, continue
    
        if ( aMenuAs[i].className.indexOf("submenu") == -1 )
    
    	  continue;
    
        ...
    
      }
    
    } //for

Processing is a bit different here. We start with child LI as well, but then, for each item we fetch the child A elements. We said that, for IE, submenu is marked by placing class="submenu" in A element. So, we check for that, and if class name does not match, we jump to next A element (if any…).

If marker is found, then the processing continues similar to above:


    aUL = ADXM_GetChildsByTagName(oMenuLI, "UL");
    
    oUL = aUL[0];
    
    
    
    // indicate that this is not main menu
    
    oUL.main = false;
    
    
    
    // saved for compatibility with IE 5.0x and for WCH
    
    oMenuLI.nIndex = nIndex;
    
    oMenuLI.parentMenu = oMenuUL;
    
    
    
    // used for positioning
    
    oMenuLI.submenu = oUL;
    
    oMenuLI.menuLayout = sMenuLayout;

Few stuff are added, for compatibility with IE 5.0x and WCH script, but it is all straight-forward.


    oMenuLI.onmouseover = function() {
    
      ADXM_SetMenuPos(this);
    
    
    
	// hide windowed controls, if WCH is present
    
      if (typeof(WCH_HideWndCtrl) != "undefined")
    
        WCH_HideWndCtrl(this.submenu, this);
    
    
    
      // show submenu
    
      xShow(this.submenu);
    
    
    
      // save for WCH
    
      this.parentMenu.shownMenu = this.submenu;
    
    };

First, position the menu where it should be. Then, call WCH to hide windowed controls below the menu (condition is there for script to work when WCH is not present).

Since IE does not recognize adjacent selectors, we need to set visibility programmaticaly, using xShow function from X-DOM. In the end, we save parent-child pointer that will be used by WCH.


    oMenuLI.onmouseout = function() {
    
      // hide submenu
    
      xHide(this.submenu);
    
    
    
      // show windowed controls
    
      ADXM_HideWCH(ADXM_oMainMenu[this.nIndex]);
    
    };

onmouseout function is doing the opposite - if hides the menu and all WCH objects. WCH hiding should always start from the top menu list as we can’t be sure how much submenus are opened when moving mouse from one item to the next. Event model makes sure that WCH object below the current menu is always present, as each submenu is inside of some LI element and thus its onmouseover eventhandler will be fired.

As before, we recursively call the processing function for nested list.

Now, lets deal with WCH object hiding.


    function ADXM_HideWCH(oMenu) {
    
      var curmenu = oMenu.shownMenu;
    
      var prevMenu;
    
      while ( curmenu ) {
    
      	...
    
      }
    
    
    
      // show windowed controls 4 IE5.0x
    
      // it should be done only when all menus are hidden
    
      if (typeof(WCH_ShowWndCtrl4Oldies) != "undefined" && oMenu.main)
    
        if (!WCH_bSupportHider)
    
          WCH_ShowWndCtrl4Oldies();
    
    }

We pass the pointer to main UL list and start the hiding from its shown submenu. If this is IE 5.0x, it does not support WCH objects, so for it we need to call old-school workaround.


    // show windowed controls for current child menu
    
    if (typeof(WCH_ShowWndCtrl) != "undefined")
    
      WCH_ShowWndCtrl(curmenu);
    
    
    
    // save the pointer to current child menu
    
    prevMenu = curmenu;
    
    
    
    // prepare the next child menu
    
    curmenu = curmenu.shownMenu;
    
    
    
    // kill the shownMenu info for current child menu
    
    prevMenu.shownMenu = null;

Inside of while loop above we first hide the WCH object, then save the pointer to the submenu of current menu and then remove the pointer to this menu as it is not needed anymore. Loop will go as deep as needed, depending on the submenus opened.

Positioning

Common function for all browsers is ADXM_SetMenuPos(), with one argument (oItem) - pointer to LI element with nested list.

    
    var nLeft, nTop;
    
    var oMenu = oItem.submenu;
    
    if (oItem.menuLayout == "H") {
    
      nLeft = 0;
    
      nTop = xHeight(oItem);
    
    } else {
    
      nLeft = xWidth(oItem);
    
      nTop = 0;
    
    }
    
    xMoveTo(oMenu, nLeft, nTop);

For starters, we place the menu to its default position, based on this item menu layout (on the left for vertical item, at the bottom for horizontal item). Easy so far. Lets check is menu fully visible.

    
    var nW = xWidth(oMenu);
    
    var nH = xHeight(oMenu);
    
    // get page coordinates</span>
    
    var nPageX = xPageX(oMenu);
    
    var nPageY = xPageY(oMenu);
    
    // get available client dims</span>
    
    var nClientW = xClientWidth();
    
    var nClientH = xClientHeight();
    
    if ( nClientW != 0 && nClientH != 0 ) {
    
      ...
    
    }

We need the width and height of the menu, its position in page coordinate system and the available client space. Only if the last bit of information is known, we can continue.


    var nDiffX = nClientW + xScrollLeft() - (nPageX + nW);
    
    if (nDiffX < 0) {
    
      if (ADXM_bIsIE) {
    
        nLeft = parseInt(nW/2);
    
        nTop = -nH + xHeight(oItem);
    
        xTop(oMenu, nTop);
    
      } else {
    
        nLeft = -nW;
    
      }
    
    }

correct placement in Firebird

First, check the X-axis. If menu is placed outside the visible page area, then we need to move it somewhere. Natural position is on the right of the item, which is done for CSS2 browsers. Currently, I was able to test this only in Firebird 0.7 - more tests are needed to verify whether this is good decision.

overlapping in IE

In IE, this is not possible, because of overlapping with the rest of the items in parent menu (Firebird is fine with this, as can be seen in the figure above). So, only reasonable thing to do is move towards the top. This is not good, as I don’t check whether menu now goes above the page visible top. I also moved it to half-width, which is again not very good as I don’t check for nW/2 > nDiffX.

Yes, all this can be checked, but in the end, we will end up in the corner and no place to go from there. So, instead of much complicated code, I believe this will suffice. Just keep this in mind when you plan your menus.


    var nDiffY = nClientH + xScrollTop() - (nPageY + nH);
    
    if (nDiffY < 0) {
    
      nTop += nDiffY;
    
    }
    
    if (nDiffX < 0 || nDiffY < 0)
    
      xMoveTo(oMenu, nLeft, nTop);

Now we check for going below the page visible bottom and simply move the menu up. Again, no check if this actually moved the menu above the page visible top, for the same reason as above. That is the problem of small screen, problem which can be solved, but I believe that it’s an overkill to do it here. Simply - too much code that will rarely be used.

Pheew.

What’s left to do?

Plenty.

Opera wierdness

Opera 6 should be kicked out (I didn’t tested it there). I did tested with Opera 7.22 which shows that it still has problems with CSS2, so I will ignore it by now. I hope that Norse guys will fix it soon.

Then, it needs to be tested in Mac browsers, as well as Unix ones. IE 5 on Mac is bound to have problems because of floats with no width set and who knows what else. I’m waiting for one particular G4 iMac to arrive at my office, so I can deal with it.

As for Unix, that is a problem to test. If you are using one, please post URL to screenshots, using comments below.

Have fun, until the next version.