bugfix: disable hinting mode if a new page is loaded, but not via hinting
[vimprobable2.git] / hinting.js
blob7821b940546493da57d8648a4fdd7aed19e8d351
1 /*
2     (c) 2009 by Leon Winter
3     (c) 2009, 2010 by Hannes Schueller
4     (c) 2010 by Hans-Peter Deifel
5     (c) 2011 by Daniel Carl
6     see LICENSE file
7 */
8 function Hints() {
9     var config = {
10         maxAllowedHints: 500,
11         hintCss: "z-index:100000;font-family:monospace;font-size:10px;"
12                + "font-weight:bold;color:white;background-color:red;"
13                + "padding:0px 1px;position:absolute;",
14         hintClass: "hinting_mode_hint",
15         hintClassFocus: "hinting_mode_hint_focus",
16         elemBackground: "#ff0",
17         elemBackgroundFocus: "#8f0",
18         elemColor: "#000"
19     };
21     var hintContainer;
22     var currentFocusNum = 1;
23     var hints = [];
24     var mode;
26     this.createHints = function(inputText, hintMode)
27     {
28         if (hintMode) {
29             mode = hintMode;
30         }
32         var topwin = window;
33         var top_height = topwin.innerHeight;
34         var top_width = topwin.innerWidth;
35         var xpath_expr;
37         var hintCount = 0;
38         this.clearHints();
40         function helper (win, offsetX, offsetY) {
41             var doc = win.document;
43             var win_height = win.height;
44             var win_width = win.width;
46             /* Bounds */
47             var minX = offsetX < 0 ? -offsetX : 0;
48             var minY = offsetY < 0 ? -offsetY : 0;
49             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
50             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
52             var scrollX = win.scrollX;
53             var scrollY = win.scrollY;
55             hintContainer = doc.createElement("div");
56             hintContainer.id = "hint_container";
58             xpath_expr = _getXpathXpression(inputText);
60             var res = doc.evaluate(xpath_expr, doc,
61                 function (p) {
62                     return "http://www.w3.org/1999/xhtml";
63                 }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
65             /* generate basic hint element which will be cloned and updated later */
66             var hintSpan = doc.createElement("span");
67             hintSpan.setAttribute("class", config.hintClass);
68             hintSpan.style.cssText = config.hintCss;
70             /* due to the different XPath result type, we will need two counter variables */
71             var rect, elem, text, node, show_text;
72             for (var i = 0; i < res.snapshotLength; i++)
73             {
74                 if (hintCount >= config.maxAllowedHints)
75                     break;
77                 elem = res.snapshotItem(i);
78                 rect = elem.getBoundingClientRect();
79                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
80                     continue;
82                 var style = topwin.getComputedStyle(elem, "");
83                 if (style.display == "none" || style.visibility != "visible")
84                     continue;
86                 var leftpos = Math.max((rect.left + scrollX), scrollX);
87                 var toppos = Math.max((rect.top + scrollY), scrollY);
89                 /* making this block DOM compliant */
90                 var hint = hintSpan.cloneNode(false);
91                 hint.setAttribute("id", "vimprobablehint" + hintCount);
92                 hint.style.left = leftpos + "px";
93                 hint.style.top =  toppos + "px";
94                 text = doc.createTextNode(hintCount + 1);
95                 hint.appendChild(text);
97                 hintContainer.appendChild(hint);
98                 hintCount++;
99                 hints.push({
100                     elem:       elem,
101                     number:     hintCount,
102                     span:       hint,
103                     background: elem.style.background,
104                     foreground: elem.style.color}
105                 );
107                 /* make the link black to ensure it's readable */
108                 elem.style.color = config.elemColor;
109                 elem.style.background = config.elemBackground;
110             }
112             doc.documentElement.appendChild(hintContainer);
114             /* recurse into any iframe or frame element */
115             var frameTags = ["frame","iframe"];
116             for (var f = 0; f < frameTags.length; ++f) {
117                 var frames = doc.getElementsByTagName(frameTags[f]);
118                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
119                     elem = frames[i];
120                     rect = elem.getBoundingClientRect();
121                     if (!elem.contentWindow || !rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
122                         continue;
123                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
124                 }
125             }
126         }
128         helper(topwin, 0, 0);
130         this.clearFocus();
131         this.focusHint(1);
132         if (hintCount == 1) {
133             /* just one hinted element - might as well follow it */
134             return this.fire(1);
135         }
136     };
138     /* set focus on hint with given number */
139     this.focusHint = function(n)
140     {
141         /* reset previous focused hint */
142         var hint = _getHintByNumber(currentFocusNum);
143         if (hint !== null) {
144             hint.elem.className = hint.elem.className.replace(config.hintClassFocus, config.hintClass);
145             hint.elem.style.background = config.elemBackground;
146         }
148         currentFocusNum = n;
150         /* mark new hint as focused */
151         var hint = _getHintByNumber(currentFocusNum);
152         if (hint !== null) {
153             hint.elem.className = hint.elem.className.replace(config.hintClass, config.hintClassFocus);
154             hint.elem.style.background = config.elemBackgroundFocus;
155         }
156     };
158     /* set focus to next avaiable hint */
159     this.focusNextHint = function()
160     {
161         var index = _getHintIdByNumber(currentFocusNum);
163         if (typeof(hints[index + 1]) != "undefined") {
164             this.focusHint(hints[index + 1].number);
165         } else {
166             this.focusHint(hints[0].number);
167         }
168     };
170     /* set focus to previous avaiable hint */
171     this.focusPreviousHint = function()
172     {
173         var index = _getHintIdByNumber(currentFocusNum);
174         if (index != 0 && typeof(hints[index - 1].number) != "undefined") {
175             this.focusHint(hints[index - 1].number);
176         } else {
177             this.focusHint(hints[hints.length - 1].number);
178         }
179     };
181     /* filters hints matching given number */
182     this.updateHints = function(n)
183     {
184         if (n == 0) {
185             return this.createHints();
186         }
187         /* remove none matching hints */
188         var remove = [];
189         for (var i = 0; i < hints.length; ++i) {
190             var hint = hints[i];
191             if (0 != hint.number.toString().indexOf(n.toString())) {
192                 remove.push(hint.number);
193             }
194         }
196         for (var i = 0; i < remove.length; ++i) {
197             _removeHint(remove[i]);
198         }
200         if (hints.length === 1) {
201             return this.fire(hints[0].number);
202         } else {
203             return this.focusHint(n);
204         }
205     };
207     this.clearFocus = function()
208     {
209         if (document.activeElement && document.activeElement.blur) {
210             document.activeElement.blur();
211         }
212     };
214     /* remove all hints and set previous style to them */
215     this.clearHints = function()
216     {
217         if (hints.length == 0) {
218             return;
219         }
220         for (var i = 0; i < hints.length; ++i) {
221             var hint = hints[i];
222             if (typeof(hint.elem) != "undefined") {
223                 hint.elem.style.background = hint.background;
224                 hint.elem.style.color = hint.foreground;
225                 hint.span.parentNode.removeChild(hint.span);
226             }
227         }
228         hints = [];
229         hintContainer.parentNode.removeChild(hintContainer);
230         window.onkeyup = null;
231     };
233     /* fires the modeevent on hint with given number */
234     this.fire = function(n)
235     {
236         var doc, result;
237         if (!n) {
238             var n = currentFocusNum;
239         }
240         var hint = _getHintByNumber(n);
241         if (typeof(hint.elem) == "undefined")
242             return "done;";
244         var el = hint.elem;
245         var tag = el.nodeName.toLowerCase();
247         this.clearHints();
249         if (tag == "iframe" || tag == "frame" || tag == "textarea" || tag == "input" && (el.type == "text" || el.type == "password" || el.type == "checkbox" || el.type == "radio") || tag == "select") {
250             el.focus();
251             if (tag == "input" || tag == "textarea") {
252                 return "insert;"
253             }
254             return "done;";
255         }
257         switch (mode)
258         {
259             case "f": case "i": result = _open(el); break;
260             case "F": case "I": result = _openNewWindow(el); break;
261             case "s": result = "save;" + _getElemtSource(el); break;
262             case "y": result = "yank;" + _getElemtSource(el); break;
263             case "O": result = "colon;" + _getElemtSource(el); break;
264             default:  result = _getElemtSource(el);
265         }
267         return result;
268     };
270     this.focusInput = function()
271     {
272         if (document.getElementsByTagName("body")[0] === null || typeof(document.getElementsByTagName("body")[0]) != "object")
273             return;
275         /* prefixing html: will result in namespace error */
276         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
277         var r = document.evaluate(hinttags, document,
278             function(p) {
279                 return "http://www.w3.org/1999/xhtml";
280             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
281         var i;
282         var j = 0;
283         var k = 0;
284         var first = null;
285         for (i = 0; i < r.snapshotLength; i++) {
286             var elem = r.snapshotItem(i);
287             if (k == 0) {
288                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
289                     first = elem;
290                 } else {
291                     k--;
292                 }
293             }
294             if (j == 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
295                 elem.focus();
296                 var tag = elem.nodeName.toLowerCase();
297                 if (tag == "textarea" || tag == "input") {
298                     return "insert;";
299                 }
300                 break;
301             }
302             if (elem == document.activeElement) {
303                 j = 1;
304             }
305             k++;
306         }
307         /* no appropriate field found focused - focus the first one */
308         if (j == 0 && first !== null) {
309             first.focus();
310             var tag = elem.nodeName.toLowerCase();
311             if (tag == "textarea" || tag == "input") {
312                 return "insert;";
313             }
314         }
315     };
317     /* retrieves text content fro given element */
318     function _getTextFromElement(el)
319     {
320         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
321             text = el.value;
322         } else if (el instanceof HTMLSelectElement) {
323             if (el.selectedIndex >= 0) {
324                 text = el.item(el.selectedIndex).text;
325             } else{
326                 text = "";
327             }
328         } else {
329             text = el.textContent;
330         }
331         return text.toLowerCase();;
332     }
334     /* retrieves the hint for given hint number */
335     function _getHintByNumber(n)
336     {
337         var index = _getHintIdByNumber(n);
338         if (index !== null) {
339             return hints[index];
340         }
341         return null;
342     }
344     /* retrieves the id of hint with given number */
345     function _getHintIdByNumber(n)
346     {
347         for (var i = 0; i < hints.length; ++i) {
348             var hint = hints[i];
349             if (hint.number === n) {
350                 return i;
351             }
352         }
353         return null;
354     }
356     /* removes hint with given number from hints array */
357     function _removeHint(n)
358     {
359         var index = _getHintIdByNumber(n);
360         if (index === null) {
361             return;
362         }
363         var hint = hints[index];
364         if (hint.number === n) {
365             hint.elem.style.background = hint.background;
366             hint.elem.style.color = hint.foreground;
367             hint.span.parentNode.removeChild(hint.span);
369             /* remove hints from all hints */
370             hints.splice(index, 1);
371         }
372     }
374     /* opens given element */
375     function _open(elem)
376     {
377         if (elem.target == "_blank") {
378             elem.removeAttribute("target");
379         }
380         _clickElement(elem);
381         return "done;";
382     }
384     /* opens given element into new window */
385     function _openNewWindow(elem)
386     {
387         var oldTarget = elem.target;
389         /* set target to open in new window */
390         elem.target = "_blank";
391         _clickElement(elem);
392         elem.target = oldTarget;
394         return "done;";
395     }
396     
397     /* fire moudedown and click event on given element */
398     function _clickElement(elem)
399     {
400         doc = elem.ownerDocument;
401         view = elem.contentWindow;
403         var evObj = doc.createEvent("MouseEvents");
404         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
405         elem.dispatchEvent(evObj);
407         var evObj = doc.createEvent("MouseEvents");
408         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
409         elem.dispatchEvent(evObj);
410     }
412     /* retrieves the url of given element */
413     function _getElemtSource(elem)
414     {
415         var url = elem.href || elem.src;
416         return url;
417     }
419     /* retrieves the xpath expression according to mode */
420     function _getXpathXpression(text)
421     {
422         var expr;
423         if (typeof(text) == "undefined") {
424             text = "";
425         }
426         switch (mode) {
427             case "f":
428             case "F":
429                 if (text == "") {
430                     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";
431                 } else {
432                     expr = "//*[(@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href) and contains(., '" + text + "')] | //input[not(@type='hidden') and contains(., '" + text + "')] | //a[@href and contains(., '" + text + "')] | //area[contains(., '" + text + "')] |  //textarea[contains(., '" + text + "')] | //button[contains(@value, '" + text + "')] | //select[contains(., '" + text + "')]";
433                 }
434                 break;
435             case "i":
436             case "I":
437                 if (text == "") {
438                     expr = "//img[@src]";
439                 } else {
440                     expr = "//img[@src and contains(., '" + text + "')]";
441                 }
442                 break;
443             default:
444                 if (text == "") {
445                     expr = "//*[@role='link' or @href] | //a[href] | //area | //img[not(ancestor::a)]";
446                 } else {
447                     expr = "//*[(@role='link' or @href) and contains(., '" + text + "')] | //a[@href and contains(., '" + text + "')] | //area[contains(., '" + text + "')] | //img[not(ancestor::a) and contains(., '" + text + "')]";
448                 }
449                 break;
450         }
451         return expr;
452     }
455 hints = new Hints();