├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── jest.config.js ├── libs └── matter.min.css ├── package.json ├── rollup.config.mjs ├── src ├── _locales │ ├── en │ │ └── messages.json │ └── fr │ │ └── messages.json ├── index.ts ├── manifest.json ├── res │ ├── LICENSE │ ├── icon128.png │ ├── icon16.png │ └── icon48.png ├── settings │ ├── default-settings.ts │ ├── ignored-urls.spec.ts │ ├── ignored-urls.ts │ ├── index.html │ ├── index.scss │ └── index.ts └── types.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mickael Allonneau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fast Scroll 2 | 3 | NOTE: PLEASE REFRESH YOUR OPEN TABS AFTER THE INSTALLATION FOR THE EXTENSION TO WORK. 4 | 5 | This small extension allows you to increase scroll speed on web pages either : 6 | - by pressing a key of your choice between Left Alt (the default), Right Alt, Left Ctrl and Left Shift 7 | - or without doing anything : scroll speed will always be increased, and you'll have the ability to decrease it temporarily by pressing one of the keys mentioned above 8 | 9 | You'll also be able to configure the new scroll speed through the extension's Settings page. 10 | 11 | Quite handy to scroll through long web pages or big documents quickly ! 12 | 13 | If you have any feedback, feel free to ping me @flawyte on Twitter, send me an email or open an issue on the GitHub repo if you're a developer! 14 | 15 | ## Download 16 | 17 | Get it on the [Chrome Webstore](https://chrome.google.com/webstore/detail/fast-scroll/ecnjcglleblahonnenpaiofkabfakgdi)! 18 | 19 | ## Reminders (for myself) 20 | 21 | ### Zipping 22 | 23 | ```bash 24 | FS_VERSION=X.Y.Z 25 | cd fast-scroll/_build 26 | zip -r fast-scroll-v$FS_VERSION.zip . 27 | mv fast-scroll-v$FS_VERSION.zip ../.. 28 | cd ../.. 29 | ``` 30 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | builddir=_build/ 4 | srcdir=src/ 5 | 6 | # Clean old builds 7 | rm -rf $builddir 8 | mkdir $builddir 9 | 10 | # Copy main files 11 | cp $srcdir/manifest.json $builddir 12 | cp -r $srcdir/res $builddir 13 | cp -r $srcdir/_locales $builddir 14 | 15 | # Copy settings files 16 | mkdir $builddir/settings 17 | cp $srcdir/settings/index.html $builddir/settings/ 18 | 19 | # Copy libs files 20 | cp -r libs $builddir 21 | 22 | # Compile code 23 | node node_modules/rollup/dist/bin/rollup -c $1 # '$1' equals '--watch' when provided 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /libs/matter.min.css: -------------------------------------------------------------------------------- 1 | /* Matter 0.2.2 (min) */ 2 | .matter-button-contained{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));--matter-helper-ontheme:var(--matter-ontheme-rgb,var(--matter-onprimary-rgb,255,255,255));position:relative;display:inline-block;box-sizing:border-box;border:none;border-radius:4px;padding:0 16px;min-width:64px;height:36px;vertical-align:middle;text-align:center;text-overflow:ellipsis;color:rgb(var(--matter-helper-ontheme));background-color:rgb(var(--matter-helper-theme));box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:14px;font-weight:500;line-height:36px;outline:none;cursor:pointer;transition:box-shadow .2s}.matter-button-contained::-moz-focus-inner{border:none}.matter-button-contained:after,.matter-button-contained:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:inherit;opacity:0}.matter-button-contained:before{background-color:rgb(var(--matter-helper-ontheme));transition:opacity .2s}.matter-button-contained:after{background:radial-gradient(circle at center,currentColor 1%,transparent 0) 50%/10000% 10000% no-repeat;transition:opacity 1s,background-size .5s}.matter-button-contained:focus,.matter-button-contained:hover{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.matter-button-contained:hover:before{opacity:.08}.matter-button-contained:focus:before{opacity:.24}.matter-button-contained:hover:focus:before{opacity:.32}.matter-button-contained:active{box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12)}.matter-button-contained:active:after{opacity:.32;background-size:100% 100%;transition:background-size 0s}.matter-button-contained:disabled{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.12);box-shadow:none;cursor:auto}.matter-button-contained:disabled:after,.matter-button-contained:disabled:before{opacity:0}.matter-button-unelevated{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));--matter-helper-ontheme:var(--matter-ontheme-rgb,var(--matter-onprimary-rgb,255,255,255));position:relative;display:inline-block;box-sizing:border-box;border:none;border-radius:4px;padding:0 16px;min-width:64px;height:36px;vertical-align:middle;text-align:center;text-overflow:ellipsis;color:rgb(var(--matter-helper-ontheme));background-color:rgb(var(--matter-helper-theme));font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:14px;font-weight:500;line-height:36px;outline:none;cursor:pointer}.matter-button-unelevated::-moz-focus-inner{border:none}.matter-button-unelevated:after,.matter-button-unelevated:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:inherit;opacity:0}.matter-button-unelevated:before{background-color:rgb(var(--matter-helper-ontheme));transition:opacity .2s}.matter-button-unelevated:after{background:radial-gradient(circle at center,currentColor 1%,transparent 0) 50%/10000% 10000% no-repeat;transition:opacity 1s,background-size .5s}.matter-button-unelevated:hover:before{opacity:.08}.matter-button-unelevated:focus:before{opacity:.24}.matter-button-unelevated:hover:focus:before{opacity:.32}.matter-button-unelevated:active:after{opacity:.32;background-size:100% 100%;transition:background-size 0s}.matter-button-unelevated:disabled{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.12);cursor:auto}.matter-button-unelevated:disabled:after,.matter-button-unelevated:disabled:before{opacity:0}.matter-button-outlined{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));position:relative;display:inline-block;box-sizing:border-box;margin:0;border:1px solid;border-color:rgba(var(--matter-onsurface-rgb,0,0,0),.24);border-radius:4px;padding:0 16px;min-width:64px;height:36px;vertical-align:middle;text-align:center;text-overflow:ellipsis;color:rgb(var(--matter-helper-theme));background-color:transparent;font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:14px;font-weight:500;line-height:34px;outline:none;cursor:pointer}.matter-button-outlined::-moz-focus-inner{border:none}.matter-button-outlined:after,.matter-button-outlined:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:3px;opacity:0}.matter-button-outlined:before{background-color:rgb(var(--matter-helper-theme));transition:opacity .2s}.matter-button-outlined:after{background:radial-gradient(circle at center,currentColor 1%,transparent 0) 50%/10000% 10000% no-repeat;transition:opacity 1s,background-size .5s}.matter-button-outlined:hover:before{opacity:.04}.matter-button-outlined:focus:before{opacity:.12}.matter-button-outlined:hover:focus:before{opacity:.16}.matter-button-outlined:active:after{opacity:.16;background-size:100% 100%;transition:background-size 0s}.matter-button-outlined:disabled{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);background-color:transparent;cursor:auto}.matter-button-outlined:disabled:after,.matter-button-outlined:disabled:before{opacity:0}.matter-button-text{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));position:relative;display:inline-block;box-sizing:border-box;margin:0;border:none;border-radius:4px;padding:0 8px;min-width:64px;height:36px;vertical-align:middle;text-align:center;text-overflow:ellipsis;color:rgb(var(--matter-helper-theme));background-color:transparent;font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:14px;font-weight:500;line-height:36px;outline:none;cursor:pointer}.matter-button-text::-moz-focus-inner{border:none}.matter-button-text:after,.matter-button-text:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:inherit;opacity:0}.matter-button-text:before{background-color:rgb(var(--matter-helper-theme));transition:opacity .2s}.matter-button-text:after{background:radial-gradient(circle at center,currentColor 1%,transparent 0) 50%/10000% 10000% no-repeat;transition:opacity 1s,background-size .5s}.matter-button-text:hover:before{opacity:.04}.matter-button-text:focus:before{opacity:.12}.matter-button-text:hover:focus:before{opacity:.16}.matter-button-text:active:after{opacity:.16;background-size:100% 100%;transition:background-size 0s}.matter-button-text:disabled{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);background-color:transparent;cursor:auto}.matter-button-text:disabled:after,.matter-button-text:disabled:before{opacity:0}.matter-link{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));--matter-helper-safari1:rgba(var(--matter-helper-theme),0.12);border-radius:4px;color:rgb(var(--matter-helper-theme));text-decoration:none;transition:background-color .2s,box-shadow .2s}.matter-link:hover{text-decoration:underline}.matter-link:focus{background-color:var(--matter-helper-safari1);box-shadow:0 0 0 .16em var(--matter-helper-safari1);outline:none}.matter-link:active{background-color:transparent;box-shadow:none}.matter-progress-circular{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;border:none;border-radius:50%;padding:.25em;width:3em;height:3em;color:rgb(var(--matter-helper-theme));background-color:transparent;font-size:16px;overflow:hidden}.matter-progress-circular::-webkit-progress-bar{background-color:transparent}.matter-progress-circular:indeterminate{animation:matter-progress-circular 6s cubic-bezier(.3,.6,1,1) infinite}.matter-progress-circular:indeterminate,:-ms-lang(x){animation:none}.matter-progress-circular:indeterminate::-webkit-progress-value,.matter-progress-circular:indeterminate:before{content:"";display:block;box-sizing:border-box;margin-bottom:.25em;border:.25em solid;border-radius:50%;width:100%!important;height:100%;background-color:transparent;-webkit-clip-path:polygon(50% 50%,37% 0,50% 0,50% 0,50% 0,50% 0);clip-path:polygon(50% 50%,37% 0,50% 0,50% 0,50% 0,50% 0);animation:matter-progress-circular-pseudo .75s linear infinite alternate;animation-play-state:inherit;animation-delay:inherit}.matter-progress-circular:indeterminate::-moz-progress-bar{box-sizing:border-box;border:.25em solid;border-radius:50%;width:100%;height:100%;background-color:transparent;clip-path:polygon(50% 50%,37% 0,50% 0,50% 0,50% 0,50% 0);animation:matter-progress-circular-pseudo .75s linear infinite alternate;animation-play-state:inherit;animation-delay:inherit}.matter-progress-circular:indeterminate::-ms-fill{animation-name:-ms-ring}@keyframes matter-progress-circular{0%{transform:rotate(0deg)}12.5%{transform:rotate(180deg);animation-timing-function:linear}25%{transform:rotate(630deg)}37.5%{transform:rotate(810deg);animation-timing-function:linear}50%{transform:rotate(1260deg)}62.5%{transform:rotate(4turn);animation-timing-function:linear}75%{transform:rotate(1890deg)}87.5%{transform:rotate(2070deg);animation-timing-function:linear}to{transform:rotate(7turn)}}@keyframes matter-progress-circular-pseudo{0%{-webkit-clip-path:polygon(50% 50%,37% 0,50% 0,50% 0,50% 0,50% 0);clip-path:polygon(50% 50%,37% 0,50% 0,50% 0,50% 0,50% 0)}18%{-webkit-clip-path:polygon(50% 50%,37% 0,100% 0,100% 0,100% 0,100% 0);clip-path:polygon(50% 50%,37% 0,100% 0,100% 0,100% 0,100% 0)}53%{-webkit-clip-path:polygon(50% 50%,37% 0,100% 0,100% 100%,100% 100%,100% 100%);clip-path:polygon(50% 50%,37% 0,100% 0,100% 100%,100% 100%,100% 100%)}88%{-webkit-clip-path:polygon(50% 50%,37% 0,100% 0,100% 100%,0 100%,0 100%);clip-path:polygon(50% 50%,37% 0,100% 0,100% 100%,0 100%,0 100%)}to{-webkit-clip-path:polygon(50% 50%,37% 0,100% 0,100% 100%,0 100%,0 63%);clip-path:polygon(50% 50%,37% 0,100% 0,100% 100%,0 100%,0 63%)}}.matter-progress-linear{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;width:160px;height:4px;vertical-align:middle;color:rgb(var(--matter-helper-theme));background-color:rgba(var(--matter-helper-theme),.12)}.matter-progress-linear::-webkit-progress-bar{background-color:transparent}.matter-progress-linear::-webkit-progress-value{background-color:currentColor;transition:all .2s}.matter-progress-linear::-moz-progress-bar{background-color:currentColor;transition:all .2s}.matter-progress-linear::-ms-fill{border:none;background-color:currentColor;transition:all .2s}.matter-progress-linear:indeterminate{background-size:200% 100%;background-image:linear-gradient(90deg,currentColor 16%,transparent 0),linear-gradient(90deg,currentColor 16%,transparent 0),linear-gradient(90deg,currentColor 25%,transparent 0);animation:matter-progress-linear 1.8s linear infinite}.matter-progress-linear:indeterminate::-webkit-progress-value{background-color:transparent}.matter-progress-linear:indeterminate::-moz-progress-bar{background-color:transparent}.matter-progress-linear:indeterminate::-ms-fill{animation-name:none}@keyframes matter-progress-linear{0%{background-position:32% 0,32% 0,50% 0}2%{background-position:32% 0,32% 0,50% 0}21%{background-position:32% 0,-18% 0,0 0}42%{background-position:32% 0,-68% 0,-27% 0}50%{background-position:32% 0,-93% 0,-46% 0}56%{background-position:32% 0,-118% 0,-68% 0}66%{background-position:-11% 0,-200% 0,-100% 0}71%{background-position:-32% 0,-200% 0,-100% 0}79%{background-position:-54% 0,-242% 0,-100% 0}86%{background-position:-68% 0,-268% 0,-100% 0}to{background-position:-100% 0,-300% 0,-100% 0}}.matter-checkbox{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));--matter-helper-ontheme:var(--matter-ontheme-rgb,var(--matter-onprimary-rgb,255,255,255));z-index:0;position:relative;display:inline-block;color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:16px;line-height:1.5}.matter-checkbox>input{appearance:none;-moz-appearance:none;-webkit-appearance:none;z-index:1;position:absolute;display:block;box-sizing:border-box;margin:3px 1px;border:2px solid;border-color:rgba(var(--matter-onsurface-rgb,0,0,0),.6);border-radius:2px;width:18px;height:18px;outline:none;cursor:pointer;transition:border-color .2s,background-color .2s}.matter-checkbox>input+span{display:inline-block;box-sizing:border-box;padding-left:30px;width:inherit;cursor:pointer}.matter-checkbox>input+span:before{content:"";position:absolute;left:-10px;top:-8px;display:block;border-radius:50%;width:40px;height:40px;background-color:rgb(var(--matter-onsurface-rgb,0,0,0));opacity:0;transform:scale(1);pointer-events:none;transition:opacity .3s,transform .2s}.matter-checkbox>input+span:after{content:"";z-index:1;display:block;position:absolute;top:3px;left:1px;box-sizing:content-box;width:10px;height:5px;border-color:transparent;border-style:solid;border-width:0 0 2px 2px;pointer-events:none;transform:translate(3px,4px) rotate(-45deg);transition:border-color .2s}.matter-checkbox>input:checked,.matter-checkbox>input:indeterminate{border-color:rgb(var(--matter-helper-theme));background-color:rgb(var(--matter-helper-theme))}.matter-checkbox>input:checked+span:before,.matter-checkbox>input:indeterminate+span:before{background-color:rgb(var(--matter-helper-theme))}.matter-checkbox>input:checked+span:after,.matter-checkbox>input:indeterminate+span:after{border-color:rgb(var(--matter-helper-ontheme,255,255,255))}.matter-checkbox>input:indeterminate+span:after{border-left-width:0;transform:translate(4px,3px)}.matter-checkbox:hover>input+span:before{opacity:.04}.matter-checkbox>input:focus+span:before{opacity:.12}.matter-checkbox:hover>input:focus+span:before{opacity:.16}.matter-checkbox:active:hover>input,.matter-checkbox:active>input{border-color:rgb(var(--matter-helper-theme))}.matter-checkbox:active>input:checked{border-color:transparent;background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.6)}.matter-checkbox:active>input+span:before{opacity:1;transform:scale(0);transition:transform 0s,opacity 0s}.matter-checkbox>input:disabled{border-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);cursor:auto}.matter-checkbox>input:checked:disabled,.matter-checkbox>input:indeterminate:disabled{border-color:transparent;background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38)}.matter-checkbox>input:disabled+span{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);cursor:auto}.matter-checkbox>input:disabled+span:before{opacity:0;transform:scale(0)}.matter-radio{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));z-index:0;position:relative;display:inline-block;color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:16px;line-height:1.5}.matter-radio>input{appearance:none;-moz-appearance:none;-webkit-appearance:none;z-index:1;position:absolute;display:block;box-sizing:border-box;margin:2px 0;border:2px solid;border-color:rgba(var(--matter-onsurface-rgb,0,0,0),.6);border-radius:50%;width:20px;height:20px;outline:none;cursor:pointer;transition:border-color .2s}.matter-radio>input+span{display:inline-block;box-sizing:border-box;padding-left:30px;width:inherit;cursor:pointer}.matter-radio>input+span:before{content:"";position:absolute;left:-10px;top:-8px;display:block;border-radius:50%;width:40px;height:40px;background-color:rgb(var(--matter-onsurface-rgb,0,0,0));opacity:0;transform:scale(0);pointer-events:none;transition:opacity .3s,transform .2s}.matter-radio>input+span:after{content:"";display:block;position:absolute;top:2px;left:0;border-radius:50%;width:10px;height:10px;background-color:rgb(var(--matter-helper-theme));transform:translate(5px,5px) scale(0);transition:transform .2s}.matter-radio>input:checked{border-color:rgb(var(--matter-helper-theme))}.matter-radio>input:checked+span:before{background-color:rgb(var(--matter-helper-theme))}.matter-radio>input:checked+span:after{transform:translate(5px,5px) scale(1)}.matter-radio:hover>input+span:before{transform:scale(1);opacity:.04}.matter-radio>input:focus+span:before{transform:scale(1);opacity:.12}.matter-radio:hover>input:focus+span:before{transform:scale(1);opacity:.16}.matter-radio:active>input{border-color:rgb(var(--matter-helper-theme))}.matter-radio:active:hover>input+span:before,.matter-radio:active>input+span:before{opacity:1;transform:scale(0);transition:transform 0s,opacity 0s}.matter-radio>input:disabled{border-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);cursor:auto}.matter-radio>input:disabled+span{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);cursor:auto}.matter-radio>input:disabled+span:before{opacity:0;transform:scale(0)}.matter-radio>input:disabled+span:after{background-color:currentColor}.matter-switch{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));z-index:0;color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:16px;line-height:1.5}.matter-switch,.matter-switch>input{position:relative;display:inline-block}.matter-switch>input{appearance:none;-moz-appearance:none;-webkit-appearance:none;z-index:1;float:right;margin:0 0 0 5px;border:5px solid transparent;border-radius:12px;width:46px;height:24px;background-clip:padding-box;background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);outline:none;cursor:pointer;transition:background-color .2s,opacity .2s}.matter-switch>input+span{display:inline-block;box-sizing:border-box;margin-right:-51px;padding-right:51px;width:inherit;cursor:pointer}.matter-switch>input+span:before{right:11px;top:-8px;display:block;width:40px;height:40px;background-color:rgb(var(--matter-onsurface-rgb,0,0,0));opacity:0;transform:scale(1);transition:opacity .3s .1s,transform .2s .1s}.matter-switch>input+span:after,.matter-switch>input+span:before{content:"";position:absolute;border-radius:50%;pointer-events:none}.matter-switch>input+span:after{z-index:1;top:2px;right:21px;width:20px;height:20px;background-color:rgb(var(--matter-surface-rgb,255,255,255));box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);transition:background-color .2s,transform .2s}.matter-switch>input:checked{background-color:rgba(var(--matter-helper-theme),.6)}.matter-switch>input:checked+span:before{right:-5px;background-color:rgb(var(--matter-helper-theme))}.matter-switch>input:checked+span:after{background-color:rgb(var(--matter-helper-theme));transform:translateX(16px)}.matter-switch:hover>input+span:before{opacity:.04}.matter-switch>input:focus+span:before{opacity:.12}.matter-switch:hover>input:focus+span:before{opacity:.16}.matter-switch:active>input{background-color:rgba(var(--matter-helper-theme),.6)}.matter-switch:active>input:checked{background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38)}.matter-switch:active>input+span:before{opacity:1;transform:scale(0);transition:transform 0s,opacity 0s}.matter-switch>input:disabled{background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);opacity:.38;cursor:default}.matter-switch>input:checked:disabled{background-color:rgba(var(--matter-helper-theme),.6)}.matter-switch>input:disabled+span{color:rgba(var(--matter-onsurface-rgb,0,0,0,.38));cursor:default}.matter-switch>input:disabled+span:before{z-index:1;margin:10px;width:20px;height:20px;background-color:rgb(var(--matter-surface-rgb,255,255,255));transform:scale(1);opacity:1;transition:none}.matter-switch>input:disabled+span:after{opacity:.38}.matter-textfield-standard{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));position:relative;display:inline-block;font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:16px;line-height:1.5}.matter-textfield-standard>input,.matter-textfield-standard>textarea{display:block;box-sizing:border-box;margin:0;border:none;border-top:24px solid transparent;border-bottom:1px solid rgba(var(--matter-onsurface-rgb,0,0,0),.6);padding:0 0 7px;width:100%;height:inherit;color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);-webkit-text-fill-color:currentColor;background-color:transparent;box-shadow:none;font-family:inherit;font-size:inherit;line-height:inherit;caret-color:rgb(var(--matter-helper-theme));transition:border-bottom .2s,background-color .2s}.matter-textfield-standard>input+span,.matter-textfield-standard>textarea+span{position:absolute;top:0;left:0;right:0;bottom:0;display:block;box-sizing:border-box;padding:7px 0 0;color:rgba(var(--matter-onsurface-rgb,0,0,0),.6);font-size:75%;line-height:18px;pointer-events:none;transition:color .2s,font-size .2s,line-height .2s}.matter-textfield-standard>input+span:after,.matter-textfield-standard>textarea+span:after{content:"";position:absolute;left:0;bottom:0;display:block;width:100%;height:2px;background-color:rgb(var(--matter-helper-theme));transform-origin:bottom center;transform:scaleX(0);transition:transform .2s}.matter-textfield-standard:hover>input,.matter-textfield-standard:hover>textarea{border-bottom-color:rgba(var(--matter-onsurface-rgb,0,0,0),.87)}.matter-textfield-standard>input:not(:focus):placeholder-shown+span,.matter-textfield-standard>textarea:not(:focus):placeholder-shown+span{font-size:inherit;line-height:56px}.matter-textfield-standard>input:focus,.matter-textfield-standard>textarea:focus{outline:none}.matter-textfield-standard>input:focus+span,.matter-textfield-standard>textarea:focus+span{color:rgb(var(--matter-helper-theme))}.matter-textfield-standard>input:focus+span:after,.matter-textfield-standard>textarea:focus+span:after{transform:scale(1)}.matter-textfield-standard>input:disabled,.matter-textfield-standard>textarea:disabled{border-bottom-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);color:rgba(var(--matter-onsurface-rgb,0,0,0),.38)}.matter-textfield-standard>input:disabled+span,.matter-textfield-standard>textarea:disabled+span{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38)}@media not all and (min-resolution:.001dpcm){@supports (-webkit-appearance:none){.matter-textfield-standard>input,.matter-textfield-standard>input+span,.matter-textfield-standard>input+span:after,.matter-textfield-standard>textarea,.matter-textfield-standard>textarea+span,.matter-textfield-standard>textarea+span:after{transition-duration:.1s}}}.matter-textfield-filled{--matter-helper-theme:var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243));position:relative;display:inline-block;font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:16px;line-height:1.5}.matter-textfield-filled>input,.matter-textfield-filled>textarea{display:block;box-sizing:border-box;margin:0;border:none;border-top:24px solid transparent;border-bottom:1px solid rgba(var(--matter-onsurface-rgb,0,0,0),.6);border-radius:4px 4px 0 0;padding:0 12px 7px;width:100%;height:inherit;color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);-webkit-text-fill-color:currentColor;background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.04);box-shadow:none;font-family:inherit;font-size:inherit;line-height:inherit;caret-color:rgb(var(--matter-helper-theme));transition:border-bottom .2s,background-color .2s}.matter-textfield-filled>input+span,.matter-textfield-filled>textarea+span{position:absolute;top:0;left:0;right:0;bottom:0;display:block;box-sizing:border-box;padding:7px 12px 0;color:rgba(var(--matter-onsurface-rgb,0,0,0),.6);font-size:75%;line-height:18px;pointer-events:none;transition:color .2s,font-size .2s,line-height .2s}.matter-textfield-filled>input+span:after,.matter-textfield-filled>textarea+span:after{content:"";position:absolute;left:0;bottom:0;display:block;width:100%;height:2px;background-color:rgb(var(--matter-helper-theme));transform-origin:bottom center;transform:scaleX(0);transition:transform .3s}.matter-textfield-filled:hover>input,.matter-textfield-filled:hover>textarea{border-bottom-color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.08)}.matter-textfield-filled>input:not(:focus):placeholder-shown+span,.matter-textfield-filled>textarea:not(:focus):placeholder-shown+span{font-size:inherit;line-height:48px}.matter-textfield-filled>input:focus,.matter-textfield-filled>textarea:focus{outline:none}.matter-textfield-filled>input:focus+span,.matter-textfield-filled>textarea:focus+span{color:rgb(var(--matter-helper-theme))}.matter-textfield-filled>input:focus+span:after,.matter-textfield-filled>textarea:focus+span:after{transform:scale(1)}.matter-textfield-filled>input:disabled,.matter-textfield-filled>textarea:disabled{border-bottom-color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.24)}.matter-textfield-filled>input:disabled+span,.matter-textfield-filled>textarea:disabled+span{color:rgba(var(--matter-onsurface-rgb,0,0,0),.38)}@media not all and (min-resolution:.001dpcm){@supports (-webkit-appearance:none){.matter-textfield-filled>input,.matter-textfield-filled>input+span,.matter-textfield-filled>input+span:after,.matter-textfield-filled>textarea,.matter-textfield-filled>textarea+span,.matter-textfield-filled>textarea+span:after{transition-duration:.1s}}}.matter-textfield-outlined{--matter-helper-theme:rgb(var(--matter-theme-rgb,var(--matter-primary-rgb,33,150,243)));--matter-helper-safari1:rgba(var(--matter-onsurface-rgb,0,0,0),0.38);--matter-helper-safari2:rgba(var(--matter-onsurface-rgb,0,0,0),0.6);--matter-helper-safari3:rgba(var(--matter-onsurface-rgb,0,0,0),0.87);position:relative;display:inline-block;padding-top:6px;font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:16px;line-height:1.5}.matter-textfield-outlined>input,.matter-textfield-outlined>textarea{box-sizing:border-box;margin:0;border:1px solid var(--matter-helper-safari2);border-top:1px solid transparent;border-radius:4px;padding:15px 13px;width:100%;height:inherit;color:rgba(var(--matter-onsurface-rgb,0,0,0),.87);-webkit-text-fill-color:currentColor;background-color:transparent;box-shadow:inset 1px 0 transparent,inset -1px 0 transparent,inset 0 -1px transparent;font-family:inherit;font-size:inherit;line-height:inherit;caret-color:var(--matter-helper-theme);transition:border .2s,box-shadow .2s}.matter-textfield-outlined>input:not(:focus):placeholder-shown,.matter-textfield-outlined>textarea:not(:focus):placeholder-shown{border-top-color:var(--matter-helper-safari2)}.matter-textfield-outlined>input+span,.matter-textfield-outlined>textarea+span{position:absolute;top:0;left:0;display:flex;width:100%;max-height:100%;color:rgba(var(--matter-onsurface-rgb,0,0,0),.6);font-size:75%;line-height:15px;cursor:text;transition:color .2s,font-size .2s,line-height .2s}.matter-textfield-outlined>input:not(:focus):placeholder-shown+span,.matter-textfield-outlined>textarea:not(:focus):placeholder-shown+span{font-size:inherit;line-height:68px}.matter-textfield-outlined>input+span:after,.matter-textfield-outlined>input+span:before,.matter-textfield-outlined>textarea+span:after,.matter-textfield-outlined>textarea+span:before{content:"";display:block;box-sizing:border-box;margin-top:6px;border-top:1px solid var(--matter-helper-safari2);min-width:10px;height:8px;pointer-events:none;box-shadow:inset 0 1px transparent;transition:border .2s,box-shadow .2s}.matter-textfield-outlined>input+span:before,.matter-textfield-outlined>textarea+span:before{margin-right:4px;border-left:1px solid transparent;border-radius:4px 0}.matter-textfield-outlined>input+span:after,.matter-textfield-outlined>textarea+span:after{flex-grow:1;margin-left:4px;border-right:1px solid transparent;border-radius:0 4px}.matter-textfield-outlined>input:not(:focus):placeholder-shown+span:after,.matter-textfield-outlined>input:not(:focus):placeholder-shown+span:before,.matter-textfield-outlined>textarea:not(:focus):placeholder-shown+span:after,.matter-textfield-outlined>textarea:not(:focus):placeholder-shown+span:before{border-top-color:transparent}.matter-textfield-outlined:hover>input,.matter-textfield-outlined:hover>textarea{border-left-color:var(--matter-helper-safari3);border-bottom-color:var(--matter-helper-safari3);border-right-color:var(--matter-helper-safari3);border-top-color:transparent}.matter-textfield-outlined:hover>input+span:after,.matter-textfield-outlined:hover>input+span:before,.matter-textfield-outlined:hover>textarea+span:after,.matter-textfield-outlined:hover>textarea+span:before{border-top-color:var(--matter-helper-safari3)}.matter-textfield-outlined:hover>input:not(:focus):placeholder-shown,.matter-textfield-outlined:hover>textarea:not(:focus):placeholder-shown{border-color:var(--matter-helper-safari3)}.matter-textfield-outlined>input:focus,.matter-textfield-outlined>textarea:focus{border-left-color:var(--matter-helper-theme);border-bottom-color:var(--matter-helper-theme);border-right-color:var(--matter-helper-theme);border-top-color:transparent;box-shadow:inset 1px 0 var(--matter-helper-theme),inset -1px 0 var(--matter-helper-theme),inset 0 -1px var(--matter-helper-theme);outline:none}.matter-textfield-outlined>input:focus+span,.matter-textfield-outlined>textarea:focus+span{color:var(--matter-helper-theme)}.matter-textfield-outlined>input:focus+span:after,.matter-textfield-outlined>input:focus+span:before,.matter-textfield-outlined>textarea:focus+span:after,.matter-textfield-outlined>textarea:focus+span:before{border-top-color:var(--matter-helper-theme)!important;box-shadow:inset 0 1px var(--matter-helper-theme)}.matter-textfield-outlined>input:disabled,.matter-textfield-outlined>input:disabled+span,.matter-textfield-outlined>textarea:disabled,.matter-textfield-outlined>textarea:disabled+span{border-left-color:var(--matter-helper-safari1)!important;border-bottom-color:var(--matter-helper-safari1)!important;border-right-color:var(--matter-helper-safari1)!important;border-top-color:transparent!important;color:rgba(var(--matter-onsurface-rgb,0,0,0),.38);pointer-events:none}.matter-textfield-outlined>input:disabled+span:after,.matter-textfield-outlined>input:disabled+span:before,.matter-textfield-outlined>textarea:disabled+span:after,.matter-textfield-outlined>textarea:disabled+span:before{border-top-color:var(--matter-helper-safari1)!important}.matter-textfield-outlined>input:disabled:placeholder-shown,.matter-textfield-outlined>input:disabled:placeholder-shown+span,.matter-textfield-outlined>textarea:disabled:placeholder-shown,.matter-textfield-outlined>textarea:disabled:placeholder-shown+span{border-top-color:var(--matter-helper-safari1)!important}.matter-textfield-outlined>input:disabled:placeholder-shown+span:after,.matter-textfield-outlined>input:disabled:placeholder-shown+span:before,.matter-textfield-outlined>textarea:disabled:placeholder-shown+span:after,.matter-textfield-outlined>textarea:disabled:placeholder-shown+span:before{border-top-color:transparent!important}@media not all and (min-resolution:.001dpcm){@supports (-webkit-appearance:none){.matter-textfield-outlined>input,.matter-textfield-outlined>input+span,.matter-textfield-outlined>input+span:after,.matter-textfield-outlined>input+span:before,.matter-textfield-outlined>textarea,.matter-textfield-outlined>textarea+span,.matter-textfield-outlined>textarea+span:after,.matter-textfield-outlined>textarea+span:before{transition-duration:.1s}}}.matter-tooltip,.matter-tooltip-top{z-index:10;position:absolute;left:0;right:0;font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:10px;font-weight:400;line-height:16px;white-space:nowrap;text-transform:none;text-align:center;pointer-events:none}.matter-tooltip{bottom:-40px}.matter-tooltip-top{top:-40px}.matter-tooltip-top>span,.matter-tooltip>span{position:-webkit-sticky;position:sticky;left:0;right:0;display:inline-block;box-sizing:content-box;margin:0 -100vw;border:8px solid transparent;border-radius:12px;padding:4px 8px;color:rgb(var(--matter-surface-rgb,255,255,255));background-clip:padding-box;background-image:linear-gradient(rgba(var(--matter-surface-rgb,255,255,255),.34),rgba(var(--matter-surface-rgb,255,255,255),.34));background-color:rgba(var(--matter-onsurface-rgb,0,0,0),.85);transform:scale(0);opacity:0;pointer-events:auto;transition:transform 75ms,opacity 75ms}.matter-tooltip-top:hover>span,.matter-tooltip:hover>span,:not(html):hover>.matter-tooltip-top>span,:not(html):hover>.matter-tooltip>span{transform:scale(1);opacity:1;transition:transform .15s,opacity .15s}:focus-within>.matter-tooltip-top>span,:focus-within>.matter-tooltip>span{transform:scale(1);opacity:1;transition:transform .15s,opacity .15s}@media (hover:none),(pointer:coarse){.matter-tooltip,.matter-tooltip-top{font-size:14px;line-height:20px}.matter-tooltip{bottom:-48px}.matter-tooltip-top{top:-48px}.matter-tooltip-top>span,.matter-tooltip>span{padding:6px 16px}}.matter-primary{--matter-theme-rgb:var(--matter-primary-rgb,33,150,243);--matter-ontheme-rgb:var(--matter-onprimary-rgb,255,255,255)}.matter-secondary{--matter-theme-rgb:var(--matter-secondary-rgb,102,0,238);--matter-ontheme-rgb:var(--matter-onsecondary-rgb,255,255,255)}.matter-error{--matter-theme-rgb:var(--matter-error-rgb,238,0,0);--matter-ontheme-rgb:var(--matter-error-rgb,255,255,255)}.matter-warning{--matter-theme-rgb:var(--matter-warning-rgb,238,102,0);--matter-ontheme-rgb:var(--matter-onwarning-rgb,255,255,255)}.matter-success{--matter-theme-rgb:var(--matter-success-rgb,17,136,34);--matter-ontheme-rgb:var(--matter-onsuccess-rgb,255,255,255)}.matter-primary-text{color:rgb(var(--matter-primary-rgb,33,150,243))}.matter-secondary-text{color:rgb(var(--matter-secondary-rgb,102,0,238))}.matter-error-text{color:rgb(var(--matter-error-rgb,238,0,0))}.matter-warning-text{color:rgb(var(--matter-warning-rgb,238,102,0))}.matter-success-text{color:rgb(var(--matter-success-rgb,17,136,34))}.matter-h1{font-size:96px;letter-spacing:-1.5px;line-height:120px}.matter-h1,.matter-h2{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-weight:300}.matter-h2{font-size:60px;letter-spacing:-.5px;line-height:80px}.matter-h3{font-size:48px;letter-spacing:0;line-height:64px}.matter-h3,.matter-h4{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-weight:400}.matter-h4{font-size:34px;letter-spacing:.25px;line-height:48px}.matter-h5{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:24px;font-weight:400;letter-spacing:0;line-height:36px}.matter-h6{font-size:20px;font-weight:500;line-height:28px}.matter-h6,.matter-subtitle1{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);letter-spacing:.15px}.matter-subtitle1{font-size:16px;font-weight:400;line-height:24px}.matter-subtitle2{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-size:14px;font-weight:500;letter-spacing:.1px;line-height:20px}.matter-body1{font-size:16px;letter-spacing:.5px;line-height:24px}.matter-body1,.matter-body2{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-weight:400}.matter-body2{letter-spacing:.25px}.matter-body2,.matter-button{font-size:14px;line-height:20px}.matter-button{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-weight:500;letter-spacing:1.25px;text-transform:uppercase}.matter-caption{font-size:12px;letter-spacing:.4px;line-height:20px}.matter-caption,.matter-overline{font-family:var(--matter-font-family,"Roboto","Segoe UI",BlinkMacSystemFont,system-ui,-apple-system);font-weight:400}.matter-overline{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;line-height:16px} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "./build.sh", 4 | "test": "jest", 5 | "watch": "./build.sh --watch" 6 | }, 7 | "devDependencies": { 8 | "@rollup/plugin-commonjs": "^23.0.2", 9 | "@rollup/plugin-node-resolve": "^15.0.1", 10 | "@rollup/plugin-typescript": "^9.0.2", 11 | "@types/jest": "^29.2.2", 12 | "jest": "^29.3.1", 13 | "rollup": "^3.29.5", 14 | "rollup-plugin-copy": "^3.4.0", 15 | "rollup-plugin-node-builtins": "^2.1.2", 16 | "rollup-plugin-node-globals": "^1.4.0", 17 | "rollup-plugin-scss": "^3.0.0", 18 | "sass": "^1.56.1", 19 | "ts-jest": "^29.0.3", 20 | "tslib": "^2.4.1", 21 | "typescript": "^4.8.4" 22 | }, 23 | "dependencies": { 24 | "micromatch": "^4.0.8" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import copy from 'rollup-plugin-copy'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import nodeBuiltinModules from 'rollup-plugin-node-builtins'; 4 | import nodeGlobals from 'rollup-plugin-node-globals'; // (1) 5 | import resolve from '@rollup/plugin-node-resolve'; 6 | import scss from 'rollup-plugin-scss'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | 9 | /* (1) Required because the node global `process` is used somewhere in micromatch or one of its dependencies */ 10 | 11 | const plugins = [ 12 | copy({ 13 | /* Changes to these files won't be automatically picked in watch mode. To work around this, one will have to save 14 | a file that *is* watched for the copy to happen again. */ 15 | targets: [ 16 | { src: 'src/settings/index.html', dest: '_build/settings/' } 17 | ] 18 | }), 19 | resolve(), 20 | commonjs(), 21 | nodeGlobals(), 22 | nodeBuiltinModules(), 23 | scss({ 24 | output: '_build/settings/index.css' 25 | }), 26 | typescript(), 27 | ]; 28 | 29 | export default [{ 30 | input: 'src/index.ts', 31 | output: { 32 | file: '_build/index.js', 33 | format: 'iife' 34 | }, 35 | plugins 36 | }, { 37 | input: 'src/settings/index.ts', 38 | output: { 39 | file: '_build/settings/index.js', 40 | format: 'iife' 41 | }, 42 | plugins 43 | }]; 44 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta_description": { 3 | "description": "The extension's description", 4 | "message": "Fast Scroll is a tiny but very handy extension that allows you to scroll through web pages way faster." 5 | }, 6 | 7 | "Settings_IgnoredUrls_label": { 8 | "description": "Label for the text area that allows to specify a list of URL globs where not to run the extension", 9 | "message": "Disable the extension on these URLs" 10 | }, 11 | "Settings_Mode_label": { 12 | "description": "Label for the dropdown menu that allows to select the extension's default behavior", 13 | "message": "Increase scroll speed" 14 | }, 15 | "Settings_Mode_Always_label": { 16 | "description": "Label for the dropdown menu item to always scroll faster", 17 | "message": "Always" 18 | }, 19 | "Settings_Mode_Always_description": { 20 | "description": "Description for the dropdown menu item to always scroll faster", 21 | "message": "In this mode, the scroll speed will always be increased except when the Trigger key is pressed." 22 | }, 23 | "Settings_Mode_Always_warning": { 24 | "description": "Warning for the dropdown menu item to always scroll faster", 25 | "message": "Warning : This might not work on some websites and break scrolling completely. In this case, you can add these websites to the list of URLs to ignore below." 26 | }, 27 | "Settings_Mode_OnTriggerKeyPressed_label": { 28 | "description": "Label for the dropdown menu item to scroll faster only when the trigger key is pressed", 29 | "message": "When the Trigger key is pressed" 30 | }, 31 | "Settings_Mode_OnTriggerKeyPressed_description": { 32 | "description": "Description for the dropdown menu item to scroll faster only when the trigger key is pressed", 33 | "message": "In this mode, the scroll speed will be increased only when the Trigger key is pressed." 34 | }, 35 | "Settings_SaveButton_label": { 36 | "description": "Save button's label", 37 | "message": "Save" 38 | }, 39 | "Settings_ScrollSpeedMultiplier_label": { 40 | "description": "Label for the text field that allows to define the scroll speed multiplier", 41 | "message": "Multiply scroll speed by" 42 | }, 43 | "Settings_TriggerKey_label": { 44 | "description": "Label for the dropdown menu that allows to define the trigger key", 45 | "message": "Trigger key" 46 | }, 47 | "Settings_TriggerKey_AltLeft": { 48 | "description": "Left Alt key's name", 49 | "message": "Left Alt" 50 | }, 51 | "Settings_TriggerKey_AltRight": { 52 | "description": "Right Alt key's name", 53 | "message": "Right Alt" 54 | }, 55 | "Settings_TriggerKey_ControlLeft": { 56 | "description": "Left Control key's name", 57 | "message": "Left Ctrl" 58 | }, 59 | "Settings_TriggerKey_ShiftLeft": { 60 | "description": "Left Shift key's name", 61 | "message": "Left Shift" 62 | }, 63 | "Settings_TriggerKey_ControlLeft_warning": { 64 | "description": "Left Control key's warning messsage", 65 | "message": "Warning : This will override this key's default behavior, which allows you to zoom in / out on websites using your mouse wheel." 66 | }, 67 | "Settings_TriggerKey_ShiftLeft_warning": { 68 | "description": "Left Control key's warning messsage", 69 | "message": "Warning : This will override this key's default behavior, which allows you to scroll horizontally." 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta_description": { 3 | "description": "La description de l'extension", 4 | "message": "Fast Scroll est une petite extension bien pratique qui vous permet de faire défiler les pages web beaucoup plus rapidement." 5 | }, 6 | 7 | "Settings_IgnoredUrls_label": { 8 | "description": "Libellé pour la zone de texte qui permet d'indiquer une liste d'URLs où l'extension ne doit pas être lancée", 9 | "message": "Désactiver l'extension sur ces URLs" 10 | }, 11 | "Settings_Mode_label": { 12 | "description": "Libellé pour le menu déroulant qui permet de sélectionner le comportement par défaut de l'extension", 13 | "message": "Augmenter la vitesse de défilement" 14 | }, 15 | "Settings_Mode_Always_label": { 16 | "description": "Libellé pour l'élément du menu déroulant pour toujours faire défiler plus vite", 17 | "message": "Toujours" 18 | }, 19 | "Settings_Mode_Always_description": { 20 | "description": "Description pour l'élément du menu déroulant pour toujours faire défiler plus vite", 21 | "message": "Dans ce mode, la vitesse de défilement sera toujours augmentée sauf lorsque la Touche d'activation est enfoncée." 22 | }, 23 | "Settings_Mode_Always_warning": { 24 | "description": "Avertissement pour l'élément du menu déroulant pour toujours faire défiler plus vite", 25 | "message": "Attention : Cela pourrait ne pas fonctionner sur certains sites et empêcher complètement de faire défiler la page. Dans ce cas, vous pouvez ajouter ces sites à la liste des URLs à ignorer ci-dessous." 26 | }, 27 | "Settings_Mode_OnTriggerKeyPressed_label": { 28 | "description": "Libellé pour l'élement du menu déroulant pour faire défiler plus vite lorsque la touche d'activation est enfoncée", 29 | "message": "Lorsque la Touche d'activation est enfoncée" 30 | }, 31 | "Settings_Mode_OnTriggerKeyPressed_description": { 32 | "description": "Description pour l'élement du menu déroulant pour faire défiler plus vite lorsque la touche d'activation est enfoncée", 33 | "message": "Dans ce mode, la vitesse de défilement sera augmentée uniquement lorsque la Touche d'activation est enfoncée." 34 | }, 35 | "Settings_SaveButton_label": { 36 | "description": "Libellé du bouton Enregistrer", 37 | "message": "Enregistrer" 38 | }, 39 | "Settings_ScrollSpeedMultiplier_label": { 40 | "description": "Libellé du champ de texte qui permet de définir le multiplicateur de la vitesse de défilement", 41 | "message": "Multiplier la vitesse de défilement par" 42 | }, 43 | "Settings_TriggerKey_label": { 44 | "description": "Libellé du champ de texte qui permet de définir la touche à enfoncer", 45 | "message": "Touche d'activation" 46 | }, 47 | "Settings_TriggerKey_AltLeft": { 48 | "description": "Nom de la touche Alt Gauche", 49 | "message": "Alt Gauche" 50 | }, 51 | "Settings_TriggerKey_AltRight": { 52 | "description": "Nom de la touche Alt Droite", 53 | "message": "Alt Droite" 54 | }, 55 | "Settings_TriggerKey_ControlLeft": { 56 | "description": "Nom de la touche Ctrl Gauche", 57 | "message": "Ctrl Gauche" 58 | }, 59 | "Settings_TriggerKey_ShiftLeft": { 60 | "description": "Nom de la touche Maj Gauche", 61 | "message": "Maj Gauche" 62 | }, 63 | "Settings_TriggerKey_ControlLeft_warning": { 64 | "description": "Avertissement de la touche Ctrl Gauche", 65 | "message": "Attention : Cela va écraser le comportement par défaut de cette touche, qui permet de zoomer / dézoomer sur les sites web à l'aide de la molette." 66 | }, 67 | "Settings_TriggerKey_ShiftLeft_warning": { 68 | "description": "Avertissement de la touche Maj Gauche", 69 | "message": "Attention : Cela va écraser le comportement par défaut de cette touche, qui permet de faire défiler la page horizontalement." 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | declare const chrome; 2 | 3 | // 4 | // Imports 5 | // 6 | 7 | import { Settings, ScrollAxis } from './types'; 8 | import defaultSettings from './settings/default-settings'; 9 | import { urlMatchesAnyGlobOf } from './settings/ignored-urls'; 10 | 11 | // 12 | // Variables 13 | // 14 | 15 | let pressedKeys = new Set(); 16 | let settings: Settings; 17 | let triggerKeyIsPressed = false; 18 | 19 | const overflowValuesThatEnableScrollbarsOnContentElements = ['auto', 'overlay', 'scroll']; 20 | 21 | // 22 | // Init 23 | // 24 | 25 | loadSettings(() => { 26 | attachPageFocusLossListener(); 27 | attachKeysListeners(); 28 | attachWheelListener(); 29 | console.debug('[fast-scroll] ready', window.location.href); 30 | }); 31 | 32 | // 33 | // Helpers 34 | // 35 | 36 | function attachPageFocusLossListener() { 37 | /* clear pressed keys when the page loses focus, because by default they stay pressed when tab-switching to another 38 | OS window, which causes unexpected scrolling behavior when returning to the browser */ 39 | window.addEventListener('blur', () => { 40 | pressedKeys.clear(); 41 | triggerKeyIsPressed = false; 42 | }); 43 | } 44 | 45 | function attachKeysListeners() { 46 | window.addEventListener('keydown', event => { 47 | pressedKeys.add(event.code); 48 | triggerKeyIsPressed = pressedKeys.has(settings.triggerKey); 49 | }); 50 | window.addEventListener('keyup', event => { 51 | pressedKeys.delete(event.code); 52 | triggerKeyIsPressed = pressedKeys.has(settings.triggerKey); 53 | }); 54 | 55 | /* By default, on Windows (but not on Linux), Google Chrome uses the Alt Left key to focus the browser's menu button, 56 | which interferes with Fast Scroll because the web page loses focus when Alt Left is released so you can't press it 57 | multiple times in a row to scroll faster, you have to click on the page so it regains focus everytime. So we're 58 | preventing this default behavior when Alt Left is used as the trigger key. */ 59 | const preventAltLeftDefault = settings.triggerKey === Settings.TriggerKey.AltLeft; 60 | if (preventAltLeftDefault) { 61 | window.addEventListener('keyup', event => { 62 | if (event.code === Settings.TriggerKey.AltLeft) 63 | event.preventDefault(); 64 | }, { passive: false }); 65 | } 66 | } 67 | 68 | function attachWheelListener() { 69 | if (settings.mode === Settings.Mode.OnTriggerKeyPressed) 70 | window.addEventListener('wheel', onWheelModeOnTriggerKeyPressed, { passive: false }); 71 | else if (settings.mode === Settings.Mode.Always) 72 | window.addEventListener('wheel', onWheelModeAlways, { passive: false }); 73 | else 74 | throw new Error(`Unknown mode '${settings.mode}'`); 75 | } 76 | 77 | function elementIsScrollable(element, axis: ScrollAxis): boolean { 78 | const elementComputedStyle = window.getComputedStyle(element); 79 | 80 | if (axis === 'horizontal') { 81 | // if the element is as big as its content, it can't be scrolled (and doesn't need to) 82 | if (element.scrollWidth === element.clientWidth) 83 | return false; 84 | 85 | // and elements are scrollable as long as their overflow content isn't hidden 86 | if (element instanceof HTMLBodyElement || element instanceof HTMLHtmlElement) 87 | return true; 88 | 89 | // other elements are scrollable only when their 'overflow-' CSS attr has a value that enables scrollbars 90 | return overflowValuesThatEnableScrollbarsOnContentElements.includes(elementComputedStyle.overflowX); 91 | } 92 | else { 93 | // if the element is as big as its content, it can't be scrolled (and doesn't need to) 94 | if (element.scrollHeight === element.clientHeight) 95 | return false; 96 | 97 | // and elements are scrollable as long as their overflow content isn't hidden 98 | if (element instanceof HTMLBodyElement || element instanceof HTMLHtmlElement) 99 | return true; 100 | 101 | // other elements are scrollable only when their 'overflow-' CSS attr has a value that enables scrollbars 102 | return overflowValuesThatEnableScrollbarsOnContentElements.includes(elementComputedStyle.overflowY); 103 | } 104 | } 105 | 106 | /** 107 | * @returns The first element in the `element`'s hierarchy (including the element itself) that has a scrollbar for the 108 | * given `axis`, or `null` if none has any. 109 | */ 110 | function findScrollTarget(element, axis: ScrollAxis) { 111 | if (element === document.body) { 112 | /* On some websites the element is seen as scrollable by Fast Scroll but `body.scrollBy()` does nothing, 113 | however as per my tests in all these cases `document.scrollingElement.scrollBy()` does work (with `scrollingElement` 114 | being the element). That's why we check if `document.scrollingElement` is scrollable before checking the 115 | . */ 116 | if (elementIsScrollable(document.scrollingElement, axis)) 117 | return document.scrollingElement; 118 | } 119 | 120 | if (elementIsScrollable(element, axis)) 121 | return element; 122 | else if (element.parentElement) 123 | return findScrollTarget(element.parentElement, axis); 124 | return null; 125 | } 126 | 127 | function handleScroll(event: WheelEvent, speed: 'custom' | 'default') { 128 | event.preventDefault(); 129 | 130 | const scrollAmount = (() => { 131 | /* When scrolling to the bottom or to the right, `event.deltaY` will be a positive int ; when scrolling to the top or 132 | to the left, it will be a negative int. 133 | During my tests `event.deltaX` never changed and was always `0`, but I think that's because my mouse has a 134 | unidirectional wheel, while some other mouses can be have a bidirectional wheel, that's why I'm checking it ‒ to 135 | [hopefully] support all possible use cases */ 136 | const scrollAmountDefault = event.deltaY || event.deltaX; 137 | 138 | if (speed === 'custom') 139 | return scrollAmountDefault * settings.scrollSpeedMultiplier; 140 | else if (speed === 'default') 141 | return scrollAmountDefault; 142 | })(); 143 | 144 | /* Use the first scrollable element in the target's hierachy's instead of `window` to allow to scroll faster not only 145 | in the page itself but also in inner elements, like text areas or divs with overflow content. This also allows the 146 | extension to work on websites like Trello where the scrollable area isn't the nor element but a child 147 | element. */ 148 | 149 | const axis: ScrollAxis = (settings.triggerKey !== Settings.TriggerKey.ShiftLeft 150 | && (pressedKeys.has('ShiftLeft') || pressedKeys.has('ShiftRight'))) 151 | ? 'horizontal' : 'vertical'; 152 | const scrollTarget = findScrollTarget(event.target, axis) || window; // if no scrollable element is found fallback to `window` 153 | 154 | if (axis === 'horizontal') 155 | scrollTarget.scrollBy(scrollAmount, 0); 156 | else 157 | scrollTarget.scrollBy(0, scrollAmount); 158 | } 159 | 160 | function loadSettings(callback: () => void) { 161 | // load saved settings 162 | chrome.storage.sync.get(defaultSettings).then(savedSettings => { 163 | settings = savedSettings; 164 | 165 | // if current URL doesn't match any ignore glob 166 | if (!urlMatchesAnyGlobOf(window.location.href, settings.ignoredUrls)) 167 | callback(); 168 | }); 169 | 170 | // listen to changes 171 | chrome.storage.onChanged.addListener(changes => { 172 | if (changes.mode) 173 | settings.mode = changes.mode.newValue; 174 | if (changes.scrollSpeedMultiplier) 175 | settings.scrollSpeedMultiplier = changes.scrollSpeedMultiplier.newValue; 176 | if (changes.triggerKey) 177 | settings.triggerKey = changes.triggerKey.newValue; 178 | }); 179 | } 180 | 181 | function onWheelModeAlways(event: WheelEvent) { 182 | // brackets are important here because of this: https://gist.github.com/flawyte/e7e39d1d48aa1d5e7512b21bb8429b1f 183 | if (triggerKeyIsPressed) { 184 | if (settings.triggerKey === Settings.TriggerKey.ControlLeft) 185 | handleScroll(event, 'default'); // handle normal scroll by ourself since by default ControlLeft is used to zoom in/out on the page 186 | else if (settings.triggerKey === Settings.TriggerKey.ShiftLeft) 187 | handleScroll(event, 'default'); // handle normal scroll by ourself since by default ShiftLeft is used to scroll horizontally 188 | } 189 | else if (!pressedKeys.has('ControlLeft')) // pass if ControlLeft is pressed to preserve default zoom in/out behavior 190 | handleScroll(event, 'custom'); 191 | } 192 | 193 | function onWheelModeOnTriggerKeyPressed(event: WheelEvent) { 194 | if (triggerKeyIsPressed) 195 | handleScroll(event, 'custom'); 196 | } 197 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Fast Scroll", 4 | "version": "5.0", 5 | 6 | "default_locale": "en", 7 | "description": "__MSG_meta_description__", 8 | 9 | "author": "Mickaël Allonneau", 10 | "content_scripts": [ 11 | { 12 | "matches": [""], 13 | "all_frames": true, 14 | "run_at": "document_start", 15 | "js": ["index.js"] 16 | } 17 | ], 18 | "action": { 19 | "default_popup": "settings/index.html" 20 | }, 21 | "homepage_url": "https://github.com/flawyte/fast-scroll", 22 | "icons": { 23 | "16": "res/icon16.png", 24 | "48": "res/icon48.png", 25 | "128": "res/icon128.png" 26 | }, 27 | "permissions": [ 28 | "storage" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/res/LICENSE: -------------------------------------------------------------------------------- 1 | The icon has been modified to match Google Chrome extensions' guidelines. 2 | 3 | original: https://www.iconfinder.com/icons/297669/mouse_icon 4 | author: Vivien Bocquelet (http://vivienbocquelet.fr/) 5 | license: https://creativecommons.org/licenses/by/3.0/ 6 | -------------------------------------------------------------------------------- /src/res/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hormesiel/fast-scroll/c9597136614fc115bf138cd4c7af7b46dfd7e4d0/src/res/icon128.png -------------------------------------------------------------------------------- /src/res/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hormesiel/fast-scroll/c9597136614fc115bf138cd4c7af7b46dfd7e4d0/src/res/icon16.png -------------------------------------------------------------------------------- /src/res/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hormesiel/fast-scroll/c9597136614fc115bf138cd4c7af7b46dfd7e4d0/src/res/icon48.png -------------------------------------------------------------------------------- /src/settings/default-settings.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from '../types'; 2 | 3 | const defaultSettings: Settings = { 4 | mode: Settings.Mode.OnTriggerKeyPressed, 5 | scrollSpeedMultiplier: 3, 6 | triggerKey: Settings.TriggerKey.AltLeft, 7 | ignoredUrls: [], 8 | }; 9 | 10 | export default defaultSettings; 11 | -------------------------------------------------------------------------------- /src/settings/ignored-urls.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | urlMatchesAnyGlobOf 3 | } from './ignored-urls'; 4 | 5 | describe('urlMatchesAnyGlobOf()', () => { 6 | const excludedWebsites = [ 7 | // top level domain + all subdomains + all pages 8 | 'example.com', 9 | 10 | // top level domain but not subdomains 11 | 'https://example2.com', 12 | 13 | // top level domain but not subdomains except "www" 14 | 'https://(www.)?example3.com', 15 | 16 | // one subdomain only 17 | 'https://api.example4.com', 18 | 19 | // one page only 20 | 'https://example5.com/foo/bar/baz.html', 21 | 22 | // whole directory / path 23 | 'https://example6.com/foo*', 24 | // OR 'https://example6.com/foo/**', 25 | ]; 26 | 27 | it('top level domain + all subdomains + all pages', () => { 28 | expect(urlMatchesAnyGlobOf('https://example.com', excludedWebsites)).toBe(true); 29 | expect(urlMatchesAnyGlobOf('https://example.com/', excludedWebsites)).toBe(true); 30 | expect(urlMatchesAnyGlobOf('https://example.com/foo', excludedWebsites)).toBe(true); 31 | expect(urlMatchesAnyGlobOf('https://example.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 32 | 33 | expect(urlMatchesAnyGlobOf('https://api.example.com', excludedWebsites)).toBe(true); 34 | expect(urlMatchesAnyGlobOf('https://api.example.com/', excludedWebsites)).toBe(true); 35 | expect(urlMatchesAnyGlobOf('https://api.example.com/foo', excludedWebsites)).toBe(true); 36 | expect(urlMatchesAnyGlobOf('https://api.example.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 37 | 38 | expect(urlMatchesAnyGlobOf('https://docs.api.example.com', excludedWebsites)).toBe(true); 39 | expect(urlMatchesAnyGlobOf('https://docs.api.example.com/', excludedWebsites)).toBe(true); 40 | expect(urlMatchesAnyGlobOf('https://docs.api.example.com/foo', excludedWebsites)).toBe(true); 41 | expect(urlMatchesAnyGlobOf('https://docs.api.example.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 42 | 43 | expect(urlMatchesAnyGlobOf('https://www.example.com', excludedWebsites)).toBe(true); 44 | expect(urlMatchesAnyGlobOf('https://www.example.com/', excludedWebsites)).toBe(true); 45 | expect(urlMatchesAnyGlobOf('https://www.example.com/foo', excludedWebsites)).toBe(true); 46 | expect(urlMatchesAnyGlobOf('https://www.example.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 47 | }); 48 | 49 | it('top level domain but not subdomains', () => { 50 | expect(urlMatchesAnyGlobOf('https://example2.com', excludedWebsites)).toBe(true); 51 | expect(urlMatchesAnyGlobOf('https://example2.com/', excludedWebsites)).toBe(true); 52 | expect(urlMatchesAnyGlobOf('https://example2.com/foo', excludedWebsites)).toBe(true); 53 | expect(urlMatchesAnyGlobOf('https://example2.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 54 | 55 | expect(urlMatchesAnyGlobOf('https://api.example2.com', excludedWebsites)).toBe(false); 56 | expect(urlMatchesAnyGlobOf('https://api.example2.com/', excludedWebsites)).toBe(false); 57 | expect(urlMatchesAnyGlobOf('https://api.example2.com/foo', excludedWebsites)).toBe(false); 58 | expect(urlMatchesAnyGlobOf('https://api.example2.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 59 | 60 | expect(urlMatchesAnyGlobOf('https://docs.api.example2.com', excludedWebsites)).toBe(false); 61 | expect(urlMatchesAnyGlobOf('https://docs.api.example2.com/', excludedWebsites)).toBe(false); 62 | expect(urlMatchesAnyGlobOf('https://docs.api.example2.com/foo', excludedWebsites)).toBe(false); 63 | expect(urlMatchesAnyGlobOf('https://docs.api.example2.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 64 | 65 | expect(urlMatchesAnyGlobOf('https://www.example2.com', excludedWebsites)).toBe(false); 66 | expect(urlMatchesAnyGlobOf('https://www.example2.com/', excludedWebsites)).toBe(false); 67 | expect(urlMatchesAnyGlobOf('https://www.example2.com/foo', excludedWebsites)).toBe(false); 68 | expect(urlMatchesAnyGlobOf('https://www.example2.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 69 | }); 70 | 71 | it('top level domain but not subdomains except "www"', () => { 72 | expect(urlMatchesAnyGlobOf('https://example3.com', excludedWebsites)).toBe(true); 73 | expect(urlMatchesAnyGlobOf('https://example3.com/', excludedWebsites)).toBe(true); 74 | expect(urlMatchesAnyGlobOf('https://example3.com/foo', excludedWebsites)).toBe(true); 75 | expect(urlMatchesAnyGlobOf('https://example3.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 76 | 77 | expect(urlMatchesAnyGlobOf('https://api.example3.com', excludedWebsites)).toBe(false); 78 | expect(urlMatchesAnyGlobOf('https://api.example3.com/', excludedWebsites)).toBe(false); 79 | expect(urlMatchesAnyGlobOf('https://api.example3.com/foo', excludedWebsites)).toBe(false); 80 | expect(urlMatchesAnyGlobOf('https://api.example3.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 81 | 82 | expect(urlMatchesAnyGlobOf('https://docs.api.example3.com', excludedWebsites)).toBe(false); 83 | expect(urlMatchesAnyGlobOf('https://docs.api.example3.com/', excludedWebsites)).toBe(false); 84 | expect(urlMatchesAnyGlobOf('https://docs.api.example3.com/foo', excludedWebsites)).toBe(false); 85 | expect(urlMatchesAnyGlobOf('https://docs.api.example3.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 86 | 87 | expect(urlMatchesAnyGlobOf('https://www.example3.com', excludedWebsites)).toBe(true); 88 | expect(urlMatchesAnyGlobOf('https://www.example3.com/', excludedWebsites)).toBe(true); 89 | expect(urlMatchesAnyGlobOf('https://www.example3.com/foo', excludedWebsites)).toBe(true); 90 | expect(urlMatchesAnyGlobOf('https://www.example3.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 91 | }); 92 | 93 | it('one subdomain only', () => { 94 | expect(urlMatchesAnyGlobOf('https://example4.com', excludedWebsites)).toBe(false); 95 | expect(urlMatchesAnyGlobOf('https://example4.com/', excludedWebsites)).toBe(false); 96 | expect(urlMatchesAnyGlobOf('https://example4.com/foo', excludedWebsites)).toBe(false); 97 | expect(urlMatchesAnyGlobOf('https://example4.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 98 | 99 | expect(urlMatchesAnyGlobOf('https://api.example4.com', excludedWebsites)).toBe(true); 100 | expect(urlMatchesAnyGlobOf('https://api.example4.com/', excludedWebsites)).toBe(true); 101 | expect(urlMatchesAnyGlobOf('https://api.example4.com/foo', excludedWebsites)).toBe(true); 102 | expect(urlMatchesAnyGlobOf('https://api.example4.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 103 | 104 | expect(urlMatchesAnyGlobOf('https://docs.api.example4.com', excludedWebsites)).toBe(false); 105 | expect(urlMatchesAnyGlobOf('https://docs.api.example4.com/', excludedWebsites)).toBe(false); 106 | expect(urlMatchesAnyGlobOf('https://docs.api.example4.com/foo', excludedWebsites)).toBe(false); 107 | expect(urlMatchesAnyGlobOf('https://docs.api.example4.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 108 | 109 | expect(urlMatchesAnyGlobOf('https://www.example4.com', excludedWebsites)).toBe(false); 110 | expect(urlMatchesAnyGlobOf('https://www.example4.com/', excludedWebsites)).toBe(false); 111 | expect(urlMatchesAnyGlobOf('https://www.example4.com/foo', excludedWebsites)).toBe(false); 112 | expect(urlMatchesAnyGlobOf('https://www.example4.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 113 | }); 114 | 115 | it('one page only', () => { 116 | expect(urlMatchesAnyGlobOf('https://example5.com', excludedWebsites)).toBe(false); 117 | expect(urlMatchesAnyGlobOf('https://example5.com/', excludedWebsites)).toBe(false); 118 | expect(urlMatchesAnyGlobOf('https://example5.com/foo', excludedWebsites)).toBe(false); 119 | expect(urlMatchesAnyGlobOf('https://example5.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 120 | 121 | expect(urlMatchesAnyGlobOf('https://api.example5.com', excludedWebsites)).toBe(false); 122 | expect(urlMatchesAnyGlobOf('https://api.example5.com/', excludedWebsites)).toBe(false); 123 | expect(urlMatchesAnyGlobOf('https://api.example5.com/foo', excludedWebsites)).toBe(false); 124 | expect(urlMatchesAnyGlobOf('https://api.example5.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 125 | 126 | expect(urlMatchesAnyGlobOf('https://docs.api.example5.com', excludedWebsites)).toBe(false); 127 | expect(urlMatchesAnyGlobOf('https://docs.api.example5.com/', excludedWebsites)).toBe(false); 128 | expect(urlMatchesAnyGlobOf('https://docs.api.example5.com/foo', excludedWebsites)).toBe(false); 129 | expect(urlMatchesAnyGlobOf('https://docs.api.example5.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 130 | 131 | expect(urlMatchesAnyGlobOf('https://www.example5.com', excludedWebsites)).toBe(false); 132 | expect(urlMatchesAnyGlobOf('https://www.example5.com/', excludedWebsites)).toBe(false); 133 | expect(urlMatchesAnyGlobOf('https://www.example5.com/foo', excludedWebsites)).toBe(false); 134 | expect(urlMatchesAnyGlobOf('https://www.example5.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 135 | }); 136 | 137 | it('whole directory / path', () => { 138 | expect(urlMatchesAnyGlobOf('https://example6.com', excludedWebsites)).toBe(false); 139 | expect(urlMatchesAnyGlobOf('https://example6.com/', excludedWebsites)).toBe(false); 140 | expect(urlMatchesAnyGlobOf('https://example6.com/foo', excludedWebsites)).toBe(true); 141 | expect(urlMatchesAnyGlobOf('https://example6.com/foo/bar/baz.html', excludedWebsites)).toBe(true); 142 | 143 | expect(urlMatchesAnyGlobOf('https://api.example6.com', excludedWebsites)).toBe(false); 144 | expect(urlMatchesAnyGlobOf('https://api.example6.com/', excludedWebsites)).toBe(false); 145 | expect(urlMatchesAnyGlobOf('https://api.example6.com/foo', excludedWebsites)).toBe(false); 146 | expect(urlMatchesAnyGlobOf('https://api.example6.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 147 | 148 | expect(urlMatchesAnyGlobOf('https://docs.api.example6.com', excludedWebsites)).toBe(false); 149 | expect(urlMatchesAnyGlobOf('https://docs.api.example6.com/', excludedWebsites)).toBe(false); 150 | expect(urlMatchesAnyGlobOf('https://docs.api.example6.com/foo', excludedWebsites)).toBe(false); 151 | expect(urlMatchesAnyGlobOf('https://docs.api.example6.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 152 | 153 | expect(urlMatchesAnyGlobOf('https://www.example6.com', excludedWebsites)).toBe(false); 154 | expect(urlMatchesAnyGlobOf('https://www.example6.com/', excludedWebsites)).toBe(false); 155 | expect(urlMatchesAnyGlobOf('https://www.example6.com/foo', excludedWebsites)).toBe(false); 156 | expect(urlMatchesAnyGlobOf('https://www.example6.com/foo/bar/baz.html', excludedWebsites)).toBe(false); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /src/settings/ignored-urls.ts: -------------------------------------------------------------------------------- 1 | import * as micromatch from 'micromatch'; 2 | 3 | export function urlMatchesAnyGlobOf(url: string, patterns: Array): boolean { 4 | return micromatch.contains(url, patterns); 5 | } 6 | -------------------------------------------------------------------------------- /src/settings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Settings - Fast Scroll 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Textfield 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/settings/index.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Global rules 3 | // 4 | 5 | button, input, select, textarea { 6 | font-family: inherit; 7 | font-size: 95% !important; // !important required to override matter value 8 | } 9 | 10 | p { 11 | font-size: 85%; 12 | margin: 0; 13 | margin-top: 0.5rem; 14 | } 15 | 16 | // 17 | // Helpers 18 | // 19 | 20 | .flex-column { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | .matter-select-filled { 26 | outline: none; 27 | position: relative; // for label positionning 28 | 29 | // label 30 | 31 | label { 32 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); 33 | display: flex; 34 | align-items: center; 35 | font-size: 75%; 36 | height: 23px; 37 | padding: 4px 12px 0; 38 | position: absolute; 39 | transition: color 0.2s; 40 | } 41 | 42 | // select 43 | 44 | select { 45 | background: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.04); 46 | border: none; 47 | border-bottom: solid 1px rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.6); 48 | border-top: solid 26px transparent !important; 49 | border-radius: 4px 4px 0 0; 50 | outline: none; 51 | padding: 0 9px 9px; 52 | transition: 0.2s background; 53 | 54 | &:disabled { 55 | background: rgba(var(--matter-onsurface-rgb, 0, 0, 0), .24); 56 | 57 | & + label { 58 | color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), .38); 59 | } 60 | } 61 | &:focus, &:focus:hover { 62 | border-bottom: solid 2px RGBA(var(--matter-helper-theme)); 63 | padding-bottom: 8px; 64 | } 65 | &:focus + label { 66 | color: RGB(var(--matter-helper-theme)); 67 | } 68 | &:hover { 69 | background: rgba(var(--matter-onsurface-rgb, 0, 0, 0), 0.08); 70 | border-bottom-color: rgba(var(--matter-onsurface-rgb, 0, 0, 0), .6); 71 | } 72 | } 73 | } 74 | 75 | .WarningMessage { 76 | color: orange; 77 | display: none; 78 | 79 | &.is-visible { 80 | display: inherit; 81 | } 82 | } 83 | 84 | // 85 | // Page Body 86 | // 87 | 88 | #ExtensionBody { 89 | margin: 0; 90 | margin: 1rem 1rem 0 1rem; 91 | } 92 | 93 | // 94 | // Page content 95 | // 96 | 97 | #ExtensionContent { 98 | position: relative; 99 | width: 350px; 100 | max-width: 350px; 101 | } 102 | 103 | #UserSettings { 104 | margin: auto; 105 | height: 500px; 106 | padding-right: 10px; 107 | overflow-x: hidden; 108 | margin-bottom: 4rem; 109 | 110 | > * { 111 | width: 100%; 112 | 113 | & ~ * { 114 | margin-top: 1.5rem; 115 | } 116 | } 117 | } 118 | 119 | // source: https://css-scroll-shadows.now.sh/?bgColor=ffffff&shadowColor=858585&pxSize=8 120 | .scroll-gradient { 121 | background: 122 | linear-gradient(#ffffff 33%, rgba(255,255,255, 0)), 123 | linear-gradient(rgba(255,255,255, 0), #ffffff 66%) 0 100%, 124 | radial-gradient(farthest-side at 50% 0, rgba(133,133,133, 0.5), rgba(0,0,0,0)), 125 | radial-gradient(farthest-side at 50% 100%, rgba(133,133,133, 0.5), rgba(0,0,0,0)) 0 100%; 126 | background-color: #ffffff; 127 | background-repeat: no-repeat; 128 | background-attachment: local, local, scroll, scroll; 129 | background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; 130 | } 131 | 132 | // 133 | // Trigger key 134 | // 135 | 136 | select#triggerKey { 137 | @extend .matter-select-filled; 138 | } 139 | 140 | // 141 | // Mode 142 | // 143 | 144 | select#mode { 145 | @extend .matter-select-filled; 146 | } 147 | 148 | // 149 | // Ignored URLs 150 | // 151 | 152 | textarea { 153 | max-height: 350px; // Auto-size max 154 | min-height: 150px; 155 | resize: none; 156 | 157 | /* Only show placeholder text when textarea is focused and its label is floating, otherwise the label and the 158 | placeholder would overlap when unfocused */ 159 | &::placeholder { 160 | opacity: 0; 161 | } 162 | &:focus::placeholder { 163 | opacity: 1; 164 | } 165 | } 166 | 167 | #ignoredUrlsLabel { 168 | /* .matter-textarea-filled gives span 48px line-height which is undesirable for the extension popup */ 169 | line-height: inherit; 170 | } 171 | 172 | // 173 | // Save button 174 | // 175 | 176 | button { 177 | height: 2.5rem !important; // !important required to override matter value 178 | text-transform: uppercase; 179 | } 180 | 181 | .save-button-container { 182 | position: absolute; 183 | bottom: 0; 184 | width: 100%; 185 | height: 4rem; 186 | justify-content: center; 187 | } 188 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | declare const chrome; 2 | 3 | // 4 | // Imports 5 | // 6 | 7 | import defaultSettings from './default-settings'; 8 | import { Settings, Settings_Mode, Settings_TriggerKey } from './../types'; 9 | import './index.scss'; 10 | 11 | // 12 | // Variables 13 | // 14 | 15 | const i18n = chrome.i18n; 16 | 17 | let ignoredUrls: HTMLTextAreaElement; 18 | let mode: HTMLSelectElement; 19 | let modeDescription: HTMLParagraphElement; 20 | let modeWarning: HTMLParagraphElement; 21 | let saveButton: HTMLButtonElement; 22 | let scrollSpeedMultiplier: HTMLInputElement; 23 | let triggerKey: HTMLSelectElement; 24 | let triggerKeyWarning: HTMLParagraphElement; 25 | 26 | let currentSettings: Settings; // to enable / disable the 'Save' button based on form changes 27 | 28 | // 29 | // Init 30 | // 31 | 32 | document.addEventListener('DOMContentLoaded', () => { 33 | queryElements(); 34 | loadLocalizedStrings(); 35 | loadCurrentSettings(); 36 | attachFormListeners(); 37 | }); 38 | 39 | // 40 | // Helpers 41 | // 42 | 43 | function attachFormListeners() { 44 | document.addEventListener('input', () => updateSaveButtonState()); 45 | 46 | ignoredUrls.addEventListener('input', event => autoSizeTextArea(event.target)); 47 | mode.addEventListener('input', () => { 48 | updateModeDescription(Settings.Mode[mode.value]); 49 | updateModeWarning(Settings.Mode[mode.value]); 50 | }); 51 | triggerKey.addEventListener('input', () => updateTriggerKeyWarning(Settings.TriggerKey[triggerKey.value])); 52 | 53 | saveButton.addEventListener('click', save); 54 | } 55 | 56 | // Source: https://stackoverflow.com/a/25621277/1276306 57 | function autoSizeTextArea(textArea) { 58 | textArea.style.height = 'auto'; 59 | 60 | const borderBottomWidth = getComputedStyle(textArea).getPropertyValue('border-bottom-width'); 61 | const borderTopWidth = getComputedStyle(textArea).getPropertyValue('border-top-width'); 62 | textArea.style.height = `calc(${textArea.scrollHeight}px + ${borderTopWidth} + ${borderBottomWidth})`; 63 | } 64 | 65 | function formValuesHaveChanged(): boolean { 66 | const formValues = getFormValues(); 67 | 68 | const ignoredUrlsChanged = formValues.ignoredUrls.toString() !== currentSettings.ignoredUrls.toString(); 69 | const modeChanged = formValues.mode !== currentSettings.mode; 70 | const scrollSpeedMultiplierChanged = formValues.scrollSpeedMultiplier !== currentSettings.scrollSpeedMultiplier; 71 | const triggerKeyChanged = formValues.triggerKey !== currentSettings.triggerKey; 72 | 73 | return ignoredUrlsChanged || modeChanged || scrollSpeedMultiplierChanged || triggerKeyChanged; 74 | } 75 | 76 | function getFormValues(): Settings { 77 | return { 78 | ignoredUrls: ignoredUrls.value.trim().split('\n'), 79 | mode: Settings.Mode[mode.value], 80 | scrollSpeedMultiplier: Number(scrollSpeedMultiplier.value), 81 | triggerKey: Settings.TriggerKey[triggerKey.value], 82 | }; 83 | } 84 | 85 | function loadLocalizedStrings() { 86 | // scroll speeed multiplier 87 | const scrollSpeedMultiplierLabel = document.getElementById('scrollSpeedMultiplierLabel'); 88 | scrollSpeedMultiplierLabel.innerHTML = i18n.getMessage('Settings_ScrollSpeedMultiplier_label'); 89 | 90 | // mode 91 | const modeLabel = document.getElementById('modeLabel'); 92 | modeLabel.innerHTML = i18n.getMessage('Settings_Mode_label'); 93 | 94 | const modeOnTriggerKeyPressed = document.getElementById('modeOnTriggerKeyPressed'); 95 | modeOnTriggerKeyPressed.innerHTML = i18n.getMessage('Settings_Mode_OnTriggerKeyPressed_label'); 96 | const modeAlways = document.getElementById('modeAlways'); 97 | modeAlways.innerHTML = i18n.getMessage('Settings_Mode_Always_label'); 98 | 99 | modeWarning.innerHTML = i18n.getMessage('Settings_Mode_Always_warning'); 100 | 101 | // trigger key 102 | const triggerKeyLabel = document.getElementById('triggerKeyLabel'); 103 | triggerKeyLabel.innerHTML = i18n.getMessage('Settings_TriggerKey_label'); 104 | 105 | const triggerKeyAltLeftOptionLabel = document.getElementById('triggerKeyAltLeftOptionLabel'); 106 | triggerKeyAltLeftOptionLabel.innerHTML = i18n.getMessage('Settings_TriggerKey_AltLeft'); 107 | const triggerKeyAltRightOptionLabel = document.getElementById('triggerKeyAltRightOptionLabel'); 108 | triggerKeyAltRightOptionLabel.innerHTML = i18n.getMessage('Settings_TriggerKey_AltRight'); 109 | const triggerKeyControlLeftOptionLabel = document.getElementById('triggerKeyControlLeftOptionLabel'); 110 | triggerKeyControlLeftOptionLabel.innerHTML = i18n.getMessage('Settings_TriggerKey_ControlLeft'); 111 | const triggerKeyShiftLeftOptionLabel = document.getElementById('triggerKeyShiftLeftOptionLabel'); 112 | triggerKeyShiftLeftOptionLabel.innerHTML = i18n.getMessage('Settings_TriggerKey_ShiftLeft'); 113 | 114 | // ignored urls 115 | const ignoredUrlsLabel = document.getElementById('ignoredUrlsLabel'); 116 | ignoredUrlsLabel.innerHTML = i18n.getMessage('Settings_IgnoredUrls_label'); 117 | 118 | // save button 119 | saveButton.innerHTML = i18n.getMessage('Settings_SaveButton_label'); 120 | } 121 | 122 | function loadCurrentSettings() { 123 | chrome.storage.sync.get(defaultSettings).then(savedSettings => { 124 | setFormValues(savedSettings); 125 | setFormEnabled(true); 126 | 127 | currentSettings = savedSettings; 128 | // cast `scrollSpeedMultiplier` to a Number since v2 stored a String 129 | currentSettings.scrollSpeedMultiplier = Number(currentSettings.scrollSpeedMultiplier); 130 | }); 131 | } 132 | 133 | function queryElements() { 134 | mode = document.getElementById('mode') as HTMLSelectElement; 135 | modeDescription = document.getElementById('modeDescription') as HTMLParagraphElement; 136 | modeWarning = document.getElementById('modeWarning') as HTMLParagraphElement; 137 | saveButton = document.getElementById('saveButton') as HTMLButtonElement; 138 | scrollSpeedMultiplier = document.getElementById('scrollSpeedMultiplier') as HTMLInputElement; 139 | triggerKey = document.getElementById('triggerKey') as HTMLSelectElement; 140 | triggerKeyWarning = document.getElementById('triggerKeyWarning') as HTMLParagraphElement; 141 | ignoredUrls = document.getElementById('ignoredUrls') as HTMLTextAreaElement; 142 | } 143 | 144 | function save() { 145 | const formValues = getFormValues(); 146 | 147 | chrome.storage.sync.set(formValues).then(() => { 148 | saveButton.disabled = true; 149 | currentSettings = formValues; 150 | }); 151 | } 152 | 153 | function setFormEnabled(enabled: boolean) { 154 | const disabled = !enabled; 155 | 156 | ignoredUrls.disabled = disabled; 157 | mode.disabled = disabled; 158 | scrollSpeedMultiplier.disabled = disabled; 159 | triggerKey.disabled = disabled; 160 | } 161 | 162 | function setFormValues(settings: Settings) { 163 | ignoredUrls.value = settings.ignoredUrls.join('\n'); 164 | mode.value = settings.mode; 165 | scrollSpeedMultiplier.value = settings.scrollSpeedMultiplier.toString(); 166 | triggerKey.value = settings.triggerKey; 167 | 168 | updateModeDescription(settings.mode); 169 | updateModeWarning(settings.mode); 170 | updateTriggerKeyWarning(settings.triggerKey); 171 | } 172 | 173 | function updateModeDescription(mode: Settings_Mode) { 174 | modeDescription.innerHTML = i18n.getMessage(`Settings_Mode_${mode}_description`); 175 | } 176 | 177 | function updateModeWarning(mode: Settings_Mode) { 178 | modeWarning.classList.toggle('is-visible', mode === Settings.Mode.Always); 179 | } 180 | 181 | function updateSaveButtonState() { 182 | saveButton.disabled = !formValuesHaveChanged(); 183 | } 184 | 185 | function updateTriggerKeyWarning(triggerKey: Settings_TriggerKey) { 186 | const warningMessage = i18n.getMessage(`Settings_TriggerKey_${triggerKey}_warning`); 187 | triggerKeyWarning.innerHTML = warningMessage; 188 | triggerKeyWarning.classList.toggle('is-visible', warningMessage !== ''); 189 | } 190 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export enum Settings_Mode { 2 | /* set these enum values to string values to be able to use them as strings too */ 3 | Always = 'Always', 4 | OnTriggerKeyPressed = 'OnTriggerKeyPressed', 5 | } 6 | 7 | export enum Settings_TriggerKey { 8 | /* set these enum values to string values to be able to use them as strings too */ 9 | AltLeft = 'AltLeft', 10 | AltRight = 'AltRight', 11 | ShiftLeft = 'ShiftLeft', 12 | ControlLeft = 'ControlLeft', 13 | } 14 | 15 | export class Settings { 16 | static Mode = Settings_Mode; // allows client files to write `Settings.Mode` 17 | static TriggerKey = Settings_TriggerKey; // allows client files to write `Settings.TriggerKey` 18 | 19 | ignoredUrls: Array; 20 | mode: Settings_Mode; 21 | scrollSpeedMultiplier: number; 22 | triggerKey: Settings_TriggerKey; 23 | } 24 | 25 | export type ScrollAxis = 'horizontal' | 'vertical'; 26 | --------------------------------------------------------------------------------