Instant Jump Scrollbar

Instant quick jumps: 1=top, 0=bottom, 2-9=20%->90%. Click anywhere on the custom scrollbar to jump there.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        Instant Jump Scrollbar
// @namespace   https://github.com/quantavil
// @version     1.0.5
// @description Instant quick jumps: 1=top, 0=bottom, 2-9=20%->90%. Click anywhere on the custom scrollbar to jump there.
// @match       *://*/*

// --- Music & Audio Only ---
// @exclude     *://music.youtube.com/*
// @exclude     *://*.soundcloud.com/*
// @exclude     *://*.deezer.com/*
// @exclude     *://*.pandora.com/*
// @exclude     *://music.apple.com/*
// @exclude     *://*.tidal.com/*
// @exclude     *://*.gaana.com/*
// @exclude     *://*.jiosaavn.com/*
// @exclude     *://*.wynk.in/*

// --- Productivity, Cloud & Office Suites (Prevents Key Conflicts with Data Entry) ---
// @exclude     *://super-productivity.com/*
// @exclude     *://app.super-productivity.com/*
// @exclude     *://calendar.google.com/*
// @exclude     *://docs.google.com/*
// @exclude     *://drive.google.com/*
// @exclude     *://mail.google.com/*
// @exclude     *://keep.google.com/*
// @exclude     *://meet.google.com/*
// @exclude     *://contacts.google.com/*
// @exclude     *://*.office.com/*
// @exclude     *://outlook.live.com/*
// @exclude     *://*.microsoft365.com/*
// @exclude     *://*.notion.so/*
// @exclude     *://*.trello.com/*
// @exclude     *://*.asana.com/*
// @exclude     *://*.atlassian.net/*
// @exclude     *://*.jira.com/*
// @exclude     *://*.monday.com/*
// @exclude     *://*.clickup.com/*
// @exclude     *://*.linear.app/*
// @exclude     *://*.miro.com/*
// @exclude     *://*.figma.com/*
// @exclude     *://*.canva.com/*
// @exclude     *://*.dropbox.com/*
// @exclude     *://*.box.com/*
// @exclude     *://*.onedrive.live.com/*
// @exclude     *://*.evernote.com/*

// --- Search Engines & Maps ---
// @exclude     *://www.google.*/*
// @exclude     *://*.google.*/*
// @exclude     *://search.brave.com/*
// @exclude     *://*.bing.com/*
// @exclude     *://*.duckduckgo.com/*
// @exclude     *://*.yahoo.com/*
// @exclude     *://*.baidu.com/*
// @exclude     *://*.yandex.com/*
// @exclude     *://*.ecosia.org/*
// @exclude     *://*.startpage.com/*
// @exclude     *://*.google.com/maps/*
// @exclude     *://*.openstreetmap.org/*

// --- Trading, Visual Editors, and Desktop-like Apps (High Interference Risk) ---
// @exclude     *://*.tradingview.com/*
// @exclude     *://*.webflow.com/*
// @exclude     *://*.adobe.com/*
// @exclude     *://app.postman.com/*
// @exclude     *://*.framer.com/*
// @exclude     *://*.zotero.org/*

// --- Banking & Finance (Global) ---
// @exclude     *://*.paypal.com/*
// @exclude     *://*.stripe.com/*
// @exclude     *://*.wise.com/*
// @exclude     *://*.revolut.com/*
// @exclude     *://*.americanexpress.com/*
// @exclude     *://*.mastercard.com/*
// @exclude     *://*.visa.com/*

// --- Banking & Finance (Indian) ---
// @exclude     *://*.onlinesbi.sbi/*
// @exclude     *://retail.onlinesbi.com/*
// @exclude     *://*.hdfcbank.com/*
// @exclude     *://netbanking.hdfcbank.com/*
// @exclude     *://*.icicibank.com/*
// @exclude     *://infinity.icicibank.com/*
// @exclude     *://*.axisbank.com/*
// @exclude     *://*.kotak.com/*
// @exclude     *://*.pnbindia.in/*
// @exclude     *://*.bankofbaroda.in/*
// @exclude     *://*.canarabank.com/*
// @exclude     *://*.unionbankofindia.co.in/*
// @exclude     *://*.idfcfirstbank.com/*
// @exclude     *://*.indusind.com/*
// @exclude     *://*.yesbank.in/*
// @exclude     *://*.rblbank.com/*
// @exclude     *://*.idbibank.in/*
// @exclude     *://*.paytm.com/*
// @exclude     *://*.phonepe.com/*
// @exclude     *://*.razorpay.com/*

// --- Government & Official (India) ---
// @exclude     *://*.gov.in/*
// @exclude     *://*.uidai.gov.in/*
// @exclude     *://*.incometax.gov.in/*
// @exclude     *://*.gst.gov.in/*
// @exclude     *://*.epfindia.gov.in/*
// @exclude     *://*.passportindia.gov.in/*
// @exclude     *://*.irctc.co.in/*

// --- Security & Password Managers ---
// @exclude     *://*.lastpass.com/*
// @exclude     *://*.1password.com/*
// @exclude     *://*.bitwarden.com/*
// @exclude     *://*.dashlane.com/*

// @run-at     document-start
// @license    MIT
// ==/UserScript==


(() => {
  'use strict';

  const MIN_THUMB = 30, Z = 9999999;
  const root = () => document.scrollingElement || document.documentElement || document.body;
  let activeC = null;
  let lastEl = null;
  const cp = (e) => (typeof e.composedPath === 'function') ? e.composedPath()[0] : e.target;
  const isScrollable = (el) => {
    if (!el || !el.nodeType) return false;
    const cs = getComputedStyle(el);
    if (cs.display === 'none' || cs.visibility === 'hidden') return false;
    const oy = (cs.overflowY || cs.overflow || '').toLowerCase();
    return /auto|scroll|overlay/.test(oy) && el.scrollHeight > el.clientHeight + 1;
  };
  const findScrollable = (t) => {
    if (!t) return root();
    for (let el = t; el && el !== document.documentElement; el = el.parentElement) {
      if (el === document.body) return root();
      if (isScrollable(el)) return el;
    }
    return root();
  };
  const getActive = () => {
    const a = activeC && isScrollable(activeC) ? activeC : null;
    return a || findScrollable(document.activeElement) || root();
  };
  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  function metrics(el) {
    const r = el || getActive();
    const isRoot = r === root();
    const vh = isRoot ? window.innerHeight : r.clientHeight;
    const sh = Math.max((isRoot ? (r.scrollHeight || 0) : r.scrollHeight) || 0, vh);
    const max = Math.max(0, sh - vh);
    return { el: r, scroll: r.scrollTop || 0, vh, sh, max };
  }

  function injectCSS() {
    const css = `
      /* Hide native vertical scrollbar on root */
      html::-webkit-scrollbar:vertical, body::-webkit-scrollbar:vertical { width:0!important; }
      html, body { scrollbar-width:none!important; -ms-overflow-style:none!important; }

      #ij-bar {
        position: fixed; right: 0; top: 0; height: 100vh; width: 10px;
        background: rgba(0,0,0,0.10);
        z-index: ${Z};
      }
      #ij-thumb {
        position: absolute; left: 1px; right: 1px; top: 0;
        min-height: ${MIN_THUMB}px;
        background: rgba(120,120,120,0.7);
        border-radius: 6px;
        will-change: transform;
        user-select: none; touch-action: none; cursor: grab;
      }
      #ij-thumb:active { cursor: grabbing; }
    `;
    const st = document.createElement('style');
    st.textContent = css;
    (document.head || document.documentElement).appendChild(st);
  }

  function createBar() {
    const bar = document.createElement('div');
    bar.id = 'ij-bar';
    const thumb = document.createElement('div');
    thumb.id = 'ij-thumb';
    bar.appendChild(thumb);
    document.body.appendChild(bar);

    let raf = 0;
    const update = () => {
      const s = metrics(getActive());
      const visible = s.max > 0;
      bar.style.display = visible ? 'block' : 'none';
      if (!visible) return;
      const barH = window.innerHeight;
      const th = Math.max(MIN_THUMB, barH * (s.vh / Math.max(s.sh, 1)));
      const y = s.max ? (barH - th) * (s.scroll / s.max) : 0;
      thumb.style.height = th + 'px';
      thumb.style.transform = `translateY(${Math.round(y)}px)`;
    };
    const req = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(update); };
    const setActive = (el) => {
      const next = el || root();
      if (lastEl && lastEl !== next) lastEl.removeEventListener('scroll', req);
      if (next && lastEl !== next) next.addEventListener('scroll', req, { passive: true });
      lastEl = next;
      activeC = next;
    };

    window.addEventListener('scroll', req, { passive: true });
    window.addEventListener('resize', req, { passive: true });
    document.addEventListener('wheel', (e) => { setActive(findScrollable(cp(e))); req(); }, { passive: true });
    document.addEventListener('mouseover', (e) => { setActive(findScrollable(cp(e))); }, { passive: true });
    document.addEventListener('focusin', (e) => { setActive(findScrollable(cp(e))); req(); }, { passive: true });
    setActive(findScrollable(document.activeElement));

    // Click on track -> jump to that percentage
    bar.addEventListener('click', (e) => {
      if (e.target !== bar) return;
      const rect = bar.getBoundingClientRect();
      const p = clamp((e.clientY - rect.top) / rect.height, 0, 1);
      const s = metrics(getActive());
      s.el.scrollTop = Math.round(s.max * p);
      req();
    });

    // Drag thumb (instant)
    thumb.addEventListener('pointerdown', (e) => {
      e.preventDefault();
      thumb.setPointerCapture(e.pointerId);
      const startY = e.clientY;
      const s0 = metrics(getActive());
      const rect = bar.getBoundingClientRect();
      const th = thumb.offsetHeight || MIN_THUMB;
      const space = Math.max(1, rect.height - th);
      const onMove = (ev) => {
        const dy = ev.clientY - startY;
        const s = metrics(s0.el);
        const delta = (dy / space) * s.max;
        s.el.scrollTop = clamp(s0.scroll + delta, 0, s.max);
        req();
      };
      const onUp = () => {
        thumb.releasePointerCapture(e.pointerId);
        window.removeEventListener('pointermove', onMove);
        window.removeEventListener('pointerup', onUp);
        window.removeEventListener('pointercancel', onUp);
      };
      window.addEventListener('pointermove', onMove);
      window.addEventListener('pointerup', onUp, { once: true });
      window.addEventListener('pointercancel', onUp, { once: true });
    });

    req(); // initial
  }

  function isEditable(el) {
    const t = el && el.tagName;
    return el && (el.isContentEditable || t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT');
  }
  function getDigit(e) {
    const k = e.key;
    if (k && k.length === 1 && k >= '0' && k <= '9') return k;
    const c = e.code || '';
    if (/^Digit[0-9]$/.test(c)) return c.slice(5);
    if (/^Numpad[0-9]$/.test(c)) return c.slice(6);
    return null;
  }
  function bindKeys() {
    const onKey = (e) => {
      if (e.repeat) return;
      if (e.ctrlKey || e.metaKey || e.altKey || isEditable(e.target)) return;
      const d = getDigit(e);
      if (d == null) return;
      e.preventDefault();
      e.stopPropagation();
      if (e.stopImmediatePropagation) e.stopImmediatePropagation();
      const s = metrics(getActive());
      let p;
      if (d === '1') p = 0;
      else if (d === '0') p = 1;
      else p = parseInt(d, 10) / 10;
      s.el.scrollTop = Math.round(s.max * p);
      req();
    };
    document.addEventListener('keydown', onKey, true);
  }

  function start() {
    injectCSS();
    createBar();
    bindKeys();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else if (document.body) {
    start();
  } else {
    window.addEventListener('load', start, { once: true });
  }
})();