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