// -----------------------------------------------------
// Title: Heading Navigation
// version: 0.1
// Date: 2006-04-19
// Author: Gez Lemon
// Copyright (c) 2006, Juicy Studio
// Requires Greasemonkey 0.6.4 or higher
// -----------------------------------------------------
//
// ==UserScript==
// @name Heading Navigation
// @namespace http://juicystudio.com/
// @description Allows documents to be navigated by headings
// @include *
// @exclude http://gmail.google.com/*
// @exclude http://mail.google.com/mail/*
// ==/UserScript==

// SETUP VARIABLES FOR THE SCRIPT
// ==============================

// Heading activation character
var strNav = 'H';

// =====================================
// END OF SETUP VARIABLES FOR THE SCRIPT

// Global to track the current element
var objCurrent;

var iNavKey;

setup();

function setup()
{
	var objNodes = document.getElementsByTagName('*');

	objCurrent = objNodes[0];

	// Add focus listener to all elements
	for (var iCounter=0; iCounter<objNodes.length; iCounter++)
	{
		if (objNodes[iCounter].nodeType == 1)
			objNodes[iCounter].addEventListener('focus', updateCurrent, false);

		// Illegal according to spec, but required to make headings focusable
		if (objNodes[iCounter].nodeName.match(/^h[1-6]$/i))
			objNodes[iCounter].setAttribute('tabindex', '-1');
	}

	// Start listening
	document.addEventListener('keyup', headingNavigation, false);
	iNavKey = strNav.toUpperCase().charCodeAt(0);

	var objHead = document.getElementsByTagName('head')[0];

	if (objHead)
	{
		var objCSS = document.createElement('style');

		objCSS.setAttribute('type', 'text/css');
		objCSS.appendChild(document.createTextNode('h1:focus, h2:focus, h3:focus, h4:focus, h5:focus, h6:focus {outline: 1px solid #c00;}'));
		objHead.appendChild(objCSS);
	}
}

function updateCurrent(objEvent)
{
	objCurrent = objEvent.target;
}

function headingNavigation(objEvent)
{
	var iKey = objEvent.keyCode;
	var strKey = String.fromCharCode(iKey);

	if ((iKey != iNavKey && !isNumeric(strKey)) || typeable(objCurrent))
		return true;

	var iOuter, iCounter;
	var iStarted = 0;

	var objNodes = document.getElementsByTagName('*');

	// Make sure we cycle right through the collection, as the first stop
	// looks for the node that currently has focus
	for (iOuter=0; iOuter<2; iOuter++)
	{
		if (objEvent.shiftKey) // Cycle backwards
		{
			for (iCounter=objNodes.length-1; iCounter>=0; iCounter--)
			{
				iStarted = checkNode(objNodes[iCounter], strKey, iStarted);
				if (iStarted == 1)
					break;
			}
		}
		else // Cycle forwards
		{
			for (iCounter=0; iCounter<objNodes.length; iCounter++)
			{
				iStarted = checkNode(objNodes[iCounter], strKey, iStarted);
				if (iStarted == 1)
					break;
			}
		}
	}
}

function checkNode(objNode, strKey, iStarted)
{
	if (isNumeric(strKey))
	{
		// Look for specific heading
		if (iStarted == 2 && objNode.nodeName.toLowerCase() == 'h' + strKey)
		{
			objNode.focus();
			return 1;
		}
	}
	else
	{
		// Any heading
		if (iStarted == 2 && objNode.nodeName.match(/^h[1-6]$/i))
		{
			objNode.focus();
			return 1;
		}
	}

	if (objNode == objCurrent)
		return 2;

	return iStarted;
}

function typeable(objNode)
{
	if (objNode.nodeName.toLowerCase() == 'input' ||
		objNode.nodeName.toLowerCase() == 'select' ||
		objNode.nodeName.toLowerCase() == 'optgroup' ||
		objNode.nodeName.toLowerCase() == 'option' ||
		objNode.nodeName.toLowerCase() == 'textarea')
		return true;

	return false;
}

function isNumeric(strValue)
{
	return (strValue.match(/[1|2|3|4|5|6]/));
}
