去你的固定頂欄

自動處理固定或粘性定位的頂部導航欄,根據滾動狀態智能顯示/隱藏,提升瀏覽體驗

// ==UserScript==
// @name         F**k sticky header
// @name:zh-CN   去你的固定顶栏
// @name:zh-TW   去你的固定頂欄
// @namespace    fuck.sticky.header
// @version      1.0
// @description  Automatically handle sticky/fixed top headers, show/hide based on scroll
// @description:zh-CN  自动处理固定或粘性定位的顶部导航栏,根据滚动状态智能显示/隐藏,提升浏览体验
// @description:zh-TW  自動處理固定或粘性定位的頂部導航欄,根據滾動狀態智能顯示/隱藏,提升瀏覽體驗
// @author       You
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';

    // Configuration parameters
    const CONFIG = {
        scrollThreshold: 5,        // Minimum scroll distance to trigger action
        topThreshold: 100,         // Top area where header should always show
        transitionDuration: '0.3s', // Animation duration
        maxTopValue: 40            // Accept elements with top value up to this (pixels)
    };

    // Get whitelist from storage
    let whitelist = GM_getValue('whitelist', []);

    // Check if current domain is whitelisted
    function isWhitelisted() {
        const currentDomain = window.location.hostname;
        return whitelist.some(domain => currentDomain.includes(domain));
    }

    // Add current site to whitelist
    function addToWhitelist() {
        const currentDomain = window.location.hostname;
        if (!whitelist.includes(currentDomain)) {
            whitelist.push(currentDomain);
            GM_setValue('whitelist', whitelist);
            alert(`Added ${currentDomain} to whitelist`);
            window.location.reload();
        } else {
            alert(`${currentDomain} is already in whitelist`);
        }
    }

    // Remove current site from whitelist
    function removeFromWhitelist() {
        const currentDomain = window.location.hostname;
        const index = whitelist.indexOf(currentDomain);
        if (index !== -1) {
            whitelist.splice(index, 1);
            GM_setValue('whitelist', whitelist);
            alert(`Removed ${currentDomain} from whitelist`);
            window.location.reload();
        } else {
            alert(`${currentDomain} is not in whitelist`);
        }
    }

    // Register Tampermonkey menu commands
    GM_registerMenuCommand('Add current site to whitelist', addToWhitelist);
    GM_registerMenuCommand('Remove current site from whitelist', removeFromWhitelist);

    // Exit if site is whitelisted
    if (isWhitelisted()) {
        return;
    }

    // Helper function to parse pixel values
    function parsePixelValue(value) {
        if (value.endsWith('px')) {
            return parseFloat(value);
        }
        return 0;
    }

    // Detect eligible header elements
    function detectHeaderElements() {
        // Collect all potential header elements
        const candidates = new Set();

        // 1. Add <header> tags
        const headerTags = document.getElementsByTagName('header');
        if (headerTags.length > 0) {
            Array.from(headerTags).forEach(el => candidates.add(el));
        }

        // 2. Add elements with relevant keywords
        const keywords = ['nav', 'banner', 'header'];
        const allElements = document.getElementsByTagName('*');

        for (const el of allElements) {
            const className = typeof el.className === 'string' ? el.className : '';
            const hasKeyword = keywords.some(keyword =>
                (el.id && el.id.toLowerCase().includes(keyword)) ||
                (className && className.toLowerCase().includes(keyword))
            );

            if (hasKeyword) {
                candidates.add(el);
            }
        }

        // 3. Find full-width elements (100vw) regardless of keywords
        for (const el of allElements) {
            const computed = window.getComputedStyle(el);
            if (computed.width === '100vw') {
                candidates.add(el);
            }
        }

        // Filter to find valid top headers
        const validHeaders = [];
        for (const el of candidates) {
            // Check positioning
            const computed = window.getComputedStyle(el);
            const isStickyOrFixed = computed.position === 'sticky' || computed.position === 'fixed';
            if (!isStickyOrFixed) continue;

            // Check top value (allow small values up to maxTopValue)
            const topValue = parsePixelValue(computed.top);
            if (topValue > CONFIG.maxTopValue) continue;

            // Check visual position and dimensions
            const rect = el.getBoundingClientRect();
            const isWideEnough = rect.width >= window.innerWidth * 0.8 || computed.width === '100vw';
            const isTallEnough = rect.height > 20;
            const isNearTop = rect.top <= CONFIG.topThreshold;

            if (isWideEnough && isTallEnough && isNearTop) {
                validHeaders.push(el);
            }
        }

        return validHeaders;
    }

    // Get all eligible headers
    const headerElements = detectHeaderElements();
    if (headerElements.length === 0) {
        return; // No eligible headers found
    }

    // Add necessary styles
    function addStyles() {
        const styleSheet = document.createElement('style');
        styleSheet.id = 'fuck-sticky-header-style';

        // Generate unique selectors
        const selectors = headerElements.map(el => {
            if (el.id) return `#${CSS.escape(el.id)}`;
            return Array.from(el.classList).map(cls => `.${CSS.escape(cls)}`).join(', ');
        }).join(', ');

        styleSheet.textContent = `
            ${selectors} {
                will-change: transform, opacity !important;
                transition: transform ${CONFIG.transitionDuration} cubic-bezier(0.4, 0, 0.2, 1),
                            opacity ${CONFIG.transitionDuration} cubic-bezier(0.4, 0, 0.2, 1) !important;
            }

            html {
                overscroll-behavior-y: contain;
            }
        `;
        document.head.appendChild(styleSheet);
    }

    // Initialize variables
    let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;

    // Update header visibility
    function updateHeaders(shouldShow) {
        headerElements.forEach(el => {
            el.style.transform = shouldShow ? 'translateY(0)' : 'translateY(-100%)';
            el.style.opacity = shouldShow ? '1' : '0';
        });
    }

    // Scroll handler function
    function handleScroll() {
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;

        // Handle Safari overscroll bounce
        const documentHeight = document.documentElement.scrollHeight;
        const viewportHeight = document.documentElement.clientHeight;
        const maxValidScrollTop = documentHeight - viewportHeight;
        if (scrollTop < 0 || scrollTop > maxValidScrollTop) return;

        // Calculate scroll difference
        const scrollDiff = scrollTop - lastScrollTop;

        // Only act on significant scroll movements
        if (Math.abs(scrollDiff) >= CONFIG.scrollThreshold) {
            // Special handling for top area
            if (scrollTop <= CONFIG.topThreshold) {
                updateHeaders(true);
            }
            // Show on upward scroll
            else if (scrollDiff < 0) {
                updateHeaders(true);
            }
            // Hide on downward scroll
            else if (scrollDiff > 0) {
                updateHeaders(false);
            }

            // Update last scroll position
            lastScrollTop = scrollTop;
        }
    }

    // Initialization
    function init() {
        addStyles();

        // Set initial state
        const initialScrollTop = window.pageYOffset || document.documentElement.scrollTop;
        updateHeaders(initialScrollTop <= CONFIG.topThreshold);

        // Add scroll listener
        window.addEventListener('scroll', () => {
            requestAnimationFrame(handleScroll);
        }, { passive: true });
    }

    // Initialize when page is loaded
    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }

})();