VSCode Like Browser Minimap — Shadow DOM, Multi-Scroll, Hydration-Safe

Minimal, fast, layout-only minimap in a Shadow DOM that follows hovered scrollable areas. M toggles map. 1 jumps to top (0%), 0 jumps to end (100%). Click/drag to scroll. Hydration-safe for React/Next sites.

// ==UserScript==
// @name         VSCode Like Browser Minimap — Shadow DOM, Multi-Scroll, Hydration-Safe
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Minimal, fast, layout-only minimap in a Shadow DOM that follows hovered scrollable areas. M toggles map. 1 jumps to top (0%), 0 jumps to end (100%). Click/drag to scroll. Hydration-safe for React/Next sites.
// @author       Your Name
// @match        *://*/*
// @run-at       document-idle
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ---------------------------
  // Config
  // ---------------------------
  const MM_WIDTH = 120;
  const MIN_VP_PX = 18;
  const MAX_PER_RENDER = 5000;
  const HOVER_THROTTLE_MS = 200;

  // User shortcuts/config
  // User config
  const START_HIDDEN = true;            // start minimap hidden after refresh
  const TOGGLE_KEY = 'm';               // toggle key (case-insensitive)
  const TOGGLE_MOD = {                  // require a modifier? set to true
    ctrl: false, alt: false, shift: false, meta: false
  };

  // Exclude websites (any match will disable the script)
  const EXCLUDE_HOSTS = ['www.youtube.com', 'twitter.com', 'www.facebook.com', 'www.reddit.com'];
  const EXCLUDE_PATTERNS = [
    // /^https?:\/\/(\w+\.)?google\./i,   // any google.* domain
    // /:\/\/localhost:\d+\//,
  ];

  // Helper: returns true if current page should be excluded
  function isExcluded(loc) {
    const href = typeof loc === 'string' ? loc : loc.href;
    let host;
    try { host = typeof loc === 'string' ? new URL(href).hostname : loc.hostname; }
    catch { host = location.hostname; }
    if (EXCLUDE_HOSTS.includes(host)) return true;
    for (const re of EXCLUDE_PATTERNS) {
      try { if (re.test(href) || re.test(host)) return true; } catch { }
    }
    return false;
  }

  const SELECTORS = [
    'main', 'article', 'section', 'nav', 'aside', 'header', 'footer',
    'div', 'p', 'ul', 'ol', 'li',
    'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
    'pre', 'code', 'blockquote',
    'figure', 'figcaption',
    'table', 'thead', 'tbody', 'tr', 'td', 'th',
    'a', 'span', 'img', 'canvas', 'video', 'iframe'
  ].join(',');

  // requestIdleCallback polyfill (for older browsers)
  const ric = window.requestIdleCallback
    ? window.requestIdleCallback.bind(window)
    : (cb, opts) => setTimeout(() => cb({
      didTimeout: true,
      timeRemaining: () => 0
    }), (opts && opts.timeout) || 1);

  // ---------------------------
  // State
  // ---------------------------
  let initialized = false;
  let activeEl = null; // current scroll container
  let dragging = false;
  let dragStartY = null;
  let dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
  let scheduled = false;
  let scrollDebounce = null;
  let mutationDebounce = null;

  // element cache (per container)
  let cachedElements = null;
  let cacheInvalidated = true;

  // DOM refs
  let host, shadow, mm, canvas, ctx, viewport;

  // ---------------------------
  // Shadow CSS (scoped)
  // ---------------------------
  const CSS = `
  :host {
    position: fixed;
    top: 0;
    right: 0;
    width: ${MM_WIDTH}px;
    height: 100vh;
    z-index: 2147483647;
    transform: none !important;
    contain: layout style paint;
    display: block;
    pointer-events: auto;
  }

  :host(.hidden) {
    display: none !important;
    pointer-events: none !important;
  }

  #mm {
    position: relative;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    pointer-events: auto;
    cursor: crosshair;

    background: rgba(15, 15, 20, 0.45);
    border-left: 1px solid rgba(255,255,255,0.06);
    box-shadow: -2px 0 12px rgba(0,0,0,0.35);
    backdrop-filter: blur(6px) saturate(1.15);
    -webkit-backdrop-filter: blur(6px) saturate(1.15);
  }

  #canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    image-rendering: crisp-edges;
  }

  #viewport {
    position: absolute;
    left: 0;
    width: 100%;
    pointer-events: none;
    box-sizing: border-box;
    background: rgba(255, 255, 255, 0.10);
    border: none;
    outline: none;
  }

  #mm::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
      linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px),
      linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
    background-size: 100% 8px, 8px 100%;
    pointer-events: none;
  }
  `;

  // ---------------------------
  // Init (Hydration-safe)
  // ---------------------------
  function startWhenSafe() {
    const kick = () => {
      if (initialized) return;
      if (!document.body) return setTimeout(kick, 200);
      if (document.readyState !== 'complete') {
        window.addEventListener('load', () => ric(init, { timeout: 2000 }), { once: true });
        return;
      }
      ric(init, { timeout: 2000 });
    };
    kick();
  }

  function init() {
    if (initialized) return;
    if (!document.body) return setTimeout(init, 200);
    initialized = true;

    createShadowMinimap();

    // Start with main scrolling element
    const rootScroll = document.scrollingElement || document.documentElement;
    setActive(rootScroll);

    attachGlobalListeners();

    sizeCanvas();
    scheduleRender();
  }

  // ---------------------------
  // Shadow DOM component
  // ---------------------------
  function createShadowMinimap() {
    host = document.createElement('mm-vscode-minimap');
    shadow = host.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent = CSS;

    mm = document.createElement('div');
    mm.id = 'mm';
    if (START_HIDDEN) {
      host.classList.add('hidden'); // fully disable overlay area
    }

    canvas = document.createElement('canvas');
    canvas.id = 'canvas';

    viewport = document.createElement('div');
    viewport.id = 'viewport';

    mm.appendChild(canvas);
    mm.appendChild(viewport);
    shadow.appendChild(style);
    shadow.appendChild(mm);

    document.body.appendChild(host);

    ctx = canvas.getContext('2d', { alpha: false });

    mm.addEventListener('mousedown', (e) => {
      if (e.button !== 0) return;
      e.stopPropagation();
      e.stopImmediatePropagation();
      dragging = true;
      dragStartY = e.clientY;
      e.preventDefault();
      handlePointer(e.clientY);
    });

    window.addEventListener('mousemove', (e) => {
      if (!dragging) return;
      e.stopPropagation();
      handlePointer(e.clientY);
    }, { passive: true, capture: true });

    window.addEventListener('mouseup', (e) => {
      if (!dragging) return;
      e.stopPropagation();
      dragging = false;
    }, { passive: true, capture: true });

    mm.addEventListener('click', (e) => {
      e.stopPropagation();
      if (dragStartY !== null && Math.abs(e.clientY - dragStartY) < 3) {
        handlePointer(e.clientY);
      }
      dragStartY = null;
    });

    mm.addEventListener('wheel', (e) => {
      e.stopPropagation();
      e.preventDefault();
      const { scrollHeight, viewportHeight } = getMetrics();
      const delta = Math.sign(e.deltaY) * Math.max(24, viewportHeight * 0.15);
      scrollToY(clamp(getMetrics().scrollTop + delta, 0, Math.max(0, scrollHeight - viewportHeight)));
    }, { passive: false });
  }

  function isEventInsideMinimap(e) {
    const path = (typeof e.composedPath === 'function') ? e.composedPath() : [];
    return path.includes(host) || (mm && path.includes(mm));
  }

  function sizeCanvas() {
    const cssW = MM_WIDTH;
    const cssH = host.clientHeight || window.innerHeight || 0;
    canvas.style.width = cssW + 'px';
    canvas.style.height = cssH + 'px';
    canvas.width = Math.max(1, Math.floor(cssW * dpr));
    canvas.height = Math.max(1, Math.floor(cssH * dpr));
    if (ctx && ctx.setTransform) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // ---------------------------
  // Active Scroll Container
  // ---------------------------
  function setActive(el) {
    const rootScroll = document.scrollingElement || document.documentElement;
    const newActive = el || rootScroll;
    if (newActive === activeEl) return;

    // Ignore our own UI (host or descendants), but allow ancestors like <body>/<html>
    if (host && (newActive === host || (host.contains && host.contains(newActive)))) return;

    // Only activate visible, scrollable containers (unless it's the document scrolling element)
    if (newActive !== rootScroll && !isVisibleContainer(newActive)) {
      return;
    }

    const prev = activeEl;
    activeEl = newActive;

    if (prev) detachContainerListeners(prev);
    attachContainerListeners();

    cacheInvalidated = true;
    sizeCanvas();
    scheduleRender();
  }

  function isVisibleContainer(node) {
    if (!node || node.nodeType !== 1) return false;
    const cs = getComputedStyle(node);
    if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
    const r = node.getBoundingClientRect();
    if (r.width <= 0 || r.height <= 0) return false;
    return isScrollable(node);
  }

  function isScrollable(node) {
    if (!node || node.nodeType !== 1) return false;
    const cs = getComputedStyle(node);
    if (cs.display === 'none') return false;

    const overflowY = cs.overflowY;
    const overflowX = cs.overflowX;
    const overflow = cs.overflow;

    const canScrollY = /(auto|scroll)/.test(overflowY) || /(auto|scroll)/.test(overflow);
    const canScrollX = /(auto|scroll)/.test(overflowX) || /(auto|scroll)/.test(overflow);

    if (!canScrollY && !canScrollX) return false;

    const hasScrollableY = node.scrollHeight > Math.ceil(node.clientHeight);
    const hasScrollableX = node.scrollWidth > Math.ceil(node.clientWidth);

    return (canScrollY && hasScrollableY) || (canScrollX && hasScrollableX);
  }

  function findScrollableParent(el) {
    if (!el) return document.scrollingElement || document.documentElement;
    let cur = el;
    while (cur && cur !== document.documentElement) {
      if (cur === document.body) return document.scrollingElement || document.documentElement;
      if (cur === host) return activeEl || (document.scrollingElement || document.documentElement);
      if (isScrollable(cur)) return cur;
      cur = cur.parentElement;
    }
    return document.scrollingElement || document.documentElement;
  }

  // ---------------------------
  // Metrics
  // ---------------------------
  function getMetrics() {
    const scrollingRoot = document.scrollingElement || document.documentElement;

    // Fallback if current active el is missing or detached
    if (!activeEl || !activeEl.isConnected) activeEl = scrollingRoot;

    const isMain = activeEl === scrollingRoot;
    let scrollTop, scrollHeight, viewportHeight, containerRect, containerWidth;

    if (isMain) {
      scrollTop = scrollingRoot.scrollTop || window.pageYOffset || 0;
      scrollHeight = scrollingRoot.scrollHeight || (document.body && document.body.scrollHeight) || 0;
      viewportHeight = window.innerHeight || scrollingRoot.clientHeight || 0;
      containerRect = { top: 0, left: 0 };
      containerWidth = window.innerWidth || scrollingRoot.clientWidth || 1;
    } else {
      scrollTop = activeEl.scrollTop;
      scrollHeight = activeEl.scrollHeight;
      viewportHeight = activeEl.clientHeight;
      containerRect = activeEl.getBoundingClientRect();
      containerWidth = activeEl.clientWidth || 1;
    }

    return { isMain, scrollTop, scrollHeight, viewportHeight, containerRect, containerWidth };
  }

  // ---------------------------
  // Render Scheduling
  // ---------------------------
  function withRAF(fn) {
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(() => {
      try {
        fn();
      } catch (err) {
        console.error('[Minimap]', err);
      } finally {
        scheduled = false;
      }
    });
  }

  function scheduleRender() {
    withRAF(render);
  }

  function scheduleRenderLight() {
    clearTimeout(scrollDebounce);
    scrollDebounce = setTimeout(() => withRAF(render), 120);
  }

  // ---------------------------
  // Rendering (Layout-Only)
  // ---------------------------
  function render() {
    if (!ctx) return;

    const cssW = canvas.clientWidth || 0;
    const cssH = canvas.clientHeight || 0;
    if (cssW === 0 || cssH === 0) return;

    // Clear
    ctx.fillStyle = 'rgba(18,18,22,0.9)';
    ctx.fillRect(0, 0, cssW, cssH);

    const { isMain, scrollHeight, containerRect, containerWidth } = getMetrics();
    const scaleY = cssH / Math.max(1, scrollHeight);
    const root = isMain ? (document.body || document.documentElement) : activeEl;

    if (!root || !root.querySelectorAll) {
      updateViewport();
      return;
    }

    // Cache elements per container
    if (cacheInvalidated) {
      try {
        cachedElements = Array.from(root.querySelectorAll(SELECTORS))
          .filter(el => isVisibleElement(el));
      } catch {
        cachedElements = [];
      }
      cacheInvalidated = false;
    }

    let elements = cachedElements || [];

    if (elements.length > MAX_PER_RENDER) {
      const step = Math.ceil(elements.length / MAX_PER_RENDER);
      elements = elements.filter((_, i) => i % step === 0);
    }

    const cw = Math.max(1, containerWidth);

    for (const el of elements) {
      const rect = el.getBoundingClientRect();
      let top, left, height, width;

      if (isMain) {
        const scrollingRoot = document.scrollingElement || document.documentElement;
        top = rect.top + (scrollingRoot.scrollTop || window.pageYOffset || 0);
        left = rect.left;
      } else {
        top = (rect.top - containerRect.top) + activeEl.scrollTop;
        left = (rect.left - containerRect.left) + activeEl.scrollLeft;
      }

      height = rect.height;
      width = rect.width;
      if (height <= 0 || width <= 8) continue;

      const x = Math.max(0, Math.min(cssW, (left / cw) * cssW));
      const w = Math.max(1, Math.min(cssW - x, (width / cw) * cssW));
      const y = Math.max(0, top * scaleY);
      const h = Math.max(1, height * scaleY);

      ctx.fillStyle = colorFor(el.tagName.toLowerCase());
      ctx.fillRect(x, y, w, h);
    }

    updateViewport();
  }

  function isVisibleElement(el) {
    if (!el || el.nodeType !== 1) return false;

    const t = el.tagName;
    if (!t || t === 'SCRIPT' || t === 'STYLE' || t === 'LINK' || t === 'META' || t === 'NOSCRIPT') return false;

    const cs = getComputedStyle(el);
    if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;

    // Exclude fixed elements (not part of scroll layout mapping)
    if (cs.position === 'fixed') return false;

    const r = el.getBoundingClientRect();
    if (r.width <= 0 || r.height <= 0) return false;

    return true;
  }

  function colorFor(tag) {
    switch (tag) {
      case 'h1': return '#5BC0EB';
      case 'h2': return '#4FA8E0';
      case 'h3': return '#3F95D0';
      case 'h4': return '#2E86C1';
      case 'h5': return '#2278B6';
      case 'h6': return '#1B6AA7';
      case 'a': return '#4EC9B0';
      case 'code':
      case 'pre': return '#CE9178';
      case 'blockquote': return '#7CB342';
      case 'img': return '#C792EA';
      case 'table': return '#A5D6A7';
      case 'ul':
      case 'ol':
      case 'li': return '#B2CCD6';
      case 'section':
      case 'article':
      case 'main':
      case 'aside':
      case 'nav':
      case 'header':
      case 'footer': return '#9CDCFE';
      case 'div': return '#8A8A8A';
      case 'span': return '#6E6E6E';
      case 'iframe': return '#F78C6C';
      default: return '#7A7A7A';
    }
  }

  // ---------------------------
  // Viewport box
  // ---------------------------
  function updateViewport() {
    if (!viewport) return;
    const { scrollTop, scrollHeight, viewportHeight } = getMetrics();
    const canvasH = canvas.clientHeight || 0;
    if (!canvasH || !scrollHeight) {
      viewport.style.top = '0px';
      viewport.style.height = MIN_VP_PX + 'px';
      return;
    }
    const scaleY = canvasH / Math.max(1, scrollHeight);
    const vpTop = Math.max(0, Math.floor(scrollTop * scaleY));
    const vpH = Math.max(MIN_VP_PX, Math.floor(viewportHeight * scaleY));

    viewport.style.top = vpTop + 'px';
    viewport.style.height = vpH + 'px';
  }

  // ---------------------------
  // Pointer -> Scroll
  // ---------------------------
  function handlePointer(clientY) {
    const rect = canvas.getBoundingClientRect();
    const h = Math.max(1, canvas.clientHeight || (rect.bottom - rect.top));
    const y = clamp(clientY - rect.top, 0, h);
    const { scrollHeight, viewportHeight } = getMetrics();
    if (!scrollHeight) return;
    const target = (y / h) * scrollHeight - (viewportHeight / 2);
    scrollToY(clamp(target, 0, Math.max(0, scrollHeight - viewportHeight)));
  }

  function scrollToY(y) {
    const scrollingRoot = document.scrollingElement || document.documentElement;
    const el = (!activeEl || !activeEl.isConnected) ? scrollingRoot : activeEl;
    if (el === scrollingRoot) {
      scrollingRoot.scrollTop = y;
    } else {
      el.scrollTop = y;
    }
  }

  function jumpToPercent(p) {
    const { scrollHeight, viewportHeight } = getMetrics();
    const max = Math.max(0, scrollHeight - viewportHeight);
    scrollToY(p * max);
  }

  // ---------------------------
  // Helpers
  // ---------------------------
  function clamp(v, min, max) { return v < min ? min : (v > max ? max : v); }

  function attachGlobalListeners() {
    let lastHoverCheck = 0;

    // Hover to switch active scroll container (throttled)
    document.addEventListener('mouseover', (e) => {
      if (isEventInsideMinimap(e)) return;

      const now = Date.now();
      if (now - lastHoverCheck < HOVER_THROTTLE_MS) return;
      lastHoverCheck = now;

      const el = findScrollableParent(e.target);
      if (el !== activeEl) setActive(el);
    }, true);

    // Wheel inside nested scrollables also switches
    document.addEventListener('wheel', (e) => {
      if (isEventInsideMinimap(e)) return;
      const el = findScrollableParent(e.target);
      if (el !== activeEl) setActive(el);
    }, { passive: true });

    // Keyboard
    document.addEventListener('keydown', (e) => {
      if (isEventInsideMinimap(e)) {
        // Allow toggles even if focus is elsewhere
      }
      const tag = e.target && e.target.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target && e.target.isContentEditable)) return;

      const key = (e.key || '').toLowerCase();
      const wants = TOGGLE_KEY.toLowerCase();

      // Toggle minimap using configurable shortcut
      if (
        key === wants &&
        (!TOGGLE_MOD.ctrl || e.ctrlKey) &&
        (!TOGGLE_MOD.alt || e.altKey) &&
        (!TOGGLE_MOD.shift || e.shiftKey) &&
        (!TOGGLE_MOD.meta || e.metaKey)
      ) {
        e.preventDefault();
        host.classList.toggle('hidden');
        if (!host.classList.contains('hidden')) {
          // When showing, trigger a render
          cacheInvalidated = true;
          scheduleRender();
        }
        return;
      }

      // Keep number jumps behavior (optional to keep/change)
      if (key >= '0' && key <= '9') {
        e.preventDefault();
        if (key === '1') { jumpToPercent(0); return; }
        if (key === '0') { jumpToPercent(1); return; }
        jumpToPercent(parseInt(key, 10) / 10);
      }
    });

    // Window resize
    window.addEventListener('resize', () => {
      dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
      sizeCanvas();
      cacheInvalidated = true;
      scheduleRender();
    }, { passive: true });
  }

  function attachContainerListeners() {
    if (!activeEl) return;

    const onScroll = () => scheduleRenderLight();

    const onMut = () => {
      cacheInvalidated = true;
      clearTimeout(mutationDebounce);
      mutationDebounce = setTimeout(() => scheduleRender(), 200);
    };

    // Attach scroll
    const scrollingRoot = document.scrollingElement || document.documentElement;
    if (activeEl === scrollingRoot) {
      const options = { passive: true, capture: true };
      window.addEventListener('scroll', onScroll, options);
      activeEl.__mmScrollHandler = onScroll;
      activeEl.__mmScrollOptions = options;
    } else {
      const options = { passive: true };
      activeEl.addEventListener('scroll', onScroll, options);
      activeEl.__mmScrollHandler = onScroll;
      activeEl.__mmScrollOptions = options;
    }

    // Observe size + DOM changes
    const observeTarget = (activeEl === scrollingRoot ? (document.body || document.documentElement) : activeEl);

    if (window.ResizeObserver) {
      const ro = new ResizeObserver(() => {
        cacheInvalidated = true;
        scheduleRender();
      });
      try { ro.observe(observeTarget); } catch { }
      activeEl.__mmResizeObserver = ro;
    }

    const mo = new MutationObserver(onMut);
    try { mo.observe(observeTarget, { childList: true, subtree: true, attributes: true }); } catch { }
    activeEl.__mmMutObserver = mo;
  }

  function detachContainerListeners(target) {
    const prev = target || (document.scrollingElement || document.documentElement);

    if (prev.__mmResizeObserver) {
      try { prev.__mmResizeObserver.disconnect(); } catch { }
      delete prev.__mmResizeObserver;
    }

    if (prev.__mmMutObserver) {
      try { prev.__mmMutObserver.disconnect(); } catch { }
      delete prev.__mmMutObserver;
    }

    if (prev.__mmScrollHandler) {
      const scrollingRoot = document.scrollingElement || document.documentElement;
      const isMain = prev === scrollingRoot;
      const scrollTarget = isMain ? window : prev;
      const options = prev.__mmScrollOptions || (isMain ? { capture: true } : undefined);
      try {
        scrollTarget.removeEventListener('scroll', prev.__mmScrollHandler, options);
      } catch (_) {
        scrollTarget.removeEventListener('scroll', prev.__mmScrollHandler);
      }
      delete prev.__mmScrollHandler;
      delete prev.__mmScrollOptions;
    }
  }

  // ---------------------------
  // Start
  // ---------------------------
  if (isExcluded(location)) return;
  startWhenSafe();
})();