]> git.armaanb.net Git - chorizo.git/blob - user-scripts/hints.js
0db5349668d2aebbb16c9dee3a3ebbd4550eeb56
[chorizo.git] / user-scripts / hints.js
1 // This is NOT a core component of lariza, but an optional user script.
2 // Please refer to lariza.usage(1) for more information on user scripts.
3
4 // Press "f" (open link in current window) or "F" (open in new window)
5 // to activate link hints. After typing the characters for one of them,
6 // press Enter to confirm. Press Escape to abort.
7 //
8 // This is an "80% solution". It works for many web sites, but has
9 // flaws. For more background on this topic, see this blog post:
10 // https://www.uninformativ.de/blog/postings/2020-02-24/0/POSTING-en.html
11
12 // Based on the following, but modified for lariza and personal taste:
13 //
14 // easy links for surf
15 // christian hahn <ch radamanthys de>, sep 2010
16 // http://surf.suckless.org/files/easy_links/
17 //
18 // link hints for surf
19 // based on chromium plugin code, adapted by Nibble<.gs@gmail.com>
20 // http://surf.suckless.org/files/link_hints/
21
22 // Anonymous function to get private namespace.
23 (function() {
24
25 var charset = "sdfghjklertzuivbn".split("");
26 var key_follow = "f";
27 var key_follow_new_win = "F";
28
29 function update_highlights_or_abort()
30 {
31     var submatch;
32     var col_sel, col_unsel;
33     var longest_id = 0;
34
35     if (document.lariza_hints.state === "follow_new")
36     {
37         col_unsel = "#DAFFAD";
38         col_sel = "#FF5D00";
39     }
40     else
41     {
42         col_unsel = "#A7FFF5";
43         col_sel = "#33FF00";
44     }
45
46     for (var id in document.lariza_hints.labels)
47     {
48         var label = document.lariza_hints.labels[id];
49         var bgcol = col_unsel;
50
51         longest_id = Math.max(longest_id, id.length);
52
53         if (document.lariza_hints.box.value !== "")
54         {
55             submatch = id.match("^" + document.lariza_hints.box.value);
56             if (submatch !== null)
57             {
58                 var href_suffix = "";
59                 var box_shadow_inner = "#B00000";
60                 if (id === document.lariza_hints.box.value)
61                 {
62                     bgcol = col_sel;
63                     box_shadow_inner = "red";
64                     if (label.elem.tagName.toLowerCase() === "a")
65                         href_suffix = ": <span style='font-size: 75%'>" +
66                                       label.elem.href + "</span>";
67                 }
68
69                 var len = submatch[0].length;
70                 label.span.innerHTML = "<b>" + submatch[0] + "</b>" +
71                                        id.substring(len, id.length) +
72                                        href_suffix;
73                 label.span.style.visibility = "visible";
74
75                 save_parent_style(label);
76                 label.elem.style.boxShadow = "0 0 5pt 2pt black, 0 0 0 2pt " +
77                                              box_shadow_inner + " inset";
78             }
79             else
80             {
81                 label.span.style.visibility = "hidden";
82                 reset_parent_style(label);
83             }
84         }
85         else
86         {
87             label.span.style.visibility = "visible";
88             label.span.innerHTML = id;
89             reset_parent_style(label);
90         }
91         label.span.style.backgroundColor = bgcol;
92     }
93
94     if (document.lariza_hints.box.value.length > longest_id)
95         set_state("inactive");
96 }
97
98 function open_match()
99 {
100     var choice = document.lariza_hints.box.value;
101     var was_state = document.lariza_hints.state;
102
103     var elem = document.lariza_hints.labels[choice].elem;
104     set_state("inactive");  /* Nukes labels. */
105
106     if (elem)
107     {
108         var tag_name = elem.tagName.toLowerCase();
109         var type = elem.type ? elem.type.toLowerCase() : "";
110
111         console.log("[hints] Selected elem [" + elem + "] [" + tag_name +
112                     "] [" + type + "]");
113
114         if (was_state === "follow_new" && tag_name === "a")
115             window.open(elem.href);
116         else if (
117             (
118                 tag_name === "input" &&
119                 type !== "button" &&
120                 type !== "color" &&
121                 type !== "checkbox" &&
122                 type !== "file" &&
123                 type !== "radio" &&
124                 type !== "reset" &&
125                 type !== "submit"
126             ) ||
127             tag_name === "textarea" ||
128             tag_name === "select"
129         )
130             elem.focus();
131         else
132             elem.click();
133     }
134 }
135
136 function reset_parent_style(label)
137 {
138     if (label.parent_style !== null)
139         label.elem.style.boxShadow = label.parent_style.boxShadow;
140 }
141
142 function save_parent_style(label)
143 {
144     if (label.parent_style === null)
145     {
146         var style = window.getComputedStyle(label.elem);
147         label.parent_style = new Object();
148         label.parent_style.boxShadow = style.getPropertyValue("boxShadow");
149     }
150 }
151
152 function set_state(new_state)
153 {
154     console.log("[hints] New state: " + new_state);
155
156     document.lariza_hints.state = new_state;
157
158     if (document.lariza_hints.state === "inactive")
159     {
160         nuke_labels();
161
162         // Removing our box causes unwanted scrolling. Just hide it.
163         document.lariza_hints.box.blur();
164         document.lariza_hints.box.value = "";
165         document.lariza_hints.box.style.visibility = "hidden";
166     }
167     else
168     {
169         if (document.lariza_hints.labels === null)
170             create_labels();
171
172         // What a terrible hack.
173         //
174         // Web sites often grab key events. That interferes with our
175         // script. But of course, they tend to ignore key events if an
176         // input element is currently focused. So ... yup, we install an
177         // invisible text box (opacity 0) and focus it while follow mode
178         // is active.
179         var box = document.lariza_hints.box;
180         if (box === null)
181         {
182             document.lariza_hints.box = document.createElement("input");
183             box = document.lariza_hints.box;
184
185             box.addEventListener("keydown", on_box_key);
186             box.addEventListener("input", on_box_input);
187             box.style.opacity = "0";
188             box.style.position = "fixed";
189             box.style.left = "0px";
190             box.style.top = "0px";
191             box.type = "text";
192
193             box.setAttribute("lariza_input_box", "yes");
194
195             document.body.appendChild(box);
196         }
197
198         box.style.visibility = "visible";
199         box.focus();
200
201         update_highlights_or_abort();
202     }
203 }
204
205 function create_labels()
206 {
207     document.lariza_hints.labels = new Object();
208
209     var selector = "a[href]:not([href=''])";
210     if (document.lariza_hints.state !== "follow_new")
211     {
212         selector += ", input:not([type=hidden]):not([lariza_input_box=yes])";
213         selector += ", textarea, select, button";
214     }
215
216     var elements = document.body.querySelectorAll(selector);
217
218     for (var i = 0; i < elements.length; i++)
219     {
220         var elem = elements[i];
221
222         var label_id = "";
223         var n = i;
224         do
225         {
226             // Appending the next "digit" (instead of prepending it as
227             // you would do it in a base conversion) scatters the labels
228             // better.
229             label_id += charset[n % charset.length];
230             n = Math.floor(n / charset.length);
231         } while (n !== 0);
232
233         var span = document.createElement("span");
234         span.style.border = "black 1pt solid";
235         span.style.color = "black";
236         span.style.fontFamily = "monospace";
237         span.style.fontSize = "10pt";
238         span.style.fontWeight = "normal";
239         span.style.margin = "0px 2pt";
240         span.style.position = "absolute";
241         span.style.textTransform = "lowercase";
242         span.style.visibility = "hidden";
243         span.style.zIndex = "2147483647";  // Max for WebKit according to luakit
244
245         document.lariza_hints.labels[label_id] = {
246             "elem": elem,
247             "span": span,
248             "parent_style": null,
249         };
250
251         // Appending the spans as children to anchors gives better
252         // placement results, but we can *only* do this for <a> ...
253         var tag_name = elem.tagName.toLowerCase();
254         if (tag_name === "a")
255         {
256             span.style.borderTopLeftRadius = "10pt";
257             span.style.borderBottomLeftRadius = "10pt";
258             span.style.padding = "0px 2pt 0px 5pt";
259             elem.appendChild(span);
260         }
261         else
262         {
263             span.style.borderRadius = "10pt";
264             span.style.padding = "0px 5pt";
265             elem.parentNode.insertBefore(span, elem);
266         }
267
268         console.log("[hints] Label ID " + label_id + ", " + i +
269                     " for elem [" + elem + "]");
270     }
271 }
272
273 function nuke_labels()
274 {
275     for (var id in document.lariza_hints.labels)
276     {
277         var label = document.lariza_hints.labels[id];
278
279         reset_parent_style(label);
280
281         var tag_name = label.elem.tagName.toLowerCase();
282         if (tag_name === "a")
283             label.elem.removeChild(label.span);
284         else
285             label.elem.parentNode.removeChild(label.span);
286     }
287
288     document.lariza_hints.labels = null;
289 }
290
291 function on_box_input(e)
292 {
293     update_highlights_or_abort();
294 }
295
296 function on_box_key(e)
297 {
298     if (e.key === "Escape")
299     {
300         e.preventDefault();
301         e.stopPropagation();
302         set_state("inactive");
303     }
304     else if (e.key === "Enter")
305     {
306         e.preventDefault();
307         e.stopPropagation();
308         open_match();
309     }
310 }
311
312 function on_window_key(e)
313 {
314     if (e.target.nodeName.toLowerCase() === "textarea" ||
315         e.target.nodeName.toLowerCase() === "input" ||
316         document.designMode === "on" ||
317         e.target.contentEditable === "true")
318     {
319         return;
320     }
321
322     if (document.lariza_hints.state === "inactive")
323     {
324         if (e.key === key_follow)
325             set_state("follow");
326         else if (e.key === key_follow_new_win)
327             set_state("follow_new");
328     }
329 }
330
331 if (document.lariza_hints === undefined)
332 {
333     document.lariza_hints = new Object();
334     document.lariza_hints.box = null;
335     document.lariza_hints.labels = null;
336     document.lariza_hints.state = "inactive";
337
338     document.addEventListener("keyup", on_window_key);
339
340     console.log("[hints] Initialized.");
341 }
342 else
343     console.log("[hints] ALREADY INSTALLED");
344
345 }());