Auto scroll to anchor on dynamic pages

Tries to scroll to an anchor on pages with dynamic content loading by repeatedly scrolling down. Handles hash changes.

当前为 2025-05-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Auto scroll to anchor on dynamic pages
  3. // @name:en Auto scroll to anchor on dynamic pages
  4. // @name:ru Автоматическая прокрутка к якорю на динамических страницах
  5. // @namespace http://tampermonkey.net/
  6. // @version 2025-05-14
  7. // @description Tries to scroll to an anchor on pages with dynamic content loading by repeatedly scrolling down. Handles hash changes.
  8. // @description:en Tries to scroll to an anchor on pages with dynamic content loading by repeatedly scrolling down. Handles hash changes.
  9. // @description:ru Пытается прокрутить до якоря на страницах с динамической загрузкой контента, многократно прокручивая вниз. Обрабатывает изменения хеша.
  10. // @author Igor Lebedev + (DeepSeek and Gemini Pro)
  11. // @license GPL-3.0-or-later
  12. // @match *://*/*
  13. // @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSIzNiIgaGVpZ2h0PSIzNiIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT7QlNC40L3FtYW3QuNC30LXRgNCw0Y8g0L/RgNC+0YDRgtC60Log0LrINC30L7RgNGN0YDPjwvdGl0bGU+PHN0eWxlPi5wYWdlIHsgZmlsbDogI2YwZjBmMDsgc3Ryb2tlOiAjMzMzOyBzdHJva2Utd2lkdGg6MjsgfSAuYW5jaG9yLXN5bWJvbCB7IGZpbGw6ICMzMzM7IGZvbnQtZmFtaWx5OiBzYW5zLXNlcmlmOyBmb250LXNpemU6IDE4cHg7IGZvbnQtd2VpZ2h0OiBib2xkOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyB9IC5hcnJvdyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogIzMzMzsgc3Ryb2tlLXdpZHRoOjM7IHN0cm9rZS1saW5lY2FwOnJvdW5kOyBzdHJva2UtbGluZWpvaW46cm91bmQ7IH08L3N0eWxlPjxyZWN0IGNsYXNzPSJwYWdlIiB4PSIyLjYyODgzNCIgeT0iMi41NDIzNDkxIiB3aWR0aD0iNDIuNzc3MDg4IiBoZWlnaHQ9IjQyLjc3NzA4OCIgcng9IjIuMzU1MzE1MyIvPjx0ZXh0IGNsYXNzPSJhbmNob3Itc3ltYm9sIiB4PSIyNC4wMDAwMDIiIHk9IjM5LjMzMzMyOCIgc3R5bGU9ImZpbGw6IzRmNGZkZDlmaWxsLW9wYWNpdHk6MTtzdHJva2U6IzAyMDA1YTtzdHJva2Utb3BhY2l0eToxIj4jPC90ZXh0Pjxwb2x5bGluZSBjbGFzcz0iYXJyb3ciIHBvaW50cz0iMzIsMTUgMzIsMzUiIHRyYW5zZm9ybT0ibWF0cml4KDEsMCwwLDAuNjczMDQzNDgsLTcuOTk5OTk5OSwtMS4zOTk0MjAzKSIgc3R5bGU9InN0cm9rZTojZjBhZTEzO3N0cm9rZS1vcGFjaXR5OjEiLz48cG9seWxpbmUgY2xhc3M9ImFycm93IiBwb2ludHM9IjI2LDI4IDMyLDM1IDM4LDI4IiBzdHlsZT0ic3Ryb2tlOiNmMGFlMTM7c3Ryb2tlLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTcuOTk5OTk5OSwtMTMuMTA3MDYpIi8+PGxpbmUgeDE9IjEzLjEwNjY2NyIgeTE9IjguNTQ2NjY2MSIgeDI9IjM1LjEwNjY2MyIgeTI9IjguNTQ2NjY2MSIgc3Ryb2tlPSIjY2NjY2NjIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2U6I2YwYWUxMztzdHJva2Utb3BhY2l0eToxIi8+PC9zdmc+
  14. // @grant none
  15. // @run-at document-start // Запускаем раньше, чтобы успеть повесить слушатели событий
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // Настройки
  22. const MAX_ATTEMPTS = 30; // Максимальное количество попыток прокрутки
  23. const SCROLL_INTERVAL_MS = 750; // Интервал между попытками в миллисекундах
  24. const SCROLL_AMOUNT_PX = window.innerHeight * 0.8; // На сколько прокручивать за раз (80% высоты окна)
  25. const FAST_CHECK_DELAY_MS = 250; // Задержка для быстрой проверки после скролла
  26. const INITIAL_DELAY_MS = 500; // Начальная задержка перед первым запуском
  27.  
  28. let currentIntervalId = null; // ID текущего интервала прокрутки
  29. let currentSearchAnchorName = ''; // Имя якоря, который активно ищется
  30.  
  31. function log(message) {
  32. console.log(`[AutoScrollToAnchor] ${message}`);
  33. }
  34.  
  35. /**
  36. * Останавливает текущий активный поиск якоря.
  37. * @param {string} reason - Причина остановки для логирования.
  38. */
  39. function stopCurrentSearch(reason = "generic stop") {
  40. if (currentIntervalId) {
  41. clearInterval(currentIntervalId);
  42. currentIntervalId = null;
  43. log(`Search for #${currentSearchAnchorName} stopped. Reason: ${reason}`);
  44. }
  45. // Сбрасываем имя искомого якоря, только если причина не в том, что он был найден
  46. // (чтобы подсветка могла использовать правильное имя)
  47. // Однако, для чистоты лучше всегда сбрасывать, а для подсветки передавать имя якоря отдельно.
  48. // currentSearchAnchorName = ''; // Решим ниже, когда сбрасывать
  49. }
  50.  
  51. /**
  52. * Пытается найти элемент якоря и прокрутить к нему.
  53. * @param {string} anchorName - Имя якоря для поиска.
  54. * @returns {boolean} - True, если элемент найден и прокрутка выполнена, иначе false.
  55. */
  56. function findAndScrollToElement(anchorName) {
  57. // Если URL хеш изменился, пока мы искали этот anchorName, значит этот поиск уже не актуален.
  58. const currentUrlAnchor = window.location.hash.substring(1);
  59. if (anchorName && currentUrlAnchor !== anchorName && currentUrlAnchor !== '') {
  60. log(`URL hash changed to #${currentUrlAnchor} while searching for #${anchorName}. Stopping this specific search.`);
  61. // Не останавливаем глобальный поиск здесь, это сделает обработчик hashchange
  62. return false; // Этот конкретный элемент искать больше не нужно
  63. }
  64.  
  65. if (!anchorName) return false; // Нет якоря для поиска
  66.  
  67. const elementById = document.getElementById(anchorName);
  68. const elementByName = !elementById ? document.querySelector(`[name="${anchorName}"]`) : null;
  69. const targetElement = elementById || elementByName;
  70.  
  71. if (targetElement) {
  72. log(`Anchor #${anchorName} found.`);
  73. targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  74.  
  75. // Опциональная подсветка
  76. const originalBg = targetElement.style.backgroundColor;
  77. targetElement.style.backgroundColor = 'yellow';
  78. setTimeout(() => {
  79. targetElement.style.backgroundColor = originalBg;
  80. }, 2000);
  81.  
  82. return true;
  83. }
  84. return false;
  85. }
  86.  
  87. /**
  88. * Запускает процесс поиска и прокрутки к указанному якорю.
  89. * @param {string} anchorNameToSearch - Имя якоря для поиска.
  90. */
  91. function startSearchingForAnchor(anchorNameToSearch) {
  92. stopCurrentSearch(`starting new search for #${anchorNameToSearch}`); // Останавливаем любой предыдущий поиск
  93.  
  94. if (!anchorNameToSearch) {
  95. log("No anchor specified in URL, nothing to do.");
  96. currentSearchAnchorName = ''; // Убедимся, что нет "зависшего" имени
  97. return;
  98. }
  99.  
  100. currentSearchAnchorName = anchorNameToSearch; // Устанавливаем новый искомый якорь
  101. log(`Starting search for anchor: #${currentSearchAnchorName}`);
  102.  
  103. let attempts = 0;
  104.  
  105. // Попытка найти сразу
  106. if (findAndScrollToElement(currentSearchAnchorName)) {
  107. stopCurrentSearch(`found #${currentSearchAnchorName} immediately`);
  108. // currentSearchAnchorName = ''; // Сбрасываем после успеха
  109. return;
  110. }
  111.  
  112. currentIntervalId = setInterval(() => {
  113. const currentUrlAnchorWhenIntervalFired = window.location.hash.substring(1);
  114. // Если хеш в URL изменился или был удален, пока работал интервал,
  115. // и он не соответствует тому, что мы ищем, останавливаем этот поиск.
  116. if (currentUrlAnchorWhenIntervalFired !== currentSearchAnchorName) {
  117. log(`URL hash changed to #${currentUrlAnchorWhenIntervalFired} (or removed) while actively searching for #${currentSearchAnchorName}. Stopping this search.`);
  118. stopCurrentSearch("URL hash changed during interval");
  119. // Новый поиск, если он нужен, будет инициирован событием hashchange
  120. return;
  121. }
  122.  
  123. if (findAndScrollToElement(currentSearchAnchorName)) {
  124. stopCurrentSearch(`found #${currentSearchAnchorName} after scrolling`);
  125. // currentSearchAnchorName = ''; // Сбрасываем после успеха
  126. return;
  127. }
  128.  
  129. attempts++;
  130. if (attempts > MAX_ATTEMPTS) {
  131. console.warn(`[AutoScrollToAnchor] Anchor #${currentSearchAnchorName} not found after ${MAX_ATTEMPTS} attempts.`);
  132. stopCurrentSearch(`max attempts reached for #${currentSearchAnchorName}`);
  133. // currentSearchAnchorName = ''; // Сбрасываем после неудачи
  134. return;
  135. }
  136.  
  137. log(`Attempt ${attempts}/${MAX_ATTEMPTS} for #${currentSearchAnchorName}: Scrolling down...`);
  138. window.scrollBy(0, SCROLL_AMOUNT_PX);
  139.  
  140. // Короткая проверка почти сразу после прокрутки
  141. setTimeout(() => {
  142. if (!currentIntervalId) return; // Поиск мог быть остановлен
  143.  
  144. const currentUrlAnchorForFastCheck = window.location.hash.substring(1);
  145. if (currentUrlAnchorForFastCheck !== currentSearchAnchorName) {
  146. // Если хеш изменился за время этой короткой задержки
  147. return;
  148. }
  149.  
  150. if (findAndScrollToElement(currentSearchAnchorName)) {
  151. stopCurrentSearch(`found #${currentSearchAnchorName} after scroll and fast check`);
  152. // currentSearchAnchorName = ''; // Сбрасываем после успеха
  153. }
  154. }, FAST_CHECK_DELAY_MS);
  155.  
  156. }, SCROLL_INTERVAL_MS);
  157. }
  158.  
  159. /**
  160. * Обработчик для первоначальной загрузки и изменения хеша URL.
  161. */
  162. function initialLoadOrHashChangeHandler() {
  163. const anchorNameFromUrl = window.location.hash.substring(1);
  164.  
  165. // Если якорь в URL тот же, что и текущий искомый, и поиск уже активен, ничего не делаем.
  166. // Это предотвращает лишние перезапуски, если hashchange сработало, но якорь не изменился.
  167. if (anchorNameFromUrl === currentSearchAnchorName && currentIntervalId !== null) {
  168. // log(`Hash event for the same active anchor #${anchorNameFromUrl}. No action needed.`);
  169. return;
  170. }
  171.  
  172. // Если в URL нет якоря, но какой-то поиск был активен, останавливаем его.
  173. if (!anchorNameFromUrl && currentSearchAnchorName) { // currentSearchAnchorName проверяем, чтобы не логировать без надобности
  174. stopCurrentSearch(`anchor removed from URL (was #${currentSearchAnchorName})`);
  175. currentSearchAnchorName = ''; // Явно сбрасываем
  176. return;
  177. }
  178.  
  179. // Во всех остальных случаях (новый якорь, или тот же якорь, но поиск неактивен, или якоря нет и не было)
  180. // запускаем/перезапускаем поиск (startSearchingForAnchor сама обработает пустой anchorNameFromUrl)
  181. startSearchingForAnchor(anchorNameFromUrl);
  182. }
  183.  
  184. // Устанавливаем слушателей
  185. // Используем setTimeout для initialLoadOrHashChangeHandler, чтобы дать странице "успокоиться"
  186. // даже если DOM готов или страница полностью загружена.
  187.  
  188. function onPageReady() {
  189. setTimeout(initialLoadOrHashChangeHandler, INITIAL_DELAY_MS);
  190. window.addEventListener('hashchange', initialLoadOrHashChangeHandler, false);
  191. }
  192.  
  193. if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) {
  194. // DOM уже готов или страница загружена
  195. onPageReady();
  196. } else {
  197. // Дожидаемся полной загрузки DOM
  198. document.addEventListener('DOMContentLoaded', onPageReady, { once: true });
  199. }
  200.  
  201. })();