VimJ

Vimium Mock

  1. // ==UserScript==
  2. // @name VimJ
  3. // @namespace VimJ
  4. // @version 1.0
  5. // @description Vimium Mock
  6. // @author Jim
  7. // @require http://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.2.1.slim.min.js
  8. // @match *://*/*
  9. // @grant GM_openInTab
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15. // Hook
  16. Element.prototype._addEventListener = Element.prototype.addEventListener;
  17. Element.prototype.addEventListener = function (type, listener, userCapture) {
  18. this._addEventListener(type, listener, userCapture);
  19. if (this.tagName.match(/^(DIV|I|LI)$/) && type.match(/(mouse|click)/)) {
  20. Page.clickElements.push(this);
  21. }
  22. };
  23. // Event
  24. $(window).on('click resize scroll', () => Page.escape());
  25.  
  26. window ? register() : setTimeout(register, 0);
  27. function register() {
  28. addEventListener('keydown', (event) => {
  29. var isCommand = Page.isCommand(event);
  30. var activeElement = document.activeElement;
  31.  
  32. if (event.code === 'Tab' && !tab()) {
  33. event.preventDefault();
  34. event.stopImmediatePropagation();
  35. isCommand ? Page.escape() : activeElement && activeElement.blur();
  36. document.body.click();
  37. } else if (isCommand) {
  38. event.stopImmediatePropagation();
  39. }
  40.  
  41. function tab() {
  42. return activeElement && activeElement.tagName === 'INPUT' &&
  43. (!activeElement.type || activeElement.type === 'text') &&
  44. $(activeElement).closest('form').find('input[type="password"]').length;
  45. }
  46. }, true);
  47.  
  48. addEventListener('keyup', (event) => {
  49. if (Page.isCommand(event)) {
  50. event.preventDefault();
  51. event.stopImmediatePropagation();
  52. }
  53. }, true);
  54.  
  55. addEventListener('keypress', (event) => {
  56. if (Page.isCommand(event)) {
  57. event.preventDefault();
  58. event.stopImmediatePropagation();
  59.  
  60. var char = String.fromCharCode(event.keyCode).toUpperCase();
  61. switch (char) {
  62. case 'F':
  63. $('._hint').length ? Page.match(char) : Page.linkHint();
  64. break;
  65.  
  66. case 'J':
  67. Page.scrollTop(200);
  68. break;
  69.  
  70. case 'K':
  71. Page.scrollTop(-200);
  72. break;
  73.  
  74. case ' ':
  75. Page.plus();
  76. break;
  77.  
  78. default:
  79. Page.match(char);
  80. break;
  81. }
  82. }
  83. }, true);
  84. }
  85.  
  86. $(`<style>
  87. ._plus{font-weight: bold}
  88. ._click{
  89. box-shadow: 0 0 1px 1px gray;
  90. pointer-events: none;
  91. position: absolute;
  92. z-index: 2147483648;
  93. }
  94. ._hint{
  95. background-color: rgba(173, 216, 230, 0.7);
  96. border-radius: 3px;
  97. box-shadow: 0 0 2px;
  98. color: black;
  99. font-family: monospace;
  100. font-size: 13px;
  101. position: fixed;
  102. z-index: 2147483648;
  103. }
  104. </style>`).appendTo('html');
  105.  
  106. var Page = {
  107. clickElements: [],
  108. chars: '',
  109. hintMap: {},
  110. isPlus: false,
  111.  
  112. linkHint: () => {
  113. var elements = getElements();
  114. var hints = getHints(elements);
  115. Page.hintMap = popupHints(elements, hints);
  116.  
  117. function getElements() {
  118. var elements = $('a, button, select, input, textarea, [role="button"], [contenteditable], [onclick]');
  119. var clickElements = $(Page.clickElements);
  120. return purify(elements, clickElements.add(clickElements.find('div')).addClass('_strict'));
  121.  
  122. function purify(elements, clickElements) {
  123. var length = 16;
  124. var substitutes = [];
  125.  
  126. function isDisplayed(element) {
  127. var style = getComputedStyle(element);
  128. if (style.opacity === '0' || (element.classList.contains('_strict') &&
  129. style.cursor.search(/pointer|text/) === -1)) {
  130. return;
  131. }
  132.  
  133. var rect = element.getClientRects()[0];
  134. if (rect && rect.left >= 0 && rect.top >= 0 &&
  135. rect.right <= innerWidth && rect.bottom <= innerHeight) {
  136. element._left = rect.left;
  137. element._top = rect.top;
  138. var positions = [[element._left + rect.width / 3, element._top + rect.height / 3],
  139. [
  140. Math.min(element._left + rect.width - 1, element._left + length),
  141. Math.min(element._top + rect.height - 1, element._top + length)
  142. ]];
  143.  
  144. for (var i = 0; i < positions.length; i++) {
  145. var targetElement = document.elementFromPoint(positions[i][0], positions[i][1]);
  146. if (targetElement === element || element.contains(targetElement)) {
  147. return true;
  148. }
  149. }
  150. if (element.tagName === 'INPUT' && targetElement.tagName !== 'INPUT') {
  151. var a = xPath(element);
  152. var b = xPath(targetElement);
  153. if (a.substr(0, a.lastIndexOf('/')) === b.substr(0, b.lastIndexOf('/'))) {
  154. return true;
  155. }
  156. }
  157. else if (element.tagName === 'A') {
  158. substitutes.push(element);
  159. }
  160. }
  161. }
  162.  
  163. elements = elements.filter((i, elem) => isDisplayed(elem));
  164. clickElements = clickElements.filter((i, elem) => isDisplayed(elem));
  165. clickElements = clickElements.add($(substitutes).find('> *').filter((i, elem) => isDisplayed(elem)));
  166.  
  167. var xTree = Tree.create(0, innerWidth);
  168. var yTree = Tree.create(0, innerHeight);
  169. elements = elements.get().reverse().filter(isExclusive);
  170. clickElements = clickElements.get().reverse().filter(isExclusive);
  171.  
  172. function isExclusive(element) {
  173. var overlapsX = $();
  174. var overlapsY = $();
  175.  
  176. var leftTo = Math.min(element._left + length, xTree.to);
  177. var topTo = Math.min(element._top + length, yTree.to);
  178. Tree.search(xTree, element._left, leftTo, x => overlapsX = overlapsX.add(x));
  179. Tree.search(yTree, element._top, topTo, y => overlapsY = overlapsY.add(y));
  180.  
  181. if (overlapsX.filter(overlapsY).length === 0) {
  182. Tree.insert(xTree, element._left, leftTo, element);
  183. Tree.insert(yTree, element._top, topTo, element);
  184.  
  185. overlapsY.map((i, elem) => {
  186. if (Math.abs(element._top - elem._top) <= 5 &&
  187. Math.abs(element._left - elem._left) <= innerWidth / 10) {
  188. element._top = elem._top;
  189. return false;
  190. }
  191. });
  192. return true;
  193. }
  194. }
  195.  
  196. return $(elements).add(clickElements);
  197. }
  198. }
  199.  
  200. function getHints(elements) {
  201. var hints = [];
  202. var Y = 'ABCDEGHILM';
  203. var X = '1234567890';
  204. var B = 'NOPQRSTUVWXYZ' + Y + X;
  205. var lengthB = B.length;
  206.  
  207. var all = {};
  208. for (var i = 0; i < B.length; i++) {
  209. all[B.charAt(i)] = B;
  210. }
  211.  
  212. for (i = 0; i < elements.length; i++) {
  213. var element = elements[i];
  214.  
  215. var y = Y.charAt(Math.round(element._top / innerHeight * (Y.length - 1)));
  216. var x = X.charAt(Math.round(element._left / innerWidth * (X.length - 1)));
  217.  
  218. if (all[y].length === 0) {
  219. y = B.charAt(0);
  220. }
  221. if (!all[y].includes(x)) {
  222. x = all[y].charAt(0);
  223. }
  224.  
  225. all[y] = all[y].replace(x, '');
  226. if (all[y] === '') {
  227. B = B.replace(y, '');
  228. }
  229.  
  230. hints.splice(Math.round(hints.length * 0.618 % 1 * hints.length), 0, y + x);
  231. }
  232.  
  233. var availableChars = [];
  234. var singletonChars = [];
  235. for (i = 0; i < B.length; i++) {
  236. var char = B.charAt(i);
  237. if (all[char].length === lengthB) {
  238. availableChars.push(char);
  239. } else if (all[char].length === lengthB - 1) {
  240. singletonChars.push(char);
  241. }
  242. }
  243.  
  244. for (i = 0; i < hints.length; i++) {
  245. var startChar = hints[i].charAt(0);
  246. if (singletonChars.includes(startChar)) {
  247. hints[i] = startChar;
  248. } else if (availableChars.length) {
  249. hints[i] = availableChars.pop();
  250. if ((all[startChar] += '.').length === lengthB - 1) {
  251. singletonChars.push(startChar);
  252. }
  253. }
  254. }
  255.  
  256. var singletonChar;
  257. var availableChar = 'F';
  258. for (i = 0; i < elements.length && availableChar === 'F'; i++) {
  259. element = elements[i];
  260.  
  261. if ((element.tagName === 'INPUT' &&
  262. element.type.search(/(button|checkbox|file|hidden|image|radio|reset|submit)/i) === -1)
  263. || element.hasAttribute('contenteditable') || element.tagName === 'TEXTAREA') {
  264. var hint = hints[i];
  265. hints[i] = availableChar;
  266. availableChar = hint;
  267.  
  268. startChar = hint.charAt(0);
  269. if (availableChar.length > 1 && (all[startChar] += '.').length === lengthB - 1) {
  270. singletonChar = startChar;
  271. }
  272. }
  273. }
  274.  
  275. for (i = 0; availableChar.length === 1 && i < hints.length; i++) {
  276. hint = hints[i];
  277. if (hint.length > 1) {
  278. hints[i] = availableChar;
  279. availableChar = hint;
  280.  
  281. startChar = hint.charAt(0);
  282. if ((all[startChar] += '.').length === lengthB - 1) {
  283. singletonChar = startChar;
  284. }
  285. }
  286. }
  287.  
  288. for (i = 0; singletonChar && i < hints.length; i++) {
  289. if (hints[i].startsWith(singletonChar)) {
  290. hints[i] = singletonChar;
  291. break;
  292. }
  293. }
  294. return hints;
  295. }
  296.  
  297. function popupHints(elements, hints) {
  298. var map = {};
  299. for (var i = 0; i < elements.length; i++) {
  300. var element = elements[i];
  301. var hint = hints[i];
  302. map[hint] = element;
  303. var style = {
  304. top: element._top,
  305. left: element._left
  306. };
  307.  
  308. $('<div class="_hint">' + hint + '</div>')
  309. .css(style)
  310. .appendTo('html');
  311. }
  312. return map;
  313. }
  314. },
  315.  
  316. escape: () => {
  317. $('._hint').remove();
  318. Page.chars = '';
  319. Page.hintMap = {};
  320. Page.isPlus = false;
  321. },
  322.  
  323. match: (char) => {
  324. var hints = $('._hint');
  325. if (hints.length) {
  326. Page.chars += char;
  327.  
  328. var removeElements = [];
  329. hints = hints.filter((i, element) => {
  330. if (element.innerText.startsWith(char)) {
  331. return element.innerText = element.innerText.substr(-1);
  332. } else {
  333. removeElements.push(element);
  334. }
  335. });
  336. $(removeElements).remove();
  337.  
  338. if (hints.length === 1) {
  339. var done;
  340. var element = Page.hintMap[Page.chars];
  341. if (Page.isPlus) {
  342. if (element.tagName === 'A' && element.href) {
  343. done = GM_openInTab(element.href, true);
  344. } else {
  345. for (var parent of $(element).parentsUntil(document.body)) {
  346. if (parent.tagName === 'A' && parent.href) {
  347. done = GM_openInTab(parent.href, true);
  348. break;
  349. }
  350. }
  351. }
  352. }
  353. if (!done) {
  354. Page.click(element);
  355. }
  356.  
  357. var rect = element.getBoundingClientRect();
  358. var style = {
  359. width: rect.width,
  360. height: rect.height,
  361. top: rect.top + window.pageYOffset,
  362. left: rect.left + window.pageXOffset,
  363. };
  364. $('<div class="_click"></div>')
  365. .css(style)
  366. .appendTo('html');
  367. setTimeout(() => $('._click').remove(), 500);
  368. Page.escape();
  369. }
  370. }
  371. },
  372.  
  373. scrollTop: (offset) => {
  374. var targets = Array
  375. .from(document.querySelectorAll('div'))
  376. .filter((elem) => elem.scrollHeight >= elem.clientHeight && getComputedStyle(elem).overflowY !== 'hidden')
  377. .sort((a, b) => a.scrollHeight > b.scrollHeight);
  378.  
  379. if (typeof document.activeElement !== typeof document.scrollingElement) {
  380. if (document.scrollingElement.tagName.match(/^(DIV|BODY)$/)) targets.push(document.scrollingElement);
  381. } else {
  382. if (document.activeElement.tagName.match(/^(DIV|BODY)$/)) targets.push(document.activeElement);
  383. }
  384.  
  385. for (var i = targets.length - 1; i >= 0; i--) {
  386. var target = targets[i];
  387. if ((target.scrollTop += 1) !== 1 || (target.scrollTop += -1) !== -1) {
  388. return target.scrollTop += offset;
  389. }
  390. }
  391. scrollBy(0, offset);
  392. },
  393.  
  394. plus: () => {
  395. Page.isPlus = !Page.isPlus;
  396. $('._hint').toggleClass('_plus');
  397. },
  398.  
  399. click: (element) => {
  400. if ((element.tagName === 'INPUT' && element.type.search(/(button|checkbox|file|hidden|image|radio|reset|submit)/i) === -1)
  401. || element.hasAttribute('contenteditable') || element.tagName === 'TEXTAREA') {
  402. element.focus();
  403. if (element.setSelectionRange) {
  404. try {
  405. var len = element.value.length * 2;
  406. element.setSelectionRange(len, len);
  407. } catch (e) {
  408. }
  409. }
  410. }
  411. else if (element.tagName === 'A' || element.tagName === 'INPUT') {
  412. element.click();
  413. }
  414. else {
  415. var names = ['mousedown', 'mouseup', 'click', 'mouseout'];
  416. for (var i = 0; i < names.length; i++) {
  417. element.dispatchEvent(new MouseEvent(names[i], {bubbles: true}));
  418. }
  419. }
  420. },
  421.  
  422. isCommand: (event) => {
  423. var element = document.activeElement;
  424. var isInput = element && !element.hasAttribute('readonly') && element.type !== 'checkbox' &&
  425. (element.tagName.match(/INPUT|TEXTAREA/) || element.hasAttribute('contenteditable'));
  426.  
  427. var char = String.fromCharCode(event.keyCode).toUpperCase();
  428. var isUseful = $('._hint, ._click').length || 'FJK'.includes(char);
  429.  
  430. return !event.ctrlKey && !isInput && isUseful;
  431. }
  432. };
  433.  
  434. var Tree = {
  435. create: (from, to) => {
  436. return {
  437. from: Math.floor(from),
  438. to: Math.floor(to)
  439. };
  440. },
  441.  
  442. getLeft: (node) => {
  443. if (node.left) {
  444. return node.left;
  445. } else {
  446. return node.left = Tree.create(node.from, Math.floor((node.from + node.to) / 2));
  447. }
  448. },
  449.  
  450. getRight: (node) => {
  451. if (node.right) {
  452. return node.right;
  453. } else {
  454. return node.right = Tree.create(Math.floor((node.from + node.to) / 2) + 1, node.to);
  455. }
  456. },
  457.  
  458. insert: (node, from, to, value) => {
  459. from = Math.floor(from);
  460. to = Math.floor(to);
  461.  
  462. if (node.from === from && node.to === to) {
  463. if (node.values) {
  464. return node.values.push(value);
  465. } else {
  466. return node.values = [value];
  467. }
  468. }
  469.  
  470. var mid = Math.floor((node.from + node.to) / 2);
  471. if (from < mid) {
  472. Tree.insert(Tree.getLeft(node), from, Math.min(to, mid), value);
  473. }
  474. if (to > mid) {
  475. Tree.insert(Tree.getRight(node), Math.max(from, mid + 1), to, value);
  476. }
  477. },
  478.  
  479. search: (node, from, to, outPipe) => {
  480. from = Math.floor(from);
  481. to = Math.floor(to);
  482.  
  483. if (node.from === from && node.to === to) {
  484. return include(node, outPipe);
  485. }
  486. if (node.values && node.values.length) {
  487. outPipe(node.values);
  488. }
  489.  
  490. var mid = Math.floor((node.from + node.to) / 2);
  491. if (from < mid) {
  492. Tree.search(Tree.getLeft(node), from, Math.min(to, mid), outPipe);
  493. }
  494. if (to > mid) {
  495. Tree.search(Tree.getRight(node), Math.max(from, mid + 1), to, outPipe);
  496. }
  497.  
  498. function include(node, outPipe) {
  499. if (node.values && node.values.length) {
  500. outPipe(node.values);
  501. }
  502. if (node.left) {
  503. include(node.left, outPipe);
  504. }
  505. if (node.right) {
  506. include(node.right, outPipe);
  507. }
  508. }
  509. }
  510. };
  511.  
  512. function xPath(node) {
  513. if (!(node && node.nodeType === 1)) {
  514. return '';
  515. }
  516. var count = 0;
  517. var siblings = node.parentNode.childNodes;
  518. for (var i = 0; i < siblings.length; i++) {
  519. var sibling = siblings[i];
  520. if (sibling.tagName === node.tagName) {
  521. count += 1;
  522. }
  523. if (sibling === node) {
  524. break;
  525. }
  526. }
  527. var suffix = count > 1 ? '[' + count + ']' : '';
  528. return xPath(node.parentNode) + '/' + node.tagName + suffix;
  529. }
  530. })();