// KeyNav
// version 0.1.1 beta
// Itamar Benzaken
// modified by Andrwe and Seiichiro0185
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "KeyNav", and click Uninstall.
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name KeyNav
// @description Enables keyboard navigation
// @namespace tag:itamar.benzaken@gmail.com,2008-09-10:KeyNav
// @include *
// ==/UserScript==
/*
@what
The script enables keyboard navigation.
@how
Pressing a hotkey displays a (or hides the) label near each clickable element.
Pressing the shortcut triggers the "click" for the associated element.
@structure
- A Map object: enables mapping of a String (the shortcut) to an Object (the DOM element).
- A Configuration object: contains anything that can be "externalized" (preferences, actual label generation, etc).
- A KeyNav object: the main object that handles all the messy stuff.
@changelog
0.1
initial
0.1.1
- added labels index for faster looking up of labels
- moved creation of labels to initialization
- added event handler for invalidating labels when document changes
*/
//javascript.options.strict = true;
//browser.dom.window.dump.enabled = true;
function Map() {
function LOG(message) {
//window.dump( "[Map] " + message + "\n");
}
// internal map
var map;
var bEmpty;
/* returns the value associated with the given key
*
* @param key
* @return the associated value, or null if none
*/
this.getValue = function (key) {
if (key in map)
return map[key];
return null;
}
/* returns a set of values whose keys pass a certain filter
*
* @param predicate a filter Function [key->Boolean] that returns True
* iff the key's value should be included in the result set
* @return a values Array
*/
this.getValuesByPredicate = function (predicate) {
var values = new Array(0);
for (key in map) {
if ( predicate(key) ) {
values.push( map[key] );
}
}
return values;
}
/* add a key=value binding
*
* @param val
* @param key
*/
this.setValue = function (key, val) {
bEmpty = false;
map[key] = val;
LOG( "bound \"" + key + "\" to " + val);
}
this.clear = function () {
map = new Object();
bEmpty = true;
}
this.isEmpty = function() {
return bEmpty;
}
this.keys = function () {
var keys = new Array();
for ( var key in map ) {
keys.push(key);
}
return keys;
}
this.values = function () {
var values = new Array();
for ( var key in map ) {
values.push( map[key] );
}
return values;
}
this.clear();
}
/* Configuration object */
function Configuration() {
function LOG(message) {
//window.dump( "[Configuration] " + message + "\n");
}
this.getPreference = function (key) {
if ( key=="activationKeyCode" ) {
return 70; //f
}
else if ( key=="deactivationKeyCode" ) {
return 70; //f
}
else
return null;
}
/* returns a UNIQUE shortcut calculated using `ndx`
*
* @param ndx a non negative integer Number
* @return a unique (in terms of ndx) String
*/
this.generateShortcut = function (ndx) {
return ndx;
}
this.createLabel = function(element,shortcut) {
var overlay = getElementOverlay(element);
var overlayId = "keynav.shortcut["+shortcut+"]";
// no overlay at all yet? create one
if (!overlay) {
LOG("creating a new empty, hidden overlay");
overlay = document.createElement("span");
overlay.style.position = "absolute";
overlay.style.background = "lightyellow";
overlay.style.fontSize = "small";
overlay.style.fontColor = "black";
overlay.style.border = "1px dashed darkgray";
overlay.style.fontColor = "black";
overlay.style.visibility = "hidden";
overlay.style.padding = "1px";
// insert as a sibling, because the element itself might not be able
// to have children (like a Button, for example)
element.parentNode.insertBefore(overlay,element);
}
// new/wrong shortcut? fix shortcut
if ( (!overlay.id) | (overlay.id!=overlayId) ) {
LOG("changing overlay id from '" + overlay.id + "' to '" + overlayId + "'");
overlay.id = overlayId;
overlay.innerHTML = "" + shortcut + "";
element.setAttribute( "keynav:shortcut", shortcut );
}
else {
LOG("overlay already exists for id: " + overlayId);
}
return overlay;
}
/** Returns the shortcut overlay of the specified element
*
* @param element
* @return
*/
function getElementOverlay(element) {
if ( element.hasAttribute("keynav:shortcut") ) {
var shortcutElementId = "keynav.shortcut[" +
element.getAttribute("keynav:shortcut") + "]";
var shortcutElement = document.getElementById(shortcutElementId);
return shortcutElement;
} else {
return null;
}
}
/** highlights (or dims) the element's shortcut.
*
* @param element the Element whose shortcut is to be highlighted/dimmed
* @param on a Boolean - true to highlight, false to dim.
*/
this.highlightShortcut = function (element,on) {
var overlay = getElementOverlay(element);
if (overlay) {
overlay.style.background = on ? "lightgreen" : "lightyellow";
overlay.style.border = on ? "1px solid black" : "1px dashed darkgray";
}
}
/** Selects the elements of the document to assign shortcuts to
*
* @return an array of Elements
*/
this.getClickableElements = function() {
var ems = new Array(0);
function addClickableElementsIn( parent ) {
for (var i=0; i(or press Enter to \"click\" " +
lastShortcut + ")");
LOG(lastShortcut + " bound to " + targets.length + " targets");
}
}
function onKeyDown(evt) {
var currEle = document.activeElement;
if ( currEle.tagName == "input" | currEle.tagName == "INPUT" | currEle.tagName == "textarea" | currEle.tagName == "TEXTAREA" )
{
toggle(false);
return;
}
// activation
if (!acceptInput & evt.keyCode==conf.getPreference("activationKeyCode")) {
toggle(true);
}
// deactivation
else if (acceptInput & evt.keyCode==conf.getPreference("deactivationKeyCode")) {
toggle(false);
return;
}
if (!acceptInput) return;
switch (evt.keyCode) {
//case KeyEvent.DOM_VK_RETURN:
case 0x0D: //KeyEvent.DOM_VK_ENTER:
// match the exact shortcut typed so far
var target = bindings.getValue(lastShortcut);
LOG("match for exact shortcut " + lastShortcut + ": " + target);
if (target) {
toggle(false);
conf.simulateClick(target);
}
lastShortcut = "";
break;
}
}
function installEventListeners() {
window.addEventListener("keydown",onKeyDown,true);
window.addEventListener("keypress",onKeyPress,true);
}
createLabels();
installEventListeners();
}
var keynav = new KeyNav();