Displaying accesskeys in a fixed status bar
Access Bar displays accesskeys defined on a web page. Accesskeys are an accessibility feature that allow you to jump to a specific link or form field with a keyboard shortcut defined by the page author. (Learn more about accesskeys.)
Firefox supports accesskeys, but it does not display which keys are defined on a page. Thus, Access Bar was born.
Example: accessbar.user.js
// ==UserScript== // @name Access Bar // @namespace http://diveintogreasemonkey.org/download/ // @description show accesskeys defined on page // @include * // ==/UserScript== function addGlobalStyle(css) { var head, style; head = document.getElementsByTagName('head')[0]; if (!head) { return; } style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = css; head.appendChild(style); } var akeys, descriptions, a, desc, label, div; akeys = document.evaluate( "//*[@accesskey]", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); if (!akeys.snapshotLength) { return; } descriptions = new Array(); desc = ''; for (var i = 0; i < akeys.snapshotLength; i++) { a = akeys.snapshotItem(i); desctext = ''; if (a.nodeName == 'INPUT') { label = document.evaluate("//label[@for='" + a.name + "']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (label) { desctext = label.title; if (!desctext) { desctext = label.textContent; } } } if (!desctext) { desctext = a.textContent; } if (!desctext) { desctext = a.title; } if (!desctext) { desctext = a.name; } if (!desctext) { desctext = a.id; } if (!desctext) { desctext = a.href; } if (!desctext) { desctext = a.value; } desc = '<strong>[' + a.getAttribute('accesskey').toUpperCase() + ']</strong> '; if (a.href) { desc += '<a href="' + a.href + '">' + desctext + '</a>'; } else { desc += desctext; } descriptions.push(desc); } descriptions.sort(); div = document.createElement('div'); div.id = 'accessbar-div-0'; desc = '<div><ul><li class="first">' + descriptions[0] + '</li>'; for (var i = 1; i < descriptions.length; i++) { desc = desc + '<li>' + descriptions[i] + '</li>'; } desc = desc + '</ul></div>'; div.innerHTML = desc; document.body.style.paddingBottom = "4em"; window.addEventListener( "load", function() { document.body.appendChild(div); }, 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;' + '}' + '#accessbar-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;' + '}');
The code breaks down into six steps:
- Define a helper function,
addGlobalStyle
- Find all the page elements that include an
accesskey
attribute - Loop through those elements and determine the most appropriate text to describe each element
- Construct a sorted list of links to the
accesskey
-enabled elements - Add the sorted list to the page, using standard
ul
andli
elements - Style the list so it appears as a fixed faux-status-bar at the bottom of the viewport
First things first. I need a helper function, addGlobalStyle
, to inject my own CSS styles (used in step 6). See Adding CSS styles for more details on this pattern.
function addGlobalStyle(css) { var head, style; head = document.getElementsByTagName('head')[0]; if (!head) { return; } style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = css; head.appendChild(style); }
Step 2 gets a list of all the page elements that contain an accesskey
attribute. This is easy with Firefox's XPath support. Note that if the XPath query returns no results, I simply bail, since
there will be nothing to display. See Doing something for every element with a certain attribute for more information on this pattern.
var akeys, descriptions, a, i, desc, label, div; akeys = document.evaluate( "//*[@accesskey]", document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); if (!akeys.snapshotLength) { return; }
Step 3, constructing the list of suitable descriptions for each accesskey
-enabled element, is the most complicated part of the script. The problem is that accesskey
attributes can appear in several different HTML elements.
An input
element in a form can define an accesskey
. input
elements may or may not have an associated label
that contains an associated text label for the input
field. If so, the label
may contain a title
attribute that gives even more detailed information about the input
field. Or the label
attribute may simply contain text. Or the input
element may have no associated label at all, in which case the value
attribute of the input
element is the best I can do.
On the other hand, the label
itself can define the accesskey
, instead of the input
element the label describes. Again, I'll look for a description 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 only contains an
image), then the link's title
attribute is the next place to look. If the link contains no text and no title
, I fall back to the link's name
attribute, and failing that, the link's id
attribute.
Ain't heuristics a bitch? Here's the full algorithm. Remember, akeys
is an XPathResult
object, so I need to get each result by calling akeys.snapshotItem(i)
.
descriptions = new Array(); desc = ''; for (var i = 0; i < akeys.snapshotLength; i++) { a = akeys.snapshotItem(i); desctext = ''; if (a.nodeName == 'INPUT') { label = document.evaluate("//label[@for='" + a.name + "']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (label) { desctext = label.title; if (!desctext) { desctext = label.textContent; } } } if (!desctext) { desctext = a.textContent; } if (!desctext) { desctext = a.title; } if (!desctext) { desctext = a.name; } if (!desctext) { desctext = a.id; } if (!desctext) { desctext = a.href; } if (!desctext) { desctext = a.value; } desc = '<strong>[' + a.getAttribute('accesskey').toUpperCase() + ']</strong> '; if (a.href) { desc += '<a href="' + a.href + '">' + desctext + '</a>'; } else { desc += desctext; } descriptions.push(desc); }
Step 4 is simple, since Javascript arrays have a sort
method that sorts the array in place.
descriptions.sort();
Step 5 creates the HTML to render the list of accesskey
-enabled elements. I create a wrapper <div>
, construct the HTML for the list of accesskeys as a string, set the wrapper <div>
's innerHTML
property, and finally add it to the end of the page. Because of when user scripts are executed, complex changes to a page
should be delayed until after the page is done loading, so I use window.addEventListener
to add an onload
event that adds the wrapper <div>
to the page.
See Inserting complex HTML quickly for more information on the use of innerHTML
, and Post-processing a page after it renders for information on the use of window.addEventListener
.
div = document.createElement('div'); div.id = 'accessbar-div-0'; desc = '<div><ul><li class="first">' + descriptions[0] + '</li>'; for (var i = 1; i < descriptions.length; i++) { desc = desc + '<li>' + descriptions[i] + '</li>'; } desc = desc + '</ul></div>'; div.innerHTML = desc; document.body.style.paddingBottom = "4em"; window.addEventListener( "load", function() { document.body.appendChild(div); }, true);
Finally, in step 6, I add my own set of CSS declarations to the page so that the HTML I'm injecting will look pretty. (Specifically, it will appear as a fixed black bar along the bottom of the page, that stays
in view even as you scroll the page. This is made possible by Firefox's support for the position:fixed
display type.) See Adding CSS styles for more information.
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;' + '}' + '#accessbar-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;' + '}');