Scroll to Top Button

Adds a customizable scroll-to-top button near the page bottom.

  1. // ==UserScript==
  2. // @name Scroll to Top Button
  3. // @namespace sttb-ujs-dxrk1e
  4. // @description Adds a customizable scroll-to-top button near the page bottom.
  5. // @icon https://i.imgur.com/FxF8TLS.png
  6. // @match *://*/*
  7. // @grant none
  8. // @version 3.1.0
  9. // @author DXRK1E
  10. // @license MIT
  11. // @noframes
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const _cfg = {
  18. b: {
  19. sz: '45px', fs: '18px', bg: '#3a3a3a', hBg: '#555', clr: '#f5f5f5',
  20. br: '50%', pos: { b: '25px', r: '25px' }, sh: '0 4px 12px rgba(0,0,0,0.4)',
  21. trMs: 300, z: 2147483647,
  22. svg: { w: '20px', h: '20px', vb: '0 0 16 16', pd: 'M8 3L14 9L12.6 10.4L8 5.8L3.4 10.4L2 9L8 3Z' },
  23. lbl: 'Scroll to Top'
  24. },
  25. bh: { shThrPx: 300, dDelMs: 150, smScr: true, natSmScr: false },
  26. sc: { durMs: 800, eas: 'easeInOutCubic' }
  27. };
  28.  
  29. const _eas = {
  30. linear: t => t, easeInQuad: t => t * t, easeOutQuad: t => t * (2 - t),
  31. easeInOutQuad: t => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  32. easeInCubic: t => t * t * t, easeOutCubic: t => (--t) * t * t + 1,
  33. easeInOutCubic: t => t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
  34. easeInQuart: t => t * t * t * t, easeOutQuart: t => 1 - (--t) * t * t * t,
  35. easeInOutQuart: t => t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
  36. easeInQuint: t => t * t * t * t * t, easeOutQuint: t => 1 + (--t) * t * t * t * t,
  37. easeInOutQuint: t => t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t,
  38. easeInExpo: t => (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)),
  39. easeOutExpo: t => (t === 1) ? 1 : 1 - Math.pow(2, -10 * t),
  40. easeInOutExpo: t => t === 0 ? 0 : t === 1 ? 1 : t < .5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2
  41. };
  42.  
  43. const _bid = 'estb-dxrk1e-s';
  44. const _sid = 'estb-styles-dxrk1e-s';
  45.  
  46. let _btn = null;
  47. let _sto = null;
  48. let _raf = null;
  49.  
  50. function _gSP() { return window.scrollY || document.documentElement.scrollTop; }
  51.  
  52. function _deb(fn, wt) {
  53. return function (...a) {
  54. clearTimeout(_sto);
  55. _sto = setTimeout(() => { fn.apply(this, a); }, wt);
  56. };
  57. }
  58.  
  59. function _gEF() { return _eas[_cfg.sc.eas] || _eas.linear; }
  60.  
  61. function _injS() {
  62. if (document.getElementById(_sid)) return;
  63. const css = `
  64. #${_bid}{position:fixed;bottom:${_cfg.b.pos.b};right:${_cfg.b.pos.r};width:${_cfg.b.sz};height:${_cfg.b.sz};background-color:${_cfg.b.bg};color:${_cfg.b.clr};border:none;border-radius:${_cfg.b.br};cursor:pointer;box-shadow:${_cfg.b.sh};opacity:0;visibility:hidden;z-index:${_cfg.b.z};transition:opacity ${_cfg.b.trMs}ms ease-in-out,visibility ${_cfg.b.trMs}ms ease-in-out,background-color ${_cfg.b.trMs}ms ease-in-out,transform ${_cfg.b.trMs}ms ease-in-out;display:flex;align-items:center;justify-content:center;padding:0;transform:scale(1);outline:none;will-change:opacity,transform;overflow:hidden;}
  65. #${_bid}:hover{background-color:${_cfg.b.hBg};transform:scale(1.1);}
  66. #${_bid}:active{transform:scale(0.95);}
  67. #${_bid}.visible{opacity:1;visibility:visible;}
  68. #${_bid} svg{display:block;width:${_cfg.b.svg.w};height:${_cfg.b.svg.h};fill:currentColor;}
  69. `;
  70. const se = document.createElement('style');
  71. se.id = _sid; se.textContent = css;
  72. (document.head || document.documentElement).appendChild(se);
  73. }
  74.  
  75. function _crB() {
  76. const b = document.createElement('button');
  77. b.id = _bid; b.setAttribute('aria-label', _cfg.b.lbl); b.setAttribute('title', _cfg.b.lbl); b.type = 'button';
  78. b.innerHTML = `<svg width="${_cfg.b.svg.w}" height="${_cfg.b.svg.h}" viewBox="${_cfg.b.svg.vb}" xmlns="http://www.w3.org/2000/svg"><path d="${_cfg.b.svg.pd}" /></svg>`;
  79. b.addEventListener('click', (e) => { e.preventDefault(); _scT(); });
  80. return b;
  81. }
  82.  
  83. function _smS() {
  84. const sPos = _gSP(); if (sPos <= 0) return;
  85. const sT = performance.now(); const dur = _cfg.sc.durMs; const easing = _gEF();
  86. if (_raf) { cancelAnimationFrame(_raf); }
  87. function step(cT) {
  88. const el = cT - sT; const prog = Math.min(el / dur, 1);
  89. const eP = easing(prog); const nPos = sPos * (1 - eP);
  90. window.scrollTo(0, nPos);
  91. if (prog < 1) { _raf = requestAnimationFrame(step); } else { _raf = null; }
  92. }
  93. _raf = requestAnimationFrame(step);
  94. }
  95.  
  96. function _scT() {
  97. if (_cfg.bh.smScr) {
  98. if (_cfg.bh.natSmScr && 'scrollBehavior' in document.documentElement.style) {
  99. window.scrollTo({ top: 0, behavior: 'smooth' });
  100. } else { _smS(); }
  101. } else { window.scrollTo({ top: 0, behavior: 'auto' }); }
  102. }
  103.  
  104. function _hSE() {
  105. if (!_btn) return;
  106. const sPos = _gSP();
  107. if (sPos > _cfg.bh.shThrPx) { _btn.classList.add('visible'); }
  108. else { _btn.classList.remove('visible'); }
  109. }
  110.  
  111. function _init() {
  112. if (document.getElementById(_bid) || !document.body) return;
  113. try {
  114. _injS(); _btn = _crB(); document.body.appendChild(_btn);
  115. const dBounce = _deb(_hSE, _cfg.bh.dDelMs);
  116. window.addEventListener('scroll', dBounce, { passive: true });
  117. window.addEventListener('resize', dBounce, { passive: true });
  118. const mObs = new MutationObserver(dBounce);
  119. mObs.observe(document.body, { childList: true, subtree: true, attributes: false });
  120. _hSE();
  121. } catch (e) { console.error("STTB Error:", e); }
  122. }
  123.  
  124. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', _init); }
  125. else { _init(); }
  126.  
  127. })();