Scroll Everywhere

Scroll entire page smoothly with long left-click and drag.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name            Scroll Everywhere
// @description     Scroll entire page smoothly with long left-click and drag.
// @author          tumpio
// @oujs:author     tumpio
// @contributor     joeytwiddle
// @namespace       [email protected]
// @homepageURL     https://openuserjs.org/scripts/tumpio/Scroll_Everywhere
// @supportURL      https://github.com/tumpio/gmscripts
// @icon            https://raw.githubusercontent.com/tumpio/gmscripts/master/Scroll_Everywhere/large.png
// @include         *
// @grant           GM_addStyle
// @run-at          document-body
// @version         0.3q
// @license         MIT
// ==/UserScript==

// Does not work on Steam: https://steamcommunity.com/discussions/forum/10/458604254435648435/?ctp=9

// This is a version of tumpio's script which defaults to left-click drag after a long press, and will do a relative scroll of the entire page.  I find this more intuitive.

/* jshint multistr: true, strict: false, browser: true, devel: true */
/* global escape: true,GM_getValue: true,GM_setValue: true,GM_addStyle: true,GM_xmlhttpRequest: true */

/* eslint-disable eqeqeq */
/* eslint-disable curly */
/* eslint-disable no-redeclare */

// TODO: add slow scroll start mode
// FIXME: Linux/mac context menu on mousedown, probably needs browser level
// FUTURE: Options dialog

// ISSUES:
//   The fix for scrollbars works, but we need a similar for for other widgets, for example native dropdown menu widgets.

var mouseBtn, reverse, stopOnSecondClick, verticalScroll, startAnimDelay, cursorStyle, down,
    scrollevents, scrollBarWidth, cursorMask, isWin, fScrollX, fScrollY, fScroll, slowScrollStart;

var middleIsStart, startX, startY, startScrollTop, startScrollLeft, lastScrollHeight;

var relativeScrolling, lastX, lastY, scaleX, scaleY, power, offsetMiddle;

var lastMiddleClickTime;

var startAfterLongPress, longPressTimer, eventBeforeLongPress, longPressStylesAdded;

var scrollStartTime, scrollStopTime;

var elementToScroll;

// NOTE: Do not run on iframes
if (window.top === window.self) {
    // USER SETTINGS
    mouseBtn = 1; // 1:left, 2:middle, 3:right mouse button
    startAfterLongPress = true; // Only start scrolling after a long click
    reverse = true; // reversed scroll direction
    stopOnSecondClick = false; // keep scrolling until the left mouse button clicked
    verticalScroll = false; // vertical scrolling
    slowScrollStart = false; // slow scroll start on begin
    startAnimDelay = 150; // slow scroll start mode animation delay
    cursorStyle = "grab"; // cursor style on scroll
    middleIsStart = true; // don't jump when the mouse starts moving
    relativeScrolling = false; // scroll the page relative to where we are now
    scaleX = 3; // how fast to scroll with relative scrolling (disable to scroll full page in one gesture)
    scaleY = 3;
    power = 3; // when moving the mouse faster, how quickly should it speed up?
    // END

    fScroll = ((reverse) ? fRevPos : fPos);
    fScrollX = ((verticalScroll) ? fScroll : noScrollX);
    fScrollY = fScroll;
    down = false;
    scrollevents = 0;
    scrollBarWidth = 2 * getScrollBarWidth();
    cursorMask = document.createElement('div');
    isWin = window.navigator.appVersion.indexOf("Win") >= 0;
    if (cursorStyle === "grab")
        cursorStyle = "-webkit-grabbing; cursor: -moz-grabbing";
    cursorMask.id = "SE_cursorMask_cursor";
    cursorMask.setAttribute("style", "position: fixed; width: 100%; height: 100%; zindex: 5000; top: 0px; left: 0px; cursor: " + cursorStyle + "; background: none; display: none;");
    document.body.appendChild(cursorMask);

    window.addEventListener("mousedown", handleMouseDown, false);
    window.addEventListener("mouseup", handleMouseUp, false);
    window.addEventListener("click", handleClick, true);
    window.addEventListener('paste', handlePaste, true);
}

function handleMouseDown(e) {
    // From: https://stackoverflow.com/questions/10045423/determine-whether-user-clicking-scrollbar-or-content-onclick-for-native-scroll
    var wasClickOnScrollbar = e.target.clientWidth > 0 && e.offsetX > e.target.clientWidth || e.target.clientHeight > 0 && e.offsetY > e.target.clientHeight;
    if (wasClickOnScrollbar) {
        //console.log('Ignoring click on scrollbar:', e, `${e.offsetX} > ${e.target.clientWidth} || ${e.offsetY} > ${e.target.clientHeight}`);
        return;
    }
    if (e.which == mouseBtn) {
        if (startAfterLongPress) {
            startLongPress(e);
        } else {
            if (!down) {
                start(e);
            } else {
                stop();
            }
        }
    }
}

function handleMouseUp(e) {
    if (e.which == 2) {
        lastMiddleClickTime = Date.now();
    }
    if (startAfterLongPress) {
        cancelLongPress();
    }
}

function handleClick(e) {
    // If we were just in scrolling mode, then we don't want other listeners to see this click event
    var justStoppedScrolling = Date.now() <= scrollStopTime + 20;
    // But if we went in and out of scrolling mode in a short time, then this was actually a click
    var wasShortClick = !startAfterLongPress && scrollStopTime - scrollStartTime < 200;
    if (justStoppedScrolling && !wasShortClick) {
        //console.info("MUTING click event");
        e.preventDefault();
        e.stopPropagation();
    }
}

function handlePaste(e) {
    var timeSinceLastMiddleClick = Date.now() - lastMiddleClickTime;
    //console.log("Pasting (" + timeSinceLastMiddleClick + "ms):", (event.clipboardData || window.clipboardData).getData('text'));

    // If you use middle button for scrolling on Linux, then you might be sending a paste event every time you use this scroller.
    // Depending on the contents of your clipboard, that could be a privacy leak!
    // Therefore we disable paste events if they come after a middle click (if the user uses middle click for scrolling).
    //
    // Note this solution is still not entirely safe.  There could be an event listener registered before us, which would see the paste.
    // Another option is to disable middle-click but this also isn't trivial to do universally: https://askubuntu.com/questions/4507
    //
    // TODO: It would be better to check if this was a middle-click drag (i.e. a scroll).  A plain short middle-click we could interpret as a paste.

    if (mouseBtn == 2 && timeSinceLastMiddleClick < 200) {
        e.preventDefault();
        e.stopPropagation();
        return false;
    }
}

function startLongPress(e) {
    cancelLongPress();
    eventBeforeLongPress = e;
    longPressTimer = setTimeout(longPressDetected, 500);
    window.addEventListener("mousemove", cancelLongPress, false);
}

function longPressDetected() {
    // Cleanup
    cancelLongPress();
    if (mouseBtn == 1) {
        // After a long press with the left mouse button, the browser will start selecting text, which will get messy when we scroll
        // So we try to cancel that selection
        selectNoText();
    }
    start(eventBeforeLongPress);
    // Give the user a visual indication that scrolling mode has started
    cursorMask.style.display = "";
    // A stronger indication: a ripple effect starting from the mouse location
    // This is especially useful when our pointer change is overriden by the page's CSS
    // Based on: https://css-tricks.com/how-to-recreate-the-ripple-effect-of-material-design-buttons/
    if (!longPressStylesAdded) {
        GM_addStyle(`
            #scroll-anywhere-ripple-animation {
                position: fixed;
                width: 20px;
                height: 20px;
                border-radius: 50%;
                transform: scale(0);
                animation: ripple 600ms ease-out;
                background-color: #aaa8;
                z-index: 999999;
                pointer-events: none;
            }
            @keyframes ripple {
                from {
                    transform: scale(0);
                    opacity: 1;
                }
                to {
                    transform: scale(16);
                    opacity: 0;
                }
            }
        `);
        longPressStylesAdded = true;
    }
    var circleDiv = document.createElement('div');
    circleDiv.id = 'scroll-anywhere-ripple-animation';
    circleDiv.style.left = (eventBeforeLongPress.clientX - 10) + 'px';
    circleDiv.style.top = (eventBeforeLongPress.clientY - 10) + 'px';
    document.body.appendChild(circleDiv);
    setTimeout(() => {
        circleDiv.parentNode.removeChild(circleDiv);
    }, 2000);
}

function cancelLongPress() {
    clearTimeout(longPressTimer);
    window.removeEventListener("mousemove", cancelLongPress);
}

function start(e) {
    down = true;
    elementToScroll = findElementToScroll(e.target);
    //console.log('Will do scrolling on:', elementToScroll, elementToScroll.scrollTop, elementToScroll.scrollHeight, getComputedStyle(elementToScroll).overflow);
    scrollStartTime = Date.now();
    setStartData(e);
    lastX = e.clientX;
    lastY = e.clientY;
    if (!slowScrollStart)
        scroll(e);
    window.addEventListener("mousemove", waitScroll, false);
    if (!stopOnSecondClick)
        window.addEventListener("mouseup", stop, false);
}

function findElementToScroll(elem) {
    if (elem.clientHeight > 0 && elem.scrollHeight > elem.clientHeight) {
        var overflow = getComputedStyle(elem).overflow;
        if (overflow === '' || overflow.match(/(auto|scroll|overlay)/)) {
            //console.log('overflow:', overflow);
            return elem;
        }
    }
    if (!elem.parentNode) {
        // On some sites, documentElement works better than body
        return document.documentElement.scrollHeight > 0 ? document.documentElement : document.body;
    }
    return findElementToScroll(elem.parentNode);
}

function setStartData(e) {
    lastScrollHeight = getScrollHeight();
    startX = e.clientX;
    startY = e.clientY;
    // On some pages, body.scrollTop changes whilst documentElement.scrollTop remains 0.
    // For example: https://docs.kde.org/trunk5/en/kde-workspace/kcontrol/autostart/index.html
    // See: https://stackoverflow.com/questions/19618545
    startScrollTop = elementToScroll.scrollTop || 0;
    startScrollLeft = elementToScroll.scrollLeft || 0;
    if (elementToScroll === document.documentElement || elementToScroll === document.body) {
        startScrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
        startScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0;
    }
}

function waitScroll(e) {
    scrollevents += 1;
    if (scrollevents > 2) {
        cursorMask.style.display = "";
        if (isWin)
            document.oncontextmenu = fFalse;
        window.removeEventListener("mousemove", waitScroll, false);
        window.addEventListener("mousemove", scroll, false);
    }
}

function scroll(e) {
    // If the site has just changed the height of the webpage (e.g. by auto-loading more content)
    // then we must adapt to the new height to avoid jumping.
    if (lastScrollHeight !== getScrollHeight()) {
        setStartData(e);
    }
    //scrollevents += 1;
    if (!stopOnSecondClick && e.buttons === 0) {
        stop();
        return;
    }
    if (relativeScrolling) {
      var diffX = e.clientX - lastX;
      var diffY = e.clientY - lastY;
      var distance = Math.sqrt(diffX * diffX + diffY * diffY);
      var velocity = 1 + distance * power / 100;
      var reverseScale = reverse ? -1 : 1;
      //doScrollTo(window, window.scrollX + diffX * scaleX * velocity * reverseScale, window.scrollY + diffY * scaleY * velocity * reverseScale);
      doScrollTo(elementToScroll, elementToScroll.scrollLeft + diffX * scaleX * velocity * reverseScale, elementToScroll.scrollTop + diffY * scaleY * velocity * reverseScale);
      lastX = e.clientX;
      lastY = e.clientY;
      return;
    }
    var newX = fScrollX(
        window.innerWidth - scrollBarWidth,
        getScrollWidth() - getClientWidth(),
        e.clientX);
    var newY = fScrollY(
        // Applying this `- scrollBarWidth` when there is no bottom scroll bar, means we cannot scroll to the top of the page!
        // It would be better if we would only do this subtraction, if we detect that a bottom scrollbar really exists.
        // For now, we will assume there isn't one, which is usually the case.
        window.innerHeight /* - scrollBarWidth */,
        getScrollHeight() - getClientHeight(),
        e.clientY);
    doScrollTo(elementToScroll, newX, newY);
}

function doScrollTo(elem, x, y) {
    //console.log(`Doing scroll: ${x} ${y}`);
    // For normal HTML elements
    elem.scrollTo(x, y);
    // For React Native elements
    elem.scrollTo({ x: x, y: y, animated: false });
    // Sometimes this is needed, but it can result in a smooth scroll
    // (The case I found that needed it was when elem === documentElement)
    elem.scrollTop = y;
    if (elem === document.documentElement) {
        document.body.scrollTo(x, y);
        document.body.scrollTo({ x: x, y: y, animated: false });
    }
    if (elem === document.body) {
        document.documentElement.scrollTo(x, y);
        document.documentElement.scrollTo({ x: x, y: y, animated: false });
    }
}

function stop() {
    cursorMask.style.display = "none";
    if (isWin)
        document.oncontextmenu = !fFalse;
    down = false;
    scrollStopTime = Date.now();
    scrollevents = 0;
    window.removeEventListener("mouseup", stop, false);
    window.removeEventListener("mousemove", scroll, false);
    window.removeEventListener("mousemove", waitScroll, false);
}

function noScrollX() {
    return elementToScroll.scrollLeft;
}

function fPos(win, doc, pos) {
    if (middleIsStart) {
        if (pos < startY) {
            return startScrollTop * pos / startY;
        } else {
            return startScrollTop + (doc - startScrollTop) * (pos - startY) / (win - startY);
        }
    }
    return doc * (pos / win);
}

function fRevPos(win, doc, pos) {
    if (middleIsStart) {
        if (pos < startY) {
            return startScrollTop + (doc - startScrollTop) * (startY - pos) / startY;
        } else {
            return startScrollTop - startScrollTop * (pos - startY) / (win - startY);
        }
    }
    return doc - fPos(win, doc, pos);
}

function getScrollHeight() {
    return elementToScroll.scrollHeight || 0;
}

function getScrollWidth() {
  return elementToScroll.scrollWidth || 0;
}

function getClientHeight(e) {
    // Sometimes documentElement will return the full scrollHeight, but we want the smaller visible portal that body returns
    if (elementToScroll === document.documentElement || elementToScroll === document.body) {
        return Math.min(document.documentElement.clientHeight, document.body.clientHeight);
    }
    return elementToScroll.clientHeight || 0;
}

function getClientWidth(e) {
    if (elementToScroll === document.documentElement || elementToScroll === document.body) {
        return Math.min(document.documentElement.clientWidth, document.body.clientWidth);
    }
    return elementToScroll.clientWidth || 0;
}

function getScrollBarWidth() {
    var originalOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    var width = document.body.clientWidth;
    document.body.style.overflow = 'scroll';
    width -= document.body.clientWidth;
    if (!width) width = document.body.offsetWidth - document.body.clientWidth;

    // Now we set overflow back to how it was
    // But if style === '' then Firefox will sometimes leave the temporary scrollbar still showing!
    // We can prevent that by setting it to 'initial', and forcing a relayout, before setting it to ''
    document.body.style.overflow = originalOverflow || 'initial';
    var triggerLayout = document.body.clientWidth;
    document.body.style.overflow = originalOverflow;

    return width;
}

function fFalse() {
    return false;
}

function slowF(x) {
    return 1 / (1 + Math.pow(Math.E, (-0.1 * x)));
}

function selectNoText() {
    if (document.body.createTextRange) {
        const range = document.body.createTextRange();
        range.select();
    } else if (window.getSelection) {
        const selection = window.getSelection();
        const range = document.createRange();
        selection.removeAllRanges();
    } else {
        console.warn("Could not unselect text: Unsupported browser.");
    }
}