var hints = Object.freeze((function(){ 'use strict'; var hints = [], /* holds all hint data (hinted element, label, number) in view port */ docs = [], /* hold the affected document with the start and end index of the hints */ validHints = [], /* holds the valid hinted elements matching the filter condition */ activeHint, /* holds the active hint object */ filterText = "", /* holds the typed text filter */ filterKeys = "", /* holds the typed hint-keys filter */ attr = "neovimbhint", config; /* the hint class used to maintain hinted element and labels */ function Hint() { var state = ""; /* hide hint label and remove coloring from hinted element */ this.hide = function() { var l = this.label, e = this.e; /* remove hint labels from no more visible hints */ l.style.display = "none"; l.setAttribute(attr, ""); e.setAttribute(attr, ""); state = "hidden"; }; /* marks the element and label of a hint as focused */ this.focus = function() { this.label.setAttribute(attr, "label focus"); this.e.setAttribute(attr, "hint focus"); state = "focus"; }; /* remove focus mark from hint and label */ this.unfocus = function() { /* do not unfocus hidden hints */ if (state != "hidden") { this.label.setAttribute(attr, "label"); this.e.setAttribute(attr, "hint"); state = "visible"; } }; /* show the hint element colored with the hint label */ this.show = function() { var e = this.e, l = this.label, text = []; if (state != "focus") { l.style.display = ""; l.setAttribute(attr, "label"); e.setAttribute(attr, "hint"); state = "visible"; } /* create the label with the hint number/letters */ if (e instanceof HTMLInputElement) { var type = e.type; if (type === "checkbox") { text.push(e.checked ? "☑" : "☐"); } else if (type === "radio") { text.push(e.checked ? "⊙" : "○"); } } if (this.showText && this.text) { text.push(this.text.substr(0, 20)); } /* use \x20 instead of ' ' to keep this space during js2h.sh processing */ l.innerText = this.num + (text.length ? ":\x20" + text.join("\x20") : ""); }; } function onresize() { clear(false); create(); show(false); } function clear(removeListener) { var i, j, doc, e, w = window; if (removeListener && w) { w.removeEventListener("resize", onresize, true); w.removeEventListener("scroll", onresize, false); for (i = 0; i < w.frames.length; i++) { try { w.frames[i].frameElement.contentDocument.removeEventListener("scroll", onresize, false); } catch (ex) { } } } for (i = 0; i < docs.length; i++) { doc = docs[i]; /* find all hinted elements neovimbhint 'hint' */ var res = xpath(doc.doc, "//*[@neovimbhint]"); for (j = 0; j < res.snapshotLength; j++) { e = res.snapshotItem(j); e.removeAttribute(attr); } doc.div.parentNode.removeChild(doc.div); } docs = []; hints = []; validHints = []; filterText = ""; filterKeys = ""; } function create() { var count = 0; function helper(win, offsets) { /* document may be undefined for frames out of the same origin */ /* policy and will break the whole code - so we check this before */ if (typeof win.document == "undefined") { return; } offsets = offsets || {left: 0, right: 0, top: 0, bottom: 0}; offsets.right = win.innerWidth - offsets.right; offsets.bottom = win.innerHeight - offsets.bottom; /* checks if given elemente is in viewport and visible */ function isVisible(e) { if (typeof e == "undefined") { return false; } var rect = e.getBoundingClientRect(); if (!rect || rect.top >= offsets.bottom || rect.bottom <= offsets.top || rect.left >= offsets.right || rect.right <= offsets.left ) { return false; } if ((!rect.width || !rect.height) && (e.textContent || !e.name)) { var arr = Array.prototype.slice.call(e.childNodes); var check = function(e) { return e instanceof Element && e.style.float != "none" && isVisible(e); }; if (!arr.some(check)) { return false; } } var s = win.getComputedStyle(e, null); return s.display !== "none" && s.visibility == "visible"; } var doc = win.document, res = xpath(doc, config.xpath), /* generate basic hint element which will be cloned and updated later */ labelTmpl = doc.createElement("span"), e, i; labelTmpl.setAttribute(attr, "label"); var containerOffsets = getOffsets(doc), offsetX = containerOffsets[0], offsetY = containerOffsets[1], fragment = doc.createDocumentFragment(), rect, label, text, showText, start = hints.length; /* collect all visible elements in hints array */ for (i = 0; i < res.snapshotLength; i++) { e = res.snapshotItem(i); if (!isVisible(e)) { continue; } rect = e.getClientRects()[0]; if (!rect) { continue; } count++; label = labelTmpl.cloneNode(false); label.style.display = "none"; label.style.left = Math.max(rect.left - 4, 0) + "px"; label.style.top = Math.max(rect.top - 4, 0) + "px"; /* create the hint label with number/letters */ /* if hinted element is an image - show title or alt of the image in hint label */ /* this allows to see how to filter for the image */ text = ""; showText = false; if (e instanceof HTMLImageElement) { text = e.title || e.alt; showText = true; } else if (e.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(e.textContent)) { text = e.firstElementChild.title || e.firstElementChild.alt; showText = true; } else if (e instanceof HTMLInputElement) { var type = e.type; if (type === "image") { text = e.alt || ""; } else if (e.value && type !== "password") { text = e.value; showText = (type === "radio" || type === "checkbox"); } } else if (e instanceof HTMLSelectElement) { if (e.selectedIndex >= 0) { text = e.item(e.selectedIndex).text; } } else { text = e.textContent; } /* add the hint class to the hinted element */ fragment.appendChild(label); e.setAttribute(attr, "hint"); hints.push({ e: e, label: label, text: text, showText: showText, __proto__: new Hint }); if (count >= config.maxHints) { break; } } /* append the fragment to the document */ var hDiv = doc.createElement("div"); hDiv.setAttribute(attr, "container"); hDiv.style.position = "fixed"; hDiv.style.top = "0"; hDiv.style.left = "0"; hDiv.style.zIndex = "2147483647"; hDiv.appendChild(fragment); if (doc.body) { doc.body.appendChild(hDiv); } docs.push({ doc: doc, start: start, end: hints.length - 1, div: hDiv }); /* recurse into any iframe or frame element */ for (i = 0; i < win.frames.length; i++) { try { var rect, f = win.frames[i], e = f.frameElement; } catch (ex) { continue; } if (isVisible(e)) { rect = e.getBoundingClientRect(); helper(f, { left: Math.max(offsets.left - rect.left, 0), right: Math.max(rect.right - offsets.right, 0), top: Math.max(offsets.top - rect.top, 0), bottom: Math.max(rect.bottom - offsets.bottom, 0) }); } } } helper(window); } function show(fireLast) { var i, hint, newIdx, matcher = getMatcher(filterText); var hintCount = 0, candidates = []; /* Check which hints match to the filter. */ for (i = 0; i < hints.length; i++) { hint = hints[i]; /* hide hints not matching the text filter */ if (!matcher(hint.text)) { hint.hide(); } else { hintCount++; candidates.push(hint); } } /* clear the array of valid hints */ validHints = []; /* Now we can assigne the hint labels and check if hose match. */ var labeler = config.getHintLabeler(hintCount); for (i = 0; i < candidates.length; i++) { hint = candidates[i]; /* assign the new hint number/letters as label to the hint */ hint.num = labeler(); /* check for hint-keys filter */ if (!filterKeys.length || hint.num.indexOf(filterKeys) == 0) { hint.show(); validHints.push(hint); } else { hint.hide(); } } if (fireLast && config.followLast && validHints.length <= 1) { focusHint(0); return fire(); } /* if the previous active hint isn't valid set focus to first */ if (!activeHint || validHints.indexOf(activeHint) < 0) { return focusHint(0); } } /* Returns a validator method to check if the hint elements text matches */ /* the given text filter. */ function getMatcher(text) { var tokens = text.toLowerCase().split(/\s+/); return function (itemText) { itemText = itemText.toLowerCase(); return tokens.every(function (token) { return 0 <= itemText.indexOf(token); }); }; } function getOffsets(doc) { var body = doc.body || doc.documentElement, style = body.style, rect; if (style && /^(absolute|fixed|relative)$/.test(style.position)) { rect = body.getClientRects()[0]; return [-rect.left, -rect.top]; } return [doc.defaultView.scrollX, doc.defaultView.scrollY]; } function focus(back) { var idx = validHints.indexOf(activeHint); /* previous active hint not found */ if (idx < 0) { idx = 0; } if (back) { if (--idx < 0) { idx = validHints.length - 1; } } else { if (++idx >= validHints.length) { idx = 0; } } return focusHint(idx); } function fire() { if (!activeHint) { return "ERROR:"; } var e = activeHint.e, res; /* process form actions like focus toggling inputs */ if (config.handleForm) { res = handleForm(e); } if (config.keepOpen) { /* reset the hint-keys filter */ filterKeys = ""; show(false); } else { clear(true); } return res || config.action(e); } /* focus or toggle form fields */ function handleForm(e) { var tag = e.nodeName.toLowerCase(), type = e.type || ""; if (tag === "input" || tag === "textarea" || tag === "select") { if (type === "radio" || type === "checkbox") { e.focus(); e.click(); return "DONE:"; } if (type === "submit" || type === "reset" || type === "button" || type === "image") { e.click(); return "DONE:"; } e.focus(); return "INSERT:"; } if (tag === "iframe" || tag === "frame") { e.focus(); return "DONE:"; } } /* internal used methods */ function open(e) { /* We call click() in async mode to return as fast as possible. If * we don't return immediately, the EvalJS dbus call will probably * timeout and cause errors. */ window.setTimeout(function() { var href; if ((href = e.getAttribute('href')) && href != '#') { window.location.href = href; } else { e.click(); } }, 0); } /* set focus on hint with given index valid hints array */ function focusHint(newIdx) { /* reset previous focused hint */ if (activeHint) { activeHint.unfocus(); mouseEvent(activeHint.e, "mouseout"); } /* get the new active hint */ if ((activeHint = validHints[newIdx])) { var e = activeHint.e; activeHint.focus(); mouseEvent(e, "mouseover"); return "OVER:" + (e instanceof HTMLImageElement ? "I:" : "A:") + getSrc(e); } } function mouseEvent(e, name) { var evObj = e.ownerDocument.createEvent("MouseEvents"); evObj.initMouseEvent( name, true, true, e.ownerDocument.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null ); e.dispatchEvent(evObj); } /* retrieves the url of given element */ function getSrc(e) { return e.href || e.src || ""; } function xpath(doc, expr) { return doc.evaluate( expr, doc, function (p) {return "http://www.w3.org/1999/xhtml";}, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); } function allFrames(win) { var i, f, frames = [win]; for (i = 0; i < win.frames.length; i++) { frames.push(win.frames[i].frameElement); } return frames; } function _labeler(keys, sameLength) { var kl = keys.length, /* Avoid don't consider the hint keys to be numeric in case the */ /* hintKeys='0' to avoid endless loop by attempt to use next */ /* hintKey char later. */ num = (keys[0] == '0' && kl > 1) ? 1 : 0, sl = sameLength; return function (count) { /* if hint keys starts with '0' count from 1 instead of 0 */ var hcount = num, offset = 0; function getMaxHintOfLen(len) { return (len > 0) ? ((kl - num) ** len) : 0; } /* We can generate same length label if there is only one hint key */ /* except in case there is only one hint. But we don't need to */ /* handle this. */ if (sl && kl > 1) { if (num) { offset = 1; /* increase starting point of hint numbers until there are */ /* enough available numbers */ while (offset * (kl - 1) < count) { offset *= kl; } offset--; } else { var val, labellen = 0, res = 0; /* Find hint string length to describe all hints with same length. */ while ((val = getMaxHintOfLen(labellen)) < count) { labellen++; res += val; } /* the offset-th hint string is the first one to use */ offset = res; } } return function () { /* Start on second hint key in incase of numeric hints. */ var res = [], n = hcount + offset; do { res.push(keys[n % kl]); n = ~~(n / kl); if (!num) { n--; } } while (n - num >= 0); hcount++; return res.reverse().join(""); }; }; } /* the api */ return { init: function(mode, keepOpen, maxHints, hintKeys, followLast, keysSameLength) { var prop, /* holds the xpaths for the different modes */ xpathmap = { otY: "//*[@href] | //*[@onclick or @tabindex or @class='lk' or @role='link' or @role='button'] | //input[not(@type='hidden' or @disabled or @readonly)] | //textarea[not(@disabled or @readonly)] | //button | //select | //summary", k: "//div", e: "//input[not(@type) or @type='text'] | //textarea", iI: "//img[@src]", OpPsTxy: "//*[@href] | //img[@src and not(ancestor::a)] | //iframe[@src]" }, /* holds the actions to perform on hint fire */ actionmap = { k: function(e) {e.remove(); return "DONE:";}, ot: function(e) {open(e); return "DONE:";}, eiIOpPsTxy: function(e) {return "DATA:" + getSrc(e);}, Y: function(e) {return "DATA:" + (e.textContent || "");} }; config = { maxHints: maxHints, keepOpen: keepOpen, /* handle forms only useful when there are form fields in xpath */ /* don't handle form for Y to allow to yank form filed content */ /* instead of switching to input mode */ handleForm: ("eot".indexOf(mode) >= 0), hintKeys: hintKeys, followLast: followLast, keysSameLength: keysSameLength, getHintLabeler: _labeler(hintKeys, keysSameLength) }; for (prop in xpathmap) { if (prop.indexOf(mode) >= 0) { config.xpath = xpathmap[prop]; break; } } for (prop in actionmap) { if (prop.indexOf(mode) >= 0) { config.action = actionmap[prop]; break; } } window.addEventListener("resize", onresize, true); window.addEventListener("scroll", onresize, false); for (var i = 0; i < window.frames.length; i++) { try { window.frames[i].frameElement.contentDocument.addEventListener("scroll", onresize, false); } catch (ex) { } } create(); return show(true); }, filter: function(text) { /* remove previously set hint-keys filters to make the filter */ /* easier to understand for the users */ filterKeys = ""; filterText = text || ""; return show(true); }, update: function(n) { var pos; /* delete last hint-keys filter digit */ if (null === n && filterKeys.length) { filterKeys = filterKeys.slice(0, -1); return show(false); } if ((pos = config.hintKeys.indexOf(n)) >= 0) { filterKeys = filterKeys + n; return show(true); } return "ERROR:"; }, clear: clear, fire: fire, focus: focus, }; })());