Scroll Button

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

Per 30-06-2026. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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