The Blog

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
var nPageX = xPageX(oMenu);
var nPageY = xPageY(oMenu);
// get available client dims
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.

Banca

Banca

Beautiful and functional currency converter, supports just about any currency in the world.

Go Couch to 5k

Go Couch to 5k

The most popular starter running program in beautiful feature-rich app (GPS tracking, charts, detailed history etc)

Quickie to do

Quickie to do

The fastest short-term task-list / check-list app on the App Store. Really.

Guerrilla Cardio

Guerrilla Cardio

The most challenging high-impulse interval training in the world.

Run Mate

Run Mate

A versatile running coach app, with unlimited number of running programs. Perfect for casual runners.

11 Comments

Feel free to chime in, looking forward to it. Leave a Comment

  1. apartness says:

    Very nice article, but see the comment I had on ‘Suckerfish Dropdowns’ in the ‘Talk about it’ section of alistapart.com:

    http://www.alistapart.com/discuss/dropdowns/7/#c5629

  2. aleck says:

    Thanks. For the sake of thread completeness, I responded on ALA.

  3. Zoe says:

    Unfortunately none of the examples work correctly for me on NN7.1 on WinXP. Each menu displays in the same place, rather than below its corresponding button.

  4. Zoe, you are right. It seems that Mozilla team has introduced proper support for DOM in a later version of its core then the one used by NN 7.1. I just checked, they used 1.4, which is really suprising (I test with Firebird 0.7 which relates to 1.5).

    I would consider this a bug, but will certainly try to see is there a simpe workaround. If not, I will let it be, since I don’t want to introduce browser tweaks, unless they are absolutely neccesary.

    I started testing with Mac browsers too, and it seems that Safari (KHTML engine actually) has a bug with LI:onmouseout so the menu stays open once you open it.I should have timeto deal with it all in few days.

  5. Tim says:

    I’ve added a few lines to ADxMenu.js to address the Netscape 7 problems temporarily. In case people are interested I’ll include some snippets below.

    In the function ADXM_SetMenuPos I added a few lines below these two:

    var nLeft, nTop;
    var oMenu = oItem.submenu;

    became

    var nLeft, nTop;
    var oMenu = oItem.submenu;
    // Netscape 7 hack
    var isNS = false;
    var HoffsetLeft = 149;
    var HoffsetTop = 70;
    if (ADXM_ua.indexOf(“netscape/7″) >= 0) isNS = true;

    Then inside the next conditional:

    if (oItem.menuLayout == “H”) {

    I added my own conditional for Netscape 7
    if (isNS) {
    nLeft = xLeft(oItem) + HoffsetLeft;
    nTop = xTop(oItem) + xHeight(oItem) + HoffsetTop;
    } else {
    nLeft = 0;
    nTop = xHeight(oItem);
    }

    The offset variables I added were hardcoded and basically just set a starting point which is where the menu is supposed to start. Then I had to add the xLeft and xTop functions to get additional placement info. Now they work fine in my layout. It’s an ugly hack and I will look at any possible easier ones in the meantime. I think one improvement I immediately see with this getting rid of the offset variables I setup and using xTop and xLeft on the menu id. It didn’t seem to work at first so I’ve just hardcoded the values for now.

  6. Tim, did you check the current examples (I think they are online from December) in N7? I have tweaked the CSS and all the examples back then, to work properly in Netscape 7.02/Win and Netscape 7.1/Mac (those two I have installed).

    Horizontal main menu is opening as expected, and submenus are opening 30px to the right (since positioning stuff does not work in N7).

    Give them a try.

  7. Tim says:

    Aleksandar, yes those examples worked fine in my install of Netscape 7.1 but for some reason the placement of those menus in the current project I’m working on has an effect on them causing all the submenus to appear in the top left corner of the site.

    If you’re interested in any screenshots or code examples of this specific case let me know. I forgot to add another snipped in my last post regarding the vertical submenus. I had to change the top position from 0 to xTop(oItem). After these few changes the menus were working correctly in Netscape 7.1.

    By the way I’m using the latest version of your script.

  8. aleck says:

    Sure Tim, post the link, I will take a look when I find time (next week probably).

    Menus are very dependent on the surrounding elements and the CSS used. I don’t like that very much but until the better solution, we will have to cope with that.

    Just today, I found out that positioning library (x.js) does not work correctly in IE 6 when calculating xPageX in some cases. Work in progress… :(

  9. Tim says:

    Aleck I won’t be able to post any links since this is a redesign for equifax. All the work is still internal but if were interested in seeing the code I could email you a zip.

    I know the surrounding elements can have an effect on the menu and it may not be worth addressing. I’m still trying out different solutions hoping to keep it a clean list based menu.

  10. Russell says:

    Can you provide some more info about the x.js xPageX problem? When have you noticed the problem? I’m using X for a project of mine in development, and I want to make sure that things won’t fall apart for IE6 at some point.

  11. Russell, problem is when you use it in the following scenario (commonly found in my ADxMenu script).

    LI is float:left|right and pos:rel, and UL inside of it has left:100% nad pos:abs. In IE, offsetLeft for that UL is always 0. Same goes for offsetTop. This problem then translates to xPageX and xPageY. There is no way I could find to get around this. Mozilla works properly, as do Safari 1.2.

    I’m totally rewriting ADxMenu, both CSS and JS parts. I can happily report that I have it working beatifuly in IE 6 — I’ve even solved the “phone-book” problem (all new basic CSS part was the key). I have problems in IE5 and I have much to test for in other browsers so it should wait a bit until published.

Comments are now closed for this article.