JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
ea6eff51f794528f9cff4f441769bbcf4fcc494a
[userscripts.git] / numbered_links.user.js
1 /*
2 *   This script is derived (by Jason Woofenden) from an example script from
3 *   uzbl (see uzbl.org) and is thus presumably licensed under the GNU GPLv3. In
4 *   any case, Jason's changes are in the public domain.
5 */
6
7 // ==UserScript==
8 // @name          numbered links
9 // @namespace     http://patcavit.com/greasemonkey
10 // @description   make all links followable with the keyboard
11 // @include       *
12 // ==/UserScript==
13
14 (function()
15 {
16
17 var active = 0;
18 var got = '';
19
20 // This is activated (and canceled) by pressing the ^C
21 // Press L (first) to switch to one-hand mode
22
23 // I was getting some very funky return values from String.fromCharCode() for
24 // punctuation keys so I made my own table. You might need to change this for
25 // your computer/keyboard. You'll need to have every character in "charset" in
26 // here.
27
28 var key_to_char = {
29          '84': 't',
30          '72': 'h',
31          '83': 's',
32          '78': 'n',
33          '68': 'd',
34         '189': '-',
35          '82': 'r',
36          '67': 'c',
37          '71': 'g',
38          '77': 'm',
39          '86': 'v',
40          '87': 'w',
41          '66': 'b',
42         '191': '/',
43         '186': ';',
44          '55': '7',
45          '56': '8',
46          '57': '9',
47          '65': 'a',
48          '69': 'e',
49          '70': 'f',
50          '73': 'i',
51          '74': 'j',
52          '75': 'k',
53          '79': 'o',
54          '80': 'p',
55          '81': 'q',
56          '85': 'u',
57          '88': 'x',
58          '89': 'y',
59          '90': 'z',
60          '50': '2',
61          '51': '3',
62          '52': '4',
63         '222': "'",
64          '76': 'l' // switch to one-hand mode
65 };
66
67 //Just some shortcuts and globals
68 var charset = 'thsnd-rcgmvwb/;789aefijkopquxyz234'; // update key_to_char if you add to this
69 var uzblid = 'uzbl_link_hint';
70 var uzbldivid = uzblid + '_div_container';
71 var doc = document;
72 var win = window;
73 var links = document.links;
74 var forms = document.forms;
75
76 //Calculate element position to draw the hint
77 //Pretty accurate but on fails in some very fancy cases
78 function element_position(el) {
79         var x = el.offsetLeft;
80         var y = el.offsetTop;
81         var width = el.offsetWidth;
82         var height = el.offsetHeight;
83         while (el.offsetParent) {
84                 el = el.offsetParent;
85                 y += el.offsetTop;
86                 x += el.offsetLeft;
87         }
88         return { x: x, y: y, width: width, height: height };
89 }
90
91 // Simulate a click on the element
92 function click_element(target, options) {
93         var event = target.ownerDocument.createEvent('MouseEvents');
94         var pos = element_position(target);
95         options = options || {};
96
97
98         event.initMouseEvent(
99                 options.type            || 'click',
100                 options.canBubble       || true,
101                 options.cancelable      || true,
102                 options.view            || target.ownerDocument.defaultView,
103                 options.detail          || 1,
104                 options.screenX         || pos.x - window.pageXOffset,
105                 options.screenY         || pos.y - window.pageYOffset,
106                 options.clientX         || pos.x,
107                 options.clientY         || pos.y,
108                 options.ctrlKey         || false,
109                 options.altKey          || false,
110                 options.shiftKey        || false,
111                 options.metaKey         || false,
112                 options.button          || 0, //0 = left, 1 = middle, 2 = right
113                 options.relatedTarget   || null
114         );
115
116         target.dispatchEvent(event);
117 }
118
119 //Calculate if an element is visible
120 function isVisible(el) {
121         if (el == doc) {
122                 return true;
123         }
124         if (!el) {
125                 return false;
126         }
127         if (!el.parentNode) {
128                 return false;
129         }
130         if (el.style) {
131                 if (el.style.display == 'none') {
132                         return false;
133                 }
134                 if (el.style.visibility == 'hidden') {
135                         return false;
136                 }
137         }
138         return isVisible(el.parentNode);
139 }
140 //Calculate if an element is on the viewport.
141 function elementInViewport(el) {
142         var pos = element_position(el);
143         return pos.y < window.pageYOffset + window.innerHeight && pos.x < window.pageXOffset + window.innerWidth && (pos.y + pos.height) > window.pageYOffset && (pos.x + pos.width) > window.pageXOffset;
144 }
145 //Removes all hints/leftovers that might be generated
146 //by this script.
147 function removeAllHints() {
148         var elements = doc.getElementById(uzbldivid);
149         if (elements) {
150                 elements.parentNode.removeChild(elements);
151         }
152 }
153 //Generate a hint for an element with the given label
154 //Here you can play around with the style of the hints!
155 function generateHint(el, label) {
156         var pos = element_position(el);
157         var hint = doc.createElement('div');
158         hint.setAttribute('name', uzblid);
159         hint.innerText = label;
160         hint.style.display = 'inline';
161         hint.style.backgroundColor = '#B9FF00';
162         hint.style.border = '2px solid #4A6600';
163         hint.style.color = 'black';
164         hint.style.fontSize = '9px';
165         hint.style.fontWeight = 'bold';
166         hint.style.lineHeight = '9px';
167         hint.style.margin = '0px';
168         hint.style.width = 'auto'; // fix broken rendering on w3schools.com
169         hint.style.padding = '1px';
170         hint.style.position = 'absolute';
171         hint.style.zIndex = '1000';
172         hint.style.textTransform = 'uppercase';
173         hint.style.left = Math.max(-1, (pos.x - (7 + label.length * 9))) + 'px';
174         hint.style.top = (pos.y + 1) + 'px';
175         var img = el.getElementsByTagName('img');
176         //if (img.length > 0) {
177                 //hint.style.top = pos.x + img[0].height / 2 - 6 + 'px';
178         //}
179         hint.style.textDecoration = 'none';
180         // hint.style.webkitBorderRadius = '6px'; // slow
181         return hint;
182 }
183
184 // This function takes action on the element chosen by entering the "numbered
185 // link" shortcut key(s)
186 function do_element(item) {
187         removeAllHints();
188         if (item) {
189                 var name = item.tagName;
190                 if (name == 'A') {
191                         click_element(item);
192                         window.location = item.href;
193                 } else if (name == 'INPUT') {
194                         var type = (item.getAttribute('type') || 'text').toLowerCase();
195                         switch (type) {
196                                 case 'checkbox':
197                                         item.checked = !item.checked;
198                                 break;
199                                 case 'radio':
200                                         item.checked = true;
201                                 break;
202                                 case 'submit':
203                                 case 'reset':
204                                 case 'image':
205                                 case 'button':
206                                         item.click(); // only tested on submit buttons
207                                 break;
208                                 case 'file': // don't think js can activate this, so focus
209                                 case 'text':
210                                 case 'password':
211                                 case 'search':
212                                 case 'number':
213                                 case 'email':
214                                 case 'url':
215                                 case 'range':
216                                 default:
217                                         // give it keyboard focus
218                                         item.focus();
219                                         item.select();
220                                 break;
221                         }
222                 } else if (name == 'TEXTAREA' || name == 'SELECT') {
223                         item.focus();
224                         item.select();
225                 } else {
226                         // as a catch-all, try simulating a mouse click on that element
227                         click_element(item);
228                         window.location = item.href;
229                 }
230         }
231 }
232
233 //Returns a list of all links (in this version
234 //just the elements itself, but in other versions, we
235 //add the label here.
236 function addLinks() {
237         var res = [[], []];
238         for (var l = 0; l < links.length; l++) {
239                 var li = links[l];
240                 if (isVisible(li) && elementInViewport(li)) {
241                         res[0].push(li);
242                 }
243         }
244         return res;
245 }
246 //Same as above, just for the form elements
247 function addFormElems() {
248         var res = [[], []];
249         for (var f = 0; f < forms.length; f++) {
250                 for (var e = 0; e < forms[f].elements.length; e++) {
251                         var el = forms[f].elements[e];
252                         if (el && ['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) + 1 && isVisible(el) && elementInViewport(el)) {
253                                 res[0].push(el);
254                         }
255                 }
256         }
257         return res;
258 }
259 //Draw all hints for all elements passed. "len" is for
260 //the number of chars we should use to avoid collisions
261 function reDrawHints(elems, chars) {
262         removeAllHints();
263         var hintdiv = doc.createElement('div');
264         hintdiv.setAttribute('id', uzbldivid);
265         for (var i = 0; i < elems[0].length; i++) {
266                 if (elems[0][i]) {
267                         var label = elems[1][i].substring(chars);
268                         var h = generateHint(elems[0][i], label);
269                         hintdiv.appendChild(h);
270                 }
271         }
272         if (document.body) {
273                 document.body.appendChild(hintdiv);
274         }
275 }
276 // pass: number of keys
277 // returns: key length
278 function labelLength(n) {
279         var oldn = n;
280         var keylen = 0;
281         if(n < 2) {
282                 return 1;
283         }
284         n -= 1; // our highest key will be n-1
285         while(n) {
286                 keylen += 1;
287                 n = Math.floor(n / charset.length);
288         }
289         return keylen;
290 }
291 // pass: number
292 // returns: label
293 function intToLabel(n) {
294         var label = '';
295         do {
296                 label = charset.charAt(n % charset.length) + label;
297                 n = Math.floor(n / charset.length);
298         } while(n);
299         return label;
300 }
301 // pass: label
302 // returns: number
303 function labelToInt(label) {
304         var n = 0;
305         var i;
306         for(i = 0; i < label.length; ++i) {
307                 n *= charset.length;
308                 n += charset.indexOf(label[i]);
309         }
310         return n;
311 }
312 //Put it all together
313 function followLinks(follow) {
314         if(follow.charAt(0) == 'l') {
315                 follow = follow.substr(1);
316                 charset = 'thsnlrcgfdbmwvz-/';
317         }
318         var s = follow.split('');
319         var linknr = labelToInt(follow);
320         var linkelems = addLinks();
321         var formelems = addFormElems();
322         var elems = [linkelems[0].concat(formelems[0]), linkelems[1].concat(formelems[1])];
323         var len = labelLength(elems[0].length);
324         var oldDiv = doc.getElementById(uzbldivid);
325         var leftover = [[], []];
326         if (s.length == len && linknr < elems[0].length && linknr >= 0) {
327                 do_element(elems[0][linknr]);
328                 got = '';
329                 active = 0;
330         } else {
331                 for (var j = 0; j < elems[0].length; j++) {
332                         var b = true;
333                         var label = intToLabel(j);
334                         var n = label.length;
335                         for (n; n < len; n++) {
336                                 label = charset.charAt(0) + label;
337                         }
338                         for (var k = 0; k < s.length; k++) {
339                                 b = b && label.charAt(k) == s[k];
340                         }
341                         if (b) {
342                                 leftover[0].push(elems[0][j]);
343                                 leftover[1].push(label);
344                         }
345                 }
346                 reDrawHints(leftover, s.length);
347         }
348 }
349
350 // from your event handler you can: return stop_event(e)
351 function stop_event(e) {
352         // try {
353                 e.stopPropagation();
354                 e.preventDefault();
355         // } catch (ex) {
356         //      return false; // IE-compat
357         // }
358 }
359
360 function init() {
361         document.addEventListener(
362                 'keydown',
363                 function(e) {
364                         // [de]activate on ^C
365                         // deactivate on ESC
366                         if(
367                                 (e.ctrlKey && e.keyCode == 67)
368                                 || (e.keyCode == 27 && active == 1)
369                         ) {
370                                 if(active) {
371                                         got = '';
372                                         removeAllHints();
373                                 } else {
374                                         followLinks(got);
375                                 }
376                                 active = 1 - active;
377                                 return stop_event(e);
378                         } else {
379                                 if(active == 1 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
380                                         if(key_to_char[e.keyCode]) {
381                                                 got += key_to_char[e.keyCode];
382                                                 followLinks(got);
383                                                 return stop_event(e);
384                                         }
385                                 } else {
386                                         // general keybinding, unrelated to numbered links
387                                         var active_type = (document.activeElement.type || 'a').toLowerCase();
388                                         var typing = true;
389                                         if (document.activeElement == document.body) {
390                                                 typing = false;
391                                         } else {
392                                                 switch ((document.activeElement.type || 'a').toLowerCase()) {
393                                                         case 'checkbox':
394                                                         case 'radio':
395                                                         case 'submit':
396                                                         case 'reset':
397                                                         case 'image':
398                                                         case 'button':
399                                                                 typing = false;
400                                                 }
401                                         }
402                                         if (!typing) {
403                                                 var c = key_to_char[e.keyCode];
404                                                 switch (c) {
405                                                         case 'c':
406                                                                 window.scrollBy(0, -200);
407                                                                 stop_event(e);
408                                                         break;
409                                                         case 't':
410                                                                 window.scrollBy(0, 200);
411                                                                 stop_event(e);
412                                                         break;
413                                                         case 'h':
414                                                                 if (e.shiftKey) {
415                                                                         history.back()
416                                                                         stop_event(e);
417                                                                 }
418                                                         break;
419                                                         case 'n':
420                                                                 if (e.shiftKey) {
421                                                                         history.forward()
422                                                                         stop_event(e);
423                                                                 }
424                                                         break;
425                                                         case 'f':
426                                                                 if (!active && !e.ctrlKey) {
427                                                                         followLinks(got);
428                                                                         active = 1;
429                                                                         stop_event(e);
430                                                                 }
431                                                         break;
432                                                 }
433                                         } else if (e.keyCode == 27) {
434                                                 // unfocus on ESC
435                                                 document.activeElement.blur();
436                                                 stop_event(e);
437                                         }
438                                 }
439                         }
440                 },
441                 true);
442 }
443
444 init();
445
446 })();