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