Wednesday, September 26, 2007

Javascript

Navigation: display

I've considerably changed my navigation frame since the writing this page. The code on this page still works, but I added a few features to my navigation frame which aren't described on this page. Study the code for the details.

The setNav() function doesn't work in Safari 1.0.

On this page I explain the navigation script I use in the navigation frame. See there for an example.

I knew exactly what I wanted when I wrote this script:

  1. I wanted pairs of labels and content blocks. Clicking on the label shows or hides the content block.
  2. I wanted unlimited nesting possibilities (though in practice I stuck to two nested levels).
  3. I didn't want to see one single event handler in the HTML of the page.
  4. I wanted the link to the page currently in the content frame to have a special style. This meant I had to write a script that finds this link.

The code

I devised the following code:

HTML

The pairing of labels and content blocks was very simple: a label is always followed by its paired content block, so label.nextSibling always accesses it (excluding empty text node problems).

<div class="label"> 	JavaScript </div> <div class="content"> 	<a href="js/intro.html" 		>Introduction</a> 	<a href="js/contents.html" 		>Table of contents</a> 	<a href="js/links.html">Links</a> 	<div class="label"> 		Building blocks 	</div> 	<div class="content"> 		<a href="js/placejs.html" 			>Placing JavaScript</a> 		<a href="js/support.html" 			>Object detection</a> 		[etc] 	</div> </div> 

CSS

The CSS isn't particularly complicated, see navi.css. The only thing I needed were some extra selectors to give a nested label a subtly different style from a first level label:

div.label { 	styles }  div.content div.label { 	overrule a few styles } 

JavaScript

I wrote the following script:

window.onload = function () { 	var x = document.getElementsByTagName('div'); 	for (var i=0;i<x.length;i++) 	{ 		if (x[i].className == 'label') 			x[i].onclick = clickNav; 	} 	closeNav(); 	if (top.setNav) 		setNav(top.setNav,'currentPage'); }  function closeNav() { 	var x = document.getElementsByTagName('div'); 	for (var i=0;i<x.length;i++) 	{ 		if (x[i].className == 'content') 			x[i].style.display = 'none'; 	} }  function clickNav(e) { 	if (!e) var e = window.event; 	if (e.target) var tg = e.target; 	else if (e.srcElement) var tg = e.srcElement; 	while (tg.nodeName != 'DIV') // Safari GRRRRRRRRRR 		tg = tg.parentNode; 	var nextSib = tg.nextSibling; 	while (nextSib.nodeType != 1) 		nextSib = nextSib.nextSibling; 	var nextSibStatus = (nextSib.style.display == 'none') ? 'block' : 'none'; 	nextSib.style.display = nextSibStatus; }  function setNav(page,newID) { 	var test = page.indexOf('#')+1; 	if (test) 		page = page.substring(0,test-1); 	var x = document.getElementsByTagName('a'); 	var i; 	for (i=0;i<x.length;i++) 	{ 		if (x[i].href == page) 		{ 			x[i].id = newID; 			break; 		} 	} 	if (i < x.length && newID == 'currentPage') 	{ 		var parDiv = x[i]; 		while (parDiv.parentNode.tagName == 'DIV') 		{ 			parDiv = parDiv.parentNode; 			parDiv.style.display = 'block'; 		} 	} } 

Explanation

onload

As soon as the page is loaded we prepare the navigation.

window.onload = function () { 

We go through all divs on the page

	var x = document.getElementsByTagName('div'); 	for (var i=0;i<x.length;i++) 	{ 

If a div has a class label we set the clickNav event handler.

		if (x[i].className == 'label') 			x[i].onclick = clickNav; 	} 

We close the entire navigation (see below)

	closeNav(); 

Finally we see if the top frame has a variable setNav. This variable is set by any page loaded into the content frame. If it exists, we call the setNav() function to give the link to this content page a special style (see below).

	if (top.setNav) 		setNav(top.setNav,'currentPage'); } 

closeNav()

Closing the entire navigation is extremely simple. We go through all divs, and if one has a class content we set its display to none.

function closeNav() { 	var x = document.getElementsByTagName('div'); 	for (var i=0;i<x.length;i++) 	{ 		if (x[i].className == 'content') 			x[i].style.display = 'none'; 	} } 

clickNav()

The function clickNav() is executed whenever the user clicks on a label. It first finds the target of the event (see the Events properties page for details).

function clickNav(e) { 	if (!e) var e = window.event; 	if (e.target) var tg = e.target; 	else if (e.srcElement) var tg = e.srcElement; 

Defeat a Safari peculiarity: in this browser the target is the text node you clicked on, not the element. So while the target is not a div we go to its parentNode.

	while (tg.nodeName != 'DIV') // Safari GRRRRRRRRRR 		tg = tg.parentNode; 

Then we take the nextSibling of the target. This is always the related content block because of the structure of the HTML. We continue until the nextSibling is an element node, because some browsers count the empty text node between the two divs.

	var nextSib = tg.nextSibling; 	while (nextSib.nodeType != 1) 		nextSib = nextSib.nextSibling; 

If the content block has display: block it should get display: none and vice versa. Find out which display it should get and change it.

	var nextSibStatus = (nextSib.style.display == 'none') ? 'block' : 'none'; 	nextSib.style.display = nextSibStatus; } 

setNav()

setNav() sets or removes the special style for the link to the page that's currently shown in the content frame.

It receives two arguments: the location.href of the page and the new ID the link should get. This last variable can have two values: 'currentPage' to set the special style, or '' (empty string) to remove it.

function setNav(page,newID) { 

First we remove any hash ('#somewhere') because it'd confuse our script.

	var test = page.indexOf('#')+1; 	if (test) 		page = page.substring(0,test-1); 

Then we go through all a elements in the navigation frame.

	var x = document.getElementsByTagName('a'); 	var i; 	for (i=0;i<x.length;i++) 	{ 

If the href of a links is equal to page (which contains the location.href of the page in the content frame), we set the id of the link and break the for{} loop.

		if (x[i].href == page) 		{ 			x[i].id = newID; 			break; 		} 	} 

One more thing: if we set the special style (but not if we remove it) the link should become visible. We must set the display of all its ancestors to block.

First see if we actually found the link and if we set the special style instead of removing it:

	if (i < x.length && newID == 'currentPage') 

If so we start at the link and go up through the document tree. Any ancestor of our link that is a div element gets display: block

	{ 		var parDiv = x[i]; 		while (parDiv.parentNode.tagName == 'DIV') 		{ 			parDiv = parDiv.parentNode; 			parDiv.style.display = 'block'; 		} 	} } 

Clean up

I added a 'Clean up' function. It closes the entire navigation, except for the divs containing the link to the current page. First I close the navigation by calling closeNav(). Then I call setNav() to open the link to the current page and its ancestors.

function cleanNav() { 	closeNav(); 	setNav(top.setNav,'currentPage'); } 

No comments:

analytics