VimJ

Vimium Mock

// ==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;
    }
})();