version 1.3.0
[vimprobable2.git] / hinting.js
blob4997caec825da35dd60096739bc7f3767b6a9ce1
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 == "iframe" || tag == "frame" || tag == "textarea" || tag == "input" && (el.type == "text" || el.type == "password" || el.type == "checkbox" || el.type == "radio") || tag == "select") {
252             el.focus();
253             if (tag == "input" || tag == "textarea") {
254                 return "insert;";
255             }
256             return "done;";
257         }
259         switch (mode)
260         {
261             case "f": result = _open(el); break;
262             case "F": result = _openNewWindow(el); break;
263             case "i": result = "open;" + _getElemtSource(el); break;
264             case "I": result = "tabopen;" + _getElemtSource(el); break;
265             case "s": result = "save;" + _getElemtSource(el); break;
266             case "y": result = "yank;" + _getElemtSource(el); break;
267             case "O": result = "colon;" + _getElemtSource(el); break;
268             default:  result = _getElemtSource(el); break;
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;
278         }
280         /* prefixing html: will result in namespace error */
281         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
282         var r = document.evaluate(hinttags, document,
283             function(p) {
284                 return "http://www.w3.org/1999/xhtml";
285             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
286         var i;
287         var j = 0;
288         var k = 0;
289         var first = null;
290         var tag;
291         for (i = 0; i < r.snapshotLength; i++) {
292             var elem = r.snapshotItem(i);
293             if (k === 0) {
294                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
295                     first = elem;
296                 } else {
297                     k--;
298                 }
299             }
300             if (j === 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
301                 elem.focus();
302                 return "insert;";
303             }
304             if (elem == document.activeElement) {
305                 j = 1;
306             }
307             k++;
308         }
309         /* no appropriate field found focused - focus the first one */
310         if (j === 0 && first !== null) {
311             first.focus();
312             return "insert;";
313         }
314     };
316     /* retrieves text content fro given element */
317     function _getTextFromElement(el)
318     {
319         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
320             text = el.value;
321         } else if (el instanceof HTMLSelectElement) {
322             if (el.selectedIndex >= 0) {
323                 text = el.item(el.selectedIndex).text;
324             } else{
325                 text = "";
326             }
327         } else {
328             text = el.textContent;
329         }
330         return text.toLowerCase();
331     }
333     /* retrieves the hint for given hint number */
334     function _getHintByNumber(n)
335     {
336         var index = _getHintIdByNumber(n);
337         if (index !== null) {
338             return hints[index];
339         }
340         return null;
341     }
343     /* retrieves the id of hint with given number */
344     function _getHintIdByNumber(n)
345     {
346         for (var i = 0; i < hints.length; ++i) {
347             var hint = hints[i];
348             if (hint.number === n) {
349                 return i;
350             }
351         }
352         return null;
353     }
355     /* removes hint with given number from hints array */
356     function _removeHint(n)
357     {
358         var index = _getHintIdByNumber(n);
359         if (index === null) {
360             return;
361         }
362         var hint = hints[index];
363         if (hint.number === n) {
364             hint.elem.style.background = hint.background;
365             hint.elem.style.color = hint.foreground;
366             hint.span.parentNode.removeChild(hint.span);
368             /* remove hints from all hints */
369             hints.splice(index, 1);
370         }
371     }
373     /* opens given element */
374     function _open(elem)
375     {
376         if (elem.target == "_blank") {
377             elem.removeAttribute("target");
378         }
379         _clickElement(elem);
380         return "done;";
381     }
383     /* opens given element into new window */
384     function _openNewWindow(elem)
385     {
386         var oldTarget = elem.target;
388         /* set target to open in new window */
389         elem.target = "_blank";
390         _clickElement(elem);
391         elem.target = oldTarget;
393         return "done;";
394     }
395     
396     /* fire moudedown and click event on given element */
397     function _clickElement(elem)
398     {
399         doc = elem.ownerDocument;
400         view = elem.contentWindow;
402         var evObj = doc.createEvent("MouseEvents");
403         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
404         elem.dispatchEvent(evObj);
406         evObj = doc.createEvent("MouseEvents");
407         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
408         elem.dispatchEvent(evObj);
409     }
411     /* retrieves the url of given element */
412     function _getElemtSource(elem)
413     {
414         var url = elem.href || elem.src;
415         return url;
416     }
418     /* retrieves the xpath expression according to mode */
419     function _getXpathXpression(text)
420     {
421         var expr;
422         if (typeof(text) == "undefined") {
423             text = "";
424         }
425         switch (mode) {
426             case "f":
427             case "F":
428                 if (text === "") {
429                     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";
430                 } else {
431                     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 + "')]";
432                 }
433                 break;
434             case "i":
435             case "I":
436                 if (text === "") {
437                     expr = "//img[@src]";
438                 } else {
439                     expr = "//img[@src and contains(., '" + text + "')]";
440                 }
441                 break;
442             default:
443                 if (text === "") {
444                     expr = "//*[@role='link' or @href] | //a[href] | //area | //img[not(ancestor::a)]";
445                 } else {
446                     expr = "//*[(@role='link' or @href) and contains(., '" + text + "')] | //a[@href and contains(., '" + text + "')] | //area[contains(., '" + text + "')] | //img[not(ancestor::a) and contains(., '" + text + "')]";
447                 }
448                 break;
449         }
450         return expr;
451     }
453 hints = new Hints();