Advertisement
Guest User

Midori VimNav keyboard navigation

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