JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
youtube: refactor, handle embeds
[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       http://*
12 // ==/UserScript==
13
14 (function() 
15 {
16
17
18 // This is activated (and canceled) by pressing the ^C
19 // Press L (first) to switch to one-hand mode
20
21 // I was getting some very funky return values from String.fromCharCode() for
22 // punctuation keys so I made my own table. You might need to change this for
23 // your computer/keyboard. You'll need to have every character in "charset" in
24 // here.
25
26 var key_to_char = {
27          '84': 't',
28          '72': 'h',
29          '83': 's',
30          '78': 'n',
31          '68': 'd',
32         '189': '-',
33          '82': 'r',
34          '67': 'c',
35          '71': 'g',
36          '77': 'm',
37          '86': 'v',
38          '87': 'w',
39          '66': 'b',
40         '191': '/',
41         '186': ';',
42          '55': '7',
43          '56': '8',
44          '57': '9',
45          '65': 'a',
46          '69': 'e',
47          '70': 'f',
48          '73': 'i',
49          '74': 'j',
50          '75': 'k',
51          '79': 'o',
52          '80': 'p',
53          '81': 'q',
54          '85': 'u',
55          '88': 'x',
56          '89': 'y',
57          '90': 'z',
58          '50': '2',
59          '51': '3',
60          '52': '4',
61         '222': "'",
62          '76': 'l' // switch to one-hand mode
63 }
64
65 //Just some shortcuts and globals
66 var charset = 'thsnd-rcgmvwb/;789aefijkopquxyz234'; // update key_to_char if you add to this
67 var uzblid = 'uzbl_link_hint';
68 var uzbldivid = uzblid + '_div_container';
69 var doc = document;
70 var win = window;
71 var links = document.links;
72 var forms = document.forms;
73 //Make onlick-links "clickable"
74 try {
75     HTMLElement.prototype.click = function() {
76         if (typeof this.onclick == 'function') {
77             this.onclick({
78                 type: 'click'
79             });
80         }
81     };
82 } catch(e) {}
83 //Calculate element position to draw the hint
84 //Pretty accurate but on fails in some very fancy cases
85 function elementPosition(el) {
86     var up = el.offsetTop;
87     var left = el.offsetLeft;
88     var width = el.offsetWidth;
89     var height = el.offsetHeight;
90     while (el.offsetParent) {
91         el = el.offsetParent;
92         up += el.offsetTop;
93         left += el.offsetLeft;
94     }
95     return [up, left, width, height];
96 }
97 //Calculate if an element is visible
98 function isVisible(el) {
99     if (el == doc) {
100         return true;
101     }
102     if (!el) {
103         return false;
104     }
105     if (!el.parentNode) {
106         return false;
107     }
108     if (el.style) {
109         if (el.style.display == 'none') {
110             return false;
111         }
112         if (el.style.visibility == 'hidden') {
113             return false;
114         }
115     }
116     return isVisible(el.parentNode);
117 }
118 //Calculate if an element is on the viewport.
119 function elementInViewport(el) {
120     offset = elementPosition(el);
121     var up = offset[0];
122     var left = offset[1];
123     var width = offset[2];
124     var height = offset[3];
125     return up < window.pageYOffset + window.innerHeight && left < window.pageXOffset + window.innerWidth && (up + height) > window.pageYOffset && (left + width) > window.pageXOffset;
126 }
127 //Removes all hints/leftovers that might be generated
128 //by this script.
129 function removeAllHints() {
130     var elements = doc.getElementById(uzbldivid);
131     if (elements) {
132         elements.parentNode.removeChild(elements);
133     }
134 }
135 //Generate a hint for an element with the given label
136 //Here you can play around with the style of the hints!
137 function generateHint(el, label) {
138     var pos = elementPosition(el);
139     var hint = doc.createElement('div');
140     hint.setAttribute('name', uzblid);
141     hint.innerText = label;
142     hint.style.display = 'inline';
143     hint.style.backgroundColor = '#B9FF00';
144     hint.style.border = '2px solid #4A6600';
145     hint.style.color = 'black';
146     hint.style.fontSize = '9px';
147     hint.style.fontWeight = 'bold';
148     hint.style.lineHeight = '9px';
149     hint.style.margin = '0px';
150     hint.style.width = 'auto'; // fix broken rendering on w3schools.com
151     hint.style.padding = '1px';
152     hint.style.position = 'absolute';
153     hint.style.zIndex = '1000';
154     hint.style.textTransform = 'uppercase';
155     hint.style.left = Math.max(-1, (pos[1] - (7 + label.length * 9))) + 'px';
156     hint.style.top = (pos[0] + 1) + 'px';
157     var img = el.getElementsByTagName('img');
158     //if (img.length > 0) {
159         //hint.style.top = pos[1] + img[0].height / 2 - 6 + 'px';
160     //}
161     hint.style.textDecoration = 'none';
162     // hint.style.webkitBorderRadius = '6px'; // slow
163     return hint;
164 }
165 //Here we choose what to do with an element if we
166 //want to "follow" it. On form elements we "select"
167 //or pass the focus, on links we try to perform a click,
168 //but at least set the href of the link. (needs some improvements)
169 function clickElem(item) {
170     removeAllHints();
171     if (item) {
172         var name = item.tagName;
173         if (name == 'A') {
174             item.click();
175             window.location = item.href;
176         } else if (name == 'INPUT') {
177             var type = item.getAttribute('type').toUpperCase();
178             if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
179                 item.focus();
180                 item.select();
181             } else {
182                 item.click();
183             }
184         } else if (name == 'TEXTAREA' || name == 'SELECT') {
185             item.focus();
186             item.select();
187         } else {
188             item.click();
189             window.location = item.href;
190         }
191     }
192 }
193 //Returns a list of all links (in this version
194 //just the elements itself, but in other versions, we
195 //add the label here.
196 function addLinks() {
197     res = [[], []];
198     for (var l = 0; l < links.length; l++) {
199         var li = links[l];
200         if (isVisible(li) && elementInViewport(li)) {
201             res[0].push(li);
202         }
203     }
204     return res;
205 }
206 //Same as above, just for the form elements
207 function addFormElems() {
208     res = [[], []];
209     for (var f = 0; f < forms.length; f++) {
210         for (var e = 0; e < forms[f].elements.length; e++) {
211             var el = forms[f].elements[e];
212             if (el && ['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) + 1 && isVisible(el) && elementInViewport(el)) {
213                 res[0].push(el);
214             }
215         }
216     }
217     return res;
218 }
219 //Draw all hints for all elements passed. "len" is for
220 //the number of chars we should use to avoid collisions
221 function reDrawHints(elems, chars) {
222     removeAllHints();
223     var hintdiv = doc.createElement('div');
224     hintdiv.setAttribute('id', uzbldivid);
225     for (var i = 0; i < elems[0].length; i++) {
226         if (elems[0][i]) {
227             var label = elems[1][i].substring(chars);
228             var h = generateHint(elems[0][i], label);
229             hintdiv.appendChild(h);
230         }
231     }
232     if (document.body) {
233         document.body.appendChild(hintdiv);
234     }
235 }
236 // pass: number of keys
237 // returns: key length
238 function labelLength(n) {
239         var oldn = n;
240         var keylen = 0;
241         if(n < 2) {
242                 return 1;
243         }
244         n -= 1; // our highest key will be n-1
245         while(n) {
246                 keylen += 1;
247                 n = Math.floor(n / charset.length);
248         }
249         return keylen;
250 }
251 // pass: number
252 // returns: label
253 function intToLabel(n) {
254         var label = '';
255         do {
256                 label = charset.charAt(n % charset.length) + label;
257                 n = Math.floor(n / charset.length);
258         } while(n);
259         return label;
260 }
261 // pass: label
262 // returns: number
263 function labelToInt(label) {
264         var n = 0;
265         var i;
266         for(i = 0; i < label.length; ++i) {
267                 n *= charset.length;
268                 n += charset.indexOf(label[i]);
269         }
270         return n;
271 }
272 //Put it all together
273 function followLinks(follow) {
274     if(follow.charAt(0) == 'l') {
275         follow = follow.substr(1);
276         charset = 'thsnlrcgfdbmwvz-/';
277     }
278     var s = follow.split('');
279     var linknr = labelToInt(follow);
280     var linkelems = addLinks();
281     var formelems = addFormElems();
282     var elems = [linkelems[0].concat(formelems[0]), linkelems[1].concat(formelems[1])];
283     var len = labelLength(elems[0].length);
284     var oldDiv = doc.getElementById(uzbldivid);
285     var leftover = [[], []];
286     if (s.length == len && linknr < elems[0].length && linknr >= 0) {
287         clickElem(elems[0][linknr]);
288         got = '';
289         active = 0;
290     } else {
291         for (var j = 0; j < elems[0].length; j++) {
292             var b = true;
293             var label = intToLabel(j);
294             var n = label.length;
295             for (n; n < len; n++) {
296                 label = charset.charAt(0) + label;
297             }
298             for (var k = 0; k < s.length; k++) {
299                 b = b && label.charAt(k) == s[k];
300             }
301             if (b) {
302                 leftover[0].push(elems[0][j]);
303                 leftover[1].push(label);
304             }
305         }
306         reDrawHints(leftover, s.length);
307     }
308 }
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335         // from your event handler you can: return stop_event(e)
336         function stop_event(e) {
337                 // try {
338                         e.stopPropagation();
339                         e.preventDefault();
340                 // } catch (ex) {
341                 //      return false; // IE-compat
342                 // }
343         }
344         var active = 0;
345         var got = '';
346         document.addEventListener(
347                 'keydown',
348                 function(e) {
349                         // [de]activate on ^C
350                         // deactivate on ESC
351                         if(
352                                 (e.ctrlKey && e.keyCode == 67)
353                                 || (e.keyCode == 27 && active == 1)
354                         ) {
355                                 if(active) {
356                                         got = '';
357                                         removeAllHints();
358                                 } else {
359                                         followLinks(got);
360                                 }
361                                 active = 1 - active;
362                                 return stop_event(e);
363                         } else {
364                                 if(active == 1 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
365                                         if(key_to_char[e.keyCode]) {
366                                                 got += key_to_char[e.keyCode];
367                                                 followLinks(got);
368                                                 return stop_event(e);
369                                         }
370                                 }
371                         }
372                 },
373                 true);
374 })();