Scroll Button

Add buttons to scroll to the bottom and top of the website. Two-finger tap or Ctrl+Z to hide/show buttons.

30.06.2026 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Advertisement:

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

Advertisement:

// ==UserScript==
// @name         Scroll Button
// @name:zh      滚动按钮
// @namespace    https://greasyfork.org/
// @version      2.3
// @description  Add buttons to scroll to the bottom and top of the website. Two-finger tap or Ctrl+Z to hide/show buttons.
// @description:zh 添加按钮以滚动到网页的底部和顶部,支持双指单击或 Ctrl+Z 隐藏/显示按钮
// @author       chowxi
// @match        *://*/*
// @license      MIT
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    if (window !== window.top) {
        return;
    }

    function throttle(func, delay) {
        let lastCall = 0;
        return function(...args) {
            const now = Date.now();
            if (now - lastCall < delay) {
                return;
            }
            lastCall = now;
            return func.apply(this, args);
        };
    }

    function createButton(iconBase64, altText, buttonStyles) {
        const btn = document.createElement('button');
        const img = document.createElement('img');
        img.src = `data:image/svg+xml;base64,${iconBase64}`;
        img.alt = altText;
        img.style.width = '16px';
        img.style.height = '16px';
        img.style.display = 'block';
        btn.appendChild(img);
        Object.assign(btn.style, buttonStyles);
        return btn;
    }

    function setHoverShadow(button) {
        button.addEventListener('mouseenter', function() {
            button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.25)';
        });
        button.addEventListener('mouseleave', function() {
            button.style.boxShadow = '0 1px 3px rgba(0,0,0,0.15)';
        });
    }

    function init() {
        if (!document.body) {
            // 如果 document.body 不存在,等待 DOMContentLoaded 事件
            window.addEventListener('DOMContentLoaded', init);
            return;
        }

        // Base64-encoded SVG data for bottom icon
        const base64BottomIcon =
            'PHN2ZyBzdHJva2U9ImN1cnJlbnRDb2xvciIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imljb24tc20gbS0xIiBoZWlnaHQ9IjFlbSIgd2lkdGg9IjFlbSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMTIiIHkxPSI1IiB4Mj0iMTIiIHkyPSIxOSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjE5IDEyIDEyIDE5IDUgMTIiPjwvcG9seWxpbmU+PC9zdmc+';

        // Base64-encoded SVG data for top icon
        const base64TopIcon =
            'PHN2ZyBzdHJva2U9ImN1cnJlbnRDb2xvciIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imljb24tc20gbS0xIiBoZWlnaHQ9IjFlbSIgd2lkdGg9IjFlbSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMTIiIHkxPSIxOSIgeDI9IjEyIiB5Mj0iNSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjUgMTIgMTIgNSAxOSAxMiI+PC9wb2x5bGluZT48L3N2Zz4=';

        // Common styles for both buttons
        const buttonStyles = {
            position: 'fixed',
            zIndex: '99',
            width: '25px',
            height: '25px',
            minWidth: '25px',
            minHeight: '25px',
            maxWidth: '25px',
            maxHeight: '25px',
            flexShrink: '0',
            boxSizing: 'border-box',
            backgroundColor: 'transparent',
            backdropFilter: 'blur(8px)',
            WebkitBackdropFilter: 'blur(8px)',
            boxShadow: '0 1px 3px rgba(0,0,0,0.35)',
            border: '0.5px solid transparent',
            borderRadius: '50%',
            padding: '4px',
            margin: '0',
            cursor: 'pointer',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            transition: 'opacity 0.2s, box-shadow 0.2s'
        };

        // 按钮显示/隐藏状态
        let buttonsHidden = false;

        function applyButtonsVisibility() {
            if (buttonsHidden) {
                bottomButton.style.setProperty('display', 'none', 'important');
                topButton.style.setProperty('display', 'none', 'important');
            } else {
                bottomButton.style.setProperty('display', 'flex', 'important');
                toggleTopButton();
            }
        }

        function toggleButtonsVisibility() {
            buttonsHidden = !buttonsHidden;
            applyButtonsVisibility();
        }

        const bottomButton = createButton(base64BottomIcon, 'Scroll to Bottom', buttonStyles);
        bottomButton.style.bottom = '14px';
        bottomButton.style.right = '14px';

        bottomButton.addEventListener('click', function() {
            window.scrollTo({
                top: document.documentElement.scrollHeight || document.body.scrollHeight,
                behavior: 'smooth'
            });
        });

        const topButton = createButton(base64TopIcon, 'Scroll to Top', buttonStyles);
        topButton.style.bottom = '50px';
        topButton.style.right = '14px';

        topButton.addEventListener('click', function() {
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
        });

        document.body.append(bottomButton, topButton);

        setHoverShadow(bottomButton);
        setHoverShadow(topButton);

        // 智能避让:检测右下角是否有其他浮动元素,自动上移按钮
        function avoidOverlap() {
            const margin = 14;        // 按钮离右边的基础距离
            const baseBottom = 14;    // 最底部按钮的基础距离
            const gap = 36;           // 按钮之间的固定间距
            const buttonSize = 32;    // 按钮估算尺寸(含 padding)

            let extraOffset = 0;

            const allElements = document.querySelectorAll('body *');
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;

            allElements.forEach(el => {
                if (el === bottomButton || el === topButton || bottomButton.contains(el) || topButton.contains(el)) {
                    return;
                }

                const style = window.getComputedStyle(el);
                if (style.position !== 'fixed' && style.position !== 'sticky') {
                    return;
                }
                if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
                    return;
                }

                const rect = el.getBoundingClientRect();
                if (rect.width === 0 || rect.height === 0) {
                    return;
                }

                const isInBottomRightZone =
                    rect.right > viewportWidth - 120 &&
                    rect.bottom > viewportHeight - 150;

                if (isInBottomRightZone) {
                    const occupiedFromBottom = viewportHeight - rect.top;
                    if (occupiedFromBottom > extraOffset) {
                        extraOffset = occupiedFromBottom;
                    }
                }
            });

            const offset = extraOffset > 0 ? extraOffset + 8 : 0;

            bottomButton.style.setProperty('bottom', `${baseBottom + offset}px`, 'important');
            topButton.style.setProperty('bottom', `${baseBottom + gap + offset}px`, 'important');
        }

        avoidOverlap();

        window.addEventListener('scroll', throttle(avoidOverlap, 300));
        window.addEventListener('resize', throttle(avoidOverlap, 300));

        const observer = new MutationObserver(throttle(avoidOverlap, 500));
        observer.observe(document.body, { childList: true, subtree: true });

        function toggleTopButton() {
            if (buttonsHidden) {
                topButton.style.display = 'none';
                return;
            }
            topButton.style.display = window.scrollY === 0 ? 'none' : 'flex';
        }

        toggleTopButton();
        window.addEventListener('scroll', throttle(toggleTopButton, 100));

        // 快捷键 Ctrl+Z:切换按钮显示/隐藏
        window.addEventListener('keydown', function(e) {
            if (e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 'z' || e.key === 'Z')) {
                e.preventDefault();
                toggleButtonsVisibility();
            }
        });

        let touchPointerCount = 0;

        window.addEventListener('pointerdown', function(e) {
            if (e.pointerType !== 'touch') {
                return;
            }
            touchPointerCount++;
            if (touchPointerCount === 2) {
                toggleButtonsVisibility();
            }
        }, { passive: true, capture: true });

        window.addEventListener('pointerup', function(e) {
            if (e.pointerType === 'touch' && touchPointerCount > 0) {
                touchPointerCount--;
            }
        }, { passive: true, capture: true });

        window.addEventListener('pointercancel', function(e) {
            if (e.pointerType === 'touch' && touchPointerCount > 0) {
                touchPointerCount--;
            }
        }, { passive: true, capture: true });
    }

    init();
})();