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 เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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