NoMouseGoogle

Shortcut for Google search results. j/k to move focus, enter/l/h to open in current/new/background tab.

  1. // ==UserScript==
  2. // @name NoMouseGoogle
  3. // @namespace com.gmail.fujifruity.greasemonkey
  4. // @version 1.17
  5. // @description Shortcut for Google search results. j/k to move focus, enter/l/h to open in current/new/background tab.
  6. // @author fujifruity
  7. // @include https://www.google.com/search*
  8. // @include https://www.google.co.*/search*
  9. // @grant GM.openInTab
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. {
  14. const tag = "noMouseGoogleCurrentItem";
  15. const itemQuery =
  16. "#res div[data-hveid][data-ved][lang], #botstuff div[data-hveid][data-ved][lang], #rso video-voyager>div";
  17. const findItems = () =>
  18. [...document.querySelectorAll(itemQuery)].filter(
  19. (e) => e.offsetParent != null /* is visible */
  20. );
  21. const findCurrentItem = (items) => items.find((e) => e.hasAttribute(tag));
  22. const moveCursor = (step) => {
  23. const items = findItems();
  24. const currentItem = findCurrentItem(items);
  25. if (!isVisible(currentItem, false)) {
  26. const dist = (e) => {
  27. const r = e.getBoundingClientRect();
  28. return Math.abs(window.innerHeight - (r.top + r.bottom));
  29. };
  30. const nearestItem = items.reduce((acc, e) =>
  31. dist(acc) < dist(e) ? acc : e
  32. );
  33. select(nearestItem, currentItem);
  34. return;
  35. }
  36. const nextIdx =
  37. (items.indexOf(currentItem) + step + items.length) % items.length;
  38. select(items[nextIdx], currentItem);
  39. };
  40. const isVisible = (item, fullyVisible) => {
  41. const rect = item.getBoundingClientRect();
  42. const isTopVisible = 0 < rect.top && rect.top < window.innerHeight;
  43. const isBottomVisible = 0 < rect.bottom && rect.bottom < window.innerHeight;
  44. return fullyVisible
  45. ? isTopVisible && isBottomVisible
  46. : isTopVisible || isBottomVisible;
  47. };
  48. const highlight = (e) => {
  49. const isDarkTheme = window.matchMedia?.(
  50. "(prefers-color-scheme: dark)"
  51. )?.matches;
  52. e.style.backgroundColor = isDarkTheme ? "#2a2a2a" : "WhiteSmoke";
  53. };
  54. const select = (item, currentItem) => {
  55. // Deselect current item.
  56. if (currentItem) {
  57. currentItem.style.backgroundColor = null;
  58. currentItem.removeAttribute(tag);
  59. }
  60. // Select the item.
  61. item.setAttribute(tag, "");
  62. highlight(item);
  63. // Scroll only if the item is not fully visible
  64. if (!isVisible(item, true)) {
  65. item.scrollIntoView({ behavior: "smooth", block: "center" });
  66. }
  67. console.log("select", item);
  68. };
  69.  
  70. const currentItemHref = () =>
  71. findCurrentItem(findItems()).querySelector("a").href;
  72. const openInNewTab = (inBackground) =>
  73. GM.openInTab(currentItemHref(), inBackground);
  74. const openInThisTab = () => window.open(currentItemHref(), "_self");
  75.  
  76. // Select the first item without scrolling.
  77. const items = findItems();
  78. items[0].setAttribute(tag, "");
  79. highlight(items[0]);
  80.  
  81. window.addEventListener("keydown", (event) => {
  82. if (
  83. ["INPUT", "TEXTAREA"].includes(event.target.tagName) ||
  84. event.ctrlKey ||
  85. event.altKey ||
  86. event.metaKey
  87. )
  88. return;
  89. if (event.key == "j") moveCursor(+1);
  90. if (event.key == "k") moveCursor(-1);
  91. if (event.key == "l") openInNewTab(false);
  92. if (event.key == "h") openInNewTab(true);
  93. if (event.key == "Enter") openInThisTab();
  94. });
  95. }