adding Daniel Carl's authorship notice
[vimprobable/e.git] / hinting.js
blobe23abc62de70826efffd00e90bbae56a31724acb
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             if (typeof(inputText) == "undefined" || inputText == "") {
59                 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";
60             } else {
61                 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 + "')]";
62             }
64             var res = doc.evaluate(xpath_expr, doc,
65                 function (p) {
66                     return "http://www.w3.org/1999/xhtml";
67                 }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
69             /* generate basic hint element which will be cloned and updated later */
70             var hintSpan = doc.createElement("span");
71             hintSpan.setAttribute("class", config.hintClass);
72             hintSpan.style.cssText = config.hintCss;
74             /* due to the different XPath result type, we will need two counter variables */
75             var rect, elem, text, node, show_text;
76             for (var i = 0; i < res.snapshotLength; i++)
77             {
78                 if (hintCount >= config.maxAllowedHints)
79                     break;
81                 elem = res.snapshotItem(i);
82                 rect = elem.getBoundingClientRect();
83                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
84                     continue;
86                 var style = topwin.getComputedStyle(elem, "");
87                 if (style.display == "none" || style.visibility != "visible")
88                     continue;
90                 var leftpos = Math.max((rect.left + scrollX), scrollX);
91                 var toppos = Math.max((rect.top + scrollY), scrollY);
93                 /* making this block DOM compliant */
94                 var hint = hintSpan.cloneNode(false);
95                 hint.setAttribute("id", "vimprobablehint" + hintCount);
96                 hint.style.left = leftpos + "px";
97                 hint.style.top =  toppos + "px";
98                 text = doc.createTextNode(hintCount + 1);
99                 hint.appendChild(text);
101                 hintContainer.appendChild(hint);
102                 hintCount++;
103                 hints.push({
104                     elem:       elem,
105                     number:     hintCount,
106                     span:       hint,
107                     background: elem.style.background,
108                     foreground: elem.style.color}
109                 );
111                 /* make the link black to ensure it's readable */
112                 elem.style.color = config.elemColor;
113                 elem.style.background = config.elemBackground;
114             }
116             doc.documentElement.appendChild(hintContainer);
118             /* recurse into any iframe or frame element */
119             var frameTags = ["frame","iframe"];
120             for (var f = 0; f < frameTags.length; ++f) {
121                 var frames = doc.getElementsByTagName(frameTags[f]);
122                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
123                     elem = frames[i];
124                     rect = elem.getBoundingClientRect();
125                     if (!elem.contentWindow || !rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
126                         continue;
127                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
128                 }
129             }
130         }
132         helper(topwin, 0, 0);
134         this.clearFocus();
135         this.focusHint(1);
136         if (hintCount == 1) {
137             /* just one hinted element - might as well follow it */
138             return this.fire(1);
139         }
140     };
142     /* set focus on hint with given number */
143     this.focusHint = function(n)
144     {
145         /* reset previous focused hint */
146         var hint = _getHintByNumber(currentFocusNum);
147         if (hint !== null) {
148             hint.elem.className = hint.elem.className.replace(config.hintClassFocus, config.hintClass);
149             hint.elem.style.background = config.elemBackground;
150         }
152         currentFocusNum = n;
154         /* mark new hint as focused */
155         var hint = _getHintByNumber(currentFocusNum);
156         if (hint !== null) {
157             hint.elem.className = hint.elem.className.replace(config.hintClass, config.hintClassFocus);
158             hint.elem.style.background = config.elemBackgroundFocus;
159         }
160     };
162     /* set focus to next avaiable hint */
163     this.focusNextHint = function()
164     {
165         var index = _getHintIdByNumber(currentFocusNum);
167         if (typeof(hints[index + 1]) != "undefined") {
168             this.focusHint(hints[index + 1].number);
169         } else {
170             this.focusHint(hints[0].number);
171         }
172     };
174     /* set focus to previous avaiable hint */
175     this.focusPreviousHint = function()
176     {
177         var index = _getHintIdByNumber(currentFocusNum);
178         if (index != 0 && typeof(hints[index - 1].number) != "undefined") {
179             this.focusHint(hints[index - 1].number);
180         } else {
181             this.focusHint(hints[hints.length - 1].number);
182         }
183     };
185     /* filters hints matching given number */
186     this.updateHints = function(n)
187     {
188         if (n == 0) {
189             return this.createHints();
190         }
191         /* remove none matching hints */
192         var remove = [];
193         for (var i = 0; i < hints.length; ++i) {
194             var hint = hints[i];
195             if (0 != hint.number.toString().indexOf(n.toString())) {
196                 remove.push(hint.number);
197             }
198         }
200         for (var i = 0; i < remove.length; ++i) {
201             _removeHint(remove[i]);
202         }
204         if (hints.length === 1) {
205             return this.fire(hints[0].number);
206         } else {
207             return this.focusHint(n);
208         }
209     };
211     this.clearFocus = function()
212     {
213         if (document.activeElement && document.activeElement.blur) {
214             document.activeElement.blur();
215         }
216     };
218     /* remove all hints and set previous style to them */
219     this.clearHints = function()
220     {
221         if (hints.length == 0) {
222             return;
223         }
224         for (var i = 0; i < hints.length; ++i) {
225             var hint = hints[i];
226             if (typeof(hint.elem) != "undefined") {
227                 hint.elem.style.background = hint.background;
228                 hint.elem.style.color = hint.foreground;
229                 hint.span.parentNode.removeChild(hint.span);
230             }
231         }
232         hints = [];
233         hintContainer.parentNode.removeChild(hintContainer);
234         window.onkeyup = null;
235     };
237     /* fires the modeevent on hint with given number */
238     this.fire = function(n)
239     {
240         var doc, result;
241         if (!n) {
242             var n = currentFocusNum;
243         }
244         var hint = _getHintByNumber(n);
245         if (typeof(hint.elem) == "undefined")
246             return "done;";
248         var el = hint.elem;
249         var tag = el.nodeName.toLowerCase();
251         this.clearHints();
253         if (tag == "iframe" || tag == "frame" || tag == "textarea" || tag == "input" && (el.type == "text" || el.type == "password" || el.type == "checkbox" || el.type == "radio") || tag == "select") {
254             el.focus();
255             if (tag == "input" || tag == "textarea") {
256                 return "insert;"
257             }
258             return "done;";
259         }
261         switch (mode)
262         {
263             case "f": result = _open(el); break;
264             case "F": result = _openNewWindow(el); break;
265             default:  result = _getElemtSource(el);
266         }
268         return result;
269     };
271     this.focusInput = function()
272     {
273         if (document.getElementsByTagName("body")[0] === null || typeof(document.getElementsByTagName("body")[0]) != "object")
274             return;
276         /* prefixing html: will result in namespace error */
277         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
278         var r = document.evaluate(hinttags, document,
279             function(p) {
280                 return "http://www.w3.org/1999/xhtml";
281             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
282         var i;
283         var j = 0;
284         var k = 0;
285         var first = null;
286         for (i = 0; i < r.snapshotLength; i++) {
287             var elem = r.snapshotItem(i);
288             if (k == 0) {
289                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
290                     first = elem;
291                 } else {
292                     k--;
293                 }
294             }
295             if (j == 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
296                 elem.focus();
297                 var tag = elem.nodeName.toLowerCase();
298                 if (tag == "textarea" || tag == "input") {
299                     return "insert;";
300                 }
301                 break;
302             }
303             if (elem == document.activeElement) {
304                 j = 1;
305             }
306             k++;
307         }
308         /* no appropriate field found focused - focus the first one */
309         if (j == 0 && first !== null) {
310             first.focus();
311             var tag = elem.nodeName.toLowerCase();
312             if (tag == "textarea" || tag == "input") {
313                 return "insert;";
314             }
315         }
316     };
318     /* retrieves text content fro given element */
319     function _getTextFromElement(el)
320     {
321         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
322             text = el.value;
323         } else if (el instanceof HTMLSelectElement) {
324             if (el.selectedIndex >= 0) {
325                 text = el.item(el.selectedIndex).text;
326             } else{
327                 text = "";
328             }
329         } else {
330             text = el.textContent;
331         }
332         return text.toLowerCase();;
333     }
335     /* retrieves the hint for given hint number */
336     function _getHintByNumber(n)
337     {
338         var index = _getHintIdByNumber(n);
339         if (index !== null) {
340             return hints[index];
341         }
342         return null;
343     }
345     /* retrieves the id of hint with given number */
346     function _getHintIdByNumber(n)
347     {
348         for (var i = 0; i < hints.length; ++i) {
349             var hint = hints[i];
350             if (hint.number === n) {
351                 return i;
352             }
353         }
354         return null;
355     }
357     /* removes hint with given number from hints array */
358     function _removeHint(n)
359     {
360         var index = _getHintIdByNumber(n);
361         if (index === null) {
362             return;
363         }
364         var hint = hints[index];
365         if (hint.number === n) {
366             hint.elem.style.background = hint.background;
367             hint.elem.style.color = hint.foreground;
368             hint.span.parentNode.removeChild(hint.span);
370             /* remove hints from all hints */
371             hints.splice(index, 1);
372         }
373     }
375     /* opens given element */
376     function _open(elem)
377     {
378         if (elem.target == "_blank") {
379             elem.removeAttribute("target");
380         }
381         _clickElement(elem);
382         return "done;";
383     }
385     /* opens given element into new window */
386     function _openNewWindow(elem)
387     {
388         var oldTarget = elem.target;
390         /* set target to open in new window */
391         elem.target = "_blank";
392         _clickElement(elem);
393         elem.target = oldTarget;
395         return "done;";
396     }
398     /* fire moudedown and click event on given element */
399     function _clickElement(elem)
400     {
401         doc = elem.ownerDocument;
402         view = elem.contentWindow;
404         var evObj = doc.createEvent("MouseEvents");
405         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
406         elem.dispatchEvent(evObj);
408         var evObj = doc.createEvent("MouseEvents");
409         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
410         elem.dispatchEvent(evObj);
411     }
413     /* retrieves the url of given element */
414     function _getElemtSource(elem)
415     {
416         var url = elem.href || elem.src;
417         return url;
418     }
420 hints = new Hints();