Strip whitespace for config inputs
[xombrero.git] / hinting.js
blob0ceeb2b614b7f97962c55f9ff44f1c3ab416a339
1 /*
2 Copyright (c) 2009 Leon Winter
3 Copyright (c) 2009-2011 Hannes Schueller
4 Copyright (c) 2009-2010 Matto Fransen
5 Copyright (c) 2010-2011 Hans-Peter Deifel
6 Copyright (c) 2010-2011 Thomas Adam
7 Copyright (c) 2011 Albert Kim
8 Copyright (c) 2011 Daniel Carl
10 Permission is hereby granted, free of charge, to any person obtaining a copy
11 of this software and associated documentation files (the "Software"), to deal
12 in the Software without restriction, including without limitation the rights
13 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 copies of the Software, and to permit persons to whom the Software is
15 furnished to do so, subject to the following conditions:
17 The above copyright notice and this permission notice shall be included in
18 all copies or substantial portions of the Software.
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 THE SOFTWARE.
28 function Hints() {
29     var config = {
30         maxAllowedHints: 500,
31         hintCss: "z-index:100000;font-family:monospace;font-size:10px;"
32                + "font-weight:bold;color:white;background-color:red;"
33                + "padding:0px 1px;position:absolute;",
34         hintClass: "hinting_mode_hint",
35         hintClassFocus: "hinting_mode_hint_focus",
36         elemBackground: "#ff0",
37         elemBackgroundFocus: "#8f0",
38         elemColor: "#000"
39     };
41     var hintContainer;
42     var currentFocusNum = 1;
43     var hints = [];
44     var mode;
46     this.createHints = function(inputText, hintMode)
47     {
48         if (hintMode) {
49             mode = hintMode;
50         }
52         var topwin = window;
53         var top_height = topwin.innerHeight;
54         var top_width = topwin.innerWidth;
55         var xpath_expr;
57         var hintCount = 0;
58         this.clearHints();
60         function helper (win, offsetX, offsetY) {
61             var doc = win.document;
63             var win_height = win.height;
64             var win_width = win.width;
66             /* Bounds */
67             var minX = offsetX < 0 ? -offsetX : 0;
68             var minY = offsetY < 0 ? -offsetY : 0;
69             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
70             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
72             var scrollX = win.scrollX;
73             var scrollY = win.scrollY;
75             hintContainer = doc.createElement("div");
76             hintContainer.id = "hint_container";
78             if (typeof(inputText) == "undefined" || inputText == "") {
79                 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";
80             } else {
81                 xpath_expr = _caseInsensitiveExpr("//*[(@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href) and contains(translate(., '$u', '$l'), '$l')] | //input[not(@type='hidden') and contains(translate(., '$u', '$l'), '$l')] | //a[@href and contains(translate(., '$u', '$l'), '$l')] | //area[contains(translate(., '$u', '$l'), '$l')] | //textarea[contains(translate(., '$u', '$l'), '$l')] | //button[contains(translate(@value, '$u', '$l'), '$l')] | //select[contains(translate(., '$u', '$l'), '$l')]", inputText);
82             }
84             var res = doc.evaluate(xpath_expr, doc,
85                 function (p) {
86                     return "http://www.w3.org/1999/xhtml";
87                 }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
89             /* generate basic hint element which will be cloned and updated later */
90             var hintSpan = doc.createElement("span");
91             hintSpan.setAttribute("class", config.hintClass);
92             hintSpan.style.cssText = config.hintCss;
94             /* due to the different XPath result type, we will need two counter variables */
95             var rect, elem, text, node, show_text;
96             for (var i = 0; i < res.snapshotLength; i++)
97             {
98                 if (hintCount >= config.maxAllowedHints)
99                     break;
101                 elem = res.snapshotItem(i);
102                 rect = elem.getBoundingClientRect();
103                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
104                     continue;
106                 var style = topwin.getComputedStyle(elem, "");
107                 if (style.display == "none" || style.visibility != "visible")
108                     continue;
110                 var leftpos = Math.max((rect.left + scrollX), scrollX);
111                 var toppos = Math.max((rect.top + scrollY), scrollY);
113                 /* check if we already created a hint for this URL */
114                 var hintNumber = hintCount;
115                 if (elem.nodeName.toLowerCase() == "a") {
116                         for (var j = 0; j < hints.length; j++) {
117                                 var h = hints[j];
118                                 if (h.elem.nodeName.toLowerCase() != "a")
119                                         continue;
120                                 if (h.elem.href.toLowerCase() == elem.href.toLowerCase()){
121                                         hintNumber = h.number - 1;
122                                         break;
123                                 }
124                         }
125                 }
127                 /* making this block DOM compliant */
128                 var hint = hintSpan.cloneNode(false);
129                 hint.setAttribute("id", "vimprobablehint" + hintNumber);
130                 hint.style.left = leftpos + "px";
131                 hint.style.top =  toppos + "px";
132                 text = doc.createTextNode(hintNumber + 1);
133                 hint.appendChild(text);
135                 hintContainer.appendChild(hint);
136                 if (hintNumber == hintCount)
137                         hintCount++;
138                 else
139                         hintNumber = -2; /* do not follow dupes */
141                 hints.push({
142                     elem:       elem,
143                     number:     hintNumber+1,
144                     span:       hint,
145                     background: elem.style.background,
146                     foreground: elem.style.color}
147                 );
149                 /* make the link black to ensure it's readable */
150                 elem.style.color = config.elemColor;
151                 elem.style.background = config.elemBackground;
152             }
154             doc.documentElement.appendChild(hintContainer);
156             /* recurse into any iframe or frame element */
157             var frameTags = ["frame","iframe"];
158             for (var f = 0; f < frameTags.length; ++f) {
159                 var frames = doc.getElementsByTagName(frameTags[f]);
160                 for (var i = 0, nframes = frames.length; i < nframes; ++i) {
161                     elem = frames[i];
162                     rect = elem.getBoundingClientRect();
163                     if (!elem.contentWindow || !rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
164                         continue;
165                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
166                 }
167             }
168         }
170         helper(topwin, 0, 0);
172         this.clearFocus();
173         this.focusHint(1);
174         if (hintCount == 1) {
175             /* just one hinted element - might as well follow it */
176             return this.fire(1);
177         }
178     };
180     /* set focus on hint with given number */
181     this.focusHint = function(n)
182     {
183         /* reset previous focused hint */
184         var hint = _getHintByNumber(currentFocusNum);
185         if (hint !== null) {
186             hint.elem.className = hint.elem.className.replace(config.hintClassFocus, config.hintClass);
187             hint.elem.style.background = config.elemBackground;
188         }
190         currentFocusNum = n;
192         /* mark new hint as focused */
193         var hint = _getHintByNumber(currentFocusNum);
194         if (hint !== null) {
195             hint.elem.className = hint.elem.className.replace(config.hintClass, config.hintClassFocus);
196             hint.elem.style.background = config.elemBackgroundFocus;
197         }
198     };
200     /* set focus to next avaiable hint */
201     this.focusNextHint = function()
202     {
203         var index = _getHintIdByNumber(currentFocusNum);
205         if (typeof(hints[index + 1]) != "undefined") {
206             this.focusHint(hints[index + 1].number);
207         } else {
208             this.focusHint(hints[0].number);
209         }
210     };
212     /* set focus to previous avaiable hint */
213     this.focusPreviousHint = function()
214     {
215         var index = _getHintIdByNumber(currentFocusNum);
216         if (index != 0 && typeof(hints[index - 1].number) != "undefined") {
217             this.focusHint(hints[index - 1].number);
218         } else {
219             this.focusHint(hints[hints.length - 1].number);
220         }
221     };
223     /* filters hints matching given number */
224     this.updateHints = function(n)
225     {
226         if (n == 0) {
227             return this.createHints();
228         }
229         /* remove none matching hints */
230         var remove = [];
231         for (var i = 0; i < hints.length; ++i) {
232             var hint = hints[i];
233             if (0 != hint.number.toString().indexOf(n.toString())) {
234                 remove.push(hint.number);
235             }
236         }
238         for (var i = 0; i < remove.length; ++i) {
239             _removeHint(remove[i]);
240         }
242         if (hints.length === 1) {
243             return this.fire(hints[0].number);
244         } else {
245             return this.focusHint(n);
246         }
247     };
249     this.clearFocus = function()
250     {
251         if (document.activeElement && document.activeElement.blur) {
252             document.activeElement.blur();
253         }
254     };
256     /* remove all hints and set previous style to them */
257     this.clearHints = function()
258     {
259         if (hints.length == 0) {
260             return;
261         }
262         for (var i = 0; i < hints.length; ++i) {
263             var hint = hints[i];
264             if (typeof(hint.elem) != "undefined") {
265                 hint.elem.style.background = hint.background;
266                 hint.elem.style.color = hint.foreground;
267                 hint.span.parentNode.removeChild(hint.span);
268             }
269         }
270         hints = [];
271         hintContainer.parentNode.removeChild(hintContainer);
272         window.onkeyup = null;
273     };
275     /* fires the modeevent on hint with given number */
276     this.fire = function(n)
277     {
278         var doc, result;
279         if (!n) {
280             var n = currentFocusNum;
281         }
282         var hint = _getHintByNumber(n);
283         if (typeof(hint.elem) == "undefined")
284             return "done;";
286         var el = hint.elem;
287         var tag = el.nodeName.toLowerCase();
289         this.clearHints();
291         if (tag == "iframe" || tag == "frame" || tag == "textarea" || tag == "input" && (el.type == "text" || el.type == "password" || el.type == "checkbox" || el.type == "radio") || tag == "select") {
292             el.focus();
293             if (tag == "input" || tag == "textarea") {
294                 return "insert;"
295             }
296             return "done;";
297         }
299         switch (mode)
300         {
301             case "f": result = _open(el); break;
302             case "F": result = _openNewWindow(el); break;
303             default:  result = _getElemtSource(el);
304         }
306         return result;
307     };
309     this.focusInput = function()
310     {
311         if (document.getElementsByTagName("body")[0] === null || typeof(document.getElementsByTagName("body")[0]) != "object")
312             return;
314         /* prefixing html: will result in namespace error */
315         var hinttags = "//input[@type='text'] | //input[@type='password'] | //textarea";
316         var r = document.evaluate(hinttags, document,
317             function(p) {
318                 return "http://www.w3.org/1999/xhtml";
319             }, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
320         var i;
321         var j = 0;
322         var k = 0;
323         var first = null;
324         for (i = 0; i < r.snapshotLength; i++) {
325             var elem = r.snapshotItem(i);
326             if (k == 0) {
327                 if (elem.style.display != "none" && elem.style.visibility != "hidden") {
328                     first = elem;
329                 } else {
330                     k--;
331                 }
332             }
333             if (j == 1 && elem.style.display != "none" && elem.style.visibility != "hidden") {
334                 elem.focus();
335                 var tag = elem.nodeName.toLowerCase();
336                 if (tag == "textarea" || tag == "input") {
337                     return "insert;";
338                 }
339                 break;
340             }
341             if (elem == document.activeElement) {
342                 j = 1;
343             }
344             k++;
345         }
346         /* no appropriate field found focused - focus the first one */
347         if (j == 0 && first !== null) {
348             first.focus();
349             var tag = elem.nodeName.toLowerCase();
350             if (tag == "textarea" || tag == "input") {
351                 return "insert;";
352             }
353         }
354     };
356     /* retrieves text content fro given element */
357     function _getTextFromElement(el)
358     {
359         if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
360             text = el.value;
361         } else if (el instanceof HTMLSelectElement) {
362             if (el.selectedIndex >= 0) {
363                 text = el.item(el.selectedIndex).text;
364             } else{
365                 text = "";
366             }
367         } else {
368             text = el.textContent;
369         }
370         return text.toLowerCase();;
371     }
373     /* retrieves the hint for given hint number */
374     function _getHintByNumber(n)
375     {
376         var index = _getHintIdByNumber(n);
377         if (index !== null) {
378             return hints[index];
379         }
380         return null;
381     }
383     /* retrieves the id of hint with given number */
384     function _getHintIdByNumber(n)
385     {
386         for (var i = 0; i < hints.length; ++i) {
387             var hint = hints[i];
388             if (hint.number === n) {
389                 return i;
390             }
391         }
392         return null;
393     }
395     /* removes hint with given number from hints array */
396     function _removeHint(n)
397     {
398         var index = _getHintIdByNumber(n);
399         if (index === null) {
400             return;
401         }
402         var hint = hints[index];
403         if (hint.number === n) {
404             hint.elem.style.background = hint.background;
405             hint.elem.style.color = hint.foreground;
406             hint.span.parentNode.removeChild(hint.span);
408             /* remove hints from all hints */
409             hints.splice(index, 1);
410         }
411     }
413     /* opens given element */
414     function _open(elem)
415     {
416         if (elem.target == "_blank") {
417             elem.removeAttribute("target");
418         }
419         _clickElement(elem);
420         return "done;";
421     }
423     /* opens given element into new window */
424     function _openNewWindow(elem)
425     {
426         var oldTarget = elem.target;
428         /* set target to open in new window */
429         elem.target = "_blank";
430         _clickElement(elem);
431         elem.target = oldTarget;
433         return "done;";
434     }
436     /* fire moudedown and click event on given element */
437     function _clickElement(elem)
438     {
439         doc = elem.ownerDocument;
440         view = elem.contentWindow;
442         var evObj = doc.createEvent("MouseEvents");
443         evObj.initMouseEvent("mousedown", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
444         elem.dispatchEvent(evObj);
446         var evObj = doc.createEvent("MouseEvents");
447         evObj.initMouseEvent("click", true, true, view, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, null);
448         elem.dispatchEvent(evObj);
449     }
451     /* retrieves the url of given element */
452     function _getElemtSource(elem)
453     {
454         var url = elem.href || elem.src;
455         return url;
456     }
458     /* returns a case-insensitive version of the XPath expression */
459     function _caseInsensitiveExpr(xpath, searchString)
460     {
461         return xpath.split("$u").join(searchString.toUpperCase())
462                     .split("$l").join(searchString.toLowerCase());
463     }
465 hints = new Hints();