VimJ

Vimium Mock

Versione datata 08/06/2017. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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