NoMouseGoogle

Shortcut for Google search results. j/k to move focus, enter/l/h to open in current/new/background tab, n/p to go to next/previous page.

目前為 2023-09-16 提交的版本,檢視 最新版本

  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, n/p to go to next/previous page.
  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 = "#res div[data-hveid][data-ved][lang], #botstuff div[data-hveid][data-ved][lang], #rso video-voyager>div"
  16. const findItems = () => [...document.querySelectorAll(itemQuery)]
  17. .filter(e => e.offsetParent != null /* is visible */)
  18. const findCurrentItem = (items) => items.find(e => e.hasAttribute(tag))
  19. const moveCursor = (step) => {
  20. const items = findItems()
  21. const currentItem = findCurrentItem(items)
  22. if (!isVisible(currentItem, false)) {
  23. const dist = (e) => {
  24. const r = e.getBoundingClientRect()
  25. return Math.abs(window.innerHeight - (r.top + r.bottom))
  26. }
  27. const nearestItem = items.reduce((acc, e) => dist(acc) < dist(e) ? acc : e)
  28. select(nearestItem, currentItem)
  29. return
  30. }
  31. const nextIdx = (items.indexOf(currentItem) + step + items.length) % items.length
  32. select(items[nextIdx], currentItem)
  33. }
  34. const isVisible = (item, fullyVisible) => {
  35. const rect = item.getBoundingClientRect();
  36. const isTopVisible = 0 < rect.top && rect.top < window.innerHeight
  37. const isBottomVisible = 0 < rect.bottom && rect.bottom < window.innerHeight
  38. return fullyVisible ? isTopVisible && isBottomVisible : isTopVisible || isBottomVisible
  39. }
  40. const highlight = (e) => {
  41. const isDarkTheme = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches
  42. e.style.backgroundColor = isDarkTheme ? '#2a2a2a' : 'WhiteSmoke'
  43. }
  44. const select = (item, currentItem) => {
  45. // Deselect current item.
  46. if (currentItem) {
  47. currentItem.style.backgroundColor = null
  48. currentItem.removeAttribute(tag)
  49. }
  50. // Select the item.
  51. item.setAttribute(tag, '')
  52. highlight(item)
  53. // Scroll only if the item is not fully visible
  54. if (!isVisible(item, true)) {
  55. item.scrollIntoView({ behavior: "smooth", block: "center" })
  56. }
  57. console.log('select', item)
  58. }
  59.  
  60. const currentItemHref = () => findCurrentItem(findItems()).querySelector('a').href
  61. const openInNewTab = (inBackground) => GM.openInTab(currentItemHref(), inBackground)
  62. const openInThisTab = () => window.open(currentItemHref(), "_self")
  63.  
  64. // Select the first item without scrolling.
  65. const items = findItems()
  66. items[0].setAttribute(tag, '')
  67. highlight(items[0])
  68.  
  69. window.addEventListener('keydown', event => {
  70. if (["INPUT", "TEXTAREA"].includes(event.target.tagName) || event.ctrlKey || event.altKey || event.metaKey) return
  71. if (event.key == 'j') moveCursor(+1)
  72. if (event.key == 'k') moveCursor(-1)
  73. if (event.key == 'l') openInNewTab(false)
  74. if (event.key == 'h') openInNewTab(true)
  75. if (event.key == 'Enter') openInThisTab()
  76. if (event.key == 'n') document.querySelector('#pnnext')?.click()
  77. if (event.key == 'p') document.querySelector('#pnprev')?.click()
  78. })
  79.  
  80. }