Learn Microsoft - Resizable Sidebars

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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