Scroll to Nearest Paragraph (← → keys)

Scroll to nearest paragraph using ← and → keys with 50px offset, starting from current scroll position if already scrolled down the page (no modifier keys required)

  1. // ==UserScript==
  2. // @name Scroll to Nearest Paragraph (← → keys)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  5. // @description Scroll to nearest paragraph using ← and → keys with 50px offset, starting from current scroll position if already scrolled down the page (no modifier keys required)
  6. // @author Işık Barış Fidaner
  7. // @match *://*/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. let paragraphs = [];
  15. let current = 0;
  16. let initialized = false;
  17. const offset = 50; // pixels above paragraph
  18.  
  19. function init() {
  20. paragraphs = Array.from(document.querySelectorAll('p'))
  21. .filter(p => p.offsetHeight > 0 && p.offsetParent !== null);
  22. setInitialIndex();
  23. initialized = true;
  24. }
  25.  
  26. function setInitialIndex() {
  27. const viewportTop = window.scrollY;
  28. for (let i = 0; i < paragraphs.length; i++) {
  29. const rect = paragraphs[i].getBoundingClientRect();
  30. const paragraphTop = window.scrollY + rect.top;
  31. if (paragraphTop - offset >= viewportTop) {
  32. current = i;
  33. return;
  34. }
  35. }
  36. current = paragraphs.length; // in case scrolled past all
  37. }
  38.  
  39. function scrollToParagraph(index) {
  40. if (index >= 0 && index < paragraphs.length) {
  41. const rect = paragraphs[index].getBoundingClientRect();
  42. const scrollY = window.scrollY + rect.top - offset;
  43.  
  44. window.scrollTo({
  45. top: Math.max(scrollY, 0),
  46. behavior: 'smooth'
  47. });
  48.  
  49. current = index;
  50. }
  51. }
  52.  
  53. document.addEventListener('keydown', (e) => {
  54. // Ignore keypresses in input fields or editable areas
  55. const target = e.target;
  56. if (
  57. target.tagName === 'INPUT' ||
  58. target.tagName === 'TEXTAREA' ||
  59. target.isContentEditable
  60. ) {
  61. return;
  62. }
  63.  
  64. if (!initialized) init();
  65.  
  66. if (e.key === 'ArrowRight') {
  67. e.preventDefault();
  68. if (current < paragraphs.length) {
  69. scrollToParagraph(current);
  70. current++;
  71. }
  72. } else if (e.key === 'ArrowLeft') {
  73. e.preventDefault();
  74. current = Math.max(current - 2, 0); // adjust for previous move
  75. scrollToParagraph(current);
  76. current++;
  77. }
  78. });
  79. })();