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-15 提交的版本,查看 最新版本

  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-15_15-5 // Не забывайте обновлять версию
  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,PHN2ZyB2aWV3Qm94PSIwIDAgNDggNDgiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+0JTQuNC9xpnbQuNC30LXRgNCw0Y8g0L/RgNC+0YDRgtC60Log0LrINC30L7RgNGN0YDPjwvdGl0bGU+PHN0eWxlPi5wYWdlIHsgZmlsbDogI2YwZjBmMDsgc3Ryb2tlOiAjMzMzOyBzdHJva2Utd2lkdGg6MjsgfSAuYW5jaG9yLXN5bWJvbCB7IGZpbGw6ICMzMzM7IGZvbnQtZmFtaWx5OiBzYW5zLXNlcmlmOyBmb250LXNpemU6IDE4cHg7IGZvbnQtd2VpZ2h0OiBib2xkOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyB9IC5hcnJvdyB7IGZpbGw6IG5vbmU7IHN0cm9rZTogIzMzMzsgc3Ryb2tlLXdpZHRoOjM7IHN0cm9rZS1saW5lY2FwOnJvdW5kOyBzdHJva2UtbGluZWpvaW46cm91bmQ7IH08L3N0eWxlPjxyZWN0IGNsYXNzPSJwYWdlIiB4PSIyLjYyODgzNCIgeT0iMi41NDIzNDkxIiB3aWR0aD0iNDIuNzc3MDg4IiBoZWlnaHQ9IjQyLjc3NzA4OCIgcng9IjIuMzU1MzE1MyIgLz48dGV4dCBjbGFzcz0iYW5jaG9yLXN5bWJvbCIgeD0iMjQuMDAwMDAyIiB5PSIzOS4zMzMzMjgiIHN0eWxlPSJmaWxsOiM0ZjRmZGQ7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOiMwMjAwNWE7c3Ryb2tlLW9wYWNpdHk6MSI+IzwvdGV4dD48cG9seWxpbmUgY2xhc3M9ImFycm93IiBwb2ludHM9IjMyLDE1IDMyLDM1IiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwwLjY3MzA0MzQ4LC03Ljk5OTk5OTksLTEuMzk5NDIwMykiIHN0eWxlPSJzdHJva2U6I2YwYWUxMztzdHJva2Utb3BhY2l0eToxIiAvPjxwb2x5bGluZSBjbGFzcz0iYXJyb3ciIHBvaW50cz0iMjYsMjggMzIsMzUgMzgsMjgiIHN0eWxlPSJzdHJva2U6I2YwYWUxMztzdHJva2Utb3BhY2l0eToxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNy45OTk5OTk5LC0xMy4xMDcwNikiIC8+PGxpbmUgeDE9IjEzLjEwNjY2NyIgeTE9IjguNTQ2NjY2MSIgeDI9IjM1LjEwNjY2MyIgeTI9IjguNTQ2NjY2MSIgc3Ryb2tlPSIjY2NjY2NjIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2U6I2YwYWUxMztzdHJva2Utb3BhY2l0eToxIiAvPjwvc3ZnPg==
  14. // @grant none
  15. // @run-at document-start
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // --- Определение среды выполнения ---
  22. let executionEnvironment = 'userscript'; // По умолчанию считаем, что это userscript
  23. let logPrefix = "[AutoScrollToAnchor]"; // Префикс для логов userscript
  24.  
  25. try {
  26. if (typeof browser !== 'undefined' && browser.runtime && browser.runtime.id) {
  27. executionEnvironment = 'extension_firefox';
  28. logPrefix = "[AutoScrollExt FF]";
  29. } else if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) {
  30. // Это может быть Chrome расширение или Firefox (где chrome является псевдонимом browser)
  31. // Для большей точности можно проверить chrome.runtime.getURL("").startsWith("moz-extension://") для Firefox
  32. if (chrome.runtime.getURL && chrome.runtime.getURL("").startsWith("moz-extension://")) {
  33. executionEnvironment = 'extension_firefox';
  34. logPrefix = "[AutoScrollExt FF]";
  35. } else {
  36. executionEnvironment = 'extension_chrome_or_edge'; // или другое на базе Chromium
  37. logPrefix = "[AutoScrollExt Cr]";
  38. }
  39. }
  40. } catch (e) {
  41. // Ошибка доступа к browser.runtime или chrome.runtime может возникнуть, если они не определены.
  42. // В этом случае остаемся со значением по умолчанию 'userscript'.
  43. }
  44. // ------------------------------------
  45.  
  46. // --- Попытка сохранить исходный якорь ---
  47. let initialAnchorOnLoad = window.location.hash.substring(1);
  48. if (initialAnchorOnLoad) {
  49. log(`Initial anchor detected at document-start: #${initialAnchorOnLoad}`);
  50. }
  51. // --------------------------------------------
  52.  
  53. // Настройки (остаются общими)
  54. const MAX_ATTEMPTS = 30;
  55. const SCROLL_INTERVAL_MS = 750;
  56. const SCROLL_AMOUNT_PX = window.innerHeight * 0.8;
  57. const FAST_CHECK_DELAY_MS = 250;
  58. const INITIAL_DELAY_MS = 500;
  59.  
  60. let currentIntervalId = null;
  61. let currentSearchAnchorName = '';
  62.  
  63. // Используем определенный ранее logPrefix
  64. function log(message) {
  65. console.log(`${logPrefix} ${message}`);
  66. }
  67. if (executionEnvironment !== 'userscript') { // Для расширений можно добавить ID расширения, если нужно
  68. log(`Running as ${executionEnvironment}. Extension ID (if applicable): ${ (typeof browser !== 'undefined' && browser.runtime && browser.runtime.id) || (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) || 'N/A'}`);
  69. } else {
  70. log(`Running as ${executionEnvironment}.`);
  71. }
  72.  
  73.  
  74. function stopCurrentSearch(reason = "generic stop") {
  75. if (currentIntervalId) {
  76. clearInterval(currentIntervalId);
  77. currentIntervalId = null;
  78. log(`Search for #${currentSearchAnchorName} stopped. Reason: ${reason}`);
  79. }
  80. }
  81.  
  82. // ... (функции log, stopCurrentSearch, findAndScrollToElement, startSearchingForAnchor) ...
  83. // Важно: findAndScrollToElement должна быть готова к тому, что currentUrlAnchor может быть пуст,
  84. // если сайт успел его удалить, но мы все еще ищем initialAnchorOnLoad.
  85.  
  86. /**
  87. * Модифицированная findAndScrollToElement
  88. * @param {string} anchorNameToFind - Имя якоря для поиска.
  89. * @param {string} currentExpectedUrlAnchor - Якорь, который мы ОЖИДАЕМ сейчас в URL (может быть '' если сайт его удалил).
  90. * @returns {boolean}
  91. */
  92. function findAndScrollToElement(anchorNameToFind, currentExpectedUrlAnchor) {
  93. if (!anchorNameToFind) return false;
  94.  
  95. // Проверка, не изменился ли ЦЕЛЕВОЙ якорь, к которому мы стремимся
  96. // (например, если пользователь кликнул на другой якорь уже после начала поиска)
  97. const actualCurrentUrlAnchor = window.location.hash.substring(1);
  98. if (actualCurrentUrlAnchor && actualCurrentUrlAnchor !== anchorNameToFind && actualCurrentUrlAnchor !== currentExpectedUrlAnchor) {
  99. log(`User navigated to a new anchor #${actualCurrentUrlAnchor} while searching for #${anchorNameToFind}. Stopping this search.`);
  100. return false; // Пользователь перешел на другой якорь, этот поиск неактуален
  101. }
  102.  
  103. const elementById = document.getElementById(anchorNameToFind);
  104. const elementByName = !elementById ? document.querySelector(`[name="${anchorNameToFind}"]`) : null;
  105. const targetElement = elementById || elementByName;
  106.  
  107. if (targetElement) {
  108. log(`Anchor #${anchorNameToFind} found.`);
  109. targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  110.  
  111. const originalBg = targetElement.style.backgroundColor;
  112. targetElement.style.backgroundColor = 'yellow';
  113. setTimeout(() => {
  114. targetElement.style.backgroundColor = originalBg;
  115. }, 2000);
  116.  
  117. return true;
  118. }
  119. return false;
  120. }
  121.  
  122. /**
  123. * Модифицированная startSearchingForAnchor
  124. * @param {string} anchorNameToSearch - Имя якоря для поиска.
  125. * @param {string} currentUrlAnchorAtStart - Якорь, который был в URL в момент инициации этого поиска
  126. */
  127. function startSearchingForAnchor(anchorNameToSearch, currentUrlAnchorAtStart) {
  128. stopCurrentSearch(`starting new search for #${anchorNameToSearch}`);
  129.  
  130. if (!anchorNameToSearch) {
  131. log("No anchor specified for search, nothing to do.");
  132. currentSearchAnchorName = '';
  133. return;
  134. }
  135.  
  136. currentSearchAnchorName = anchorNameToSearch; // Это якорь, который мы ИЩЕМ на странице
  137. log(`Starting search for anchor: #${currentSearchAnchorName} (URL hash was #${currentUrlAnchorAtStart || 'empty'} at initiation)`);
  138.  
  139. let attempts = 0;
  140.  
  141. if (findAndScrollToElement(currentSearchAnchorName, currentUrlAnchorAtStart)) {
  142. stopCurrentSearch(`found #${currentSearchAnchorName} immediately`);
  143. return;
  144. }
  145.  
  146. currentIntervalId = setInterval(() => {
  147. // Важно: передаем currentUrlAnchorAtStart, чтобы findAndScrollToElement
  148. // знала, какой якорь мы ожидали в URL на момент начала поиска.
  149. if (findAndScrollToElement(currentSearchAnchorName, currentUrlAnchorAtStart)) {
  150. stopCurrentSearch(`found #${currentSearchAnchorName} after scrolling`);
  151. return;
  152. }
  153.  
  154. attempts++;
  155. if (attempts > MAX_ATTEMPTS) {
  156. console.warn(`${logPrefix} Anchor #${currentSearchAnchorName} not found after ${MAX_ATTEMPTS} attempts.`);
  157. stopCurrentSearch(`max attempts reached for #${currentSearchAnchorName}`);
  158. return;
  159. }
  160.  
  161. log(`Attempt ${attempts}/${MAX_ATTEMPTS} for #${currentSearchAnchorName}: Scrolling down...`);
  162. window.scrollBy(0, SCROLL_AMOUNT_PX);
  163.  
  164. setTimeout(() => {
  165. if (!currentIntervalId) return;
  166. if (findAndScrollToElement(currentSearchAnchorName, currentUrlAnchorAtStart)) {
  167. stopCurrentSearch(`found #${currentSearchAnchorName} after scroll and fast check`);
  168. }
  169. }, FAST_CHECK_DELAY_MS);
  170. }, SCROLL_INTERVAL_MS);
  171. }
  172.  
  173. function initialLoadOrHashChangeHandler() {
  174. let anchorToActUpon = window.location.hash.substring(1);
  175. let currentUrlAnchorForContext = anchorToActUpon; // Якорь, который СЕЙЧАС в URL
  176.  
  177. if (!anchorToActUpon && initialAnchorOnLoad) {
  178. // Если в URL якоря нет, НО он был при самой первой загрузке
  179. log(`URL hash is empty, but an initial anchor #${initialAnchorOnLoad} was detected. Attempting to use it.`);
  180. anchorToActUpon = initialAnchorOnLoad;
  181. // currentUrlAnchorForContext остается пустым, так как сайт его удалил
  182. }
  183.  
  184. // Сброс initialAnchorOnLoad после первой попытки его использовать,
  185. // чтобы при последующих hashchange (если пользователь кликает по другим якорям на странице)
  186. // мы не пытались вернуться к самому первому якорю.
  187. // Но делаем это только если мы действительно собираемся действовать (т.е. anchorToActUpon не пуст)
  188. if (anchorToActUpon) {
  189. initialAnchorOnLoad = null; // Используем его только один раз
  190. }
  191.  
  192.  
  193. // Это условие для предотвращения перезапуска, если хеш не изменился, немного усложняется.
  194. // Мы должны сравнивать anchorToActUpon с тем, что мы активно ищем (currentSearchAnchorName).
  195. if (anchorToActUpon === currentSearchAnchorName && currentIntervalId !== null) {
  196. // Если мы уже ищем этот якорь (или пытались искать), и поиск активен, ничего не делаем.
  197. return;
  198. }
  199.  
  200. if (!anchorToActUpon && currentSearchAnchorName) {
  201. stopCurrentSearch(`Anchor removed or no longer relevant (was #${currentSearchAnchorName})`);
  202. currentSearchAnchorName = '';
  203. return;
  204. }
  205.  
  206. // Запускаем поиск, передавая якорь, который нужно найти,
  207. // и якорь, который был в URL в момент принятия решения о поиске.
  208. startSearchingForAnchor(anchorToActUpon, currentUrlAnchorForContext);
  209. }
  210.  
  211. function onPageReady() {
  212. // initialAnchorOnLoad уже должен быть установлен здесь
  213. log(`onPageReady. Initial anchor was: #${initialAnchorOnLoad || 'none'}. Current hash: #${window.location.hash.substring(1) || 'none'}`);
  214. setTimeout(initialLoadOrHashChangeHandler, INITIAL_DELAY_MS);
  215. window.addEventListener('hashchange', initialLoadOrHashChangeHandler, false);
  216. }
  217.  
  218. // Код для запуска при загрузке страницы
  219. // Эта логика подходит для обоих случаев, так как @run-at document-start и
  220. // "run_at": "document_start" в manifest.json ведут себя схожим образом.
  221. if (document.readyState === 'loading') {
  222. document.addEventListener('DOMContentLoaded', onPageReady, { once: true });
  223. } else {
  224. // DOMContentLoaded уже сработал, или мы находимся в состоянии interactive/complete
  225. onPageReady();
  226. }
  227.  
  228.  
  229. })();