Smart Context Scroll-to-Top (Advanced)

Adds a smart back-to-top button not only for the page, but for every scrollable container (modals, divs).

目前為 2025-12-29 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Smart Context Scroll-to-Top (Advanced)
// @namespace    http://tampermonkey.net/
// @version      2025.12.29.1.1
// @license      MIT License
// @description  Adds a smart back-to-top button not only for the page, but for every scrollable container (modals, divs).
// @description:en  Adds a smart back-to-top button not only for the page, but for every scrollable container (modals, divs).
// @author       rurzowiutki
// @match        *://*/*
// @icon         https://raw.githubusercontent.com/rurzowiutki/scroll-up-icon/refs/heads/main/arrow-up1.png
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // config
    const SCROLL_THRESHOLD = 300;     // After how many pixels should the button appear
    const BUTTON_SIZE = 40;           // Button size in px
    const BUTTON_OFFSET = 20;         // Margin from the edge
    const Z_INDEX = 2137483647;       // Maximum possible Z-Index so that it is always on top
    const IGNORE_TEXT_FIELDS = true;  // If true, the button will not appear inside textareas or inputs

    // We store references to active buttons in WeakMap,
    // so we don't clutter up memory when elements disappear from the page (e.g., closing a modal).
    const buttonMap = new WeakMap();

    // Arrow icon (SVG)
    const arrowIcon = `
        <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round" style="color: white;">
            <line x1="12" y1="19" x2="12" y2="5"></line>
            <polyline points="5 12 12 5 19 12"></polyline>
        </svg>
    `;

    // CSS styles for the button (injected dynamically)
    const styleId = 'smart-scroll-btn-style';
    if (!document.getElementById(styleId)) {
        const style = document.createElement('style');
        style.id = styleId;
        style.textContent = `
            .smart-scroll-top-btn {
                position: fixed;
                width: ${BUTTON_SIZE}px;
                height: ${BUTTON_SIZE}px;
                background: rgba(0, 0, 0, 0.6);
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                opacity: 0;
                transform: scale(0.8);
                transition: opacity 0.3s, transform 0.3s, background 0.3s;
                box-shadow: 0 4px 6px rgba(0,0,0,0.1);
                pointer-events: none; /* By default, not clickable when invisible */
                z-index: ${Z_INDEX};
            }
            .smart-scroll-top-btn.visible {
                opacity: 1;
                transform: scale(1);
                pointer-events: auto;
            }
            .smart-scroll-top-btn:hover {
                background: rgba(0, 0, 0, 0.85);
                transform: scale(1.1);
            }
        `;
        document.head.appendChild(style);
    }

    function getOrCreateButton(scrollTarget) {
        if (buttonMap.has(scrollTarget)) {
            return buttonMap.get(scrollTarget);
        }

        const btn = document.createElement('div');
        btn.className = 'smart-scroll-top-btn';
        btn.innerHTML = arrowIcon;
        btn.title = "Scroll to Top";

        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            scrollTarget.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
        });

        document.body.appendChild(btn);

        buttonMap.set(scrollTarget, btn);
        return btn;
    }

    function updateButtonPosition(btn, target) {
        const isMainPage = target === document || target === document.documentElement || target === document.body;

        if (isMainPage) {
            btn.style.bottom = `${BUTTON_OFFSET}px`;
            btn.style.right = `${BUTTON_OFFSET}px`;
            btn.style.left = 'auto';
            btn.style.top = 'auto';
        } else {
            const rect = target.getBoundingClientRect();

            if (rect.width === 0 || rect.height === 0) {
                btn.classList.remove('visible');
                return;
            }

            const scrollbarWidth = target.offsetWidth - target.clientWidth;
            const rightPos = (window.innerWidth - rect.right) + BUTTON_OFFSET + scrollbarWidth;
            const bottomPos = (window.innerHeight - rect.bottom) + BUTTON_OFFSET;

            btn.style.right = `${Math.max(BUTTON_OFFSET, rightPos)}px`;
            btn.style.bottom = `${Math.max(BUTTON_OFFSET, bottomPos)}px`;
        }
    }

    window.addEventListener('scroll', (event) => {
        const target = event.target;

        if (!target || target.nodeType !== 1 && target !== document) return;

        if (IGNORE_TEXT_FIELDS) {
            const tagName = target.tagName.toUpperCase();
            if (tagName === 'TEXTAREA' || tagName === 'INPUT' || target.isContentEditable) {
                return;
            }
        }

        let scrollElement = target;
        let scrollTop = target.scrollTop;

        if (target === document) {
            scrollElement = document.documentElement;
            scrollTop = window.scrollY || document.documentElement.scrollTop;
        }

        const storageKey = (target === document) ? document.documentElement : target;
        const btn = getOrCreateButton(storageKey);

        if (scrollTop > SCROLL_THRESHOLD) {
            updateButtonPosition(btn, scrollElement);
            btn.classList.add('visible');
        } else {
            btn.classList.remove('visible');
        }

    }, { capture: true, passive: true });

    window.addEventListener('resize', () => {
       // Optionally, you can add logic here to refresh all visible buttons.
       // But for performance reasons, we omit this in the basic version.
       // The button will refresh the next time you scroll.
    }, { passive: true });

})();