Changed the way hints are processed in js (#53).
authorDaniel Carl <danielcarl@gmx.de>
Sun, 15 Dec 2013 20:33:30 +0000 (21:33 +0100)
committerDaniel Carl <danielcarl@gmx.de>
Wed, 18 Dec 2013 19:50:32 +0000 (20:50 +0100)
To allow to enable the new hint modes g;{MODE} it's better to filter the hint
texts without using xPath. So we filter only those items that are actual valid
and don't need to query the whole document for matching items.
On the other side, do we have more control about how the pattern matching is
done, case sensitive, matching word beginnings or fuzzy search or what ever.

src/hints.c
src/hints.js

index 8f575ca..a1c9429 100644 (file)
@@ -130,9 +130,13 @@ void hints_create(const char *input)
 
         run_script(js);
         g_free(js);
+
+        /* if hinting is started there won't be any aditional filter given and
+         * we can go out of this function */
+        return;
     }
 
-    js = g_strdup_printf("%s.create('%s');", HINT_VAR, input + 2);
+    js = g_strdup_printf("%s.filter('%s');", HINT_VAR, *(input + 2) ? input + 2 : "");
     run_script(js);
     g_free(js);
 }
index 666c8e6..a5e4b67 100644 (file)
@@ -1,18 +1,65 @@
 var VbHint = (function(){
     'use strict';
 
-    var hConts   = [],               /* holds the hintcontainers of the different documents */
-        hints    = [],               /* holds all hint data (hinted element, label, number) */
-        ixdFocus = 0,                /* index of current focused hint */
-        cId      = "_hintContainer", /* id of the conteiner holding the hint lables */
-        lClass   = "_hintLabel",     /* class used on the hint labels with the hint numbers */
-        hClass   = "_hintElem",      /* marks hinted elements */
-        fClass   = "_hintFocus",     /* marks focused element and focued hint */
-        config;
-
-    function create(inputText) {
-        clear();
+    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 = 1,                /* number of current focused hint in valid hints array */
+        filterText = "",               /* holds the typed filter text */
+        filterNum  = 0,                /* holds the numeric filter */
+        cId      = "_hintContainer",   /* id of the conteiner holding the hint lables */
+        lClass   = "_hintLabel",       /* class used on the hint labels with the hint numbers */
+        hClass   = "_hintElem",        /* marks hinted elements */
+        fClass   = "_hintFocus",       /* marks focused element and focued hint */
+        config,
+        style    = "." + lClass + "{" +
+            "-webkit-transform:translate(-4px,-4px);" +
+            "position:absolute;" +
+            "z-index:100000;" +
+            "font-family:monospace;" +
+            "font-weight:bold;" +
+            "font-size:10px;" +
+            "color:#000;" +
+            "background-color:#fff;" +
+            "margin:0;" +
+            "padding:0px 1px;" +
+            "border:1px solid #444;" +
+            "opacity:0.7" +
+            "}" +
+            "." + hClass + "{" +
+            "background-color:#ff0 !important;" +
+            "color:#000 !important" +
+            "}" +
+            "." + hClass + "." + fClass + "{" +
+            "background-color:#8f0 !important" +
+            "}" +
+            "." + lClass + "." + fClass + "{" +
+            "opacity:1" +
+            "}";
+
+    function clear() {
+        var i, j, hint, doc, e;
+        for (i = 0; i < docs.length; i++) {
+            doc = docs[i];
+            /* find all hinted elements vimbhint 'hint' */
+            var res = xpath(doc.doc, "//*[contains(@vimbhint, 'hint')]");
+            for (j = 0; j < res.snapshotLength; j++) {
+                e = res.snapshotItem(j);
+                e.removeAttribute("vimbhint");
+                e.classList.remove(fClass);
+                e.classList.remove(hClass);
+            }
+            doc.div.parentNode.removeChild(doc.div);
+        }
+        docs       = [];
+        hints      = [];
+        validHints = [];
+        filterText = "";
+        filterNum  = 0;
+        activeHint = 1;
+    }
 
+    function create() {
         var count = 0;
 
         function helper(win, offsets) {
@@ -43,24 +90,20 @@ var VbHint = (function(){
                 return s.display !== "none" && s.visibility == "visible";
             }
 
-            var doc   = win.document,
-                xpath = getXpath(inputText),
-                res   = doc.evaluate(
-                    xpath, doc,
-                    function (p) {return "http://www.w3.org/1999/xhtml";},
-                    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null
-                ),
+            var doc       = win.document,
+                res       = xpath(doc, getXpath()),
                 /* generate basic hint element which will be cloned and updated later */
                 labelTmpl = doc.createElement("span"),
                 e, i;
 
             labelTmpl.className = lClass;
+            labelTmpl.setAttribute("vimbhint", "label");
 
             var containerOffsets = getOffsets(doc),
                 offsetX = containerOffsets[0],
                 offsetY = containerOffsets[1],
                 fragment = doc.createDocumentFragment(),
-                rect, label, text;
+                rect, label, text, showText, start = hints.length;
 
             /* collect all visible elements in hints array */
             for (i = 0; i < res.snapshotLength; i++) {
@@ -76,28 +119,42 @@ var VbHint = (function(){
                 label = labelTmpl.cloneNode(false);
                 label.style.left = Math.max((rect.left + offsetX), offsetX) + "px";
                 label.style.top  = Math.max((rect.top  + offsetY), offsetY) + "px";
-                label.innerText  = count;
+                label.style.display = "none";
 
                 /* 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.alt || e.title;
+                    text     = e.title || e.alt;
+                    showText = true;
                 } else if (e.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(e.textContent)) {
-                    text = e.firstElementChild.title || e.firstElementChild.alt;
-                }
-                if (text) {
-                    label.innerText += ": " + text.substr(0, 20);
+                    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 */
-                e.classList.add(hClass);
                 fragment.appendChild(label);
+                e.setAttribute("vimbhint", "hint");
 
                 hints.push({
-                    e:     e,
-                    num:   count,
-                    label: label
+                    e:        e,
+                    label:    label,
+                    text:     text,
+                    showText: showText
                 });
 
                 if (count >= config.maxHints) {
@@ -109,11 +166,19 @@ var VbHint = (function(){
             var hDiv = doc.createElement("div");
             hDiv.id  = cId;
             hDiv.appendChild(fragment);
-            doc.documentElement.appendChild(hDiv);
+            hDiv.setAttribute("vimbhint", "container");
+            if (doc.body) {
+                doc.body.appendChild(hDiv);
+            }
             /* create the default style sheet */
             createStyle(doc);
 
-            hConts.push(hDiv);
+            docs.push({
+                doc:   doc,
+                start: start,
+                end:   hints.length - 1,
+                div:   hDiv
+            });
 
             /* recurse into any iframe or frame element */
             for (var f in win.frames) {
@@ -131,11 +196,59 @@ var VbHint = (function(){
         }
 
         helper(window);
+    }
+
+    function show() {
+        var i, hint, doc, num = 1, activeHint = filterNum || 1,
+            matcher = getMatcher(filterText);
+        /* clear the array of valid hints */
+        validHints = [];
+        for (i = 0; i < hints.length; i++) {
+            hint = hints[i];
+            /* collect only hints matching the filter text */
+            if (matcher(hint.text)) {
+                /* assigne the new hint number to the hint */
+                hint.num = num++;
+                setFocus(hint, activeHint === hint.num);
+                /* check for number filter */
+                if (!filterNum || 0 === hint.num.toString().indexOf(filterNum.toString())) {
+                    hint.label.style.display = "";
+                    hint.e.classList.add(hClass);
+
+                    /* create the label with the hint number */
+                    hint.label.innerText = hint.num;
+                    if (hint.showText && hint.text) {
+                        /* use \x20 instead of ' ' to keep this space during */
+                        /* js2h.sh processing */
+                        hint.label.innerText += ":\x20" + hint.text.substr(0, 20);
+                    }
+                    validHints.push(hint);
+                    continue;
+                }
+            }
+            /* remove hint labels from no more visible hints */
+            hint.label.style.display = "none";
+            hint.label.classList.remove(fClass);
+            hint.e.classList.remove(fClass);
+            hint.e.classList.remove(hClass);
+        }
 
-        if (count <= 1) {
-            return fire(0);
+        if (validHints.length === 1) {
+            return fire();
         }
-        return focusHint(0);
+        return focusHint(1, activeHint);
+    }
+
+    /* Retruns a vlidator method to check if the hint elemens text matches */
+    /* the given filter text. */
+    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) {
@@ -155,102 +268,28 @@ var VbHint = (function(){
             return;
         }
         var e = doc.createElement("style");
-        e.innerHTML += "." + lClass + "{" +
-            "-webkit-transform:translate(-4px,-4px);" +
-            "position:absolute;" +
-            "z-index:100000;" +
-            "font-family:monospace;" +
-            "font-weight:bold;" +
-            "font-size:10px;" +
-            "color:#000;" +
-            "background-color:#fff;" +
-            "margin:0;" +
-            "padding:0px 1px;" +
-            "border:1px solid #444;" +
-            "opacity:0.7" +
-            "}" +
-            "." + hClass + "{" +
-            "background-color:#ff0 !important;" +
-            "color:#000 !important" +
-            "}" +
-            "." + hClass + "." + fClass + "{" +
-            "background-color:#8f0 !important" +
-            "}" +
-            "." + lClass + "." + fClass + "{" +
-            "opacity:1" +
-            "}";
-
+        e.innerHTML = style;
         doc.head.appendChild(e);
         /* prevent us from adding the style multiple times */
         doc.hasStyle = true;
     }
 
     function focus(back) {
-        var n, i = ixdFocus;
+        var old = activeHint;
         if (back) {
-            n = (i >= 1) ? i - 1 : hints.length - 1;
-        } else {
-            n = (i + 1 < hints.length) ? i + 1 : 0;
-        }
-        return focusHint(n);
-    }
-
-    function update(n) {
-        var remove = [], idx, r, i, hint;
-        if (n === 0) {
-            return create();
-        }
-        /* remove none matching hints */
-        for (i = 0; i < hints.length; ++i) {
-            hint = hints[i];
-            /* collect the hints to be removed */
-            if (0 !== hint.num.toString().indexOf(n.toString())) {
-                remove.push(hint);
-            }
-        }
-
-        /* now remove the hints */
-        for (i = 0; i < remove.length; ++i) {
-            r = remove[i];
-            r.e.classList.remove(fClass);
-            r.e.classList.remove(hClass);
-            r.label.parentNode.removeChild(r.label);
-
-            /* remove hints from all hints */
-            if ((idx = hints.indexOf(r)) !== -1) {
-                hints.splice(idx, 1);
+            if (--activeHint < 1) {
+                activeHint = validHints.length;
             }
-        }
-
-
-        if (hints.length === 1) {
-            return fire(0);
-        }
-        return focusHint(0);
-    }
-
-    function clear() {
-        var i, hint;
-        if (hints.length === 0) {
-            return;
-        }
-        for (i = 0; i < hints.length; ++i) {
-            hint = hints[i];
-            if (hint.e) {
-                hint.e.classList.remove(fClass);
-                hint.e.classList.remove(hClass);
-                hint.label.parentNode.removeChild(hint.label);
+        } else {
+            if (++activeHint > validHints.length) {
+                activeHint = 1;
             }
         }
-        hints = [];
-        for (i = 0; i < hConts.length; ++i) {
-            hConts[i].parentNode.removeChild(hConts[i]);
-        }
-        hConts = [];
+        return focusHint(activeHint, old);
     }
 
-    function fire(i) {
-        var hint = getHint(i || ixdFocus);
+    function fire(num) {
+        var hint = validHints[(num || activeHint) - 1];
         if (!hint) {
             return "DONE:";
         }
@@ -300,21 +339,19 @@ var VbHint = (function(){
     }
 
     /* set focus on hint with given number */
-    function focusHint(i) {
+    function focusHint(newNum, oldNum) {
         /* reset previous focused hint */
         var hint;
-        if ((hint = getHint(ixdFocus))) {
-            hint.e.classList.remove(fClass);
-            hint.label.classList.remove(fClass);
+        if ((hint = validHints[oldNum - 1])) {
+            setFocus(hint, false);
 
             mouseEvent(hint.e, "mouseout");
         }
 
         /* mark new hint as focused */
-        ixdFocus = i;
-        if ((hint = getHint(i))) {
-            hint.e.classList.add(fClass);
-            hint.label.classList.add(fClass);
+        activeHint = newNum;
+        if ((hint = validHints[newNum - 1])) {
+            setFocus(hint, true);
 
             mouseEvent(hint.e, "mouseover");
 
@@ -323,13 +360,26 @@ var VbHint = (function(){
         }
     }
 
-    /* retrieves the hint for given hint number */
-    function getHint(i) {
-        return hints[i] || null;
+    /* Toggles the focus highlight of a hint */
+    function setFocus(hint, active) {
+        if (active) {
+            /* TODO place and remove the active value independent from 'hint' */
+            hint.e.setAttribute("vimbhint", "hint active");
+            hint.e.classList.add(fClass);
+            hint.label.setAttribute("vimbhint", "label active");
+            hint.label.classList.add(fClass);
+        } else {
+            hint.e.setAttribute("vimbhint", "hint");
+            hint.e.classList.remove(fClass);
+            hint.label.setAttribute("vimbhint", "label");
+            hint.label.classList.remove(fClass);
+        }
     }
 
     function click(e) {
+        /* for sites that are interested in mouseover before click */
         mouseEvent(e, "mouseover");
+        /* this is the w3.org definition for DOM-Level 2 events */
         mouseEvent(e, "mousedown");
         mouseEvent(e, "mouseup");
         mouseEvent(e, "click");
@@ -347,51 +397,24 @@ var VbHint = (function(){
     }
 
     /* retrieves the xpath expression according to mode */
-    function getXpath(s) {
-        if (s === undefined) {
-            s = "";
-        }
-        /* replace $WHAT in xpath to contains(translate(WHAT, 'SEARCH', 'search'), 'search') */
-        function buildQuery(what, x, s) {
-            var l, i, parts;
-            l = s.toLowerCase();
-            parts = [
-                "contains(translate(",
-                ",'"+s.toUpperCase()+"','"+l+"'),'"+l+"')"
-            ];
-            for (i = 0; i < what.length; ++i) {
-                x = x.split("$" + what[i]).join(parts.join(what[i]));
-            }
-            return x;
-        }
-
+    function getXpath() {
         switch (config.mode) {
             case "l":
-                if (!s) {
-                    return "//*[@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";
-                }
-                return buildQuery(
-                    ["@value", ".", "@placeholder", "@title", "@alt"],
-                    "//*[@href and ($. or child::img[$@title or $@alt])] | //*[(@onclick or @class='lk' or @role='link' or role='button') and $.] | //input[not(@type='hidden' or @disabled or @readonly) and ($@value or $@placeholder)] | //textarea[not(@disabled or @readonly) and $.] | //button[$.] | //select[$.]",
-                    s
-                );
+                return "//*[@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";
             case "e":
-                if (!s) {
-                    return "//input[not(@type) or @type='text'] | //textarea";
-                }
-                return buildQuery(
-                    ["@value", ".", "@placeholder"],
-                    "//input[(not(@type) or @type='text') and ($@value or $@placeholder)] | //textarea[$.]",
-                    s
-                );
+                return "//input[not(@type) or @type='text'] | //textarea";
             case "i":
-                if (!s) {
-                    return "//img[@src]";
-                }
-                return buildQuery(["@title", "@alt"], "//img[@src and ($@title or $@alt)]", s);
+                return "//img[@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
+        );
+    }
+
     /* follow the count last link on pagematching the given pattern */
     function followLink(rel, pattern, count) {
         /* returns array of matching elements */
@@ -478,9 +501,17 @@ var VbHint = (function(){
                 config.mode  = map[prefix][0];
                 config.usage = map[prefix][1];
             }
+            create();
+            return show();
+        },
+        filter: function filter(text) {
+            filterText = text || "";
+            return show();
+        },
+        update: function update(n) {
+            filterNum = n;
+            return show();
         },
-        create:     create,
-        update:     update,
         clear:      clear,
         fire:       fire,
         focus:      focus,