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