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 |