Learn Microsoft - Resizable Sidebars

Makes the Ask Learn sidebar resizable and left TOC fixed width on learn.microsoft.com (only on >2K monitors)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Learn Microsoft - Resizable Sidebars
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Makes the Ask Learn sidebar resizable and left TOC fixed width on learn.microsoft.com (only on >2K monitors)
// @author       You
// @match        https://learn.microsoft.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ─── Resolution thresholds (CSS pixels = usable screen space) ──────
    const MIN_SCREEN_WIDTH  = 1920;
    const MIN_SCREEN_HEIGHT = 1080;

    // Right sidebar (Ask Learn) settings
    const MIN_WIDTH     = 250;
    const MAX_WIDTH     = 900;
    const DEFAULT_WIDTH = 400;
    const STORAGE_KEY   = 'askLearnSidebarWidth';
    const EDGE_SIZE     = 3;

    // Left sidebar (TOC) settings
    const LEFT_MIN_WIDTH     = 300;
    const LEFT_MAX_WIDTH     = 400;
    const LEFT_DEFAULT_WIDTH = 280;
    const LEFT_STORAGE_KEY   = 'leftTocSidebarWidth';

    // ─── State tracking ────────────────────────────────────────────────
    let isActive          = false;
    let styleElement      = null;
    let observer          = null;
    let lastSidebarState  = null;

    // ─── Resolution check (CSS pixels only — ignores DPR) ─────────────
    function isAbove2K() {
        const w = window.screen.width;
        const h = window.screen.height;

        console.log(`[Resizable Sidebars] screen: ${w}×${h}`);

        // Only activate when BOTH dimensions exceed the threshold
        return w > MIN_SCREEN_WIDTH && h > MIN_SCREEN_HEIGHT;
    }

    // ─── CSS injection / removal ───────────────────────────────────────
    function injectStyles() {
        if (styleElement) return;

        styleElement = document.createElement('style');
        styleElement.id = 'resizable-sidebar-styles';
        styleElement.textContent = `
            #layout-body-flyout.resizable-enabled {
                position: relative !important;
            }

            #layout-body-flyout.resizable-enabled > * {
                width: 100% !important;
                min-width: 0 !important;
                max-width: none !important;
            }

            .sidebar-resize-handle {
                position: absolute !important;
                left: 0;
                top: 0;
                width: ${EDGE_SIZE}px;
                height: 100%;
                background: transparent;
                cursor: col-resize;
                z-index: 999999;
                pointer-events: none;
            }

            .sidebar-resize-handle.active {
                background: rgba(0, 120, 212, 0.4);
                pointer-events: auto;
            }

            main.layout-body-main {
                display: flex !important;
            }

            #layout-body-menu {
                flex: 0 0 ${LEFT_DEFAULT_WIDTH}px !important;
                width: ${LEFT_DEFAULT_WIDTH}px !important;
                min-width: ${LEFT_DEFAULT_WIDTH}px !important;
                max-width: ${LEFT_DEFAULT_WIDTH}px !important;
            }

            .layout-body-main > div:not(#layout-body-menu):not(section) {
                flex: 1 1 auto !important;
            }

            #left-container,
            #left-container.width-full,
            div#left-container.left-container.width-full {
                width: 100% !important;
                max-width: 100% !important;
            }
        `;
        document.head.appendChild(styleElement);
    }

    function removeStyles() {
        if (styleElement) {
            styleElement.remove();
            styleElement = null;
        }
    }

    // ─── Width helpers ─────────────────────────────────────────────────
    function getSavedWidth() {
        const w = parseInt(localStorage.getItem(STORAGE_KEY), 10);
        return w >= MIN_WIDTH && w <= MAX_WIDTH ? w : DEFAULT_WIDTH;
    }

    function saveWidth(w) {
        localStorage.setItem(STORAGE_KEY, String(w));
    }

    function getLeftSavedWidth() {
        const w = parseInt(localStorage.getItem(LEFT_STORAGE_KEY), 10);
        return w >= LEFT_MIN_WIDTH && w <= LEFT_MAX_WIDTH ? w : LEFT_DEFAULT_WIDTH;
    }

    function setWidth(flyout, width) {
        flyout.style.setProperty('width',     `${width}px`, 'important');
        flyout.style.setProperty('min-width', `${width}px`, 'important');
        flyout.style.setProperty('max-width', `${width}px`, 'important');
    }

    function clearWidth(flyout) {
        flyout.style.removeProperty('width');
        flyout.style.removeProperty('min-width');
        flyout.style.removeProperty('max-width');
    }

    function updateBodyGrid(isOpen) {
        if (document.body.classList.contains('layout-body')) {
            document.body.style.setProperty(
                'grid-template-columns',
                isOpen ? '1fr 1500px' : '1fr 500px',
                'important'
            );
        }
    }

    function clearBodyGrid() {
        if (document.body.classList.contains('layout-body')) {
            document.body.style.removeProperty('grid-template-columns');
        }
    }

    function isSidebarOpen() {
        const flyout = document.getElementById('layout-body-flyout');
        if (!flyout) return false;
        const style = window.getComputedStyle(flyout);
        return style.display !== 'none' && flyout.offsetWidth > 0;
    }

    // ─── Left sidebar ──────────────────────────────────────────────────
    function initLeftSidebar() {
        const menuSection = document.getElementById('layout-body-menu');
        if (!menuSection || menuSection.dataset.leftInit) return;

        menuSection.dataset.leftInit = 'true';
        const width = getLeftSavedWidth();

        menuSection.style.cssText = `
            flex: 0 0 ${width}px !important;
            width: ${width}px !important;
            min-width: ${width}px !important;
            max-width: ${width}px !important;
        `;

        const leftContainer = document.getElementById('left-container');
        if (leftContainer) {
            leftContainer.style.cssText = `
                width: 100% !important;
                max-width: 100% !important;
            `;
        }
    }

    function teardownLeftSidebar() {
        const menuSection = document.getElementById('layout-body-menu');
        if (menuSection) {
            delete menuSection.dataset.leftInit;
            menuSection.style.cssText = '';
        }

        const leftContainer = document.getElementById('left-container');
        if (leftContainer) {
            leftContainer.style.cssText = '';
        }
    }

    // ─── Right sidebar (resizable) ────────────────────────────────────
    function initResizable() {
        const flyout = document.getElementById('layout-body-flyout');
        if (!flyout || flyout.classList.contains('resizable-enabled')) return;

        flyout.classList.add('resizable-enabled');

        const handle = document.createElement('div');
        handle.className = 'sidebar-resize-handle';
        flyout.appendChild(handle);

        setWidth(flyout, getSavedWidth());

        let isResizing = false;
        let startX     = 0;
        let startWidth = 0;

        flyout._resizeMouseMove = (e) => {
            const rect = flyout.getBoundingClientRect();
            const nearEdge = e.clientX >= rect.left && e.clientX <= rect.left + EDGE_SIZE;
            handle.style.pointerEvents = nearEdge || isResizing ? 'auto' : 'none';
        };
        flyout.addEventListener('mousemove', flyout._resizeMouseMove);

        handle._pointerDown = (e) => {
            if (e.button !== 0) return;
            isResizing = true;
            startX     = e.clientX;
            startWidth = flyout.getBoundingClientRect().width;
            handle.classList.add('active');
            handle.setPointerCapture(e.pointerId);
            e.preventDefault();
        };
        handle.addEventListener('pointerdown', handle._pointerDown);

        flyout._docPointerMove = (e) => {
            if (!isResizing) return;
            const deltaX = startX - e.clientX;
            let newWidth = Math.round(startWidth + deltaX);
            newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, newWidth));
            setWidth(flyout, newWidth);
        };
        document.addEventListener('pointermove', flyout._docPointerMove);

        flyout._docPointerUp = () => {
            if (!isResizing) return;
            isResizing = false;
            handle.classList.remove('active');
            handle.style.pointerEvents = 'none';
            saveWidth(Math.round(flyout.getBoundingClientRect().width));
        };
        document.addEventListener('pointerup', flyout._docPointerUp);
    }

    function teardownResizable() {
        const flyout = document.getElementById('layout-body-flyout');
        if (!flyout) return;

        if (flyout._resizeMouseMove) {
            flyout.removeEventListener('mousemove', flyout._resizeMouseMove);
        }
        if (flyout._docPointerMove) {
            document.removeEventListener('pointermove', flyout._docPointerMove);
        }
        if (flyout._docPointerUp) {
            document.removeEventListener('pointerup', flyout._docPointerUp);
        }

        const handle = flyout.querySelector('.sidebar-resize-handle');
        if (handle) handle.remove();

        flyout.classList.remove('resizable-enabled');
        clearWidth(flyout);
    }

    // ─── Sidebar state ────────────────────────────────────────────────
    function checkSidebarState() {
        const isOpen = isSidebarOpen();
        if (lastSidebarState !== isOpen) {
            lastSidebarState = isOpen;
            updateBodyGrid(isOpen);
        }
    }

    // ─── Activate / deactivate ─────────────────────────────────────────
    function activate() {
        if (isActive) return;
        isActive = true;
        console.log('[Resizable Sidebars] Activating – high-res monitor detected');

        injectStyles();
        initResizable();
        initLeftSidebar();
        checkSidebarState();

        observer = new MutationObserver(() => {
            initResizable();
            initLeftSidebar();
            checkSidebarState();
        });
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['style', 'class'],
        });
    }

    function deactivate() {
        if (!isActive) return;
        isActive = false;
        console.log('[Resizable Sidebars] Deactivating – monitor is ≤ threshold');

        if (observer) {
            observer.disconnect();
            observer = null;
        }

        teardownResizable();
        teardownLeftSidebar();
        removeStyles();
        clearBodyGrid();
        lastSidebarState = null;
    }

    // ─── Resolution change watcher ─────────────────────────────────────
    function evaluateResolution() {
        if (isAbove2K()) {
            activate();
        } else {
            deactivate();
        }
    }

    // Check on all events that can signal a monitor change
    window.addEventListener('resize',             evaluateResolution);
    window.addEventListener('focus',              evaluateResolution);
    document.addEventListener('visibilitychange', evaluateResolution);

    // Poll every 2s as a safety net (screen props don't always fire events)
    setInterval(evaluateResolution, 2000);

    // Initial check
    evaluateResolution();
})();