Add support for ";l" to show a link destination in the status bar.
[vimprobable.git] / hinting.js
blob793160ed264ac4d1c05ca9034ce32e90a4a164c8
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                 if (hintCount >= config.maxAllowedHints) {
74                     break;
75                 }
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;
81                 }
83                 var style = topwin.getComputedStyle(elem, "");
84                 if (style.display == "none" || style.visibility != "visible") {
85                     continue;
86                 }
88                 var leftpos = Math.max((rect.left + scrollX), scrollX);
89                 var toppos = Math.max((rect.top + scrollY), scrollY);
91                 /* making this block DOM compliant */
92                 var hint = hintSpan.cloneNode(false);
93                 hint.setAttribute("id", "vimprobablehint" + hintCount);
94                 hint.style.left = leftpos + "px";
95                 hint.style.top =  toppos + "px";
96                 text = doc.createTextNode(hintCount + 1);
97                 hint.appendChild(text);
99                 hintContainer.appendChild(hint);
100                 hintCount++;
101                 hints.push({
102                     elem:       elem,
103                     number:     hintCount,
104                     span:       hint,
105                     background: elem.style.background,
106                     foreground: elem.style.color}
107                 );
109                 /* make the link black to ensure it's readable */
110                 elem.style.color = config.elemColor;
111                 elem.style.background = config.elemBackground;
112             }
114             doc.documentElement.appendChild(hintContainer);
116             /* recurse into any iframe or frame element */
117             var frameTags = ["frame","iframe"];
118             for (var f = 0; f < frameTags.length; ++f) {
119                 var frames = doc.getElementsByTagName(frameTags[f]);
120                 for (i = 0, nframes = frames.length; i < nframes; ++i) {
121                     elem = frames[i];
122                     rect = elem.getBoundingClientRect();
123                     if (!elem.contentWindow || !rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY) {
124                         continue;
125                     }
126                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
127                 }
128             }
129         }
131         helper(topwin, 0, 0);
133         this.clearFocus();
134         this.focusHint(1);
135         if (hintCount == 1) {
136             /* just one hinted element - might as well follow it */
137             return this.fire(1);
138         }
139     };
141     /* set focus on hint with given number */
142     this.focusHint = function(n)
143     {
144         /* reset previous focused hint */
145         var hint = _getHintByNumber(currentFocusNum);
146         if (hint !== null) {
147             hint.elem.className = hint.elem.className.replace(config.hintClassFocus, config.hintClass);
148             hint.elem.style.background = config.elemBackground;
149         }
151         currentFocusNum = n;
153         /* mark new hint as focused */
154         hint = _getHintByNumber(currentFocusNum);
155         if (hint !== null) {
156             hint.elem.className = hint.elem.className.replace(config.hintClass, config.hintClassFocus);
157             hint.elem.style.background = config.elemBackgroundFocus;
158         }
159     };
161     /* set focus to next avaiable hint */
162     this.focusNextHint = function()
163     {
164         var index = _getHintIdByNumber(currentFocusNum);
166         if (typeof(hints[index + 1]) != "undefined") {
167             this.focusHint(hints[index + 1].number);
168         } else {
169             this.focusHint(hints[0].number);
170         }
171     };
173     /* set focus to previous avaiable hint */
174     this.focusPreviousHint = function()
175     {
176         var index = _getHintIdByNumber(currentFocusNum);
177         if (index !== 0 && typeof(hints[index - 1].number) != "undefined") {
178             this.focusHint(hints[index - 1].number);
179         } else {
180             this.focusHint(hints[hints.length - 1].number);
181         }
182     };
184     /* filters hints matching given number */
185     this.updateHints = function(n)
186     {
187         if (n === 0) {
188             return this.createHints();
189         }
190         /* remove none matching hints */
191         var i, remove = [];
192         for (i = 0; i < hints.length; ++i) {
193             var hint = hints[i];
194             if (0 !== hint.number.toString().indexOf(n.toString())) {
195                 remove.push(hint.number);
196             }
197         }
199         for (i = 0; i < remove.length; ++i) {
200             _removeHint(remove[i]);
201         }
203         if (hints.length === 1) {
204             return this.fire(hints[0].number);
205         } else {
206             return this.focusHint(n);
207         }
208     };
210     this.clearFocus = function()
211     {
212         if (document.activeElement && document.activeElement.blur) {
213             document.activeElement.blur();
214         }
215     };
217     /* remove all hints and set previous style to them */
218     this.clearHints = function()
219     {
220         if (hints.length === 0) {
221             return;
222         }
223         for (var i = 0; i < hints.length; ++i) {
224             var hint = hints[i];
225             if (typeof(hint.elem) != "undefined") {
226                 hint.elem.style.background = hint.background;
227                 hint.elem.style.color = hint.foreground;
228                 hint.span.parentNode.removeChild(hint.span);
229             }
230         }
231         hints = [];
232         hintContainer.parentNode.removeChild(hintContainer);
233         window.onkeyup = null;
234     };
236     /* fires the modeevent on hint with given number */
237     this.fire = function(n)
238     {
239         var doc, result;
240         n = n ? n : currentFocusNum;
241         var hint = _getHintByNumber(n);
242         if (typeof(hint.elem) == "undefined") {
243             return "done;";
244         }
246         var el = hint.elem;
247         var tag = el.nodeName.toLowerCase();
249         this.clearHints();
251         if (tag == "input" || tag == "textarea" || tag == "select") {
252             if (el.type == "radio" || el.type == "checkbox") {
253                 el.focus();
254                 _clickElement(el);
255                 return "done;";
256             }
257             if (el.type == "submit" || el.type == "reset" || el.type == "button" || el.type === "image") {
258                 _clickElement(el);
259                 return "done;";
260             }
261             el.focus();
262             return "insert;";
263         }
264         if (tag == "iframe" || tag == "frame") {
265             e.focus();
266             return "done;";
267         }
269         switch (mode)
270         {
271             case "f": result = _open(el); break;
272             case "F": result = _openNewWindow(el); break;
273             case "i": result = "open;" + _getElemtSource(el); break;
274             case "I": result = "tabopen;" + _getElemtSource(el); break;
275             case "l": result = "show_link;" + _getElemtSource(el); break;
276             case "s": result = "save;" + _getElemtSource(el); break;
277             case "y": result = "yank;" + _getElemtSource(el); break;
278             case "O": result = "colon;" + _getElemtSource(el); break;
279             default:  result = _getElemtSource(el); break;
280         }
282         return result;
283     };
285     this.focusInput = function()
286     {
287         if (document.getElementsByTagName("body")[0] === null || typeof(document.getElementsByTagName("body")[0]) != "object") {
288             return;
289         }
291         /* prefixing html: will result in namespace error */
292         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
293         var r = document.evaluate(hinttags, document,
294             function(p) {
295                 return "http://www.w3.org/1999/xhtml";
296             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
297         var i;
298         var j = 0;
299         var k = 0;
300         var first = null;
301         var tag;
302         for (i = 0; i < r.snapshotLength; i++) {
303             var elem = r.snapshotItem(i);
304             if (k === 0) {
305                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
306                     first = elem;
307                 } else {
308                     k--;
309                 }
310             }
311             if (j === 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
312                 elem.focus();
313                 return "insert;";
314             }
315             if (elem == document.activeElement) {
316                 j = 1;
317             }
318             k++;
319         }
320         /* no appropriate field found focused - focus the first one */
321         if (j === 0 && first !== null) {
322             first.focus();
323             return "insert;";
324         }
325     };
327     /* retrieves text content fro given element */
328     function _getTextFromElement(el)
329     {
330         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
331             text = el.value;
332         } else if (el instanceof HTMLSelectElement) {
333             if (el.selectedIndex >= 0) {
334                 text = el.item(el.selectedIndex).text;
335             } else{
336                 text = "";
337             }
338         } else {
339             text = el.textContent;
340         }
341         return text.toLowerCase();
342     }
344     /* retrieves the hint for given hint number */
345     function _getHintByNumber(n)
346     {
347         var index = _getHintIdByNumber(n);
348         if (index !== null) {
349             return hints[index];
350         }
351         return null;
352     }
354     /* retrieves the id of hint with given number */
355     function _getHintIdByNumber(n)
356     {
357         for (var i = 0; i < hints.length; ++i) {
358             var hint = hints[i];
359             if (hint.number === n) {
360                 return i;
361             }
362         }
363         return null;
364     }
366     /* removes hint with given number from hints array */
367     function _removeHint(n)
368     {
369         var index = _getHintIdByNumber(n);
370         if (index === null) {
371             return;
372         }
373         var hint = hints[index];
374         if (hint.number === n) {
375             hint.elem.style.background = hint.background;
376             hint.elem.style.color = hint.foreground;
377             hint.span.parentNode.removeChild(hint.span);
379             /* remove hints from all hints */
380             hints.splice(index, 1);
381         }
382     }
384     /* opens given element */
385     function _open(elem)
386     {
387         if (elem.target == "_blank") {
388             elem.removeAttribute("target");
389         }
390         _clickElement(elem);
391         return "done;";
392     }
394     /* opens given element into new window */
395     function _openNewWindow(elem)
396     {
397         var oldTarget = elem.target;
399         /* set target to open in new window */
400         elem.target = "_blank";
401         _clickElement(elem);
402         elem.target = oldTarget;
404         return "done;";
405     }
406     
407     /* fire moudedown and click event on given element */
408     function _clickElement(elem)
409     {
410         doc = elem.ownerDocument;
411         view = elem.contentWindow;
413         var evObj = doc.createEvent("MouseEvents");
414         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
415         elem.dispatchEvent(evObj);
417         evObj = doc.createEvent("MouseEvents");
418         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
419         elem.dispatchEvent(evObj);
420     }
422     /* retrieves the url of given element */
423     function _getElemtSource(elem)
424     {
425         var url = elem.href || elem.src;
426         return url;
427     }
429     /* retrieves the xpath expression according to mode */
430     function _getXpathXpression(text)
431     {
432         var expr;
433         if (typeof(text) == "undefined") {
434             text = "";
435         }
436         switch (mode) {
437             case "f":
438             case "F":
439                 if (text === "") {
440                     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";
441                 } else {
442                     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 + "')]";
443                 }
444                 break;
445             case "i":
446             case "I":
447                 if (text === "") {
448                     expr = "//img[@src]";
449                 } else {
450                     expr = "//img[@src and contains(., '" + text + "')]";
451                 }
452                 break;
453             default:
454                 if (text === "") {
455                     expr = "//*[@role='link' or @href] | //a[href] | //area | //img[not(ancestor::a)]";
456                 } else {
457                     expr = "//*[(@role='link' or @href) and contains(., '" + text + "')] | //a[@href and contains(., '" + text + "')] | //area[contains(., '" + text + "')] | //img[not(ancestor::a) and contains(., '" + text + "')]";
458                 }
459                 break;
460         }
461         return expr;
462     }
464 hints = new Hints();