Instant Jump Scrollbar

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

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

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