Scroll entire page smoothly with long left-click and drag.
// ==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.3p // @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 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( 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 }); 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."); } }