Scroll by mouse drag

Scroll the page while holding chosen mouse button anywhere on the website

  1. // ==UserScript==
  2. // @name Scroll by mouse drag
  3. // @namespace FawayTT
  4. // @description Scroll the page while holding chosen mouse button anywhere on the website
  5. // @match *://*/*
  6. // @version 1.2
  7. // @author FawayTT
  8. // @license MIT
  9. // @homepage https://github.com/FawayTT/userscripts
  10. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_openInTab
  15. // ==/UserScript==
  16.  
  17. GM_registerMenuCommand('Settings', opencfg);
  18.  
  19. const configcss = `height: 40rem;
  20. width: 35rem;
  21. border-radius: 10px;
  22. z-index: 9999999;
  23. position: fixed;
  24. `;
  25.  
  26. const defaults = {
  27. touchArea: `width: 100%;
  28. height: 100%;
  29. position: fixed;
  30. left: 50%;
  31. top: 0;
  32. pointer-events: none;
  33. transform: translateX(-50%);`,
  34. };
  35.  
  36. let gmc = new GM_config({
  37. id: 'config',
  38. title: 'Scroll by mouse drag settings',
  39. fields: {
  40. scrollButton: {
  41. section: ['Behavior'],
  42. label: 'Scroll button',
  43. labelPos: 'left',
  44. type: 'select',
  45. default: 'Left', // Default value for Blue
  46. options: ['Left', 'Right', 'MiddleClick'],
  47. },
  48. touchArea: {
  49. label: 'Touchpad zone position custom CSS (by default, its invisible and centered in the middle of the page)',
  50. labelPos: 'left',
  51. type: 'textarea',
  52. default: defaults.touchArea,
  53. },
  54. msClick: {
  55. label: 'Number of ms before scroll starts',
  56. labelPos: 'left',
  57. type: 'number',
  58. default: 100,
  59. },
  60. textSelectable: {
  61. label: 'Turn off when selecting text',
  62. labelPos: 'right',
  63. type: 'checkbox',
  64. default: true,
  65. },
  66. scrollSpeed: {
  67. section: ['Speed'],
  68. label: 'Speed of scroll',
  69. labelPos: 'left',
  70. type: 'number',
  71. default: 1.5,
  72. },
  73. blacklistedSites: {
  74. section: ['Sites'],
  75. label: 'Blacklisted sites (separated by comma)',
  76. labelPos: 'right',
  77. type: 'text',
  78. default: '',
  79. },
  80. whitelistEnabled: {
  81. label: 'Whitelist enabled',
  82. labelPos: 'right',
  83. type: 'checkbox',
  84. default: false,
  85. },
  86. whitelistedSites: {
  87. label: 'Whitelisted sites (separated by comma)',
  88. labelPos: 'right',
  89. type: 'text',
  90. default: '',
  91. },
  92. url: {
  93. section: ['Support'],
  94. label: 'https://github.com/FawayTT/userscripts',
  95. type: 'button',
  96. click: () => {
  97. GM_openInTab('https://github.com/FawayTT/userscripts');
  98. },
  99. },
  100. },
  101. events: {
  102. save: function () {
  103. gmc.close();
  104. },
  105. init: onInit,
  106. },
  107. });
  108.  
  109. const scrollDiv = document.createElement('div');
  110. let animationDuration = 100;
  111. let isMouseDown = false;
  112. let startY = 0;
  113. let startX = 0;
  114. let startTimeout;
  115. let distance = 0;
  116. let isScrolling;
  117. let clicks = 0;
  118. let textSelectable;
  119. let button;
  120. let msClick;
  121. let scrollSpeed = 1.5;
  122.  
  123. const animateScroll = (distance) => {
  124. const startTime = performance.now();
  125. const startScrollY = window.scrollY;
  126. let extraScroll = true;
  127.  
  128. const scroll = (timestamp) => {
  129. const elapsedTime = timestamp - startTime;
  130. const progress = Math.min(elapsedTime / animationDuration, 1);
  131. const scrollAmount = startScrollY + distance * progress;
  132. if (window.scrollY < 50 && distance < 0) {
  133. return;
  134. }
  135. window.scrollTo(0, Math.abs(scrollAmount));
  136.  
  137. if (progress < 1) {
  138. requestAnimationFrame(scroll);
  139. } else {
  140. isScrolling = false;
  141. if (extraScroll) {
  142. extraScroll = false;
  143. window.scrollBy({ top: distance * 0.5, behavior: 'smooth' });
  144. }
  145. }
  146. };
  147.  
  148. requestAnimationFrame(scroll);
  149. };
  150.  
  151. function onInit() {
  152. const whitelistEnabled = gmc.get('whitelistEnabled');
  153. const blacklistedSites = gmc.get('blacklistedSites');
  154. const whitelistedSites = gmc.get('whitelistedSites');
  155. button = gmc.get('scrollButton');
  156. textSelectable = gmc.get('textSelectable');
  157. msClick = gmc.get('msClick');
  158. scrollSpeed = gmc.get('scrollSpeed');
  159.  
  160. switch (button) {
  161. case 'Left':
  162. button = 0;
  163. break;
  164. case 'Right':
  165. button = 2;
  166. break;
  167. default:
  168. button = 1;
  169. break;
  170. }
  171. if (whitelistEnabled) {
  172. if (whitelistedSites.length > 0) {
  173. const whitelistedSitesArray = whitelistedSites.split(',').map((site) => site.trim());
  174. const currentSite = window.location.href;
  175. if (whitelistedSitesArray.includes(currentSite)) init();
  176. }
  177. } else {
  178. if (blacklistedSites.length > 0) {
  179. const blacklistedSitesArray = blacklistedSites.split(',').map((site) => site.trim());
  180. const currentSite = window.location.href;
  181. if (!blacklistedSitesArray.includes(currentSite)) init();
  182. } else init();
  183. }
  184. }
  185.  
  186. function opencfg() {
  187. gmc.open();
  188. config.style = configcss;
  189. }
  190.  
  191. const pauseEvent = (e) => {
  192. if (e.stopPropagation) e.stopPropagation();
  193. if (e.preventDefault) e.preventDefault();
  194. e.cancelBubble = true;
  195. e.returnValue = false;
  196. return false;
  197. };
  198.  
  199. const getScrollValue = (value) => {
  200. return value * scrollSpeed;
  201. };
  202.  
  203. const reset = () => {
  204. if (isMouseDown) {
  205. document.body.style.cursor = null;
  206. }
  207. isMouseDown = false;
  208. isScrolling = false;
  209. clearTimeout(startTimeout);
  210. };
  211.  
  212. const checkScroll = (e) => {
  213. const rect = scrollDiv.getBoundingClientRect();
  214. const scrollDivX = rect.left + window.scrollX;
  215. clearTimeout(startTimeout);
  216. if (e.button === button && e.pageX >= scrollDivX && e.pageX <= scrollDivX + rect.width) {
  217. if (button !== 0) e.preventDefault();
  218. startTimeout = setTimeout(() => {
  219. isMouseDown = true;
  220. startY = e.clientY;
  221. startX = e.clientX;
  222. document.body.style.setProperty('cursor', 'grabbing');
  223. }, msClick);
  224. }
  225. };
  226.  
  227. const scroll = (e) => {
  228. if (isMouseDown && e.buttons & (1 === 1)) pauseEvent(e);
  229. if (isScrolling || !isMouseDown) return;
  230. if (window.getSelection().toString().length > 0 || (textSelectable && Math.abs(startX - e.clientX) > 50)) {
  231. reset();
  232. return;
  233. }
  234. distance = -(startY - e.clientY);
  235. isScrolling = true;
  236. distance = getScrollValue(distance);
  237. animateScroll(distance);
  238. };
  239.  
  240. function init() {
  241. scrollDiv.style.cssText = gmc.get('touchArea');
  242. document.body.appendChild(scrollDiv);
  243. document.addEventListener(
  244. 'contextmenu',
  245. (e) => {
  246. if (button === 2 && !doubleClick) e.preventDefault();
  247. },
  248. false
  249. );
  250. document.addEventListener('mousedown', checkScroll);
  251. document.addEventListener('mouseup', reset);
  252. document.addEventListener('mousemove', scroll);
  253. }