VimJ

Vimium Mock

Mint 2017.06.08.. Lásd a legutóbbi verzió

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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