Annotated Flyout Menus Source

The flyout source has evolved greatly since its inception in early 2000. There is still quite a bit of legacy code still in the flyouts which is updated as time permits. This page documents the source of version 2.10 from January, 2008.

Global Initialization

The first thing the file does is test whether the browser supports DHTML, and runs some initialization.

Global Variables

Most variables are static class variables, but there are a couple global variables.

var d = document;

The only purpose of this variable is to make an alias for the document variable.

FlyLyr.on = 0;

The FlyLyr.on variable is used to tell us whether flyouts will be enabled.

These scripts will only run on browsers which implement the Document Object Model. If that's the case, then make sure other functions know that it's OK to run. We also check whether the client browser is running on a Mac and whether it is Opera or Konqueror, for later use.

initFlyLyr ();
initDelay ();

Each of the classes defined by this script need some initialization, so we do that here.

The last thing we do is define the default style for the flyout menu layers. This works because the script is being called in the HEAD protion of the HTML document, which is where styles also need to be defined. Note we set a negative margin for the right and bottom sides of the menus; this is needed for IE which will expose a scrollbar if we position directly at the edge of the windows, and that will cover the right (or bottom) side of the menu. Setting the margins prevents IE from doing this, and does no harm to other browsers.

We also define styles used in generated menus, which also have the flyoutgen class. The main table cells have 2 pixels of padding, and the

Layer Creation

The Flyout Menus are impemented as hidden layers. When a menu should be displayed, the menu is made visible. You can choose to provide your own layer or have one dynamically created.

useLayer

This function is called with predefined menus. One application for using useLayer () instead of makeLayer () is if you have a list of links with flyouts as sublists. Note that in most cases useLayer () will detach the object from the normal HTML stream and reattach it directly in the body of the document.

We first make sure that this browser can support what we want to do before continuing. We then look for an object with the same id as the trigger image but with l_ prepended to the name. If we can't find the object, we silently fail.

We first call a function which will tag whether we are a submenu. Then, if the user has defined a function to be called before we detach the object from its normal position in the document, we call that now.

Finally, we make the object a direct child of the document's body, so we'll have much more freedom to position it. However, if this is a child menu, or if the page is positioning the menu with CSS, we do not make it a direct child of the document's body.

makeLayer

This is the function called by the user to create the hidden layers. The layer consists of a table with rows of menu blocks (the title is a block, as are any items grouped together by a dividing line.) Each item block is also a table, and the rows of the inner table could consist of several cells, depending on the indentation level.

The other way the function is called is with a list of strings. This is how the function is called when a user defines a layer. We need to define an image object to pass to any potential submenus. If we can find the image (and it really is an image), we use the source and class of that image. If we don't have an image, then we use the default image object and no class. If the user has set a default inactive image, we use that instead in either case.

We make sure to always have a cls member of the image object. If we don't do that, then instead of getting an empty string later, we'd get undefined.

var id = a[0];
var title = a[1];

Once we have our array of strings, we know that the first one is the tag for the arrow image, and the second one is the optional menu title.

The first thing we write is the start of the flyout menu layer and the outermost table, which will be responsible for drawing the border of the table itself as well as any divider lines (we do not use CSS to define the border, since some browsers use different shading for CSS borders vs. borders defined with the border attribute). If a title is defined for this menu, write that, along with the appropriate colors. We use the makeCell function to translate the string to the actual cell.

We then start the first text cell. Since we'll also use this string after any division lines, we save it for later use. We also predefine a string which we will use to start a row for each entry, in which we use a shortcut to test for whether the item should be right-aligned (if the alignright default is set, then return the align="right" string, otherwise return an empty string).

var havesubmenu = 0;
for (var j = 2; j < a.length; ++j) {

We declare a flag which says whether we've created a submenu, then begin a loop over all of the arguments which are items (the first two are the menu's identifier and title.)

We go ahead and start the row, since we we're going to have a flyout item. We save a copy of the item with all the leading spaces removed. The difference between the length of the original string and this shorter one tells us how many spaces were removed, which also tells us how much indentation should be added. We add that space with non-breaking spaces. Our CSS will make sure the spaces use a small font.

var submenu = makeCell (s, img);

We then write the cell, but this time the second argument will contain the image object if this item defines a submenu. The makeCell function will return the name of the submenu if this item defines one.

If this item did have a submenu, we create an array with the submenu tag, removing all the items after the current one up to the line which signals the end of the submenu. For example, if the submenu had a name of submenu1, we look for the string <submenu1. Next, we call the function to create the submenu, then set the flag which says we've created a submenu.

The last thing to do is close off the tables and layer, then create an object for us to use which will contain information about this layer.

makeCell

This function takes the string defining either the title or an entry and writes a cell containing the item. If the entry has a = then text after that will become the link. The second argument to the function is the image to use if we have a submenu.

Start building the string we'll return, making sure to define the class. The style class to use is the second argument. If we were passed arguments, add that to the cell definition. Be sure to close the table cell tag.

If the item contains a >, that means there will be a submenu. We save the string after the >, and build the event arguments we will add to the link. We also build the string we'll use for the arrow image.

We write out the link item and possible image. We then return the tag of our submenu if there is one.

Positioning Layers

Netscape 4, DOM-based browsers (such as Mozilla), Internet Explorer, and Opera all have different Document Object Models, which can make code for positioning layers very complicated. The approach we take is to use one function to do all positioning, but call browser-dependent functions where the behaviors differ. We also attempt to normalize the variable names used for position and size.

positionLayer

This function, which is only called if CSS is not being used to position the menus, is where all the computations are done to figure out where to place the menus. First we do the default positioning, and then if complex positioning is being used, apply that. Finally, make sure the menus will still be in the window.

We first make sure that we know whether we are a submenu. We know that tagParent will either set flyParent to the parent menu or to null, which when tested will return false. However, typeof null will return object, so we use that fact to test whether tagParent has been called yet.

if (FlyLyr.isMac && document.all)
this.positionLayer ();
}

Internet Explorer/Mac exhibits a strange bug when using menus generated with makeLayer. These menus appear behind the document when they are first positioned, and then appear above the document (as expected) the second time. This part of the code does a second positioning, but only after the first time. Just setting the position twice (at the bottom of positionLayer) isn't sufficient.

We next grab the image against which we do all alignment, since it will be used often in this function. To simplify the code in this function, we want to make sure that a common set of variable names is used for position and size of the object, so we call a function which does that. Finally, we want to have a common set of variables for the menu layer and the document itself.

These lines perform the default positioning. For horizontal, we want to position the left edge to the right of the image, and add a little bit of space (which can be overridden by changing the hpad default.) If positionleft is selected, we instead want the right edge of the menu to be near the left edge of the image.

For vertical positioning, we want the top of the image and the top of the menu to align, and then adjust according to the vpad default.

For each rule, we figure out whether we'll be adjusting the menu horizontally or vertically. If we find neither, abort the use of this rule. However, if we do have a proper direction character, save it for later use. Note we cannot use str[pos] because Internet Explorer doesn't recognize the use of strings as arrays of characters.

We next figure out the image we need to use for positioning. If the image name is IMG then we use the image already associated with this menu. If the image name is anything else, we find that object and normalize its size and position variables. If we can't find the image, ignore the rule.

We now compute the target positions of both the named object and the menu. The targetPos function takes the string representing the position of an object in which we're interested, its left or top position, and its width or height.

We save the relation character, and test if the menu and image are correctly aligned. Note that if the relation is = then we know we need to force the position of the menu. If we find that the menu needs to be moved, we adjust the left or top edge as appropriate.

If the current menu is a submenu, we adjust the position to be relative to the position parent (the parent object which doesn't have static positioning). Finally, we actually position the menu.

getObjMetricsIE

Since Internet Explorer has an object model which does not define absolute positions for elements and uses different names for size, it's easier to map its position and size variables to ones that match DOM-based and Netscape browsers. The WebReference DHTML Diner has articles about determining element positions in Internet Explorer, but it does not take Internet Explorer 5.x for the Mac into account. As seen below, it has a quite unique implementation for setting of the position variables.

The main intent is to add together all the offsetLeft and offsetTop variables for objects in the offsetParent chain. However, because of the way IE on Windows treats the size and position of objects, we also need to take clientLeft and clientTop into account, except for the BODY and any TABLE elements. IE for the Mac is a bit different; some of the differences are explained here, others when we define the noCpos and noOpos objects.

The width and height are straightforward. Opera actually already uses .width and .height, so if we don't find .offsetWidth or .offsetHeight we use them. We also save a pointer to the original object, since we'll be traversing up the positioning hierarchy.

We start with zero as our position, and we'll adjust down and to the right for every object in the positioning hierarchy. For the Mac, we need to keep track of whether we're in the innermost table or one which is further up the hierarchy.

For IE on Windows, we do not take into account the clientLeft and clientTop variables for the BODY element, since we are positioning within the body itself. However, with IE on the Mac, we need to use those variables to our position, but only if the object is directly positioned within the body, and then return, because we don't want to use any other offsets in our computations. If the object has undefined clientLeft and clientTop properties, then use offsetLeft and offsetTop instead. Note this code will not correctly work if the image is positioned at the upper-left corner.

In general, we are only adding the clientLeft and clientTop positions for elements which we haven't already declared to not use client values, and only adding the offsetLeft and offsetTop positions for elements for which we haven't declared to not use offset values. However, for IE on the Mac, we never add the client values for the object itself. In addition, Opera doesn't define clientLeft or clientTop, so we never add those.

One other thing to check is there are some elements where we only use offsetLeft and offsetTop if they're not block elements (such as A objects for Internet Explorer). The way we indicate such elements is we set the value in noOpos for that object to -1 instead of 1.

IE on the Mac needs one more adjustment. Our position need to be moved down (only, not horizontally) by the amount of the table's cell spacing. However, we don't need that adjustment for the table which immediately contains our element, which is why we have a counter which will only return false with the innermost table. Note that cellSpacing is a string, even though it looks like a number, so we pass it through getInt.

getObjMetricsIE Bugs

There are some positioning bugs which crop up with certain HTML with IE on the Mac. If you have a table which has cells which span multiple rows, and the object we're targeting is in a cell which is in a row, and is to the right of the spanned cell, the size of that spanned cell will not be taken into account. For example:

cell A

same row as cell A

image in this cell

The image in the lower right cell cannot be used for positioning with IE on the Mac, because the position will not take the width of cell A into account.

There are some other bugs which crop up with IE on the Mac using tables and non-zero cell padding, spacing, and or borders, possibly while nested. These, however, have not been isolated as of yet.

IE on Windows doesn't correctly position in some cases, either, when there are absolutely positioned containers. It's possible this has to do with padding and/or borders set on the positioned container or its parent, but the cause has not yet been found.

getObjMetricsDOM

We need to also walk the container hierarchy for DOM-based browsers, but it's sufficiently enough different to make it easier to create a separate function.

Make sure that the width and height are defined. Note we prefer width and height over offsetWidth and offsetHeight, which is different than IE.

Some Gecko-based browsers (Mozilla 0.9.7 and above or browsers based on that build) have a bug in that if a table's border is not defined, and the frame attribute is defined, the position is off by one pixel in each direction.

getInt

We first use parseInt () to convert the string into an integer. However, if the string doesn't look like an integer, the function will return NaN, and any computations using NaN will also result in NaN. For our purposes, we'd rather use 0 than NaN.

targetPos

This is the function which computes where on an object or menu we should try to check the alignment. For example, if we're passed t then we return the top of the object. If we are passed c+10, then we return 10 pixels to the right of the horizontal center of the object.

In reality, the function treats horizontal and vertical alignment the same, assuming that it is correctly called. It also treats any characters other than those representing the top, bottom, left, and right of the object as the middle or center.

If the left or top of the object is to be the target, we return the amount of offset added to the start position. If the right or bottom is the target, we also add the length of the object. If neither of these, then we assume the center is desired, so we find only add half of the length to find the center.

posInWindow

This function makes sure that the window does not obscure the menu. There may be times, however, when the menu is too long or too wide to completely fit into the window, so we choose to make sure the top-left corner of the menu is always in the window, sacrificing the bottom and/or right side.

We are passed the current location (top or left), how tall or wide the object is, how far the window is scrolled in the horizontal or vertical direction, and the height or width of the window.

We first check that the menu isn't off the bottom or right of the window. If we're not using Internet Explorer (or Opera), then we also have to take the scrollbar into account. We do this by computing how much we'd need to move the menu to make sure the bottom (or right) is exactly at the bottom (or right) edge of the window. If it ends up that we need to move the menu up to do this, we do so. If we need to move the menu down then it's already completely visible, so we need to do nothing in that case.

if (loc < scroll)
loc = scroll;
return loc;
}

Next we turn our attention to the left or top of the menu. If we're above or to the left of where the window begins, we adjust the location so we're exactly at the top or left edge of the window.

We go through all positionable parent objects and look for the first one which has flyout as a class. It's sufficient to look through positionable objects since we know that the flyout class defines a new position frame of reference, since it declares absolute positioning.

If we don't think we're a submenu, we set the parent menu to ourselves.

FlyLyr Object

We define a wrapper object to contain information about the flyout menus. In addition, this gives us an object for which we can define different methods depending on which browser is being used. Netscape 4 allows methods to be added to predefined objects, but Internet Explorer does not, which is why we do not build directly onto the Layer object.

FlyLyr

This is the function which defines the FlyLyr object. It must be called after the actual layer has been defined in HTML.

function FlyLyr (id) {
this.lyr = findObj ('l_' + id);

Set a property to reference the actual menu layer. Note we do not check for its existence - if it isn't defined, the browser will error out.

We first cancel the timer which triggered the call to us, and then make sure that we know where the image object is.

if (this.position != 'CSS')
this.positionLayer ();

If this object isn't using only CSS positioning, we position the layer relative to the arrow image.

for (var l in FlyLyr.hideQueue)
if (FlyLyr.hideQueue[l])
FlyLyr.hideQueue[l].doHide ();

There is quite a bit subtlety in these three lines. If there are any menus in the to-hide queue, hide them now. This will hide sibling menus but not parent menus. As long as we have at least a small delay before showing menus, this will work. If menu "A" is showing, and then the mouse moves to menu "B", "A" will receive an onmouseout event, while "B" will receive an onmouseover event. When the pause to show a menu is shorter than the delay to hide one, we want to make sure that "A" goes away before we display "B".

This even works with multiple levels of menus. Say menu "A" is showing, as is child menu "A-1". When the mouse moves to the other child menu "A-2", the events sent are, in order:

"A" onmouseout, hide delay set

"A-1" onmouseout, hide delay set

"A" onmouseover, hide delay canceled; since menu is already showing, no show pause set

"A-2" onmouseover, show pause set

When "A-2"'s timer fires, it will hide all menus in the hide queue, or "A-1" in this example. If the user's mouse instead went into a different menu "B", both "A" and "A-1" would be hidden.

If a callback is defined to show the image, call that. If not, then put in the image used when a menu is showing.

this.realShow ();
FlyLyr.showing[this.id] = this;
};

The doShow () function will display the menu associated with the current object. The first thing we do is to double-check that we know where the arrow image is, since we won't be able to position the layer without that information. We don't do the same check when hiding the layer, since the layer won't be showing unless we've positioned it next to the arrow.

When all positioning is done, and unwanted menus hidden, we show our menu. We also indicate that the menu is being shown.

FlyLyr.queueHide

This function is called when we want to set a timer to hide the current menu.

Create a Delay object to call our own doHide () function when the timer expires. We also indicate that we're queued to be hidden. We make sure that there isn't already an event to hide this layer, since sometimes Opera 6 will send multiple events for the same trigger.

FlyLyr.queueShow

This function is just like queueHide, but queues our menu to be shown.

We define an object to contain our defaults, declare the initial defaults, then use them (calling flyDefs () with no arguments does this). We also define a positionLayer class method to be the function we defined above.

For Internet Explorer, we first define the necessary methods to find information about layers and other objects. Note that we explicitly test to make sure that the browser isn't Konqueror, which defines document.all but should use the routines for DOM-based browsers. Also, we don't need to map variables for Konqueror as we do with Internet Explorer.

When a DOCTYPE is declared in the HTML, IE6 for Windows uses the size of the actual content for clientWidth and clientHeight, rather than the window size. However, document.documentElement.clientWidth and clientHeight do represent the window size, so we use those if they're available.

Internet Explorer requires us to compute an image's position by also computing the position of the positionable containers. We also need to add the client variables for most containers, but not for BODY or TABLE containers. We therefore set up an object to let us know which containers to skip. We also define an object to tell us which offset variables to skip; IE on windows requires us to skip anchors if they are set to display as blocks.

Internet Explorer 5.0 for the Mac reverses the use of the client and offset variables, so we swap the definitions here. In addition, we must not use the client variables (which would be offset variables in IE for Windows) for DIV, TD or TH containers.

We have two different identifiers that we use, a unique ID and a timer ID. The first allows us to keep track of which Delay objects are still valid, and the other allows us to cancel a timeout if we need to. We can't use the timeout ID as the unique ID, since we only receive the timeout ID after we set a timeout. However, we need a unique ID to pass to the setTimeout () function. Using two different IDs solves this dilemma.

initDelay

As with other objects we've defined, we do some initialization.

function initDelay () {

Delay.stop

In addition to cancelling the timeout, the function clears this particular object from the dispatch array. Note the array will never get smaller; this is to work around a problem with older versions of Safari where it hangs after the stop () function has been called serveral times. The timeout objects themselves should get released, so only one array entry will remain each time this function is called.

Delay.dispatch

This is implemented as a class function rather than a member function, because it is called when we do not have a reference to a particular member.

If we can find the item, then we make sure the timer is stopped and then call the callback function.

Delay Variables

Delay.nuid = 0;
Delay.disparr = new Object;
}

In order to generate unique IDs, we need to seed our counter. Also, we need to make sure the dispatch array is an object.

Utility Functions

These functions are general utility functions.

findObj

We use this function to find objects on the page. Normally getElementById () would find the objects, but it's only guaranteed to work with id attributes. Items which have name attributes won't be found that way.

If getElementById () finds the object, we return it right away. If not, then we look to see if it is defined in the document object itself. If it isn't, and we're running IE, which means document.all is defined, we look in there.

Find the layer associated with this event. If we can't find the layer, we look to see if there's an object with the same name as our trigger image but with a l_ prepended. If we find such an object, we assume the user wants us to automatically use it.

if (FlyLyr.showing[id])
lyr.stopHide ();

If we've found the layer and it's is the one which is currently showing, cancel any previous pause which may be queued (to delay when the layer is shown.)

else
lyr.queueShow ();
}

Otherwise, we schedule a pause which will show the layer after it expires.

mOut

This is the event handler for onMouseOut.

function mOut () {
if (! Flylyr.on)
return;
if (! id) {

Again, we check whether the flyouts are enabled. For backward compatibility with older versions of the flyout menus, we test if there was no menu identifier passed to us.

for (var l in FlyLyr.showing)
if (FlyLyr.showing[l])
FlyLyr.showing[l].queueHide ();

For all menus which are showing (which should be at most one, the current one), queue it to be hidden.

for (l in FlyLyr.showQueue)
if (FlyLyr.showQueue[l])
FlyLyr.showQueue[l].stopShow ();

For all menus which are queued to be shown (as with the hide queue, the current menu should be the only one in the queue), remove them from the queue of menus to show.