VimJ

Vimium Mock

Per 08-06-2017. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         VimJ
// @namespace    VimJ
// @version      1.0
// @description  Vimium Mock
// @author       Jim
// @require      http://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.slim.min.js
// @match        *://*/*
// @grant        GM_openInTab
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';
    // Hook
    Element.prototype._addEventListener = Element.prototype.addEventListener;
    Element.prototype.addEventListener = function (type, listener, userCapture) {
        this._addEventListener(type, listener, userCapture);
        if (this.tagName.match(/^(DIV|I|LI)$/) && type.match(/(mouse|click)/)) {
            Page.clickElements.push(this);
        }
    };
    // Event
    $(window).on('click resize scroll', () => Page.escape());

    window ? register() : setTimeout(register, 0);
    function register() {
        addEventListener('keydown', (event) => {
            var isCommand = Page.isCommand(event);
            var activeElement = document.activeElement;

            if (event.code === 'Tab' && !tab()) {
                event.preventDefault();
                event.stopImmediatePropagation();
                isCommand ? Page.escape() : activeElement && activeElement.blur();
                document.body.click();
            } else if (isCommand) {
                event.stopImmediatePropagation();
            }

            function tab() {
                return activeElement && activeElement.tagName === 'INPUT' &&
                    (!activeElement.type || activeElement.type === 'text') &&
                    $(activeElement).closest('form').find('input[type="password"]').length;
            }
        }, true);

        addEventListener('keyup', (event) => {
            if (Page.isCommand(event)) {
                event.preventDefault();
                event.stopImmediatePropagation();
            }
        }, true);

        addEventListener('keypress', (event) => {
            if (Page.isCommand(event)) {
                event.preventDefault();
                event.stopImmediatePropagation();

                var char = String.fromCharCode(event.keyCode).toUpperCase();
                switch (char) {
                    case 'F':
                        $('._hint').length ? Page.match(char) : Page.linkHint();
                        break;

                    case 'J':
                        Page.scrollTop(200);
                        break;

                    case 'K':
                        Page.scrollTop(-200);
                        break;

                    case ' ':
                        Page.plus();
                        break;

                    default:
                        Page.match(char);
                        break;
                }
            }
        }, true);
    }

    $(`<style>
._plus{font-weight: bold}
._click{
box-shadow: 0 0 1px 1px gray;
pointer-events: none;
position: absolute;
z-index: 2147483648;
}
._hint{
background-color: rgba(173, 216, 230, 0.7);
border-radius: 3px;
box-shadow: 0 0 2px;
color: black;
font-family: monospace;
font-size: 13px;
position: fixed;
z-index: 2147483648;
}
</style>`).appendTo('html');

    var Page = {
        clickElements: [],
        chars: '',
        hintMap: {},
        isPlus: false,

        linkHint: () => {
            var elements = getElements();
            var hints = getHints(elements);
            Page.hintMap = popupHints(elements, hints);

            function getElements() {
                var elements = $('a, button, select, input, textarea, [role="button"], [contenteditable], [onclick]');
                var clickElements = $(Page.clickElements);
                return purify(elements, clickElements.add(clickElements.find('div')).addClass('_strict'));

                function purify(elements, clickElements) {
                    var length = 16;
                    var substitutes = [];

                    function isDisplayed(element) {
                        var style = getComputedStyle(element);
                        if (style.opacity === '0' || (element.classList.contains('_strict') &&
                            style.cursor.search(/pointer|text/) === -1)) {
                            return;
                        }

                        var rect = element.getClientRects()[0];
                        if (rect && rect.left >= 0 && rect.top >= 0 &&
                            rect.right <= innerWidth && rect.bottom <= innerHeight) {
                            element._left = rect.left;
                            element._top = rect.top;
                            var positions = [[element._left + rect.width / 3, element._top + rect.height / 3],
                                [
                                    Math.min(element._left + rect.width - 1, element._left + length),
                                    Math.min(element._top + rect.height - 1, element._top + length)
                                ]];

                            for (var i = 0; i < positions.length; i++) {
                                var targetElement = document.elementFromPoint(positions[i][0], positions[i][1]);
                                if (targetElement === element || element.contains(targetElement)) {
                                    return true;
                                }
                            }
                            if (element.tagName === 'INPUT' && targetElement.tagName !== 'INPUT') {
                                var a = xPath(element);
                                var b = xPath(targetElement);
                                if (a.substr(0, a.lastIndexOf('/')) === b.substr(0, b.lastIndexOf('/'))) {
                                    return true;
                                }
                            }
                            else if (element.tagName === 'A') {
                                substitutes.push(element);
                            }
                        }
                    }

                    elements = elements.filter((i, elem) => isDisplayed(elem));
                    clickElements = clickElements.filter((i, elem) => isDisplayed(elem));
                    clickElements = clickElements.add($(substitutes).find('> *').filter((i, elem) => isDisplayed(elem)));

                    var xTree = Tree.create(0, innerWidth);
                    var yTree = Tree.create(0, innerHeight);
                    elements = elements.get().reverse().filter(isExclusive);
                    clickElements = clickElements.get().reverse().filter(isExclusive);

                    function isExclusive(element) {
                        var overlapsX = $();
                        var overlapsY = $();

                        var leftTo = Math.min(element._left + length, xTree.to);
                        var topTo = Math.min(element._top + length, yTree.to);
                        Tree.search(xTree, element._left, leftTo, x => overlapsX = overlapsX.add(x));
                        Tree.search(yTree, element._top, topTo, y => overlapsY = overlapsY.add(y));

                        if (overlapsX.filter(overlapsY).length === 0) {
                            Tree.insert(xTree, element._left, leftTo, element);
                            Tree.insert(yTree, element._top, topTo, element);

                            overlapsY.map((i, elem) => {
                                if (Math.abs(element._top - elem._top) <= 5 &&
                                    Math.abs(element._left - elem._left) <= innerWidth / 10) {
                                    element._top = elem._top;
                                    return false;
                                }
                            });
                            return true;
                        }
                    }

                    return $(elements).add(clickElements);
                }
            }

            function getHints(elements) {
                var hints = [];
                var Y = 'ABCDEGHILM';
                var X = '1234567890';
                var B = 'NOPQRSTUVWXYZ' + Y + X;
                var lengthB = B.length;

                var all = {};
                for (var i = 0; i < B.length; i++) {
                    all[B.charAt(i)] = B;
                }

                for (i = 0; i < elements.length; i++) {
                    var element = elements[i];

                    var y = Y.charAt(Math.round(element._top / innerHeight * (Y.length - 1)));
                    var x = X.charAt(Math.round(element._left / innerWidth * (X.length - 1)));

                    if (all[y].length === 0) {
                        y = B.charAt(0);
                    }
                    if (!all[y].includes(x)) {
                        x = all[y].charAt(0);
                    }

                    all[y] = all[y].replace(x, '');
                    if (all[y] === '') {
                        B = B.replace(y, '');
                    }

                    hints.splice(Math.round(hints.length * 0.618 % 1 * hints.length), 0, y + x);
                }

                var availableChars = [];
                var singletonChars = [];
                for (i = 0; i < B.length; i++) {
                    var char = B.charAt(i);
                    if (all[char].length === lengthB) {
                        availableChars.push(char);
                    } else if (all[char].length === lengthB - 1) {
                        singletonChars.push(char);
                    }
                }

                for (i = 0; i < hints.length; i++) {
                    var startChar = hints[i].charAt(0);
                    if (singletonChars.includes(startChar)) {
                        hints[i] = startChar;
                    } else if (availableChars.length) {
                        hints[i] = availableChars.pop();
                        if ((all[startChar] += '.').length === lengthB - 1) {
                            singletonChars.push(startChar);
                        }
                    }
                }

                var singletonChar;
                var availableChar = 'F';
                for (i = 0; i < elements.length && availableChar === 'F'; i++) {
                    element = elements[i];

                    if ((element.tagName === 'INPUT' &&
                        element.type.search(/(button|checkbox|file|hidden|image|radio|reset|submit)/i) === -1)
                        || element.hasAttribute('contenteditable') || element.tagName === 'TEXTAREA') {
                        var hint = hints[i];
                        hints[i] = availableChar;
                        availableChar = hint;

                        startChar = hint.charAt(0);
                        if (availableChar.length > 1 && (all[startChar] += '.').length === lengthB - 1) {
                            singletonChar = startChar;
                        }
                    }
                }

                for (i = 0; availableChar.length === 1 && i < hints.length; i++) {
                    hint = hints[i];
                    if (hint.length > 1) {
                        hints[i] = availableChar;
                        availableChar = hint;

                        startChar = hint.charAt(0);
                        if ((all[startChar] += '.').length === lengthB - 1) {
                            singletonChar = startChar;
                        }
                    }
                }

                for (i = 0; singletonChar && i < hints.length; i++) {
                    if (hints[i].startsWith(singletonChar)) {
                        hints[i] = singletonChar;
                        break;
                    }
                }
                return hints;
            }

            function popupHints(elements, hints) {
                var map = {};
                for (var i = 0; i < elements.length; i++) {
                    var element = elements[i];
                    var hint = hints[i];
                    map[hint] = element;
                    var style = {
                        top: element._top,
                        left: element._left
                    };

                    $('<div class="_hint">' + hint + '</div>')
                        .css(style)
                        .appendTo('html');
                }
                return map;
            }
        },

        escape: () => {
            $('._hint').remove();
            Page.chars = '';
            Page.hintMap = {};
            Page.isPlus = false;
        },

        match: (char) => {
            var hints = $('._hint');
            if (hints.length) {
                Page.chars += char;

                var removeElements = [];
                hints = hints.filter((i, element) => {
                    if (element.innerText.startsWith(char)) {
                        return element.innerText = element.innerText.substr(-1);
                    } else {
                        removeElements.push(element);
                    }
                });
                $(removeElements).remove();

                if (hints.length === 1) {
                    var done;
                    var element = Page.hintMap[Page.chars];
                    if (Page.isPlus) {
                        if (element.tagName === 'A' && element.href) {
                            done = GM_openInTab(element.href, true);
                        } else {
                            for (var parent of $(element).parentsUntil(document.body)) {
                                if (parent.tagName === 'A' && parent.href) {
                                    done = GM_openInTab(parent.href, true);
                                    break;
                                }
                            }
                        }
                    }
                    if (!done) {
                        Page.click(element);
                    }

                    var rect = element.getBoundingClientRect();
                    var style = {
                        width: rect.width,
                        height: rect.height,
                        top: rect.top + window.pageYOffset,
                        left: rect.left + window.pageXOffset,
                    };
                    $('<div class="_click"></div>')
                        .css(style)
                        .appendTo('html');
                    setTimeout(() => $('._click').remove(), 500);
                    Page.escape();
                }
            }
        },

        scrollTop: (offset) => {
            var targets = Array
                .from(document.querySelectorAll('div'))
                .filter((elem) => elem.scrollHeight >= elem.clientHeight && getComputedStyle(elem).overflowY !== 'hidden')
                .sort((a, b) => a.scrollHeight > b.scrollHeight);

            if (typeof document.activeElement !== typeof document.scrollingElement) {
                if (document.scrollingElement.tagName.match(/^(DIV|BODY)$/)) targets.push(document.scrollingElement);
            } else {
                if (document.activeElement.tagName.match(/^(DIV|BODY)$/)) targets.push(document.activeElement);
            }

            for (var i = targets.length - 1; i >= 0; i--) {
                var target = targets[i];
                if ((target.scrollTop += 1) !== 1 || (target.scrollTop += -1) !== -1) {
                    return target.scrollTop += offset;
                }
            }
            scrollBy(0, offset);
        },

        plus: () => {
            Page.isPlus = !Page.isPlus;
            $('._hint').toggleClass('_plus');
        },

        click: (element) => {
            if ((element.tagName === 'INPUT' && element.type.search(/(button|checkbox|file|hidden|image|radio|reset|submit)/i) === -1)
                || element.hasAttribute('contenteditable') || element.tagName === 'TEXTAREA') {
                element.focus();
                if (element.setSelectionRange) {
                    try {
                        var len = element.value.length * 2;
                        element.setSelectionRange(len, len);
                    } catch (e) {
                    }
                }
            }
            else if (element.tagName === 'A' || element.tagName === 'INPUT') {
                element.click();
            }
            else {
                var names = ['mousedown', 'mouseup', 'click', 'mouseout'];
                for (var i = 0; i < names.length; i++) {
                    element.dispatchEvent(new MouseEvent(names[i], {bubbles: true}));
                }
            }
        },

        isCommand: (event) => {
            var element = document.activeElement;
            var isInput = element && !element.hasAttribute('readonly') && element.type !== 'checkbox' &&
                (element.tagName.match(/INPUT|TEXTAREA/) || element.hasAttribute('contenteditable'));

            var char = String.fromCharCode(event.keyCode).toUpperCase();
            var isUseful = $('._hint, ._click').length || 'FJK'.includes(char);

            return !event.ctrlKey && !isInput && isUseful;
        }
    };

    var Tree = {
        create: (from, to) => {
            return {
                from: Math.floor(from),
                to: Math.floor(to)
            };
        },

        getLeft: (node) => {
            if (node.left) {
                return node.left;
            } else {
                return node.left = Tree.create(node.from, Math.floor((node.from + node.to) / 2));
            }
        },

        getRight: (node) => {
            if (node.right) {
                return node.right;
            } else {
                return node.right = Tree.create(Math.floor((node.from + node.to) / 2) + 1, node.to);
            }
        },

        insert: (node, from, to, value) => {
            from = Math.floor(from);
            to = Math.floor(to);

            if (node.from === from && node.to === to) {
                if (node.values) {
                    return node.values.push(value);
                } else {
                    return node.values = [value];
                }
            }

            var mid = Math.floor((node.from + node.to) / 2);
            if (from < mid) {
                Tree.insert(Tree.getLeft(node), from, Math.min(to, mid), value);
            }
            if (to > mid) {
                Tree.insert(Tree.getRight(node), Math.max(from, mid + 1), to, value);
            }
        },

        search: (node, from, to, outPipe) => {
            from = Math.floor(from);
            to = Math.floor(to);

            if (node.from === from && node.to === to) {
                return include(node, outPipe);
            }
            if (node.values && node.values.length) {
                outPipe(node.values);
            }

            var mid = Math.floor((node.from + node.to) / 2);
            if (from < mid) {
                Tree.search(Tree.getLeft(node), from, Math.min(to, mid), outPipe);
            }
            if (to > mid) {
                Tree.search(Tree.getRight(node), Math.max(from, mid + 1), to, outPipe);
            }

            function include(node, outPipe) {
                if (node.values && node.values.length) {
                    outPipe(node.values);
                }
                if (node.left) {
                    include(node.left, outPipe);
                }
                if (node.right) {
                    include(node.right, outPipe);
                }
            }
        }
    };

    function xPath(node) {
        if (!(node && node.nodeType === 1)) {
            return '';
        }
        var count = 0;
        var siblings = node.parentNode.childNodes;
        for (var i = 0; i < siblings.length; i++) {
            var sibling = siblings[i];
            if (sibling.tagName === node.tagName) {
                count += 1;
            }
            if (sibling === node) {
                break;
            }
        }
        var suffix = count > 1 ? '[' + count + ']' : '';
        return xPath(node.parentNode) + '/' + node.tagName + suffix;
    }
})();