View difference between Paste ID: QdGsLgg9 and
SHOW: | | - or go back to the newest paste.
1-
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