Archive for December, 2007

Suckerfish menus with hide delay

Saturday, December 8th, 2007

You’re using Suckerfish Dropdown menus but you would like to prevent the dropdown menus from disappearing the moment the users mouse steps 1 pixel outside the dropdown?

This question is especially relevant with multi-level dropdown menus; multi-level menus and laptop track-pads are something I’ll never get used to!

The code presented below is designed to be a drop-in replacement for the standard suckerfish Javascript. The original code was only required to make MSIE work correctly, the replacement code targets all browsers. If Javascript has been disabled by the user, then the CSS will function as before.

sfHover = function() {
	var timeout = 600;
	var cssClass = "sfhover";

	var queue = [];
	var reCSS = new RegExp("\\b" + cssClass + "\\b");
	var sfEls = document.getElementById("nav").getElementsByTagName("li");
	for (var i=0; i<sfEls.length; i++) {

		// mouseover and mouseout handlers for regular mouse based interface.
		sfEls[i].onmouseover = function() {
			queueFlush();
			this.className += " " + cssClass;
		}
		sfEls[i].onmouseout = function() {
			queue.push([setTimeout(queueTimeout, timeout), this]);
		}

		// focus and blur handlers for keyboard based navigation.
		sfEls[i].onfocus = function() {
			queueFlush();
			this.className += " " + cssClass;
		}
		sfEls[i].onblur = function() {
			queue.push([setTimeout(queueTimeout, timeout), this]);
		}

		// click event handler needed for tablet type interfaces (e.g. Apple iPhone).
		sfEls[i].onclick = function(e) {
			if (this.className.search(reCSS) == -1) {
				// CSS not set, so clear all sibling (and decendants) menus, and then set CSS on this menu...
				var elems = this.parentNode.getElementsByTagName("li");
				for (var i=0; i<elems.length; i++) {
					elems[i].className = elems[i].className.replace(reCSS, "");
				}
				this.className += " " + cssClass;
			} else {
				// CSS already set, so clear all decendant menus and then this menu...
				var elems = this.getElementsByTagName("li");
				for (var i=0; i<elems.length; i++) {
					elems[i].className = elems[i].className.replace(reCSS, "");
				}
				this.className = this.className.replace(reCSS, "");
			}
			if (e && e.stopPropagation)
				e.stopPropagation();
			else
				window.event.cancelBubble = true;
		}
	}

	queueFlush = function () {
		while (queue.length) {
			clearTimeout(queue[0][0]);
			queueTimeout();
		}
	}

	queueTimeout = function() {
		if (queue.length) {
			var el = queue.shift()[1];
			el.className = el.className.replace(reCSS, "");
		}
	}
}
addLoadEvent(sfHover);

The code also includes handlers for focus and blur events, to allow site visitors to tab through the menus without using a mouse.

And finally, a click event handler is added to parent menus so that they can be opened on devices that don’t support hover events such as the iPhone/iPod Touch. This might cause a problem if your parent menu item is also a link however!

Oh… and finally finally, the above code calls the an addLoadEvent() function to force sfHover to run on page load. If you don’t already have a similar function, here’s one you can use:

function addLoadEvent(func) {
	var oldonload = window.onload;
	if (typeof window.onload != 'function') {
		window.onload = func;
	} else {
		window.onload = function() {
			oldonload();
			func();
		}
	}
}

UPDATE 2008-05-19: I have uploaded a very basic example page here, and the JavaScript is available for download here.