Forum Moderators: open

Message Too Old, No Replies

Mouseover Menu Mystery with setTimeout

Using setTimeout for delays in a rollover menu

         

rchrd

9:06 pm on Jan 29, 2008 (gmt 0)

10+ Year Member



Hi all. This is my first post, and I'm a JS novice, so please bear with me.

I've got a menu with top links that shows or hides sub-items on mouseover/out and I need a delay 3 seconds delay before the show/hide is called so visitors can absorb the navigation concept. Simple... use setTimeout, right?

Unfortunately, I've tried inserting setTimeout in all kinds of places (onload, the script, links), but keep getting nada. Always checking syntax carefully, of course, I use:


setTimeout("closeItem()", 3000);

and

setTimeout("closeItem()", 3000);

This is really driving me insane. Any help would be appreciated.

Following are the script and menu. Thanks!


<html>
<head>
<script language="JavaScript" type="text/JavaScript">
function getItem(id)
{
var itm = false;
if(document.getElementById)
itm = document.getElementById(id);
else if(document.all)
itm = document.all[id];
else if(document.layers)
itm = document.layers[id];
return itm;
}
function closeItem(id)
{
itm = getItem(id);
if(!itm)
return false;
itm.style.display = 'none';
}
function openItem(id)
{
itm = getItem(id);
itm.style.display = '';
return false;
}
function init()
{
this.closeItem("sub1");
this.closeItem("sub2");
this.closeItem("sub3");
}
</script>
</head>
<body onload="init();">
<ul>
<li><a href="#" onMouseOver="openItem('sub1');closeItem('sub2');closeItem('sub3');" onMouseOut="return true;">Top Link 1</a></li></ul>
<ul id="sub1">
<li><a href="#">sub1a</a></li>
<li><a href="#">sub1b</a></li>
<li><a href="#">sub1c</a></li>
</ul>
<ul>
<li><a href="#" onMouseOver="openItem('sub2');closeItem('sub1');closeItem('sub3');" onMouseOut="return true;">Top Link 2</a></li></ul>
<ul id="sub2">
<li><a href="#">sub2a</a></li>
<li><a href="#">sub2b</a></li>
<li><a href="#">sub2c</a></li>
</ul>
<ul>
<li><a href="#" onMouseOver="openItem('sub3');closeItem('sub1');closeItem('sub2');" onMouseOut="return true;">Top Link 3</a></li></ul>
<ul id="sub3">
<li><a href="#">sub3a</a></li>
<li><a href="#">sub3b</a></li>
<li><a href="#">sub3c</a></li>
</ul>
</body>
</html>

Fotiman

10:41 pm on Jan 29, 2008 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Welcome!

1. <script language="JavaScript" type="text/JavaScript">
Don't use the language attribute. Also, the type should be "text/javascript" (I'm not sure if case matters, but it's common convention).

2. It's better to use setTimeout with a function reference vs. a string. See example below.

3. Your getItem method is really old school. All modern browsers support getElementById. There's no need to add all this extra logic.

4. You should avoid inline event handlers. Instead, use unobtrusive JavaScript to cleanly attach your event handlers, keeping your HTML nice and clean. It also makes it easier to get an idea what those with JavaScript disabled will see.

5. Your lists looked like the semantics were slightly wrong, so I made a little change.

6. Performance tip: Put scripts at the bottom of your page, so your content loads first.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style type="text/css">
ul {
list-style: none;
margin: 0;
padding: 0;
}
</style>
<title>Example</title>
</head>
<body>
<ul>
<li><a href="#sub1" id="top1">Top Link 1</a>
<div id="sub1">
<ul>
<li><a href="#">sub1a</a></li>
<li><a href="#">sub1b</a></li>
<li><a href="#">sub1c</a></li>
</ul>
</div>
</li>
<li><a href="#sub2" id="top2">Top Link 2</a>
<div id="sub2">
<ul>
<li><a href="#">sub2a</a></li>
<li><a href="#">sub2b</a></li>
<li><a href="#">sub2c</a></li>
</ul>
</div>
</li>
<li><a href="#sub3" id="top3">Top Link 3</a>
<div id="sub3">
<ul>
<li><a href="#">sub3a</a></li>
<li><a href="#">sub3b</a></li>
<li><a href="#">sub3c</a></li>
</ul>
</div>
</li>
</ul>
<script type="text/javascript">
var menuTimers = {
items : ['sub1', 'sub2', 'sub3'],
open : null,
close : null,
init : function() {
for (var i = 0; i < menuTimers.items.length; i++) {
var itm = document.getElementById(menuTimers.items[i]);
if (itm) { itm.style.display = 'none'; }
}
// Attach event listeners
var top1 = document.getElementById('top1');
top1.onmouseover = function() {
menuTimers.openItem('sub1');
}
top1.onmouseout = function() {
menuTimers.closeItem('sub1');
}
var top2 = document.getElementById('top2');
top2.onmouseover = function() {
menuTimers.openItem('sub2');
}
top2.onmouseout = function() {
menuTimers.closeItem('sub2');
}
var top3 = document.getElementById('top3');
top3.onmouseover = function() {
menuTimers.openItem('sub3');
}
top3.onmouseout = function() {
menuTimers.closeItem('sub3');
}
},
openItem : function(id) {
clearTimeout(menuTimers.open);
menuTimers.open = setTimeout(function() {
var itm;
// Hide others
for (var i = 0; i < menuTimers.items.length; i++) {
if (id!= menuTimers.items[i]) {
itm = document.getElementById(menuTimers.items[i]);
if(itm) { itm.style.display = 'none'; }
}
}
// Show this one
itm = document.getElementById(id);
if(itm) { itm.style.display = ''; }
}, 1000); // Set the timeout value here. I used 1 second for testing.
},
closeItem : function(id) {
clearTimeout(menuTimers.close);
menuTimers.close = setTimeout(function() {
var itm = document.getElementById(id);
if(itm) { itm.style.display = 'none'; }
}, 1000); // Set the timeout value here. I used 1 second for testing.
}
}
window.onload = menuTimers.init;
</script>
</body>
</html>

Fotiman

10:48 pm on Jan 29, 2008 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Also, note that in my example above, I've created only 1 item in the global scope: menuTimers. That variable contains an object which has 3 public properties (items, open, and close), and 3 public methods (init, openItem, and closeItem). Why did I do that? Because it's good practice to minimize the amount of global variables you create with your scripts. That way, you reduce the risk of a conflict with any 3rd party scripts you might want to include, while at the same time utilizing an object oriented design.

If you have any questions on the changes I've made above, please don't hesitate to ask. :)

rchrd

11:57 pm on Jan 29, 2008 (gmt 0)

10+ Year Member



Woah... not only does this do the trick but it's soo much leaner and meaner. That is way clever! And not only do I have a working solution, but my code will be smarter here on out. Thanks so much for all the thorough explanation, Fotiman! Awesome.

rchrd

4:32 am on Jan 30, 2008 (gmt 0)

10+ Year Member



Follow-up question: What would be the best way to allow for selective always-open functionality?

For example, if a user clicked "sub2b" and was directed to the "sub2b" page in the "Top Link 2" section, what would be the best way to have the entire "Top Link 2" menu open on that page, as well as all pages in that section?

Fotiman

7:42 pm on Jan 30, 2008 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month




For example, if a user clicked "sub2b" and was directed to the "sub2b" page in the "Top Link 2" section, what would be the best way to have the entire "Top Link 2" menu open on that page, as well as all pages in that section?

One way would be to give the body of your page an id. Then in your script, check the value of the body id to know whether or not the menu should be opened.

rchrd

8:51 pm on Jan 30, 2008 (gmt 0)

10+ Year Member



Aha. Thank you. I think I underdstand:

1. Write a function that sets display to visible only


keepopen : function (id) {
itm = getItem(id);
itm.style.display = '';
return false;
}

2. If the body has an id where a menu is to be always open, run that
function on that menu (my syntax is horrible... guidance would be greatly appreciated here), something like:


if (body.id =='top2page') = keepopen.top2;

Would this method sitll work within the rest of the script, so that when a user mouseovers the always-open menu, the open/close function isn't called?

Fotiman

4:19 pm on Jan 31, 2008 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Below is an updated method. I've done a couple things.
1. Your body should now have an id with the format "page-" + unique menu sub value. For exampe:
page-sub1
page-sub2
page-sub3

2. I've replaced the items array with a menus array. This is an array of objects. Each object has 2 properties:
top - The id of the top link for the menu
sub - The id of the sub link container

3. I've cleaned up the code that attaches the listeners to use the menus array (so you don't need to hand code each one).

4. Added some code that removes (ie - nulls out) the current page from the menus array.

It's worth noting that I've not really included a bunch of validation (for example, getting the id of the page I'm stripping off the first 5 characters without checking the length or verifying that it's in the correct format of "page-subN". It might be a good idea to add a few safety checks.

Here's the updated code:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<style type="text/css">
ul {
list-style: none;
margin: 0;
padding: 0;
}
</style>
<title>Example</title>
</head>
<body id="page-sub2">
<ul>
<li><a href="#sub1" id="top1">Top Link 1</a>
<div id="sub1">
<ul>
<li><a href="#">sub1a</a></li>
<li><a href="#">sub1b</a></li>
<li><a href="#">sub1c</a></li>
</ul>
</div>
</li>
<li><a href="#sub2" id="top2">Top Link 2</a>
<div id="sub2">
<ul>
<li><a href="#">sub2a</a></li>
<li><a href="#">sub2b</a></li>
<li><a href="#">sub2c</a></li>
</ul>
</div>
</li>
<li><a href="#sub3" id="top3">Top Link 3</a>
<div id="sub3">
<ul>
<li><a href="#">sub3a</a></li>
<li><a href="#">sub3b</a></li>
<li><a href="#">sub3c</a></li>
</ul>
</div>
</li>
</ul>
<script type="text/javascript">
var menuTimers = {
menus : [{top:"top1", sub:"sub1"},{top:"top2", sub:"sub2"},{top:"top3", sub:"sub3"}],
open : null,
close : null,
init : function() {
// Remove the current page from menus array
var page = document.body.id;
if (page) {
page = page.substring(5); // Strip off the 'page-' part of the id
}
for (i = 0; i < menuTimers.menus.length; i++) {
if (menuTimers.menus[i].sub == page) {
menuTimers.menus[i] = null;
continue;
}
var top = document.getElementById(menuTimers.menus[i].top);
var itm = document.getElementById(menuTimers.menus[i].sub);
if (itm) { itm.style.display = 'none'; }
// Attach event listeners
top.onmouseover = function(id) {
return function() {
menuTimers.openItem(id);
};
}(menuTimers.menus[i].sub);
top.onmouseout = function(id) {
return function() {
menuTimers.closeItem(id);
};
}(menuTimers.menus[i].sub);
}
},
openItem : function(id) {
clearTimeout(menuTimers.open);
menuTimers.open = setTimeout(function() {
var itm;
// Hide others
for (var i = 0; i < menuTimers.menus.length; i++) {
if (menuTimers.menus[i] == null) { continue; }
if (id!= menuTimers.menus[i].sub) {
itm = document.getElementById(menuTimers.menus[i].sub);
if(itm) { itm.style.display = 'none'; }
}
}
// Show this one
itm = document.getElementById(id);
if(itm) { itm.style.display = ''; }
}, 1000); // Set the timeout value here. I used 1 second for testing.
},
closeItem : function(id) {
clearTimeout(menuTimers.close);
menuTimers.close = setTimeout(function() {
var itm = document.getElementById(id);
if(itm) { itm.style.display = 'none'; }
}, 1000); // Set the timeout value here. I used 1 second for testing.
}
}
window.onload = menuTimers.init;
</script>
</body>
</html>

[edited by: Fotiman at 4:21 pm (utc) on Jan. 31, 2008]

rchrd

4:34 am on Feb 1, 2008 (gmt 0)

10+ Year Member



Poetry.

NOW I see! This is awesome. I couldn't get it clear in my head before how to selectively keep one menu always open, but the nulling out makes total sense (now that I actually see it). And good call attaching the listeners to use the menus array: even more elegant (and maintainable). I see your point about the safety check, too - but as long as the page ids are consistent, this should work just fine.

Again, Fotiman, thanks for all your help. Above and beyond, sir.

Fotiman

3:27 pm on Feb 1, 2008 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Always a pleasure. :-)