Universal Scroll Top/Bottom Arrows

Adds floating up and down arrows to the bottom right of every webpage for quick scrolling. Works in dark/light mode.

目前為 2025-11-28 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Universal Scroll Top/Bottom Arrows
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds floating up and down arrows to the bottom right of every webpage for quick scrolling. Works in dark/light mode.
// @author       Snow2122
// @license      MIT
// @match        *://*/*
// @run-at       document-idle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const CONFIG = {
        size: '45px',
        color: '#ffffff',
        bgColor: '#222222',
        opacity: '0.8',
        hoverOpacity: '1.0',
        distanceFromRight: '20px',
        distanceFromBottom: '70px', // Moved up to avoid footer ads/bars
        zIndex: '2147483647'
    };

    // --- Create Container ---
    const container = document.createElement('div');
    container.id = 'tm-scroll-btn-container';

    // Apply container styles with !important
    container.style.cssText = `
        position: fixed !important;
        bottom: ${CONFIG.distanceFromBottom} !important;
        right: ${CONFIG.distanceFromRight} !important;
        z-index: ${CONFIG.zIndex} !important;
        display: flex !important;
        flex-direction: column !important;
        gap: 8px !important;
        pointer-events: none !important;
        isolation: isolate !important; /* Fixes stacking context issues */
    `;

    // --- Helper Function to Create Buttons ---
    function createButton(svgIcon, action) {
        const btn = document.createElement('div'); // Changed to div to avoid default button styles
        btn.innerHTML = svgIcon;

        // Robust styling with !important
        btn.style.cssText = `
            width: ${CONFIG.size} !important;
            height: ${CONFIG.size} !important;
            background-color: ${CONFIG.bgColor} !important;
            color: ${CONFIG.color} !important;
            border: 2px solid rgba(255, 255, 255, 0.2) !important; /* Visible border for dark mode */
            border-radius: 50% !important; /* Circle shape looks cleaner */
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            opacity: ${CONFIG.opacity} !important;
            transition: all 0.2s ease !important;
            box-shadow: 0 4px 6px rgba(0,0,0,0.3) !important;
            pointer-events: auto !important; /* Re-enable clicks for buttons */
            user-select: none !important;
            -webkit-user-select: none !important;
        `;

        // Hover Effects (via JS since we are using inline styles)
        btn.onmouseenter = () => {
            btn.style.opacity = CONFIG.hoverOpacity;
            btn.style.transform = 'scale(1.1)';
            btn.style.backgroundColor = '#000000';
            btn.style.borderColor = 'rgba(255, 255, 255, 0.8)';
        };

        btn.onmouseleave = () => {
            btn.style.opacity = CONFIG.opacity;
            btn.style.transform = 'scale(1)';
            btn.style.backgroundColor = CONFIG.bgColor;
            btn.style.borderColor = 'rgba(255, 255, 255, 0.2)';
        };

        // Click Action
        btn.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            action();
        };

        return btn;
    }

    // --- Smart Scroll Logic ---
    // Finds the element that is actually scrollable (Window vs Wrapper Div)
    function getScrollableTarget() {
        // 1. Check if the main document is scrollable and not locked
        const bodyStyle = window.getComputedStyle(document.body);
        const htmlStyle = window.getComputedStyle(document.documentElement);

        if (bodyStyle.overflowY !== 'hidden' && htmlStyle.overflowY !== 'hidden' &&
           (document.body.scrollHeight > window.innerHeight || document.documentElement.scrollHeight > window.innerHeight)) {
            return window;
        }

        // 2. If main is locked, find the largest scrollable element on screen
        let largestScrollable = null;
        let maxArea = 0;

        // Efficiently query potential scroll containers (divs, main, article)
        const elements = document.querySelectorAll('div, main, article, section');

        for (let el of elements) {
            const style = window.getComputedStyle(el);
            const isScrollable = (style.overflowY === 'auto' || style.overflowY === 'scroll');

            if (isScrollable && el.scrollHeight > el.clientHeight) {
                const rect = el.getBoundingClientRect();
                const area = rect.width * rect.height;
                // We want the largest visible scroll area
                if (area > maxArea && rect.height > 100) {
                    maxArea = area;
                    largestScrollable = el;
                }
            }
        }

        return largestScrollable || window; // Fallback to window
    }

    const scrollUp = () => {
        const target = getScrollableTarget();
        target.scrollTo({
            top: 0,
            behavior: 'smooth'
        });
    };

    const scrollDown = () => {
        const target = getScrollableTarget();

        // Calculate height based on target type
        let topVal = 0;
        if (target === window) {
            topVal = Math.max(
                document.body.scrollHeight,
                document.documentElement.scrollHeight
            );
        } else {
            topVal = target.scrollHeight;
        }

        target.scrollTo({
            top: topVal,
            behavior: 'smooth'
        });
    };

    // --- Assemble UI ---
    const svgStyle = 'width: 24px; height: 24px; stroke-width: 3;';

    const svgUp = `<svg style="${svgStyle}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>`;
    const svgDown = `<svg style="${svgStyle}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;

    const btnUp = createButton(svgUp, scrollUp);
    const btnDown = createButton(svgDown, scrollDown);

    container.appendChild(btnUp);
    container.appendChild(btnDown);

    // --- Inject into DOM ---
    const init = () => {
        // Prefer appending to documentElement (html) to escape body stacking contexts
        const target = document.documentElement || document.body;
        if (target) {
            target.appendChild(container);
        } else {
            setTimeout(init, 500);
        }
    };

    // Run initialization
    init();

})();