Scroll to Top Button

Adds a scroll to top button at the bottom right of the page when scrolled to the bottom, and hides it at the top.

Verze ze dne 23. 11. 2024. Zobrazit nejnovější verzi.

// ==UserScript==
// @name        Scroll to Top Button
// @namespace   sttb-ujs-dxrk1e
// @description  Adds a scroll to top button at the bottom right of the page when scrolled to the bottom, and hides it at the top.
// @icon https://i.imgur.com/FxF8TLS.png
// @match       *://*/*
// @grant       none
// @version     2.1.1
// @author      DXRK1E
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configuration object
    const CONFIG = {
        buttonSize: '40px',
        fontSize: '16px',
        backgroundColor: '#333',
        hoverColor: '#444',
        textColor: '#FFF',
        borderRadius: '50%',
        bottom: '20px',
        right: '20px',
        showThreshold: 300,
        minimumPageHeight: 1000,
        fadeSpeed: 300,
        debounceDelay: 150,
        zIndex: 2147483647,
        // Smooth scroll configuration
        scroll: {
            duration: 800,  // Duration of scroll animation in ms
            easing: 'easeInOutCubic', // Easing function for smooth acceleration/deceleration
            fps: 60,  // Frames per second for smooth animation
            breakpoints: {  // Adjust scroll speed based on distance
                short: 500,    // px
                medium: 1500,  // px
                long: 3000     // px
            }
        }
    };

    // Easing functions
    const easingFunctions = {
        // Cubic easing in/out
        easeInOutCubic: t => t < 0.5 
            ? 4 * t * t * t 
            : 1 - Math.pow(-2 * t + 2, 3) / 2,
        
        // Quadratic easing out
        easeOutQuad: t => 1 - (1 - t) * (1 - t),
        
        // Exponential easing in/out
        easeInOutExpo: t => t === 0
            ? 0
            : t === 1
            ? 1
            : t < 0.5 
            ? Math.pow(2, 20 * t - 10) / 2
            : (2 - Math.pow(2, -20 * t + 10)) / 2
    };

    function createScrollButton() {
        const button = document.createElement('button');
        button.id = 'enhanced-scroll-top-btn';
        button.innerHTML = `
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M8 3L14 9L12.6 10.4L8 5.8L3.4 10.4L2 9L8 3Z" fill="currentColor"/>
            </svg>
        `;

        const styles = {
            position: 'fixed',
            bottom: CONFIG.bottom,
            right: CONFIG.right,
            width: CONFIG.buttonSize,
            height: CONFIG.buttonSize,
            fontSize: CONFIG.fontSize,
            backgroundColor: CONFIG.backgroundColor,
            color: CONFIG.textColor,
            border: 'none',
            borderRadius: CONFIG.borderRadius,
            cursor: 'pointer',
            boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
            opacity: '0',
            visibility: 'hidden',
            zIndex: CONFIG.zIndex,
            transition: `all ${CONFIG.fadeSpeed}ms ease-in-out`,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '0',
            transform: 'scale(1)',
            outline: 'none'
        };

        Object.assign(button.style, styles);

        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = CONFIG.hoverColor;
            button.style.transform = 'scale(1.1)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = CONFIG.backgroundColor;
            button.style.transform = 'scale(1)';
        });

        button.addEventListener('mousedown', () => {
            button.style.transform = 'scale(0.95)';
        });

        button.addEventListener('mouseup', () => {
            button.style.transform = 'scale(1.1)';
        });

        return button;
    }

    function smoothScrollToTop() {
        const startPosition = window.pageYOffset || document.documentElement.scrollTop;
        const startTime = performance.now();
        let duration = CONFIG.scroll.duration;

        // Adjust duration based on scroll distance
        if (startPosition < CONFIG.scroll.breakpoints.short) {
            duration *= 0.7;  // Faster for short distances
        } else if (startPosition > CONFIG.scroll.breakpoints.long) {
            duration *= 1.3;  // Slower for long distances
        }

        function scrollStep(currentTime) {
            const timeElapsed = currentTime - startTime;
            const progress = Math.min(timeElapsed / duration, 1);

            const easedProgress = easingFunctions[CONFIG.scroll.easing](progress);
            
            const position = startPosition - (startPosition * easedProgress);
            
            window.scrollTo(0, position);

            if (timeElapsed < duration) {
                requestAnimationFrame(scrollStep);
            }
        }

        requestAnimationFrame(scrollStep);
    }

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function shouldShowButton() {
        const scrollHeight = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight
        );
        const viewportHeight = window.innerHeight;
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        
        return (
            scrollTop > CONFIG.showThreshold &&
            scrollHeight > CONFIG.minimumPageHeight &&
            scrollHeight > viewportHeight * 1.5
        );
    }

    function handleScroll() {
        const button = document.getElementById('enhanced-scroll-top-btn');
        if (!button) return;

        if (shouldShowButton()) {
            button.style.visibility = 'visible';
            button.style.opacity = '1';
        } else {
            button.style.opacity = '0';
            setTimeout(() => {
                if (button.style.opacity === '0') {
                    button.style.visibility = 'hidden';
                }
            }, CONFIG.fadeSpeed);
        }
    }

    function init() {
        const existingButton = document.getElementById('enhanced-scroll-top-btn');
        if (existingButton) return;

        const button = createScrollButton();
        document.body.appendChild(button);

        const debouncedScroll = debounce(handleScroll, CONFIG.debounceDelay);
        window.addEventListener('scroll', debouncedScroll, { passive: true });
        window.addEventListener('resize', debouncedScroll, { passive: true });
        
        const observer = new MutationObserver(debouncedScroll);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        button.addEventListener('click', (e) => {
            e.preventDefault();
            smoothScrollToTop();
        });

        // Initial check
        handleScroll();
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();