├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── gruntfile.js ├── package-lock.json ├── package.json └── src ├── background.js ├── content.js ├── down.png ├── icon128.png ├── icon16.png ├── icon16dis.png ├── icon32.png ├── icon48.png ├── manifest.json ├── offscreen.html ├── offscreen.js ├── options.html ├── options.js └── up.png /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | My setup: 4 | 5 | - Mouse button used when scrolling: 6 | - Chrome version: 7 | - Operating system: 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | key.pem 2 | build/* 3 | /build 4 | .settings 5 | node_modules/ 6 | todo.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David Pärsson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scrollbar Anywhere 2 | 3 | An extension for Google Chrome that makes it possible to to scroll pages as if the scrollbar was right under your pointer. Just press the configured mouse button and move the mouse. 4 | 5 | If you want grab-and-drag style scrolling, just set it up in the options. 6 | 7 | The extension is based on the [Wet Banana extension](https://github.com/jedediah/wetbanana) and inspired by Scrollbar Anywhere for Firefox. 8 | 9 | ## Change Log 10 | 11 | ### 3.1 12 | 13 | - Allows the extension to be properly enabled when disabled. 14 | 15 | ### 3.0 16 | 17 | - Support future versions of Chrome with Manifest V3. 18 | 19 | ### 2.17 20 | 21 | - Prevent the configured extra keys from interfering with scrolling. 22 | 23 | ### 2.16 24 | 25 | - Stop gliding when a key is pressed or the mouse wheel is scrolled. 26 | - Fix a bug where the default mouse click actions were incorrectly prevented. 27 | 28 | ### 2.15 29 | 30 | - Fix a bug where having an empty blacklist would disable the extension for `file:///` URLs. 31 | 32 | ### 2.14 33 | 34 | - Fix scrolling on some sites in Chrome v61+, by using `document.scrollingElement` as the outmost scrolling element, instead of `document.body`, even if `document.body` seems scrollable. 35 | 36 | ### 2.13 37 | 38 | - Fix scrolling in Chrome v61+, by using `document.scrollingElement` as the outmost scrolling element, instead of `document.body`. 39 | 40 | ### 2.12 41 | 42 | - Fix scrolling in Chrome v60+, by looking at the `buttons` property of the `mousemove` event. 43 | 44 | ### 2.11 45 | 46 | - Add a blacklist, allowing users to disable scrolling on certain domains. 47 | - Fixed a bug where settings in open multi-frame tabs were not updated. 48 | 49 | ### 2.10 50 | 51 | - Fixed a bug where settings in open tabs were not updated. 52 | 53 | ### 2.9 54 | 55 | - Fixed a bug where left-clicking would leave dragging enabled on some web pages. 56 | 57 | ### 2.8 58 | 59 | - Fixed a bug where left-clicking would leave dragging enabled. 60 | 61 | ### 2.7 62 | 63 | - Fixed gliding in Chrome v49+. 64 | 65 | ### 2.6 66 | 67 | - Allow the extension to be used on file paths. 68 | 69 | ### 2.5 70 | 71 | - Changed mouse cursor to grabbing when dragging. Thanks to [koMah](https://github.com/koMah). 72 | 73 | ### 2.4 74 | 75 | - Fixed compatibility issues related to Change Colors extension 76 | 77 | ### 2.3 78 | 79 | - Compatibility fixes for future versions of Chrome. 80 | 81 | ### 2.2 82 | 83 | - Now enables clicks on gmail buttons (and similar) when scrolling with left mouse button. 84 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('jit-grunt')(grunt) 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | manifest: grunt.file.readJSON('src/manifest.json'), 7 | clean: { 8 | build: ['build/'], 9 | }, 10 | zip: { 11 | distrtibution: { 12 | src: ['src/**/*'], 13 | dest: 'build/scrollbar_anywhere-<%= manifest.version %>.zip', 14 | }, 15 | }, 16 | }) 17 | 18 | grunt.registerTask('default', ['build']) 19 | 20 | grunt.registerTask('build', ['clean', 'zip']) 21 | } 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollbar-anywhere", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "grunt": "^0.4.5", 9 | "grunt-cli": "^1.5.0", 10 | "grunt-contrib-clean": "^0.7.0", 11 | "grunt-zip": "^0.16.2", 12 | "jit-grunt": "^0.9.1", 13 | "prettier": "^3.3.0" 14 | } 15 | }, 16 | "node_modules/abbrev": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 19 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", 20 | "dev": true 21 | }, 22 | "node_modules/argparse": { 23 | "version": "0.1.16", 24 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", 25 | "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", 26 | "dev": true, 27 | "dependencies": { 28 | "underscore": "~1.7.0", 29 | "underscore.string": "~2.4.0" 30 | } 31 | }, 32 | "node_modules/argparse/node_modules/underscore.string": { 33 | "version": "2.4.0", 34 | "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", 35 | "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", 36 | "dev": true, 37 | "engines": { 38 | "node": "*" 39 | } 40 | }, 41 | "node_modules/array-each": { 42 | "version": "1.0.1", 43 | "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", 44 | "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", 45 | "dev": true, 46 | "engines": { 47 | "node": ">=0.10.0" 48 | } 49 | }, 50 | "node_modules/array-slice": { 51 | "version": "1.1.0", 52 | "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", 53 | "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", 54 | "dev": true, 55 | "engines": { 56 | "node": ">=0.10.0" 57 | } 58 | }, 59 | "node_modules/async": { 60 | "version": "0.1.22", 61 | "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", 62 | "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", 63 | "dev": true, 64 | "engines": { 65 | "node": "*" 66 | } 67 | }, 68 | "node_modules/braces": { 69 | "version": "3.0.3", 70 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 71 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 72 | "dev": true, 73 | "dependencies": { 74 | "fill-range": "^7.1.1" 75 | }, 76 | "engines": { 77 | "node": ">=8" 78 | } 79 | }, 80 | "node_modules/coffee-script": { 81 | "version": "1.3.3", 82 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", 83 | "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", 84 | "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", 85 | "dev": true, 86 | "bin": { 87 | "cake": "bin/cake", 88 | "coffee": "bin/coffee" 89 | }, 90 | "engines": { 91 | "node": ">=0.4.0" 92 | } 93 | }, 94 | "node_modules/colors": { 95 | "version": "0.6.2", 96 | "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", 97 | "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", 98 | "dev": true, 99 | "engines": { 100 | "node": ">=0.1.90" 101 | } 102 | }, 103 | "node_modules/dateformat": { 104 | "version": "1.0.2-1.2.3", 105 | "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", 106 | "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", 107 | "dev": true, 108 | "engines": { 109 | "node": "*" 110 | } 111 | }, 112 | "node_modules/detect-file": { 113 | "version": "1.0.0", 114 | "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", 115 | "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", 116 | "dev": true, 117 | "engines": { 118 | "node": ">=0.10.0" 119 | } 120 | }, 121 | "node_modules/esprima": { 122 | "version": "1.0.4", 123 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", 124 | "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", 125 | "dev": true, 126 | "bin": { 127 | "esparse": "bin/esparse.js", 128 | "esvalidate": "bin/esvalidate.js" 129 | }, 130 | "engines": { 131 | "node": ">=0.4.0" 132 | } 133 | }, 134 | "node_modules/eventemitter2": { 135 | "version": "0.4.14", 136 | "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", 137 | "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", 138 | "dev": true 139 | }, 140 | "node_modules/exit": { 141 | "version": "0.1.2", 142 | "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", 143 | "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", 144 | "dev": true, 145 | "engines": { 146 | "node": ">= 0.8.0" 147 | } 148 | }, 149 | "node_modules/expand-tilde": { 150 | "version": "2.0.2", 151 | "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", 152 | "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", 153 | "dev": true, 154 | "dependencies": { 155 | "homedir-polyfill": "^1.0.1" 156 | }, 157 | "engines": { 158 | "node": ">=0.10.0" 159 | } 160 | }, 161 | "node_modules/extend": { 162 | "version": "3.0.2", 163 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 164 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 165 | "dev": true 166 | }, 167 | "node_modules/fill-range": { 168 | "version": "7.1.1", 169 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 170 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 171 | "dev": true, 172 | "dependencies": { 173 | "to-regex-range": "^5.0.1" 174 | }, 175 | "engines": { 176 | "node": ">=8" 177 | } 178 | }, 179 | "node_modules/findup-sync": { 180 | "version": "0.1.3", 181 | "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", 182 | "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", 183 | "dev": true, 184 | "dependencies": { 185 | "glob": "~3.2.9", 186 | "lodash": "~2.4.1" 187 | }, 188 | "engines": { 189 | "node": ">= 0.6.0" 190 | } 191 | }, 192 | "node_modules/findup-sync/node_modules/glob": { 193 | "version": "3.2.11", 194 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 195 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 196 | "deprecated": "Glob versions prior to v9 are no longer supported", 197 | "dev": true, 198 | "dependencies": { 199 | "inherits": "2", 200 | "minimatch": "0.3" 201 | }, 202 | "engines": { 203 | "node": "*" 204 | } 205 | }, 206 | "node_modules/findup-sync/node_modules/lodash": { 207 | "version": "2.4.2", 208 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", 209 | "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", 210 | "dev": true, 211 | "engines": [ 212 | "node", 213 | "rhino" 214 | ] 215 | }, 216 | "node_modules/findup-sync/node_modules/minimatch": { 217 | "version": "0.3.0", 218 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 219 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 220 | "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", 221 | "dev": true, 222 | "dependencies": { 223 | "lru-cache": "2", 224 | "sigmund": "~1.0.0" 225 | }, 226 | "engines": { 227 | "node": "*" 228 | } 229 | }, 230 | "node_modules/fined": { 231 | "version": "1.2.0", 232 | "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", 233 | "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", 234 | "dev": true, 235 | "dependencies": { 236 | "expand-tilde": "^2.0.2", 237 | "is-plain-object": "^2.0.3", 238 | "object.defaults": "^1.1.0", 239 | "object.pick": "^1.2.0", 240 | "parse-filepath": "^1.0.1" 241 | }, 242 | "engines": { 243 | "node": ">= 0.10" 244 | } 245 | }, 246 | "node_modules/flagged-respawn": { 247 | "version": "1.0.1", 248 | "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", 249 | "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", 250 | "dev": true, 251 | "engines": { 252 | "node": ">= 0.10" 253 | } 254 | }, 255 | "node_modules/for-in": { 256 | "version": "1.0.2", 257 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", 258 | "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", 259 | "dev": true, 260 | "engines": { 261 | "node": ">=0.10.0" 262 | } 263 | }, 264 | "node_modules/for-own": { 265 | "version": "1.0.0", 266 | "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", 267 | "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", 268 | "dev": true, 269 | "dependencies": { 270 | "for-in": "^1.0.1" 271 | }, 272 | "engines": { 273 | "node": ">=0.10.0" 274 | } 275 | }, 276 | "node_modules/function-bind": { 277 | "version": "1.1.2", 278 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 279 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 280 | "dev": true, 281 | "funding": { 282 | "url": "https://github.com/sponsors/ljharb" 283 | } 284 | }, 285 | "node_modules/getobject": { 286 | "version": "0.1.0", 287 | "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", 288 | "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", 289 | "dev": true, 290 | "engines": { 291 | "node": ">= 0.8.0" 292 | } 293 | }, 294 | "node_modules/glob": { 295 | "version": "3.1.21", 296 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", 297 | "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", 298 | "deprecated": "Glob versions prior to v9 are no longer supported", 299 | "dev": true, 300 | "dependencies": { 301 | "graceful-fs": "~1.2.0", 302 | "inherits": "1", 303 | "minimatch": "~0.2.11" 304 | }, 305 | "engines": { 306 | "node": "*" 307 | } 308 | }, 309 | "node_modules/glob/node_modules/inherits": { 310 | "version": "1.0.2", 311 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", 312 | "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", 313 | "dev": true 314 | }, 315 | "node_modules/global-modules": { 316 | "version": "1.0.0", 317 | "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", 318 | "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", 319 | "dev": true, 320 | "dependencies": { 321 | "global-prefix": "^1.0.1", 322 | "is-windows": "^1.0.1", 323 | "resolve-dir": "^1.0.0" 324 | }, 325 | "engines": { 326 | "node": ">=0.10.0" 327 | } 328 | }, 329 | "node_modules/global-prefix": { 330 | "version": "1.0.2", 331 | "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", 332 | "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", 333 | "dev": true, 334 | "dependencies": { 335 | "expand-tilde": "^2.0.2", 336 | "homedir-polyfill": "^1.0.1", 337 | "ini": "^1.3.4", 338 | "is-windows": "^1.0.1", 339 | "which": "^1.2.14" 340 | }, 341 | "engines": { 342 | "node": ">=0.10.0" 343 | } 344 | }, 345 | "node_modules/global-prefix/node_modules/which": { 346 | "version": "1.3.1", 347 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 348 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 349 | "dev": true, 350 | "dependencies": { 351 | "isexe": "^2.0.0" 352 | }, 353 | "bin": { 354 | "which": "bin/which" 355 | } 356 | }, 357 | "node_modules/graceful-fs": { 358 | "version": "1.2.3", 359 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", 360 | "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", 361 | "deprecated": "please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js", 362 | "dev": true, 363 | "engines": { 364 | "node": ">=0.4.0" 365 | } 366 | }, 367 | "node_modules/grunt": { 368 | "version": "0.4.5", 369 | "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", 370 | "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", 371 | "dev": true, 372 | "dependencies": { 373 | "async": "~0.1.22", 374 | "coffee-script": "~1.3.3", 375 | "colors": "~0.6.2", 376 | "dateformat": "1.0.2-1.2.3", 377 | "eventemitter2": "~0.4.13", 378 | "exit": "~0.1.1", 379 | "findup-sync": "~0.1.2", 380 | "getobject": "~0.1.0", 381 | "glob": "~3.1.21", 382 | "grunt-legacy-log": "~0.1.0", 383 | "grunt-legacy-util": "~0.2.0", 384 | "hooker": "~0.2.3", 385 | "iconv-lite": "~0.2.11", 386 | "js-yaml": "~2.0.5", 387 | "lodash": "~0.9.2", 388 | "minimatch": "~0.2.12", 389 | "nopt": "~1.0.10", 390 | "rimraf": "~2.2.8", 391 | "underscore.string": "~2.2.1", 392 | "which": "~1.0.5" 393 | }, 394 | "engines": { 395 | "node": ">= 0.8.0" 396 | } 397 | }, 398 | "node_modules/grunt-cli": { 399 | "version": "1.5.0", 400 | "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.5.0.tgz", 401 | "integrity": "sha512-rILKAFoU0dzlf22SUfDtq2R1fosChXXlJM5j7wI6uoW8gwmXDXzbUvirlKZSYCdXl3LXFbR+8xyS+WFo+b6vlA==", 402 | "dev": true, 403 | "dependencies": { 404 | "grunt-known-options": "~2.0.0", 405 | "interpret": "~1.1.0", 406 | "liftup": "~3.0.1", 407 | "nopt": "~5.0.0", 408 | "v8flags": "^4.0.1" 409 | }, 410 | "bin": { 411 | "grunt": "bin/grunt" 412 | }, 413 | "engines": { 414 | "node": ">=10" 415 | } 416 | }, 417 | "node_modules/grunt-cli/node_modules/nopt": { 418 | "version": "5.0.0", 419 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", 420 | "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", 421 | "dev": true, 422 | "dependencies": { 423 | "abbrev": "1" 424 | }, 425 | "bin": { 426 | "nopt": "bin/nopt.js" 427 | }, 428 | "engines": { 429 | "node": ">=6" 430 | } 431 | }, 432 | "node_modules/grunt-contrib-clean": { 433 | "version": "0.7.0", 434 | "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.7.0.tgz", 435 | "integrity": "sha1-EvynC79SW5GLc+XMsUUPQ762Kc0=", 436 | "dev": true, 437 | "dependencies": { 438 | "rimraf": "^2.2.1" 439 | }, 440 | "engines": { 441 | "node": ">=0.10.0" 442 | }, 443 | "peerDependencies": { 444 | "grunt": ">=0.4.0" 445 | } 446 | }, 447 | "node_modules/grunt-known-options": { 448 | "version": "2.0.0", 449 | "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", 450 | "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", 451 | "dev": true, 452 | "engines": { 453 | "node": ">=0.10.0" 454 | } 455 | }, 456 | "node_modules/grunt-legacy-log": { 457 | "version": "0.1.3", 458 | "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", 459 | "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", 460 | "dev": true, 461 | "dependencies": { 462 | "colors": "~0.6.2", 463 | "grunt-legacy-log-utils": "~0.1.1", 464 | "hooker": "~0.2.3", 465 | "lodash": "~2.4.1", 466 | "underscore.string": "~2.3.3" 467 | }, 468 | "engines": { 469 | "node": ">= 0.8.0" 470 | } 471 | }, 472 | "node_modules/grunt-legacy-log-utils": { 473 | "version": "0.1.1", 474 | "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", 475 | "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", 476 | "dev": true, 477 | "dependencies": { 478 | "colors": "~0.6.2", 479 | "lodash": "~2.4.1", 480 | "underscore.string": "~2.3.3" 481 | }, 482 | "engines": { 483 | "node": ">= 0.8.0" 484 | } 485 | }, 486 | "node_modules/grunt-legacy-log-utils/node_modules/lodash": { 487 | "version": "2.4.2", 488 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", 489 | "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", 490 | "dev": true, 491 | "engines": [ 492 | "node", 493 | "rhino" 494 | ] 495 | }, 496 | "node_modules/grunt-legacy-log-utils/node_modules/underscore.string": { 497 | "version": "2.3.3", 498 | "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", 499 | "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", 500 | "dev": true, 501 | "engines": { 502 | "node": "*" 503 | } 504 | }, 505 | "node_modules/grunt-legacy-log/node_modules/lodash": { 506 | "version": "2.4.2", 507 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", 508 | "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", 509 | "dev": true, 510 | "engines": [ 511 | "node", 512 | "rhino" 513 | ] 514 | }, 515 | "node_modules/grunt-legacy-log/node_modules/underscore.string": { 516 | "version": "2.3.3", 517 | "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", 518 | "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", 519 | "dev": true, 520 | "engines": { 521 | "node": "*" 522 | } 523 | }, 524 | "node_modules/grunt-legacy-util": { 525 | "version": "0.2.0", 526 | "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", 527 | "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", 528 | "dev": true, 529 | "dependencies": { 530 | "async": "~0.1.22", 531 | "exit": "~0.1.1", 532 | "getobject": "~0.1.0", 533 | "hooker": "~0.2.3", 534 | "lodash": "~0.9.2", 535 | "underscore.string": "~2.2.1", 536 | "which": "~1.0.5" 537 | }, 538 | "engines": { 539 | "node": ">= 0.8.0" 540 | } 541 | }, 542 | "node_modules/grunt-retro": { 543 | "version": "0.6.4", 544 | "resolved": "https://registry.npmjs.org/grunt-retro/-/grunt-retro-0.6.4.tgz", 545 | "integrity": "sha1-8mqEj2pHl6X/foUOYCIMDea+jnI=", 546 | "dev": true, 547 | "engines": { 548 | "node": ">= 0.8.0" 549 | } 550 | }, 551 | "node_modules/grunt-zip": { 552 | "version": "0.16.2", 553 | "resolved": "https://registry.npmjs.org/grunt-zip/-/grunt-zip-0.16.2.tgz", 554 | "integrity": "sha1-1p9qHJ/RA8AjdusdAht3MONa15o=", 555 | "dev": true, 556 | "dependencies": { 557 | "grunt-retro": "~0.6.0", 558 | "jszip": "~2.2.2" 559 | }, 560 | "bin": { 561 | "grunt-zip": "bin/grunt-zip" 562 | }, 563 | "engines": { 564 | "node": ">= 0.8.0" 565 | } 566 | }, 567 | "node_modules/hasown": { 568 | "version": "2.0.2", 569 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 570 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 571 | "dev": true, 572 | "dependencies": { 573 | "function-bind": "^1.1.2" 574 | }, 575 | "engines": { 576 | "node": ">= 0.4" 577 | } 578 | }, 579 | "node_modules/homedir-polyfill": { 580 | "version": "1.0.3", 581 | "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", 582 | "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", 583 | "dev": true, 584 | "dependencies": { 585 | "parse-passwd": "^1.0.0" 586 | }, 587 | "engines": { 588 | "node": ">=0.10.0" 589 | } 590 | }, 591 | "node_modules/hooker": { 592 | "version": "0.2.3", 593 | "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", 594 | "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", 595 | "dev": true, 596 | "engines": { 597 | "node": "*" 598 | } 599 | }, 600 | "node_modules/iconv-lite": { 601 | "version": "0.2.11", 602 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", 603 | "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", 604 | "dev": true, 605 | "engines": { 606 | "node": ">=0.4.0" 607 | } 608 | }, 609 | "node_modules/inherits": { 610 | "version": "2.0.3", 611 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 612 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 613 | "dev": true 614 | }, 615 | "node_modules/ini": { 616 | "version": "1.3.8", 617 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 618 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 619 | "dev": true 620 | }, 621 | "node_modules/interpret": { 622 | "version": "1.1.0", 623 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", 624 | "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", 625 | "dev": true 626 | }, 627 | "node_modules/is-absolute": { 628 | "version": "1.0.0", 629 | "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", 630 | "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", 631 | "dev": true, 632 | "dependencies": { 633 | "is-relative": "^1.0.0", 634 | "is-windows": "^1.0.1" 635 | }, 636 | "engines": { 637 | "node": ">=0.10.0" 638 | } 639 | }, 640 | "node_modules/is-core-module": { 641 | "version": "2.15.1", 642 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", 643 | "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", 644 | "dev": true, 645 | "dependencies": { 646 | "hasown": "^2.0.2" 647 | }, 648 | "engines": { 649 | "node": ">= 0.4" 650 | }, 651 | "funding": { 652 | "url": "https://github.com/sponsors/ljharb" 653 | } 654 | }, 655 | "node_modules/is-extglob": { 656 | "version": "2.1.1", 657 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 658 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 659 | "dev": true, 660 | "engines": { 661 | "node": ">=0.10.0" 662 | } 663 | }, 664 | "node_modules/is-glob": { 665 | "version": "4.0.3", 666 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 667 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 668 | "dev": true, 669 | "dependencies": { 670 | "is-extglob": "^2.1.1" 671 | }, 672 | "engines": { 673 | "node": ">=0.10.0" 674 | } 675 | }, 676 | "node_modules/is-number": { 677 | "version": "7.0.0", 678 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 679 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 680 | "dev": true, 681 | "engines": { 682 | "node": ">=0.12.0" 683 | } 684 | }, 685 | "node_modules/is-plain-object": { 686 | "version": "2.0.4", 687 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 688 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 689 | "dev": true, 690 | "dependencies": { 691 | "isobject": "^3.0.1" 692 | }, 693 | "engines": { 694 | "node": ">=0.10.0" 695 | } 696 | }, 697 | "node_modules/is-relative": { 698 | "version": "1.0.0", 699 | "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", 700 | "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", 701 | "dev": true, 702 | "dependencies": { 703 | "is-unc-path": "^1.0.0" 704 | }, 705 | "engines": { 706 | "node": ">=0.10.0" 707 | } 708 | }, 709 | "node_modules/is-unc-path": { 710 | "version": "1.0.0", 711 | "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", 712 | "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", 713 | "dev": true, 714 | "dependencies": { 715 | "unc-path-regex": "^0.1.2" 716 | }, 717 | "engines": { 718 | "node": ">=0.10.0" 719 | } 720 | }, 721 | "node_modules/is-windows": { 722 | "version": "1.0.2", 723 | "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", 724 | "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", 725 | "dev": true, 726 | "engines": { 727 | "node": ">=0.10.0" 728 | } 729 | }, 730 | "node_modules/isexe": { 731 | "version": "2.0.0", 732 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 733 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 734 | "dev": true 735 | }, 736 | "node_modules/isobject": { 737 | "version": "3.0.1", 738 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 739 | "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", 740 | "dev": true, 741 | "engines": { 742 | "node": ">=0.10.0" 743 | } 744 | }, 745 | "node_modules/jit-grunt": { 746 | "version": "0.9.1", 747 | "resolved": "https://registry.npmjs.org/jit-grunt/-/jit-grunt-0.9.1.tgz", 748 | "integrity": "sha1-9mKT31f+Nz7sA9aVTRlmFmNAYZM=", 749 | "dev": true, 750 | "engines": { 751 | "node": ">=0.8.0" 752 | }, 753 | "peerDependencies": { 754 | "grunt": "~0.4.0" 755 | } 756 | }, 757 | "node_modules/js-yaml": { 758 | "version": "2.0.5", 759 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", 760 | "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", 761 | "dev": true, 762 | "dependencies": { 763 | "argparse": "~ 0.1.11", 764 | "esprima": "~ 1.0.2" 765 | }, 766 | "bin": { 767 | "js-yaml": "bin/js-yaml.js" 768 | }, 769 | "engines": { 770 | "node": ">= 0.6.0" 771 | } 772 | }, 773 | "node_modules/jszip": { 774 | "version": "2.2.2", 775 | "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.2.2.tgz", 776 | "integrity": "sha1-T/2cpr15CralnECrjeKhMps0c0E=", 777 | "dev": true, 778 | "dependencies": { 779 | "pako": "~0.2.1" 780 | } 781 | }, 782 | "node_modules/kind-of": { 783 | "version": "6.0.3", 784 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 785 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 786 | "dev": true, 787 | "engines": { 788 | "node": ">=0.10.0" 789 | } 790 | }, 791 | "node_modules/liftup": { 792 | "version": "3.0.1", 793 | "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", 794 | "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", 795 | "dev": true, 796 | "dependencies": { 797 | "extend": "^3.0.2", 798 | "findup-sync": "^4.0.0", 799 | "fined": "^1.2.0", 800 | "flagged-respawn": "^1.0.1", 801 | "is-plain-object": "^2.0.4", 802 | "object.map": "^1.0.1", 803 | "rechoir": "^0.7.0", 804 | "resolve": "^1.19.0" 805 | }, 806 | "engines": { 807 | "node": ">=10" 808 | } 809 | }, 810 | "node_modules/liftup/node_modules/findup-sync": { 811 | "version": "4.0.0", 812 | "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", 813 | "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", 814 | "dev": true, 815 | "dependencies": { 816 | "detect-file": "^1.0.0", 817 | "is-glob": "^4.0.0", 818 | "micromatch": "^4.0.2", 819 | "resolve-dir": "^1.0.1" 820 | }, 821 | "engines": { 822 | "node": ">= 8" 823 | } 824 | }, 825 | "node_modules/lodash": { 826 | "version": "0.9.2", 827 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", 828 | "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", 829 | "dev": true, 830 | "engines": [ 831 | "node", 832 | "rhino" 833 | ] 834 | }, 835 | "node_modules/lru-cache": { 836 | "version": "2.7.3", 837 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 838 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 839 | "dev": true 840 | }, 841 | "node_modules/make-iterator": { 842 | "version": "1.0.1", 843 | "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", 844 | "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", 845 | "dev": true, 846 | "dependencies": { 847 | "kind-of": "^6.0.2" 848 | }, 849 | "engines": { 850 | "node": ">=0.10.0" 851 | } 852 | }, 853 | "node_modules/map-cache": { 854 | "version": "0.2.2", 855 | "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", 856 | "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", 857 | "dev": true, 858 | "engines": { 859 | "node": ">=0.10.0" 860 | } 861 | }, 862 | "node_modules/micromatch": { 863 | "version": "4.0.8", 864 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 865 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 866 | "dev": true, 867 | "dependencies": { 868 | "braces": "^3.0.3", 869 | "picomatch": "^2.3.1" 870 | }, 871 | "engines": { 872 | "node": ">=8.6" 873 | } 874 | }, 875 | "node_modules/minimatch": { 876 | "version": "0.2.14", 877 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 878 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 879 | "deprecated": "Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue", 880 | "dev": true, 881 | "dependencies": { 882 | "lru-cache": "2", 883 | "sigmund": "~1.0.0" 884 | }, 885 | "engines": { 886 | "node": "*" 887 | } 888 | }, 889 | "node_modules/nopt": { 890 | "version": "1.0.10", 891 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 892 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", 893 | "dev": true, 894 | "dependencies": { 895 | "abbrev": "1" 896 | }, 897 | "bin": { 898 | "nopt": "bin/nopt.js" 899 | }, 900 | "engines": { 901 | "node": "*" 902 | } 903 | }, 904 | "node_modules/object.defaults": { 905 | "version": "1.1.0", 906 | "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", 907 | "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", 908 | "dev": true, 909 | "dependencies": { 910 | "array-each": "^1.0.1", 911 | "array-slice": "^1.0.0", 912 | "for-own": "^1.0.0", 913 | "isobject": "^3.0.0" 914 | }, 915 | "engines": { 916 | "node": ">=0.10.0" 917 | } 918 | }, 919 | "node_modules/object.map": { 920 | "version": "1.0.1", 921 | "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", 922 | "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", 923 | "dev": true, 924 | "dependencies": { 925 | "for-own": "^1.0.0", 926 | "make-iterator": "^1.0.0" 927 | }, 928 | "engines": { 929 | "node": ">=0.10.0" 930 | } 931 | }, 932 | "node_modules/object.pick": { 933 | "version": "1.3.0", 934 | "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", 935 | "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", 936 | "dev": true, 937 | "dependencies": { 938 | "isobject": "^3.0.1" 939 | }, 940 | "engines": { 941 | "node": ">=0.10.0" 942 | } 943 | }, 944 | "node_modules/pako": { 945 | "version": "0.2.9", 946 | "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", 947 | "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", 948 | "dev": true 949 | }, 950 | "node_modules/parse-filepath": { 951 | "version": "1.0.2", 952 | "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", 953 | "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", 954 | "dev": true, 955 | "dependencies": { 956 | "is-absolute": "^1.0.0", 957 | "map-cache": "^0.2.0", 958 | "path-root": "^0.1.1" 959 | }, 960 | "engines": { 961 | "node": ">=0.8" 962 | } 963 | }, 964 | "node_modules/parse-passwd": { 965 | "version": "1.0.0", 966 | "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", 967 | "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", 968 | "dev": true, 969 | "engines": { 970 | "node": ">=0.10.0" 971 | } 972 | }, 973 | "node_modules/path-parse": { 974 | "version": "1.0.7", 975 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 976 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 977 | "dev": true 978 | }, 979 | "node_modules/path-root": { 980 | "version": "0.1.1", 981 | "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", 982 | "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", 983 | "dev": true, 984 | "dependencies": { 985 | "path-root-regex": "^0.1.0" 986 | }, 987 | "engines": { 988 | "node": ">=0.10.0" 989 | } 990 | }, 991 | "node_modules/path-root-regex": { 992 | "version": "0.1.2", 993 | "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", 994 | "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", 995 | "dev": true, 996 | "engines": { 997 | "node": ">=0.10.0" 998 | } 999 | }, 1000 | "node_modules/picomatch": { 1001 | "version": "2.3.1", 1002 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1003 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1004 | "dev": true, 1005 | "engines": { 1006 | "node": ">=8.6" 1007 | }, 1008 | "funding": { 1009 | "url": "https://github.com/sponsors/jonschlinkert" 1010 | } 1011 | }, 1012 | "node_modules/prettier": { 1013 | "version": "3.3.0", 1014 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.0.tgz", 1015 | "integrity": "sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==", 1016 | "dev": true, 1017 | "bin": { 1018 | "prettier": "bin/prettier.cjs" 1019 | }, 1020 | "engines": { 1021 | "node": ">=14" 1022 | }, 1023 | "funding": { 1024 | "url": "https://github.com/prettier/prettier?sponsor=1" 1025 | } 1026 | }, 1027 | "node_modules/rechoir": { 1028 | "version": "0.7.1", 1029 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", 1030 | "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", 1031 | "dev": true, 1032 | "dependencies": { 1033 | "resolve": "^1.9.0" 1034 | }, 1035 | "engines": { 1036 | "node": ">= 0.10" 1037 | } 1038 | }, 1039 | "node_modules/resolve": { 1040 | "version": "1.22.8", 1041 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 1042 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 1043 | "dev": true, 1044 | "dependencies": { 1045 | "is-core-module": "^2.13.0", 1046 | "path-parse": "^1.0.7", 1047 | "supports-preserve-symlinks-flag": "^1.0.0" 1048 | }, 1049 | "bin": { 1050 | "resolve": "bin/resolve" 1051 | }, 1052 | "funding": { 1053 | "url": "https://github.com/sponsors/ljharb" 1054 | } 1055 | }, 1056 | "node_modules/resolve-dir": { 1057 | "version": "1.0.1", 1058 | "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", 1059 | "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", 1060 | "dev": true, 1061 | "dependencies": { 1062 | "expand-tilde": "^2.0.0", 1063 | "global-modules": "^1.0.0" 1064 | }, 1065 | "engines": { 1066 | "node": ">=0.10.0" 1067 | } 1068 | }, 1069 | "node_modules/rimraf": { 1070 | "version": "2.2.8", 1071 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", 1072 | "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", 1073 | "deprecated": "Rimraf versions prior to v4 are no longer supported", 1074 | "dev": true, 1075 | "bin": { 1076 | "rimraf": "bin.js" 1077 | } 1078 | }, 1079 | "node_modules/sigmund": { 1080 | "version": "1.0.1", 1081 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 1082 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 1083 | "dev": true 1084 | }, 1085 | "node_modules/supports-preserve-symlinks-flag": { 1086 | "version": "1.0.0", 1087 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1088 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1089 | "dev": true, 1090 | "engines": { 1091 | "node": ">= 0.4" 1092 | }, 1093 | "funding": { 1094 | "url": "https://github.com/sponsors/ljharb" 1095 | } 1096 | }, 1097 | "node_modules/to-regex-range": { 1098 | "version": "5.0.1", 1099 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1100 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1101 | "dev": true, 1102 | "dependencies": { 1103 | "is-number": "^7.0.0" 1104 | }, 1105 | "engines": { 1106 | "node": ">=8.0" 1107 | } 1108 | }, 1109 | "node_modules/unc-path-regex": { 1110 | "version": "0.1.2", 1111 | "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", 1112 | "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", 1113 | "dev": true, 1114 | "engines": { 1115 | "node": ">=0.10.0" 1116 | } 1117 | }, 1118 | "node_modules/underscore": { 1119 | "version": "1.7.0", 1120 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", 1121 | "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", 1122 | "dev": true 1123 | }, 1124 | "node_modules/underscore.string": { 1125 | "version": "2.2.1", 1126 | "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", 1127 | "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", 1128 | "dev": true, 1129 | "engines": { 1130 | "node": "*" 1131 | } 1132 | }, 1133 | "node_modules/v8flags": { 1134 | "version": "4.0.1", 1135 | "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", 1136 | "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", 1137 | "dev": true, 1138 | "engines": { 1139 | "node": ">= 10.13.0" 1140 | } 1141 | }, 1142 | "node_modules/which": { 1143 | "version": "1.0.9", 1144 | "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", 1145 | "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", 1146 | "dev": true, 1147 | "bin": { 1148 | "which": "bin/which" 1149 | } 1150 | } 1151 | } 1152 | } 1153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "grunt": "^0.4.5", 5 | "grunt-cli": "^1.5.0", 6 | "grunt-contrib-clean": "^0.7.0", 7 | "grunt-zip": "^0.16.2", 8 | "jit-grunt": "^0.9.1", 9 | "prettier": "^3.3.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | button: 2, 3 | key_shift: false, 4 | key_ctrl: false, 5 | key_alt: false, 6 | key_meta: false, 7 | scaling: 1, 8 | speed: 6000, 9 | friction: 10, 10 | cursor: true, 11 | notext: false, 12 | grab_and_drag: false, 13 | debug: false, 14 | blacklist: '', 15 | browser_enabled: true, 16 | } 17 | 18 | self.addEventListener('install', async (event) => { 19 | console.log('Service worker installed') 20 | }) 21 | 22 | self.addEventListener('activate', (event) => { 23 | console.log('Service worker activated') 24 | event.waitUntil(self.clients.claim()) 25 | updateExtensionIcon() 26 | }) 27 | 28 | function saveOptions(o) { 29 | console.log('Saving options:', o) 30 | chrome.storage.local.set(o) 31 | } 32 | 33 | async function getOptionsFromLocalStorage() { 34 | console.log('Converting options from localStorage...') 35 | 36 | await chrome.offscreen.createDocument({ 37 | url: 'offscreen.html', 38 | reasons: ['DOM_PARSER'], 39 | justification: 'Preserve options from previous versions', 40 | }) 41 | 42 | const oldOptions = await chrome.runtime.sendMessage({ 43 | action: 'getOptionsFromLocalStorage', 44 | }) 45 | console.log('Old options:', oldOptions) 46 | 47 | await chrome.offscreen.closeDocument() 48 | 49 | console.log('Restored options from localStorage:', oldOptions) 50 | return oldOptions 51 | } 52 | 53 | function sanitizeOptions(loadedOptions) { 54 | const sanitizedOptions = {} 55 | for (var key in defaultOptions) { 56 | if (typeof loadedOptions[key] == 'undefined') { 57 | sanitizedOptions[key] = defaultOptions[key] 58 | } else { 59 | sanitizedOptions[key] = loadedOptions[key] 60 | } 61 | } 62 | return sanitizedOptions 63 | } 64 | 65 | chrome.runtime.onInstalled.addListener(async (details) => { 66 | console.log('Extension installed:', details) 67 | let loadedOptions = await chrome.storage.local.get(null) 68 | 69 | if ( 70 | details.reason === chrome.runtime.OnInstalledReason.UPDATE && 71 | Object.keys(loadedOptions).length === 0 72 | ) { 73 | loadedOptions = await getOptionsFromLocalStorage() 74 | } 75 | 76 | const options = sanitizeOptions(loadedOptions) 77 | 78 | saveOptions(options) 79 | }) 80 | 81 | chrome.runtime.onStartup.addListener(async () => { 82 | console.log('Extension started') 83 | updateExtensionIcon() 84 | }) 85 | 86 | async function getBrowserEnabled() { 87 | const loadedOptions = await chrome.storage.local.get('browser_enabled') 88 | return loadedOptions.browser_enabled 89 | } 90 | 91 | async function updateExtensionIcon() { 92 | const browser_enabled = await getBrowserEnabled() 93 | setExtensionIcon(browser_enabled) 94 | } 95 | 96 | function setExtensionIcon(enabled) { 97 | if (enabled) { 98 | chrome.action.setIcon({ path: 'icon16.png' }) 99 | } else { 100 | chrome.action.setIcon({ path: 'icon16dis.png' }) 101 | } 102 | } 103 | 104 | chrome.action.onClicked.addListener(async (tab) => { 105 | const browserEnabled = !(await getBrowserEnabled()) 106 | saveOptions({ browser_enabled: browserEnabled }) 107 | }) 108 | 109 | chrome.storage.local.onChanged.addListener(function (changes, namespace) { 110 | for (var key in changes) { 111 | if (key === 'browser_enabled') { 112 | setExtensionIcon(changes[key].newValue) 113 | } 114 | } 115 | }) 116 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | var ScrollbarAnywhere = (function () { 2 | // === Options === 3 | 4 | var options = { debug: true, enabled: false } 5 | 6 | function parseLoadedOptions(loadedOptions) { 7 | options = loadedOptions 8 | options.cursor = isTrue(options.cursor) 9 | options.notext = isTrue(options.notext) 10 | options.grab_and_drag = isTrue(options.grab_and_drag) 11 | options.debug = isTrue(options.debug) 12 | options.enabled = isEnabled(options.blacklist) 13 | options.browser_enabled = isTrue(options.browser_enabled) 14 | debug('Loaded options: ', options) 15 | } 16 | 17 | chrome.storage.local.onChanged.addListener(function (changes) { 18 | debug('Stored options changed changed', changes) 19 | for (var key in changes) { 20 | options[key] = changes[key].newValue 21 | parseLoadedOptions(options) 22 | } 23 | }) 24 | 25 | async function loadOptions() { 26 | const loadedOptions = await chrome.storage.local.get(null) 27 | debug('Trying to load options', loadedOptions) 28 | if (Object.keys(loadedOptions).length > 0) { 29 | parseLoadedOptions(loadedOptions) 30 | } 31 | } 32 | 33 | loadOptions() 34 | 35 | function isTrue(value) { 36 | return value == true || value == 'true' 37 | } 38 | 39 | function isEnabled(blacklist) { 40 | if (!blacklist) { 41 | return true 42 | } 43 | var blacklistedHosts = blacklist.split('\n') 44 | var hostname = document.location.hostname 45 | for (var i = blacklistedHosts.length - 1; i >= 0; i--) { 46 | var blacklistedHost = blacklistedHosts[i].trim() 47 | if ( 48 | hostname === blacklistedHost || 49 | hostname.endsWith('.' + blacklistedHost) 50 | ) { 51 | return false 52 | } 53 | } 54 | return true 55 | } 56 | 57 | // === Debuggering === 58 | 59 | function debug() { 60 | if (options.debug) { 61 | console.debug.apply( 62 | console, 63 | ['SA:'].concat(Array.prototype.slice.call(arguments)), 64 | ) 65 | } 66 | } 67 | 68 | const DEBUG_INTERVAL = 100 69 | var lastDebug = 0 70 | 71 | function debugCont() { 72 | if (options.debug) { 73 | var now = Date.now() 74 | if (lastDebug + DEBUG_INTERVAL <= now) { 75 | lastDebug = now 76 | debug.apply(this, arguments) 77 | } 78 | } 79 | } 80 | 81 | // === Util === 82 | 83 | String.prototype.padLeft = function (n, c) { 84 | if (!c) c = ' ' 85 | var a = [] 86 | for (var i = this.length; i < n; i++) a.push(c) 87 | a.push(this) 88 | return a.join('') 89 | } 90 | 91 | function formatCoord(n) { 92 | return new Number(n).toFixed(3).padLeft(8) 93 | } 94 | 95 | function formatVector(v) { 96 | return '[' + formatCoord(v[0]) + ',' + formatCoord(v[1]) + ']' 97 | } 98 | 99 | // === Vector math === 100 | 101 | function vadd(a, b) { 102 | return [a[0] + b[0], a[1] + b[1]] 103 | } 104 | function vsub(a, b) { 105 | return [a[0] - b[0], a[1] - b[1]] 106 | } 107 | function vmul(s, v) { 108 | return [s * v[0], s * v[1]] 109 | } 110 | function vdiv(s, v) { 111 | return [v[0] / s, v[1] / s] 112 | } 113 | function vmag2(v) { 114 | return v[0] * v[0] + v[1] * v[1] 115 | } 116 | function vmag(v) { 117 | return Math.sqrt(v[0] * v[0] + v[1] * v[1]) 118 | } 119 | function vunit(v) { 120 | return vdiv(vmag(v), v) 121 | } 122 | 123 | // Test if the given point is directly over text 124 | var isOverText = (function () { 125 | var bonet = document.createElement('SPAN') 126 | return function (ev) { 127 | var mommy = ev.target 128 | if (mommy == null) return false 129 | for (var i = 0; i < mommy.childNodes.length; i++) { 130 | var baby = mommy.childNodes[i] 131 | if ( 132 | baby.nodeType == Node.TEXT_NODE && 133 | baby.textContent.search(/\S/) != -1 134 | ) { 135 | // debug("TEXT_NODE: '"+baby.textContent+"'") 136 | try { 137 | bonet.appendChild(mommy.replaceChild(bonet, baby)) 138 | if ( 139 | bonet.isSameNode( 140 | document.elementFromPoint(ev.clientX, ev.clientY), 141 | ) 142 | ) 143 | return true 144 | } finally { 145 | if (baby.isSameNode(bonet.firstChild)) bonet.removeChild(baby) 146 | if (bonet.isSameNode(mommy.childNodes[i])) 147 | mommy.replaceChild(baby, bonet) 148 | } 149 | } 150 | } 151 | return false 152 | } 153 | })() 154 | 155 | /* 156 | large .clientHeight: 157 | http://www.artima.com/scalazine/articles/twitter_on_scala.html 158 | 159 | small .clientHeight: 160 | http://highscalability.com/scaling-twitter-making-twitter-10000-percent-faster 161 | 162 | short document.body 163 | http://damienkatz.net/2008/04/couchdb_language_change.html 164 | */ 165 | 166 | // Test if a mouse event occurred over a scrollbar by testing if the 167 | // coordinates of the event are on the outside of a scrollable element. 168 | // The body element is treated separately since the visible size is 169 | // fetched differently depending on the doctype. 170 | function isOverScrollbar(ev) { 171 | var t = 172 | ev.target == document.documentElement 173 | ? document.scrollingElement 174 | : ev.target 175 | if (t == document.scrollingElement) { 176 | var d = document.documentElement 177 | var clientWidth 178 | var clientHeight 179 | if ( 180 | d.scrollHeight == d.clientHeight && 181 | d.scrollHeight == d.offsetHeight 182 | ) { 183 | // Guessing it's a no doctype document 184 | clientWidth = t.clientWidth 185 | clientHeight = t.clientHeight 186 | } else { 187 | clientWidth = d.clientWidth 188 | clientHeight = d.clientHeight 189 | } 190 | return ( 191 | ev.offsetX - t.scrollLeft >= clientWidth || 192 | ev.offsetY - t.scrollTop >= clientHeight 193 | ) 194 | } else if (!isScrollable(t)) { 195 | return false 196 | } else { 197 | return ( 198 | ev.offsetX - t.scrollLeft >= t.clientWidth || 199 | ev.offsetY - t.scrollTop >= t.clientHeight 200 | ) 201 | } 202 | } 203 | 204 | // Can the given element be scrolled on either axis? 205 | // That is, is the scroll size greater than the client size 206 | // and the CSS overflow set to scroll or auto? 207 | function isScrollable(e) { 208 | var o 209 | if (e.scrollWidth > e.clientWidth) { 210 | o = document.defaultView.getComputedStyle(e)['overflow-x'] 211 | if (o == 'auto' || o == 'scroll') return true 212 | } 213 | if (e.scrollHeight > e.clientHeight) { 214 | o = document.defaultView.getComputedStyle(e)['overflow-y'] 215 | if (o == 'auto' || o == 'scroll') return true 216 | } 217 | return false 218 | } 219 | 220 | // Return the first ancestor (or the element itself) that is scrollable 221 | function findInnermostScrollable(e) { 222 | if (e == document.documentElement || e == document.body) 223 | return document.scrollingElement 224 | if (e == null || e == document.scrollingElement || isScrollable(e)) { 225 | return e 226 | } else { 227 | return arguments.callee(e.parentNode) 228 | } 229 | } 230 | 231 | // Don't drag when left-clicking on these elements 232 | const LBUTTON_OVERRIDE_TAGS = [ 233 | 'A', 234 | 'INPUT', 235 | 'SELECT', 236 | 'TEXTAREA', 237 | 'BUTTON', 238 | 'LABEL', 239 | 'OBJECT', 240 | 'EMBED', 241 | ] 242 | const MBUTTON_OVERRIDE_TAGS = ['A'] 243 | const RBUTTON_OVERRIDE_TAGS = ['A', 'INPUT', 'TEXTAREA', 'OBJECT', 'EMBED'] 244 | function hasOverrideAncestor(e) { 245 | if (e == null) return false 246 | if (options.button == LBUTTON && shouldOverrideLeftButton(e)) return true 247 | if ( 248 | options.button == MBUTTON && 249 | MBUTTON_OVERRIDE_TAGS.some(function (tag) { 250 | return tag == e.tagName 251 | }) 252 | ) 253 | return true 254 | if ( 255 | options.button == RBUTTON && 256 | RBUTTON_OVERRIDE_TAGS.some(function (tag) { 257 | return tag == e.tagName 258 | }) 259 | ) 260 | return true 261 | return arguments.callee(e.parentNode) 262 | } 263 | 264 | function shouldOverrideLeftButton(e) { 265 | return ( 266 | LBUTTON_OVERRIDE_TAGS.some(function (tag) { 267 | return tag == e.tagName 268 | }) || hasRoleButtonAttribute(e) 269 | ) 270 | } 271 | 272 | function hasRoleButtonAttribute(e) { 273 | if (e.attributes && e.attributes.role) { 274 | return e.attributes.role.value === 'button' 275 | } 276 | return false 277 | } 278 | 279 | // === Clipboard Stuff === 280 | var Clipboard = (function () { 281 | var blockElement = null 282 | 283 | function isPastable(e) { 284 | return (e && e.tagName == 'INPUT') || e.tagName == 'TEXTAREA' 285 | } 286 | 287 | // Block the next paste event if a text element is active. This is a 288 | // workaround for middle-click paste not being preventable on Linux. 289 | function blockPaste() { 290 | var e = document.activeElement 291 | if (blockElement != e) { 292 | if (blockElement) unblockPaste() 293 | if (isPastable(e)) { 294 | debug('blocking paste for active text element', e) 295 | blockElement = e 296 | e.addEventListener('paste', onPaste, true) 297 | } 298 | } 299 | } 300 | 301 | function unblockPaste() { 302 | if (blockElement) { 303 | debug('unblocking paste', blockElement) 304 | blockElement.removeEventListener('paste', onPaste, true) 305 | blockElement = null 306 | } 307 | } 308 | 309 | function onPaste(ev) { 310 | var e = ev.target 311 | if (e) { 312 | if (blockElement == e) { 313 | blockElement = null 314 | ev.preventDefault() 315 | } 316 | e.removeEventListener('paste', arguments.callee, true) 317 | } 318 | } 319 | 320 | return { blockPaste: blockPaste, unblockPaste: unblockPaste } 321 | })() 322 | 323 | // === Scrollfix hack === 324 | var ScrollFix = (function () { 325 | var scrollFixElement = null 326 | var showingScrollFix = false 327 | 328 | function createScrollFix() { 329 | var element = document.createElement('div') 330 | element.setAttribute('style', 'background: transparent none !important') 331 | element.style.position = 'fixed' 332 | element.style.top = 0 333 | element.style.right = 0 334 | element.style.bottom = 0 335 | element.style.left = 0 336 | element.style.zIndex = 99999999 337 | element.style.display = 'block' 338 | //element.style.borderRight='5px solid rgba(0,0,0,0.04)'; 339 | return element 340 | } 341 | 342 | function show() { 343 | if (scrollFixElement === null) { 344 | scrollFixElement = createScrollFix() 345 | } 346 | if (!showingScrollFix) { 347 | debug('showing scrollfix') 348 | document.body.appendChild(scrollFixElement) 349 | showingScrollFix = true 350 | } 351 | } 352 | 353 | function hide() { 354 | if ( 355 | showingScrollFix && 356 | scrollFixElement !== null && 357 | scrollFixElement.parentNode !== null 358 | ) { 359 | debug('hiding scrollfix') 360 | scrollFixElement.parentNode.removeChild(scrollFixElement) 361 | showingScrollFix = false 362 | } 363 | } 364 | 365 | return { show: show, hide: hide } 366 | })() 367 | 368 | // === Fake Selection === 369 | 370 | var Selector = (function () { 371 | var startRange = null 372 | 373 | function start(x, y) { 374 | debug('Selector.start(' + x + ',' + y + ')') 375 | startRange = document.caretRangeFromPoint(x, y) 376 | var s = getSelection() 377 | s.removeAllRanges() 378 | s.addRange(startRange) 379 | } 380 | 381 | function update(x, y) { 382 | debug('Selector.update(' + x + ',' + y + ')') 383 | 384 | if (y < 0) y = 0 385 | else if (y >= innerHeight) y = innerHeight - 1 386 | if (x < 0) x = 0 387 | else if (x >= innerWidth) x = innerWidth - 1 388 | 389 | if (!startRange) start(x, y) 390 | var a = startRange 391 | var b = document.caretRangeFromPoint(x, y) 392 | 393 | if (b != null) { 394 | if (b.compareBoundaryPoints(Range.START_TO_START, a) > 0) { 395 | b.setStart(a.startContainer, a.startOffset) 396 | } else { 397 | b.setEnd(a.startContainer, a.startOffset) 398 | } 399 | 400 | var s = getSelection() 401 | s.removeAllRanges() 402 | s.addRange(b) 403 | } 404 | } 405 | 406 | function cancel() { 407 | debug('Selector.cancel()') 408 | startRange = null 409 | getSelection().removeAllRanges() 410 | } 411 | 412 | function scroll(ev) { 413 | var y = ev.clientY 414 | if (y < 0) { 415 | scrollBy(0, y) 416 | return true 417 | } else if (y >= innerHeight) { 418 | scrollBy(0, y - innerHeight) 419 | return true 420 | } 421 | return false 422 | } 423 | 424 | return { start: start, update: update, cancel: cancel, scroll: scroll } 425 | })() 426 | 427 | // === Motion === 428 | 429 | var Motion = (function () { 430 | const MIN_SPEED_SQUARED = 1 431 | const FILTER_INTERVAL = 100 432 | var position = null 433 | var velocity = [0, 0] 434 | var updateTime = null 435 | var impulses = [] 436 | 437 | // ensure velocity is within min and max values 438 | // return if/not there is motion 439 | function clamp() { 440 | var speedSquared = vmag2(velocity) 441 | if (speedSquared <= MIN_SPEED_SQUARED) { 442 | velocity = [0, 0] 443 | return false 444 | } else if (speedSquared > options.speed * options.speed) { 445 | velocity = vmul(options.speed, vunit(velocity)) 446 | } 447 | return true 448 | } 449 | 450 | // zero velocity 451 | function stop() { 452 | impulses = [] 453 | velocity = [0, 0] 454 | } 455 | 456 | // impulsively move to given position and time 457 | // return if/not there is motion 458 | function impulse(pos, time) { 459 | position = pos 460 | updateTime = time 461 | 462 | while (impulses.length > 0 && time - impulses[0].time > FILTER_INTERVAL) 463 | impulses.shift() 464 | impulses.push({ pos: pos, time: time }) 465 | 466 | if (impulses.length < 2) { 467 | velocity = [0, 0] 468 | return false 469 | } else { 470 | var a = impulses[0] 471 | var b = impulses[impulses.length - 1] 472 | 473 | velocity = vdiv((b.time - a.time) / 1000, vsub(b.pos, a.pos)) 474 | return clamp() 475 | } 476 | } 477 | 478 | // update free motion to given time 479 | // return if/not there is motion 480 | function glide(time) { 481 | impulses = [] 482 | var moving 483 | 484 | if (updateTime == null) { 485 | moving = false 486 | } else { 487 | var deltaSeconds = (time - updateTime) / 1000 488 | var frictionMultiplier = Math.max( 489 | 1 - options.friction / FILTER_INTERVAL, 490 | 0, 491 | ) 492 | frictionMultiplier = Math.pow( 493 | frictionMultiplier, 494 | deltaSeconds * FILTER_INTERVAL, 495 | ) 496 | velocity = vmul(frictionMultiplier, velocity) 497 | moving = clamp() 498 | position = vadd(position, vmul(deltaSeconds, velocity)) 499 | } 500 | updateTime = time 501 | return moving 502 | } 503 | 504 | function getPosition() { 505 | return position 506 | } 507 | 508 | return { 509 | stop: stop, 510 | impulse: impulse, 511 | glide: glide, 512 | getPosition: getPosition, 513 | } 514 | })() 515 | 516 | var Scroll = (function () { 517 | var scrolling = false 518 | var element 519 | var scrollOrigin 520 | var viewportSize 521 | var scrollSize 522 | var scrollListener 523 | var scrollMultiplier 524 | 525 | // Return the size of the element as it appears in parent's layout 526 | function getViewportSize(el) { 527 | if (el == document.scrollingElement) { 528 | return [window.innerWidth, window.innerHeight] 529 | } else { 530 | return [el.clientWidth, el.clientHeight] 531 | } 532 | } 533 | 534 | // Start dragging given element 535 | function start(el) { 536 | if (element) stop() 537 | element = el 538 | viewportSize = getViewportSize(el) 539 | scrollSize = [el.scrollWidth, el.scrollHeight] 540 | scrollOrigin = [el.scrollLeft, el.scrollTop] 541 | if (options.grab_and_drag) { 542 | scrollMultiplier = [-options.scaling, -options.scaling] 543 | } else { 544 | scrollMultiplier = [ 545 | (scrollSize[0] / viewportSize[0]) * 1.15 * options.scaling, 546 | (scrollSize[1] / viewportSize[1]) * 1.15 * options.scaling, 547 | ] 548 | } 549 | } 550 | 551 | // Move the currently dragged element relative to the starting position 552 | // and applying the the scaling setting. 553 | // Return if/not the element actually moved (i.e. if it did not hit a 554 | // boundary on both axes). 555 | function move(pos) { 556 | if (element) { 557 | var x = element.scrollLeft 558 | var y = element.scrollTop 559 | try { 560 | scrolling = true 561 | element.scrollLeft = scrollOrigin[0] + pos[0] * scrollMultiplier[0] 562 | element.scrollTop = scrollOrigin[1] + pos[1] * scrollMultiplier[1] 563 | } finally { 564 | scrolling = false 565 | } 566 | return element.scrollLeft != x || element.scrollTop != y 567 | } 568 | } 569 | 570 | // Stop dragging 571 | function stop() { 572 | if (element) { 573 | element = null 574 | viewportSize = null 575 | scrollSize = null 576 | scrollOrigin = null 577 | } 578 | } 579 | 580 | function listen(fn) { 581 | scrollListener = fn 582 | } 583 | 584 | return { start: start, move: move, stop: stop, listen: listen } 585 | })() 586 | 587 | const LBUTTON = 0, 588 | MBUTTON = 1, 589 | RBUTTON = 2 590 | const KEYS = ['shift', 'ctrl', 'alt', 'meta'] 591 | const KEYS_KEY_CODES = [16, 17, 18, 91, 92] 592 | const TIME_STEP = 10 593 | 594 | const STOP = 0, 595 | CLICK = 1, 596 | DRAG = 2, 597 | GLIDE = 3 598 | const ACTIVITIES = ['STOP', 'CLICK', 'DRAG', 'GLIDE'] 599 | for (var i = 0; i < ACTIVITIES.length; i++) window[ACTIVITIES[i]] = i 600 | 601 | var activity = STOP 602 | var blockContextMenu = false 603 | var mouseOrigin = null 604 | var dragElement = null 605 | 606 | function updateGlide() { 607 | if (activity == GLIDE) { 608 | debug('glide update') 609 | var moving = Motion.glide(performance.now()) 610 | moving = Scroll.move(vsub(Motion.getPosition(), mouseOrigin)) && moving 611 | if (moving) { 612 | setTimeout(updateGlide, TIME_STEP) 613 | } else { 614 | stopGlide() 615 | } 616 | } 617 | } 618 | 619 | function stopGlide() { 620 | debug('glide stop') 621 | activity = STOP 622 | Motion.stop() 623 | Scroll.stop() 624 | } 625 | 626 | function updateDrag(ev) { 627 | debug('drag update') 628 | var v = [ev.clientX, ev.clientY] 629 | var moving = false 630 | if (v[0] && v[1]) { 631 | moving = Motion.impulse(v, ev.timeStamp) 632 | Scroll.move(vsub(v, mouseOrigin)) 633 | } 634 | return moving 635 | } 636 | 637 | function startDrag(ev) { 638 | debug('drag start') 639 | activity = DRAG 640 | if (options.cursor) { 641 | document.body.style.cursor = '-webkit-grabbing' 642 | document.body.style.cursor = '-moz-grabbing' 643 | document.body.style.cursor = 'grabbing' 644 | } 645 | Scroll.start(dragElement) 646 | return updateDrag(ev) 647 | } 648 | 649 | function stopDrag(ev) { 650 | debug('drag stop') 651 | if (options.cursor) document.body.style.cursor = 'auto' 652 | Clipboard.unblockPaste() 653 | ScrollFix.hide() 654 | if (updateDrag(ev)) { 655 | window.setTimeout(updateGlide, TIME_STEP) 656 | activity = GLIDE 657 | } else { 658 | Scroll.stop() 659 | activity = STOP 660 | } 661 | } 662 | 663 | function onMouseDown(ev) { 664 | blockContextMenu = false 665 | 666 | if (!options.enabled) { 667 | debug('blacklisted domain, ignoring') 668 | return true 669 | } 670 | 671 | if (!options.browser_enabled) { 672 | debug('browserAction is disabled, ignoring') 673 | return true 674 | } 675 | 676 | switch (activity) { 677 | case GLIDE: 678 | stopGlide(ev) 679 | // fall through 680 | 681 | case STOP: 682 | if (!ev.target) { 683 | debug('target is null, ignoring') 684 | break 685 | } 686 | 687 | if (ev.button != options.button) { 688 | debug( 689 | 'wrong button, ignoring ev.button=' + 690 | ev.button + 691 | ' options.button=' + 692 | options.button, 693 | ) 694 | break 695 | } 696 | 697 | if ( 698 | !KEYS.every(function (key) { 699 | return (options['key_' + key] + '' == 'true') == ev[key + 'Key'] 700 | }) 701 | ) { 702 | debug('wrong modkeys, ignoring') 703 | break 704 | } 705 | 706 | if (hasOverrideAncestor(ev.target)) { 707 | debug('forbidden target element, ignoring', ev) 708 | break 709 | } 710 | 711 | if (isOverScrollbar(ev)) { 712 | debug('detected scrollbar click, ignoring', ev) 713 | break 714 | } 715 | 716 | dragElement = findInnermostScrollable(ev.target) 717 | if (!dragElement) { 718 | debug('no scrollable ancestor found, ignoring', ev) 719 | break 720 | } 721 | 722 | if (options.notext && isOverText(ev)) { 723 | debug('detected text node, ignoring') 724 | break 725 | } 726 | 727 | debug('click MouseEvent=', ev, ' dragElement=', dragElement) 728 | activity = CLICK 729 | mouseOrigin = [ev.clientX, ev.clientY] 730 | Motion.impulse(mouseOrigin, ev.timeStamp) 731 | ev.preventDefault() 732 | if (ev.button == MBUTTON && ev.target != document.activeElement) 733 | Clipboard.blockPaste() 734 | if ( 735 | ev.button == RBUTTON && 736 | (navigator.platform.match(/Mac/) || navigator.platform.match(/Linux/)) 737 | ) { 738 | blockContextMenu = true 739 | } 740 | break 741 | 742 | default: 743 | debug( 744 | 'WARNING: illegal activity for mousedown: ' + ACTIVITIES[activity], 745 | ) 746 | if (options.cursor) document.body.style.cursor = 'auto' 747 | Clipboard.unblockPaste() 748 | ScrollFix.hide() 749 | activity = STOP 750 | return onMouseDown(ev) 751 | } 752 | } 753 | 754 | function onMouseMove(ev) { 755 | switch (activity) { 756 | case STOP: 757 | break 758 | 759 | case CLICK: 760 | if (ev.buttons == buttonToMouseMoveButtons(options.button)) { 761 | if ( 762 | positionsWithinDistance(mouseOrigin, [ev.clientX, ev.clientY], 0) 763 | ) { 764 | debug('ignore, short drag') 765 | break 766 | } 767 | if (options.button == RBUTTON) blockContextMenu = true 768 | ScrollFix.show() 769 | startDrag(ev) 770 | ev.preventDefault() 771 | } 772 | break 773 | 774 | case DRAG: 775 | if (ev.buttons == buttonToMouseMoveButtons(options.button)) { 776 | updateDrag(ev) 777 | ev.preventDefault() 778 | } 779 | break 780 | 781 | case GLIDE: 782 | break 783 | 784 | default: 785 | debug('WARNING: unknown state: ' + activity) 786 | break 787 | } 788 | } 789 | 790 | function onMouseUp(ev) { 791 | switch (activity) { 792 | case STOP: 793 | break 794 | 795 | case CLICK: 796 | debug('unclick, no drag') 797 | Clipboard.unblockPaste() 798 | ScrollFix.hide() 799 | if (ev.button == 0) getSelection().removeAllRanges() 800 | if (document.activeElement) document.activeElement.blur() 801 | if (ev.target) ev.target.focus() 802 | if (ev.button == options.button) activity = STOP 803 | break 804 | 805 | case DRAG: 806 | if (ev.button == options.button) { 807 | stopDrag(ev) 808 | ev.preventDefault() 809 | } 810 | break 811 | 812 | case GLIDE: 813 | stopGlide(ev) 814 | break 815 | 816 | default: 817 | debug('WARNING: unknown state: ' + activity) 818 | break 819 | } 820 | } 821 | 822 | function onMouseOut(ev) { 823 | switch (activity) { 824 | case STOP: 825 | break 826 | 827 | case CLICK: 828 | break 829 | 830 | case DRAG: 831 | if (ev.toElement == null) stopDrag(ev) 832 | break 833 | 834 | case GLIDE: 835 | break 836 | 837 | default: 838 | debug('WARNING: unknown state: ' + activity) 839 | break 840 | } 841 | } 842 | 843 | function onContextMenu(ev) { 844 | if (blockContextMenu) { 845 | blockContextMenu = false 846 | debug('blocking context menu') 847 | ev.preventDefault() 848 | } 849 | } 850 | 851 | function positionsWithinDistance(position1, position2, maxDistance) { 852 | var distance = vmag(vsub(position1, position2)) 853 | return distance >= -maxDistance && distance <= maxDistance 854 | } 855 | 856 | function onAbortingAction(ev) { 857 | switch (activity) { 858 | case STOP: 859 | break 860 | 861 | case CLICK: 862 | break 863 | 864 | case DRAG: 865 | break 866 | 867 | case GLIDE: 868 | if (!KEYS_KEY_CODES.includes(ev.keyCode)) { 869 | debug('key pressed or wheel scrolled while gliding') 870 | stopGlide(ev) 871 | } 872 | break 873 | 874 | default: 875 | debug('WARNING: unknown state: ' + activity) 876 | break 877 | } 878 | } 879 | 880 | return { 881 | init: function () { 882 | addEventListener('mousedown', onMouseDown, true) 883 | addEventListener('mouseup', onMouseUp, true) 884 | addEventListener('mousemove', onMouseMove, true) 885 | addEventListener('mouseout', onMouseOut, true) 886 | addEventListener('contextmenu', onContextMenu, true) 887 | addEventListener('keydown', onAbortingAction, true) 888 | addEventListener('wheel', onAbortingAction, true) 889 | }, 890 | } 891 | 892 | function buttonToMouseMoveButtons(button) { 893 | if (button == LBUTTON) return 1 894 | if (button == MBUTTON) return 4 895 | if (button == RBUTTON) return 2 896 | return 0 897 | } 898 | })() 899 | 900 | ScrollbarAnywhere.init() 901 | -------------------------------------------------------------------------------- /src/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/down.png -------------------------------------------------------------------------------- /src/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/icon128.png -------------------------------------------------------------------------------- /src/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/icon16.png -------------------------------------------------------------------------------- /src/icon16dis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/icon16dis.png -------------------------------------------------------------------------------- /src/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/icon32.png -------------------------------------------------------------------------------- /src/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/icon48.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Scrollbar Anywhere", 3 | "version": "3.1", 4 | "description": "Click and drag anywhere to scroll the page.", 5 | "icons": { 6 | "128": "icon128.png", 7 | "48": "icon48.png", 8 | "32": "icon32.png", 9 | "16": "icon16.png" 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": [""], 14 | "js": ["content.js"], 15 | "all_frames": true, 16 | "run_at": "document_start" 17 | } 18 | ], 19 | "background": { 20 | "service_worker": "background.js", 21 | "type": "module" 22 | }, 23 | "action": { 24 | "default_title": "Click to toggle Scrollbar Anywhere" 25 | }, 26 | "permissions": ["storage", "offscreen"], 27 | "options_page": "options.html", 28 | "manifest_version": 3, 29 | "minimum_chrome_version": "129" 30 | } 31 | -------------------------------------------------------------------------------- /src/offscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Offscreen Worker 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/offscreen.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const number = 'number' 3 | const boolean = 'boolean' 4 | const string = 'string' 5 | 6 | const optionsToConvert = { 7 | button: number, 8 | key_shift: boolean, 9 | key_ctrl: boolean, 10 | key_alt: boolean, 11 | key_meta: boolean, 12 | scaling: number, 13 | speed: number, 14 | friction: number, 15 | cursor: boolean, 16 | notext: boolean, 17 | grab_and_drag: boolean, 18 | debug: boolean, 19 | blacklist: string, 20 | browser_enabled: boolean, 21 | } 22 | 23 | function parseOptions() { 24 | const options = {} 25 | Object.keys(optionsToConvert).forEach((key) => { 26 | const value = localStorage.getItem(key) 27 | console.log('Converting ', key, 'value:', value) 28 | if (value !== null) { 29 | if (optionsToConvert[key] === boolean) { 30 | options[key] = value === 'true' 31 | } else if (optionsToConvert[key] === number) { 32 | options[key] = Number(value) 33 | } else { 34 | options[key] = value 35 | } 36 | } 37 | }) 38 | return options 39 | } 40 | 41 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 42 | if (request.action === 'getOptionsFromLocalStorage') { 43 | const options = parseOptions() 44 | sendResponse(options) 45 | } 46 | return false 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollbar Anywhere 5 | 6 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 65 | 66 | 70 | 85 | 86 | 87 | 88 | 92 | 120 | 121 | 122 | 123 | 124 | 130 | 131 | 132 | 133 | 134 | 141 | 142 | 143 | 144 | 149 | 155 | 156 | 157 | 158 | 163 | 167 | 168 | 169 | 170 | 175 | 179 | 180 | 181 | 182 | 186 | 190 | 191 | 192 | 193 | 198 | 202 | 203 | 204 | 205 | 206 | 210 | 211 | 212 | 213 | 214 | 215 |
67 |

Button

68 | Which button will you hold down to drag? 69 |
71 | 76 | 77 | 84 |
89 |

Keys

90 | Which extra keys will you hold down? 91 |
93 | 94 | 95 | 99 | 103 | 104 | 105 | 109 | 113 | 114 |
96 | 97 | 98 | 100 | 101 | 102 |
106 | 107 | 108 | 110 | 111 | 112 |
115 |
116 | Tip: any unchecked keys will disable dragging 117 | when held down 118 |
119 |
125 |
129 |
135 |
139 |
Try unchecking this if you notice any delays
140 |
145 |

Grab and drag

146 | Grab-and-drag style scrolling will be enabled instead of the 147 | scrollbar anywhere style. 148 |
150 |
154 |
159 |

Scaling

160 | Mouse motion will be magnified by this factor. A negative number 161 | will invert scrolling. 100 is default. 162 |
164 | 165 | % of original scroll speed 166 |
171 |

Top Speed

172 | The maximum speed at which the page will glide after you release it. 173 | Enter 0 to disable gliding. 174 |
176 | 177 | pixels per second 178 |
183 |

Friction

184 | How quickly the page will come to a stop when gliding. 185 |
187 | 188 | velocities per second 189 |
194 |

Blacklist

195 | Disable this extension on the configured domains and all of their 196 | subdomains. 197 |
199 |
200 | One domain name per line. 201 |
207 | 208 | 209 |
216 | 217 |

218 | Scrollbar Anywhere by David Pärsson - 219 | Code - 220 | Support 223 |
224 | Based on 225 | Wet Banana 229 | by Jedediah Smith
230 | Inspired by 231 | Scrollbar Anywhere 235 | for Firefox by Marc Boullet 236 |

237 | 238 | Test your settings by dragging this page
239 | Down arrow 240 | 241 |
242 | Up arrow

Back to the Top
245 |
246 |
247 | Up arrow

Back to the Top
250 |
251 |
252 | Up arrow

Back to the Top
255 |
256 |
257 | Up arrow

Back to the Top
260 |
261 |
262 | Up arrow

Back to the Top
265 |
266 |
267 | Up arrow

Back to the Top
270 |
271 |
272 | Up arrow

Back to the Top
275 |
276 |
277 | Up arrow

Back to the Top
280 |
281 |
282 | Up arrow

Back to the Top
285 |
286 |
287 | Up arrow

Back to the Top
290 |
291 |
292 | Up arrow

Back to the Top
295 |
296 |
297 | Up arrow

Back to the Top
300 |
301 |
302 | 303 | 304 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | var KEYS = ['shift', 'ctrl', 'alt', 'meta'] 2 | 3 | function $(id) { 4 | return document.getElementById(id) 5 | } 6 | 7 | function error(msg) { 8 | $('message').innerHTML += '
' + msg + '
' 9 | } 10 | 11 | function clearMessage() { 12 | $('message').innerHTML = '' 13 | } 14 | 15 | function save() { 16 | var x 17 | var o = {} 18 | 19 | clearMessage() 20 | 21 | x = $('button').selectedIndex 22 | if (x < 0 || x > 2) { 23 | error('Somehow, you broke the button field') 24 | } else o.button = x 25 | 26 | x = $('scaling').value - 0 27 | if (isNaN(x)) { 28 | error('Scaling must be a number') 29 | } else o.scaling = x / 100 30 | 31 | x = $('speed').value - 0 32 | if (isNaN(x) || x < 0) { 33 | error('Top speed must be a positive number or zero') 34 | } else o.speed = x 35 | 36 | x = $('friction').value - 0 37 | if (isNaN(x) || x < 0) { 38 | error('Friction must be a positive number') 39 | } else o.friction = x 40 | 41 | for (var i = 0; i < KEYS.length; i++) { 42 | o['key_' + KEYS[i]] = $('key_' + KEYS[i]).checked 43 | } 44 | 45 | x = $('blacklist').value 46 | var hosts = x.split('\n') 47 | for (var i = hosts.length - 1; i >= 0; i--) { 48 | var host = hosts[i].trim() 49 | if (!host.match(/^[a-z0-9-.]*$/)) { 50 | error('The blacklisted domain name "' + host + '" is not valid') 51 | } 52 | } 53 | o.blacklist = x 54 | 55 | o.cursor = $('cursor').checked 56 | o.notext = $('notext').checked 57 | o.grab_and_drag = $('grab_and_drag').checked 58 | o.debug = $('debug').checked 59 | 60 | console.log('Saving options:', o) 61 | chrome.storage.local.set(o) 62 | } 63 | 64 | function load(o) { 65 | $('button').selectedIndex = o.button 66 | 67 | for (var i = 0; i < KEYS.length; i++) { 68 | $('key_' + KEYS[i]).checked = o['key_' + KEYS[i]] + '' == 'true' 69 | } 70 | 71 | $('scaling').value = o.scaling * 100 72 | $('speed').value = o.speed 73 | $('friction').value = o.friction 74 | $('blacklist').value = o.blacklist 75 | 76 | $('cursor').checked = isTrue(o.cursor) 77 | $('notext').checked = isTrue(o.notext) 78 | $('grab_and_drag').checked = isTrue(o.grab_and_drag) 79 | $('debug').checked = isTrue(o.debug) 80 | 81 | console.log('Options loaded:', o) 82 | } 83 | 84 | function isTrue(value) { 85 | return value == true || value == 'true' 86 | } 87 | 88 | var updateTimeoutId 89 | 90 | function onUpdate(ev) { 91 | if (updateTimeoutId != null) clearTimeout(updateTimeoutId) 92 | updateTimeoutId = setTimeout(save, 200) 93 | 94 | $('windows_middle_warning').style.display = 95 | $('button').selectedIndex == 1 && 96 | navigator.userAgent.search(/Windows/) != -1 && 97 | navigator.userAgent.search(/Chrome\/[012345]\./) != -1 98 | ? 'block' 99 | : 'none' 100 | } 101 | 102 | document.addEventListener( 103 | 'DOMContentLoaded', 104 | function (ev) { 105 | chrome.storage.local.get(null, function (loadedOptions) { 106 | console.log('Loaded stored options:', loadedOptions) 107 | if (Object.keys(loadedOptions).length > 0) { 108 | load(loadedOptions) 109 | } 110 | }) 111 | ;['button', 'cursor', 'notext', 'debug', 'grab_and_drag'].forEach( 112 | function (id) { 113 | $(id).addEventListener('change', onUpdate, false) 114 | }, 115 | ) 116 | 117 | KEYS.forEach(function (key) { 118 | $('key_' + key).addEventListener('change', onUpdate, false) 119 | }) 120 | ;['scaling', 'speed', 'friction', 'blacklist'].forEach(function (id) { 121 | $(id).addEventListener('change', onUpdate, true) 122 | $(id).addEventListener('keydown', onUpdate, true) 123 | $(id).addEventListener('mousedown', onUpdate, true) 124 | $(id).addEventListener('blur', onUpdate, true) 125 | }) 126 | }, 127 | true, 128 | ) 129 | 130 | document.addEventListener('unload', save, true) 131 | -------------------------------------------------------------------------------- /src/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidparsson/scrollbar-anywhere/ce59bbf3fe9401d648fb0f4f9115f9d707734ec0/src/up.png --------------------------------------------------------------------------------