// 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();