From b5ac61129fb3b6e41b56f603d7eaa6fe724bc8ff Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Thu, 10 Mar 2022 15:16:07 +0800 Subject: [PATCH 1/9] Load script earlier: run at document-body --- Scroll_Everywhere/scrolle.user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index 409d250..f6d8474 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -9,7 +9,8 @@ // @icon https://raw.githubusercontent.com/tumpio/gmscripts/master/Scroll_Everywhere/large.png // @include * // @grant none -// @version 0.3d +// @run-at document-body +// @version 0.3i // ==/UserScript== /* jshint multistr: true, strict: false, browser: true, devel: true */ From 0e30187e2be5f287c684de1a6972c6c162bb53bd Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Thu, 10 Mar 2022 15:16:46 +0800 Subject: [PATCH 2/9] Add option startAfterLongPress --- Scroll_Everywhere/scrolle.user.js | 84 +++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index f6d8474..3fe4b90 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -10,7 +10,7 @@ // @include * // @grant none // @run-at document-body -// @version 0.3i +// @version 0.3k // ==/UserScript== /* jshint multistr: true, strict: false, browser: true, devel: true */ @@ -27,10 +27,13 @@ var middleIsStart, startX, startY, startScrollTop, startScrollLeft, lastScrollHe var relativeScrolling, lastX, lastY, scaleX, scaleY, power, offsetMiddle; +var startAfterLongPress, longPressTimer, eventBeforeLongPress; + // NOTE: Do not run on iframes if (window.top === window.self) { // USER SETTINGS mouseBtn = 3; // 1:left, 2:middle, 3:right mouse button + startAfterLongPress = false; // Only start scrolling after a long click reverse = false; // reversed scroll direction stopOnSecondClick = false; // keep scrolling until the left mouse button clicked verticalScroll = false; // vertical scrolling @@ -58,27 +61,67 @@ if (window.top === window.self) { 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", rightMbDown, false); + window.addEventListener("mousedown", handleMouseDown, false); + window.addEventListener("mouseup", handleMouseUp, false); } -function rightMbDown(e) { +function handleMouseDown(e) { if (e.which == mouseBtn) { - if (!down) { - down = true; - setStartData(e); - lastX = e.clientX; - lastY = e.clientY; - if (!slowScrollStart) - scroll(e); - window.addEventListener("mousemove", waitScroll, false); - if (!stopOnSecondClick) - window.addEventListener("mouseup", stop, false); + if (startAfterLongPress) { + startLongPress(e); } else { - stop(); + if (!down) { + start(e); + } else { + stop(); + } } } } +function handleMouseUp(e) { + if (startAfterLongPress) { + cancelLongPress(); + } +} + +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 = ""; +} + +function cancelLongPress() { + clearTimeout(longPressTimer); + window.removeEventListener("mousemove", cancelLongPress); +} + +function start(e) { + down = true; + 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 setStartData(e) { lastScrollHeight = getScrollHeight(); startX = e.clientX; @@ -206,3 +249,16 @@ function fFalse() { 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."); + } +} From 85384def3d2a09b2feba45241fe3a16d70218143 Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Fri, 11 Mar 2022 04:03:53 +0800 Subject: [PATCH 3/9] Indicate scrolling started with a ripple (only after long press) --- Scroll_Everywhere/scrolle.user.js | 43 ++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index 3fe4b90..5da4703 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -8,9 +8,9 @@ // @supportURL https://github.com/tumpio/gmscripts // @icon https://raw.githubusercontent.com/tumpio/gmscripts/master/Scroll_Everywhere/large.png // @include * -// @grant none +// @grant GM_addStyle // @run-at document-body -// @version 0.3k +// @version 0.3l // ==/UserScript== /* jshint multistr: true, strict: false, browser: true, devel: true */ @@ -27,7 +27,7 @@ var middleIsStart, startX, startY, startScrollTop, startScrollLeft, lastScrollHe var relativeScrolling, lastX, lastY, scaleX, scaleY, power, offsetMiddle; -var startAfterLongPress, longPressTimer, eventBeforeLongPress; +var startAfterLongPress, longPressTimer, eventBeforeLongPress, longPressStylesAdded; // NOTE: Do not run on iframes if (window.top === window.self) { @@ -103,6 +103,43 @@ function longPressDetected() { 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: #8886; + 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() { From 9521b8975915e695381fb38862fd2e6c4b543d0f Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Fri, 11 Mar 2022 05:21:13 +0800 Subject: [PATCH 4/9] Use a lighter ripple, for equal perceived contrast on light/dark backgrounds --- Scroll_Everywhere/scrolle.user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index 5da4703..bd89aa4 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -115,7 +115,7 @@ function longPressDetected() { border-radius: 50%; transform: scale(0); animation: ripple 600ms ease-out; - background-color: #8886; + background-color: #aaa8; z-index: 999999; pointer-events: none; } From 1c195f7a876c404aca892843680ac963e23b95b1 Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Sun, 13 Mar 2022 11:31:18 +0800 Subject: [PATCH 5/9] Mute click events after a scroll --- Scroll_Everywhere/scrolle.user.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index bd89aa4..6ab7757 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -29,6 +29,8 @@ var relativeScrolling, lastX, lastY, scaleX, scaleY, power, offsetMiddle; var startAfterLongPress, longPressTimer, eventBeforeLongPress, longPressStylesAdded; +var scrollStartTime, scrollStopTime; + // NOTE: Do not run on iframes if (window.top === window.self) { // USER SETTINGS @@ -63,6 +65,7 @@ if (window.top === window.self) { window.addEventListener("mousedown", handleMouseDown, false); window.addEventListener("mouseup", handleMouseUp, false); + window.addEventListener("click", handleClick, true); } function handleMouseDown(e) { @@ -85,6 +88,17 @@ function handleMouseUp(e) { } } +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 startLongPress(e) { cancelLongPress(); eventBeforeLongPress = e; @@ -149,6 +163,7 @@ function cancelLongPress() { function start(e) { down = true; + scrollStartTime = Date.now(); setStartData(e); lastX = e.clientX; lastY = e.clientY; @@ -221,6 +236,7 @@ function stop() { if (isWin) document.oncontextmenu = !fFalse; down = false; + scrollStopTime = Date.now(); scrollevents = 0; window.removeEventListener("mouseup", stop, false); window.removeEventListener("mousemove", scroll, false); From 0c27a055c89980fc0efa405030adc0bfa7ab07a8 Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Sun, 13 Mar 2022 12:41:18 +0800 Subject: [PATCH 6/9] Ignore clicks on native scrollbar --- Scroll_Everywhere/scrolle.user.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index 6ab7757..73982ab 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -10,7 +10,7 @@ // @include * // @grant GM_addStyle // @run-at document-body -// @version 0.3l +// @version 0.3m // ==/UserScript== /* jshint multistr: true, strict: false, browser: true, devel: true */ @@ -69,6 +69,12 @@ if (window.top === window.self) { } function handleMouseDown(e) { + // From: https://stackoverflow.com/questions/10045423/determine-whether-user-clicking-scrollbar-or-content-onclick-for-native-scroll + var wasClickOnScrollbar = e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight; + if (wasClickOnScrollbar) { + //console.log('Ignoring click on scrollbar:', e); + return; + } if (e.which == mouseBtn) { if (startAfterLongPress) { startLongPress(e); From 3be431736a265f4a3eb0a2ebd0ee39562ebfcf08 Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Mon, 14 Mar 2022 15:33:09 +0800 Subject: [PATCH 7/9] Capture paste events, to partially protect Linux middle-click users --- Scroll_Everywhere/scrolle.user.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index 73982ab..d680b44 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -3,6 +3,7 @@ // @description Scroll everywhere with right mouse button. // @author tumpio // @oujs:author tumpio +// @contributor joeytwiddle // @namespace tumpio@sci.fi // @homepageURL https://openuserjs.org/scripts/tumpio/Scroll_Everywhere // @supportURL https://github.com/tumpio/gmscripts @@ -27,6 +28,8 @@ var middleIsStart, startX, startY, startScrollTop, startScrollLeft, lastScrollHe var relativeScrolling, lastX, lastY, scaleX, scaleY, power, offsetMiddle; +var lastMiddleClickTime; + var startAfterLongPress, longPressTimer, eventBeforeLongPress, longPressStylesAdded; var scrollStartTime, scrollStopTime; @@ -66,6 +69,7 @@ if (window.top === window.self) { window.addEventListener("mousedown", handleMouseDown, false); window.addEventListener("mouseup", handleMouseUp, false); window.addEventListener("click", handleClick, true); + window.addEventListener('paste', handlePaste, true); } function handleMouseDown(e) { @@ -89,6 +93,9 @@ function handleMouseDown(e) { } function handleMouseUp(e) { + if (e.which == 2) { + lastMiddleClickTime = Date.now(); + } if (startAfterLongPress) { cancelLongPress(); } @@ -105,6 +112,27 @@ function handleClick(e) { 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; From e1847bddabfa0d97a430cc163d849a23b0f16f40 Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Sat, 19 Mar 2022 15:29:19 +0800 Subject: [PATCH 8/9] Pacify eslint --- Scroll_Everywhere/scrolle.user.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index d680b44..8ac2915 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -17,6 +17,10 @@ /* 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 @@ -59,7 +63,7 @@ if (window.top === window.self) { scrollevents = 0; scrollBarWidth = 2 * getScrollBarWidth(); cursorMask = document.createElement('div'); - isWin = (window.navigator.appVersion.indexOf("Win") != -1 ? true : false); + isWin = window.navigator.appVersion.indexOf("Win") >= 0; if (cursorStyle === "grab") cursorStyle = "-webkit-grabbing; cursor: -moz-grabbing"; cursorMask.id = "SE_cursorMask_cursor"; @@ -338,7 +342,7 @@ function slowF(x) { } function selectNoText() { - if (document.body.createTextRange) { + if (document.body.createTextRange) { const range = document.body.createTextRange(); range.select(); } else if (window.getSelection) { From 39b2e13f5426e882a2e28c252d2d7af220b9d0fe Mon Sep 17 00:00:00 2001 From: Paul Joey Clark Date: Sat, 19 Mar 2022 15:31:52 +0800 Subject: [PATCH 9/9] Implement scrolling of any element, not only body element --- Scroll_Everywhere/scrolle.user.js | 97 ++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 22 deletions(-) diff --git a/Scroll_Everywhere/scrolle.user.js b/Scroll_Everywhere/scrolle.user.js index 8ac2915..ea2e247 100644 --- a/Scroll_Everywhere/scrolle.user.js +++ b/Scroll_Everywhere/scrolle.user.js @@ -11,7 +11,7 @@ // @include * // @grant GM_addStyle // @run-at document-body -// @version 0.3m +// @version 0.3o // ==/UserScript== /* jshint multistr: true, strict: false, browser: true, devel: true */ @@ -38,6 +38,8 @@ var startAfterLongPress, longPressTimer, eventBeforeLongPress, longPressStylesAd var scrollStartTime, scrollStopTime; +var elementToScroll; + // NOTE: Do not run on iframes if (window.top === window.self) { // USER SETTINGS @@ -78,9 +80,9 @@ if (window.top === window.self) { function handleMouseDown(e) { // From: https://stackoverflow.com/questions/10045423/determine-whether-user-clicking-scrollbar-or-content-onclick-for-native-scroll - var wasClickOnScrollbar = e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight; + 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); + //console.log('Ignoring click on scrollbar:', e, `${e.offsetX} > ${e.target.clientWidth} || ${e.offsetY} > ${e.target.clientHeight}`); return; } if (e.which == mouseBtn) { @@ -201,6 +203,8 @@ function 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; @@ -212,6 +216,21 @@ function start(e) { 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; @@ -219,8 +238,12 @@ function setStartData(e) { // 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 = document.documentElement.scrollTop || document.body.scrollTop || 0; - startScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0; + 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) { @@ -251,22 +274,37 @@ function scroll(e) { var distance = Math.sqrt(diffX * diffX + diffY * diffY); var velocity = 1 + distance * power / 100; var reverseScale = reverse ? -1 : 1; - window.scrollTo(window.scrollX + diffX * scaleX * velocity * reverseScale, window.scrollY + diffY * scaleY * velocity * reverseScale); + //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; } - // The original absolute scrolling - window.scrollTo( - fScrollX( - window.innerWidth - scrollBarWidth, - getScrollWidth() - window.innerWidth, - e.clientX), - fScrollY( - window.innerHeight - scrollBarWidth, - getScrollHeight() - window.innerHeight, - e.clientY) - ); + 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() { @@ -282,7 +320,7 @@ function stop() { } function noScrollX() { - return document.documentElement.scrollLeft; + return elementToScroll.scrollLeft; } function fPos(win, doc, pos) { @@ -307,12 +345,27 @@ function fRevPos(win, doc, pos) { return doc - fPos(win, doc, pos); } -function getScrollHeight(e) { - return document.body.scrollHeight || document.documentElement.scrollHeight || 0; +function getScrollHeight() { + return elementToScroll.scrollHeight || 0; +} + +function getScrollWidth() { + return elementToScroll.scrollWidth || 0; } -function getScrollWidth(e) { - return document.body.scrollWidth || document.documentElement.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() {