Removed links without href attributes from hinting.
[vimprobable2.git] / hinting.js
blobcb002276dc49a0ee519ad1b1abba2d722505bc25
1 /*
2     (c) 2009 by Leon Winter
3     (c) 2009, 2010 by Hannes Schueller
4     (c) 2010 by Hans-Peter Deifel
5     see LICENSE file
6 */
7 function Hints() {
8     var config = {
9         maxAllowedHints: 500,
10         hintCss: "z-index:10000000;font-family:monospace;font-size:10px;"
11                + "font-weight:bold;color:white;background-color:red;"
12                + "padding:0px 1px;position:absolute;",
13         hintClass: "hinting_mode_hint",
14         hintClassFocus: "hinting_mode_hint_focus",
15         elemBackground: "#ff0",
16         elemBackgroundFocus: "#8f0",
17         elemColor: "#000"
18     };
20     var hintContainer;
21     var currentFocusNum = 1;
22     var hints = [];
23     var mode;
25     this.createHints = function(inputText, hintMode)
26     {
27         if (hintMode) {
28             mode = hintMode;
29         }
31         var topwin = window;
32         var top_height = topwin.innerHeight;
33         var top_width = topwin.innerWidth;
34         var xpath_expr;
36         var hintCount = 0;
37         this.clearHints();
39         function helper (win, offsetX, offsetY) {
40             var doc = win.document;
42             var win_height = win.height;
43             var win_width = win.width;
45             /* Bounds */
46             var minX = offsetX < 0 ? -offsetX : 0;
47             var minY = offsetY < 0 ? -offsetY : 0;
48             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
49             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
51             var scrollX = win.scrollX;
52             var scrollY = win.scrollY;
54             hintContainer = doc.createElement("div");
55             hintContainer.id = "hint_container";
57             if (typeof(inputText) == "undefined" || inputText == "") {
58                 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";
59             } else {
60                 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 + "')]";
61             }
63             var res = doc.evaluate(xpath_expr, doc,
64                 function (p) {
65                     return "http://www.w3.org/1999/xhtml";
66                 }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
68             /* generate basic hint element which will be cloned and updated later */
69             var hintSpan = doc.createElement("span");
70             hintSpan.setAttribute("class", config.hintClass);
71             hintSpan.style.cssText = config.hintCss;
73             /* due to the different XPath result type, we will need two counter variables */
74             var rect, elem, text, node, show_text;
75             for (var i = 0; i < res.snapshotLength; i++)
76             {
77                 if (hintCount >= config.maxAllowedHints)
78                     break;
80                 elem = res.snapshotItem(i);
81                 rect = elem.getBoundingClientRect();
82                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
83                     continue;
85                 var style = topwin.getComputedStyle(elem, "");
86                 if (style.display == "none" || style.visibility != "visible")
87                     continue;
89                 var leftpos = Math.max((rect.left + scrollX), scrollX);
90                 var toppos = Math.max((rect.top + scrollY), scrollY);
92                 /* process elements text */
93                 text = _getTextFromElement(elem);
95                 /* making this block DOM compliant */
96                 var hint = hintSpan.cloneNode(false);
97                 hint.setAttribute("id", "vimprobablehint" + hintCount);
98                 hint.style.left = leftpos + "px";
99                 hint.style.top =  toppos + "px";
100                 text = doc.createTextNode(hintCount + 1);
101                 hint.appendChild(text);
103                 hintContainer.appendChild(hint);
104                 hintCount++;
105                 hints.push({
106                     elem:       elem,
107                     number:     hintCount,
108                     text:       text,
109                     span:       hint,
110                     background: elem.style.background,
111                     foreground: elem.style.color}
112                 );
114                 /* make the link black to ensure it's readable */
115                 elem.style.color = config.elemColor;
116                 elem.style.background = config.elemBackground;
117             }
119             doc.documentElement.appendChild(hintContainer);
121             /* recurse into any iframe or frame element */
122             var frameTags = ["frame","iframe"];
123             for (var f = 0; f < frameTags.length; ++f) {
124                 var frames = doc.getElementsByTagName(frameTags[f]);
125                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
126                     elem = frames[i];
127                     rect = elem.getBoundingClientRect();
128                     if (!elem.contentWindow || !rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
129                         continue;
130                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
131                 }
132             }
133         }
135         helper(topwin, 0, 0);
137         this.clearFocus();
138         this.focusHint(1);
139         if (hintCount == 1) {
140             /* just one hinted element - might as well follow it */
141             return this.fire(1);
142         }
143     };
145     /* set focus on hint with given number */
146     this.focusHint = function(n)
147     {
148         /* reset previous focused hint */
149         var hint = _getHintByNumber(currentFocusNum);
150         if (hint !== null) {
151             hint.elem.className = hint.elem.className.replace(config.hintClassFocus, config.hintClass);
152             hint.elem.style.background = config.elemBackground;
153         }
155         currentFocusNum = n;
157         /* mark new hint as focused */
158         var hint = _getHintByNumber(currentFocusNum);
159         if (hint !== null) {
160             hint.elem.className = hint.elem.className.replace(config.hintClass, config.hintClassFocus);
161             hint.elem.style.background = config.elemBackgroundFocus;
162         }
163     };
165     /* set focus to next avaiable hint */
166     this.focusNextHint = function()
167     {
168         var index = _getHintIdByNumber(currentFocusNum);
170         if (typeof(hints[index + 1]) != "undefined") {
171             this.focusHint(hints[index + 1].number);
172         } else {
173             this.focusHint(hints[0].number);
174         }
175     };
177     /* set focus to previous avaiable hint */
178     this.focusPreviousHint = function()
179     {
180         var index = _getHintIdByNumber(currentFocusNum);
181         if (index != 0 && typeof(hints[index - 1].number) != "undefined") {
182             this.focusHint(hints[index - 1].number);
183         } else {
184             this.focusHint(hints[hints.length - 1].number);
185         }
186     };
188     /* filters hints matching given number */
189     this.updateHints = function(n)
190     {
191         if (n == 0) {
192             return this.createHints();
193         }
194         /* remove none matching hints */
195         var remove = [];
196         for (var i = 0; i < hints.length; ++i) {
197             var hint = hints[i];
198             if (0 != hint.number.toString().indexOf(n.toString())) {
199                 remove.push(hint.number);
200             }
201         }
203         for (var i = 0; i < remove.length; ++i) {
204             _removeHint(remove[i]);
205         }
207         if (hints.length === 1) {
208             return this.fire(hints[0].number);
209         } else {
210             return this.focusHint(n);
211         }
212     };
214     this.clearFocus = function()
215     {
216         if (document.activeElement && document.activeElement.blur) {
217             document.activeElement.blur();
218         }
219     };
221     /* remove all hints and set previous style to them */
222     this.clearHints = function()
223     {
224         if (hints.length == 0) {
225             return;
226         }
227         for (var i = 0; i < hints.length; ++i) {
228             var hint = hints[i];
229             if (typeof(hint.elem) != "undefined") {
230                 hint.elem.style.background = hint.background;
231                 hint.elem.style.color = hint.foreground;
232                 hint.span.parentNode.removeChild(hint.span);
233             }
234         }
235         hints = [];
236         hintContainer.parentNode.removeChild(hintContainer);
237         window.onkeyup = null;
238     };
240     /* fires the modeevent on hint with given number */
241     this.fire = function(n)
242     {
243         var doc, result;
244         if (!n) {
245             var n = currentFocusNum;
246         }
247         var hint = _getHintByNumber(n);
248         if (typeof(hint.elem) == "undefined")
249             return "done;";
251         var el = hint.elem;
252         var tag = el.nodeName.toLowerCase();
254         this.clearHints();
256         if (tag == "iframe" || tag == "frame" || tag == "textarea" || tag == "input" && (el.type == "text" || el.type == "password" || el.type == "checkbox" || el.type == "radio") || tag == "select") {
257             el.focus();
258             if (tag == "input" || tag == "textarea") {
259                 return "insert;"
260             }
261             return "done;";
262         }
264         switch (mode)
265         {
266             case "f": result = _open(el); break;
267             case "F": result = _openNewWindow(el); break;
268             default:  result = _getElemtSource(el);
269         }
271         return result;
272     };
274     this.focusInput = function()
275     {
276         if (document.getElementsByTagName("body")[0] === null || typeof(document.getElementsByTagName("body")[0]) != "object")
277             return;
279         /* prefixing html: will result in namespace error */
280         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
281         var r = document.evaluate(hinttags, document,
282             function(p) {
283                 return "http://www.w3.org/1999/xhtml";
284             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
285         var i;
286         var j = 0;
287         var k = 0;
288         var first = null;
289         for (i = 0; i < r.snapshotLength; i++) {
290             var elem = r.snapshotItem(i);
291             if (k == 0) {
292                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
293                     first = elem;
294                 } else {
295                     k--;
296                 }
297             }
298             if (j == 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
299                 elem.focus();
300                 var tag = elem.nodeName.toLowerCase();
301                 if (tag == "textarea" || tag == "input") {
302                     return "insert;";
303                 }
304                 break;
305             }
306             if (elem == document.activeElement) {
307                 j = 1;
308             }
309             k++;
310         }
311         /* no appropriate field found focused - focus the first one */
312         if (j == 0 && first !== null) {
313             first.focus();
314             var tag = elem.nodeName.toLowerCase();
315             if (tag == "textarea" || tag == "input") {
316                 return "insert;";
317             }
318         }
319     };
321     /* retrieves text content fro given element */
322     function _getTextFromElement(el)
323     {
324         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
325             text = el.value;
326         } else if (el instanceof HTMLSelectElement) {
327             if (el.selectedIndex >= 0) {
328                 text = el.item(el.selectedIndex).text;
329             } else{
330                 text = "";
331             }
332         } else {
333             text = el.textContent;
334         }
335         return text.toLowerCase();;
336     }
338     /* retrieves the hint for given hint number */
339     function _getHintByNumber(n)
340     {
341         var index = _getHintIdByNumber(n);
342         if (index !== null) {
343             return hints[index];
344         }
345         return null;
346     }
348     /* retrieves the id of hint with given number */
349     function _getHintIdByNumber(n)
350     {
351         for (var i = 0; i < hints.length; ++i) {
352             var hint = hints[i];
353             if (hint.number === n) {
354                 return i;
355             }
356         }
357         return null;
358     }
360     /* removes hint with given number from hints array */
361     function _removeHint(n)
362     {
363         var index = _getHintIdByNumber(n);
364         if (index === null) {
365             return;
366         }
367         var hint = hints[index];
368         if (hint.number === n) {
369             hint.elem.style.background = hint.background;
370             hint.elem.style.color = hint.foreground;
371             hint.span.parentNode.removeChild(hint.span);
373             /* remove hints from all hints */
374             hints.splice(index, 1);
375         }
376     }
378     /* opens given element */
379     function _open(elem)
380     {
381         if (elem.target == "_blank") {
382             elem.removeAttribute("target");
383         }
384         _clickElement(elem);
385         return "done;";
386     }
388     /* opens given element into new window */
389     function _openNewWindow(elem)
390     {
391         var oldTarget = elem.target;
393         /* set target to open in new window */
394         elem.target = "_blank";
395         _clickElement(elem);
396         elem.target = oldTarget;
398         return "done;";
399     }
401     /* fire moudedown and click event on given element */
402     function _clickElement(elem)
403     {
404         doc = elem.ownerDocument;
405         view = elem.contentWindow;
407         var evObj = doc.createEvent("MouseEvents");
408         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
409         elem.dispatchEvent(evObj);
411         var evObj = doc.createEvent("MouseEvents");
412         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
413         elem.dispatchEvent(evObj);
414     }
416     /* retrieves the url of given element */
417     function _getElemtSource(elem)
418     {
419         var url = elem.href || elem.src;
420         return url;
421     }
423 hints = new Hints();