Top and Down Scroll Buttons

Smooth scroll buttons for top and bottom of page

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Top and Down Scroll Buttons
// @description  Smooth scroll buttons for top and bottom of page
// @version      1.1
// @author       lunagus
// @license      MIT
// @include      *
// @run-at       document-end
// @grant        none
// @namespace https://greasyfork.org/users/1517518
// ==/UserScript==

(function() {
    'use strict';
    
    // Skip iframes
    if (window.self !== window.top) return;
    
    // Configuration
    const CONFIG = {
        speedClick: 500,        // Smooth scroll duration on click (ms)
        speedHover: 100,        // Continuous scroll speed on hover (ms)
        zIndex: 1001,           // Z-index for buttons
        scrollStep: 1,          // Pixels to scroll per interval on hover
        hideThreshold: 0        // Scroll position to show/hide buttons
    };
    
    // SVG icons (inline for better performance)
    const ICONS = {
        up: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"%3E%3Cpath d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/%3E%3C/svg%3E',
        down: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"%3E%3Cpath d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/%3E%3C/svg%3E'
    };
    
    // Inject CSS
    const injectStyles = () => {
        const style = document.createElement('style');
        style.textContent = `
            .scroll-btn {
                position: fixed;
                right: 0;
                width: 40px;
                height: 40px;
                background: rgba(0, 0, 0, 0.7);
                border-radius: 5px 0 0 5px;
                cursor: pointer;
                opacity: 0.65;
                transition: opacity 0.3s ease, transform 0.2s ease;
                z-index: ${CONFIG.zIndex};
                display: flex;
                align-items: center;
                justify-content: center;
                user-select: none;
            }
            
            .scroll-btn:hover {
                opacity: 1;
                transform: translateX(-2px);
            }
            
            .scroll-btn:active {
                transform: translateX(-2px) scale(0.95);
            }
            
            .scroll-btn-up {
                bottom: calc(50% + 25px);
            }
            
            .scroll-btn-down {
                top: calc(50% + 25px);
            }
            
            .scroll-btn-hidden {
                display: none;
            }
            
            .scroll-btn img {
                width: 24px;
                height: 24px;
                pointer-events: none;
            }
        `;
        document.head.appendChild(style);
    };
    
    // Easing function for smooth scroll
    const easeInOutQuad = (t, b, c, d) => {
        t /= d / 2;
        if (t < 1) return c / 2 * t * t + b;
        t--;
        return -c / 2 * (t * (t - 2) - 1) + b;
    };
    
    // Smooth scroll to position
    const smoothScrollTo = (target, duration = CONFIG.speedClick) => {
        const start = window.pageYOffset || document.documentElement.scrollTop;
        const change = target - start;
        const increment = 20;
        let currentTime = 0;
        
        const animateScroll = () => {
            currentTime += increment;
            const val = easeInOutQuad(currentTime, start, change, duration);
            window.scrollTo(0, val);
            if (currentTime < duration) {
                requestAnimationFrame(animateScroll);
            }
        };
        
        animateScroll();
    };
    
    // Scroll animation class
    class ScrollAnimator {
        constructor() {
            this.animationFrame = null;
            this.direction = 0; // 0: stop, 1: up, -1: down
        }
        
        start(direction) {
            this.direction = direction;
            if (!this.animationFrame) {
                this.animate();
            }
        }
        
        stop() {
            this.direction = 0;
            if (this.animationFrame) {
                cancelAnimationFrame(this.animationFrame);
                this.animationFrame = null;
            }
        }
        
        animate() {
            if (this.direction === 0) {
                this.animationFrame = null;
                return;
            }
            
            const currentPos = window.pageYOffset || document.documentElement.scrollTop;
            window.scrollTo(0, currentPos - this.direction * CONFIG.scrollStep);
            
            this.animationFrame = requestAnimationFrame(() => this.animate());
        }
    }
    
    // Button manager
    class ScrollButtons {
        constructor() {
            this.animator = new ScrollAnimator();
            this.upBtn = null;
            this.downBtn = null;
            this.init();
        }
        
        createButton(className, iconData) {
            const btn = document.createElement('div');
            btn.className = `scroll-btn ${className}`;
            
            const img = document.createElement('img');
            img.src = iconData;
            img.alt = className.includes('up') ? 'Scroll to top' : 'Scroll to bottom';
            
            btn.appendChild(img);
            return btn;
        }
        
        getDocumentHeight() {
            return Math.max(
                document.body.scrollHeight,
                document.body.offsetHeight,
                document.documentElement.clientHeight,
                document.documentElement.scrollHeight,
                document.documentElement.offsetHeight
            );
        }
        
        updateButtonVisibility() {
            const scrolled = window.pageYOffset || document.documentElement.scrollTop;
            const maxScroll = this.getDocumentHeight() - window.innerHeight;
            
            // Show/hide up button
            this.upBtn.classList.toggle('scroll-btn-hidden', scrolled <= CONFIG.hideThreshold);
            
            // Show/hide down button
            this.downBtn.classList.toggle('scroll-btn-hidden', scrolled >= maxScroll - 10);
        }
        
        init() {
            // Check if page is scrollable
            const isScrollable = this.getDocumentHeight() > window.innerHeight;
            if (!isScrollable) return;
            
            // Inject styles
            injectStyles();
            
            // Create buttons
            this.upBtn = this.createButton('scroll-btn-up', ICONS.up);
            this.downBtn = this.createButton('scroll-btn-down', ICONS.down);
            
            // Add event listeners for up button
            this.upBtn.addEventListener('mouseenter', () => this.animator.start(1));
            this.upBtn.addEventListener('mouseleave', () => this.animator.stop());
            this.upBtn.addEventListener('click', () => {
                this.animator.stop();
                smoothScrollTo(0);
            });
            
            // Add event listeners for down button
            this.downBtn.addEventListener('mouseenter', () => this.animator.start(-1));
            this.downBtn.addEventListener('mouseleave', () => this.animator.stop());
            this.downBtn.addEventListener('click', () => {
                this.animator.stop();
                smoothScrollTo(this.getDocumentHeight());
            });
            
            // Append to body
            document.body.appendChild(this.upBtn);
            document.body.appendChild(this.downBtn);
            
            // Initial visibility update
            this.updateButtonVisibility();
            
            // Update on scroll (throttled)
            let scrollTimeout;
            window.addEventListener('scroll', () => {
                if (scrollTimeout) return;
                scrollTimeout = setTimeout(() => {
                    this.updateButtonVisibility();
                    scrollTimeout = null;
                }, 100);
            }, { passive: true });
            
            // Update on resize
            window.addEventListener('resize', () => {
                this.updateButtonVisibility();
            }, { passive: true });
        }
    }
    
    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => new ScrollButtons());
    } else {
        new ScrollButtons();
    }
})();