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