Greasy Fork is available in English.

Auto Scroll to Bottom with Dynamic Loading

Automatically scroll to the bottom of the page, continue scrolling to trigger dynamic content loading, and stop after a configurable timeout if no new content is loaded. Supports multiple languages for better usability and searchability.

  1. // ==UserScript==
  2. // @name Auto Scroll to Bottom with Dynamic Loading
  3. // @name:zh-CN 自动滚动到底部(支持动态加载)
  4. // @name:en Auto Scroll to Bottom with Dynamic Loading
  5. // @name:es Desplazamiento Automático al Final con Carga Dinámica
  6. // @name:ja 自動スクロール(動的読み込み対応)
  7. // @namespace https://github.com/strangeZombies/PractiseCode
  8. // @version 1.5
  9. // @description Automatically scroll to the bottom of the page, continue scrolling to trigger dynamic content loading, and stop after a configurable timeout if no new content is loaded. Supports multiple languages for better usability and searchability.
  10. // @description:zh-CN 自动滚动到页面底部,持续滚动以触发动态内容加载,若无新内容则在可配置的超时时间后停止。支持多语言,提升使用和检索便捷性。
  11. // @description:en Automatically scroll to the bottom of the page, continue scrolling to trigger dynamic content loading, and stop after a configurable timeout if no new content is loaded. Supports multiple languages for better usability and searchability.
  12. // @description:es Desplaza automáticamente hasta el final de la página, continúa desplazándose para activar la carga de contenido dinámico y se detiene tras un tiempo configurable si no se carga nuevo contenido. Soporta múltiples idiomas para mejor usabilidad y búsqueda.
  13. // @description:ja ページの最下部まで自動でスクロールし、動的コンテンツの読み込みをトリガーするためにスクロールを継続し、新しいコンテンツが読み込まれない場合は設定可能なタイムアウト後に停止します。多言語対応で使いやすさと検索性を向上。
  14. // @author strangezombies
  15. // @match *://*/*
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant GM_addStyle
  19. // @keyword auto scroll, infinite scroll, dynamic loading, 自动滚动, 无限滚动, 动态加载, desplazamiento automático, carga dinámica, 自動スクロール, 動的読み込み
  20. // ==/UserScript==
  21.  
  22. // 由Grok自动生成
  23. (function() {
  24. 'use strict';
  25.  
  26. // Language translations
  27. const translations = {
  28. 'zh-CN': {
  29. startScroll: '开始滚动',
  30. stopScroll: '停止滚动',
  31. openConfig: '打开设置',
  32. closeConfig: '关闭',
  33. saveConfig: '保存',
  34. configTitle: '滚动设置',
  35. scrollStep: '滚动步长(像素)',
  36. scrollInterval: '滚动间隔(毫秒)',
  37. scrollBehavior: '滚动行为',
  38. scrollBehaviorSmooth: '平滑',
  39. scrollBehaviorAuto: '即时',
  40. panelPosition: '面板位置',
  41. panelPositionTopRight: '右上',
  42. panelPositionTopLeft: '左上',
  43. panelPositionBottomRight: '右下',
  44. panelPositionBottomLeft: '左下',
  45. maxScrollTimes: '最大滚动次数(0 为无限制)',
  46. timeoutSeconds: '底部超时(秒)',
  47. language: '语言',
  48. languageZhCN: '中文(简体)',
  49. languageEn: 'English',
  50. languageEs: 'Español',
  51. languageJa: '日本語',
  52. saveSuccess: '配置保存成功!',
  53. invalidScrollStep: '滚动步长至少为 1。',
  54. invalidScrollInterval: '滚动间隔至少为 10 毫秒。',
  55. invalidScrollBehavior: '无效的滚动行为。',
  56. invalidPanelPosition: '无效的面板位置。',
  57. invalidMaxScrollTimes: '最大滚动次数必须为 0 或更大。',
  58. invalidTimeoutSeconds: '超时时间至少为 1 秒。'
  59. },
  60. 'en': {
  61. startScroll: 'Start Scroll',
  62. stopScroll: 'Stop Scroll',
  63. openConfig: 'Open Config',
  64. closeConfig: 'Close',
  65. saveConfig: 'Save',
  66. configTitle: 'Scroll Settings',
  67. scrollStep: 'Scroll Step (px)',
  68. scrollInterval: 'Scroll Interval (ms)',
  69. scrollBehavior: 'Scroll Behavior',
  70. scrollBehaviorSmooth: 'Smooth',
  71. scrollBehaviorAuto: 'Auto',
  72. panelPosition: 'Panel Position',
  73. panelPositionTopRight: 'Top Right',
  74. panelPositionTopLeft: 'Top Left',
  75. panelPositionBottomRight: 'Bottom Right',
  76. panelPositionBottomLeft: 'Bottom Left',
  77. maxScrollTimes: 'Max Scroll Times (0 for unlimited)',
  78. timeoutSeconds: 'Timeout at Bottom (seconds)',
  79. language: 'Language',
  80. languageZhCN: 'Chinese (Simplified)',
  81. languageEn: 'English',
  82. languageEs: 'Spanish',
  83. languageJa: 'Japanese',
  84. saveSuccess: 'Configuration saved successfully!',
  85. invalidScrollStep: 'Scroll Step must be at least 1.',
  86. invalidScrollInterval: 'Scroll Interval must be at least 10ms.',
  87. invalidScrollBehavior: 'Invalid Scroll Behavior.',
  88. invalidPanelPosition: 'Invalid Panel Position.',
  89. invalidMaxScrollTimes: 'Max Scroll Times must be 0 or greater.',
  90. invalidTimeoutSeconds: 'Timeout must be at least 1 second.'
  91. },
  92. 'es': {
  93. startScroll: 'Iniciar Desplazamiento',
  94. stopScroll: 'Detener Desplazamiento',
  95. openConfig: 'Abrir Configuración',
  96. closeConfig: 'Cerrar',
  97. saveConfig: 'Guardar',
  98. configTitle: 'Configuración de Desplazamiento',
  99. scrollStep: 'Paso de Desplazamiento (px)',
  100. scrollInterval: 'Intervalo de Desplazamiento (ms)',
  101. scrollBehavior: 'Comportamiento de Desplazamiento',
  102. scrollBehaviorSmooth: 'Suave',
  103. scrollBehaviorAuto: 'Instantáneo',
  104. panelPosition: 'Posición del Panel',
  105. panelPositionTopRight: 'Arriba Derecha',
  106. panelPositionTopLeft: 'Arriba Izquierda',
  107. panelPositionBottomRight: 'Abajo Derecha',
  108. panelPositionBottomLeft: 'Abajo Izquierda',
  109. maxScrollTimes: 'Máximo de Desplazamientos (0 para ilimitado)',
  110. timeoutSeconds: 'Tiempo de Espera en el Fondo (segundos)',
  111. language: 'Idioma',
  112. languageZhCN: 'Chino (Simplificado)',
  113. languageEn: 'Inglés',
  114. languageEs: 'Español',
  115. languageJa: 'Japonés',
  116. saveSuccess: '¡Configuración guardada con éxito!',
  117. invalidScrollStep: 'El paso de desplazamiento debe ser al menos 1.',
  118. invalidScrollInterval: 'El intervalo de desplazamiento debe ser al menos 10 ms.',
  119. invalidScrollBehavior: 'Comportamiento de desplazamiento inválido.',
  120. invalidPanelPosition: 'Posición del panel inválida.',
  121. invalidMaxScrollTimes: 'El máximo de desplazamientos debe ser 0 o mayor.',
  122. invalidTimeoutSeconds: 'El tiempo de espera debe ser al menos 1 segundo.'
  123. },
  124. 'ja': {
  125. startScroll: 'スクロール開始',
  126. stopScroll: 'スクロール停止',
  127. openConfig: '設定を開く',
  128. closeConfig: '閉じる',
  129. saveConfig: '保存',
  130. configTitle: 'スクロール設定',
  131. scrollStep: 'スクロールステップ(ピクセル)',
  132. scrollInterval: 'スクロール間隔(ミリ秒)',
  133. scrollBehavior: 'スクロール動作',
  134. scrollBehaviorSmooth: 'スムーズ',
  135. scrollBehaviorAuto: '即時',
  136. panelPosition: 'パネル位置',
  137. panelPositionTopRight: '右上',
  138. panelPositionTopLeft: '左上',
  139. panelPositionBottomRight: '右下',
  140. panelPositionBottomLeft: '左下',
  141. maxScrollTimes: '最大スクロール回数(0で無制限)',
  142. timeoutSeconds: '底部でのタイムアウト(秒)',
  143. language: '言語',
  144. languageZhCN: '中国語(簡体)',
  145. languageEn: '英語',
  146. languageEs: 'スペイン語',
  147. languageJa: '日本語',
  148. saveSuccess: '設定が正常に保存されました!',
  149. invalidScrollStep: 'スクロールステップは1以上でなければなりません。',
  150. invalidScrollInterval: 'スクロール間隔は10ミリ秒以上でなければなりません。',
  151. invalidScrollBehavior: '無効なスクロール動作です。',
  152. invalidPanelPosition: '無効なパネル位置です。',
  153. invalidMaxScrollTimes: '最大スクロール回数は0以上でなければなりません。',
  154. invalidTimeoutSeconds: 'タイムアウトは1秒以上でなければなりません。'
  155. }
  156. };
  157.  
  158. // Default configuration
  159. const defaultConfig = {
  160. scrollStep: 100, // Pixels per scroll
  161. scrollInterval: 50, // Interval between scrolls (ms)
  162. scrollBehavior: 'smooth', // Scroll behavior: smooth or auto
  163. panelPosition: 'top-right', // Panel position
  164. maxScrollTimes: 0, // Max scroll actions, 0 for unlimited
  165. timeoutSeconds: 10, // Timeout in seconds before stopping at bottom
  166. language: 'zh-CN' // Default language
  167. };
  168.  
  169. // Load saved configuration or use defaults
  170. let config = {
  171. scrollStep: GM_getValue('scrollStep', defaultConfig.scrollStep),
  172. scrollInterval: GM_getValue('scrollInterval', defaultConfig.scrollInterval),
  173. scrollBehavior: GM_getValue('scrollBehavior', defaultConfig.scrollBehavior),
  174. panelPosition: GM_getValue('panelPosition', defaultConfig.panelPosition),
  175. maxScrollTimes: GM_getValue('maxScrollTimes', defaultConfig.maxScrollTimes),
  176. timeoutSeconds: GM_getValue('timeoutSeconds', defaultConfig.timeoutSeconds),
  177. language: GM_getValue('language', defaultConfig.language)
  178. };
  179.  
  180. let isScrolling = false;
  181. let scrollTimer = null;
  182. let scrollCount = 0;
  183. let lastScrollTime = 0;
  184. let atBottomSince = null;
  185. let lastScrollHeight = 0;
  186.  
  187. // Inject styles
  188. GM_addStyle(`
  189. #scrollControlPanel {
  190. position: fixed;
  191. ${getPanelPositionStyles(config.panelPosition)}
  192. background: #fff;
  193. border: 1px solid #ccc;
  194. padding: 10px;
  195. z-index: 10000;
  196. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  197. border-radius: 5px;
  198. }
  199. #scrollConfigPanel {
  200. position: fixed;
  201. top: 50%;
  202. left: 50%;
  203. transform: translate(-50%, -50%);
  204. background: #fff;
  205. border: 1px solid #ccc;
  206. padding: 20px;
  207. z-index: 10001;
  208. box-shadow: 0 4px 10px rgba(0,0,0,0.3);
  209. border-radius: 5px;
  210. display: none;
  211. width: 300px;
  212. color: #333;
  213. font-family: Arial, sans-serif;
  214. }
  215. #scrollControlPanel button, #scrollConfigPanel button {
  216. margin: 5px;
  217. padding: 5px 10px;
  218. cursor: pointer;
  219. background: #007bff;
  220. color: #fff;
  221. border: none;
  222. border-radius: 3px;
  223. font-size: 14px;
  224. }
  225. #scrollControlPanel button:hover, #scrollConfigPanel button:hover {
  226. background: #0056b3;
  227. }
  228. #scrollControlPanel .toggle-btn {
  229. background: #28a745;
  230. }
  231. #scrollControlPanel .toggle-btn.stop {
  232. background: #dc3545;
  233. }
  234. #scrollConfigPanel .close-btn {
  235. background: #6c757d;
  236. }
  237. #scrollConfigPanel label {
  238. display: block;
  239. margin: 10px 0;
  240. font-size: 14px;
  241. color: #333;
  242. }
  243. #scrollConfigPanel input, #scrollConfigPanel select {
  244. width: 100px;
  245. padding: 3px;
  246. margin-left: 5px;
  247. border: 1px solid #ccc;
  248. border-radius: 3px;
  249. color: #333;
  250. background: #f9f9f9;
  251. }
  252. #scrollConfigPanel input:hover, #scrollConfigPanel select:hover {
  253. background: #e0e0e0;
  254. }
  255. #scrollConfigPanel h3 {
  256. margin: 0 0 10px;
  257. font-size: 16px;
  258. color: #333;
  259. }
  260. `);
  261.  
  262. // Get styles for panel position
  263. function getPanelPositionStyles(position) {
  264. switch (position) {
  265. case 'top-left':
  266. return 'top: 20px; left: 20px;';
  267. case 'top-right':
  268. return 'top: 20px; right: 20px;';
  269. case 'bottom-left':
  270. return 'bottom: 20px; left: 20px;';
  271. case 'bottom-right':
  272. return 'bottom: 20px; right: 20px;';
  273. default:
  274. return 'top: 20px; right: 20px;';
  275. }
  276. }
  277.  
  278. // Get translation for current language
  279. function t(key) {
  280. return translations[config.language][key] || translations['en'][key];
  281. }
  282.  
  283. // Create control panel
  284. function createControlPanel() {
  285. const panel = document.createElement('div');
  286. panel.id = 'scrollControlPanel';
  287. panel.innerHTML = `
  288. <button id="toggleScrollBtn" class="toggle-btn">${t('startScroll')}</button>
  289. <button id="openConfigBtn">${t('openConfig')}</button>
  290. `;
  291. document.body.appendChild(panel);
  292.  
  293. // Bind events
  294. document.getElementById('toggleScrollBtn').addEventListener('click', toggleScroll);
  295. document.getElementById('openConfigBtn').addEventListener('click', openConfigPanel);
  296. }
  297.  
  298. // Create configuration panel
  299. function createConfigPanel() {
  300. const configPanel = document.createElement('div');
  301. configPanel.id = 'scrollConfigPanel';
  302. configPanel.innerHTML = `
  303. <h3>${t('configTitle')}</h3>
  304. <label>${t('scrollStep')}: <input type="number" id="scrollStepInput" value="${config.scrollStep}" min="1"></label>
  305. <label>${t('scrollInterval')}: <input type="number" id="scrollIntervalInput" value="${config.scrollInterval}" min="10"></label>
  306. <label>${t('scrollBehavior')}:
  307. <select id="scrollBehaviorSelect">
  308. <option value="smooth" ${config.scrollBehavior === 'smooth' ? 'selected' : ''}>${t('scrollBehaviorSmooth')}</option>
  309. <option value="auto" ${config.scrollBehavior === 'auto' ? 'selected' : ''}>${t('scrollBehaviorAuto')}</option>
  310. </select>
  311. </label>
  312. <label>${t('panelPosition')}:
  313. <select id="panelPositionSelect">
  314. <option value="top-right" ${config.panelPosition === 'top-right' ? 'selected' : ''}>${t('panelPositionTopRight')}</option>
  315. <option value="top-left" ${config.panelPosition === 'top-left' ? 'selected' : ''}>${t('panelPositionTopLeft')}</option>
  316. <option value="bottom-right" ${config.panelPosition === 'bottom-right' ? 'selected' : ''}>${t('panelPositionBottomRight')}</option>
  317. <option value="bottom-left" ${config.panelPosition === 'bottom-left' ? 'selected' : ''}>${t('panelPositionBottomLeft')}</option>
  318. </select>
  319. </label>
  320. <label>${t('maxScrollTimes')}: <input type="number" id="maxScrollTimesInput" value="${config.maxScrollTimes}" min="0"></label>
  321. <label>${t('timeoutSeconds')}: <input type="number" id="timeoutSecondsInput" value="${config.timeoutSeconds}" min="1"></label>
  322. <label>${t('language')}:
  323. <select id="languageSelect">
  324. <option value="zh-CN" ${config.language === 'zh-CN' ? 'selected' : ''}>${t('languageZhCN')}</option>
  325. <option value="en" ${config.language === 'en' ? 'selected' : ''}>${t('languageEn')}</option>
  326. <option value="es" ${config.language === 'es' ? 'selected' : ''}>${t('languageEs')}</option>
  327. <option value="ja" ${config.language === 'ja' ? 'selected' : ''}>${t('languageJa')}</option>
  328. </select>
  329. </label>
  330. <button id="saveConfigBtn">${t('saveConfig')}</button>
  331. <button id="closeConfigBtn" class="close-btn">${t('closeConfig')}</button>
  332. `;
  333. document.body.appendChild(configPanel);
  334.  
  335. // Bind events
  336. document.getElementById('saveConfigBtn').addEventListener('click', saveConfig);
  337. document.getElementById('closeConfigBtn').addEventListener('click', closeConfigPanel);
  338. }
  339.  
  340. // Open configuration panel
  341. function openConfigPanel() {
  342. const configPanel = document.getElementById('scrollConfigPanel');
  343. configPanel.style.display = 'block';
  344. }
  345.  
  346. // Close configuration panel
  347. function closeConfigPanel() {
  348. const configPanel = document.getElementById('scrollConfigPanel');
  349. configPanel.style.display = 'none';
  350. }
  351.  
  352. // Toggle scrolling
  353. function toggleScroll() {
  354. const toggleBtn = document.getElementById('toggleScrollBtn');
  355. if (isScrolling) {
  356. clearInterval(scrollTimer);
  357. isScrolling = false;
  358. atBottomSince = null;
  359. toggleBtn.textContent = t('startScroll');
  360. toggleBtn.classList.remove('stop');
  361. } else {
  362. scrollCount = 0;
  363. lastScrollHeight = document.documentElement.scrollHeight;
  364. atBottomSince = null;
  365. startScroll();
  366. isScrolling = true;
  367. toggleBtn.textContent = t('stopScroll');
  368. toggleBtn.classList.add('stop');
  369. }
  370. }
  371.  
  372. // Start scrolling
  373. function startScroll() {
  374. if (scrollTimer) clearInterval(scrollTimer);
  375.  
  376. scrollTimer = setInterval(() => {
  377. const now = Date.now();
  378. if (now - lastScrollTime < config.scrollInterval) return;
  379.  
  380. lastScrollTime = now;
  381. window.scrollBy({ top: config.scrollStep, behavior: config.scrollBehavior });
  382. scrollCount++;
  383.  
  384. // Check if max scroll times reached
  385. if (config.maxScrollTimes > 0 && scrollCount >= config.maxScrollTimes) {
  386. clearInterval(scrollTimer);
  387. isScrolling = false;
  388. atBottomSince = null;
  389. document.getElementById('toggleScrollBtn').textContent = t('startScroll');
  390. document.getElementById('toggleScrollBtn').classList.remove('stop');
  391. console.log('Reached max scroll times');
  392. return;
  393. }
  394.  
  395. // Check if at page bottom
  396. const isAtBottom = window.innerHeight + Math.ceil(window.scrollY) >= document.documentElement.scrollHeight;
  397. if (isAtBottom) {
  398. const currentScrollHeight = document.documentElement.scrollHeight;
  399. if (currentScrollHeight > lastScrollHeight) {
  400. // New content loaded, reset timeout
  401. lastScrollHeight = currentScrollHeight;
  402. atBottomSince = null;
  403. console.log('New content loaded, continuing scroll');
  404. } else {
  405. // No new content, start or continue timeout
  406. if (!atBottomSince) {
  407. atBottomSince = now;
  408. }
  409. if (now - atBottomSince >= config.timeoutSeconds * 1000) {
  410. // Timeout reached, stop scrolling
  411. clearInterval(scrollTimer);
  412. isScrolling = false;
  413. atBottomSince = null;
  414. document.getElementById('toggleScrollBtn').textContent = t('startScroll');
  415. document.getElementById('toggleScrollBtn').classList.remove('stop');
  416. console.log('Timeout reached, no new content, stopping scroll');
  417. return;
  418. }
  419. // Continue trying to scroll to trigger loading
  420. console.log('At bottom, attempting to trigger more content');
  421. }
  422. } else {
  423. // Not at bottom, reset timeout
  424. atBottomSince = null;
  425. }
  426. }, config.scrollInterval / 2);
  427. }
  428.  
  429. // Detect manual scrolling to pause auto-scroll
  430. let manualScrollTimeout = null;
  431. window.addEventListener('scroll', () => {
  432. if (isScrolling && Date.now() - lastScrollTime > config.scrollInterval * 2) {
  433. clearInterval(scrollTimer);
  434. clearTimeout(manualScrollTimeout);
  435. manualScrollTimeout = setTimeout(() => {
  436. if (isScrolling) startScroll();
  437. }, 1000);
  438. }
  439. });
  440.  
  441. // Save configuration
  442. function saveConfig() {
  443. const scrollStep = parseInt(document.getElementById('scrollStepInput').value);
  444. const scrollInterval = parseInt(document.getElementById('scrollIntervalInput').value);
  445. const scrollBehavior = document.getElementById('scrollBehaviorSelect').value;
  446. const panelPosition = document.getElementById('panelPositionSelect').value;
  447. const maxScrollTimes = parseInt(document.getElementById('maxScrollTimesInput').value);
  448. const timeoutSeconds = parseInt(document.getElementById('timeoutSecondsInput').value);
  449. const language = document.getElementById('languageSelect').value;
  450.  
  451. // Validate inputs
  452. if (isNaN(scrollStep) || scrollStep < 1) {
  453. alert(t('invalidScrollStep'));
  454. return;
  455. }
  456. if (isNaN(scrollInterval) || scrollInterval < 10) {
  457. alert(t('invalidScrollInterval'));
  458. return;
  459. }
  460. if (!['smooth', 'auto'].includes(scrollBehavior)) {
  461. alert(t('invalidScrollBehavior'));
  462. return;
  463. }
  464. if (!['top-right', 'top-left', 'bottom-right', 'bottom-left'].includes(panelPosition)) {
  465. alert(t('invalidPanelPosition'));
  466. return;
  467. }
  468. if (isNaN(maxScrollTimes) || maxScrollTimes < 0) {
  469. alert(t('invalidMaxScrollTimes'));
  470. return;
  471. }
  472. if (isNaN(timeoutSeconds) || timeoutSeconds < 1) {
  473. alert(t('invalidTimeoutSeconds'));
  474. return;
  475. }
  476. if (!['zh-CN', 'en', 'es', 'ja'].includes(language)) {
  477. alert('Invalid language.');
  478. return;
  479. }
  480.  
  481. // Update config
  482. config.scrollStep = scrollStep;
  483. config.scrollInterval = scrollInterval;
  484. config.scrollBehavior = scrollBehavior;
  485. config.panelPosition = panelPosition;
  486. config.maxScrollTimes = maxScrollTimes;
  487. config.timeoutSeconds = timeoutSeconds;
  488. config.language = language;
  489.  
  490. // Save to storage
  491. GM_setValue('scrollStep', config.scrollStep);
  492. GM_setValue('scrollInterval', config.scrollInterval);
  493. GM_setValue('scrollBehavior', config.scrollBehavior);
  494. GM_setValue('panelPosition', config.panelPosition);
  495. GM_setValue('maxScrollTimes', config.maxScrollTimes);
  496. GM_setValue('timeoutSeconds', config.timeoutSeconds);
  497. GM_setValue('language', config.language);
  498.  
  499. // Update panel position
  500. const controlPanel = document.getElementById('scrollControlPanel');
  501. controlPanel.style.cssText = `
  502. position: fixed;
  503. ${getPanelPositionStyles(config.panelPosition)}
  504. background: #fff;
  505. border: 1px solid #ccc;
  506. padding: 10px;
  507. z-index: 10000;
  508. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  509. border-radius: 5px;
  510. `;
  511.  
  512. // Close config panel
  513. closeConfigPanel();
  514.  
  515. // Recreate panels to update language
  516. document.getElementById('scrollControlPanel').remove();
  517. document.getElementById('scrollConfigPanel').remove();
  518. createControlPanel();
  519. createConfigPanel();
  520.  
  521. // Notify user
  522. alert(t('saveSuccess'));
  523.  
  524. // Restart scrolling if active
  525. if (isScrolling) {
  526. clearInterval(scrollTimer);
  527. scrollCount = 0;
  528. lastScrollHeight = document.documentElement.scrollHeight;
  529. atBottomSince = null;
  530. startScroll();
  531. }
  532. }
  533.  
  534. // Initialize
  535. createControlPanel();
  536. createConfigPanel();
  537. })();