]> git.armaanb.net Git - chorizo.git/blob - user-scripts/hints.js
draft 4
[chorizo.git] / user-scripts / hints.js
1 // This is NOT a core component of chorizo, but an optional user script.
2 // Please refer to chorizo-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 chorizo 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     var submatch;
31     var longest_id = 0;
32
33     const color = (document.chorizo_hints.state === "follow_new") ? "#ffccff"
34         : "#99ffcc";
35
36     for (var id in document.chorizo_hints.labels) {
37         var label = document.chorizo_hints.labels[id];
38
39         longest_id = Math.max(longest_id, id.length);
40
41         if (document.chorizo_hints.box.value !== "") {
42             submatch = id.match("^" + document.chorizo_hints.box.value);
43             if (submatch !== null) {
44                 var href_suffix = "";
45                 if (id === document.chorizo_hints.box.value) {
46                     if (label.elem.tagName.toLowerCase() === "a")
47                         href_suffix = ": <span>" +
48                                       label.elem.href + "</span>";
49                 }
50
51                 var len = submatch[0].length;
52                 label.span.innerHTML = "<b>" + submatch[0] + "</b>" +
53                                        id.substring(len, id.length) +
54                                        href_suffix;
55                 label.span.style.visibility = "visible";
56
57                 save_parent_style(label);
58                 label.elem.style.boxShadow = "0 0 5px 5px red";
59             } else {
60                 label.span.style.visibility = "hidden";
61                 reset_parent_style(label);
62             }
63         } else {
64             label.span.style.visibility = "visible";
65             label.span.innerHTML = id;
66             reset_parent_style(label);
67         }
68         label.span.style.backgroundColor = color;
69     }
70
71     if (document.chorizo_hints.box.value.length > longest_id)
72         set_state("inactive");
73 }
74
75 function open_match() {
76     var choice = document.chorizo_hints.box.value;
77     var was_state = document.chorizo_hints.state;
78
79     var elem = document.chorizo_hints.labels[choice].elem;
80     set_state("inactive"); /* Nukes labels. */
81
82     if (elem) {
83         var tag_name = elem.tagName.toLowerCase();
84         var type = elem.type ? elem.type.toLowerCase() : "";
85
86         console.log("[hints] Selected elem [" + elem + "] [" + tag_name +
87                     "] [" + type + "]");
88
89         if (was_state === "follow_new" && tag_name === "a")
90             window.open(elem.href);
91         else if ((tag_name === "input" && type !== "button" &&
92                   type !== "color" && type !== "checkbox" && type !== "file" &&
93                   type !== "radio" && type !== "reset" && type !== "submit") ||
94                  tag_name === "textarea" || tag_name === "select")
95             elem.focus();
96         else
97             elem.click();
98     }
99 }
100
101 function reset_parent_style(label) {
102     if (label.parent_style !== null)
103         label.elem.style.boxShadow = label.parent_style.boxShadow;
104 }
105
106 function save_parent_style(label) {
107     if (label.parent_style === null) {
108         var style = window.getComputedStyle(label.elem);
109         label.parent_style = new Object();
110         label.parent_style.boxShadow = style.getPropertyValue("boxShadow");
111     }
112 }
113
114 function set_state(new_state) {
115     console.log("[hints] New state: " + new_state);
116
117     document.chorizo_hints.state = new_state;
118
119     if (document.chorizo_hints.state === "inactive") {
120         nuke_labels();
121
122         // Removing our box causes unwanted scrolling. Just hide it.
123         document.chorizo_hints.box.blur();
124         document.chorizo_hints.box.value = "";
125         document.chorizo_hints.box.style.visibility = "hidden";
126     } else {
127         if (document.chorizo_hints.labels === null)
128             create_labels();
129
130         // What a terrible hack.
131         //
132         // Web sites often grab key events. That interferes with our
133         // script. But of course, they tend to ignore key events if an
134         // input element is currently focused. So ... yup, we install an
135         // invisible text box (opacity 0) and focus it while follow mode
136         // is active.
137         var box = document.chorizo_hints.box;
138         if (box === null) {
139             document.chorizo_hints.box = document.createElement("input");
140             box = document.chorizo_hints.box;
141
142             box.addEventListener("keydown", on_box_key);
143             box.addEventListener("input", on_box_input);
144             box.style.opacity = "0";
145             box.style.position = "fixed";
146             box.style.left = "0px";
147             box.style.top = "0px";
148             box.type = "text";
149
150             box.setAttribute("chorizo_input_box", "yes");
151
152             document.body.appendChild(box);
153         }
154
155         box.style.visibility = "visible";
156         box.focus();
157
158         update_highlights_or_abort();
159     }
160 }
161
162 function create_labels() {
163     document.chorizo_hints.labels = new Object();
164
165     var selector = "a[href]:not([href=''])";
166     if (document.chorizo_hints.state !== "follow_new") {
167         selector += ", input:not([type=hidden]):not([chorizo_input_box=yes])";
168         selector += ", textarea, select, button";
169     }
170
171     var elements = document.body.querySelectorAll(selector);
172
173     for (var i = 0; i < elements.length; i++) {
174         var elem = elements[i];
175
176         var label_id = "";
177         var n = i;
178         do {
179             // Appending the next "digit" (instead of prepending it as
180             // you would do it in a base conversion) scatters the labels
181             // better.
182             label_id += charset[n % charset.length];
183             n = Math.floor(n / charset.length);
184         } while (n !== 0);
185
186         var span = document.createElement("span");
187         span.style.border = "black 1px solid";
188         span.style.color = "black";
189         span.style.fontFamily = "monospace";
190         span.style.fontSize = "10px";
191         span.style.fontWeight = "normal";
192         span.style.margin = "0px 2px";
193         span.style.padding = "1px 5px";
194         span.style.position = "absolute";
195         span.style.textTransform = "lowercase";
196         span.style.visibility = "hidden";
197         span.style.zIndex = "2147483647"; // Max for WebKit according to luakit
198
199         document.chorizo_hints.labels[label_id] = {
200             "elem" : elem,
201             "span" : span,
202             "parent_style" : null,
203         };
204
205         // Appending the spans as children to anchors gives better
206         // placement results, but we can *only* do this for <a> ...
207         var tag_name = elem.tagName.toLowerCase();
208         if (tag_name === "a") {
209             span.style.borderRadius = "2px";
210             elem.appendChild(span);
211         } else {
212             span.style.borderRadius = "10px";
213             elem.parentNode.insertBefore(span, elem);
214         }
215
216         console.log("[hints] Label ID " + label_id + ", " + i + " for elem [" +
217                     elem + "]");
218     }
219 }
220
221 function nuke_labels() {
222     for (var id in document.chorizo_hints.labels) {
223         var label = document.chorizo_hints.labels[id];
224
225         reset_parent_style(label);
226
227         var tag_name = label.elem.tagName.toLowerCase();
228         if (tag_name === "a")
229             label.elem.removeChild(label.span);
230         else
231             label.elem.parentNode.removeChild(label.span);
232     }
233
234     document.chorizo_hints.labels = null;
235 }
236
237 function on_box_input(e) { update_highlights_or_abort(); }
238
239 function on_box_key(e) {
240     if (e.key === "Escape") {
241         e.preventDefault();
242         e.stopPropagation();
243         set_state("inactive");
244     } else if (e.key === "Enter") {
245         e.preventDefault();
246         e.stopPropagation();
247         open_match();
248     }
249 }
250
251 function on_window_key(e) {
252     if (e.target.nodeName.toLowerCase() === "textarea" ||
253         e.target.nodeName.toLowerCase() === "input" ||
254         document.designMode === "on" || e.target.contentEditable === "true") {
255         return;
256     }
257
258     if (document.chorizo_hints.state === "inactive") {
259         if (e.key === key_follow)
260             set_state("follow");
261         else if (e.key === key_follow_new_win)
262             set_state("follow_new");
263     }
264 }
265
266 if (document.chorizo_hints === undefined) {
267     document.chorizo_hints = new Object();
268     document.chorizo_hints.box = null;
269     document.chorizo_hints.labels = null;
270     document.chorizo_hints.state = "inactive";
271
272     document.addEventListener("keyup", on_window_key);
273
274     console.log("[hints] Initialized.");
275 } else
276     console.log("[hints] ALREADY INSTALLED");
277 }());