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).

您需要先安裝使用者腳本管理器擴展,如 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.2
// @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';

    // ================= CONFIGURATION =================
    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
    const IGNORE_TEXT_FIELDS = true;  // If true, the button will not appear inside textareas or inputs
    // =================================================

    // WeakMap to store button references without causing memory leaks
    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>
    `;

    // Inject CSS styles
    const styleId = 'smart-scroll-btn-style';
    function injectStyles() {
        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;
                    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 || document.documentElement).appendChild(style);
        }
    }

    // Helper to create the button
    function getOrCreateButton(scrollTarget) {
        if (buttonMap.has(scrollTarget)) {
            return buttonMap.get(scrollTarget);
        }

        // Ensure styles are injected (safe check)
        injectStyles();

        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'
            });
        });

        // Append to body (fallback to documentElement if body not ready yet)
        (document.body || document.documentElement).appendChild(btn);

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

    // Helper to position the button
    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 element is hidden, hide button
            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`;
        }
    }

    // Main Scroll Handler
    window.addEventListener('scroll', (event) => {
        const target = event.target;

        // Basic validation
        if (!target) return;

        // 1. Text Field Check (Fixed: safe check for tagName)
        if (IGNORE_TEXT_FIELDS) {
            // Document object doesn't have tagName, so we default to empty string
            const tagName = (target.tagName || '').toUpperCase();
            const isContentEditable = target.isContentEditable || (target.getAttribute && target.getAttribute('contenteditable') === 'true');

            if (tagName === 'TEXTAREA' || tagName === 'INPUT' || isContentEditable) {
                return;
            }
        }

        // 2. Identify what is being scrolled
        if (target.nodeType !== 1 && target !== document) return;

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

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

        // 3. Get Button
        const storageKey = (target === document) ? document.documentElement : target;
        const btn = getOrCreateButton(storageKey);

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

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

})();