1. // VimNav.user.js
  2. // Version 1.1
  3. //
  4. // ==UserScript==
  5. // @name        VimNav
  6. // @author      Bernd Pol <bernd.pol@online.de>
  7. // @licence     GPL
  8. // @description Vim-like navigation in a webkit based browser.
  9. // ==/UserScript==
  10.  
  11. /*
  12.  * VERSIONS
  13.  *
  14.  * 1.1  Bugfix
  15.  *      - proper handling of password fields
  16.  *        (no more VarNav functions called from inside)
  17.  *      - input fields now properly selected
  18.  *      - labels display unaltered
  19.  *        (could have been changed to uppercase if node parent did so)
  20.  *      New Features
  21.  *      - scroll height adjustable by factors 1, 2, 3, and 4
  22.  *        (by default bound to "^1", "^2", "^3", and "^4")
  23.  *
  24.  * 1.0  Initial Release
  25.  */
  26. /*
  27.  * TODO
  28.  * - second level commands (like "gu", etc.)
  29.  * - repeat counts
  30.  */
  31. /*
  32.  * KNOWN ISSUES
  33.  *
  34.  * On more complicated structured pages the browser (Midori only?) will call up
  35.  * this script several times in a row. This will cause commands to be repeatedly
  36.  * called up, once per running instance.
  37.  * There is no way to detect such a multiple instatiating on the Javascript
  38.  * level however which means:
  39.  * - vertical movements will occur over multiples of the programmed distance
  40.  *   (to cope for this situation a window height adjust factor has been
  41.  *   introduced which will effectively reduce the height by which each movement
  42.  *   call in a row will scroll the window, by default bound to ^1, ^2, ^3, ^4)
  43.  * - hint labels will take significant more time to show up
  44.  *   (hint-based navigation will be unaffected in most cases, however, as only
  45.  *   one instance will effectively control the behaviour)
  46.  */
  47. /*
  48.  * CREDITS
  49.  *
  50.  * This is inspired by (and was partially copied from):
  51.  * - the "vimkeybindings" greasemonkey script by "arno <arenevier@fdn.fr>"
  52.  *   <http://userscripts.org/scripts/review/32369>
  53.  * - the "KeyNav" greasemonkey script, version 0.1.1 beta by Itamar Benzaken
  54.  *   <http://userscripts.org/scripts/review/33808>
  55.  * - the "follow.js" uzbl link following script
  56.  *   <http://www.uzbl.org/wiki/follow.sh>
  57.  * - and the "goup", javascript version, uzbl page/domain switching script
  58.  *   <http://www.uzbl.org/wiki/go-up>
  59.  */
  60.  
  61. /*
  62.  * Note: This is a Midori browser specific script.
  63.  *       Using other browsers may require some rewrite.
  64.  */
  65.  
  66. /****************
  67.  *** Contents ***
  68.  ****************/
  69. /*
  70.  * Configuration
  71.  *      Key Bindings Configuration
  72.  *          var keyBindings
  73.  *          var navKeyBindings
  74.  *      Basic Label Setup
  75.  *          var autoselectLink
  76.  *          var collSequence
  77.  *          var overlayId
  78.  *          var shortenLabels
  79.  *          var nodeLabelSize
  80.  *          var nodeLabelColor
  81.  *          var nodeLabelBckground
  82.  *          var partialLabelColor
  83.  *          var partialLabelBackground
  84.  *          var foundLabelColor
  85.  *          var foundLabelBackground
  86.  *          var nodeOpacity
  87.  * Simple Navigation
  88.  *      Small vertical movements
  89.  *          function goUp()
  90.  *          function goDown()
  91.  *      Halfpage vertical movements
  92.  *          function goHalfUp()
  93.  *          function goHalfDown()
  94.  *      Fullpage vertical movements
  95.  *          function goPageUp()
  96.  *          function goPageDown()
  97.  *      Horizontal movements
  98.  *          function goRight()
  99.  *          function goLeft()
  100.  *      Document wide vertical movements
  101.  *          function goTop()
  102.  *          function goBottom()
  103.  *      URL dependent stuff
  104.  *          function goUrlPageUp()
  105.  *          function goUrlDomainUp()
  106.  *      Adjust Movements
  107.  *          function adjustHeight1()
  108.  *          function adjustHeight2()
  109.  *          function adjustHeight3()
  110.  *          function adjustHeight4()
  111.  *  Link Following
  112.  *      Common variables
  113.  *      Clear all link information
  114.  *          function clearLinkInfo()
  115.  *      Label handling
  116.  *          function labelText( posNumber )
  117.  *          function labelNumber( labelString )
  118.  *          function positionOf( thisElement )
  119.  *      Maintaining navigation information
  120.  *          function isVisible( thisElement )
  121.  *          function isDisplayable( thisElement )
  122.  *          function findClickableNodes()
  123.  *          function createOverlays()
  124.  *          function showOverlays( labelHead )
  125.  *          function hideOverlays()
  126.  *          function redisplayOverlays()
  127.  *          function removeOverlays()
  128.  *      Navigating
  129.  *          function isValidLabel( thisHead )
  130.  *          function navigateByLabel()
  131.  *          function clickLabel( labelPos )
  132.  *          function startNavigating()
  133.  *          function startNavNewTab()
  134.  *          function stopNavigating()
  135.  * Keyboard Interface
  136.  *      function isEditable( element )
  137.  *      function evalKey( keyEvent )
  138.  *      function hasValidNavKey()
  139.  *      function keyHandler( keyEvent )
  140.  *      function keyRepeatHandler( keyEvent )
  141.  * Script Body
  142.  *      Initialization
  143.  *      Register keyboard event handlers
  144.  */
  145.  
  146. // -----------------------------------------------------------------------------
  147. //                       start of configuration section
  148. // -----------------------------------------------------------------------------
  149.  
  150. /*********************
  151.  *** Configuration ***
  152.  *********************/
  153. /*
  154.  * Key Bindings Configuration
  155.  * ==========================
  156.  *
  157.  * To use a Ctrl-key combination prepend "^" before the character
  158.  * (e.g. "^b" denotes the Ctrl-b control).
  159.  * Character case is implicit, e.g. "B" denotes Shift-b.
  160.  *
  161.  * The Esc key has been set up as universal stop action key which is treated
  162.  * separately in the keydown event handler.
  163.  *
  164.  * NOTE:
  165.  * Javascript apparently has problems to properly process language specific
  166.  * keyboards (like umlauts on a german layout), thus best use the ASCII
  167.  * character set only.
  168.  *
  169.  * NOTE:
  170.  * Midori processes its own shortcuts before they reach this script. So make
  171.  * sure there are no conflicts.
  172.  * --> If necessary redefine conflicting Midori specific shortcuts there in
  173.  *                       Tools->Customize Shortcuts...
  174.  */
  175. /*
  176.  * Standard key bindings
  177.  * ---------------------
  178.  */
  179. var keyBindings = {
  180.     "h" : goLeft,
  181.     "l" : goRight,
  182.     "k" : goHalfUp,
  183.     "j" : goHalfDown,
  184.     "K" : goUp,
  185.     "J" : goDown,
  186.     "u" : goPageUp,
  187.     "d" : goPageDown,
  188.     "t" : goTop,
  189.     "b" : goBottom,
  190.     "U" : goUrlPageUp,
  191.     "D" : goUrlDomainUp,
  192.    "^1" : adjustHeight1,    // factors to decrease the effective window
  193.    "^2" : adjustHeight2,    // height in scrolling (sometimes useful
  194.    "^3" : adjustHeight3,    // when this script was called up multiple
  195.    "^4" : adjustHeight4,    // times in a row)
  196.     "f" : startNavigating,  // if autoselecting, open match in current tab
  197.     "F" : startNavNewTab,   // if autoselecting, open match in a new tab
  198. }
  199. /*
  200.  * Navigation key bindings
  201.  * -----------------------
  202.  * These provide some page movement actions when navigating by labels where the
  203.  * usual navigation keys are not available.
  204.  *
  205.  * NOTE: These bindings are only valid when navigating. Otherwise the standard
  206.  *       bindings defined above apply.
  207.  */
  208. var navKeyBindings = {
  209.     "^h" : goLeft,
  210.     "^l" : goRight,
  211.     "^k" : goHalfUp,
  212.     "^j" : goHalfDown,
  213.     "^t" : goTop,
  214.     "^b" : goBottom,
  215.     "^s" : redisplayOverlays,
  216.     "^d" : hideOverlays,
  217.     "^r" : repositionLabels,    // sometimes useful if labels overlap
  218.     "^f" : stopNavigating,  
  219. }
  220. /*
  221.  * Basic Label Setup
  222.  * =================
  223.  */
  224. /*
  225.  * Link selection behaviour
  226.  * ------------------------
  227.  */
  228. /*
  229.  * How to select a link
  230.  * --------------------
  231.  * If true this will select link as soon as there is a match.
  232.  * Otherwise the user must confirm the selection:
  233.  * Return:  open link in this tab
  234.  * Space:   open link in new tab
  235.  * Although this requires an additional keypress it allows for selecting another
  236.  * link (via backspace correction).
  237.  */
  238. var autoselectLink = true;
  239. /*
  240.  * If not autoselecting we need some special keys to trigger the selection.
  241.  */
  242. var openInThisTab = "g";
  243. var openInNewTab = "t";
  244. /*
  245.  * Collateral Sequences
  246.  * -----------------------
  247.  *
  248.  * There are several label number representations possible. Just uncomment the
  249.  * one you want.
  250.  *
  251.  * Note that the first symbol in sequence will be treated as zero equivalent
  252.  * and the labels will be get those zero equivalents prepended if necessary,
  253.  * e.g. the number 1 in a three-digit "alpha" sequence will show as "aab".
  254.  */
  255. var collSequence = "optimal";   // automatic: find shortest to type sequence
  256. // var collSequence = "numeric";   // decimal numbers
  257. // var collSequence = "alpha";     // lower case letter sequences
  258. // var collSequence = "longalpha"; // lower followed by upper case letters
  259. // This can be any unique sequence of symbols, e.g.:
  260. // var collSequence = "asdfghjkl"; // home row keys (for touch typers)
  261. // var collSequence = "uiophjklnm"; // right hand only
  262. /*
  263.  * The overlay identification
  264.  * --------------------------
  265.  * This will be prepended to every label overlay element. Redefine if there are
  266.  * name conflicts.
  267.  */
  268. var overlayId = "VimNavLabel";
  269. /*
  270.  * Node label display
  271.  * ------------------
  272.  */
  273. var shortenLabels = true;   // show matching labels and selectable digits only
  274. /*
  275.  * This defines the font size shown in the labels. It may be an absolute number
  276.  * with a trailing "px" giving the font height in pixels, a number with trailing
  277.  * "%" giving the height relative to the parents font size, or one of the
  278.  * predefined font size property values (ranging from smallest to largest):
  279.  * "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large"
  280.  * or defining a relative value to the parent font size:
  281.  * "smaller", "larger"
  282.  * ( see also: http://www.w3schools.com/jsref/prop_style_fontsize.asp )
  283.  */
  284. var nodeLabelSize = "12px";
  285. //var nodeLabelSize = "85%";
  286. //var nodeLabelSize = "small";
  287.  
  288. var nodeLabelColor = "red";
  289. var nodeLabelBackground = "lightyellow";
  290.  
  291. var partialLabelColor = "blue";
  292. var partialLabelBackground = "lightgreen";
  293.  
  294. var foundLabelColor = "yellow";
  295. var foundLabelBackground = "red";
  296.  
  297. var nodeOpacity = 0.6;
  298.  
  299. // -----------------------------------------------------------------------------
  300. //                        end of configuration section
  301. // -----------------------------------------------------------------------------
  302.  
  303. /*************************
  304.  *** Simple Navigation ***
  305.  ************************/
  306.  
  307. var wndHeight;              // height of the currently focused window
  308. var wndHeightAdjust = 1;    // factor to decrease the height movement
  309. var keyLabel;               // label value of current key
  310. var keyCode;                // code value of current key
  311. var keyAction = 0;          // the action to perform on the current key
  312. /*
  313.  * Small vertical movements
  314.  */
  315. function goUp() {
  316.     window.scrollBy( 0, -5 );
  317. }
  318.  
  319. function goDown() {
  320.     window.scrollBy( 0, 5 );
  321. }
  322. /*
  323.  * Halfpage vertical movements
  324.  */
  325. function goHalfUp() {
  326.     window.scrollBy( 0, -1 * ((wndHeight / wndHeightAdjust) / 2) );
  327. }
  328.  
  329. var sc = 0;
  330. function goHalfDown() {
  331.     window.scrollBy( 0, (wndHeight / wndHeightAdjust) / 2 );
  332. }
  333. /*
  334.  * Fullpage vertical movements
  335.  */
  336. function goPageUp() {
  337.     window.scrollBy( 0, -1 * (wndHeight / wndHeightAdjust) );
  338. }
  339.  
  340. function goPageDown() {
  341.     window.scrollBy( 0, wndHeight / wndHeightAdjust );
  342. }
  343. /*
  344.  * Horizontal movements
  345.  */
  346. function goRight() {
  347.     window.scrollBy( 15, 0 );
  348. }
  349.  
  350. function goLeft() {
  351.     window.scrollBy( -15, 0 );
  352. }
  353. /*
  354.  * Document wide vertical movements
  355.  */
  356. function goTop() {
  357.     window.scroll( 0, 0 );
  358. }
  359.  
  360. function goBottom() {
  361.     window.scroll( document.width, document.height );
  362. }
  363. /*
  364.  * URL dependent stuff
  365.  * ===================
  366.  */
  367. /*
  368.  * Go up one page in the URL
  369.  * -------------------------
  370.  */
  371. function goUrlPageUp() {
  372.     /*
  373.      * Most of this could be inline below. We compute these here to keep the
  374.      * switching stuff better readable.
  375.      * TODO
  376.      * There is a recursion problem if the shortened URL was implicitely
  377.      * expanded to point to the current page again. This could be caught if
  378.      * there was a possibilitiy to keep the current URL somehow globally when
  379.      * the document reloads.
  380.      */
  381.     var oldLocation = window.location;
  382.     var newLocation = null;
  383.     var newLocArray =
  384.         window.location.href.match(/(\w+:\/\/.+?\/)([\w\?\=\+\%\&\-\.]+\/?)$/);
  385.     if (newLocArray) {
  386.         newLocation = newLocArray[1];
  387.     }
  388.     /*
  389.      *  Now go up one level if possible.
  390.      */
  391.     if (newLocation && newLocation != oldLocation) {
  392.         window.location = newLocation;
  393.     }
  394.     /*
  395.      * We are at the top page already. Let the user know this.
  396.      */
  397.     else    
  398.         alert( "Already at top page. Cannot go further up." );
  399. }
  400. /*
  401.  * Go up one domain in the URL
  402.  * ---------------------------
  403.  */
  404. function goUrlDomainUp() {
  405.     var oldDomain = document.domain;
  406.     var newDomain = null;
  407.     /*
  408.      * Even if we do not often need subdomain switching let's keep this stuff
  409.      * more readable, too.
  410.      */
  411.     var subDomArray =
  412.         oldDomain.match(/^(?!www\.)\w+\.(.+?)\.([a-z]{2,4})(?:\.([a-z]{2}))?$/);
  413.     if (subDomArray) {
  414.         /*
  415.          * The URL is now broken up into array elements.
  416.          * We take the subdomain out and join everything together to th new URL.
  417.          */
  418.         var subDomL = subDomArray.length;
  419.         newDomain =
  420.             window.location.protocol + "//" +
  421.             subDomArray.slice(1, subDomArray[subDomL] ? subDomL
  422.                                                       : subDomL-1).join(".");
  423.     }
  424.     /*
  425.      * Go up one sub domain level if possible.
  426.      */
  427.     if (newDomain) {
  428.         window.location = newDomain;
  429.     }
  430.     /*
  431.      * We are at the top domain already. Let the user know this.
  432.      */
  433.     else    
  434.         alert( "Already at top domain. Cannot go further up." );
  435. }
  436. /*
  437.  * Adjust Movements
  438.  * ================
  439.  *
  440.  * These functions only set the wndHeightAdjust factor in order to cope whith
  441.  * situations where the browser (Midori only?) initializes the script multiple
  442.  * times (which currently can happen on complicated structured pages).
  443.  * Their only purpose is to bind these adjustments to some keys.
  444.  */
  445. function adjustHeight1() {
  446.     wndHeightAdjust = 1;
  447. }
  448.  
  449. function adjustHeight2() {
  450.     wndHeightAdjust = 2;
  451. }
  452.  
  453. function adjustHeight3() {
  454.     wndHeightAdjust = 3;
  455. }
  456.  
  457. function adjustHeight4() {
  458.     wndHeightAdjust = 4;
  459. }
  460.  
  461. /**********************
  462.  *** Link Following ***
  463.  **********************/
  464. /*
  465.  * Common variables
  466.  */
  467. var navigating = false;
  468. var openNewTab = false;
  469. var waitForConfirmation = false;
  470.  
  471. var clickableNodes;
  472. var labelsOverlays;
  473. var nodeLabels;
  474.  
  475. var hasLinkNodes;
  476. var curLabelHead;
  477. var curLabelNum;
  478. var matchingLabelNum;
  479. var labelDigits;
  480.  
  481. var useSequence;
  482. var useBase;
  483.  
  484. var relPosLabels = false;
  485. /*
  486.  * Clear All Link Information
  487.  * --------------------------
  488.  */
  489. function clearLinkInfo() {
  490.     removeOverlays();
  491.     labelsOverlays = null;
  492.     nodeLabels = null;
  493.     clickableNodes = null;
  494.     hasLinkNodes = false;
  495.     curLabelHead = "";
  496.     curLabelNum = -1;   // marks number as invalid
  497.     matchingLabelNum = -1;
  498.     labelDigits = 0;
  499.     waitForConfirmation = false;
  500.     relPosLabels = false;
  501. }
  502. /*
  503.  * Label Handling
  504.  * ==============
  505.  */
  506. /*
  507.  * Construct the Label Text For a Given Position Number
  508.  * ----------------------------------------------------
  509.  * @param posNumber decimal position number
  510.  *                  (must be >= 0)
  511.  * @return          string representation of this number according to the
  512.  *                  predefined collateral sequence.
  513.  *                  Leading filled with the zero equivalence of the predefined
  514.  *                  collateral sequence up to labelDigits length.
  515.  */
  516. function labelText( posNumber ) {
  517.     var head = posNumber;
  518.     var remainder = 0;
  519.     var labelString = "";
  520.     /*
  521.      * Numeric sequences should count from 1 instead from 0.
  522.      */
  523.     if (collSequence == "numeric" ||
  524.         (collSequence == "optimal" && useSequence.charAt(0) == "0"))
  525.         head++;
  526.     /*
  527.      * Compute the symbolic digits.
  528.      */
  529.     if (head == 0) {
  530.         labelString = useSequence.charAt(0);
  531.     }
  532.     while (head > 0) {
  533.         remainder = head % useBase;
  534.         labelString = useSequence.charAt(remainder) + labelString;
  535.         head = (head - remainder) / useBase;
  536.     }
  537.     // Fill with the zero equivalent of this collateral sequence.
  538.     while (labelString.length < labelDigits) {
  539.         labelString = useSequence.charAt(0) + labelString;
  540.     }
  541.     return labelString;
  542. }
  543. /*
  544.  * Construct the Label Lumber For a Given Label String
  545.  * ---------------------------------------------------
  546.  * @param labelString   string representation of the label
  547.  * @return              decimal equivalent according to the prdefined
  548.  *                      collataration sequence.
  549.  */
  550. function labelNumber( labelString ) {
  551.     var posNumber = 0;
  552.     var curBase = useBase;
  553.     var curDigit;
  554.  
  555.     for (var i=labelString.length-1; i >= 0; i--) {
  556.         curDigit = labelString.charAt(i);
  557.         posNumber += useBase * useSequence.indexOf(curDigit);
  558.         curBase *= useBase;
  559.     }
  560.     /*
  561.      * Adjust for numeric counting from 1 instead of 0.
  562.      */
  563.     if (collSequence == "numeric" ||
  564.         (collSequence == "optimal" && useSequence.charAt(0) == "0"))
  565.         posNumber--;
  566.  
  567.     return posNumber;
  568. }
  569. /*
  570.  * Evaluate the position of an element
  571.  * -----------------------------------
  572.  *
  573.  *  @param  thisElement element to inspect
  574.  *  @return array [up, left, width, height] of position and size
  575.  */
  576. function positionOf( thisElement ) {
  577.     var up = thisElement.offsetTop;
  578.     var left = thisElement.offsetLeft;
  579.     var width = thisElement.offsetWidth;
  580.     var height = thisElement.offsetHeight;
  581.     while (thisElement.offsetParent) {
  582.         thisElement = thisElement.offsetParent;
  583.         up += thisElement.offsetTop;
  584.         left += thisElement.offsetLeft;
  585.     }
  586.     return [up, left, width, height];
  587. }
  588. /*
  589.  * Switch the relative positions mode of the labels display.
  590.  * ---------------------------------------------------------
  591.  * In some elements (i.e. some tables containing links) some labels might be put
  592.  * one over the other which makes the overwritten ones unaccessible. In these
  593.  * cases the labels often can be separated if the relative positions of the
  594.  * nodes they belong to will be used. This switch accomplishes that task.
  595.  *
  596.  * We should not make this a standard behaviour, however, because it often will
  597.  * result in the browser trying to display the labels outside the current
  598.  * viewport.
  599.  *
  600.  * NOTE: This feature is meant to be used during labels navigation only. Thus
  601.  * bind it to navKeyBindings instead of plain keyBindings.
  602.  */
  603. function repositionLabels() {
  604.     var newPosMode = ! relPosLabels;
  605.  
  606.     hideOverlays();
  607.     removeOverlays();
  608.     relPosLabels = newPosMode;
  609.     createOverlays()
  610.     showOverlays( curLabelHead );
  611. }
  612. /*
  613.  * Maintaining Navigation Information
  614.  * ==================================
  615.  */
  616. /*
  617.  * Check visibility of an element.
  618.  * -------------------------------
  619.  * Recursively checks the given Element wether it is not hidden.
  620.  *
  621.  * @param   thisElement  node to be checked.
  622.  * @return  true if this Element is not hidden and not behind a hidden parent in
  623.  *          the DOM tree.
  624.  */
  625. function isVisible( thisElement ) {
  626.     if ( thisElement == document ) {
  627.         return true;
  628.     }
  629.     if ( ! thisElement ) {
  630.         return false;
  631.     }
  632.     if ( ! thisElement.parentNode ) {
  633.         return false;
  634.     }
  635.     if ( thisElement.style ) {
  636.         if ( thisElement.style.display == 'none' ) {
  637.             return false;
  638.         }
  639.         if ( thisElement.style.visibility == 'hidden' ) {
  640.             return false;
  641.         }
  642.     }
  643.     return isVisible( thisElement.parentNode );
  644. }
  645. /*
  646.  * Check if the element is displayable at all.
  647.  * -------------------------------------------
  648.  *
  649.  * @param   thisElement element to check
  650.  * @return  false if the element width or height are equal or below zero
  651.  */
  652. function isDisplayable( thisElement ) {
  653.     var width = thisElement.offsetWidth;
  654.     var height = thisElement.offsetHeight;
  655.  
  656.     if (width <= 0 || height <= 0)
  657.         return false;
  658.     else
  659.         return true;
  660. }
  661. /*
  662.  * Find Clickable Elements in the Document
  663.  * ---------------------------------------
  664.  * Scans the child nodes of the current ducument for those being clickable.
  665.  *
  666.  * @return  clickableNodes: array of clickable child nodes collected
  667.  *          labelDigits:    number of label digits required by this
  668.  *                          collateral sequence
  669.  */
  670. function findClickableNodes() {
  671.     /*
  672.      * Make sure to always start in a clear state.
  673.      */
  674.     clearLinkInfo();
  675.     clickableNodes = new Array();
  676.     /*
  677.      * Recursively scan the DOM-provided document links array.
  678.      */
  679.     function addClickableNodesIn( thisParent ) {
  680.         for (var i = 0; i < thisParent.childNodes.length; i++) {
  681.             var curNode = thisParent.childNodes[i];
  682.             /*
  683.              * Look at available and visible type 1 nodes only.
  684.              */
  685.             if (curNode.nodeType == 1 &&
  686.                 isDisplayable( curNode ) &&
  687.                 isVisible( curNode )) {
  688.                 /*
  689.                  * Check if this is a clickable element and
  690.                  * add it to the clickableNodes array if so.
  691.                  */
  692.                 var isClickable =
  693.                     curNode.nodeName.toLowerCase()=="input" |
  694.                     curNode.nodeName.toLowerCase()=="select" |
  695.                     curNode.nodeName.toLowerCase()=="textarea" |
  696.                     curNode.hasAttribute( "tabindex" ) |
  697.                     curNode.hasAttribute( "href" ) |
  698.                     curNode.hasAttribute( "onclick" );
  699.                 if (isClickable) {
  700.                     clickableNodes.push( curNode );
  701.                 }
  702.             }
  703.             /*
  704.              * Recursively check for clickable nodes in the childs of this one.
  705.              */
  706.             addClickableNodesIn( curNode );
  707.         }
  708.     }
  709.     /*
  710.      * Now start this scan at the document root.
  711.      */
  712.     addClickableNodesIn( document );
  713.     /*
  714.      * If wanted now find an optimal collateral sequence for labels display.
  715.      */
  716.     var curLength = clickableNodes.length;
  717.     /*
  718.      * Do so only if there are any clickable nodes at all.
  719.      */
  720.     if (curLength > 0) {
  721.         hasLinkNodes = true;
  722.     } else {
  723.         hasLinkNodes = false;
  724.         return;
  725.     }
  726.  
  727.     if (collSequence == "optimal") {
  728.         if (curLength < 10) {
  729.             // Labels need one number digit only.
  730.             useSequence = "0123456789";
  731.             useBase = 10;
  732.         }
  733.         else if (curLength < 50) {
  734.             // Labels displayable with one longalpha digit.
  735.             useSequence = "abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ";
  736.             useBase = 50;
  737.         }
  738.         else if (curLength > 99) {
  739.             // Labels would need more than three number digits.
  740.             // Note: This could as well be a lower + upper case sequence but
  741.             //       using lower case only appears to be more practical.
  742.             useSequence = "abcdefghijklmnopqrstuvxyz";
  743.             useBase = 25;
  744.         }
  745.         else {
  746.             // Labels displayable with two number digits.
  747.             useSequence = "0123456789";
  748.             useBase = 10;
  749.         }
  750.     }
  751.     /*
  752.      * Finally compute the number of digits the labels need to show.
  753.      */
  754.     while (curLength > 1) {
  755.         labelDigits++;
  756.         curLength /= useBase;
  757.     }
  758. }
  759. /*
  760.  * Create Labels Overlays
  761.  * ----------------------
  762.  * Requires the clickableNodes being evaluated already and no overlays being
  763.  * created yet.
  764.  *
  765.  * @return  labelsOverlays: array of label elements
  766.  *          nodeLabels:     array of label texts
  767.  */
  768. function createOverlays() {
  769.     var curElement;
  770.     var curLabel;
  771.     var curOverlay;
  772.     var curPosition;
  773.     /*
  774.      * Do nothing if there are no clickable nodes at all.
  775.      */
  776.     if (! hasLinkNodes)
  777.         return;
  778.     /*
  779.      * Scan the clickableNodes and construct a labels overlay for each.
  780.      */
  781.     labelsOverlays = new Array();
  782.     nodeLabels = new Array();
  783.  
  784.     for (var i = 0; i < clickableNodes.length; i++) {
  785.         curLabel = labelText( i );
  786.         curElement = clickableNodes[i];
  787.         curPosition = positionOf( curElement );
  788.         /*
  789.          * Create a hidden overlay for this element.
  790.          */
  791.         curOverlay    = document.createElement( "span" );
  792.         curOverlay.id = overlayId;
  793.         //
  794.         curOverlay.style.position   = "absolute";
  795.         if (relPosLabels) {
  796.             curOverlay.style.left   = curPosition[1] + "px";
  797.             curOverlay.style.top    = curPosition[0] + "px";
  798.         }
  799.         curOverlay.style.width      = "auto";
  800.         curOverlay.style.padding    = "1px";
  801.         curOverlay.style.background = nodeLabelBackground;
  802.         curOverlay.style.fontSize   = nodeLabelSize;
  803.         curOverlay.style.fontWeight = 'bold';
  804.         curOverlay.style.fontColor  = "black";
  805.         curOverlay.style.textTransform = "none";
  806.         //
  807.         curOverlay.style.zorder = 1000;    // always on top
  808.         curOverlay.style.opacity = nodeOpacity;
  809.         //
  810.         curOverlay.style.border     = "1px dashed darkgray";
  811.         curOverlay.style.fontColor  = "black";
  812.         //
  813.         curOverlay.style.visibility = "hidden";
  814.         // This will be displayed:
  815.         curOverlay.innerHTML =
  816.             "<font color=\"" +
  817.             nodeLabelColor + "\">" +
  818.             curLabel +
  819.             "</font>";
  820.         //
  821.         labelsOverlays.push( curOverlay );
  822.         nodeLabels.push( curLabel );
  823.         /*
  824.          * Insert this into the document as sibling of the current element.
  825.          */
  826. //        curElement.setAttribute( overlayId, curLabel );
  827.         curElement.parentNode.insertBefore( curOverlay, curElement );
  828.     }
  829. }
  830. /*
  831.  * Show Labels Overlays
  832.  * --------------------
  833.  * Shows overlays starting with labelHead, hides all others.
  834.  * If no direct match yet show the label tails only. Else show the complete
  835.  * label.
  836.  *
  837.  * @param   labelHead   initial character sequence of the labels to be shown
  838.  *                      where only the remaining tail will be displayed
  839.  *                      if "*":   show all labels without change
  840.  *                      if "":    reset and show all labels
  841.  *
  842.  */
  843. function showOverlays( labelHead ) {
  844.     var curLabel;
  845.     var curOverlay;
  846.     var headLength = labelHead.length;
  847.     /*
  848.      * Do nothing if there are no clickable nodes at all.
  849.      */
  850.     if (! hasLinkNodes)
  851.         return;
  852.     /*
  853.      * Scan the labels overlays array.
  854.      */
  855.     for (var i = 0; i < labelsOverlays.length; i++) {
  856.         labelsOverlays[i].style.visibility = "hidden";
  857.         curLabel = nodeLabels[i];
  858.  
  859.         if (labelHead == "") {
  860.             // Restore the label text to all digits and show the label.
  861.             labelsOverlays[i].innerHTML =
  862.                 "<font color=\"" +
  863.                 nodeLabelColor + "\">" +
  864.                 curLabel +
  865.                 "</font>";
  866.             labelsOverlays[i].style.visibility = "visible";
  867.         }
  868.         else if (labelHead == "*") {
  869.             if (matchingLabelNum >= 0)
  870.                 labelsOverlays[matchingLabelNum].style.visibility = "visible";
  871.             else
  872.                 labelsOverlays[i].style.visibility = "visible";
  873.         }
  874.         else {
  875.             if (curLabel.substring( 0, headLength) == labelHead) {
  876.                 if (headLength != labelDigits) {
  877.                     /*
  878.                      * This is a partial label.
  879.                      */
  880.                     if (shortenLabels) {
  881.                         // Show relevant digits only.
  882.                         labelsOverlays[i].innerHTML =
  883.                             "<font color=\"" +
  884.                             nodeLabelColor + "\">" +
  885.                             curLabel.substring( headLength, labelDigits ) +
  886.                             "</font>";
  887.                     } else {
  888.                         // Mark matching labels differently.
  889.                         labelsOverlays[i].innerHTML =
  890.                             "<font style=\"background: " +
  891.                             partialLabelBackground + "\" color=\"" +
  892.                             partialLabelColor + "\">" +
  893.                             curLabel +
  894.                             "</font>";
  895.                     }
  896.                 }
  897.                 else {
  898.                     // This is a full match, remember and show it.
  899.                     matchingLabelNum = i;
  900.                     labelsOverlays[i].innerHTML =
  901.                         "<font color=\"" +
  902.                         foundLabelColor +
  903.                         "\" style=\"background: " +
  904.                         foundLabelBackground + "\">" +
  905.                         curLabel +
  906.                         "</font>";
  907.                 }
  908.                 /*
  909.                  * Show this label
  910.                  */
  911.                 labelsOverlays[i].style.visibility = "visible";
  912.             }
  913.             /*
  914.              * Treat nonmatching labels here.
  915.              */
  916.             else {
  917.                 // Restore to full label representation.
  918.                 labelsOverlays[i].innerHTML =
  919.                     "<font color=\"" +
  920.                     nodeLabelColor + "\">" +
  921.                     curLabel +
  922.                     "</font>";
  923.  
  924.                 if (shortenLabels)
  925.                     labelsOverlays[i].style.visibility = "hidden";
  926.                 else
  927.                     labelsOverlays[i].style.visibility = "visible";
  928.             }
  929.         }
  930.     }
  931. }
  932. /*
  933.  * Hide overlays
  934.  * -------------
  935.  * Hides every label overlay.
  936.  */
  937. function hideOverlays() {
  938.     /*
  939.      * Do nothing if there are no clickable nodes at all.
  940.      */
  941.     if (! hasLinkNodes)
  942.         return;
  943.     /*
  944.      * Scan the labels overlays array and hide the nodes displays.
  945.      */
  946.     for (var i = 0; i < labelsOverlays.length; i++) {
  947.         labelsOverlays[i].style.visibility = "hidden";
  948.     }
  949. }
  950. /*
  951.  * Display overlays again
  952.  * ----------------------
  953.  */
  954. function redisplayOverlays() {
  955.     showOverlays( curLabelHead );
  956. }
  957. /*
  958.  * Remove label overlays
  959.  * ---------------------
  960.  * Removes all labels overlays.
  961.  *
  962.  * NOTE: This invalidates the overlays and should not be called out of context.
  963.  */
  964. function removeOverlays() {
  965.     /*
  966.      * Do nothing if there are no overlays at all.
  967.      */
  968.     if (! hasLinkNodes) {
  969.         return;
  970.     }
  971.     /*
  972.      * Track the labels overlays array and remove the node elements kept from
  973.      * their parents.
  974.      */
  975.     var curNode;
  976.     for (var i = 0; i < labelsOverlays.length; i++) {
  977.         curNode = labelsOverlays[i];
  978.         curNode.parentNode.removeChild(curNode);
  979.     }
  980. }
  981. /*
  982.  * Navigating
  983.  * ==========
  984.  */
  985. /*
  986.  * Check if the label is valid.
  987.  * ----------------------------
  988.  * Checks if a label starting with the given digits sequence is known in the
  989.  * nodeLabels array.
  990.  *
  991.  * @param   thisHead    head sequence of the label to check
  992.  * @return  true        there are labels starting with this sequence
  993.  *                      curLabelNum: number of first occurence found
  994.  *          false       there are no such labels known
  995.  *                      curLabelNum: -1
  996.  */
  997. function isValidLabel( thisHead ) {
  998.     if (thisHead == "") {
  999.         curLabelNum = -1;
  1000.         return false;
  1001.     }
  1002.     var headLength = thisHead.length;
  1003.  
  1004.     for( var i = 0; i < nodeLabels.length; i++ ) {
  1005.         if (thisHead == nodeLabels[i].substring( 0, headLength )) {
  1006.             curLabelNum = i;
  1007.             return true;
  1008.         }
  1009.     }
  1010.     curLabelNum = -1;
  1011.     return false;
  1012. }
  1013. /*
  1014.  * Navigate by label
  1015.  * -----------------
  1016.  * Process the current key to find an according link label and perform the
  1017.  * proper action there.
  1018.  */
  1019. function navigateByLabel() {
  1020.     if (waitForConfirmation) {
  1021.         /*
  1022.          * We found a match but the user must tell what to do with it.
  1023.          */
  1024.         if (keyLabel == openInThisTab) {
  1025.             openNewTab = false;
  1026.             waitForConfirmation = false;
  1027.             clickLabel( matchingLabelNum );
  1028.         }
  1029.         else if (keyLabel == openInNewTab) {
  1030.             openNewTab = true;
  1031.             waitForConfirmation = false;
  1032.             clickLabel( matchingLabelNum );
  1033.         }
  1034.         else if (keyCode == 0x08) { // backspace
  1035.             waitForConfirmation = false;
  1036.             matchingLabelNum = -1;  // removes the match in any case
  1037.             /*
  1038.              * Remove trailing character from the selection.
  1039.              */
  1040.             curLabelHead =
  1041.                 curLabelHead.substring( 0,
  1042.                     curLabelHead.length - 1 );
  1043.             showOverlays( curLabelHead );
  1044.         }
  1045.     }
  1046.     /*
  1047.      * We assume only numbers or ASCII characters will be used in labels.
  1048.      */
  1049.     else if (hasValidNavKey()) {
  1050.         if (isValidLabel( curLabelHead + keyLabel )) {
  1051.             /*
  1052.              * The key belongs to a valid label. Show the resulting
  1053.              * selection.
  1054.              */
  1055.             curLabelHead = curLabelHead + keyLabel;
  1056.             showOverlays( curLabelHead );
  1057.  
  1058.             if (matchingLabelNum != -1) {
  1059.                 /*
  1060.                  * We found a match.
  1061.                  */
  1062.                 if (autoselectLink) {
  1063.                     waitForConfirmation = false;
  1064.                     clickLabel( matchingLabelNum );
  1065.                 } else {
  1066.                     waitForConfirmation = true;
  1067.                     //clickLabel( matchingLabelNum );
  1068.                 }
  1069.                 keyAction = null;
  1070.                 return;
  1071.             }
  1072.         } else {
  1073.             /*
  1074.              * Simply skip the invalid entry.
  1075.              */
  1076.             keyAction = null;
  1077.             return;
  1078.         }
  1079.     }
  1080.     /*
  1081.      * If neither character or number, some other action, e.g. simple extra
  1082.      * navigation to properly show the labels in the viewport, may be
  1083.      * wanted.
  1084.      */
  1085.     else {
  1086.         if (keyCode == 0x08) { // backspace
  1087.             /*
  1088.              * Remove trailing character from the selection.
  1089.              */
  1090.             if (curLabelHead != "") {
  1091.                 matchingLabelNum = -1;  // removes the match in any case
  1092.                 curLabelHead =
  1093.                     curLabelHead.substring( 0,
  1094.                         curLabelHead.length - 1 );
  1095.                 waitForConfirmation = false;
  1096.                 showOverlays( curLabelHead );
  1097.             }
  1098.         }
  1099.         else {
  1100.             /*
  1101.              * Look up if there is some special action to perform.
  1102.              */
  1103.             keyAction = navKeyBindings[keyLabel];
  1104.         }
  1105.     }
  1106. }
  1107. /*
  1108.  * Simulate a Mouseclick
  1109.  * ---------------------
  1110.  *
  1111.  *  @param  labelPos    number of the label to be clicked on
  1112.  */
  1113. function clickLabel( labelPos ) {
  1114.     var curElement = clickableNodes[ labelPos ];
  1115.     var curLabel = nodeLabels[ labelPos ];
  1116.     var curName = curElement.nodeName.toLowerCase();
  1117.  
  1118.     stopNavigating();
  1119.  
  1120.     if (curName == "a") {
  1121.         /*
  1122.          * It is a link. Just go there.
  1123.          */
  1124.         if (openNewTab) {
  1125.             openNewTab = false;
  1126.             window.open( curElement.getAttribute( "href" ), "", "" );
  1127.         } else {
  1128.             window.location = curElement.getAttribute("href");
  1129.         }
  1130.     }
  1131.     else if (curElement.hasAttribute("onclick")) {
  1132.         /*
  1133.          * This requires some more effort in order to trigger the attached
  1134.          * actions.
  1135.          * At first we need a special event to track mouse clicks.
  1136.          */
  1137.         var thisEvent = document.createEvent("MouseEvents");
  1138.         /*
  1139.          * Then the mouse click action needs to be defined.
  1140.          */
  1141.         thisEvent.initMouseEvent(
  1142.             "click",        // the event type
  1143.             true, true,     // allow bubbles and default action cancels
  1144.             window,         // this view's base
  1145.             0,              // mouse click count
  1146.             0, 0, 0, 0,     // screen and client coordinates
  1147.             false, false,   // no control or alt key depressed simultaneously
  1148.             false, false,   // ditto, shift or meta key
  1149.             0,              // mouse button
  1150.             null);          // no other related target
  1151.         /*
  1152.          * Finally get this known to the system.
  1153.          */
  1154.         curElement.dispatchEvent(thisEvent);
  1155.     }
  1156.     else if (curName == "input") {
  1157.         /*
  1158.          * There are several types of input elements which need be handled
  1159.          * differently.
  1160.          */
  1161.         var curType = curElement.getAttribute('type').toLowerCase();
  1162.  
  1163.         if (curType == 'text' || curType == 'file' || curType == 'password') {
  1164.             /*
  1165.              * These need be explicitely selected.
  1166.              */
  1167.             curElement.focus();
  1168.             curElement.select();
  1169.             curElement.click();
  1170.         } else {
  1171.             /*
  1172.              * It is a genuine input element.
  1173.              * This allows us to use the click() method.
  1174.              */
  1175.             curElement.click();
  1176.         }
  1177.     }
  1178.     else if (curName == 'textarea' || curName == 'select') {
  1179.         /*
  1180.          * Handle these like the special input element types.
  1181.          */
  1182.         curElement.focus();
  1183.         curElement.select();
  1184.     }
  1185.     else if (curElement.hasAttribute( "href" )) {
  1186.         /*
  1187.          * Handle a possible not detected link.
  1188.          */
  1189.         if (openNewTab) {
  1190.             openNewTab = false;
  1191.             window.open( curElement.getAttribute( "href" ), "", "" );
  1192.         } else {
  1193.             window.location = curElement.getAttribute("href");
  1194.         }
  1195.     }
  1196.     else {
  1197.         alert ("Could not click element " + curLabel +
  1198.                ": " + curName +
  1199.                "\nNo idea what to do with it." );
  1200.     }
  1201. }
  1202. /*
  1203.  * Start Navigating
  1204.  * ----------------
  1205.  */
  1206.  
  1207. // If autoselecting, open in new tab.
  1208. function startNavNewTab() {
  1209.     openNewTab = true;
  1210.     startNavigating();
  1211. }
  1212.  
  1213. // If autoselecting, open in current tab.
  1214. function startNavigating() {
  1215.     navigating = true;
  1216.     waitForConfirmation = false;
  1217.  
  1218.     if (hasLinkNodes) {
  1219.         clearLinkInfo();
  1220.     }
  1221.     findClickableNodes();
  1222.     if (hasLinkNodes) {
  1223.         createOverlays();
  1224.         showOverlays("");
  1225.     } else {
  1226.         navigating = false;
  1227.         alert( "No clickable node found on this page." );
  1228.     }
  1229. }
  1230. /*
  1231.  * Stop Navigating
  1232.  * ---------------
  1233.  */
  1234. function stopNavigating() {
  1235.     hideOverlays();
  1236.     navigating = false;
  1237.     clearLinkInfo();
  1238. }
  1239.  
  1240. /**************************
  1241.  *** Keyboard Interface ***
  1242.  **************************/
  1243.  
  1244. /*
  1245.  * Check for an editable element
  1246.  * -----------------------------
  1247.  * @param element   DOM element to be checked
  1248.  * @return          true if element is editable
  1249.  *                  (i.e. shall receive all keys)
  1250.  *                  false otherwise.
  1251.  */
  1252. function isEditable( element ) {
  1253.     if ( element.nodeName.toLowerCase() == "textarea" )
  1254.         return true;
  1255.     if ( element.nodeName.toLowerCase() == "input" &&
  1256.          ( element.type == "text" || element.type == "password" ) )
  1257.         return true;
  1258.     if ( document.designMode == "on" || element.contentEditable == "true" )
  1259.         return true;
  1260.     return false;
  1261. }
  1262. /*
  1263.  * The Keyboard Event Handlers
  1264.  * ===========================
  1265.  */
  1266. /*
  1267.  * Evaluate the key in the given keyEvent
  1268.  * --------------------------------------
  1269.  * @param   keyEvent    event to evalute
  1270.  * @return  function pointer in the keyAction variable.
  1271.  */
  1272. function evalKey( keyEvent ) {
  1273.     wndHeight = window.innerHeight - Math.max( window.innerHeight / 10, 2 );
  1274.  
  1275.     // Handle specific key codes.
  1276.     // NOTE: Might be device specific, not thoroughly tested.
  1277.     keyCode = keyEvent.keyCode;
  1278.     // Account for keypad (NumLock on).
  1279.     if ( keyCode >= 96 && keyCode <= 105 )
  1280.         // numbers
  1281.         keyCode -= 48;
  1282.     else if ( keyCode >= 106 && keyCode <= 110 )
  1283.         // operators and punctuation
  1284.         keyCode -= 64;
  1285.  
  1286.     // Convert to character representation.
  1287.     keyLabel = String.fromCharCode( keyCode );
  1288.     /*
  1289.      * We must explicitely process the shift key because the the keyCode
  1290.      * conversion will always return upper case.
  1291.      */
  1292.     if ( ! keyEvent.shiftKey && keyCode >= 32 )
  1293.         keyLabel = String.fromCharCode( keyCode ).toLowerCase();
  1294.     if ( keyEvent.ctrlKey )
  1295.         keyLabel = "^" + keyLabel;
  1296.     /*
  1297.      * Evaluate the action to be performed.
  1298.      */
  1299.     if (! navigating) {
  1300.         keyAction = keyBindings[keyLabel];
  1301.     }
  1302.     else {
  1303.         keyAction = navKeyBindings[keyLabel];
  1304.         /*
  1305.          * Any invalid input stops navigating.
  1306.          */
  1307. //        if (! keyAction && ! hasValidNavKey() ) {
  1308. //            stopNavigating();
  1309. //        }
  1310.     }
  1311. }
  1312. /*
  1313.  * Check for a valid navigation key
  1314.  * --------------------------------
  1315.  * Checks the current keyLabel if it is a number or an ASCII character.
  1316.  *
  1317.  * @return  true if this is a valid navigation key.
  1318.  */
  1319. function hasValidNavKey() {
  1320.     if (keyLabel >= "0" && keyLabel <= "9" ||
  1321.         keyLabel >= "a" && keyLabel <= "z" ||
  1322.         keyLabel >= "A" && keyLabel <= "Z") {
  1323.         return true;
  1324.     } else {
  1325.         return false;
  1326.     }
  1327. }
  1328. /*
  1329.  * Handle most keypresses here
  1330.  * ---------------------------
  1331.  * @param   keyEvent    event to evalute
  1332.  */
  1333. function keyHandler( keyEvent ) {
  1334.     keyAction = null;
  1335.  
  1336.     if (navigating) {
  1337.         evalKey( keyEvent );
  1338.         navigateByLabel();
  1339.     }
  1340.     else {
  1341.         /*
  1342.          *  Skip targets which need keypresses by their own.
  1343.          */
  1344.         if (isEditable( keyEvent.target )) {
  1345.             return;
  1346.         }
  1347.         /*
  1348.          * The Esc key has been handled on key down already so skip it here.
  1349.          */
  1350.         if (keyEvent.keyCode == 0x1b) {
  1351.             return;
  1352.         }
  1353.         /*
  1354.          *  Evaluate the function the key is bound to and execute it.
  1355.          */
  1356.         evalKey( keyEvent );
  1357.     }
  1358.     /*
  1359.      * Now perform the pending command.
  1360.      */
  1361.     if (keyAction) {
  1362.         // Prevent double execution of repeatable commands.
  1363.         if ( keyAction != goLeft  &&
  1364.             keyAction != goRight &&
  1365.             keyAction != goUp    &&
  1366.             keyAction != goDown ) {
  1367.             keyAction();
  1368.         }
  1369.     }
  1370. }
  1371. /*
  1372.  * Handle repeatable keys
  1373.  * ----------------------
  1374.  * There are a few commands which may be repeatably executed.
  1375.  * They need be handled separately as auto-repeat works on downheld keys only.
  1376.  *
  1377.  * @param   keyEvent    event to evalute
  1378.  *
  1379.  * NOTE:
  1380.  * There is some (possible Midori related) bug where javascript appears to send
  1381.  * keys twice if NumLock is on. Hence we restrict this to small movements only.
  1382.  */
  1383. function keyRepeatHandler( keyEvent ) {
  1384.     keyAction = null;
  1385.     /*
  1386.      * There is only one keydown action to perform if we are navigating
  1387.      * by labels.
  1388.      */
  1389.     if (navigating) {
  1390.         /*
  1391.          * The Esc key is a stop all feature.
  1392.          * Hence check for this one first.
  1393.          */
  1394.         if (keyEvent.keyCode == 0x1b) {
  1395.             stopNavigating();
  1396.             return;
  1397.         }
  1398.     }
  1399.     /*
  1400.      * Otherwise handle some special cases.
  1401.      */
  1402.     else {
  1403.         // Skip targets which need keypresses by their own.
  1404.         if (isEditable( keyEvent.target )) {
  1405.             return;
  1406.         }
  1407.     }
  1408.     /*
  1409.      * Evaluate the function the key is bound to and execute it.
  1410.      */
  1411.     evalKey( keyEvent );
  1412.     if (keyAction) {
  1413.         // Execute repeatable commands only.
  1414.         if (keyAction == goLeft  ||
  1415.             keyAction == goRight ||
  1416.             keyAction == goUp    ||
  1417.             keyAction == goDown ) {
  1418.             keyAction();
  1419.         }
  1420.     }
  1421. }
  1422.  
  1423. /*******************
  1424.  *** Script Body ***
  1425.  *******************/
  1426. /*
  1427.  * Initialization
  1428.  * ==============
  1429.  */
  1430. /*
  1431.  * Make sure to start in a clean state.
  1432.  */
  1433. clearLinkInfo();
  1434. wndHeightAdjust = 1;
  1435. /*
  1436.  * Set up a static labels collation sequence according to the configuration.
  1437.  */
  1438. if (collSequence == "numeric") {
  1439.     useSequence = "0123456789";
  1440.     useBase = 10;
  1441. }
  1442. else if (collSequence == "alpha") {
  1443.     useSequence = "abcdefghijklmnopqrstuvxyz";
  1444.     useBase = 25;
  1445. }
  1446. else if (collSequence == "longalpha") {
  1447.     // We use lower key characters first, then upper key ones
  1448.     // to ease the typing.
  1449.     useSequence = "abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ";
  1450.     useBase = 50;
  1451. }
  1452. else if (collSequence != "optimal") {
  1453.     useSequence = collSequence;
  1454.     useBase = collSequence.length;
  1455. }
  1456. /*
  1457.  * Register keyboard event handlers.
  1458.  */
  1459. window.addEventListener( "keyup", keyHandler, false );
  1460. window.addEventListener( "keydown", keyRepeatHandler, false );
  1461.  
  1462. // ----------------------------------------------------------------------------
  1463. // vim:shiftwidth=4:softtabstop=4:expandtab:textwidth=80