Add ctrl-p to toggle plugins
[xombrero.git] / hinting.js
blob135c4dd5399b59a6c7661900ddf1354dcde0b961
1 /*
2 Copyright (c) 2009 Leon Winter
3 Copyright (c) 2009-2011 Hannes Schueller
4 Copyright (c) 2009-2010 Matto Fransen
5 Copyright (c) 2010-2011 Hans-Peter Deifel
6 Copyright (c) 2010-2011 Thomas Adam
7 Copyright (c) 2011 Albert Kim
8 Copyright (c) 2011 Daniel Carl
10 Permission is hereby granted, free of charge, to any person obtaining a copy
11 of this software and associated documentation files (the "Software"), to deal
12 in the Software without restriction, including without limitation the rights
13 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 copies of the Software, and to permit persons to whom the Software is
15 furnished to do so, subject to the following conditions:
17 The above copyright notice and this permission notice shall be included in
18 all copies or substantial portions of the Software.
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 THE SOFTWARE.
28 function Hints() {
29     var config = {
30         maxAllowedHints: 500,
31         hintCss: "z-index:100000;font-family:monospace;font-size:10px;"
32                + "font-weight:bold;color:white;background-color:red;"
33                + "padding:0px 1px;position:absolute;",
34         hintClass: "hinting_mode_hint",
35         hintClassFocus: "hinting_mode_hint_focus",
36         elemBackground: "#ff0",
37         elemBackgroundFocus: "#8f0",
38         elemColor: "#000"
39     };
41     var hintContainer;
42     var currentFocusNum = 1;
43     var hints = [];
44     var mode;
46     this.createHints = function(inputText, hintMode)
47     {
48         if (hintMode) {
49             mode = hintMode;
50         }
52         var topwin = window;
53         var top_height = topwin.innerHeight;
54         var top_width = topwin.innerWidth;
55         var xpath_expr;
57         var hintCount = 0;
58         this.clearHints();
60         function helper (win, offsetX, offsetY) {
61             var doc = win.document;
63             var win_height = win.height;
64             var win_width = win.width;
66             /* Bounds */
67             var minX = offsetX < 0 ? -offsetX : 0;
68             var minY = offsetY < 0 ? -offsetY : 0;
69             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
70             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
72             var scrollX = win.scrollX;
73             var scrollY = win.scrollY;
75             hintContainer = doc.createElement("div");
76             hintContainer.id = "hint_container";
78             if (typeof(inputText) == "undefined" || inputText == "") {
79                 xpath_expr = "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a[href] | //area | //textarea | //button | //select";
80             } else {
81                 xpath_expr = "//*[(@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href) and contains(., '" + inputText + "')] | //input[not(@type='hidden') and contains(., '" + inputText + "')] | //a[@href and contains(., '" + inputText + "')] | //area[contains(., '" + inputText + "')] |  //textarea[contains(., '" + inputText + "')] | //button[contains(@value, '" + inputText + "')] | //select[contains(., '" + inputText + "')]";
82             }
84             var res = doc.evaluate(xpath_expr, doc,
85                 function (p) {
86                     return "http://www.w3.org/1999/xhtml";
87                 }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
89             /* generate basic hint element which will be cloned and updated later */
90             var hintSpan = doc.createElement("span");
91             hintSpan.setAttribute("class", config.hintClass);
92             hintSpan.style.cssText = config.hintCss;
94             /* due to the different XPath result type, we will need two counter variables */
95             var rect, elem, text, node, show_text;
96             for (var i = 0; i < res.snapshotLength; i++)
97             {
98                 if (hintCount >= config.maxAllowedHints)
99                     break;
101                 elem = res.snapshotItem(i);
102                 rect = elem.getBoundingClientRect();
103                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
104                     continue;
106                 var style = topwin.getComputedStyle(elem, "");
107                 if (style.display == "none" || style.visibility != "visible")
108                     continue;
110                 var leftpos = Math.max((rect.left + scrollX), scrollX);
111                 var toppos = Math.max((rect.top + scrollY), scrollY);
113                 /* making this block DOM compliant */
114                 var hint = hintSpan.cloneNode(false);
115                 hint.setAttribute("id", "vimprobablehint" + hintCount);
116                 hint.style.left = leftpos + "px";
117                 hint.style.top =  toppos + "px";
118                 text = doc.createTextNode(hintCount + 1);
119                 hint.appendChild(text);
121                 hintContainer.appendChild(hint);
122                 hintCount++;
123                 hints.push({
124                     elem:       elem,
125                     number:     hintCount,
126                     span:       hint,
127                     background: elem.style.background,
128                     foreground: elem.style.color}
129                 );
131                 /* make the link black to ensure it's readable */
132                 elem.style.color = config.elemColor;
133                 elem.style.background = config.elemBackground;
134             }
136             doc.documentElement.appendChild(hintContainer);
138             /* recurse into any iframe or frame element */
139             var frameTags = ["frame","iframe"];
140             for (var f = 0; f < frameTags.length; ++f) {
141                 var frames = doc.getElementsByTagName(frameTags[f]);
142                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
143                     elem = frames[i];
144                     rect = elem.getBoundingClientRect();
145                     if (!elem.contentWindow || !rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
146                         continue;
147                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
148                 }
149             }
150         }
152         helper(topwin, 0, 0);
154         this.clearFocus();
155         this.focusHint(1);
156         if (hintCount == 1) {
157             /* just one hinted element - might as well follow it */
158             return this.fire(1);
159         }
160     };
162     /* set focus on hint with given number */
163     this.focusHint = function(n)
164     {
165         /* reset previous focused hint */
166         var hint = _getHintByNumber(currentFocusNum);
167         if (hint !== null) {
168             hint.elem.className = hint.elem.className.replace(config.hintClassFocus, config.hintClass);
169             hint.elem.style.background = config.elemBackground;
170         }
172         currentFocusNum = n;
174         /* mark new hint as focused */
175         var hint = _getHintByNumber(currentFocusNum);
176         if (hint !== null) {
177             hint.elem.className = hint.elem.className.replace(config.hintClass, config.hintClassFocus);
178             hint.elem.style.background = config.elemBackgroundFocus;
179         }
180     };
182     /* set focus to next avaiable hint */
183     this.focusNextHint = function()
184     {
185         var index = _getHintIdByNumber(currentFocusNum);
187         if (typeof(hints[index + 1]) != "undefined") {
188             this.focusHint(hints[index + 1].number);
189         } else {
190             this.focusHint(hints[0].number);
191         }
192     };
194     /* set focus to previous avaiable hint */
195     this.focusPreviousHint = function()
196     {
197         var index = _getHintIdByNumber(currentFocusNum);
198         if (index != 0 && typeof(hints[index - 1].number) != "undefined") {
199             this.focusHint(hints[index - 1].number);
200         } else {
201             this.focusHint(hints[hints.length - 1].number);
202         }
203     };
205     /* filters hints matching given number */
206     this.updateHints = function(n)
207     {
208         if (n == 0) {
209             return this.createHints();
210         }
211         /* remove none matching hints */
212         var remove = [];
213         for (var i = 0; i < hints.length; ++i) {
214             var hint = hints[i];
215             if (0 != hint.number.toString().indexOf(n.toString())) {
216                 remove.push(hint.number);
217             }
218         }
220         for (var i = 0; i < remove.length; ++i) {
221             _removeHint(remove[i]);
222         }
224         if (hints.length === 1) {
225             return this.fire(hints[0].number);
226         } else {
227             return this.focusHint(n);
228         }
229     };
231     this.clearFocus = function()
232     {
233         if (document.activeElement && document.activeElement.blur) {
234             document.activeElement.blur();
235         }
236     };
238     /* remove all hints and set previous style to them */
239     this.clearHints = function()
240     {
241         if (hints.length == 0) {
242             return;
243         }
244         for (var i = 0; i < hints.length; ++i) {
245             var hint = hints[i];
246             if (typeof(hint.elem) != "undefined") {
247                 hint.elem.style.background = hint.background;
248                 hint.elem.style.color = hint.foreground;
249                 hint.span.parentNode.removeChild(hint.span);
250             }
251         }
252         hints = [];
253         hintContainer.parentNode.removeChild(hintContainer);
254         window.onkeyup = null;
255     };
257     /* fires the modeevent on hint with given number */
258     this.fire = function(n)
259     {
260         var doc, result;
261         if (!n) {
262             var n = currentFocusNum;
263         }
264         var hint = _getHintByNumber(n);
265         if (typeof(hint.elem) == "undefined")
266             return "done;";
268         var el = hint.elem;
269         var tag = el.nodeName.toLowerCase();
271         this.clearHints();
273         if (tag == "iframe" || tag == "frame" || tag == "textarea" || tag == "input" && (el.type == "text" || el.type == "password" || el.type == "checkbox" || el.type == "radio") || tag == "select") {
274             el.focus();
275             if (tag == "input" || tag == "textarea") {
276                 return "insert;"
277             }
278             return "done;";
279         }
281         switch (mode)
282         {
283             case "f": result = _open(el); break;
284             case "F": result = _openNewWindow(el); break;
285             default:  result = _getElemtSource(el);
286         }
288         return result;
289     };
291     this.focusInput = function()
292     {
293         if (document.getElementsByTagName("body")[0] === null || typeof(document.getElementsByTagName("body")[0]) != "object")
294             return;
296         /* prefixing html: will result in namespace error */
297         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
298         var r = document.evaluate(hinttags, document,
299             function(p) {
300                 return "http://www.w3.org/1999/xhtml";
301             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
302         var i;
303         var j = 0;
304         var k = 0;
305         var first = null;
306         for (i = 0; i < r.snapshotLength; i++) {
307             var elem = r.snapshotItem(i);
308             if (k == 0) {
309                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
310                     first = elem;
311                 } else {
312                     k--;
313                 }
314             }
315             if (j == 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
316                 elem.focus();
317                 var tag = elem.nodeName.toLowerCase();
318                 if (tag == "textarea" || tag == "input") {
319                     return "insert;";
320                 }
321                 break;
322             }
323             if (elem == document.activeElement) {
324                 j = 1;
325             }
326             k++;
327         }
328         /* no appropriate field found focused - focus the first one */
329         if (j == 0 && first !== null) {
330             first.focus();
331             var tag = elem.nodeName.toLowerCase();
332             if (tag == "textarea" || tag == "input") {
333                 return "insert;";
334             }
335         }
336     };
338     /* retrieves text content fro given element */
339     function _getTextFromElement(el)
340     {
341         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
342             text = el.value;
343         } else if (el instanceof HTMLSelectElement) {
344             if (el.selectedIndex >= 0) {
345                 text = el.item(el.selectedIndex).text;
346             } else{
347                 text = "";
348             }
349         } else {
350             text = el.textContent;
351         }
352         return text.toLowerCase();;
353     }
355     /* retrieves the hint for given hint number */
356     function _getHintByNumber(n)
357     {
358         var index = _getHintIdByNumber(n);
359         if (index !== null) {
360             return hints[index];
361         }
362         return null;
363     }
365     /* retrieves the id of hint with given number */
366     function _getHintIdByNumber(n)
367     {
368         for (var i = 0; i < hints.length; ++i) {
369             var hint = hints[i];
370             if (hint.number === n) {
371                 return i;
372             }
373         }
374         return null;
375     }
377     /* removes hint with given number from hints array */
378     function _removeHint(n)
379     {
380         var index = _getHintIdByNumber(n);
381         if (index === null) {
382             return;
383         }
384         var hint = hints[index];
385         if (hint.number === n) {
386             hint.elem.style.background = hint.background;
387             hint.elem.style.color = hint.foreground;
388             hint.span.parentNode.removeChild(hint.span);
390             /* remove hints from all hints */
391             hints.splice(index, 1);
392         }
393     }
395     /* opens given element */
396     function _open(elem)
397     {
398         if (elem.target == "_blank") {
399             elem.removeAttribute("target");
400         }
401         _clickElement(elem);
402         return "done;";
403     }
405     /* opens given element into new window */
406     function _openNewWindow(elem)
407     {
408         var oldTarget = elem.target;
410         /* set target to open in new window */
411         elem.target = "_blank";
412         _clickElement(elem);
413         elem.target = oldTarget;
415         return "done;";
416     }
418     /* fire moudedown and click event on given element */
419     function _clickElement(elem)
420     {
421         doc = elem.ownerDocument;
422         view = elem.contentWindow;
424         var evObj = doc.createEvent("MouseEvents");
425         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
426         elem.dispatchEvent(evObj);
428         var evObj = doc.createEvent("MouseEvents");
429         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
430         elem.dispatchEvent(evObj);
431     }
433     /* retrieves the url of given element */
434     function _getElemtSource(elem)
435     {
436         var url = elem.href || elem.src;
437         return url;
438     }
440 hints = new Hints();