Greasemonkey Hacks/Accessibility

From WikiContent

Jump to: navigation, search
Greasemonkey Hacks


Contents

Hacks 67–76: Introduction

I have cared about accessibility for almost 13 years, ever since I worked at AT&T as a relay operator for the deaf and hearing impaired. My manager was deaf, and I learned enough American Sign Language to communicate with him in his native language. (He also read lips and spoke perfect English.) One of my co-workers, with whom I became close friends, had been blind since birth. He did the same job I did, using a device that converted the words on the screen to Braille characters. We often worked different shifts, but I learned enough Braille to write him letters, which he looked forward to reading when he came in to work the next day.

Accessibility isn't just wheelchair ramps and bigger bathroom stalls. It crosses all disciplines, it affects all workplaces, and it makes no exception for gender, race, ethnicity, or income. With more and more information being published online, with more and more vital online services being developed, web accessibility is more important than ever.

Tip

In the year 2004, an estimated 7.9% (plus or minus 0.2 percentage points) of civilian, noninstitutionalized men and women, aged 18 to 64 in the United States reported a work limitation. In other words, that's 14,152,000 out of 179,133,000 (or about 1 in 13) people. [1]

The hacks in this chapter are a compilation of accessibility-related scripts I've written and found online. Some of them are tools for web developers, to help them make their own pages more accessible. Some of them leverage the accessibility features already present on the Web. The last one [Hack #76] is a proof-of-concept I developed to showcase the power of Greasemonkey as an accessibility enablement technology.

Customizing the Web isn't just fun and games. For some people, it provides the only way to use the Web at all.

Highlight Images Without Alternate Text

Quickly see which of your images are missing the required alt attribute.

If you're a web developer, you should already know that web accessibility is important. One of the primary mechanisms for enabling blind and disabled users to view your pages is to provide alternate text for every image. This is so important that the alt attribute is actually a required attribute of every <img> element. Even spacer images need an explicit alt="" attribute to tell text-only browsers and screen readers to skip over the image when they display the page or read it aloud.

Validating your page with the W3C's HTML validator (http://validator.w3.org) will tell you if an <img> element is missing the required alt attribute, but it will also tell you every other single thing you did wrong. If you aren't coding exactly to the HTML specification, the really important errors (such as missing alt attributes) will get lost in a sea of arcane rules and trivial mistakes.

The Code

This user script will run on all pages by default, but you should probably modify the @include line to include just the pages you're currently developing. The bulk of the script logic is contained in the XPath query, "//img[not(@alt)]", which finds all <img> elements that do not include any alt attribute. It will not find images that contain a blank alt attribute, which is perfectly legitimate for spacer images used solely for page layout. It will also not find images whose alternate text is useless to blind users, such as alt="filename.gif" or alt="include alternate text here".

Save the following user script as highlightnoalt.user.js:

	// ==UserScript==
	// @name		Highlight No Alt
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description highlight images without alternate text
	// @include		*
	// ==/UserScript==

	var snapBadImages = document.evaluate("//img[not(@alt)]",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = snapBadImages.snapshotLength - 1; i >= 0; i--) {
		var elmBadImage = snapBadImages.snapshotItem(i);

	elmBadImage.style.MozOutline = "2px solid red";
	elmBadImage.title = 'Missing ALT attribute! src="' +
		elmBadImage.src + '"';
	}	

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.amazon.com. You will see a number of images highlighted with a thick red border, as shown in Figure 8-1.

Figure 8-1. Inaccessible images highlighted on Amazon.com

Inaccessible images highlighted on Amazon.com

This immediately highlights several accessibility problems on Amazon's home page. In the upper-left corner, they are cross-selling one of their new partner sites for buying gourmet food online. In the upper-right corner, they have an image link to a list of most wished-for items. Each of these images is missing the required alt attribute. In the absence of an alt attribute, screen readers will read the filename from the src attribute instead, which, as you can see, is completely meaningless.

Hacking the Hack

There are many different avenues to explore in highlighting broken images. You could expand the XPath query to find images with a blank alt attribute. These are legitimate for spacer images, but they should never occur on images that convey information (such as the "Shop in Gourmet Food" image in Figure 8-1):

	var snapBadImages = document.evaluate("//img[not(@alt) or @alt='']",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

You could also use a similar technique to find images that are missing other attributes, such as width and height. width and height attributes are not strictly required, but it helps browsers lay out the page more quickly if they know in advance how large an image will be:

	var snapBadImages = document.evaluate("//img[not(@width) or not(@height)]",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

You should modify the rest of the script accordingly—for instance, to change the tool tip to indicate the problem:

	elmBadImage.title = 'Missing width or height! src="' + elmBadImage.src + '"';

Add an Access Bar with Keyboard Shortcuts

Display shortcut keys defined by a page.

An increasing number of sites define keyboard shortcuts, called access keys, for commonly used features. This is an accessibility aid for people who have difficulty using a mouse. For example, a site could define a shortcut to jump to the site's accessibility statement and another one to set focus to the site's search box (or jump to a separate search page). Unfortunately, there is no easy way to know which shortcuts the site has defined! This hack makes the keyboard shortcuts visible.

Tip

Learn more about defining keyboard shortcuts at http://diveintoaccessibility.org/15.

The Code

This user script runs on all pages. The code is divided into three parts:

  1. Find all elements that define a keyboard shortcut with the accesskey attribute.
  2. Loop through each of these elements and find the most logical label for the shortcut.
  3. Add CSS styles to the page so the list of keyboard shortcuts appears in a fixed bar along the bottom of the browser window.

Step 2 is the hard part, because different HTML elements can define an accesskey attribute. Form elements like input, textarea, and select can each define an accesskey. The form element might or might not have an associated label that contains a text description of the form field. If so, the label might contain a title attribute that gives even more detailed information about the input field. If not, the label might simply contain text. Or the form field might have no associated label at all, in which case the value attribute of the input element is the best we can do.

On the other hand, the label itself can define the accesskey, instead of the input element the label describes. Again, we'll look for a description in the title attribute of the label element, but fall back to the text of the label if no title attribute is present.

A link can also define an accesskey attribute. If so, the link text is the obvious choice. But if the link has no text (for example, if it contains only an image), then the link's title attribute is the next place to look. If the link contains no text and no title, we fall back to the link's name attribute, and, failing that, the link's id attribute.

Save the following user script as accessbar.user.js:

	// ==UserScript==
	// @name Access Bar
	// @namespace http://diveintomark.org/projects/greasemonkey/
	// @description show accesskeys defined on page
	// @include *
	// ==/UserScript==

	function addGlobalStyle(css) {
		var elmHead, elmStyle;
		elmHead = document.getElementsByTagName('head')[0];
		if (!elmHead) { return; }
		elmStyle = document.createElement('style');
		elmStyle.type = 'text/css';
		elmStyle.innerHTML = css;
		elmHead.appendChild(elmStyle);
	}

	var snapAccesskeys = document.evaluate(
		"//*[@accesskey]",
		document,
		null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
		null);
	if (!snapAccesskeys.snapshotLength) { return; }
	var arDescriptions = new Array();
	for (var i = snapAccesskeys.snapshotLength - 1; i >= 0; i--) {
		var elm = snapAccesskeys.snapshotItem(i);
		var sDescription = '';
		var elmLabel = document.evaluate("//label[@for='" + elm.id+ "']",
			document,
				null,
				XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		if (elmLabel) {
			sDescription = label.title;
			if (!sDescription) { sDescription = label.textContent; }
		}
		if (!sDescription) { sDescription = elm.textContent; }
		if (!sDescription) { sDescription = elm.title; }
		if (!sDescription) { sDescription = elm.name; }
		if (!sDescription) { sDescription = elm.id; }
		if (!sDescription) { sDescription = elm.href; }
		if (!sDescription) { sDescription = elm.value; }
		var htmlDescription = '<strong>[' +
			elm.getAttribute('accesskey').toUpperCase() + ']</strong> ';

		if (elm.href) {
			keyboard shortcutsdisplaying on access barshtmlDescription += '<a href="' + elm.href + '">' +
				sDescription + '</a>';
		} else {
			htmlDescription += sDescription;
		}
		arDescriptions.push(htmlDescription);
	}
	arDescriptions.sort();
	var elmWrapper = document.createElement('div');
	elmWrapper.id = 'accessibilityaccess bars with keyboard shortcutsaccessbar-div-0';
	var html = '<div><ul><li class="first">' + arDescriptions[0] + '</li>';
	for (var i = 1; i < arDescriptions.length; i++) {
		html += '<li>' + arDescriptions[i] + '</li>';
	}
	html += '</ul></div>';
	elmWrapper.innerHTML = html;
	document.body.style.paddingBottom = "4em";
	window.addEventListener(
		"load",
		function() { document.body.appendChild(elmWrapper); },
		true);
	addGlobalStyle(
	'#accessbar-div-0 {'+
	' position: fixed;' +
	' left: 0;' +
	' right: 0;' +
	' bottom: 0;' +
	' top: auto;' +
	' border-top: 1px solid silver;' +
	' background: black;' +
	' color: white;' +
	' margin: 1em 0 0 0;' +
	' padding: 5px 0 0.4em 0;' +
	' width: 100%;' +
	' font-family: Verdana, sans-serif;' +
	' font-size: small;' +
	' line-height: 160%;' +
	'}' +
	'#accessbar-div-0 a,' +
	'#accessbar-div-0 li,' +
	'#accessbar-div-0 span,' +
	'#accessbar-div-0 strong {' +
	' background-color: transparent;' +
	' color: white;' +
	'}' +
	'#accessbar-div-0 div {' +
	' margin: 0 1em 0 1em;' +
	'}' +
	'#accessbar-div-0 div ul {' +
	' margin-left: 0;' +
	' margin-bottom: 5px;' +
	' padding-left: 0;' +

	' display: inline;' +
	'}' +
	'#accessibilityaccess bars with keyboard shortcutsaccessbar-div-0 div ul li {' +
	' margin-left: 0;' +
	' padding: 3px 15px;' +
	' border-left: 1px solid silver;' +
	' list-style: none;' +
	' display: inline;' +
	'}' +
	'#accessbar-div-0 div ul li.first {' +
	' border-left: none;' +
	' padding-left: 0;' +
	'}');

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://diveintomark.org. At the bottom of the browser window, you will see a black bar displaying the keyboard shortcuts defined on the page, as shown in Figure 8-2.

Figure 8-2. Keyboard shortcuts defined on diveintomark.org

Keyboard shortcuts defined on diveintomark.org

How you actually use the defined keyboard shortcuts varies by platform. On Windows and Linux, you press Alt along with the defined key. On Mac OS X, you press Command and the key. On http://www.diveintomark.org, you can press Alt-0 to jump to the site's accessibility statement, as shown in Figure 8-3.

Figure 8-3. The accessibility statement for http://www.diveintomark.org

The accessibility statement for http://www.diveintomark.org

Pressing Alt-1 jumps back to the home page, and Alt-4 sets focus to the search box on the right side of the page.

Remove Conflicting Keyboard Shortcuts

Remove annoying access keys from web pages that define conflicting shortcuts.

"Add an Access Bar with Keyboard Shortcuts" [Hack #68] introduced the concept of site-specific keyboard shortcuts (called access keys, after the attribute used to define them). Like Greasemonkey itself, access keys can be used for good or for evil. A malicious web page could redefine all available access keys to point to a link that tries to download a harmful executable or pop up an advertising window. Or a web publisher could—with the best of intentions—end up defining access keys that conflict with standard keyboard shortcuts in your browser.

Wikipedia, an otherwise excellent online encyclopedia, is such a site. It defines several access keys, including some (such as Alt-E) that conflict with the keyboard shortcuts for opening menus in the Firefox menu bar. This hack removes all access keys from a page to avoid the possibility of such conflicts.

The Code

This user script runs on all pages. It uses an XPath expression to find all the elements with an accesskey attribute, and then removes the attribute. This is enough to get Firefox to remove the associated keyboard shortcut from the link or form field.

Save the following user script as unaccesskey.user.js:

	// ==UserScript==
	// @name		Remove AccessKeys
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description remove accesskey shortcuts from web pages
	// @include		*
	// ==/UserScript==

	var snapSubmit = document.evaluate("//*[@accesskey]",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = snapSubmit.snapshotLength - 1; i >= 0; i--) {
		snapSubmit.snapshotItem(i).removeAttribute('accesskey');
	}

Running the Hack

This hack runs on all platforms, but it is especially useful on Microsoft Windows, where keyboard shortcuts to open menus conflict with keyboard shortcuts defined on the web page itself.

Before installing the user script, go to http://en.wikipedia.org/wiki/Music_of_Mongolia. Press Alt-E to try to open the Edit menu, or Alt-T to open the Tools menu. Holy conflicts, Batman! The web page has redefined Alt-E to jump to the editing page, and Alt-T to jump to the discussion page.

Now, install the user script (Tools → Install This User Script), and refresh http://en.wikipedia.org/wiki/Music_of_Mongolia. You can now press Alt-E to open the Edit menu, or Alt-T to open the Tools menu. All the standard key combinations work as you would expect them to.

Wikipedia is the highest-profile site that creates this problem (and it was the inspiration for this hack), but the possibility for conflict exists on any site. I leave this script installed with the default @include *, but if you use site-specific keyboard shortcuts, you can change the @include configuration to target only the sites that cause this problem.

Make Image alt Text Visible

Display otherwise invisible information in image alt attributes as a tool tip.

In the HTML specifications, there are two attributes designed to allow text to be attached to an image: alt and title. The alt attribute is short for alternate, and it is designed to display when the image itself cannot. The title attribute is designed as an extra title to show when a user hovers his mouse over the image. Most browsers function this way. Microsoft's Internet Explorer, however, will treat an alt attribute as a title, and display it as a tool tip.(To be fair, Microsoft did this to emulate the broken behavior of Netscape 4.) As a result, many less-informed web site maintainers use alt as if it was made to display a tool tip. When using a compliant browser like Firefox, this information is inaccessible!

With the magic of Greasemonkey, though, we can resurrect this information. This hack makes all alt attributes for images appear as their tool tips, by assigning the text to the title attribute instead.

The Code

This user script runs on all pages. First, we execute an XPath query to find all the <img> and <area> elements; these are the elements usually assigned <alt> text where the author intended a <title>. Then, a simple for loop evaluates each element returned from the query. For each <img> or <area> that has an empty title attribute and a nonempty alt attribute, we copy the alt text into the title.

Save the following user script as alt-tooltips.user.js:

	// ==UserScript==
	// @name		Alt Tooltips
	// @namespace	http://www.arantius.com/
	// @description Display Alt text as tooltip if no title is available
	// @include		*
	// ==/UserScript==

	// based on code by Anthony Lieuallen
	// and included here with his gracious permission

	var res = document.evaluate("//area|//img",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	var i, el;
	for (i=0; el=res.snapshotItem(i); i++) {
		if (''==el.title && ''!=el.alt) el.title='ALT: '+el.alt;
	}

Running the Hack

Before installing this script, browse to any page that contains images with alt attributes, and they will be visible as tool tips when you hover your cursor over the image. For example, the Google home page uses an image with alt text, but no title. Pointing your mouse at the image does nothing, as shown in Figure 8-4.

Figure 8-4. Unmodified Google home page

Unmodified Google home page

Now install the script (Tools → Install This User Script) and refresh the Google home page. The alt text in the logo is revealed when you hover your mouse over the image, as shown in Figure 8-5.

Figure 8-5. Google home page with alt tool tips

Google home page with alt tool tips

Hacking the Hack

As shown in "Master XPath Expressions" [Hack #8], XPath is a language all its own. The logic used in the loop can be fit into a more complex XPath query:

	var res = document.evaluate("(//img|//area)[not(@title) and not(''=@alt)]",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

With this XPath query, we can simplify the code inside the loop:

	var res = document.evaluate("(//img|//area)[not(@title) and not(''=@alt)]",
		document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	var i, el;

	for (i=0; el=res.snapshotItem(i); i++) {
		el.title='accessibilityimage alt textALT: '+el.alt;
	}

I call this trading complexity. The overall code is not simpler; we've just moved the complexity from one part to another. The end result is the same, so it boils down to a matter of style.

Anthony Lieuallen

Add a Table of Contents to Long Pages

Create a menu out of a page's header tags.

I read a lot of specifications online. Not as part of my day job; I mean I do this for fun. There are good specifications, and there are bad specifications, but there is one thing you can say about virtually all of them: they are incredibly long. And most of them are published online as a single HTML page. Firefox's incremental find feature helps when I'm trying to find something specific (just press Ctrl-F and start typing), but I still often get lost in the endless scrolling.

One nice thing about W3C specifications in particular is that they use HTML correctly. Section and subsection titles are marked up with header tags: <h1>, <h2>, <h3>, <h4>, and so on. This hack takes those header tags and creates an in-page table of contents. Using the same technique as "Add an Access Bar with Keyboard Shortcuts" [Hack #68], the script adds a fixed bar at the bottom of the browser window that contains a drop-down menu of all the headers on the page. Selecting a header from the menu jumps directly to that section on the page.

The Code

This user script runs on all pages. It iterates through all the <h1>, <h2>, <h3>, and <h4> elements on the page, and creates a <select> menu in a fixed-position bar along the bottom of the browser window, just above the status bar. Items in the menu are indented based on the header level, so when you drop down the menu, it appears to be a hierarchical table of contents. Finally, we add a Hide TOC button on the right side of the table of contents bar. Clicking Hide TOC hides the bar temporarily until you refresh the page or follow a link to another page.

Save the following user script as autotoc.user.js:

	// ==UserScript==
	// @name		AutoTOC
	// @namespace	http://runeskaug.com/greasemonkey
	// @description Creates a table of contents for all headers on the page

	// @include *
	// ==/UserScript==

	// based on code by Rune Skaug
	// and included here with his gracious permission

	//set the optional behaviour accessibilitytables of contents menusof the TOC box
	// - true resets it to its initial state after you have selected a header
	// - false does not reset it
	var resetSelect = false;

	//if true, shows a "Hide TOC" button on the right side of the bar
	var showHide = true;
	var hideText = "Hide TOC";

	function f() {
		// only on (X)HTML pages containing at least one heading -
		// excludes XML files, text files, plugins and images
		if ( document.getElementsByTagName("html").length &&
			(document.getElementsByTagName('h1').length ||
			 document.getElementsByTagName('h2').length ||
			 document.getElementsByTagName('h3').length ||
			 document.getElementsByTagName('h4').length )) {
		  var aHs = getHTMLHeadings();
		  if (aHs.length>2) { // HTML document, more than two headings.
			  addCSS(
				'#js-toc {position: fixed; left: 0; right: 0; top: auto; ' +
				'bottom: 0; height: 20px; width: 100%; vertical-align: ' +
				'middle; display: block; border-top: 1px solid #777; ' +
				'background: #ddd; margin: 0; web pagesadding tables of contents menuspadding: 3px; ' +
				'z-index: 9999; }\n#js-toc select { font: 8pt verdana, ' +
				'sans-serif; margin: 0; margin-left:5px; ' +
				'background-color: #fff; color: #000; float: ' +
				'left; padding: 0; vertical-align: middle;}\n' +
				'#js-toc option { font: 8pt verdana, sans-serif; ' +
				'color: #000; }\n#js-toc .hideBtn { font: 8pt verdana, ' +
				'sans-serif; float: right;' +
				'margin-left: 5px; margin-right: 10px; padding: 2px 2px; ' +
				'border: 1px dotted #333; background-color: #e7e7e7; }\n' +
				'#js-toc .hideBtn a { color: #333; text-decoration: none; '+
				'background-color: transparent;} ' +
				'#js-toc .hideBtn a:hover { ' +
				'color: #333; text-decoration: none; background-color: ' +
				'transparent;}'
		);
		var toc = document.createElement(
			showHide?'tocuserjselem':'div');
		toc.id = 'js-toc';
		tocSelect = document.createElement('select');
		tocSelect.addEventListener("change", function() {
			if (this.value) {
				if (resetSelect) {

				this.selectedIndex = 0;
			}
			window.location.href = '#' + this.value;
		}
	 }, true);
	 tocSelect.id = 'navbar-toc-select';
	 tocEmptyOption = document.createElement('option');
	 tocEmptyOption.setAttribute('value','');
	 tocEmptyOption.appendChild(
		document.createTextNode('Table accessibilitytables of contents menusof Contents'));
	 tocSelect.appendChild(tocEmptyOption);
	 toc.appendChild(tocSelect);
	 if (showHide) {
		var hideDiv = document.createElement('div');
		hideDiv.setAttribute('class','hideBtn');
		var hideLink = document.createElement('a');
		hideLink.setAttribute("href","#");
		hideLink.addEventListener('click', function(event) {
			document.getElementById('js-toc').style.display =
'none';
			event.preventDefault();
		}, true);
		hideLink.appendChild(document.createTextNode(hideText));
		hideDiv.appendChild(hideLink);
		toc.appendChild(hideDiv);
	}
	document.body.style.web pagesadding tables of contents menuspaddingBottom = "27px";
	document.body.appendChild(toc);
	for (var i=0,aH;aH=aHs[i];i++) {
		if (aH.offsetWidth) {
			op = document.createElement("option");
			op.appendChild(document.createTextNode(gs(aH.tagName)+
				getInnerText(aH).substring(0,100)));
			var refID = aH.id ? aH.id : aH.tagName+'-'+(i*1+1);
			op.setAttribute("value", refID);
			document.getElementById("navbar-toc-select").
 appendChild(
				   op);
			 aH.id = refID;
		  }
	    }
      }
	}
	GM_registerMenuCommand('AutoTOC: Toggle display',
		autoTOC_toggleDisplay);
 };

 function autoTOC_toggleDisplay() {
	if (document.getElementById('js-toc').style.display == 'none') {
		document.getElementById('js-toc').style.display = 'block';
	}
	else {
		document.getElementById('js-toc').style.display = 'none';

	     }
	 }
	 function getHTMLHeadings() {
		function acceptNode(node) {
			if (node.tagName.match(/^h[1-4]$/i)) {
				return NodeFilter.FILTER_ACCEPT;
			}
			return NodeFilter.FILTER_SKIP;
		}
		outArray = new Array();
		var els = document.getElementsByTagName("*");
			var j = 0;
			for (var i=0,el;el=els[i];i++) {
				if (el.tagName.match(/^h[1-4]$/i)) {
				outArray[j++] = el;
				}
			}
		return outArray;
	}
	function addCSS(css) {
		var head, styleLink;
		head = document.getElementsByTagName('head')[0];
		if (!head) { return; }
		styleLink = document.createElement('link');
		styleLink.setAttribute('rel','stylesheet');
		styleLink.setAttribute('type','text/css');
		styleLink.setAttribute('href','data:text/css,'+escape(css));
		head.appendChild(styleLink);
	}
	function gs(s){
		s = s.toLowerCase();
		if (s=="h2") return "\u00a0 \u00a0 "
		else if (s=="h3") return "\u00a0 \u00a0 \u00a0 \u00a0 "
		else if (s=="h4") return "\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ";
		return "";
	}
	function getInnerText(el) {
		var s='';
		for (var i=0,node; node=el.childNodes[i]; i++) {
			if (node.nodeType == 1) s += getInnerText(node);
			else if (node.nodeType == 3) s += node.nodeValue;
		}
		return s;
	}

	f();

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://whatwg.org/specs/web-apps/current-work/. At the bottom of the browser window is a drop-down box labeled Table of Contents. Open the menu to see an outline of all the headers on the page, as shown in Figure 8-6.

Figure 8-6. Table of contents

Table of contents

You can select any of the headings in the menu to jump to that section on the page.

Use Real Headers on Google Web Search

Make Google's markup more semantic, and learn why it matters.

Google does an excellent job of indexing the Web, but it does a poor job of displaying the results. By poor, I mean not semantic. Why does semantic markup matter? Well, among other things, it enables hacks such as "Add a Table of Contents to Long Pages" [Hack #71] to extract meaningful information from the page.

It is also an accessibility issue. Screen readers for the blind have features that allow users to navigate a page by its header elements. Sighted users can simply glance at the page on screen and see how it's structured; screen readers can only "glance" at the page's markup. If a page uses poor markup, screen readers have a more difficult time determining how the page is structured, which makes it more difficult for blind users to navigate.

This hack changes Google search result pages to use reader header elements for each search result.

The Code

This user script runs on Google web search result pages. It uses hardcoded knowledge of Google's markup—each search result is wrapped in a <p class="g"> element—to wrap a real <h2> tag around the title of each result. It also adds an <h1>Search Results</h1> element at the top of the page. This <h1> is hidden from sighted users, but screen readers will still "see" it in the DOM and announce it to blind users.

Save the following user script as googleheaders.user.js:

	// ==UserScript==
	// @name		Google Headings
	// @namespace   http://zeus.jesus.cam.ac.uk/~jg307/mozilla/userscripts/
	// @description Add real heading elements to google search results
	// @include		http://google.tld/search*
	// @include		http://www.google.tld/search*
	// ==/UserScript==

	// based on code by James Graham
	// and included here with his gracious permission

	var mainHeading = document.createElement('h1');
	var headingText = document.createTextNode('Search Results');
	mainHeading.appendChild(headingText);
	mainHeading.style.visibility="Hidden";
	mainHeading.style.height="0";
	mainHeading.style.width="0";
	var body = document.getElementsByTagName('body')[0];
	body.insertBefore(mainHeading, body.firstChild);
	var resultsParagraphs = document.evaluate("//p[@class='g']",
		document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	if (resultsParagraphs.snapshotLength) {
		var heading = resultsParagraphs.snapshotItem(0);
		var headingSize = document.defaultView.getComputedStyle(
			heading, '').getPropertyValue("font-size");
		var headingWeight = document.defaultView.getComputedStyle(
			heading, '').getPropertyValue("font-weight");
	}
	for (var i = 0; i < resultsParagraphs.snapshotLength; i++) {
		var paragraphNode = resultsParagraphs.snapshotItem(i);
		var linkNode = paragraphNode.getElementsByTagName('a')[0];
		var heading = document.createElement('h2');
		heading.appendChild(linkNode.cloneNode(true));
		heading.style.fontSize = headingSize;
		heading.style.fontWeight = headingWeight;
		heading.style.marginBottom = 0;
		heading.style.marginTop = 0;

		paragraphNode.replaceChild(heading, linkNode);
		try {
			paragraphNode.removeChild(
				paragraphNode.getElementsByTagName('br')[0]);
		}
		catch(error) {
		}
	}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com and search for accessibility. The script does not appear to have made any difference, as shown in Figure 8-7.

Figure 8-7. Real headers?

Real headers?

This is because the script goes to great length to style the new <h2> elements so they look similar to the page's default text style. However, if you install autotoc.user.js [Hack #71], the difference becomes obvious, as shown in Figure 8-8.

Because the page now uses properly structured markup, the autotoc.user.js script can construct a table of contents for the page. This is essentially what screen readers do for blind users, by looking for real header elements and allowing the user to jump to the next or previous header.

Figure 8-8. Real headers!

Real headers!

Add a Toolbar to Zoom Images Easily

Reduce or enlarge individual images with a single click.

In Firefox, you can make any page text larger by pressing Ctrl-equals sign (=), or make it smaller by pressing Ctrl-hyphen (-).However, this does nothing to the images on the page. If you want to enlarge or reduce an image, you're out of luck.

Here's a cool little hack that adds a toolbar to each image on a page to make it larger or smaller.

The Code

This user script runs on all pages. It finds all the images on the page with the document.images collection, and then adds a toolbar of buttons (really, just a <div> with some <a> elements styled to look like buttons). Before you protest, I realize that this script isn't keyboard-accessible. You can't win them all.

Save the following user script as zoom-image.user.js:

	// ==UserScript==
	// @name Zoom Image

	// @namespace http://www.smartmenus.org/
	// @description Displays an zoom toolbar over accessibilityimage zooming toolbarsimages
	// ==/UserScript==

	// based on code by Vasil Dinkov
	// and included here with his gracious permission

	// === User Configuration ===
	const kZoomFactor = 1.7; // amount to zoom image on each click
	const kMenuShowTimeOut = 1.2; // seconds before auto-hiding menu
	const kMinimumImageWidth = 100; // minimal width of the menu-enabled images
	const kMinimumImageHeight = 50; // minimal height of the menu-enabled images

	// === Code ===
	var gTimeoutID = gPixelLeft = gPixelTop = 0;
	var gMenuBuilt = false;
	var gElmToolbar = gCurrentImage = null;

	function image_mouseover(o) {
		if ((o.clientWidth<kMinimumImageWidth ||
			 o.clientHeight<kMinimumImageHeight) &&
			!o.zoomed ||
			gMenuBuilt &&
			gElmToolbar.style.visibility == "visible") {
			return;
		}
		gCurrentImage = o;
		if (!gCurrentImage.original_width) {
			gCurrentImage.original_width = o.clientWidth;
			gCurrentImage.original_height = o.clientHeight;
		}
		gPixelLeft = o.offsetLeft;
		gPixelTop = o.offsetTop;
		var oParent = o.offsetParent;
		while (oParent) {
			gPixelLeft += oParent.offsetLeft;
			gPixelTop += oParent.offsetTop;
			oParent = oParent.offsetParent;
		}
		gTimeoutID = setTimeout(show_toolbar, kMenuShowTimeOut*1000);
	}
	
	function show_toolbar() {
		if (!build_menu()) { return; }
		gElmToolbar.style.top = gPixelTop+"px";
		gElmToolbar.style.left = gPixelLeft+"px";
		gElmToolbar.style.visibility = "visible";
	}

	function hide_toolbar(e) {
		if (gTimeoutID) {
			clearTimeout(gTimeoutID);
			gTimeoutID = 0;

		}
		if (!build_menu()) { return; }
		var relatedTarget = e?e.relatedTarget:0;
		if (relatedTarget &&
			(gElmToolbar==relatedTarget ||
			 gElmToolbar==relatedTarget.parentNode)) {
			 return;
		}
		gElmToolbar.style.visibility = "hidden";
		accessibilityimage zooming toolbarsgCurrentImage = null;
	}

	function toolbar_mouseout(e) {
		var relatedTarget = e.relatedTarget;
		if (relatedTarget && relatedTarget != gCurrentImage) {
			hide_toolbar(e);
		}
	}

	function create_button(sCaption, sTitle, fOnClick) {
		var elmButton = document.createElement("a");
		elmButton.href = '#';
		elmButton.className = "zoomtoolbarbutton";
		elmButton.title = sTitle;
		elmButton.appendChild(document.createTextNode(sCaption));
		elmButton.addEventListener("mouseover", function() {
			this.style.borderColor = "#4d4c76";
		}, false);
		elmButton.addEventListener("mousedown", function() {
			this.style.borderColor = "#000";
			this.style.background = "#eee4a5";
		}, false);
		elmButton.addEventListener("mouseup", function() {
			this.style.borderColor = "#4d4c76";
			this.style.background = "transparent";
		}, false);
		elmButton.addEventListener("mouseout", function() {
			this.style.borderColor = "#ffffdd #C1B683 #C1B683 #ffffdd";
			this.style.background = "transparent";
		}, false);
		elmButton.addEventListener("click", fOnClick, false);
		return elmButton;
	}

	function build_menu() {
		if (gMenuBuilt) { return true; }
		gElmToolbar = document.createElement("div");
		with (gElmToolbar.style) {
			position = "absolute";
			border = "1px solid";
			borderColor = "#ffffdd #857A4A #857A4A #ffffdd";
			backgroundColor = "#F5EBBC";
			margin = 0;

		padding = "2px";
		zIndex = 10000000;
	}
	gElmToolbar.appendChild(create_button("+", "Zoom in", function(e) {
		var width, height;
		width = accessibilityimage zooming toolbarsgCurrentImage.clientWidth;
		height = gCurrentImage.clientHeight;
		gCurrentImage.style.width = width*kZoomFactor+"px";
		gCurrentImage.style.height = height*kZoomFactor+"px";
		gCurrentImage.zoomed = 1;
		e.preventDefault();
	}));
	gElmToolbar.appendChild(create_button("-", "Zoom out", function(e) {
		var width, height;
		width = gCurrentImage.clientWidth;
		height = gCurrentImage.clientHeight;
		gCurrentImage.style.width = width / kZoomFactor + "px";
		gCurrentImage.style.height = height / kZoomFactor + "px";
		gCurrentImage.zoomed = 1;
		e.preventDefault();
	}));
	gElmToolbar.appendChild(create_button("\u21B2", "Restore", function(e) {
		gCurrentImage.style.width = gCurrentImage.original_width + "px";
		gCurrentImage.style.height = gCurrentImage.original_height + "px";
		gCurrentImage.zoomed = 0;
		e.preventDefault();
	}));
	document.body.appendChild(gElmToolbar);
	gElmToolbar.addEventListener("mouseout", toolbar_mouseout, false);
	gMenuBuilt = true;
	return true;
}

function addGlobalStyle(css) {
	var head, styleLink;
	head = document.getElementsByTagName('head')[0];
	if (!head) { return; }
	styleLink = document.createElement('link');
	styleLink.setAttribute('rel', 'stylesheet');
	styleLink.setAttribute('type', 'text/css');
	styleLink.setAttribute('href', 'data:text/css,' + escape(css));
	head.appendChild(styleLink);
}

for (var i = 0; i < document.images.length; i++) {
	var elmImage = document.images[i];
	elmImage.addEventListener("mouseover", function() {
		image_mouseover(this);
	}, false);
	elmImage.addEventListener("mouseout", hide_toolbar, false);
}

	addGlobalStyle('' +
	'a.zoomtoolbarbutton {' +
	' position: relative;' +
	' top: 0px;' +
	' font: 14px monospace;' +
	' border: 1px solid;' +
	' border-color: #ffffdd #c1b683 #c1b683 #ffffdd;' +
	' padding: 0 2px 0 2px;' +
	' margin: 0 2px 2px 2px;' +
	' text-decoration: none;' +
	' background-color: transparent;' +
	' color: black;' +
	'}');

Running the Hack

After installing this script (Tools → Install This User Script), go to http://www.oreilly.com. Hover your cursor over the tarsier logo in the top-left corner of the page to activate the zoom toolbar, as shown in Figure 8-9.

Figure 8-9. Image zoom toolbar

Image zoom toolbar

Click the plus (+) button to zoom in on the image, as shown in Figure 8-10.

Figure 8-10. Zoomed tarsier

Zoomed tarsier

You can also click the minus (–) button to reduce the image size, or click the ? button to restore the image to its original size.

Hacking the Hack

There are lots of interesting things to do with images besides zooming them. If you right-click on an image, Firefox gives you several choices: view the image in isolation, copy the image URL, save it to disk, and several others. We can't reproduce all of these functions in JavaScript, but we can do the first one: view the image in isolation.

Immediately before this line in the build_menu function:

	document.body.appendChild(gElmToolbar);

add this code snippet:

	gElmToolbar.appendChild(create_button("V", "View image", function(e) {
		location.href = gCurrentImage.src;
        e.preventDefault();
	}));

Now, refresh http://www.oreilly.com. Hover over the tarsier again, and you will see an additional button labeled V in the image toolbar, as shown in Figure 8-11.

Figure 8-11. Enhanced image toolbar

Enhanced image toolbar

Click on the V toolbar button to view the image in isolation, as shown in Figure 8-12.

Figure 8-12. Tarsier in isolation

Tarsier in isolation

This is the same functionality provided by selecting View Image in the image's context menu. You can click the back button to return to the O'Reilly home page.

Make Apache Directory Listing Prettier

Enhance Apache's autogenerated directory listing pages with semantic, accessible tables.

Have you ever visited a page to find nothing but a plain list of files? If a folder has no default web page, the Apache web server autogenerates a directory listing with clickable filenames. Nothing fancy, but it works, so why complain? Because we can do better! This hack takes the raw data presented in Apache directory listings and replaces the entire page with a prettier, more accessible, more functional version.

The Code

This user script runs on all pages. Of course, not all pages are Apache directory listings, so the first thing the script does is check for some common signs that this page is a directory listing. Unfortunately, there is no foolproof way to tell; recent versions of Apache add a <meta> element in the <head> of the page to say that the page was autogenerated by Apache, but earlier versions of Apache did not do this. The script checks for three things:

  • The title of the page starts with "Index of /".
  • The body of the page contains a <pre> element. Apache uses this to display the plain directory listing.
  • The body of the page contains links with query parameters. Apache uses these for the column headers. Clicking a column header link re-sorts the directory listing by name, modification date, or size.

If all three of these conditions are met, the script assumes the page is an Apache directory listing, and proceeds to parse the preformatted text to extract the name, modification date, and size of each file. It constructs a table (using an actual <table> element—what a concept) and styles alternating rows with a light-gray background.

Save the following user script as betterdir.user.js:

	// ==UserScript==
	// @name BetterDir
	// @namespace http://diveintomark.org/projects/greasemonkey/
	// @description make Apache 1.3-style directory listings prettier
	// @include *
	// ==/UserScript==

	function addGlobalStyle(css) {
		var elmHead, elmStyle;	
		elmHead = document.getElementsByTagName('head')[0];

		if (!elmHead) { return; }
		elmStyle = document.createElement('style');
		elmStyle.type = 'text/css';
		elmStyle.innerHTML = css;
		elmHead.appendChild(elmStyle);
	}

	// if page title does not start with "Index of /", bail
	if (!(/^Index of \//.test(document.title))) { return; }

	// If we can't find the PRE element, this is either
	// not a directory listing at all, or it's an
	// Apache directory listingsApache 2.x listing with fancy table output enabled
	var arPre = document.getElementsByTagName('pre');
	if (!arPre.length) { return; }
	var elmPre = arPre[0];

	// find the column headers, or bail
	var snapHeaders = document.evaluate(
		"//a[contains(@href, '?')]",
		document,
		null,
		XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
		null);
	if (!snapHeaders.snapshotLength) { return; }

	// Tables aren't evil, they're just supposed to be used for tabular data.
	// This is tabular data, so let's make a TABLE element
	var elmTable = document.createElement('table');
	// give the table a summary, for accessibilityaccessibility
	elmTable.setAttribute('summary', 'Directory listing');
	var elmCaption = document.createElement('caption');
	// the "title" of the table should go in a CAPTION element
	// inside the TABLE element, for semantic purity
	elmCaption.textContent = document.evaluate("//head/title",
		document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,
		null).singleNodeValue.textContent;
	elmTable.appendChild(elmCaption);

	var elmTR0 = document.createElement('tr');
	var iNumHeaders = 0;
	for (var i = 0; i < snapHeaders.snapshotLength; i++) {
		var elmHeader = snapHeaders.snapshotItem(i);
		// column headers go into TH elements, for accessibility
		var elmTH = document.createElement('th');
		var elmLink = document.createElement('a');
		elmLink.href = elmHeader.href;
		elmLink.innerHTML = elmHeader.innerHTML;
		// give each of the column header links a title,
		// to explain what will happen when you click on them
		elmLink.title = "Sort by " + elmHeader.innerHTML.toLowerCase();

		elmTH.appendChild(elmLink);
		elmTR0.appendChild(elmTH);
		iNumHeaders++;
	}
	elmTable.appendChild(elmTR0);

	var sPreText = elmPre.innerHTML;
	if (/<hr/.test(sPreText)) {
		sPreText = sPreText.split(/<hr.*?>/)[1];
	}
	var arRows = sPreText.split(/\n/);
	var nRows = arRows.length;
	var bOdd = true;
	for (var i = 0; i < nRows; i++) {
		var sRow = arRows[i];
		sRow = sRow.replace(/^\s*|\s*$/g, '');
		if (!sRow) { continue; }
		if (/\<hr/.test(sRow)) { continue; }
		var arTemp = sRow.split(/<\/a>/);
		var sLink = arTemp[0] + '</a>';
		if (/<img/.test(sLink)) {
			sLink = sLink.split(/<img.*?>/)[1];
		}
		sRestOfLine = arTemp[1];
		arRestOfCols = sRestOfLine.split(/\s+/);

		var elmTR = document.createElement('tr');
		var elmTD = document.createElement('td');
		elmTD.innerHTML = sLink;
		elmTR.appendChild(elmTD);

		var iNumColumns = arRestOfCols.length;
		var bRightAlign = false;
		for (var j = 1 /* really */; j < iNumColumns; j++) {
			var sColumn = arRestOfCols[j];
			if (/\d\d:\d\d/.test(sColumn)) {
				elmTD.innerHTML += ' ' + sColumn;
			} else {
				elmTD = document.createElement('td');
				elmTD.innerHTML = arRestOfCols[j];
				if (bRightAlign) {
				elmTD.setAttribute('class', 'flushright');
				}
				elmTR.appendChild(elmTD);
			}
			bRightAlign = true;
		}
		while (iNumColumns <= iNumHeaders) {
			elmTR.appendChild(document.createElement('td'));
			iNumColumns++;
	
		}

		// zebra-stripe table rows, from
		// http://www.alistapart.com/articles/zebratables/
		// and http://www.alistapart.com/articles/tableruler/
		elmTR.style.backgroundColor = bOdd ? '#eee' : '#fff';
		elmTR.addEventListener('mouseover', function() {
			this.className = 'ruled';
		}, true);
		elmTR.addEventListener('mouseout', function() {
			this.className = '';
		}, true);
		elmTable.appendChild(elmTR);

		bOdd = !bOdd;
	}

	// copy address footer -- probably a much easier way to do this,
	// but it's not always there (depends on httpd.conf options)
	var sFooter = document.getElementsByTagName('address')[0];
	var elmFooter = null;
	if (sFooter) {
		elmFooter = document.createElement('address');
		elmFooter.innerHTML = sFooter.innerHTML;
	}

	window.addEventListener('load',
		function() {
			document.body.innerHTML = '';
			document.body.appendChild(elmTable);
			if (elmFooter) {
				document.body.appendChild(elmFooter);
			}
		},
		true);

	// now that everything is semantic and accessible,
	// make it a little prettier too
	addGlobalStyle(
	'table {' +
	' border-collapse: collapse;' +
	' border-spacing: 0px 5px;' +
	' margin-top: 1em;' +
	' width: 100%;' +
	'}' +
	'caption {' +
	' text-align: left;' +
	' font-weight: bold;' +
	' font-size: 180%;' +
	' font-family: Optima, Verdana, sans-serif;' +
	
	'}' +
	'tr {' +
	'  padding-bottom: 5px;' +
	'}' +
	'td, th {' +
	' font-size: medium;' +
	' text-align: right;' +
	'}' +
	'th {' +
	' font-family: Optima, Verdana, sans-serif;' +
	' padding-right: 10px;' +
	' padding-bottom: 0.5em;' +
	'}' +
	'th:first-child {' +
	' padding-left: 20px;' +
	'}' +
	'td:first-child,' +
	'td:last-child,' +
	'th:first-child,' +
	'th:last-child {' +
	' text-align: left;' +
	'}' +
	'td {' +
	' font-family: monospace;' +
	' border-bottom: 1px solid silver;' +
	' padding: 3px 10px 3px 20px;' +
	' border-bottom: 1px dotted #003399;' +
	'}' +
	'td a {' +
	' text-decoration: none;' +
	'}' +
	'tr.ruled {' +
	' background-color: #88eecc ! important;' +
	'}' +
	'address {' +
	' margin-top: 1em;' +
	' font-style: italic;' +
	' font-family: Optima, Verdana, sans-serif;' +
	' font-size: small;' +
	' background-color: transparent;' +
	' color: silver;' +
	'}');

Running the Hack

Before installing the user script, go to http://diveintomark.org/projects/greasemonkey/. There is no default page for this directory, so Apache automatically generates a plain-text directory listing, as shown in Figure 8-13.

Now, install the user script (Tools → Install This User Script) and refresh http://diveintomark.org/projects/greasemonkey/. The user script replaces the plain directory listing with an enhanced version, which contains a real table with alternating rows shaded, as shown in Figure 8-14.

Figure 8-13. Plain Apache directory listing

Plain Apache directory listing

When you hover over a file, the entire row is highlighted, as shown in Figure 8-15.

Also, when you hover over one of the column headers, you will see a tool tip explaining that you can click to sort the directory listing, as shown in Figure 8-16.

I've probably seen thousands of autogenerated directory listings, and it wasn't until I wrote this hack that I realized that you could click a column header to change the sort order. Usability matters!

Figure 8-14. Enhanced Apache directory listing

Enhanced Apache directory listing

Figure 8-15. Row highlighting

Row highlighting

Figure 8-16. Column sorting

Column sorting

Add a Text-Sizing Toolbar to Web Forms

Insert buttons before <textarea> elements to make the text larger or smaller.

I spend a lot of time—probably too much time—commenting on weblogs and web-based discussion forums. Despite several attempts to create some sort of universal commenting API, virtually all of these sites continue to use a simple web form with a <textarea> element for entering comments.

This hack alters web forms to add a toolbar above every <textarea> element. The toolbar lets you increase or decrease the text size of the <textarea>, without changing the style of the rest of the page. The buttons are fully keyboard-accessible; you can tab to them and press Enter instead of clicking them with your mouse.

Tip

I mention this up front, because accessibility matters, and also because it was harder than it sounds.

The Code

This user script runs on all pages. The code looks complicated, and it is complicated, but not for the reason you think. It looks complicated because of the large multiline gibberish-looking strings in the middle of it. Those are data: URIs, which look like hell but are easy to generate.(See "Embed Graphics in a User Script" [Hack #11] for more on data: URIs.)

The toolbar is displayed visually as a row of buttons, but each button is really just an image of something that looks pushable, wrapped in a link that executes one of our JavaScript functions. Since we'll be creating more than one button (this script has only two, but you could easily extend it with more functionality), I created a function to encapsulate all the button-making logic:

	function createButton functioncreateButton(target, func, title, width, height, src)

The createButton function takes six arguments:

target
An element object; the <textarea> element that this button will control.
func
A function object; the JavaScript function to be called when the user clicks the button with the mouse or activates it with the keyboard.
title
A string; the text of the tool tip when the user moves her cursor over the button.
width
An integer; the width of the button. This should be the width of the graphic given in the src argument.
height
An integer; the height of the button. This should be the height of the graphic given in the src argument.
src
A string; the URL, path, or data: URI of the button graphic.

Creating the image is straightforward, but creating the link that contains the image is where the real complexity lies:

	button = document.createElement('a');
	button._target = target;
	button.title = title;
	button.href = '#';
	button.onclick = func;
	button.appendChild(img);

There are two things I want to point out here. First, I need to assign a bogus href attribute to the link; otherwise, Firefox would treat it as a named anchor and wouldn't add it to the tab index (i.e., you wouldn't be able to tab to it, making it inaccessible with the keyboard). Second, I'm setting the _target attribute to store a reference to the target <textarea>. This is perfectly legal in JavaScript; you can create new attributes on an object just by assigning them a value. I'll access the custom _target attribute later, in the onclick event handler.

If you read Mozilla's documentation on the Event object, you'll see that there are several target-related properties, including one simply called target. You might be tempted to use event.target to get a reference to the clicked link, but it behaves inconsistently. When the user tabs to the button and presses Enter, event.target is the link, but when the user clicks the button with the mouse, event.target is the image inside the link! In any case, event.currentTarget returns the link in all cases, so I use that.

Tip

See http://www.xulplanet.com/references/objref/event.html for documentation on the Event object.

Now the real fun begins. (And you thought you were having fun already!) I need to get the current dimensions and font size of the <textarea> so that I can make them bigger. Simply retrieving the appropriate attributes from textarea.style (textarea.style.width, textarea.style.height, and textarea.style.fontSize) will not work, because those only get set if the page actually defined them in a style attribute on the <textarea> itself. That's not what I want; I want the final style, after all stylesheets have been applied. For that, I need getComputedStyle:

	s = getComputedStyle(textarea, "");
	textarea.style.width = (parseFloat(s.width) * 1.5) + "px";
	textarea.style.height = (parseFloat(s.height) * 1.5) + "px";
	textarea.style.fontSize = (parseFloat(s.fontSize) + 7.0) + 'px';

Finally, do you remember that bogus href value I added to my button link to make sure it was keyboard-accessible? Well, it's now become an annoyance, because after Firefox finishes executing the onclick handler, it's going to try to follow that link. Since it points to a nonexistent anchor, Firefox is going to jump to the top of the page, regardless of where the button is. This is annoying, and to stop it, I need to call event.preventDefault() before finishing my onclick handler:

	event.preventDefault();

All this was just for the sake of keyboard accessibility. What can I say? Some people build model airplanes. I build accessible web pages.

Save the following user script as zoomtextarea.user.js:

	// ==UserScript==
	// @name Zoom Textarea
	// @namespace http://diveintomark.org/projects/greasemonkey/
	// @description add controls to zoom textareas
	// @include *
	// ==/UserScript==

	function addEvent(oTarget, sEventName, fCallback, bCapture) {
		var bReturn = false;
		if (oTarget.addEventListener) {
			oTarget.addEventListener(sEventName, fCallback, bCapture);
			bReturn = true;
		} else if (oTarget.attachEvent) {
			bReturn = oTarget.attachEvent('on' + sEventName, fCallback);
		}
		return bReturn;
	}

	function createButton(elmTarget, funcCallback, sTitle, iWidth, iHeight, urlSrc) {
		var elmImage = document.createElement('img');
		elmImage.width = iWidth;
		elmImage.height = iHeight;
		elmImage.style.borderTop = elmImage.style.borderLeft = "1px solid #ccc";
		elmImage.style.borderRight = elmImage.style.borderBottom = "1px solid #888";
		elmImage.style.marginRight = "2px";
		elmImage.src = urlSrc;

		var elmLink = document.createElement('a');
		elmLink.title = sTitle;
		elmLink.href = '#';
		addEvent(elmLink, 'click', funcCallback, true);
		elmLink.appendChild(elmImage);
		return elmLink;
	}

	var arTextareas = document.getElementsByTagName('textarea');
	for (var i = arTextareas.length - 1; i >= 0; i--) {
		var elmTextarea = arTextareas[i];

		function textarea_zoom_in(event) {
			var style = getComputedStyle(elmTextarea, "");
			elmTextarea.style.width = (parseFloat(style.width) * 1.5) + "px";
			elmTextarea.style.height = (parseFloat(style.height) * 1.5) + "px";
			elmTextarea.style.fontSize = (parseFloat(style.fontSize) + 7.0) +
	'px';
			event.preventDefault();
		}

		function textarea_zoom_out(event) {
			var style = getComputedStyle(elmTextarea, "");
			elmTextarea.style.width = (parseFloat(style.width) * 2.0 / 3.0) +
	"px";
			elmTextarea.style.height = (parseFloat(style.height) * 2.0 / 3.0) +
	"px";
			elmTextarea.style.fontSize = (parseFloat(style.fontSize) - 7.0) +
	"px";
			event.preventDefault();
		}

		elmTextarea.parentNode.insertBefore(
			createButton(
				elmTextarea,
				textarea_zoom_in,
				'Increase text size',
				20,
				20,
				'data:image/gif;base64,'+
	'R0lGODlhFAAUAOYAANPS1tva3uTj52NjY2JiY7KxtPf3%2BLOys6WkpmJiYvDw8fX19vb'+
	'296Wlpre3uEZFR%2B%2Fv8aqpq9va3a6tr6Kho%2Bjo6bKytZqZml5eYMLBxNra21JSU3'+
	'Jxc3RzdXl4emJhZOvq7KamppGQkr29vba2uGBgYdLR1dLS0lBPUVRTVYB%2Fgvj4%2BYK'+
	'Bg6SjptrZ3cPDxb69wG1tbsXFxsrJy29vccDAwfT09VJRU6uqrFlZW6moqo2Mj4yLjLKy'+
	's%2Fj4%2BK%2Busu7t783Nz3l4e19fX7u6vaalqNPS1MjHylZVV318ftfW2UhHSG9uccv'+
	'KzfHw8qqqrNPS1eXk5tvb3K%2BvsHNydeLi40pKS2JhY2hnalpZWlVVVtDQ0URDRJmZm5'+
	'mYm11dXp2cnm9vcFxcXaOjo0pJSsC%2FwuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
	'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC'+
	'H5BAAAAAAALAAAAAAUABQAAAeagGaCg4SFhoeIiYqKTSQUFwgwi4JlB0pOCkEiRQKKRxM'+
	'gKwMGDFEqBYpPRj4GAwwLCkQsijwQBAQJCUNSW1mKSUALNiVVJzIvSIo7GRUaGzUOPTpC'+

	'igUeMyNTIWMHGC2KAl5hCBENYDlcWC7gOB1LDzRdWlZMAZOEJl83VPb3ggAfUnDo5w%2F'+
	'AFRQxJPj7J4aMhYWCoPyASFFRIAA7'),
			elmTextarea);
		elmTextarea.parentNode.insertBefore(
			createButton(
				elmTextarea,
				textarea_zoom_out,
				'Decrease text size',
				20,
				20,
				'data:image/gif;base64,'+
	'R0lGODlhFAAUAOYAANPS1uTj59va3vDw8bKxtGJiYrOys6Wkpvj4%2BPb29%2FX19mJiY'+
	'%2Ff3%2BKqqrLe3uLKytURDRFpZWqmoqllZW9va3aOjo6Kho4KBg729vWJhZK%2BuskZF'+
	'R4B%2FgsLBxHNydY2Mj%2Ff396amptLS0l9fX9fW2dDQ0W1tbpmZm8DAwfT09fHw8n18f'+
	'uLi49LR1V5eYOjo6VBPUa6tr769wEhHSNra20pJStPS1KuqrNPS1ZmYm%2B7t77Kys8rJ'+
	'y%2Fj4%2BaSjpm9uca%2BvsMjHyqalqHRzdVJRU8PDxVRTVcvKzc3Nz0pKS9rZ3evq7MC'+
	'%2FwsXFxp2cnnl4e1VVVu%2Fv8ba2uM7Oz29vcbu6vZqZmnJxc9vb3PHx8uXk5mhnamJh'+
	'Y1xcXZGQklZVV29vcHl4eoyLjKqpq6Wlpl1dXuXk6AAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
	'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'+
	'AAACH5BAAAAAAALAAAAAAUABQAAAeZgGaCg4SFhoeIiYqKR1IWVgcyi4JMBiQqA0heQgG'+
	'KQTFLPQgMCVocBIoNNqMgCQoDVReKYlELCwUFI1glEYorOgopWSwiTUVfih8dLzRTKA47'+
	'Ek%2BKBGE8GEAhFQYuPooBOWAHY2ROExBbSt83QzMbVCdQST8Ck4QtZUQe9faCABlGrvD'+
	'rB4ALDBMU%2BvnrUuOBQkE4NDycqCgQADs%3D'),
			elmTextarea);
		elmTextarea.parentNode.insertBefore(
			document.createElement('br'),
			elmTextarea);
	}

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://philringnalda.com/blog/2005/06/ms_embraces_rss.php. At the bottom of the page is a form for submitting comments. Above the form, you will see two buttons inserted by the user script, as shown in Figure 8-17.

Type some text into the <textarea>, then click the first button (titled "Increase text size") to make the text and the <textarea> larger, as shown in Figure 8-18. Alternatively, while focus is in the <textarea>, you can press Shift-Tab twice to set focus to the Zoom In button, and then press Enter to activate the button.

Click the second button, titled "Decrease text size," to make the text smaller. Due to rounding, if you repeatedly zoom in and then repeatedly zoom out, the text and its surrounding box may end up a slightly different size. The zooming is not permanent, so you can refresh the page to return to the original size.

Figure 8-17. Zoom toolbar in web form

Zoom toolbar in web form

Figure 8-18. Zoomed web form

Zoomed web form

Make Google More Accessible for Low-Vision Users

Change Google's layout to make it easier for low-vision users to read.

As a class of disabilities, low-vision users are often ignored by accessibility experts. However, accessibility expert Joe Clark has recently published his research into the needs of web users with limited vision. He pioneered a technique known as the zoom layout: a special alternate style applied to a web page that specifically caters to low-vision users.

As I was learning about zoom layouts, it occurred to me that this would be a perfect application of Greasemonkey. (Actually, that thought occurs to me a lot these days.) This hack is my first attempt at transforming a site into a zoom layout.

The Code

This user script runs on several specific Google pages:

Tip

This hack is written to be cross-browser compatible. It works in Firefox with Greasemonkey, in Internet Explorer 6 for Windows with Turnabout, and in Opera 8 with its built-in support for User JavaScript. You can download Turnabout at http://reifysoft.com/turnabout.php, and Opera at http://www.opera.com.

Save the following user script as zoom-google.user.js:

	// ==UserScript==
	// @name		Zoom Google
	// @namespace	http://diveintomark.org/projects/greasemonkey/
	// @description make Google more accessible to low-vision users
	// @include		http://www.google.tld/
	// @include		http://www.google.tld/?*

	// @include http://www.google.tld/webhp*accessibilityGoogle
	// @include http://www.google.tld/imghp*
	// @include http://www.google.tld/search*
	// @include http://images.google.tld/
	// @include http://images.google.tld/?*
	// @include http://images.google.tld/images*
	// ==/Googlelow-vision usersUserScript==

	function addGlobalStyle(css) {
		var elmHead, elmStyle;
		elmHead = document.getElementsByTagName('head')[0];
		elmStyle = document.createElement('style');
		elmStyle.type = 'text/css';
		elmHead.appendChild(elmStyle);
		elmStyle.innerHTML = css;
	}

	function getElementsByClassName(sTag, sClassName) {
		sClassName = sClassName.toLowerCase() + ' ';
		var arElements = document.getElementsByTagName(sTag);
		var iMax = arElements.length;
		var arResults = new Array();
		for (var i = 0; i < iMax; i++) {
			var elm = arElements[i];
			var sThisClassName = elm.className;
			if (!sThisClassName) { continue; }
			sThisClassName = sThisClassName.toLowerCase() + ' ';
			if (sThisClassName.indexOf(sClassName) != -1) {
				arResults.push(elm);
			}	
		}
		return arResults;
	}

	function removeFontTags() {
		// remove font tags
		var arFonts = document.getElementsByTagName('font');
		for (var i = arFonts.length - 1; i >= 0; i--) {
			var elmFont = arFonts[i];
			var elmSpan = document.createElement('span');
			elmSpan.innerHTML = elmFont.innerHTML;
			elmFont.parentNode.replaceChild(elmSpan, elmFont);
		}
	}

	function zoomStyle() {
		addGlobalStyle('body { margin: 30px; } \n' +
	'body, td { font-size: large ! important; } \n' +
	'html>body, html>body td { font-size: x-large ! important; } \n' +
	'body, div, td { background: navy ! important; ' +
		'color: white ! important; } \n' +
	'a:link { background: transparent ! important; ' +
		'color: yellow ! important; } \n' +

	'a:visited { background: transparent ! important; ' +
		'color: lime ! important; } \n' +
	'a.fl { background: transparent ! important; ' +
		'color: white ! important; } \n' +
	'input { font-size: large ! important; } \n' +
	'html>body input { font-size: x-large ! important; } \n' +
	'.g { width: auto ! important; } \n' +
	'.n a, .n .i { font-size: large ! important; } \n' +
	'html>body .n a, html.body .n .i { font-size: x-large ! important; } \n' +
	'.j { width: auto ! important; }');
	}

	function accHomePage() {
		// remove personalized header, if any
		var arTable = document.getElementsByTagName('table');
		for (var i = arTable.length - 1; i >= 0; i--) {
			var elmTable = arTable = ar
			var html = elmTable.innerHTML;
			if (/\/accounts\/Logout/.test(html)) {
				elmTable.parentNode.removeChild(elmTable);
			}
		}

		// simplify logo
		var arImages = document.getElementsByTagName('img');
		for (var i = arImages.length - 1; i >= 0; i--) {
			var elmLogo = arImages[i];
			if (elmLogo.alt) {
				var elmTextLogo = document.createElement('h1');
				elmTextLogo.style.fontSize = '400%';
				var sAlt = /Firefox/.test(elmLogo.alt) ? '' : elmLogo.alt;
				elmTextLogo.appendChild(document.createTextNode(sAlt));
				elmLogo.parentNode.replaceChild(elmTextLogo, elmLogo);
				var elmLink = elmTextLogo.parentNode;
				while (elmLink.nodeName != 'BODY' &&
				elmLink.nodeName != 'HTML' &&
				elmLink.nodeName != 'A') {
				elmLink = elmLink.parentNode;
				}
				elmLink.style.textDecoration = 'none';
			} else {
				elmLogo.parentNode.removeChild(elmLogo);
			}
		}
		// simplify search form
		if (document.forms.length) {
			var arTD = document.getElementsByTagName('td');
			for (var i = arTD.length - 1; i >= 0; i--) {
				var elmTD = arTD[i];
				if (/Advanced/.test(elmTD.innerHTML)) {
				elmTD.innerHTML = '';

				}
			}
		}
	}

	function accSearchResults() {
		// simplify logo
		var elmLogo = document.getElementsByTagName('img')[0];
		var elmTextLogo = document.createElement('h1');
		elmTextLogo.appendChild(document.createTextNode('accessibilityGoogleGoogle'));
		elmTextLogo.style.marginTop = '0.2em';
		elmTextLogo.style.marginRight = '0.3em';
		elmLogo.parentNode.replaceChild(elmTextLogo, elmLogo);
		elmTextLogo.parentNode.style.textDecoration = 'none';

		// simplify top form
		var elmAdvancedWrapper = document.getElementsByTagName('table')[3];
		var elmAdvanced = elmAdvancedWrapper.getElementsByTagName('td')[1];
		elmAdvanced.parentNode.removeChild(elmAdvanced);

		// remove "tip" if present
		var elmTip = document.getElementsByTagName('table')[7];
		if (/Tip/.test(elmTip.innerHTML)) {
			elmTip.parentNode.removeChild(elmTip);
		}

		// remove ads, if any
		var aw1 = document.getElementById('aw1');
		while (aw1) {
			var table = aw1.parentNode;
			while (table.nodeName != 'TABLE') {
				table = table.parentNode;
			}
			table.parentNode.removeChild(table);
			aw1 = document.getElementById('aw1');
		}
		var tpa1 = document.getElementById('tpa1');
		if (tpa1) {
			while (tpa1.nodeName != 'DIV' && tpa1.nodeName != 'P') {
				tpa1 = tpa1.parentNode;
			}
			tpa1.parentNode.removeChild(tpa1);
		}
		var tpa2 = document.getElementById('tpa2');
		if (tpa2) {
			while (tpa2.nodeName != 'DIV' && tpa2.nodeName != 'P') {
				tpa2 = tpa2.parentNode;
			}
			tpa2.parentNode.removeChild(tpa2);
		}
		addGlobalStyle('iframe[name="google_ads_frame"] { ' +
			'display: none ! important }');

		// simplify results count
		var elmDivider = document.getElementsByTagName('table')[5];
		elmDivider.parentNode.removeChild(elmDivider);
		var elmResultsContainer = document.getElementsByTagName('table')[5];
		var arTD = elmResultsContainer.getElementsByTagName('td');
		if (arTD.length > 1) {
			var sResults = arTD[1].textContent;
			var iParen = sResults.indexOf('(');
			if (iParen != -1) {
				sResults = sResults.substring(0, iParen);
			}
			var iDef = sResults.indexOf('[');
			if (iDef != -1) {
				sResults = sResults.substring(0, iDef);
			}
			var elmResults = document.createElement('h2');
			elmResults.appendChild(document.createTextNode(sResults));
			elmResultsContainer.parentNode.replaceChild(elmResults,
				elmResultsContainer);
		} else {
			elmResultsContainer.parentNode.removeChild(elmResultsContainer);
		}

		// make search results use real headers
		var arResults = getElementsByClassName('p', 'g');
		for (var i = arResults.length - 1; i >= 0; i--) {
			var elmResult = arResults[i];
			var arLink = elmResult.getElementsByTagName('a');
			if (!arLink.length) { continue; }
			var elmLink = arLink[0];
			var elmWrapper = document.createElement('div');
			var elmHeader = document.createElement('h3');
			elmHeader.style.margin = elmHeader.style.padding = 0;
			elmHeader.innerHTML = '<a href="' + elmLink.href + '">' +
				elmLink.innerHTML + '</a>';
			var elmContent = elmResult.cloneNode(true);
			elmContent.innerHTML = elmContent.innerHTML.replace(/<nobr>/g, '');
			arLink = elmContent.getElementsByTagName('a');
			if (!arLink.length) { continue; }
			elmLink = arLink[0];
			elmContent.removeChild(elmLink);
			elmContent.style.marginTop = 0;
			elmWrapper.appendChild(elmHeader);
			elmWrapper.appendChild(elmContent);
			elmResult.parentNode.replaceChild(elmWrapper, elmResult);
		}

		// simplify next page link
		var arFont = document.getElementsByTagName('font');
		for (var i = arFont.length - 1; i >= 0; i--) {
			var elmFont = arFont[i];

		var html = elmFont.innerHTML;
		if (/Result\&nbsp\;Page\:/.test(html)) {
			var elmTable = elmFont.parentNode;	
			while (elmTable.nodeName != 'TABLE') {
				elmTable = elmTable = elm
			}
			var arTD = elmTable.getElementsByTagName('td');
			if (arTD.length) {
				var elmTD = arTD[arTD.length - 1];
				var arNext = elmTD.getElementsByTagName('a');
				if (arNext.length) {
				var elmNext = arNext[0];
				var elmTextNext = document.createElement('center');
				elmTextNext.innerHTML = '<p style="font-size: ' +	
				'xx-large; margin-bottom: 4em;">b><a href="' +
				elmNext.href + '">More Results  ' +
				'&rarr;</a></b></p>';
				elmTable.parentNode.replaceChild(elmTextNext,		
				elmTable);
				}
			}
			break;
		}
	}

	// remove bottom ads
	var arCenter = document.getElementsByTagName('center');
	if (arCenter.length > 1) {
		var elmCenter = arCenter[1];
		elmCenter.parentNode.removeChild(elmCenter);
		elmCenter = arCenter[0];
		for (var i = 0; i < 4; i++) {
			elmCenter.innerHTML = elmCenter.innerHTML.replace(/<br>/, '');
		}
	}

}

document.forms.namedItem('f') && accHomePage();
document.forms.namedItem('gs') && accSearchResults();
removeFontTags();
zoomStyle();

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://www.google.com. The normally spartan search form has been magnified and simplified even further, as shown in Figure 8-19.

Accessibility studies have shown that low-vision users have an easier time reading light text on a dark background, so therefore the page is displayed as white-on-navy. Unvisited links are displayed in yellow; visited links are displayed in light green. The hack removes several elements from the page, including the Advanced Search link, plus any advertisements for Google services or other messages that occasionally appear below the search box.

Figure 8-19. Google home page, zoomed

Google home page, zoomed

When you execute a search, the search results are displayed differently, as shown in Figures 8-20 and 8-21, with the following notable differences:

  • The entire page uses the same white-on-navy color scheme we used on the home page.
  • The Google logo in the top-left corner is displayed as plain text instead of as an image.
  • The top search form no longer includes the Advanced Search option.
  • The sponsored links along the top and right are gone.
  • The number of results is displayed much larger than before, and in the same white-on-navy color scheme.
  • Links to search results pages are displayed in yellow (or green, if you've already visited that page). Other links within each search result, such as the "Cached" and "Similar pages" links, are displayed in white.
  • The "Goooooooogle" navigation bar to see more results is replaced by a simple link titled "More results."
  • The search box at the bottom of the page is gone.

Figure 8-20. Google search results, zoomed

Google search results, zoomed

Figure 8-21. Bottom of Google search results, zoomed

Bottom of Google search results, zoomed

If you click the Images link at the top of the page to search for the same keywords in Google Image Search, you will see that the image search results have been similarly hacked, as shown in Figure 8-22.

Figure 8-22. Google image results, zoomed

Google image results, zoomed

As with the web search results, the top navigation has been simplified, the number of results is more prominent, and the "Goooooooogle" navigation bar has been replaced by a single "More results" link that moves to the next page of images. The image thumbnails themselves cannot be magnified, since Google provides them only in a specific size.

Notes

  1. Houtenville, Andrew J. "Disability Statistics in the United States." Ithaca, NY: Cornell University Rehabilitation Research and Training Center on Disability Demographics and Statistics (StatsRRTC), http://www.disabilitystatistics.org, April 4, 2005.
Personal tools