Overlay Custom Scrollbar

Custom X and Y scrollbars with drag-to-seek, stay visible during drag, no width impact, cross-browser support

目前为 2025-03-09 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Overlay Custom Scrollbar
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.1
  5. // @description Custom X and Y scrollbars with drag-to-seek, stay visible during drag, no width impact, cross-browser support
  6. // @license MIT
  7. // @author Grok and Claude
  8. // @match *://*/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Add CSS styles
  16. const style = document.createElement('style');
  17. style.textContent = `
  18. /* Hide native scrollbars */
  19. html {
  20. -ms-overflow-style: none !important; /* Edge/IE */
  21. scrollbar-width: none !important; /* Firefox */
  22. overflow: scroll; /* Enable X and Y scrolling */
  23. }
  24.  
  25. html::-webkit-scrollbar, body::-webkit-scrollbar {
  26. display: none !important; /* Chrome/Edge/Safari */
  27. width: 0px !important;
  28. height: 0px !important;
  29. }
  30.  
  31. /* Y-axis custom scrollbar container */
  32. .custom-scrollbar-y {
  33. position: fixed;
  34. top: 0;
  35. right: 0;
  36. width: 8px;
  37. height: 100%;
  38. opacity: 0;
  39. transition: opacity 0.2s ease;
  40. z-index: 9999;
  41. background: rgba(0, 0, 0, 0.05);
  42. }
  43.  
  44. /* X-axis custom scrollbar container */
  45. .custom-scrollbar-x {
  46. position: fixed;
  47. bottom: 0;
  48. left: 0;
  49. width: 100%;
  50. height: 8px;
  51. opacity: 0;
  52. transition: opacity 0.2s ease;
  53. z-index: 9999;
  54. background: rgba(0, 0, 0, 0.05);
  55. }
  56.  
  57. /* Custom scrollbar thumb */
  58. .custom-scrollbar-thumb {
  59. position: absolute;
  60. background: #888;
  61. border-radius: 4px;
  62. opacity: 1; /* Always visible */
  63. cursor: pointer;
  64. }
  65.  
  66. .custom-scrollbar-y .custom-scrollbar-thumb {
  67. width: 100%;
  68. }
  69.  
  70. .custom-scrollbar-x .custom-scrollbar-thumb {
  71. height: 100%;
  72. }
  73.  
  74. .custom-scrollbar-thumb:hover {
  75. background: #555;
  76. }
  77.  
  78. /* Show during scrolling or dragging */
  79. html.scrolling .custom-scrollbar-y,
  80. html.scrolling .custom-scrollbar-x {
  81. opacity: 1;
  82. }
  83.  
  84. /* Always show when dragging */
  85. .dragging {
  86. opacity: 1 !important;
  87. }
  88. `;
  89. document.head.appendChild(style);
  90.  
  91. // Create Y-axis scrollbar
  92. const scrollbarY = document.createElement('div');
  93. scrollbarY.className = 'custom-scrollbar-y';
  94. const thumbY = document.createElement('div');
  95. thumbY.className = 'custom-scrollbar-thumb';
  96. scrollbarY.appendChild(thumbY);
  97. document.body.appendChild(scrollbarY);
  98.  
  99. // Create X-axis scrollbar
  100. const scrollbarX = document.createElement('div');
  101. scrollbarX.className = 'custom-scrollbar-x';
  102. const thumbX = document.createElement('div');
  103. thumbX.className = 'custom-scrollbar-thumb';
  104. scrollbarX.appendChild(thumbX);
  105. document.body.appendChild(scrollbarX);
  106.  
  107. // Global variables
  108. let isDraggingY = false;
  109. let isDraggingX = false;
  110. let startY, startX, startScrollTop, startScrollLeft;
  111. let scrollTimeout;
  112. const html = document.documentElement;
  113.  
  114. function getDocumentDimensions() {
  115. return {
  116. // Client dimensions (viewport)
  117. clientWidth: window.innerWidth || html.clientWidth || document.body.clientWidth,
  118. clientHeight: window.innerHeight || html.clientHeight || document.body.clientHeight,
  119.  
  120. // Scroll dimensions (total document)
  121. scrollWidth: Math.max(
  122. document.body.scrollWidth, html.scrollWidth,
  123. document.body.offsetWidth, html.offsetWidth,
  124. document.body.clientWidth, html.clientWidth
  125. ),
  126. scrollHeight: Math.max(
  127. document.body.scrollHeight, html.scrollHeight,
  128. document.body.offsetHeight, html.offsetHeight,
  129. document.body.clientHeight, html.clientHeight
  130. ),
  131.  
  132. // Current scroll position
  133. scrollLeft: window.pageXOffset || html.scrollLeft || document.body.scrollLeft,
  134. scrollTop: window.pageYOffset || html.scrollTop || document.body.scrollTop
  135. };
  136. }
  137.  
  138. function updateScrollbars() {
  139. const dims = getDocumentDimensions();
  140.  
  141. // Calculate maximum scroll positions
  142. const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
  143. const maxScrollTop = dims.scrollHeight - dims.clientHeight;
  144.  
  145. // Y-axis scrollbar updates
  146. const isScrollableY = dims.scrollHeight > dims.clientHeight;
  147. scrollbarY.style.display = isScrollableY ? 'block' : 'none';
  148.  
  149. if (isScrollableY) {
  150. // Calculate Y thumb height and position
  151. const thumbHeightRatio = dims.clientHeight / dims.scrollHeight;
  152. const thumbHeight = Math.max(thumbHeightRatio * dims.clientHeight, 20);
  153. const scrollRatioY = maxScrollTop > 0 ? dims.scrollTop / maxScrollTop : 0;
  154. const thumbTop = scrollRatioY * (dims.clientHeight - thumbHeight);
  155.  
  156. thumbY.style.height = `${thumbHeight}px`;
  157. thumbY.style.top = `${thumbTop}px`;
  158. }
  159.  
  160. // X-axis scrollbar updates
  161. const isScrollableX = dims.scrollWidth > dims.clientWidth;
  162. scrollbarX.style.display = isScrollableX ? 'block' : 'none';
  163.  
  164. if (isScrollableX) {
  165. // Calculate X thumb width and position
  166. const thumbWidthRatio = dims.clientWidth / dims.scrollWidth;
  167. const thumbWidth = Math.max(thumbWidthRatio * dims.clientWidth, 20);
  168. const scrollRatioX = maxScrollLeft > 0 ? dims.scrollLeft / maxScrollLeft : 0;
  169. const thumbLeft = scrollRatioX * (dims.clientWidth - thumbWidth);
  170.  
  171. thumbX.style.width = `${thumbWidth}px`;
  172. thumbX.style.left = `${thumbLeft}px`;
  173. }
  174.  
  175. // Show scrollbars during scrolling
  176. if (!isDraggingX && !isDraggingY) {
  177. html.classList.add('scrolling');
  178. clearTimeout(scrollTimeout);
  179. scrollTimeout = setTimeout(() => {
  180. html.classList.remove('scrolling');
  181. }, 500);
  182. }
  183. }
  184.  
  185. // Y-axis drag handling
  186. thumbY.addEventListener('mousedown', (e) => {
  187. e.preventDefault();
  188. e.stopPropagation();
  189.  
  190. const dims = getDocumentDimensions();
  191. isDraggingY = true;
  192. startY = e.clientY;
  193. startScrollTop = dims.scrollTop;
  194.  
  195. scrollbarY.classList.add('dragging');
  196. html.classList.add('scrolling');
  197. document.body.style.userSelect = 'none';
  198. });
  199.  
  200. // X-axis drag handling
  201. thumbX.addEventListener('mousedown', (e) => {
  202. e.preventDefault();
  203. e.stopPropagation();
  204.  
  205. const dims = getDocumentDimensions();
  206. isDraggingX = true;
  207. startX = e.clientX;
  208. startScrollLeft = dims.scrollLeft;
  209.  
  210. scrollbarX.classList.add('dragging');
  211. html.classList.add('scrolling');
  212. document.body.style.userSelect = 'none';
  213. });
  214.  
  215. // Mouse move handling
  216. document.addEventListener('mousemove', (e) => {
  217. if (!isDraggingY && !isDraggingX) return;
  218.  
  219. const dims = getDocumentDimensions();
  220.  
  221. if (isDraggingY) {
  222. // Y-axis drag logic
  223. const deltaY = e.clientY - startY;
  224. const thumbHeight = parseFloat(thumbY.style.height) || 20;
  225. const trackHeight = dims.clientHeight;
  226. const maxScrollTop = dims.scrollHeight - dims.clientHeight;
  227.  
  228. // Calculate how much to scroll based on drag distance
  229. const scrollRatio = maxScrollTop / (trackHeight - thumbHeight);
  230. const newScrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + (deltaY * scrollRatio)));
  231.  
  232. // Apply scroll
  233. window.scrollTo(dims.scrollLeft, newScrollTop);
  234. }
  235.  
  236. if (isDraggingX) {
  237. // X-axis drag logic
  238. const deltaX = e.clientX - startX;
  239. const thumbWidth = parseFloat(thumbX.style.width) || 20;
  240. const trackWidth = dims.clientWidth;
  241. const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
  242.  
  243. // Calculate how much to scroll based on drag distance
  244. const scrollRatio = maxScrollLeft / (trackWidth - thumbWidth);
  245. const newScrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + (deltaX * scrollRatio)));
  246.  
  247. // IMPORTANT: Use a direct DOM method for horizontal scrolling
  248. document.documentElement.scrollLeft = newScrollLeft;
  249. document.body.scrollLeft = newScrollLeft;
  250. window.scrollTo(newScrollLeft, dims.scrollTop);
  251. }
  252.  
  253. // Ensure scrollbar visual updates
  254. updateScrollbars();
  255. });
  256.  
  257. // Click on track to jump
  258. scrollbarY.addEventListener('mousedown', (e) => {
  259. if (e.target !== scrollbarY) return;
  260.  
  261. const dims = getDocumentDimensions();
  262. const thumbHeight = parseFloat(thumbY.style.height) || 20;
  263. const clickPos = e.clientY;
  264. const thumbHalf = thumbHeight / 2;
  265.  
  266. // Center the thumb on the click position
  267. const trackHeight = dims.clientHeight;
  268. const maxScrollTop = dims.scrollHeight - dims.clientHeight;
  269. const clickRatio = (clickPos - thumbHalf) / (trackHeight - thumbHeight);
  270. const newScrollTop = Math.max(0, Math.min(maxScrollTop, clickRatio * maxScrollTop));
  271.  
  272. window.scrollTo({
  273. top: newScrollTop,
  274. left: dims.scrollLeft,
  275. behavior: 'smooth'
  276. });
  277. });
  278.  
  279. scrollbarX.addEventListener('mousedown', (e) => {
  280. if (e.target !== scrollbarX) return;
  281.  
  282. const dims = getDocumentDimensions();
  283. const thumbWidth = parseFloat(thumbX.style.width) || 20;
  284. const clickPos = e.clientX;
  285. const thumbHalf = thumbWidth / 2;
  286.  
  287. // Center the thumb on the click position
  288. const trackWidth = dims.clientWidth;
  289. const maxScrollLeft = dims.scrollWidth - dims.clientWidth;
  290. const clickRatio = (clickPos - thumbHalf) / (trackWidth - thumbWidth);
  291. const newScrollLeft = Math.max(0, Math.min(maxScrollLeft, clickRatio * maxScrollLeft));
  292.  
  293. window.scrollTo({
  294. top: dims.scrollTop,
  295. left: newScrollLeft,
  296. behavior: 'smooth'
  297. });
  298. });
  299.  
  300. // End dragging
  301. function endDragging() {
  302. if (isDraggingY) {
  303. isDraggingY = false;
  304. scrollbarY.classList.remove('dragging');
  305. }
  306.  
  307. if (isDraggingX) {
  308. isDraggingX = false;
  309. scrollbarX.classList.remove('dragging');
  310. }
  311.  
  312. document.body.style.userSelect = '';
  313.  
  314. clearTimeout(scrollTimeout);
  315. scrollTimeout = setTimeout(() => {
  316. html.classList.remove('scrolling');
  317. }, 500);
  318. }
  319.  
  320. document.addEventListener('mouseup', endDragging);
  321. document.addEventListener('mouseleave', endDragging);
  322.  
  323. // Listen for scroll events
  324. window.addEventListener('scroll', updateScrollbars, { passive: true });
  325. window.addEventListener('resize', updateScrollbars, { passive: true });
  326.  
  327. // Make sure the script runs after the page has fully loaded
  328. function initialize() {
  329. updateScrollbars();
  330.  
  331. // Force multiple updates to ensure correct initial state
  332. setTimeout(updateScrollbars, 100);
  333. setTimeout(updateScrollbars, 500);
  334. }
  335.  
  336. if (document.readyState === 'complete') {
  337. initialize();
  338. } else {
  339. window.addEventListener('load', initialize);
  340. }
  341. })();