Scroll Button

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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