Scroll Page Progress

Visual indicator of page progress while scrolling

  1.  
  2. // ==UserScript==
  3. // @license MIT
  4. // @name Scroll Page Progress
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.8.3
  7. // @description Visual indicator of page progress while scrolling
  8. // @author You
  9. // @match *://*/*
  10. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. const currentState = {
  17. deg: 0,
  18. progress: 0,
  19. zIndex: 0,
  20. movementIntervalId: null
  21. }
  22. let globalShadow
  23. let progressBar
  24. const createDiv = () => document.createElement('div')
  25.  
  26. function insertCirculaProgressBarEl() {
  27. const shadowHost = createDiv()
  28. shadowHost.id = 'host-shwadow-circular-progress'
  29. const shadow = shadowHost.attachShadow({ mode: "closed" });
  30. globalShadow = shadow
  31.  
  32. const circularProgressBar = createDiv()
  33. progressBar = circularProgressBar
  34. const contentWrapper = createDiv()
  35. const closeOverlay = createDiv()
  36. const title = createDiv()
  37. const overlay = createDiv()
  38. const leftSide = createDiv()
  39. const rightSide = createDiv()
  40.  
  41. circularProgressBar.classList.add('circular-progress-bar')
  42. title.classList.add('title');
  43. closeOverlay.classList.add('close-overlay');
  44. contentWrapper.classList.add('content-wrapper')
  45. overlay.classList.add('overlay');
  46. leftSide.classList.add('left-side');
  47. rightSide.classList.add('right-side');
  48.  
  49. title.innerText = '-%'
  50.  
  51. // this is the only way to create a trusted HTML element
  52. if (window.trustedTypes) {
  53. closeOverlay.innerHTML = window.trustedTypes.defaultPolicy.createHTML('×')
  54. } else {
  55. closeOverlay.innerHTML = '×'
  56. }
  57.  
  58. closeOverlay.addEventListener('click', function () {
  59. //circularProgressBar.style.display = 'none'
  60. const screenWidth = window.innerWidth;
  61. const elementoWidth = circularProgressBar.offsetWidth;
  62.  
  63. // Calcular la nueva posición
  64. const newPosition = screenWidth - elementoWidth / 2;
  65.  
  66. // Aplicar la nueva posición
  67. circularProgressBar.style.left = `${newPosition}px`;
  68. const topPosition = circularProgressBar.style.top
  69.  
  70. const path = window.location.pathname;
  71. let savedPaths = JSON.parse(localStorage.getItem('not-allowed-paths')) || [];
  72.  
  73. const existingEntryIndex = savedPaths.findIndex(entry => entry.path === path);
  74.  
  75. const newEntry = {
  76. path: path,
  77. left: newPosition,
  78. top: topPosition || '0px' // Si el top no está definido, usa '0px' como valor por defecto
  79. };
  80.  
  81. if (existingEntryIndex !== -1) {
  82. // Si ya existe una entrada para la ruta, actualiza la posición
  83. savedPaths[existingEntryIndex] = newEntry;
  84. } else {
  85. // Si no existe, añade la nueva entrada
  86. savedPaths.push(newEntry);
  87. }
  88. console.log("savedPaths", savedPaths)
  89. // Guardar el array actualizado en localStorage
  90. localStorage.setItem('not-allowed-paths', JSON.stringify(savedPaths));
  91. });
  92.  
  93. ;[title, overlay, leftSide, rightSide].forEach(childEl => contentWrapper.appendChild(childEl))
  94. circularProgressBar.appendChild(closeOverlay)
  95. circularProgressBar.appendChild(contentWrapper)
  96. shadow.appendChild(circularProgressBar)
  97. document.body.appendChild(shadowHost)
  98. }
  99.  
  100. function addCSS() {
  101. const styleSheet = document.createElement('style');
  102. styleSheet.textContent = `
  103. * {
  104. box-sizing: border-box;
  105. padding: 0;
  106. margin: 0;
  107. }
  108. .circular-progress-bar {
  109. --backgroundColor: #424242;
  110. --left-side-angle: 180deg;
  111. --barColor:orangered;
  112. width: 60px;
  113. height: 60px;
  114. color: #fff;
  115. border-radius: 50%;
  116. position: fixed;
  117. z-index: 2147483646;
  118. background: var(--backgroundColor);
  119. border: 5px solid white;
  120. box-shadow:
  121. 0 1px 1px hsl(0deg 0% 0% / 0.075),
  122. 0 2px 2px hsl(0deg 0% 0% / 0.075),
  123. 0 4px 4px hsl(0deg 0% 0% / 0.075),
  124. 0 8px 8px hsl(0deg 0% 0% / 0.075),
  125. 0 16px 16px hsl(0deg 0% 0% / 0.075);
  126. text-align: center;
  127. cursor: pointer;
  128. transition: opacity 0.2s ease;
  129. }
  130. .circular-progress-bar .overlay {
  131. width: 50%;
  132. height: 100%;
  133. position: absolute;
  134. top: 0;
  135. left: 0;
  136. background-color: var(--backgroundColor);
  137. transform-origin: right;
  138. transform: rotate(var(--overlay));
  139. }
  140. .close-overlay {
  141. width: 20px;
  142. height: 20px;
  143. position: absolute;
  144. left: 100%;
  145. top: -30%;
  146. z-index: 2147483647;
  147. display: flex;
  148. justify-content: center;
  149. align-items: center;
  150. border-radius: 50%;
  151. background: orangered;
  152. font-size: 19px;
  153. text-align: center;
  154. opacity: 0;
  155. }
  156. .close-overlay:hover {
  157. font-weight: bold;
  158. }
  159. .content-wrapper {
  160. overflow: hidden;
  161. height: 100%;
  162. width: 100%;
  163. border-radius: 50%;
  164. position: relative;
  165. }
  166. .circular-progress-bar:hover .close-overlay {
  167. opacity:1;
  168. }
  169. .circular-progress-bar .title {
  170. font-size: 15px;
  171. font-weight: bold;
  172. position:relative;
  173. height: 100%;
  174. display:flex;
  175. justify-content:center;
  176. align-items: center;
  177. z-index: 100;
  178. }
  179. .circular-progress-bar .left-side,
  180. .circular-progress-bar .right-side {
  181. width: 50%;
  182. height: 100%;
  183. position: absolute;
  184. top: 0;
  185. left: 0;
  186. border: 5px solid var(--barColor);
  187. border-radius: 100px 0px 0px 100px;
  188. border-right: 0;
  189. transform-origin: right;
  190. }
  191. .circular-progress-bar .left-side {
  192. transform: rotate(var(--left-side-angle));
  193. }
  194. .circular-progress-bar .right-side {
  195. transform: rotate(var(--right-side-angle));
  196. }
  197. `
  198. globalShadow.appendChild(styleSheet)
  199. }
  200.  
  201. function setAngle(deg) {
  202. const progressBar = globalShadow.querySelector('.circular-progress-bar')
  203. const leftSide = globalShadow.querySelector('.left-side')
  204. const rightSide = globalShadow.querySelector('.right-side')
  205. const overlay = globalShadow.querySelector('.circular-progress-bar .overlay')
  206.  
  207. const zIndex = deg > 180 ? 100 : 0
  208. const rightSideAngle = deg < 180 ? deg : 180
  209. const leftSideAngle = deg
  210. const overlayAngle = deg < 180 ? 0 : deg - 180
  211. const zIndexChangedToPositive = currentState.zIndex === 0 && zIndex === 100
  212. if (deg > 180) {
  213. rightSide.style.zIndex = 2
  214. leftSide.style.zIndex = 0
  215. overlay.style.zIndex = 1
  216. } else {
  217. rightSide.style.zIndex = 1
  218. leftSide.style.zIndex = 0
  219. overlay.style.zIndex = 2
  220. }
  221. progressBar.style.setProperty('--overlay', `${overlayAngle}deg`);
  222. progressBar.style.setProperty('--right-side-angle', `${rightSideAngle}deg`);
  223. progressBar.style.setProperty('--left-side-angle', `${leftSideAngle}deg`);
  224.  
  225. }
  226.  
  227. function smoothProgressBar(targetProgress, duration) {
  228.  
  229. if (currentState.movementIntervalId) {
  230. clearInterval(currentState.movementIntervalId);
  231. }
  232. let currentProgress = currentState.deg
  233. const increment = (targetProgress - currentProgress) / (duration / 10);
  234.  
  235. currentState.movementIntervalId = setInterval(function () {
  236. currentProgress += increment;
  237.  
  238. if ((increment > 0 && currentProgress >= targetProgress) || (increment < 0 && currentProgress <= targetProgress)) {
  239. currentProgress = targetProgress;
  240. clearInterval(currentState.movementIntervalId);
  241. }
  242.  
  243. setAngle(currentProgress)
  244. }, 10);
  245. }
  246.  
  247. function percentageToAngle(percentageNumber) {
  248. if (percentageNumber > 100) {
  249. return 360
  250. }
  251. if (percentageNumber < 0) {
  252. return 0
  253. }
  254. return (360 * percentageNumber) / 100
  255. }
  256.  
  257. function setPercentage(percentageNumber) {
  258. const angle = percentageToAngle(percentageNumber)
  259. smoothProgressBar(angle, 400)
  260. }
  261.  
  262. function debounce(callback, wait) {
  263. let timerId;
  264. return (...args) => {
  265. clearTimeout(timerId);
  266. timerId = setTimeout(() => {
  267. callback(...args);
  268. }, wait);
  269. };
  270. }
  271.  
  272. function setEventListeners() {
  273. let offsetX = 0, offsetY = 0, isDragging = false;
  274.  
  275. progressBar.addEventListener("mousedown", (e) => {
  276. e.preventDefault()
  277. offsetX = e.clientX - progressBar.offsetLeft;
  278. offsetY = e.clientY - progressBar.offsetTop;
  279. isDragging = true;
  280. progressBar.style.cursor = "grabbing";
  281. progressBar.style.opacity = "0.3";
  282. });
  283.  
  284. document.addEventListener("mousemove", (e) => {
  285. if (isDragging) {
  286. e.preventDefault();
  287. const left = e.clientX - offsetX;
  288. const top = e.clientY - offsetY;
  289. progressBar.style.left = `${left}px`;
  290. progressBar.style.top = `${top}px`;
  291. savePosition(left, top); // Guardar la posición cada vez que se mueve
  292. }
  293. });
  294.  
  295. document.addEventListener("mouseup", () => {
  296. isDragging = false;
  297. progressBar.style.cursor = "grab";
  298. progressBar.style.opacity = "1";
  299. });
  300. }
  301.  
  302. function savePosition(left, top) {
  303. const position = { left, top };
  304. localStorage.setItem("elementPosition", JSON.stringify(position));
  305. }
  306.  
  307. function loadPosition() {
  308. const currentPath = window.location.pathname;
  309. const savedPaths = JSON.parse(localStorage.getItem('not-allowed-paths')) || [];
  310.  
  311. const entry = savedPaths.find(entry => entry.path === currentPath);
  312.  
  313. if (entry) {
  314. console.log('exist entry')
  315. // Si existe una entrada para la ruta, aplica las posiciones guardadas
  316. progressBar.style.left = `${entry.left}px`;
  317. progressBar.style.top = `${entry.top}`;
  318. return;
  319. }
  320. const savedPosition = localStorage.getItem("elementPosition");
  321. if (savedPosition) {
  322. const { left, top } = JSON.parse(savedPosition);
  323. progressBar.style.left = `${left}px`;
  324. progressBar.style.top = `${top}px`;
  325. } else {
  326. progressBar.style.right = `10px`;
  327. progressBar.style.top = `10px`;
  328. }
  329. }
  330.  
  331. function getCurrentScrollProgress() {
  332. const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
  333. const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
  334. const progress = (winScroll / height) * 100;
  335. return Math.trunc(progress);
  336. }
  337.  
  338. function watchScroll() {
  339. const progressBarTitle = globalShadow.querySelector('.title')
  340. document.addEventListener('scroll', debounce(() => {
  341. setPercentage(getCurrentScrollProgress())
  342. progressBarTitle.innerText = getCurrentScrollProgress() + '%'
  343. currentState.progress = getCurrentScrollProgress()
  344. currentState.deg = percentageToAngle(getCurrentScrollProgress())
  345. }, 50))
  346. }
  347.  
  348.  
  349. document.onreadystatechange = function () {
  350. if (document.readyState == "complete") {
  351. if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
  352. window.trustedTypes.createPolicy('default', {
  353. createHTML: (string, sink) => string
  354. });
  355. }
  356.  
  357. insertCirculaProgressBarEl()
  358. setEventListeners()
  359. addCSS()
  360. loadPosition()
  361. watchScroll()
  362. }
  363. }
  364.  
  365.  
  366. })();
  367.