/* CASCADING POPUP MENUS v5.2 RC (c) 2001-2006 Angus Turnbull, http://www.twinhelix.com Altering this notice or redistributing this file is prohibited. */ // This is the full, commented script file, to use for reference purposes or if you feel // like tweaking anything. I used the "CodeTrimmer" utility availble from my site // (under 'Miscellaneous' scripts) to trim the comments out of this JS file. // *** COMMON CROSS-BROWSER COMPATIBILITY CODE *** // This is taken from the "Modular Layer API" available on my site. // See that for the readme if you are extending this part of the script. var isDOM=document.getElementById?1:0, isIE=document.all?1:0, isNS4=navigator.appName=='Netscape'&&!isDOM?1:0, isIE4=isIE&&!isDOM?1:0, isOp=self.opera?1:0, isDyn=isDOM||isIE||isNS4; function getRef(i, p) { p=!p?document:p.navigator?p.document:p; return isIE ? p.all[i] : isDOM ? (p.getElementById?p:p.ownerDocument).getElementById(i) : isNS4 ? p.layers[i] : null; }; function getSty(i, p) { var r=getRef(i, p); return r?isNS4?r:r.style:null; }; if (!self.LayerObj) var LayerObj = new Function('i', 'p', 'this.ref=getRef(i, p); this.sty=getSty(i, p); return this'); function getLyr(i, p) { return new LayerObj(i, p) }; function LyrFn(n, f) { LayerObj.prototype[n] = new Function('var a=arguments,p=a[0],px=isNS4||isOp?0:"px"; ' + 'with (this) { '+f+' }'); }; LyrFn('x','if (!isNaN(p)) sty.left=p+px; else return parseInt(sty.left)'); LyrFn('y','if (!isNaN(p)) sty.top=p+px; else return parseInt(sty.top)'); LyrFn('vis','sty.visibility=p'); LyrFn('bgColor','if (isNS4) sty.bgColor=p?p:null; ' + 'else sty.background=p?p:"transparent"'); LyrFn('bgImage','if (isNS4) sty.background.src=p?p:null; ' + 'else sty.background=p?"url("+p+")":"transparent"'); LyrFn('clip','if (isNS4) with(sty.clip){left=a[0];top=a[1];right=a[2];bottom=a[3]} ' + 'else sty.clip="rect("+a[1]+"px "+a[2]+"px "+a[3]+"px "+a[0]+"px)" '); LyrFn('write','if (isNS4) with (ref.document){write(p);close()} else ref.innerHTML=p'); LyrFn('alpha','var f=ref.filters,d=(p==null),o=d?"inherit":p/100; if (f) {' + 'if (!d&&sty.filter.indexOf("alpha")==-1) sty.filter+=" alpha(opacity="+p+")"; ' + 'else if (f.length&&f.alpha) with(f.alpha){if(d)enabled=false;else{opacity=p;enabled=true}} }' + 'else if (isDOM)sty.opacity=sty.MozOpacity=o'); function setLyr(v, dw, p) { if (!setLyr.seq) setLyr.seq=0; if (!dw) dw=0; var o = !p ? isNS4?self:document.body : !isNS4&&p.navigator?p.document.body:p, IA='insertAdjacentHTML', AC='appendChild', id='_sl_'+setLyr.seq++; if (o[IA]) o[IA]('beforeEnd', '
'); else if (o[AC]) { var n=document.createElement('div'); o[AC](n); n.id=id; n.style.position='absolute'; } else if (isNS4) { var n=new Layer(dw, o); id=n.id; } var l=getLyr(id, p); with (l) if (ref) { vis(v); x(0); y(0); sty.width=dw+(isNS4?0:'px') } return l; }; if (!self.page) var page = { win:self, minW:0, minH:0, MS:isIE&&!isOp }; page.db = function(p) { with (this.win.document) return (isDOM?documentElement[p]:0)||body[p]||0 }; page.winW=function() { with (this) return Math.max(minW, MS ? db('clientWidth') : win.innerWidth) }; page.winH=function() { with (this) return Math.max(minH, MS ? db('clientHeight') : win.innerHeight) }; page.scrollX=function() { with (this) return MS ? db('scrollLeft') : win.pageXOffset }; page.scrollY=function() { with (this) return MS ? db('scrollTop') : win.pageYOffset }; // *** POPUP MENU MAIN OBJECT CONSTRUCTOR *** // This takes arrays of data and names and assigns the values to a specified object. function addProps(obj, data, names, addNull) { for (var i = 0; i < names.length; i++) if(i < data.length || addNull) obj[names[i]] = data[i]; }; // Pass the name of the menu object being created, e.g. var abc = new PopupMenu('abc'); function PopupMenu(myName) { this.myName = myName; // Manage what gets lit and shown when. Override these on a per-menu-object basis. this.showTimer = this.hideTimer = this.showDelay = 0; this.hideDelay = 500; // 'menu': the main data store, contains subarrays for each menu e.g. pMenu.menu['root'][]; this.menu = {}; // litNow and litOld arrays control what items get lit in the hierarchy. // For instance, litNow['root'] will return the item number lit in that menu. this.litNow = {}; this.litOld = {}; // The item the mouse is currently over. Used by click processor to help NS4. this.overM = ''; this.overI = 0; // Hide menus on document click? Off by default. this.hideDocClick = 0; // The active menu, to which addItem() will assign its results. this.actMenu = null; // Every time we declare a new PopupMenu(), add it to our internal list. PopupMenu.list[myName] = this; }; // The internal list of popup menu objects created. PopupMenu.list = {}; // A quick reference to save a few bytes :). var PmPt = PopupMenu.prototype; // Called by the other mouse event functions, this runs the menu-wide and per-item // onmouseover/out/click events and returns their return values. PmPt.callEvt = function(mN, iN, evt) { var i = this.menu[mN][iN], r1 = this[evt] ? this[evt](mN, iN) : 0, r2; if (i[evt]) { if (i[evt].substr) i[evt] = new Function('mN', 'iN', i[evt]); r2 = i[evt](mN, iN); } return typeof r2 == 'boolean' ? r2 : r1; }; // *** MOUSE EVENT CONTROL FUNCTIONS *** // Most of these are passed the relevant 'menu Name' and 'item Number' from page events. // The 'with (this)' means it uses the properties and functions of the current menu object. PmPt.over = function(mN, iN) { with (this) { // Cancel any pending menu hides from a previous mouseout. clearTimeout(hideTimer); // Set the object-global 'over' variables to point to this item. overM = mN; overI = iN; // Call the 'onmouseover' events for the menu and this item. // 'evtRtn' is checked later and no menus shown if set. var evtRtn = iN ? callEvt(mN, iN, 'onmouseover') : 0, rtn = evtRtn||false; // Remember what was lit last time, and compute a new hierarchy. litOld = litNow; litNow = {}; var litM = mN, litI = iN; // Loop and keep adding the currently lit item number to our associative array. if (mN) do { litNow[litM] = litI; // These will become undefined at the top of the hierarchy, stopping the loop. // (Remember that root menus doesn't have a parent). litI = menu[litM][0].parentItem; litM = menu[litM][0].parentMenu; } while (litM); // If the two arrays are the same, return... no use hiding/lighting otherwise. // This stops unnecessary operations as mouse events will bubble and fire several times. var same = 1; for (var z in menu) same &= (litNow[z] == litOld[z]); if (same) return rtn; // Otherwise, if this is a different item being moused over, clear any pending shows. clearTimeout(showTimer); // Cycle through menu array, lighting and hiding menus as necessary. for (var thisM in menu) with (menu[thisM][0]) { // Menu layer doesn't exist yet (or frame has navigated)? Keep rollin'... if (!lyr) continue; // The 'new Item' and 'old Item' in this menu that are lit. lI = litNow[thisM]; oI = litOld[thisM]; // If an item is lit now and wasn't before, highlight it, // and if another item was lit but isn't now, dim it. if (lI != oI) { if (lI) changeCol(thisM, lI); if (oI) changeCol(thisM, oI); } // Clear the "show onclick submenus" flag of that menu if nothing's highlighted. if (!lI) clickDone = 0; // Don't manipulate visibilities for 'root'-type menus -- continue loop. if (isRoot) continue; // Otherwise, make sure if it's lit, it's shown, and if it's unlit, it's hidden. // We call the doVis() function of the menu object, with a name and boolean value. // It handles the positioning and visibility side of things. if (lI && !visNow) doVis(thisM, 1); if (!lI && visNow) doVis(thisM, 0); } // Get target menu to show for 'sm:' items - if we've got one, position & show it. nextMenu = ''; // Only do this if onmouseover() didn't "return false" (via the return value 'evtRtn'). if (menu[mN] && menu[mN][iN].sm && (evtRtn+'' != 'false')) { // The target menuname, and the layer object of the current menu container. var m = menu[mN], t=m[iN].sm; // No target menu? if (!menu[t]) return rtn; // DYNAMIC MENU CREATION SUPPORT - Uncomment these next lines to enable it. // This creates only the root menu on page load, and others when you point at them. // You'll also have to edit the commented "Events" file to make it work. //if (!menu[t][0].lyr) update(false, t); //if (!menu[t][0].lyr) return 0; // If the target menu is set to show submenus on click, and it hasn't been recently // clicked, don't do any onmouseover actions! if (m[0].clickSubs && !m[0].clickDone) return rtn; // Either position/show immediately or after a delay, via the doVis() function. // Set nextMenu to the impending show, so the popOut() function knows when not to cancel it. nextMenu = t; if (showDelay) showTimer = setTimeout(myName+'.doVis("'+t+'", 1)', showDelay); else doVis(t, 1); } // Return the value from our event handlers, so that "return true" onmouseover settings work. return rtn; }}; // Handles onmouseout events, same parameters. Sets a timeout to another over() to hide menus. PmPt.out = function(mN, iN) { with (this) { // Sometimes, across frames, overs and outs can get confused. // So, return if we're exiting an item we have yet to enter... if (mN!=overM || iN!=overI) return; // thisI = A quick reference to this item. // Run both global and per-item onmouseout events, if any are set. Check for cancels. var thisI = menu[mN][iN], evtRtn = iN ? callEvt(mN, iN, 'onmouseout') : 0; // Stop showing a submenu from this level if this item isn't pointing to the same one. if (thisI.sm != nextMenu) { clearTimeout(showTimer); nextMenu = ''; } // Dim/hide all menus rapidly (if it's a root menu item without a popout) or as specified. // Remember that the timeout is cancelled by another instance of the over function. // Calling 'over("", 0)' hides all menus but the root menu(s), and highlights no items. // If hideDelay equals zero (or onmouseout() returns false) the menus are never hidden. if (hideDelay && (evtRtn+'' != 'false')) { var delay = menu[mN][0].isRoot && !thisI.sm ? 50 : hideDelay; hideTimer = setTimeout(myName + '.over("", 0)', delay); } // Clear the 'over' variables. overM = ''; overI = 0; }}; // Called onclick with menu & item. Handles frame/file navigation or special items. PmPt.click = function(mN, iN) { with (this) { // A reference to this menu, the event handler's return value, and a 'hide menu?' flag. var m = menu[mN], evtRtn = callEvt(mN, iN, 'onclick'), hm = 1; // Did the event handler 'return false'? If so, we're out of here too. if (evtRtn+'' == 'false') return false; // Otherwise, decide what to do based on the type of the item. // * Targeting another popout: Either activate show-on-click or skip to the end. // Also record this menu as actively clicked so click-submenus show. // * A JavaScript function: eval() it and nothing else. // * File link: navigate the correct window then hide menus. with (m[iN]) { if (type == 'js:') eval(href); else { if (sm && m[0].clickSubs) { m[0].clickDone = 1; doVis(sm, 1); hm = 0; } if (href) { type = type || 'window'; eval(type + '.location.href="' + href + '"'); } } } // Unless this is show-on-click, we hide all menus by calling over() with no menuname. if (hm) over('', 0); // Return either a returned value, or false if none specified. return evtRtn||false; }}; // Called by the over() function, this will highlight or dim individual items. // It will call itself repeatedly for colour fading items. // Last 'fc' parameter indicates a fading background colour loop, called by a timeout. PmPt.changeCol = function(mN, iN, fc) { with (this.menu[mN][iN]) { // Quit for uncreated/unreferenced items, we can't change their colours! if (!lyr || !lyr.ref) return; // Decide whether this item has a background image (outCol contains a period?). // Set it to 0 if our colours are the same, so we don't change the value. var bgFn = outCol!=overCol ? (outCol.indexOf('.')==-1 ? 'bgColor' : 'bgImage') : 0; // Are we highlighting/dimming the item, and do we do text/alpha effects (doFX)? // We don't do text/alpha effects if the lit item isn't changing, or we're fading. var ovr = (this.litNow[mN]==iN)?1:0, doFX = (!fc && this.litNow[mN]!=this.litOld[mN]); // The default colour for non-fading items, set to over or out colour. var col = ovr ? overCol : outCol; // If this item has a colour fade set, do the math... if (fade[0]) { // Clear the existing timer, and reset our colour for calculated hex digits. clearTimeout(timer); col = '#'; // Increment the fading counter in the proper direction and speed (fade[ovr][1]). count = Math.max(0, Math.min(count+(2*ovr-1)*parseInt(fade[ovr][0]), 100)); // Since Konqueror as of v3.1 doesn't support Number.toString(radix), we need a hack. // What the heck were they thinking/smoking when they omitted that? :P var oc, nc, hexD = '0123456789ABCDEF'; // Loop through remaining fade[], to calculate 3 new hex pairs (0xRR, 0xGG and 0xBB). for (var i=1; i<4; i++) { // Make a new hex pair based on weighted averages of the out/over array indices. oc = parseInt('0x'+fade[0][i]); nc = parseInt(oc + (parseInt('0x'+fade[1][i])-oc)*(count/100)); col += hexD.charAt(Math.floor(nc/16)).toString() + hexD.charAt(nc%16); } // Set a timer to call this function again later if it's not at the start or end. // Pass the 'fc' parameter to indicate not to do alpha/text/border swaps. if (count%100 > 0) timer = setTimeout(this.myName+'.changeCol("'+mN+'",'+iN+',1)', 50); } // Regular colour change: do before text/border change due to Netscape 4 bugs. if (bgFn && isNS4) lyr[bgFn](col); // Test for CSS text/border style changes, we can skip them if not needed. // In Netscape 4, rewrite layer contents if required (causes a little flickering)... // Otherwise manipulate the DOM tree for IE/NS6+ (faster than rewriting contents). // We always rewrite if text/indicator changes are specified, of course. var reCSS = (overClass!=outClass || outBorder!=overBorder); if (doFX) with (lyr) { if (!this.noRW && (overText || overInd || isNS4&&reCSS)) write(this.getHTML(mN, iN, ovr)); if (!isNS4 && reCSS) { ref.className = (ovr ? overBorder : outBorder); var chl = (isDOM ? ref.childNodes : ref.children); if (chl && !overText) for (var i = 0; i < chl.length; i++) chl[i].className = ovr?overClass:outClass; } } // ...and some other browsers have bugs unless the colour change is AFTER the border. if (bgFn && !isNS4) lyr[bgFn](col); // Alpha filtering activated? Might as well set that then too... // Weirdly it has to be done after the border change, another random Mozilla bug... if (doFX && outAlpha!=overAlpha) lyr.alpha(ovr ? overAlpha : outAlpha); }}; // The position() function is passed either a menuname to position that menu, or nothing // to reposition all menus. It's called internally before menus are shown, and by the events // section of the code to reposition menus after window scrolling and resizing. PmPt.position = function(posMN) { with (this) { for (mN in menu) if (!posMN || posMN==mN) with (menu[mN][0]) { // If the menu hasn't been created or is not set to be visible, loop. if (!lyr || !lyr.ref || !visNow) continue; // Set up some variables and the initial calculated positions. var pM, pI, newX = eval(offX), newY = eval(offY); // Find its parent menu references, if it is not a 'root' class menu. if (!isRoot) { pM = menu[parentMenu]; pI = pM[parentItem].lyr; // Having no parent item is a bad thing, especially in cross-frame code. if (!pI) continue; } // Find parent window for correct page object, or this window if not. var eP = eval(par), pW = (eP&&eP.navigator ? eP : window); // Find proper numerical values for the current window position + edges, so menus // don't make a beeline for the upper-left corner of the page. Set window dimensions // to 9999px if they can't be found. with (pW.page) var sX=scrollX(), wX=sX+winW()||9999, sY=scrollY(), wY=winH()+sY||9999; // Relatively positioned submenus? Add parent menu/item position & check screen edges. // Subtract either 5px or 20px at the end to allow for padding and NS scrollbars. var sb = page.MS?5:20; if (pM && typeof(offX)=='number') newX = Math.max(sX, Math.min(newX+pM[0].lyr.x()+pI.x(), wX-menuW-sb)); if (pM && typeof(offY)=='number') newY = Math.max(sY, Math.min(newY+pM[0].lyr.y()+pI.y(), wY-menuH-sb)); // Assign the final calculated positions. lyr.x(newX); lyr.y(newY); } }}; // Default show/hide function, pass a menu name and true/false visibility setting to it. // They either call the animation functions, or just plain show/hide the menu. PmPt.doVis = function(mN, show) { with (this) { // References -- mA is to a possible menu animation function name. var m = menu[mN], sh = (show?'show':'hide'), mA = sh+'Menu', mE = 'on'+sh; // Record the visibility state of this menu, whatever happens. m[0].visNow = show; // Watch for users who pass wrong menunames to addItem commands, or cross-frame menus... if (m && m[0].lyr && m[0].lyr.ref) { if (show) position(mN); // Set the Z-Index of this menu to slightly more than its parent menu. var p = m[0].parentMenu; if (p) m[0].lyr.sty.zIndex = m[0].zIndex = menu[p][0].zIndex + 2; // Call onshow and onhide events. if (this[mE]) this[mE](mN); // Either call the animation function or just show/hide it ourselves. if (this[mA]) this[mA](mN); else m[0].lyr.vis(show?'visible':'hidden'); } }}; // *** MENU OBJECT CONSTRUCTION FUNCTIONS *** function ItemStyle() { // Like the other constructors, this passes a list of property names that correspond to the list // of arguments. var names = ['len', 'spacing', 'popInd', 'popPos', 'pad', 'outCol', 'overCol', 'outClass', 'overClass', 'outBorder', 'overBorder', 'outAlpha', 'overAlpha', 'normCursor', 'nullCursor']; addProps(this, arguments, names, 1); }; PmPt.startMenu = function(mName) { with (this) { // Create a new array within the menu object if none exists already, and a new menu object within. if (!menu[mName]) menu[mName] = [{}]; // Clean out existing items in this menu in case of a menu update. // actMenu is a reference to this menu for addItem() function later, while the local variable // aM is a quick reference to the current menu descriptor -- array index 0, 1+ are items. actMenu = menu[mName]; aM = actMenu[0]; actMenu.length = 1; // Not all of these are actually passed to the constructor -- the last few are null. var names = ['name', 'isVert', 'offX','offY', 'width', 'itemSty', 'par', 'clickSubs', 'clickDone', 'visNow', 'parentMenu', 'parentItem', 'oncreate', 'isRoot']; addProps(aM, arguments, names, 1); // extraHTML is a string added to menu layers for things like dropshadows, backgrounds etc. aM.extraHTML = ''; // Set the menu dimensions to zero initially. Also these are used to position items. aM.menuW = aM.menuH = 0; // The default menu z-index. aM.zIndex = 1000; // Reuse old layers if we can, no use creating new ones every time the menus refresh. if (!aM.lyr) aM.lyr = null; // Set the flag for 'root 'menus and set an oncreate event to show them initially. // oncreate is passed a reference to the main PopupMenu object. if (mName.substring(0, 4) == 'root') { aM.isRoot = 1; aM.oncreate = new Function('obj', 'this.visNow=1; obj.position("' + mName + '"); ' + 'this.lyr.vis("visible")'); } // Return a reference to this menu data array entry for tweaking. return aM; }}; PmPt.addItem = function() { with (this) with (actMenu[0]) { // 'with' the current menu object and active menu descriptor object from startMenu(). // Add these properties onto a new 'active Item' at the end of the active menu. var aI = actMenu[actMenu.length] = {}; // Add function parameters to object. Again, most will be undefined, set later. var names = ['text', 'href', 'type', 'itemSty', 'len', 'spacing', 'popInd', 'popPos', 'pad', 'outCol', 'overCol', 'outClass', 'overClass', 'outBorder', 'overBorder', 'outAlpha', 'overAlpha', 'normCursor', 'nullCursor', 'iX', 'iY', 'iW', 'iH', 'fW', 'fH', 'overText', 'overInd', 'sm', 'lyr', 'onclick', 'onmouseover', 'onmouseout']; addProps(aI, arguments, names, 1); // Find an applicable itemSty -- either passed to this item or the menu[0] object. var iSty = arguments[3] ? arguments[3] : actMenu[0].itemSty; // Loop through its properties, add them if they don't already exist (overridden e.g. length). for (prop in iSty) if (aI[prop]+'' == 'undefined') aI[prop] = iSty[prop]; // For 'sm:' items, set the 'sm' property and clear the 'href'. if (aI.type == 'sm:') { aI.sm = aI.href; aI.href = '' } // Regular expression for swapping text and popout indicator on mouseover. // Alias r to RegExp to save space, as Konq 3 has some problems using 'with (RegExp)'. var r = RegExp, re = /^SWAP:(.*)\^(.*)$/; // Split text and/or indicator to over and out properties if required. if (aI.text.match(re)) { aI.text = r.$1; aI.overText = r.$2 } if (aI.popInd.match(re)) { aI.popInd = r.$1; aI.overInd = r.$2 } // For the colour fading, pull properties out of the nn#RRGGBB format colours. aI.timer = aI.count = 0; aI.fade = []; for (var i = 0; i < 2; i++) { // Set oC to the name of the property to check, either 'outCol' or 'overCol'. var oC = i ? 'overCol' : 'outCol'; // The ItemStyle 'nn#RRGGBB' fading speed format: use a regex to match it. if (aI[oC].match(/^(\d+)\#(..)(..)(..)$/)) { // Reset the outCol and overCol values to normal for the div-writing routines. aI[oC] = '#' + r.$2 + r.$3 + r.$4; // Then store a subarray of fade speed and 3 hex colour calues if detected. aI.fade[i] = [r.$1, r.$2, r.$3, r.$4]; } } // In NS4, since borders are assigned to the contents rather than the layer, increase padding. if (aI.outBorder && isNS4) aI.pad++; // Non-IE browsers use a different cursor name for the 'hand' cursor. // I'm aware that 'pointer' is the W3C standard, I just prefer 'hand' by default :). if (!isIE) { if (aI.normCursor=='hand') aI.normCursor = 'pointer'; if (aI.nullCursor=='hand') aI.nullCursor = 'pointer'; } // The actual dimensions of the items, store here as properties so they can be accessed later. aI.iW = isVert ? width : aI.len; aI.iH = isVert ? aI.len : width; // The spacing of the previous menu item in this menu, if relevant. var lastGap = actMenu.length > 2 ? actMenu[actMenu.length - 2].spacing : 0; // 'spc' is the amount we subtract from this item's position so borders overlap a little. // Of course we don't do it to the first item. var spc = aI.outBorder && actMenu.length > 2 ? 1 : 0; // We position this item at the end of the current menu's dimensions, // and then increase the menu dimensions by the size of this item. if (isVert) { menuH += lastGap - spc; aI.iX = 0; aI.iY = menuH; menuW = width; menuH += aI.iH; } else { menuW += lastGap - spc; aI.iX = menuW; aI.iY = 0; menuW += aI.iW; menuH = width; } // Return the reference so we can tweak the item ourselves :). return aI; }}; // *** MAIN MENU HTML CREATION/UPDATE FUNCTIONS *** // Returns the inner HTML of an item, used for menu generation, and highlights in NS4. // (In these functions, just call alert() if you want to figure out what's being created :). PmPt.getHTML = function(mN, iN, isOver) { with (this) { var itemStr = ''; with (menu[mN][iN]) { // Retrieve the appropriate values for the CSS "text Class", "text" and "popout Indicator". // Also pre-compose a little common item text; including a link tag with the filename as the // HREF will help tabbed browsers a lot. var tC = isOver?overClass:outClass, txt = isOver&&overText?overText:text, popI = isOver&&overInd?overInd:popInd, ln = '' + popI + ''; else itemStr += '