├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug_report.yml │ ├── 2-feature_request.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── lint.yml ├── .gitignore ├── .gitpod.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── demo ├── images │ └── your_diary.jpg ├── index.html ├── scripts │ ├── controller.ts │ ├── index.ts │ └── monitor.ts └── styles │ └── index.styl ├── dist ├── plugins │ └── overscroll.js └── smooth-scrollbar.js ├── docs ├── README.md ├── api.md ├── assets │ ├── diagram.gif │ └── logo.svg ├── caveats.md ├── migration.md ├── overscroll.md └── plugin.md ├── package-lock.json ├── package.json ├── scripts ├── public-url.js ├── release.js ├── serve.js ├── webpack.base.js ├── webpack.dev.js ├── webpack.ghpages.js ├── webpack.prod.js └── webpack.prod.plugins.js ├── src ├── decorators │ ├── boolean.ts │ ├── debounce.ts │ ├── index.ts │ └── range.ts ├── events │ ├── index.ts │ ├── keyboard.ts │ ├── mouse.ts │ ├── resize.ts │ ├── select.ts │ ├── touch.ts │ └── wheel.ts ├── geometry │ ├── get-size.ts │ ├── index.ts │ ├── is-visible.ts │ └── update.ts ├── index.ts ├── interfaces │ ├── data-2d.ts │ ├── index.ts │ ├── plugin.ts │ ├── scrollbar.ts │ └── track.ts ├── options.ts ├── plugin.ts ├── plugins │ └── overscroll │ │ ├── bounce.ts │ │ ├── glow.ts │ │ └── index.ts ├── polyfills.ts ├── scrollbar.ts ├── scrolling │ ├── index.ts │ ├── scroll-into-view.ts │ ├── scroll-to.ts │ └── set-position.ts ├── style.ts ├── track │ ├── direction.ts │ ├── index.ts │ ├── thumb.ts │ └── track.ts └── utils │ ├── clamp.ts │ ├── debounce.ts │ ├── event-hub.ts │ ├── get-pointer-data.ts │ ├── get-position.ts │ ├── index.ts │ ├── is-one-of.ts │ ├── set-style.ts │ └── touch-record.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | # indent 12 | [*.{js, ts, css, md}] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug report" 2 | description: File a bug report 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue. 8 | 9 | Please fill out the following form as detailed as possible to help us fix the bug. 10 | - type: checkboxes 11 | attributes: 12 | label: Checks 13 | description: Before posting a report, make sure you have searched for duplicates. 14 | options: 15 | - label: "Not a duplicate." 16 | required: true 17 | - type: input 18 | validations: 19 | required: true 20 | attributes: 21 | label: Version 22 | description: The version of smooth-scrollbar. 23 | - type: textarea 24 | validations: 25 | required: true 26 | attributes: 27 | label: Description 28 | description: Describe the bug and the expected behavior. 29 | placeholder: | 30 | **Summary** 31 | ... 32 | 33 | **Expected Behavior** 34 | ... 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: Steps to Reproduce 40 | description: Tell us how to reproduce it. 41 | placeholder: | 42 | 1. ... 43 | 2. ... 44 | - type: input 45 | attributes: 46 | label: Online Demo 47 | description: Provide a URL to an online demo that reproduces the bug. (Optional but recommended) 48 | placeholder: "https://codesandbox.io/s/your-project" 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "⚡ Feature request" 2 | description: Share an idea 3 | labels: ["idea"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for sharing your idea to make smooth-scrollbar better. 9 | 10 | Please fill out the following form as detailed as possible. 11 | - type: textarea 12 | attributes: 13 | label: Motivation 14 | description: What brings you here? 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Proposal 20 | description: How can we solve the problem? 21 | validations: 22 | required: true 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "🙏 Need help with smooth-scrollbar?" 4 | url: https://github.com/idiotWu/smooth-scrollbar/discussions/new?category=q-a 5 | about: Ask a question in the discussions 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | <!--- Provide a general summary of your changes in the Title above --> 2 | 3 | ## Description 4 | <!--- Describe your changes in detail --> 5 | 6 | ## Types of changes 7 | <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 0 # security updates only 8 | allow: 9 | - dependency-type: "production" 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '16' 18 | 19 | - run: npm install 20 | - run: npm run ghpages 21 | 22 | - name: Deploy 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./ghpages 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Frontend Code 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup Node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '16' 20 | 21 | - run: npm install 22 | 23 | - name: Lint 24 | run: npm run lint 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | bower_components 29 | 30 | # DS_Store 31 | .DS_Store 32 | 33 | # build file 34 | .tmp 35 | build 36 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | command: npm run start 4 | ports: 5 | - port: 3000 6 | onOpen: open-preview 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [8.8.4](https://github.com/idiotWu/smooth-scrollbar/compare/v8.8.3...v8.8.4) (2023-06-05) 2 | 3 | ### Bug Fixes 4 | 5 | - **utils/debounce**: fix timer scheduling. @sadeghbarati [#538](https://github.com/idiotWu/smooth-scrollbar/pull/538) 6 | 7 | ## [8.8.3](https://github.com/idiotWu/smooth-scrollbar/compare/v8.8.1...v8.8.3) (2023-03-24) 8 | 9 | ### Bug Fixes 10 | 11 | - **utils/event-hub**: define `options.passive` as `enumerable` for compatibility when detecting browser support for passive events. [#520](https://github.com/idiotWu/smooth-scrollbar/pull/520) 12 | 13 | ## [8.8.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.8.0...v8.8.1) (2022-09-15) 14 | 15 | ### Bug Fixes 16 | 17 | - **event/touch**: use `devicePixelRatio` as velocity multiplier to fix the issue that scrolling is slow on iOS 16. 18 | - **event/keyboard**: add `offsetLeft` to tab key handler, fixes #421. 19 | 20 | 21 | ## [8.8.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.7.5...v8.8.0) (2022-09-15) 22 | 23 | ### Breaking Changes 24 | 25 | - Reduce lodash usage to prevent vulnerability warnings, resolves #307. @j-turner28 [#496](https://github.com/idiotWu/smooth-scrollbar/pull/496) 26 | 27 | ## [8.7.5](https://github.com/idiotWu/smooth-scrollbar/compare/v8.7.4...v8.7.5) (2022-08-02) 28 | 29 | ### Bug Fixes 30 | 31 | - **event/select**: prevent scrolling when context menu opened, resolves #489 32 | 33 | ## [8.7.4](https://github.com/idiotWu/smooth-scrollbar/compare/v8.7.3...v8.7.4) (2022-01-22) 34 | 35 | ### Bug Fixes 36 | 37 | - **event/touch**: reset touch trackers on `touchstart`, resolves #435 38 | 39 | ## [8.7.3](https://github.com/idiotWu/smooth-scrollbar/compare/v8.7.2...v8.7.3) (2022-01-10) 40 | 41 | ### Minor Changes 42 | 43 | - **geometry**: use `ResizeObserver` instead of `MutationObserver` to apply automatic re-calculates. (This is a temporary optimization and we will refactor the code in v9.) 44 | 45 | ## [8.7.2](https://github.com/idiotWu/smooth-scrollbar/compare/v8.7.1...v8.7.2) (2021-12-25) 46 | 47 | ### Minor Changes 48 | 49 | - **touch**: multiply touch moving velocity by `devicePixelRatio` on Android. 50 | 51 | ## [8.7.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.7.0...v8.7.1) (2021-12-25) 52 | 53 | ### Minor Changes 54 | 55 | - **touch**: calculate scrolling delta based on the touch moving velocity. 56 | 57 | ## [8.7.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.6.3...v8.7.0) (2021-11-01) 58 | 59 | ### Features 60 | 61 | - **event/mouse**: smoothen scrolling while dragging thumbs. 62 | 63 | ## [8.6.3](https://github.com/idiotWu/smooth-scrollbar/compare/v8.6.2...v8.6.3) (2021-07-30) 64 | 65 | ### Bug Fixes 66 | 67 | - **geometry**: add container's paddings to content's size. 68 | 69 | ## [8.6.2](https://github.com/idiotWu/smooth-scrollbar/compare/v8.6.1...v8.6.2) (2021-05-04) 70 | 71 | ### Bug Fixes 72 | 73 | - **event/select**: get new limit value when scroll function is called @longvudai [#314](https://github.com/idiotWu/smooth-scrollbar/pull/314) 74 | 75 | ## [8.6.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.6.0...v8.6.1) (2021-03-19) 76 | 77 | ### Bug Fixes 78 | 79 | - **dependencies**: upgrade lodash-es to 4.17.21 (non-vulnerable version) @huggingpixels [#306](https://github.com/idiotWu/smooth-scrollbar/pull/306) 80 | 81 | ## [8.6.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.5.3...v8.6.0) (2021-02-01) 82 | 83 | ### Breaking Changes 84 | 85 | - Upgrade core-js to v3. @milewski [#234](https://github.com/idiotWu/smooth-scrollbar/pull/234) 86 | 87 | ## [8.5.3](https://github.com/idiotWu/smooth-scrollbar/compare/v8.5.2...v8.5.3) (2020-09-17) 88 | 89 | ### Bug Fixes 90 | 91 | - **events**: ignored attempt to cancel an event with `cancelable=false`. @milkamil93 [#276](https://github.com/idiotWu/smooth-scrollbar/pull/276) 92 | 93 | ## [8.5.2](https://github.com/idiotWu/smooth-scrollbar/compare/v8.5.1...v8.5.2) (2020-03-22) 94 | 95 | ### Bug Fixes 96 | 97 | - **webpack**: make UMD build available on both browsers and Node.js. @hanjeahwan [#244](https://github.com/idiotWu/smooth-scrollbar/pull/244) 98 | 99 | 100 | ## [8.5.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.5.0...v8.5.1) (2019-12-06) 101 | 102 | ### Bug Fixes 103 | 104 | - **keyboard**: prevent keyboard navigating on `select` field. @bbtimx [#228](https://github.com/idiotWu/smooth-scrollbar/pull/228) 105 | 106 | 107 | ## [8.5.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.4.1...v8.5.0) (2019-10-20) 108 | 109 | ### Bug Fixes 110 | 111 | - **plugin.onDestroy**: fix typo. @adamcoulombe [#219](https://github.com/idiotWu/smooth-scrollbar/pull/219) 112 | 113 | ## [8.4.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.4.0...v8.4.1) (2019-09-16) 114 | 115 | ### Bug Fixes 116 | 117 | - **keyboard**: detected `contentEditable` element. @Alecyrus [#210](https://github.com/idiotWu/smooth-scrollbar/pull/210) 118 | 119 | ## [8.4.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.3.1...v8.4.0) (2019-05-13) 120 | 121 | ### Feature 122 | 123 | - Sets `tabindex` to `-1` to improve accessibility. [#160](https://github.com/idiotWu/smooth-scrollbar/pull/160) 124 | - Enables <kbd>tab</kbd> navigation. [#160](https://github.com/idiotWu/smooth-scrollbar/pull/160) 125 | 126 | ## [8.3.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.3.0...v8.3.1) (2018-08-17) 127 | 128 | ### Bug Fixes 129 | 130 | - **scrollTo**: cancel previous animation. [#168](https://github.com/idiotWu/smooth-scrollbar/issues/168) 131 | 132 | ## [8.3.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.0...v8.3.0) (2018-06-16) 133 | 134 | ### Bug Fixes 135 | 136 | - **scrollIntoView**: fix `offsetBottom` calculation. 137 | - **events**: add passive event detection. 138 | 139 | ### Feature 140 | 141 | - **options**: add `delegateTo` option. [#162](https://github.com/idiotWu/smooth-scrollbar/issues/162) 142 | 143 | ## [8.2.7](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.6...v8.2.7) (2018-03-15) 144 | 145 | ### Bug Fixes 146 | 147 | - **event/select**: remove `user-select` rules. [#151](https://github.com/idiotWu/smooth-scrollbar/issues/151) 148 | 149 | ## [8.2.6](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.5...v8.2.6) (2018-02-07) 150 | 151 | ### Bug Fixes 152 | 153 | - **scrollIntoView**: clamp delta within scrollable offset. 154 | 155 | ## [8.2.5](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.4...v8.2.5) (2017-11-28) 156 | 157 | ### Bug Fixes 158 | 159 | - **event/wheel**: fix wheel event name in IE10. [#124](https://github.com/idiotWu/smooth-scrollbar/pull/124) 160 | 161 | ## [8.2.4](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.3...v8.2.4) (2017-11-17) 162 | 163 | ### Bug Fixes 164 | 165 | - **event**: fix event propagation. 166 | 167 | ## [8.2.3](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.2...v8.2.3) (2017-11-17) 168 | 169 | ### Bug Fixes 170 | 171 | - **event**: call `event.preventDefault()` only in touchmove events. 172 | 173 | ## [8.2.2](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.1...v8.2.2) (2017-11-17) 174 | 175 | ### Minor Changes 176 | 177 | - **utils/eventHub**: remove `defaultPrevented` filter. 178 | 179 | ## [8.2.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.2.0...v8.2.1) (2017-11-16) 180 | 181 | ### Bug Fixes 182 | 183 | - **shouldPropagateMomentum**: call `shouldPropagateMomentum` after delta is transformed. [#117](https://github.com/idiotWu/smooth-scrollbar/issues/117) 184 | 185 | ## [8.2.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.0...v8.2.0) (2017-10-26) 186 | 187 | ### New Features 188 | 189 | - **plugin/overscroll**: add `onScroll` option. [doc](https://github.com/idiotWu/smooth-scrollbar/blob/develop/docs/overscroll.md#optionsonscroll) 190 | 191 | ### Minor Changes 192 | 193 | - **event/touch**: use platform based easing multiplier. 194 | 195 | ## [8.1.12](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.11...v8.1.12) (2017-10-25) 196 | 197 | ### Minor Changes 198 | 199 | - **event/touch**: use `devicePixelRatio` as easing multiplier. 200 | 201 | ## [8.1.11](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.9...v8.1.11) (2017-10-23) 202 | 203 | ### Minor Changes 204 | 205 | - **event/touch**: 1.5x easing velocity. 206 | 207 | ## [8.1.9](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.8...v8.1.9) (2017-10-23) 208 | 209 | ### Minor Changes 210 | 211 | - **Scrollbar**: add `Scrollbar.version`. 212 | - **event/touch**: improve velocity based easing algorithm. 213 | 214 | ## [8.1.8](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.7...v8.1.8) (2017-10-20) 215 | 216 | ### Bug Fixes 217 | 218 | - **track**: show track on init when `alwaysShowTracks=true`. [#108](https://github.com/idiotWu/smooth-scrollbar/issues/108) 219 | - **plugin/overscroll**: hide canvas on init. [#109](https://github.com/idiotWu/smooth-scrollbar/issues/109) 220 | 221 | ## [8.1.7](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.6...v8.1.7) (2017-10-19) 222 | 223 | ### Bug Fixes 224 | 225 | - **plugin**: `plugin.options = new Object`. 226 | 227 | ## [8.1.6](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.4...v8.1.6) (2017-10-17) 228 | 229 | ### Minor Changes 230 | 231 | - **addListener**: add type check. 232 | - **plugin**: remove lazy init. 233 | - **events**: force an update when scrollbar is detected as "unscrollable". [#106](https://github.com/idiotWu/smooth-scrollbar/issues/106) 234 | 235 | ## [8.1.4](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.3...v8.1.4) (2017-10-14) 236 | 237 | ### Bug Fixes 238 | 239 | - **options**: make properties enumerable. 240 | 241 | ## [8.1.3](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.2...v8.1.3) (2017-10-10) 242 | 243 | ### Bug Fixes 244 | 245 | - **plugin/overscroll**: preserve touch position when touch ends. 246 | 247 | ## [8.1.2](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.1...v8.1.2) (2017-10-10) 248 | 249 | ### Bug Fixes 250 | 251 | - **plugin/overscroll**: reduce amplitude when scrollbar is scrolling back. 252 | 253 | ## [8.1.1](https://github.com/idiotWu/smooth-scrollbar/compare/v8.1.0...v8.1.1) (2017-10-10) 254 | 255 | ### Bug Fixes 256 | 257 | - **init**: preserve scrolling position 258 | 259 | ## [8.1.0](https://github.com/idiotWu/smooth-scrollbar/compare/v8.0.2...v8.1.0) (2017-10-10) 260 | 261 | ## New Features 262 | 263 | - **plugin system**: add `scrollbar.updatePluginOptions` method. 264 | 265 | ## [8.0.2](https://github.com/idiotWu/smooth-scrollbar/compare/v8.0.0...v8.0.2) (2017-10-09) 266 | 267 | ### Bug Fixes 268 | 269 | - **touch**: restore damping factor when all pointers are released 270 | 271 | ## [8.0.0](https://github.com/idiotWu/smooth-scrollbar/compare/v7.4.1...v8.0.0) (2017-10-09) 272 | 273 | ### Breaking Changes 274 | 275 | - Refactored with TypeScript. 276 | - Removed overscroll effect from bundle. 277 | - [...more](https://github.com/idiotWu/smooth-scrollbar/blob/develop/docs/migration.md) 278 | 279 | ### Bug Fixes 280 | 281 | - **track**: prevent contents being selected while dragging. [#48](https://github.com/idiotWu/smooth-scrollbar/issues/48) 282 | - **IE/touch**: enable touch event capturing in IE11. [#39](https://github.com/idiotWu/smooth-scrollbar/issues/39) 283 | 284 | ### New Features 285 | 286 | - [Plugin System](https://github.com/idiotWu/smooth-scrollbar/blob/develop/docs/plugin.md). 287 | 288 | ## [7.4.1](https://github.com/idiotWu/smooth-scrollbar/compare/v7.4.0...v7.4.1) (2017-08-31) 289 | 290 | ### Bug Fixes 291 | 292 | - **scrollTo**: fix scrolling curve while `duration=0`. [#94](https://github.com/idiotWu/smooth-scrollbar/issues/94) 293 | 294 | ## [7.4.0](https://github.com/idiotWu/smooth-scrollbar/compare/v7.3.1...v7.4.0) (2017-08-24) 295 | 296 | ### Minor Changes 297 | 298 | - **init/destroy**: perserve scroll offset. [#67](https://github.com/idiotWu/smooth-scrollbar/issues/67) 299 | 300 | ## [7.3.1](https://github.com/idiotWu/smooth-scrollbar/compare/v7.3.0...v7.3.1) (2017-05-26) 301 | 302 | ### Bug Fixes 303 | 304 | - **destroy**: uses loop instead of `innerHTML = ''` to avoid empty nodes in IE. ([#77](https://github.com/idiotWu/smooth-scrollbar/pull/77)) 305 | 306 | ## [7.3.0](https://github.com/idiotWu/smooth-scrollbar/compare/v7.2.10...v7.3.0) (2017-05-22) 307 | 308 | ### Bug Fixes 309 | 310 | - **track**: Call `showTrack` whenever position changed. ([d315413](https://github.com/idiotWu/smooth-scrollbar/commit/d315413eb403563637f9eae5f4b7e93470b3341e)) 311 | 312 | ### Features 313 | 314 | - **scrollIntoView**: add `alignToTop` option. ([#75](https://github.com/idiotWu/smooth-scrollbar/pull/75)) 315 | 316 | ### Minor Changes 317 | 318 | - **scrollTo**: use computed damping factor instead of quadratic curve. [69c3a81](https://github.com/idiotWu/smooth-scrollbar/commit/69c3a813b258ded0a773056b20c4b8b2d149c11b) 319 | - **event/keyboard**: use `document.activeElement` to detect focused element. [44fc594](https://github.com/idiotWu/smooth-scrollbar/commit/44fc5948c80397e940aeb41f2a0d3282bb4799ed) 320 | 321 | ## 7.2.0 322 | 323 | - Refactor touch record. 324 | - Add `options.overscrollDamping`. 325 | 326 | ## 7.1.0 327 | 328 | - Add back individual style files to avoid crashing on server side rendering. 329 | 330 | ## 7.0.0 331 | 332 | - **Breaking change**: style files are now bundled with js files! 333 | - Support server side rendering. 334 | - Refactored to webpack based workflow. 335 | 336 | ## 6.4.0 337 | 338 | - Add `isRemoval` to destroy methods. 339 | 340 | 341 | ## 6.3.0 342 | 343 | - Add `syncCallbacks` options to perform synchronous callbacks. 344 | 345 | ## 6.2.0 346 | 347 | - Rename `options.friction` to `options.damping`. 348 | 349 | ## 6.1.0 350 | 351 | - Fix overscroll effect on non-scrollable containers. 352 | - Add `alwaysShowTracks` option 353 | 354 | ## 6.0.0 355 | 356 | - Add experimental overscroll effect. 357 | 358 | ## 5.6.0 359 | 360 | - Remove `ignoreEvents` option. 361 | - Add `unregisterEvents` and `registerEvents` to manage events. 362 | 363 | ## 5.5.0 364 | 365 | - Add `renderByPixels` option. 366 | 367 | ## 5.3.0 368 | 369 | - Add `scrollIntoView()` method. 370 | 371 | ## 5.2.0 372 | 373 | - Add `continuousScrolling` option. 374 | 375 | ## 5.1.0 376 | 377 | - Add `#clearMovement` and `#stop` method. 378 | - Allow users to temporarily disable callbacks when invoke `#setPosition` method. 379 | 380 | ## 5.0.0 381 | 382 | - **Breaking change**: rename `fricton` to `friction`. 383 | - Feature: minimal scrollbar thumb size. 384 | 385 | ## 4.2.0 386 | 387 | - Add `ignoreEvents` support. 388 | 389 | ## 4.1.0 390 | 391 | - Reduce movement at container's edge. 392 | 393 | ## 4.0.0 394 | 395 | - Movement based scrolling algorithm. 396 | - Reduce options, simple is better :) 397 | 398 | ## 3.1.0 399 | 400 | - Use quadratic curve to perform `scrollTo` method. 401 | 402 | ## 3.0.0 403 | 404 | - New easing algorithm. 405 | - Dependency free! 406 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dolphin.w.e@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Smooth Scrollbar 2 | 3 | Thanks for contributing to Smooth Scrollbar! 4 | 5 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 6 | 7 | ## Submitting an issue 8 | 9 | ### 1. Search for Duplicates 10 | 11 | Before you submit an issue, please search the issue tracker as it may already exist or even have been fixed. 12 | 13 | ### 2. Use a clear and descriptive title 14 | 15 | A good title may catch our attention and therefore, your issue may be resolved quickly. 16 | 17 | ### 3. Include as much information as possible 18 | 19 | If you are logging a bug, make sure to include the following: 20 | 21 | - The version of Smooth Scrollbar 22 | - The browser you are running on 23 | - Steps to reproduce the behavior 24 | 25 | ### 4. Provide an online demo if possible 26 | 27 | An online demo is a lifesaver! You can create an online demo on [codepan](codepan.net) with the following steps: 28 | 29 | 1. Open https://codepan.net/gist/4653b46f9e2d4c2f3585cebc1828859d 30 | 2. Modify the code as you want 31 | 3. Click **"..."** on the top right 32 | 4. Click **"Save Anonymous Gist"** (or "Save New Gist" if you've logged in) 33 | 5. Copy and paste the **URL** into issue body 34 | 35 | ### 5. Be patient 36 | 37 | We want to fix all the issues as soon as possible, but we can't make guarantees about how fast your issue can be resolved. Your understanding and patience is greatly appreciated. 38 | 39 | ## Submitting a pull request 40 | 41 | ### 1. Make your changes in a new git branch 42 | 43 | ``` 44 | $ git checkout -b my-fix-branch develop 45 | ``` 46 | 47 | ### 2. Follow the code style 48 | 49 | Run `npm run lint` before committing. 50 | 51 | ### 3. Test your code 52 | 53 | Make sure `npm test` passes. 54 | 55 | ### 4. Don't include unrelated changes 56 | 57 | DO NOT include `dist/*` in your commit. Bundle files will be updated when publishing new version. 58 | 59 | ### 5. Don't submit PRs against the `master` branch 60 | 61 | The `master` branch is considered as a snapshot of the latest release. All development should be done in the `develop` branch. 62 | 63 | ### 6. Use a clear and descriptive title for your PR 64 | 65 | ### 7. Write a convincing description 66 | 67 | - If you are fixing a bug: 68 | 69 | - Provide detailed description of the bug, or links to the related issues. 70 | 71 | - If you are adding new features: 72 | 73 | - Provide convincing reason to add this reason. 74 | 75 | 76 | ## Development setup 77 | 78 | Before starting, make sure you are using [Node.js](http://nodejs.org/) 6+. 79 | 80 | After cloning the repo, run: 81 | 82 | ```bash 83 | $ npm install 84 | ``` 85 | 86 | Then run: 87 | 88 | ```bash 89 | $ npm start 90 | ``` 91 | 92 | to start a dev server at `http://localhost:3000`. 93 | 94 | Alternatively you can use Gitpod(an Online IDE which is free for Open Source) for contributing. With a single click it will launch a workspace and automatically clone the `smooth-scrollbar` repo, install the dependencies and run `npm start`. 95 | 96 | [](https://gitpod.io/from-referrer/) 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Daofeng Wu (aka. Dolphin Wood) 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 | <div align="center"> 2 | 3 | <a href="https://idiotwu.github.io/smooth-scrollbar/"> 4 | <img src="docs/assets/logo.svg" height="150px" /> 5 | </a> 6 | 7 | # Smooth Scrollbar 8 | 9 | **Customizable, Flexible, and High Performance Scrollbars!** 10 | 11 | [![npm][npm-version-badge]](https://www.npmjs.com/package/smooth-scrollbar) 12 | [![monthly downloads][npm-downloads-badge]](https://www.npmjs.com/package/smooth-scrollbar) 13 | [![core size][size-badge]](dist/smooth-scrollbar.js) 14 | [![gzip size][gzip-size-badge]](dist/smooth-scrollbar.js) 15 | [![Build status][github-action-badge]](https://github.com/idiotWu/smooth-scrollbar/actions/workflows/deploy.yml) 16 | [![Gitpod Ready-to-Code][gitpod-badge]](https://gitpod.io/from-referrer/) 17 | 18 | </div> 19 | 20 | ## Installation 21 | 22 | > ⚠️ DO NOT use custom scrollbars unless you know what you are doing. [Read more](docs/caveats.md) 23 | 24 | > [Tell us about the features you want in the next major update](https://github.com/idiotWu/smooth-scrollbar/discussions/392). 25 | 26 | Via NPM **(recommended)**: 27 | 28 | ``` 29 | npm install smooth-scrollbar --save 30 | ``` 31 | 32 | Via Bower: 33 | 34 | ``` 35 | bower install smooth-scrollbar --save 36 | ``` 37 | 38 | ## Browser Compatibility 39 | 40 | | Browser | Version | 41 | | :------ | :-----: | 42 | | IE | 10+ | 43 | | Chrome | 22+ | 44 | | Firefox | 16+ | 45 | | Safari | 8+ | 46 | | Android Browser | 4+ | 47 | | Chrome for Android | 32+ | 48 | | iOS Safari | 7+ | 49 | 50 | ## Demo 51 | 52 | https://idiotwu.github.io/smooth-scrollbar/ 53 | 54 | ## Usage 55 | 56 | Since this package has a [pkg.module](https://github.com/rollup/rollup/wiki/pkg.module) field, it's highly recommended to import it as an ES6 module with some bundlers like [webpack](https://webpack.js.org/) or [rollup](https://rollupjs.org/): 57 | 58 | ```js 59 | import Scrollbar from 'smooth-scrollbar'; 60 | 61 | Scrollbar.init(document.querySelector('#my-scrollbar')); 62 | ``` 63 | 64 | If you are not using any bundlers, you can just load the UMD bundle: 65 | 66 | ```html 67 | <script src="dist/smooth-scrollbar.js"></script> 68 | 69 | <script> 70 | var Scrollbar = window.Scrollbar; 71 | 72 | Scrollbar.init(document.querySelector('#my-scrollbar')); 73 | </script> 74 | ``` 75 | 76 | ## Documentation 77 | 78 | | [latest](docs) | [7.x](https://github.com/idiotWu/smooth-scrollbar/tree/7.x) | 79 | |----|----| 80 | 81 | ## FAQ 82 | 83 | - How to **deal with `position: fixed` elements**? [#362](https://github.com/idiotWu/smooth-scrollbar/discussions/362#discussioncomment-854090) 84 | - How to **temporarily stop scrolling**? [#361](https://github.com/idiotWu/smooth-scrollbar/discussions/361#discussioncomment-854079) 85 | - How to **enable hash/anchor scrolling**? [#360](https://github.com/idiotWu/smooth-scrollbar/discussions/360#discussioncomment-854071) 86 | - How to **direct all scrolling to a particular direction**? [#359](https://github.com/idiotWu/smooth-scrollbar/discussions/359#discussioncomment-854052) 87 | - How to **disable scrolling in a particular direction**? [#357](https://github.com/idiotWu/smooth-scrollbar/discussions/357#discussioncomment-854036) 88 | - [more...](https://github.com/idiotWu/smooth-scrollbar/discussions/categories/faq) 89 | 90 | ## Who's Using It 91 | 92 | - [Awwwards Conference](https://conference.awwwards.com/): An Event for UX / UI Designers and Web Developers. 93 | - [Listeners Playlist](http://lp.anzi.kr/): A cool music player designed by Jiyong Ahn sharing musics from the facebook group 'Listeners Playlist'. 94 | - [Matter](https://matterapp.com/): A new and better way to grow your professional skills. 95 | - [Parsons Branding](https://www.parsonsbranding.com/): Brand strategy and design studio based in Cape Town. 96 | - [zer0bin](https://zer0b.in): Just a place to paste 97 | - Feel free to add yours here 🤗. 98 | 99 | ## Credits 100 | 101 | - [Logo](https://github.com/idiotWu/smooth-scrollbar/discussions/461) by Kainoa Kanter ([@ThatOneCalculator](https://github.com/ThatOneCalculator)) 102 | 103 | ## License 104 | 105 | [MIT](LICENSE) 106 | 107 | [npm-version-badge]: https://img.shields.io/npm/v/smooth-scrollbar.svg?style=for-the-badge 108 | [npm-downloads-badge]: https://img.shields.io/npm/dm/smooth-scrollbar.svg?style=for-the-badge 109 | [github-action-badge]: https://img.shields.io/github/actions/workflow/status/idiotWu/smooth-scrollbar/deploy.yml?branch=master&style=for-the-badge 110 | [size-badge]: http://img.badgesize.io/idiotWu/smooth-scrollbar/master/dist/smooth-scrollbar.js?label=core%20size&style=for-the-badge 111 | [gzip-size-badge]: http://img.badgesize.io/idiotWu/smooth-scrollbar/master/dist/smooth-scrollbar.js?label=gzip%20size&compression=gzip&style=for-the-badge 112 | [gitpod-badge]: https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?style=for-the-badge 113 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smooth-scrollbar", 3 | "version": "8.8.4", 4 | "authors": [ 5 | "Dolphin Wood <dolphin.w.e@gmail.com>" 6 | ], 7 | "description": "Customize scrollbar in modern browsers with smooth scrolling experience.", 8 | "main": "dist/smooth-scrollbar.js", 9 | "moduleType": [ 10 | "globals", 11 | "amd", 12 | "node" 13 | ], 14 | "keywords": [ 15 | "scrollbar", 16 | "customize", 17 | "acceleration", 18 | "performance" 19 | ], 20 | "license": "MIT", 21 | "homepage": "https://github.com/idiotWu/smooth-scrollbar", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "__demo__", 27 | "build", 28 | "test" 29 | ] 30 | } -------------------------------------------------------------------------------- /demo/images/your_diary.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiotWu/smooth-scrollbar/66c67b85d35c14486bd25a73806c0ab13ffeb267/demo/images/your_diary.jpg -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8"> 6 | <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> 7 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 8 | <meta property="og:title" content="Smooth Scrollbar"> 9 | <meta name="description" content="Customize scrollbar in modern browsers with smooth scrolling experience."> 10 | <meta property="og:description" content="Customize scrollbar in modern browsers with smooth scrolling experience."> 11 | <link rel="canonical" href="https://github.com/idiotWu/smooth-scrollbar"> 12 | <meta property="og:url" content="https://github.com/idiotWu/smooth-scrollbar"> 13 | <title>Smooth Scrollbar</title> 14 | </head> 15 | 16 | <body> 17 | <aside id="controller"></aside> 18 | <aside id="monitor"> 19 | <h4>Scrollbar Monitor</h4> 20 | <canvas id="chart"></canvas> 21 | <footer id="track"> 22 | <div id="thumb"></div> 23 | </footer> 24 | </aside> 25 | <main id="main-scrollbar" data-scrollbar> 26 | <header> 27 | <a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar" class="github-corner"> 28 | <svg width="80" height="80" viewBox="0 0 250 250" style="fill:none; color:#fff; position: absolute; top: 0; border: 0; right: 0;"> 29 | <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> 30 | <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path> 31 | <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path> 32 | </svg> 33 | </a> 34 | <nav class="badges"> 35 | <a target="_blank" href="https://twitter.com/share" class="twitter-share-button" data-url="https://github.com/idiotWu/smooth-scrollbar" data-text="Customize scrollbar in modern browsers with smooth scrolling experience." data-via="Dolphin_Wood"></a> 36 | <a target="_blank" href="https://www.npmjs.com/package/smooth-scrollbar"><img src="https://img.shields.io/npm/dt/smooth-scrollbar.svg?style=flat" alt="downloads"></a> 37 | <a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar"><img src="https://img.shields.io/github/stars/idiotWu/smooth-scrollbar.svg?style=social&label=Stars" alt="GitHub Stars"></a> 38 | <a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar"><img src="https://img.shields.io/github/forks/idiotWu/smooth-scrollbar.svg?style=social&label=Forks" alt="GitHub Forks"></a> 39 | </nav> 40 | <h1>Smooth Scrollbar</h1> 41 | <h2>Customizable, Pluginable, and High Performance Scrollbars!</h2> 42 | <a class="repo" href="https://github.com/idiotWu/smooth-scrollbar/blob/HEAD/docs" target="_blank">Documentation</a> 43 | <footer class="version-note">Version: <span id="version"></span></footer> 44 | </header> 45 | <article id="content"> 46 | <h2>What is smooth-scrollbar?</h2> 47 | <p>Smooth Scrollbar is a JavaScript Plugin that allows you customizing high perfermance scrollbars cross browsers. It is using <code>translate3d</code> to perform a momentum based scrolling (aka inertial scrolling) on modern browsers. With the flexible <a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar/blob/HEAD/docs/plugin.md">plugin system</a>, we can easily redesign the scrollbar as we want. This is the scrollbar plugin that you've ever dreamed of!</p> 48 | <h2>Installation</h2> 49 | <p>Via NPM <strong>(recommended)</strong>:</p> 50 | <div><pre><code class="language-shell">npm install smooth-scrollbar --save</code></pre></div> 51 | <p>Via Bower:</p> 52 | <div><pre><code class="language-shell">bower install smooth-scrollbar --save</code></pre></div> 53 | <h2>Browser Compatibility</h2> 54 | <table> 55 | <thead> 56 | <tr> 57 | <th style="text-align: left">Browser</th> 58 | <th style="text-align: center">Version</th> 59 | </tr> 60 | </thead> 61 | <tbody> 62 | <tr> 63 | <td style="text-align: left">IE</td> 64 | <td style="text-align: center">10+</td> 65 | </tr> 66 | <tr> 67 | <td style="text-align: left">Chrome</td> 68 | <td style="text-align: center">22+</td> 69 | </tr> 70 | <tr> 71 | <td style="text-align: left">Firefox</td> 72 | <td style="text-align: center">16+</td> 73 | </tr> 74 | <tr> 75 | <td style="text-align: left">Safari</td> 76 | <td style="text-align: center">8+</td> 77 | </tr> 78 | <tr> 79 | <td style="text-align: left">Android Browser</td> 80 | <td style="text-align: center">4+</td> 81 | </tr> 82 | <tr> 83 | <td style="text-align: left">Chrome for Android</td> 84 | <td style="text-align: center">32+</td> 85 | </tr> 86 | <tr> 87 | <td style="text-align: left">iOS Safari</td> 88 | <td style="text-align: center">7+</td> 89 | </tr> 90 | </tbody> 91 | </table> 92 | <h2>Usage</h2> 93 | <p>Since this package has a <a target="_blank" href="https://github.com/rollup/rollup/wiki/pkg.module">pkg.module</a> field, it's highly recommended to import it as an ES6 module with some bundlers like <a target="_blank" href="https://webpack.js.org/">webpack</a> or <a target="_blank" href="https://rollupjs.org/">rollup</a>:</p> 94 | <div><pre><code class="language-javascript">import Scrollbar from 'smooth-scrollbar'; 95 | 96 | Scrollbar.init(document.querySelector('#my-scrollbar'), options);</code></pre></div> 97 | <p>If you are not using any bundlers, you can just load the UMD bundle:</p> 98 | <div><pre><code class="language-markup"><script src="dist/smooth-scrollbar.js"></script> 99 | 100 | <script> 101 | var Scrollbar = window.Scrollbar; 102 | 103 | Scrollbar.init(document.querySelector('#my-scrollbar'), options); 104 | </script></code></pre></div> 105 | <h2>Common mistakes</h2> 106 | <h4>Initialize a scrollbar without a limited width or height</h4> 107 | <p>Likes the native scrollbars, a scrollable area means <strong>the content insides it is larger than the container itself</strong>, for example, a <code>500*500</code> area with a content which size is <code>1000*1000</code>:</p> 108 | <div><pre><code class="language-none"> container 109 | / 110 | +--------+ 111 | ##################### 112 | # | | # 113 | # | | # 114 | # +--------+ # -- content 115 | # # 116 | # # 117 | #####################</code></pre></div> 118 | <p>Therefore, it's necessary to set the <code>width</code> or <code>height</code> for the container element:</p> 119 | <div><pre><code class="language-css">#my-scrollbar { 120 | width: 500px; 121 | height: 500px; 122 | overflow: auto; 123 | }</code></pre></div> 124 | <p>If the container element is natively scrollable before initializing the Scrollbar, it means you are on the correct way.</p> 125 | <h2>Available Options for Scrollbar</h2> 126 | <table id="options"> 127 | <thead> 128 | <tr> 129 | <th style="text-align: center">parameter</th> 130 | <th style="text-align: center">type</th> 131 | <th style="text-align: center">default</th> 132 | <th style="text-align: left">description</th> 133 | </tr> 134 | </thead> 135 | <tbody> 136 | <tr> 137 | <td style="text-align: center">damping</td> 138 | <td style="text-align: center"><code>number</code></td> 139 | <td style="text-align: center"><code>0.1</code></td> 140 | <td style="text-align: left">Momentum reduction damping factor, a float value between <code>(0, 1)</code>, the lower the value is, the more smooth the scrolling will be (also the more paint frames).</td> 141 | </tr> 142 | <tr> 143 | <td style="text-align: center">thumbMinSize</td> 144 | <td style="text-align: center"><code>number</code></td> 145 | <td style="text-align: center"><code>20</code></td> 146 | <td style="text-align: left">Minimal size for scrollbar thumbs.</td> 147 | </tr> 148 | <tr> 149 | <td style="text-align: center">renderByPixels</td> 150 | <td style="text-align: center"><code>boolean</code></td> 151 | <td style="text-align: center"><code>true</code></td> 152 | <td style="text-align: left">Render every frame in integer pixel values, set to <code>true</code> to improve scrolling performance.</td> 153 | </tr> 154 | <tr> 155 | <td style="text-align: center">alwaysShowTracks</td> 156 | <td style="text-align: center"><code>boolean</code></td> 157 | <td style="text-align: center"><code>false</code></td> 158 | <td style="text-align: left">Keep scrollbar tracks always visible.</td> 159 | </tr> 160 | <tr> 161 | <td style="text-align: center">continuousScrolling</td> 162 | <td style="text-align: center"><code>boolean</code></td> 163 | <td style="text-align: center"><code>true</code></td> 164 | <td style="text-align: left">Set to <code>true</code> to allow outer scrollbars continue scrolling when current scrollbar reaches edge.</td> 165 | </tr> 166 | <tr> 167 | <td style="text-align: center">wheelEventTarget</td> 168 | <td style="text-align: center"><code>EventTarget</code></td> 169 | <td style="text-align: center"><code>null</code></td> 170 | <td style="text-align: left">Element to be used as a listener for mouse wheel scroll events. By default, the container element is used. This option will be useful for dealing with fixed elements.</td> 171 | </tr> 172 | <tr> 173 | <td style="text-align: center">plugins</td> 174 | <td style="text-align: center"><code>object</code></td> 175 | <td style="text-align: center"><code>{}</code></td> 176 | <td style="text-align: left">Options for plugins, see <a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar/blob/HEAD/docs/plugin.md">Plugin System</a>.</td> 177 | </tr> 178 | </tbody> 179 | </table> 180 | <p><strong>Confusing with the option field? Try real-time edit tool on the bottom left!</strong></p> 181 | <h2>DOM Structure</h2> 182 | <p>The following is the DOM structure that Scrollbar yields:</p> 183 | <div><pre><code class="language-markup"><scrollbar> 184 | <div class="scroll-content"> 185 | your contents here... 186 | </div> 187 | <div class="scrollbar-track scrollbar-track-x"> 188 | <div class="scrollbar-thumb scrollbar-thumb-x"></div> 189 | </div> 190 | <div class="scrollbar-track scrollbar-track-y"> 191 | <div class="scrollbar-thumb scrollbar-thumb-y"></div> 192 | </div> 193 | </scrollbar></code></pre></div> 194 | <h2>Documentation</h2> 195 | <table> 196 | <thead> 197 | <tr> 198 | <th><a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar/blob/HEAD/docs">latest</a></th> 199 | <th><a target="_blank" href="https://github.com/idiotWu/smooth-scrollbar/tree/7.x">7.x</a></th> 200 | </tr> 201 | </thead> 202 | <tbody> 203 | </tbody> 204 | </table> 205 | <h2>Demo</h2> 206 | <p>Okay, Let's try it:</p> 207 | <pre class="language-markup"><code><section data-scrollbar> 208 | <img src="xxx.jpg"> 209 | </section> 210 | 211 | <script> Scrollbar.initAll(); </script></code></pre> 212 | <p>Wow, it works! Now change the value of options in the control panel and see what will happen :), be careful that this may affect all scrollbars, aha!</p> 213 | <div id="inner-scrollbar" data-scrollbar> 214 | <img src="images/your_diary.jpg" height="1080" width="1920"> 215 | </div> 216 | </article> 217 | <footer> 218 | Created by <a target="_blank" href="https://github.com/idiotWu" target="_blank">Dolphin Wood</a> with love. 219 | </footer> 220 | </main> 221 | <script src="app.js"></script> 222 | <script async src="https://www.googletagmanager.com/gtag/js?id=UA-107846402-1"></script> 223 | <script> 224 | window.dataLayer = window.dataLayer || []; 225 | function gtag(){dataLayer.push(arguments);} 226 | gtag('js', new Date()); 227 | 228 | gtag('config', 'UA-107846402-1'); 229 | </script> 230 | <script>!function(d, s, id) {var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https'; if (!d.getElementById(id)) {js = d.createElement(s); js.id = id; js.src = p + '://platform.twitter.com/widgets.js'; fjs.parentNode.insertBefore(js, fjs); } }(document, 'script', 'twitter-wjs'); </script> 231 | </body> 232 | 233 | </html> 234 | -------------------------------------------------------------------------------- /demo/scripts/controller.ts: -------------------------------------------------------------------------------- 1 | import * as dat from 'dat-gui'; 2 | import Scrollbar from 'smooth-scrollbar'; 3 | import OverscrollPlugin from 'smooth-scrollbar/plugins/overscroll'; 4 | 5 | Scrollbar.use(OverscrollPlugin); 6 | 7 | const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 8 | 9 | const options = { 10 | damping: isMobile ? 0.05 : 0.1, 11 | thumbMinSize: 20, 12 | renderByPixels: !('ontouchstart' in document), 13 | alwaysShowTracks: false, 14 | continuousScrolling: true, 15 | }; 16 | 17 | const overscrollOptions = { 18 | enable: true, 19 | effect: navigator.userAgent.match(/Android/) ? 'glow' : 'bounce', 20 | damping: 0.2, 21 | maxOverscroll: 150, 22 | glowColor: '#222a2d', 23 | }; 24 | 25 | const scrollbars = [ 26 | Scrollbar.init(document.getElementById('main-scrollbar') as HTMLElement, { 27 | ...options, 28 | delegateTo: document, 29 | plugins: { 30 | overscroll: { ...overscrollOptions }, 31 | }, 32 | }), 33 | Scrollbar.init(document.getElementById('inner-scrollbar') as HTMLElement, { 34 | ...options, 35 | plugins: { 36 | overscroll: { ...overscrollOptions }, 37 | }, 38 | }), 39 | ]; 40 | const controller = new dat.GUI(); 41 | 42 | function updateScrollbar() { 43 | scrollbars.forEach((s) => { 44 | // real-time options 45 | Object.assign(s.options, options); 46 | s.updatePluginOptions('overscroll', { 47 | ...overscrollOptions, 48 | effect: overscrollOptions.enable ? overscrollOptions.effect : undefined, 49 | }); 50 | 51 | if (options.alwaysShowTracks) { 52 | s.track.xAxis.show(); 53 | s.track.yAxis.show(); 54 | } else { 55 | s.track.xAxis.hide(); 56 | s.track.yAxis.hide(); 57 | } 58 | }); 59 | } 60 | 61 | const f1 = controller.addFolder('Scrollbar Options'); 62 | f1.open(); 63 | 64 | [ 65 | f1.add(options, 'damping', 0.01, 1), 66 | f1.add(options, 'thumbMinSize', 0, 100), 67 | f1.add(options, 'renderByPixels'), 68 | f1.add(options, 'alwaysShowTracks'), 69 | f1.add(options, 'continuousScrolling'), 70 | ].forEach((ctrl) => { 71 | ctrl.onChange(updateScrollbar); 72 | }); 73 | 74 | const f2 = controller.addFolder('Overscroll Plugin Options'); 75 | [ 76 | f2.add(overscrollOptions, 'enable'), 77 | f2.add(overscrollOptions, 'effect', ['bounce', 'glow']), 78 | f2.add(overscrollOptions, 'damping', 0.01, 1), 79 | f2.add(overscrollOptions, 'maxOverscroll', 30, 300), 80 | f2.addColor(overscrollOptions, 'glowColor'), 81 | ].forEach((ctrl) => { 82 | ctrl.onChange(updateScrollbar); 83 | }); 84 | 85 | const el = document.getElementById('controller'); 86 | 87 | if (el) { 88 | el.appendChild(controller.domElement); 89 | } 90 | 91 | if (window.innerWidth < 600) { 92 | controller.close(); 93 | } 94 | 95 | export { controller }; 96 | -------------------------------------------------------------------------------- /demo/scripts/index.ts: -------------------------------------------------------------------------------- 1 | import Scrollbar from 'smooth-scrollbar'; 2 | import * as Prism from 'prismjs'; 3 | import 'prismjs/themes/prism.css'; 4 | 5 | import './monitor'; 6 | import './controller'; 7 | import '../styles/index.styl'; 8 | 9 | // for debug 10 | (window as any).Scrollbar = Scrollbar; 11 | 12 | Prism.highlightAll(false); 13 | 14 | (document.getElementById('version') as HTMLElement).textContent = Scrollbar.version; 15 | -------------------------------------------------------------------------------- /demo/scripts/monitor.ts: -------------------------------------------------------------------------------- 1 | import Scrollbar from 'smooth-scrollbar'; 2 | import { controller } from './controller'; 3 | 4 | const DPR = window.devicePixelRatio; 5 | const TIME_RANGE_MAX = 20 * 1e3; 6 | 7 | const monitor = document.getElementById('monitor') as HTMLCanvasElement; 8 | const thumb = document.getElementById('thumb') as HTMLCanvasElement; 9 | const track = document.getElementById('track') as HTMLCanvasElement; 10 | const canvas = document.getElementById('chart') as HTMLCanvasElement; 11 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; 12 | const size = { 13 | width: 300, 14 | height: 200, 15 | }; 16 | 17 | canvas.width = size.width * DPR; 18 | canvas.height = size.height * DPR; 19 | ctx.scale(DPR, DPR); 20 | 21 | const scrollbar = Scrollbar.get(document.getElementById('main-scrollbar') as HTMLElement) as Scrollbar; 22 | const monitorCtrl = controller.addFolder('Monitor'); 23 | 24 | type Coord2d = [number, number]; 25 | 26 | type RecordPoint = { 27 | offset: number, 28 | time: number, 29 | reduce: number, 30 | speed: number, 31 | }; 32 | 33 | type TangentPoint = { 34 | coord: Coord2d, 35 | point: RecordPoint, 36 | }; 37 | 38 | const records: RecordPoint[] = []; 39 | 40 | let thumbWidth = 0; 41 | let endOffset = 0; 42 | 43 | let shouldUpdate = true; 44 | 45 | let tangentPoint: TangentPoint | null = null; 46 | let tangentPointPre: TangentPoint | null = null; 47 | 48 | let hoverLocked = false; 49 | let hoverPrecision = 'ontouchstart' in document ? 5 : 1; 50 | 51 | let hoverPointerX: number | undefined; 52 | let pointerDownOnTrack: number | undefined; 53 | let renderLoopID: number; 54 | 55 | let lastTime = Date.now(); 56 | let lastOffset = 0; 57 | let reduceAmount = 0; 58 | 59 | const monitorOptions = { 60 | show: window.innerWidth > 600, 61 | data: 'offset', 62 | duration: 5, 63 | reset() { 64 | records.length = endOffset = reduceAmount = 0; 65 | hoverLocked = false; 66 | hoverPointerX = undefined; 67 | tangentPoint = null; 68 | tangentPointPre = null; 69 | sliceRecord(); 70 | }, 71 | }; 72 | 73 | if (monitorOptions.show) { 74 | monitor.style.display = 'block'; 75 | renderLoopID = requestAnimationFrame(render); 76 | } 77 | 78 | monitorCtrl.add(monitorOptions, 'reset'); 79 | monitorCtrl.add(monitorOptions, 'data', ['offset', 'speed']) 80 | .onChange(() => { 81 | shouldUpdate = true; 82 | }); 83 | 84 | monitorCtrl.add(monitorOptions, 'show') 85 | .onChange((show) => { 86 | if (show) { 87 | monitor.style.display = 'block'; 88 | renderLoopID = requestAnimationFrame(render); 89 | } else { 90 | monitor.style.display = 'none'; 91 | cancelAnimationFrame(renderLoopID); 92 | } 93 | }); 94 | 95 | monitorCtrl.add(monitorOptions, 'duration', 1, 20) 96 | .onChange(() => { 97 | shouldUpdate = true; 98 | let start = records[0]; 99 | let end = records[records.length - 1]; 100 | 101 | if (end) { 102 | endOffset = Math.min(endOffset, Math.max(0, 1 - monitorOptions.duration * 1e3 / (end.time - start.time))); 103 | } 104 | }); 105 | 106 | function notation(num: number = 0) { 107 | if (!num || Math.abs(num) > 10 ** -2) return num.toFixed(2); 108 | 109 | let exp = -3; 110 | 111 | while (!(num / 10 ** exp | 0)) { 112 | if (exp < -10) { 113 | return num > 0 ? 'Infinity' : '-Infinity'; 114 | } 115 | 116 | exp--; 117 | } 118 | 119 | return (num * 10 ** -exp).toFixed(2) + 'e' + exp; 120 | } 121 | 122 | function addEvent(elems: EventTarget | EventTarget[], evts: string, handler: (e: Event) => void) { 123 | evts.split(/\s+/).forEach((name) => { 124 | ([] as EventTarget[]).concat(elems).forEach((el) => { 125 | el.addEventListener(name, (e) => { 126 | handler(e); 127 | shouldUpdate = true; 128 | }); 129 | }); 130 | }); 131 | } 132 | 133 | function sliceRecord(): RecordPoint[] { 134 | const last = records[records.length - 1]; 135 | 136 | let endIdx = Math.floor(records.length * (1 - endOffset)); 137 | let dropIdx = 0; 138 | 139 | const result = records.filter((pt, idx) => { 140 | if (last.time - pt.time > TIME_RANGE_MAX) { 141 | dropIdx++; 142 | endIdx--; 143 | return false; 144 | } 145 | 146 | const end = records[endIdx - 1]; 147 | 148 | return end.time - pt.time <= monitorOptions.duration * 1e3 && idx <= endIdx; 149 | }); 150 | 151 | records.splice(0, dropIdx); 152 | thumbWidth = result.length ? result.length / records.length : 1; 153 | 154 | thumb.style.width = thumbWidth * 100 + '%'; 155 | thumb.style.right = endOffset * 100 + '%'; 156 | 157 | return result; 158 | } 159 | 160 | function getLimit(points: RecordPoint[]): { max: number, min: number } { 161 | return points.reduce((pre, cur) => { 162 | let val = cur[monitorOptions.data]; 163 | return { 164 | max: Math.max(pre.max, val), 165 | min: Math.min(pre.min, val), 166 | }; 167 | }, { max: -Infinity, min: Infinity }); 168 | } 169 | 170 | function assignProps(props: any) { 171 | if (!props) return; 172 | 173 | Object.keys(props).forEach((name) => { 174 | ctx[name] = props[name]; 175 | }); 176 | } 177 | 178 | function drawLine(p0: Coord2d, p1: Coord2d, options: any) { 179 | let x0 = p0[0]; 180 | let y0 = p0[1]; 181 | let x1 = p1[0]; 182 | let y1 = p1[1]; 183 | 184 | assignProps(options.props); 185 | 186 | ctx.save(); 187 | ctx.transform(1, 0, 0, -1, 0, size.height); 188 | ctx.beginPath(); 189 | ctx.setLineDash(options.dashed ? options.dashed : []); 190 | ctx.moveTo(x0, y0); 191 | ctx.lineTo(x1, y1); 192 | ctx.stroke(); 193 | ctx.closePath(); 194 | ctx.restore(); 195 | } 196 | 197 | function adjustText(content: string, p: Coord2d, options: any) { 198 | let x = p[0]; 199 | let y = p[1]; 200 | 201 | let width = ctx.measureText(content).width; 202 | 203 | if (x + width > size.width) { 204 | ctx.textAlign = 'right'; 205 | } else if (x - width < 0) { 206 | ctx.textAlign = 'left'; 207 | } else { 208 | ctx.textAlign = options.textAlign; 209 | } 210 | 211 | ctx.fillText(content, x, -y); 212 | } 213 | 214 | function fillText(content: string, p: Coord2d, options: any) { 215 | assignProps(options.props); 216 | 217 | ctx.save(); 218 | ctx.transform(1, 0, 0, 1, 0, size.height); 219 | adjustText(content, p, options); 220 | ctx.restore(); 221 | } 222 | 223 | function drawMain() { 224 | let points = sliceRecord(); 225 | if (!points.length) return; 226 | 227 | let limit = getLimit(points); 228 | 229 | let start = points[0]; 230 | let end = points[points.length - 1]; 231 | 232 | let totalX = thumbWidth === 1 ? monitorOptions.duration * 1e3 : end.time - start.time; 233 | let totalY = (limit.max - limit.min) || 1; 234 | 235 | const grd = ctx.createLinearGradient(0, size.height, 0, 0); 236 | grd.addColorStop(0, 'rgb(170, 215, 255)'); 237 | grd.addColorStop(1, 'rgba(170, 215, 255, 0.2)'); 238 | 239 | ctx.save(); 240 | ctx.transform(1, 0, 0, -1, 0, size.height); 241 | 242 | ctx.lineWidth = 1; 243 | ctx.fillStyle = grd; 244 | ctx.strokeStyle = 'rgb(64, 165, 255)'; 245 | ctx.beginPath(); 246 | ctx.moveTo(0, 0); 247 | 248 | const lastPoint = points.reduce((pre: Coord2d, cur: RecordPoint, idx: number) => { 249 | const time = cur.time; 250 | const value = cur[monitorOptions.data]; 251 | const x = (time - start.time) / totalX * size.width; 252 | const y = (value - limit.min) / totalY * (size.height - 20); 253 | 254 | ctx.lineTo(x, y); 255 | 256 | if (hoverPointerX && Math.abs(hoverPointerX - x) < hoverPrecision) { 257 | tangentPoint = { 258 | coord: [x, y], 259 | point: cur, 260 | }; 261 | 262 | tangentPointPre = { 263 | coord: pre, 264 | point: points[idx - 1], 265 | }; 266 | } 267 | 268 | return [x, y]; 269 | }, []) as Coord2d; 270 | 271 | ctx.stroke(); 272 | ctx.lineTo(lastPoint[0], 0); 273 | ctx.fill(); 274 | ctx.closePath(); 275 | ctx.restore(); 276 | 277 | drawLine([0, lastPoint[1]], lastPoint, { 278 | props: { 279 | strokeStyle: '#f60', 280 | }, 281 | }); 282 | 283 | fillText('↙' + notation(limit.min), [0, 0], { 284 | props: { 285 | fillStyle: '#000', 286 | textAlign: 'left', 287 | textBaseline: 'bottom', 288 | font: '12px sans-serif', 289 | }, 290 | }); 291 | fillText(notation(end[monitorOptions.data]), lastPoint, { 292 | props: { 293 | fillStyle: '#f60', 294 | textAlign: 'right', 295 | textBaseline: 'bottom', 296 | font: '16px sans-serif', 297 | }, 298 | }); 299 | } 300 | 301 | function drawTangentLine() { 302 | if (!tangentPoint || !tangentPointPre) { 303 | return; 304 | } 305 | 306 | const coord = tangentPoint.coord; 307 | const coordPre = tangentPointPre.coord; 308 | 309 | const k = (coord[1] - coordPre[1]) / (coord[0] - coordPre[0]) || 0; 310 | const b = coord[1] - k * coord[0]; 311 | 312 | drawLine([0, b], [size.width, k * size.width + b], { 313 | props: { 314 | lineWidth: 1, 315 | strokeStyle: '#f00', 316 | }, 317 | }); 318 | 319 | const realK = (tangentPoint.point[monitorOptions.data] - tangentPointPre.point[monitorOptions.data]) / 320 | (tangentPoint.point.time - tangentPointPre.point.time); 321 | 322 | fillText('dy/dx: ' + notation(realK), [size.width / 2, 0], { 323 | props: { 324 | fillStyle: '#f00', 325 | textAlign: 'center', 326 | textBaseline: 'bottom', 327 | font: 'bold 12px sans-serif', 328 | }, 329 | }); 330 | } 331 | 332 | function drawHover() { 333 | if (!tangentPoint) return; 334 | 335 | drawTangentLine(); 336 | 337 | let coord = tangentPoint.coord; 338 | let point = tangentPoint.point; 339 | 340 | let coordStyle = { 341 | dashed: [8, 4], 342 | props: { 343 | lineWidth: 1, 344 | strokeStyle: 'rgb(64, 165, 255)', 345 | }, 346 | }; 347 | 348 | drawLine([0, coord[1]], [size.width, coord[1]], coordStyle); 349 | drawLine([coord[0], 0], [coord[0], size.height], coordStyle); 350 | 351 | let date = new Date(point.time + point.reduce); 352 | 353 | let pointInfo = [ 354 | '(', 355 | date.getMinutes(), 356 | ':', 357 | date.getSeconds(), 358 | '.', 359 | date.getMilliseconds(), 360 | ', ', 361 | notation(point[monitorOptions.data]), 362 | ')', 363 | ].join(''); 364 | 365 | fillText(pointInfo, coord, { 366 | props: { 367 | fillStyle: '#000', 368 | textAlign: 'left', 369 | textBaseline: 'bottom', 370 | font: 'bold 12px sans-serif', 371 | }, 372 | }); 373 | } 374 | 375 | function render() { 376 | if (!shouldUpdate) { 377 | renderLoopID = requestAnimationFrame(render); 378 | return; 379 | } 380 | 381 | ctx.save(); 382 | ctx.clearRect(0, 0, size.width, size.height); 383 | 384 | fillText(monitorOptions.data.toUpperCase(), [0, size.height], { 385 | props: { 386 | fillStyle: '#f00', 387 | textAlign: 'left', 388 | textBaseline: 'top', 389 | font: 'bold 14px sans-serif', 390 | }, 391 | }); 392 | 393 | drawMain(); 394 | drawHover(); 395 | 396 | if (hoverLocked) { 397 | fillText('LOCKED', [size.width, size.height], { 398 | props: { 399 | fillStyle: '#f00', 400 | textAlign: 'right', 401 | textBaseline: 'top', 402 | font: 'bold 14px sans-serif', 403 | }, 404 | }); 405 | } 406 | 407 | ctx.restore(); 408 | 409 | shouldUpdate = false; 410 | 411 | renderLoopID = requestAnimationFrame(render); 412 | } 413 | 414 | scrollbar.addListener(() => { 415 | let current = Date.now(); 416 | let offset = scrollbar.offset.y; 417 | let duration = current - lastTime; 418 | 419 | if (!duration || offset === lastOffset) return; 420 | 421 | if (duration > 100) { 422 | reduceAmount += (duration - 1); 423 | duration -= (duration - 1); 424 | } 425 | 426 | let velocity = (offset - lastOffset) / duration; 427 | lastTime = current; 428 | lastOffset = offset; 429 | 430 | records.push({ 431 | offset, 432 | time: current - reduceAmount, 433 | reduce: reduceAmount, 434 | speed: Math.abs(velocity), 435 | }); 436 | 437 | shouldUpdate = true; 438 | }); 439 | 440 | function getPointer(e: any) { 441 | return e.touches ? e.touches[e.touches.length - 1] : e; 442 | } 443 | 444 | // hover 445 | addEvent(canvas, 'mousemove touchmove', (e) => { 446 | if (hoverLocked || pointerDownOnTrack) return; 447 | 448 | let pointer = getPointer(e); 449 | 450 | hoverPointerX = pointer.clientX - canvas.getBoundingClientRect().left; 451 | }); 452 | 453 | function resetHover() { 454 | hoverPointerX = 0; 455 | tangentPoint = null; 456 | tangentPointPre = null; 457 | } 458 | 459 | addEvent([canvas, window], 'mouseleave touchend', () => { 460 | if (hoverLocked) return; 461 | resetHover(); 462 | }); 463 | 464 | addEvent(canvas, 'click', () => { 465 | hoverLocked = !hoverLocked; 466 | 467 | if (!hoverLocked) resetHover(); 468 | }); 469 | 470 | // track 471 | addEvent(thumb, 'mousedown touchstart', (e) => { 472 | let pointer = getPointer(e); 473 | pointerDownOnTrack = pointer.clientX; 474 | }); 475 | 476 | addEvent(window, 'mousemove touchmove', (e) => { 477 | if (!pointerDownOnTrack) return; 478 | 479 | let pointer = getPointer(e); 480 | let moved = (pointer.clientX - pointerDownOnTrack) / size.width; 481 | 482 | pointerDownOnTrack = pointer.clientX; 483 | endOffset = Math.min(1 - thumbWidth, Math.max(0, endOffset - moved)); 484 | }); 485 | 486 | addEvent(window, 'mouseup touchend blur', () => { 487 | pointerDownOnTrack = undefined; 488 | }); 489 | 490 | addEvent(thumb, 'click touchstart', (e) => { 491 | e.stopPropagation(); 492 | }); 493 | 494 | addEvent(track, 'click touchstart', (e) => { 495 | let pointer = getPointer(e); 496 | let rect = track.getBoundingClientRect(); 497 | let offset = (pointer.clientX - rect.left) / rect.width; 498 | endOffset = Math.min(1 - thumbWidth, Math.max(0, 1 - (offset + thumbWidth / 2))); 499 | }); 500 | -------------------------------------------------------------------------------- /demo/styles/index.styl: -------------------------------------------------------------------------------- 1 | $root-font-size = 16px 2 | $main-blue = #70b7fd 3 | $gradient-color = #ad95e4 4 | $font-color = #555 5 | $strong-color = #dd4a68 6 | 7 | pxToRem(px) 8 | return unit(px/$root-font-size, 'rem') 9 | 10 | html, body 11 | position: fixed 12 | top: 0 13 | left: 0 14 | width: 100% 15 | height: 100% 16 | margin: 0 17 | padding: 0 18 | color: $font-color 19 | font-size: $root-font-size 20 | font-family: 'Hiragino Sans GB', 'Microsoft YaHei', 'WenQuanYi Micro Hei', sans-serif 21 | 22 | @media screen and (max-width: 960px) 23 | font-size: 14px 24 | 25 | * 26 | -webkit-tap-highlight-color: transparent 27 | 28 | #inner-scrollbar 29 | img 30 | display: block 31 | 32 | [data-scrollbar] 33 | overflow: auto 34 | 35 | strong 36 | color: lighten($strong-color, 20%) 37 | 38 | a 39 | text-decoration: none 40 | color: darken($main-blue, 30%) 41 | 42 | &:hover 43 | text-decoration: underline 44 | 45 | code 46 | font-size: 0.8em 47 | color: darken($font-color, 30%) 48 | padding: .1em .5em 49 | 50 | 51 | header 52 | position: relative 53 | padding: 1em 1em 5em 54 | background: linear-gradient(45deg, $gradient-color, $main-blue) 55 | color: #fff 56 | font-size: pxToRem(14) 57 | text-align: center 58 | 59 | nav 60 | text-align: left 61 | 62 | h1 63 | font-size: 3em 64 | 65 | h2 66 | opacity: 0.85 67 | font-size: 1.5em 68 | font-weight: normal 69 | 70 | .repo 71 | display: inline-block 72 | margin: 1em 73 | padding: 0.5em 2em 74 | color: #fff 75 | text-decoration: none 76 | opacity: 0.85 77 | border: 1px solid currentColor 78 | border-radius: 2em 79 | 80 | &.doc 81 | opacity: 1 82 | background: rgba(255, 255, 255, 0.3) 83 | 84 | &:hover 85 | transform: translate3d(0, 2px, 0) 86 | 87 | .version-note 88 | position: absolute 89 | bottom: 0.5em 90 | right: 0.5em 91 | font-size: 0.8em 92 | opacity: 0.8 93 | 94 | #content 95 | font-size: pxToRem(16) 96 | padding: 1em 2em 2em 97 | 98 | table 99 | font-size: .85em 100 | border-collapse: collapse 101 | 102 | thead, tr:nth-child(even) 103 | background-color: #f5f2f0 104 | 105 | th, td 106 | padding: 0.5em 1em 107 | border: 1px solid #ddd 108 | 109 | p::after 110 | content: '' 111 | display: table 112 | clear: both 113 | 114 | h2 115 | color: $main-blue 116 | font-size: 1.2em 117 | margin-left: -10px 118 | 119 | &::before 120 | content: '#' 121 | padding-right: .5em 122 | 123 | .intro 124 | ul 125 | padding-left: 1em 126 | 127 | .compatibility 128 | th, td 129 | &:first-child 130 | text-align: left 131 | &:last-child 132 | text-align: center 133 | 134 | .options 135 | th, td 136 | text-align: center 137 | 138 | &:last-child 139 | min-width: 20em 140 | text-align: left 141 | 142 | 143 | #scrollbar-demo 144 | display: flex 145 | justify-content: space-around 146 | 147 | #controller 148 | position: fixed 149 | bottom: 0 150 | left: 0 151 | z-index: 999 152 | 153 | #inner-scrollbar 154 | max-width: 800px 155 | height: 400px 156 | border: 1px solid #ccc 157 | 158 | footer 159 | text-align: center 160 | 161 | // dat.gui 162 | .dg 163 | user-select: none 164 | 165 | .close-button 166 | position: relative !important 167 | .property-name 168 | width: 60% !important 169 | 170 | .c 171 | width: 40% !important 172 | 173 | .selector 174 | margin-top: -105px; 175 | 176 | #main-scrollbar 177 | position: fixed 178 | top: 0 179 | right: 0 180 | bottom: 0 181 | left: 0 182 | 183 | #monitor 184 | display: none 185 | position: fixed 186 | right: 1em 187 | bottom: 1em 188 | z-index: 999 189 | background: #fff 190 | text-align: center 191 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.8) 192 | 193 | #chart 194 | width: 300px 195 | height: 200px 196 | border: 1px solid #aaa 197 | display: block 198 | 199 | #track 200 | position: relative 201 | width: 100% 202 | height: 20px 203 | background: #ccc 204 | 205 | #thumb 206 | margin: 0 207 | position: absolute 208 | top: 0 209 | right: 0 210 | height: 100% 211 | width: 100% 212 | background: #fff 213 | border: 3px solid #ccc 214 | box-sizing: border-box 215 | cursor: ew-resize 216 | 217 | // 218 | .github-corner:hover 219 | .octo-arm 220 | animation: octocat-wave 560ms ease-in-out 221 | 222 | @keyframes octocat-wave 223 | 0%, 100% 224 | transform: rotate(0) 225 | 226 | 20%, 60% 227 | transform: rotate(-25deg) 228 | 229 | 40%, 80% 230 | transform: rotate(10deg) 231 | 232 | @media (max-width:500px) 233 | .github-corner:hover .octo-arm 234 | animation: none 235 | 236 | .github-corner .octo-arm 237 | animation: octocat-wave 560ms ease-in-out 238 | 239 | .badges 240 | display: none 241 | 242 | #options 243 | th, td 244 | &:nth-child(2) 245 | &:nth-child(3) 246 | display: none 247 | 248 | code 249 | white-space: pre-wrap !important 250 | -------------------------------------------------------------------------------- /dist/plugins/overscroll.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("smooth-scrollbar")):"function"==typeof define&&define.amd?define(["smooth-scrollbar"],e):"object"==typeof exports?exports.OverscrollPlugin=e(require("smooth-scrollbar")):t.OverscrollPlugin=e(t.Scrollbar)}(this,(function(t){return function(t){var e={};function o(i){if(e[i])return e[i].exports;var r=e[i]={i:i,l:!1,exports:{}};return t[i].call(r.exports,r,r.exports,o),r.l=!0,r.exports}return o.m=t,o.c=e,o.d=function(t,e,i){o.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},o.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(o.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(i,r,function(e){return t[e]}.bind(null,r));return i},o.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(e,"a",e),e},o.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},o.p="",o(o.s=1)}([function(e,o){e.exports=t},function(t,e,o){t.exports=o(2)},function(t,e,o){"use strict";o.r(e); 2 | /*! ***************************************************************************** 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 5 | this file except in compliance with the License. You may obtain a copy of the 6 | License at http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 9 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 10 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 11 | MERCHANTABLITY OR NON-INFRINGEMENT. 12 | 13 | See the Apache Version 2.0 License for specific language governing permissions 14 | and limitations under the License. 15 | ***************************************************************************** */ 16 | var i=function(t,e){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var o in e)e.hasOwnProperty(o)&&(t[o]=e[o])})(t,e)},r=function(){return(r=Object.assign||function(t){for(var e,o=1,i=arguments.length;o<i;o++)for(var r in e=arguments[o])Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t}).apply(this,arguments)};function n(t){var e=function(t){return t.touches?t.touches[t.touches.length-1]:t}(t);return{x:e.clientX,y:e.clientY}}new WeakMap;var s=["webkit","moz","ms","o"],c=new RegExp("^-(?!(?:"+s.join("|")+")-)");function a(t,e){e=function(t){var e={};return Object.keys(t).forEach((function(o){if(c.test(o)){var i=t[o];o=o.replace(/^-/,""),e[o]=i,s.forEach((function(t){e["-"+t+"-"+o]=i}))}else e[o]=t[o]})),e}(e),Object.keys(e).forEach((function(o){var i=o.replace(/^-/,"").replace(/-([a-z])/g,(function(t,e){return e.toUpperCase()}));t.style[i]=e[o]}))}var l=function(){function t(t){this.velocityMultiplier=window.devicePixelRatio,this.updateTime=Date.now(),this.delta={x:0,y:0},this.velocity={x:0,y:0},this.lastPosition={x:0,y:0},this.lastPosition=n(t)}return t.prototype.update=function(t){var e=this.velocity,o=this.updateTime,i=this.lastPosition,r=Date.now(),s=n(t),c={x:-(s.x-i.x),y:-(s.y-i.y)},a=r-o||16.7,l=c.x/a*16.7,h=c.y/a*16.7;e.x=l*this.velocityMultiplier,e.y=h*this.velocityMultiplier,this.delta=c,this.updateTime=r,this.lastPosition=s},t}();function h(t,e,o){return Math.max(e,Math.min(o,t))}!function(){function t(){this._touchList={}}Object.defineProperty(t.prototype,"_primitiveValue",{get:function(){return{x:0,y:0}},enumerable:!0,configurable:!0}),t.prototype.isActive=function(){return void 0!==this._activeTouchID},t.prototype.getDelta=function(){var t=this._getActiveTracker();return t?r({},t.delta):this._primitiveValue},t.prototype.getVelocity=function(){var t=this._getActiveTracker();return t?r({},t.velocity):this._primitiveValue},t.prototype.getEasingDistance=function(t){var e=1-t,o={x:0,y:0},i=this.getVelocity();return Object.keys(i).forEach((function(t){for(var r=Math.abs(i[t])<=10?0:i[t];0!==r;)o[t]+=r,r=r*e|0})),o},t.prototype.track=function(t){var e=this,o=t.targetTouches;return Array.from(o).forEach((function(t){e._add(t)})),this._touchList},t.prototype.update=function(t){var e=this,o=t.touches,i=t.changedTouches;return Array.from(o).forEach((function(t){e._renew(t)})),this._setActiveID(i),this._touchList},t.prototype.release=function(t){var e=this;delete this._activeTouchID,Array.from(t.changedTouches).forEach((function(t){e._delete(t)}))},t.prototype._add=function(t){this._has(t)&&this._delete(t);var e=new l(t);this._touchList[t.identifier]=e},t.prototype._renew=function(t){this._has(t)&&this._touchList[t.identifier].update(t)},t.prototype._delete=function(t){delete this._touchList[t.identifier]},t.prototype._has=function(t){return this._touchList.hasOwnProperty(t.identifier)},t.prototype._setActiveID=function(t){this._activeTouchID=t[t.length-1].identifier},t.prototype._getActiveTracker=function(){return this._touchList[this._activeTouchID]}}();var u,p=o(0),f=function(){function t(t){this._scrollbar=t}return t.prototype.render=function(t){var e=t.x,o=void 0===e?0:e,i=t.y,r=void 0===i?0:i,n=this._scrollbar,s=n.size,c=n.track,l=n.offset;if(a(n.contentEl,{"-transform":"translate3d("+-(l.x+o)+"px, "+-(l.y+r)+"px, 0)"}),o){c.xAxis.show();var h=s.container.width/(s.container.width+Math.abs(o));a(c.xAxis.thumb.element,{"-transform":"translate3d("+c.xAxis.thumb.offset+"px, 0, 0) scale3d("+h+", 1, 1)","-transform-origin":o<0?"left":"right"})}r&&(c.yAxis.show(),h=s.container.height/(s.container.height+Math.abs(r)),a(c.yAxis.thumb.element,{"-transform":"translate3d(0, "+c.yAxis.thumb.offset+"px, 0) scale3d(1, "+h+", 1)","-transform-origin":r<0?"top":"bottom"})),c.autoHideOnIdle()},t}(),_=function(){function t(t){this._scrollbar=t,this._canvas=document.createElement("canvas"),this._ctx=this._canvas.getContext("2d"),a(this._canvas,{position:"absolute",top:0,left:0,width:"100%",height:"100%",display:"none"})}return t.prototype.mount=function(){this._scrollbar.containerEl.appendChild(this._canvas)},t.prototype.unmount=function(){this._canvas.parentNode&&this._canvas.parentNode.removeChild(this._canvas)},t.prototype.adjust=function(){var t=this._scrollbar.size,e=window.devicePixelRatio||1,o=t.container.width*e,i=t.container.height*e;o===this._canvas.width&&i===this._canvas.height||(this._canvas.width=o,this._canvas.height=i,this._ctx.scale(e,e))},t.prototype.recordTouch=function(t){var e=t.touches[t.touches.length-1];this._touchX=e.clientX,this._touchY=e.clientY},t.prototype.render=function(t,e){var o=t.x,i=void 0===o?0:o,r=t.y,n=void 0===r?0:r;if(i||n){a(this._canvas,{display:"block"});var s=this._scrollbar.size;this._ctx.clearRect(0,0,s.container.width,s.container.height),this._ctx.fillStyle=e,this._renderX(i),this._renderY(n)}else a(this._canvas,{display:"none"})},t.prototype._getMaxOverscroll=function(){var t=this._scrollbar.options.plugins.overscroll;return t&&t.maxOverscroll?t.maxOverscroll:150},t.prototype._renderX=function(t){var e=this._scrollbar.size,o=this._getMaxOverscroll(),i=e.container,r=i.width,n=i.height,s=this._ctx;s.save(),t>0&&s.transform(-1,0,0,1,r,0);var c=h(Math.abs(t)/o,0,.75),a=h(c,0,.25)*r,l=Math.abs(t),u=this._touchY||n/2;s.globalAlpha=c,s.beginPath(),s.moveTo(0,-a),s.quadraticCurveTo(l,u,0,n+a),s.fill(),s.closePath(),s.restore()},t.prototype._renderY=function(t){var e=this._scrollbar.size,o=this._getMaxOverscroll(),i=e.container,r=i.width,n=i.height,s=this._ctx;s.save(),t>0&&s.transform(1,0,0,-1,0,n);var c=h(Math.abs(t)/o,0,.75),a=h(c,0,.25)*r,l=this._touchX||r/2,u=Math.abs(t);s.globalAlpha=c,s.beginPath(),s.moveTo(-a,0),s.quadraticCurveTo(l,u,r+a,0),s.fill(),s.closePath(),s.restore()},t}();o.d(e,"OverscrollEffect",(function(){return u})),function(t){t.BOUNCE="bounce",t.GLOW="glow"}(u||(u={}));var d=/wheel|touch/,y=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._glow=new _(e.scrollbar),e._bounce=new f(e.scrollbar),e._wheelScrollBack={x:!1,y:!1},e._lockWheel={x:!1,y:!1},e._touching=!1,e._amplitude={x:0,y:0},e._position={x:0,y:0},e._releaseWheel=function(t,e,o){var i;void 0===e&&(e=0);return function(){for(var o=this,r=[],n=0;n<arguments.length;n++)r[n]=arguments[n];clearTimeout(i),i=setTimeout((function(){t.apply(o,r)}),e)}}((function(){e._lockWheel.x=!1,e._lockWheel.y=!1}),30),e}return function(t,e){function o(){this.constructor=t}i(t,e),t.prototype=null===e?Object.create(e):(o.prototype=e.prototype,new o)}(e,t),Object.defineProperty(e.prototype,"_isWheelLocked",{get:function(){return this._lockWheel.x||this._lockWheel.y},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"_enabled",{get:function(){return!!this.options.effect},enumerable:!0,configurable:!0}),e.prototype.onInit=function(){var t=this._glow,e=this.options,o=this.scrollbar,i=e.effect;Object.defineProperty(e,"effect",{get:function(){return i},set:function(e){if(e){if(e!==u.BOUNCE&&e!==u.GLOW)throw new TypeError("unknow overscroll effect: "+e);i=e,o.options.continuousScrolling=!1,e===u.GLOW?(t.mount(),t.adjust()):t.unmount()}else i=void 0}}),e.effect=i},e.prototype.onUpdate=function(){this.options.effect===u.GLOW&&this._glow.adjust()},e.prototype.onRender=function(t){if(this._enabled){this.scrollbar.options.continuousScrolling&&(this.scrollbar.options.continuousScrolling=!1);var e=t.x,o=t.y;!this._amplitude.x&&this._willOverscroll("x",t.x)&&(e=0,this._absorbMomentum("x",t.x)),!this._amplitude.y&&this._willOverscroll("y",t.y)&&(o=0,this._absorbMomentum("y",t.y)),this.scrollbar.setMomentum(e,o),this._render()}},e.prototype.transformDelta=function(t,e){if(this._lastEventType=e.type,!this._enabled||!d.test(e.type))return t;this._isWheelLocked&&/wheel/.test(e.type)&&(this._releaseWheel(),this._willOverscroll("x",t.x)&&(t.x=0),this._willOverscroll("y",t.y)&&(t.y=0));var o=t.x,i=t.y;switch(this._willOverscroll("x",t.x)&&(o=0,this._addAmplitude("x",t.x)),this._willOverscroll("y",t.y)&&(i=0,this._addAmplitude("y",t.y)),e.type){case"touchstart":case"touchmove":this._touching=!0,this._glow.recordTouch(e);break;case"touchcancel":case"touchend":this._touching=!1}return{x:o,y:i}},e.prototype._willOverscroll=function(t,e){if(!e)return!1;if(this._position[t])return!0;var o=this.scrollbar.offset[t],i=this.scrollbar.limit[t];return 0!==i&&h(o+e,0,i)===o&&(0===o||o===i)},e.prototype._absorbMomentum=function(t,e){var o=this.options,i=this._lastEventType,r=this._amplitude;d.test(i)&&(r[t]=h(e,-o.maxOverscroll,o.maxOverscroll))},e.prototype._addAmplitude=function(t,e){var o=this.options,i=this.scrollbar,r=this._amplitude,n=this._position,s=r[t],c=e*s<0,a=s+e*(1-(c?0:this._wheelScrollBack[t]?1:Math.abs(s/o.maxOverscroll)));r[t]=0===i.offset[t]?h(a,-o.maxOverscroll,0):h(a,0,o.maxOverscroll),c&&(n[t]=r[t])},e.prototype._render=function(){var t=this.options,e=this._amplitude,o=this._position;if(this._enabled&&(e.x||e.y||o.x||o.y)){var i=this._nextAmp("x"),n=this._nextAmp("y");switch(e.x=i.amplitude,o.x=i.position,e.y=n.amplitude,o.y=n.position,t.effect){case u.BOUNCE:this._bounce.render(o);break;case u.GLOW:this._glow.render(o,this.options.glowColor)}"function"==typeof t.onScroll&&t.onScroll.call(this,r({},o))}},e.prototype._nextAmp=function(t){var e=this.options,o=this._amplitude,i=this._position,r=1-e.damping,n=o[t],s=i[t],c=this._touching?n:n*r|0,a=c-s,l=s+a-(a*r|0);return!this._touching&&Math.abs(l)<Math.abs(s)&&(this._wheelScrollBack[t]=!0),this._wheelScrollBack[t]&&Math.abs(l)<=1&&(this._wheelScrollBack[t]=!1,this._lockWheel[t]=!0),{amplitude:c,position:l}},e.pluginName="overscroll",e.defaultOptions={effect:u.BOUNCE,onScroll:void 0,damping:.2,maxOverscroll:150,glowColor:"#87ceeb"},e}(p.ScrollbarPlugin);e.default=y}]).default})); -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Smooth Scrollbar 2 | 3 | > This is the API documentation for `smooth-scrollbar@8.x`, check [here](https://github.com/idiotWu/smooth-scrollbar/tree/7.x) for the docs of version 7.x. 4 | 5 | > Looking for migration guides? See [migration guide](migration.md) for details. 6 | 7 | ## What is smooth-scrollbar? 8 | 9 | Smooth Scrollbar is a JavaScript Plugin that allows you customizing high perfermance scrollbars cross browsers. It is using `translate3d` to perform a momentum based scrolling (aka inertial scrolling) on modern browsers. With the flexible [plugin system](plugin.md), we can easily redesign the scrollbar as we want. This is the scrollbar plugin that you've ever dreamed of! 10 | 11 | ## Installation 12 | 13 | Via NPM **(recommended)**: 14 | 15 | ```shell 16 | npm install smooth-scrollbar --save 17 | ``` 18 | 19 | Via Bower: 20 | 21 | ```shell 22 | bower install smooth-scrollbar --save 23 | ``` 24 | 25 | ## Browser Compatibility 26 | 27 | | Browser | Version | 28 | | :------ | :-----: | 29 | | IE | 10+ | 30 | | Chrome | 22+ | 31 | | Firefox | 16+ | 32 | | Safari | 8+ | 33 | | Android Browser | 4+ | 34 | | Chrome for Android | 32+ | 35 | | iOS Safari | 7+ | 36 | 37 | ## Demo 38 | 39 | https://idiotwu.github.io/smooth-scrollbar/ 40 | 41 | ## Usage 42 | 43 | Since this package has a [pkg.module](https://github.com/rollup/rollup/wiki/pkg.module) field, it's highly recommended to import it as an ES6 module with some bundlers like [webpack](https://webpack.js.org/) or [rollup](https://rollupjs.org/): 44 | 45 | ```js 46 | import Scrollbar from 'smooth-scrollbar'; 47 | 48 | Scrollbar.init(document.querySelector('#my-scrollbar'), options); 49 | ``` 50 | 51 | If you are not using any bundlers, you can just load the UMD bundle: 52 | 53 | ```html 54 | <script src="dist/smooth-scrollbar.js"></script> 55 | 56 | <script> 57 | var Scrollbar = window.Scrollbar; 58 | 59 | Scrollbar.init(document.querySelector('#my-scrollbar'), options); 60 | </script> 61 | ``` 62 | 63 | ### Common mistakes 64 | 65 | #### Initialize a scrollbar without a limited width or height 66 | 67 | Likes the native scrollbars, a scrollable area means **the content insides it is larger than the container itself**, for example, a `500*500` area with a content which size is `1000*1000`: 68 | 69 | ``` 70 | container 71 | / 72 | +--------+ 73 | ##################### 74 | # | | # 75 | # | | # 76 | # +--------+ # -- content 77 | # # 78 | # # 79 | ##################### 80 | ``` 81 | 82 | Therefore, it's necessary to set the `width` or `height` for the container element: 83 | 84 | ```css 85 | #my-scrollbar { 86 | width: 500px; 87 | height: 500px; 88 | overflow: auto; 89 | } 90 | ``` 91 | 92 | If the container element is natively scrollable before initializing the Scrollbar, it means you are on the correct way. 93 | 94 | ## Available Options for Scrollbar 95 | 96 | | parameter | type | default | description | 97 | | :--------: | :--: | :-----: | :---------- | 98 | | damping | `number` | `0.1` | Momentum reduction damping factor, a float value between `(0, 1)`. The lower the value is, the more smooth the scrolling will be (also the more paint frames). | 99 | | thumbMinSize | `number` | `20` | Minimal size for scrollbar thumbs. | 100 | | renderByPixels | `boolean` | `true` | Render every frame in integer pixel values, set to `true` to improve scrolling performance. | 101 | | alwaysShowTracks | `boolean` | `false` | Keep scrollbar tracks visible. | 102 | | continuousScrolling | `boolean` | `true` | Set to `true` to allow outer scrollbars continue scrolling when current scrollbar reaches edge. | 103 | | delegateTo | `EventTarget` | `null` | Delegate _wheel events_ and _touch events_ to the given element. By default, the container element is used. This option will be useful for dealing with fixed elements. | 104 | | plugins | `object` | `{}` | Options for plugins, see [Plugin System](plugin.md). | 105 | 106 | **Confusing with the option field? Try real-time edit tool on [demo page](http://idiotwu.github.io/smooth-scrollbar/)!** 107 | 108 | ## DOM Structure 109 | 110 | The following is the DOM structure that Scrollbar yields: 111 | 112 | ```html 113 | <scrollbar> 114 | <div class="scroll-content"> 115 | your contents here... 116 | </div> 117 | <div class="scrollbar-track scrollbar-track-x"> 118 | <div class="scrollbar-thumb scrollbar-thumb-x"></div> 119 | </div> 120 | <div class="scrollbar-track scrollbar-track-y"> 121 | <div class="scrollbar-thumb scrollbar-thumb-y"></div> 122 | </div> 123 | </scrollbar> 124 | ``` 125 | 126 | ## Next Step... 127 | 128 | Check the [API documentation](api.md). 129 | 130 | 131 | ## Related Projects 132 | 133 | - [react-smooth-scrollbar](https://github.com/idiotWu/react-smooth-scrollbar) 134 | -------------------------------------------------------------------------------- /docs/assets/diagram.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idiotWu/smooth-scrollbar/66c67b85d35c14486bd25a73806c0ab13ffeb267/docs/assets/diagram.gif -------------------------------------------------------------------------------- /docs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg12" width="256" height="256" fill="none" version="1.1" viewBox="0 0 256 256"><defs id="defs16"><linearGradient id="linearGradient1252"><stop style="stop-color:#9f9ce9;stop-opacity:1" id="stop1248" offset="0"/><stop style="stop-color:#7eaff7;stop-opacity:1" id="stop1250" offset="1"/></linearGradient><g id="SVG" fill="#fff" transform="scale(2) translate(20,79)"><path id="S" d="M 5.482,31.319 C2.163,28.001 0.109,23.419 0.109,18.358 C0.109,8.232 8.322,0.024 18.443,0.024 C28.569,0.024 36.782,8.232 36.782,18.358 L26.042,18.358 C26.042,14.164 22.638,10.765 18.443,10.765 C14.249,10.765 10.850,14.164 10.850,18.358 C10.850,20.453 11.701,22.351 13.070,23.721 L13.075,23.721 C14.450,25.101 15.595,25.500 18.443,25.952 L18.443,25.952 C23.509,26.479 28.091,28.006 31.409,31.324 L31.409,31.324 C34.728,34.643 36.782,39.225 36.782,44.286 C36.782,54.412 28.569,62.625 18.443,62.625 C8.322,62.625 0.109,54.412 0.109,44.286 L10.850,44.286 C10.850,48.480 14.249,51.884 18.443,51.884 C22.638,51.884 26.042,48.480 26.042,44.286 C26.042,42.191 25.191,40.298 23.821,38.923 L23.816,38.923 C22.441,37.548 20.468,37.074 18.443,36.697 L18.443,36.692 C13.533,35.939 8.800,34.638 5.482,31.319 L5.482,31.319 L5.482,31.319 Z"/><path id="V" d="M 73.452,0.024 L60.482,62.625 L49.742,62.625 L36.782,0.024 L47.522,0.024 L55.122,36.687 L62.712,0.024 L73.452,0.024 Z"/><path id="G" d="M 91.792,25.952 L110.126,25.952 L110.126,44.286 L110.131,44.286 C110.131,54.413 101.918,62.626 91.792,62.626 C81.665,62.626 73.458,54.413 73.458,44.286 L73.458,44.286 L73.458,18.359 L73.453,18.359 C73.453,8.233 81.665,0.025 91.792,0.025 C101.913,0.025 110.126,8.233 110.126,18.359 L99.385,18.359 C99.385,14.169 95.981,10.765 91.792,10.765 C87.597,10.765 84.198,14.169 84.198,18.359 L84.198,44.286 L84.198,44.286 C84.198,48.481 87.597,51.880 91.792,51.880 C95.981,51.880 99.380,48.481 99.385,44.291 L99.385,44.286 L99.385,36.698 L91.792,36.698 L91.792,25.952 L91.792,25.952 Z"/></g><clipPath id="_clipPath_8TWIgR1z3pxinjWBiigzcEIrVJKv9Gq4"><rect id="rect836" width="500" height="500"/></clipPath><filter id="Hmac7mZraFWHw0G84Yxj4QuzeTFp0E7Y" width="1.44" height="1.172" x="-.22" y="-.086" color-interpolation-filters="sRGB" filterUnits="objectBoundingBox"><feGaussianBlur id="feGaussianBlur843" in="SourceGraphic" stdDeviation="6.44"/><feOffset id="feOffset845" dx="0" dy="0" result="pf_100_offsetBlur"/><feFlood id="feFlood847" flood-color="#000" flood-opacity=".65"/><feComposite id="feComposite849" in2="pf_100_offsetBlur" operator="in" result="pf_100_dropShadow"/><feBlend id="feBlend851" in="SourceGraphic" in2="pf_100_dropShadow" mode="normal"/></filter><linearGradient id="linearGradient5041"><stop style="stop-color:#3e4b4b;stop-opacity:1" id="stop5043" offset="0"/><stop style="stop-color:#1e3449;stop-opacity:1" id="stop5045" offset="1"/></linearGradient><radialGradient id="radialGradient3133" cx="293.499" cy="825.161" r="640" fx="293.499" fy="825.161" gradientTransform="matrix(2.7998301,-1.4738538,0.45507806,0.86449643,-926.2596,769.338)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4453-1"/><linearGradient id="linearGradient4453-1"><stop id="stop4455-4" offset="0" style="stop-color:#070808;stop-opacity:1"/><stop id="stop4457-0" offset="1" style="stop-color:#152534;stop-opacity:1"/></linearGradient><linearGradient id="linearGradient1254" x1="0" x2="256" y1="0" y2="256" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient1252"/></defs><rect id="rect2" width="256" height="256" x="0" y="0" fill="#242938" rx="60" style="fill:url(#linearGradient1254);fill-opacity:1;stroke-width:1"/><path id="path1512" fill="currentColor" d="m 101.28461,179.96493 -13.749459,14.0133 41.248569,42.03992 41.24779,-42.03992 -13.74926,-14.0133 -27.49853,28.02661 z" style="stroke-width:9.8152;fill:#fff;fill-opacity:1"/><path id="path1514" fill="currentColor" d="M 156.28225,84.479853 170.03151,70.466445 128.78372,28.426139 87.535248,70.466445 101.28471,84.479853 128.78372,56.452947 Z" style="stroke-width:9.8152;fill:#fff;fill-opacity:1"/><path id="path1516" fill="currentColor" fill-rule="evenodd" d="m 128.78372,102.4962 c 16.10886,0 29.16687,13.30896 29.16687,29.72718 0,16.41723 -13.05801,29.727 -29.16687,29.727 -16.10886,0 -29.166871,-13.30977 -29.166871,-29.727 0,-16.41822 13.058011,-29.72718 29.166871,-29.72718 z m 0,19.81819 c 5.36962,0 9.7223,4.43625 9.7223,9.90899 0,5.47274 -4.35268,9.90899 -9.7223,9.90899 -5.36962,0 -9.72229,-4.43625 -9.72229,-9.90899 0,-5.47274 4.35267,-9.90899 9.72229,-9.90899 z" clip-rule="evenodd" style="stroke-width:9.8152;fill:#fff;fill-opacity:1"/><g id="layer1" transform="translate(-606.58583,-699.5242)" style="display:none"><image id="image3047" width="800" height="329" x="-90.714" y="392.148" xlink:href="../../tomasi/Projects/nimrod/logo/new-symbols.png"/></g><path style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:87.357px;line-height:125%;font-family:Discognate;-inkscape-font-specification:Discognate;letter-spacing:0;word-spacing:0;display:none;fill:#fff;fill-opacity:1;stroke:none" id="path4870" d="m 602.64247,29.71093 h -28.65 v 7.28362 c 0,1.59787 0.4668,2.98732 1.4003,4.16834 0.9336,1.18104 2.0863,1.84067 3.4542,1.77155 h 23.6088 v 5.013 h -25.1124 c -2.2405,0 -4.1698,-0.86841 -5.7879,-2.60522 -1.556,-1.80627 -2.3339,-3.95992 -2.3339,-6.46093 V 14.28805 c 0,-2.50097 0.7779,-4.61988 2.3339,-6.35673 1.6181,-1.80625 3.5474,-2.70938 5.7879,-2.70942 h 17.1773 c 2.2405,4e-5 4.1387,0.90317 5.6946,2.70942 1.6181,1.73685 2.4272,3.85576 2.4272,6.35673 v 15.42288 m -4.9576,-4.59617 V 16.1748 c 0,-1.59784 -0.4668,-2.98729 -1.4003,-4.16836 -0.9336,-1.18099 -2.1161,-1.77151 -3.5475,-1.77154 h -13.8901 c -1.3693,3e-5 -2.5206,0.59055 -3.4542,1.77154 -0.9335,1.18107 -1.6566,2.59635 -1.4003,4.16836 v 8.93996 h 23.6924 m -40.8208,-41.56827 -0.093,64.40095 h -25.4857 c -2.2406,0 -4.1699,-0.86841 -5.788,-2.60522 -1.5559,-1.80627 -2.3339,-3.95992 -2.3339,-6.46093 l 0.093,-24.59324 c 0.062,-2.50097 0.8713,-4.61988 2.4272,-6.35673 1.5559,-1.80625 3.4541,-2.70938 5.6947,-2.70942 h 20.3414 l 0.093,-21.67541 h 5.051 m -5.1444,59.80479 -0.093,-33.53321 h -18.7445 c -1.3692,4e-5 -2.5206,0.66002 -3.4542,1.97996 -0.9335,1.32001 -1.4003,2.77893 -1.4003,4.37677 l 0.093,20.81975 c 0,1.59787 0.4357,3.05679 1.307,4.37675 0.8713,1.31999 1.9915,1.97998 3.3608,1.97998 h 18.9312 m -40.4211,-4.46999 c 0,2.50101 -0.8091,4.65466 -2.4272,6.46093 -1.5559,1.73681 -3.4541,2.60522 -5.6946,2.60522 h -19.0443 c -2.2406,0 -4.1699,-0.86841 -5.788,-2.60522 -1.5559,-1.80627 -2.3339,-3.95992 -2.3339,-6.46093 V 14.28805 c 0,-2.50097 0.778,-4.61988 2.3339,-6.35673 1.6181,-1.80625 3.5474,-2.70938 5.788,-2.70942 h 19.0443 c 2.2405,4e-5 4.1387,0.90317 5.6946,2.70942 1.6181,1.73685 2.4272,3.85576 2.4272,6.35673 v 24.59324 m -4.9576,-1.88674 V 16.1748 c 0,-1.59784 -0.4668,-2.98729 -1.4003,-4.16836 -0.9336,-1.18099 -2.1161,-1.77151 -3.5475,-1.77154 h -15.7572 c -1.3692,3e-5 -2.5206,0.59055 -3.4541,1.77154 -0.9336,1.18107 -1.4004,2.57052 -1.4004,4.16836 v 20.81975 c 0,1.59787 0.4668,2.98732 1.4004,4.16834 0.9335,1.18104 2.0849,1.77155 3.4541,1.77155 h 15.7572 c 1.4314,0 2.6139,-0.59051 3.5475,-1.77155 0.9335,-1.18102 1.4003,-2.57047 1.4003,-4.16834 m -43.2742,-8.62734 h -4.9577 V 16.1748 c 0,-1.59784 -0.4667,-2.98729 -1.4003,-4.16836 -0.9335,-1.18099 -2.116,-1.77151 -3.5474,-1.77154 h -13.8902 c -1.3692,3e-5 -2.5206,0.59055 -3.4541,1.77154 -0.9336,1.18107 -1.4003,2.57052 -1.4003,4.16836 v 20.81975 c 0,1.59787 0.4667,2.98732 1.4003,4.16834 0.9335,1.18104 2.0849,1.77155 3.4541,1.77155 h 23.5155 l 0.093,5.013 h -25.1124 c -2.2405,0 -4.1698,-0.86841 -5.7879,-2.60522 -1.5559,-1.80627 -2.3339,-3.95992 -2.3339,-6.46093 V 14.28805 c 0,-2.50097 0.778,-4.61988 2.3339,-6.35673 1.6181,-1.80625 3.5474,-2.70938 5.7879,-2.70942 h 17.1773 c 2.2405,4e-5 4.1387,0.90317 5.6946,2.70942 1.6181,1.73685 2.4272,3.85576 2.4272,6.35673 v 14.07916"/></svg> -------------------------------------------------------------------------------- /docs/caveats.md: -------------------------------------------------------------------------------- 1 | # Caveats 2 | 3 | Emulating scrollbars with JavaScript is always a controversial issue. On the one hand, it provides complete control of scrollbars. On the other hand, however, it degrades user experience because native behavior is unmatchable. As the author of this plugin, I don't really want you to use it unless you are sure about what you are doing. 4 | 5 | If you just want to customize your scrollbars, you can try something like [OverlayScrollbars](https://github.com/KingSora/OverlayScrollbars) which follows the native scrolling. 6 | 7 | ## Native Behavior is Unmatchable 8 | 9 | Although this plugin tries to emulate the scrolling experience as close to the native one as possible, it still behaves weirdly especially with trackpads or touch screens, as the scrolling delta will be interpolated/smoothened twice: by the native inputs and by this plugin. 10 | 11 | Keep in mind that **native scrollbars are always the best ones**. 12 | 13 | ## Performance Issues 14 | 15 | Back in the days that this plugin was created, [the native scrolling was quite slow](https://www.html5rocks.com/en/tutorials/speed/scrolling/) notably on touch devices. Therefore, I wrote this plugin using `translate3d` to improve scrolling performance. Now that modern browsers have done a lot improving native scrolling performance, I don't think you will need this one anymore. What's worse, as the scrollable area grows, this plugin will consume a large amount of GPU resources, resulting in jittery scrolling. 16 | 17 | ## Incompatible with Pointer Event API 18 | 19 | This plugin is calling `event.preventDefault()` on `touchmove` events to prevent the native scrolling. However, this breaks pointer event streams and gives some [unexpected consequences](https://github.com/idiotWu/smooth-scrollbar/issues/111#issuecomment-339243256). 20 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | # Migration from 7.x 2 | 3 | > The following sections describe the major changes from 7.x to 8.x. 4 | 5 | ## Table of Contents 6 | 7 | * [Plugin System](#plugin-system) 8 | * [Standalone overscroll plugin](#standalone-overscroll-plugin) 9 | * [Deprecated Options](#deprecated-options) 10 | * [Imcompatible Properties](#imcompatible-properties) 11 | * [Deprecated Methods](#deprecated-methods) 12 | * [Imcompatible Methods](#imcompatible-methods) 13 | 14 | ## CSS-in-JS bundle 15 | 16 | `smooth-scrollbar.css` has been removed from 8.x, you just need to import the main js module/bundle 🙌. 17 | 18 | ## Plugin System 19 | 20 | See [Plugin System](plugin.md) for details; 21 | 22 | ## Standalone overscroll plugin 23 | 24 | Overscroll effect is no longer bundle with main package. You need to import it manually: 25 | 26 | ```js 27 | import OverscrollPlugin from 'smooth-scrollbar/plugins/overscroll'; 28 | 29 | Scrollbar.use(OverscrollPlugin); 30 | 31 | Scrollbar.init(elem, { 32 | plugins: { 33 | overscroll: options | false, 34 | }, 35 | }); 36 | ``` 37 | 38 | OR 39 | 40 | ```html 41 | <script src="dist/smooth-scrollbar.js"></script> 42 | <script src="dist/plugins/overscroll.js"></script> 43 | 44 | <script> 45 | var Scrollbar = window.Scrollbar; 46 | 47 | Scrollbar.use(window.OverscrollPlugin) 48 | 49 | Scrollbar.init(elem, { 50 | plugins: { 51 | overscroll: options | false, 52 | }, 53 | }); 54 | </script> 55 | ``` 56 | 57 | ## Deprecated Options 58 | 59 | The following options have been removed from 8.x. 60 | 61 | ### `options.speed` 62 | 63 | Reason: can be implemented with `ScrollPlugin`: 64 | 65 | ```js 66 | // 7.x speed scale 67 | options.speed = 10; 68 | 69 | // equivalent in 8.x 70 | class ScalePlugin { 71 | static pluginName = 'scale'; 72 | 73 | static defaultOptions = { 74 | speed: 1, 75 | }; 76 | 77 | transformDelta(delta) { 78 | const { speed } = this.options; 79 | 80 | return { 81 | x: delta.x * speed, 82 | y: delta.y * speed, 83 | }; 84 | } 85 | } 86 | 87 | Scrollbar.use(ScalePlugin); 88 | 89 | Scrollbar.init(elem, { 90 | plugins: { 91 | scale: { 92 | speed: 10, 93 | }, 94 | }, 95 | }); 96 | ``` 97 | 98 | ### `options.syncCallbacks` 99 | 100 | Reason: scrolling listeners will always be invoked synchronously. 101 | 102 | ```js 103 | // 7.x asynchronous listener: 104 | options.syncCallbacks = false; 105 | 106 | // equivalent in 8.x 107 | scrollbar.addListener(() => { 108 | requestAnimationFrame(() => { 109 | // do something 110 | }); 111 | }); 112 | ``` 113 | 114 | ### `options.overscrollEffect`, `options.overscrollEffectColor`, `options.overscrollDamping` 115 | 116 | Reason: use [overscroll plugin](overscroll.md). 117 | 118 | ## Imcompatible Properties 119 | 120 | ### `scrollbar.target` 121 | 122 | Removed in 8.x. 123 | 124 | ## Deprecated Methods 125 | 126 | ### `scrollbar.infiniteScroll()` 127 | 128 | Reason: can be implemented with `ScrollbarPlugin`. 129 | 130 | ### `scrollbar.stop()` 131 | 132 | Reason: use `scrollbar.setMomentum(0, 0)`. 133 | 134 | ### `scrollbar.registerEvents()`, `scrollbar.unregisterEvents()` 135 | 136 | Reason: could be implemented via plugin system: 137 | 138 | ```js 139 | import Scrollbar, { ScrollbarPlugin } from 'smooth-scrollbar'; 140 | 141 | class FilterEventPlugin extends ScrollbarPlugin { 142 | static pluginName = 'filterEvent'; 143 | 144 | static defaultOptions = { 145 | blacklist: [], 146 | }; 147 | 148 | transformDelta(delta, fromEvent) { 149 | if (this.shouldBlockEvent(fromEvent)) { 150 | return { x: 0, y: 0 }; 151 | } 152 | 153 | return delta; 154 | } 155 | 156 | shouldBlockEvent(fromEvent) { 157 | return this.options.blacklist.some(rule => fromEvent.type.match(rule)); 158 | } 159 | } 160 | 161 | Scrollbar.use(FilterEventPlugin); 162 | 163 | const scrollbar = Scrollbar.init(elem); 164 | 165 | // block events 166 | // 8.0.x 167 | scrollbar.options.plugins.filterEvent.blacklist = [/wheel/, /touch/]; 168 | 169 | // 8.1.x 170 | scrollbar.updatePluginOptions('filterEvent', { 171 | blacklist: [/wheel/, /touch/], 172 | }); 173 | ``` 174 | 175 | ## Imcompatible Methods 176 | 177 | ### `scrollbar.setPosition()` 178 | 179 | 7.x: `scrollbar.setPosition(x, y, withoutCallbacks)` 180 | 181 | 8.x: `scrollbar.setPosition(x, y, options)` 182 | 183 | ```js 184 | // 7.x 185 | scrollbar.setPosition(0, 0, true); 186 | 187 | // equivalent in 8.x 188 | scrollbar.setPosition(0, 0, { 189 | withoutCallbacks: true, 190 | }); 191 | ``` 192 | 193 | ### `scrollbar.scrollTo()` 194 | 195 | 7.x: `scrollbar.scrollTo(x, y, duration, callback)` 196 | 197 | 8.x: `scrollbar.scrollTo(x, y, duration, options)` 198 | 199 | ```js 200 | // 7.x 201 | scrollbar.scrollTo(0, 0, 300, cb); 202 | 203 | // equivalent in 8.x 204 | scrollbar.scrollTo(0, 0, 300, { 205 | callback: cb, 206 | }); 207 | ``` 208 | -------------------------------------------------------------------------------- /docs/overscroll.md: -------------------------------------------------------------------------------- 1 | # Overscroll Plugin 2 | 3 | Overscroll plugin provides the macOS style overscroll bouncing effect and Android style glow effect. 4 | 5 | ## Usage 6 | 7 | ```js 8 | import OverscrollPlugin from 'smooth-scrollbar/plugins/overscroll'; 9 | 10 | Scrollbar.use(OverscrollPlugin); 11 | 12 | Scrollbar.init(elem, { 13 | plugins: { 14 | overscroll: options | false, 15 | }, 16 | }); 17 | ``` 18 | 19 | OR 20 | 21 | ```html 22 | <script src="dist/smooth-scrollbar.js"></script> 23 | <script src="dist/plugins/overscroll.js"></script> 24 | 25 | <script> 26 | var Scrollbar = window.Scrollbar; 27 | 28 | Scrollbar.use(window.OverscrollPlugin) 29 | 30 | Scrollbar.init(elem, { 31 | plugins: { 32 | overscroll: options | false, 33 | }, 34 | }); 35 | </script> 36 | ``` 37 | 38 | 39 | ## Available Options 40 | 41 | | parameter | type | default | description | 42 | | :--------: | :--: | :-----: | :---------- | 43 | | effect | `'bounce'` | `'glow'` | `'bounce'` | Overscroll effect, `'bounce'` for iOS style effect and `'glow'` for Android style effect.| 44 | | damping | `number` | `0.2` | Momentum reduction damping factor, a float value between `(0, 1)`. The lower the value is, the more smooth the overscrolling will be (also the more paint frames). | 45 | | maxOverscroll | `number` | `150` | Max-allowed overscroll distance. | 46 | | glowColor | `string` | `'#87ceeb'` | Canvas paint color for `'glow'` effect. | 47 | | onScroll | `function` | `null` | See details below. **This option is available since `8.2.0`** | 48 | 49 | ### options.onScroll 50 | 51 | ```ts 52 | onScroll(this: OverscrollPlugin, position: Position): void 53 | 54 | type Position = { 55 | x: number, 56 | y: number, 57 | }; 58 | ``` 59 | 60 | You can listen to overscroll events by setting `options.onScroll`: 61 | 62 | ```js 63 | { 64 | plugins: { 65 | overscroll: { 66 | onScroll(position) { 67 | console.log(posision); // > { x: 12, y: 34 } 68 | } 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | The `position` parameter is a x,y coordinate that indicates current overscroll position: 75 | 76 | ``` 77 | * MAX stands for options.maxOverscroll 78 | 79 | y: [-MAX, 0] 80 | ↑ 81 | +--------------+ 82 | | scrollable | 83 | x: [-MAX, 0] ← | + | → x: [0, MAX] 84 | | area | 85 | +--------------+ 86 | ↓ 87 | y: [0, MAX] 88 | ``` 89 | 90 | ## How to disable this plugin 91 | 92 | Simply set `plugins.overscroll=false` when initializing scrollbars: 93 | 94 | ```js 95 | Scrollbar.init(elem, { 96 | plugins: { 97 | // overscroll plugin will NEVER be constructed on this scrollbar! 98 | overscroll: false, 99 | }, 100 | }); 101 | ``` 102 | 103 | ## Online Demo 104 | 105 | [http://idiotwu.github.io/smooth-scrollbar/](http://idiotwu.github.io/smooth-scrollbar/) 106 | -------------------------------------------------------------------------------- /docs/plugin.md: -------------------------------------------------------------------------------- 1 | # Plugin System 2 | 3 | > This is the API documentation for `smooth-scrollbar@8.x`, check [here](https://github.com/idiotWu/smooth-scrollbar/tree/7.x) for the docs of version 7.x. 4 | 5 | > Looking for migration guides? See [migration guide](migration.md) for details. 6 | 7 | The most exciting feature in v8 is the plugin system💥. The following section explains the lifecycle of a scrollbar the mechanism inside plugins. 8 | 9 | ## Table of Contents 10 | - [The Scrollbar Lifecycle](#the-scrollbar-lifecycle) 11 | - [Plugin System](#plugin-system) 12 | - [onInit()](#oninit) 13 | - [onUpdate()](#onupdate) 14 | - [transformDelta()](#transformdelta) 15 | - [onRender()](#onrender) 16 | - [onDestroy()](#ondestroy) 17 | - [Plugin Options](#plugin-options) 18 | - [Update Plugin Options](#update-plugin-options) 19 | - [Disable Specific Plugins](#disable-specific-plugins) 20 | - [Plugin Order](#plugin-order) 21 | - [Example: invert delta](#example-invert-delta) 22 | 23 | ## The Scrollbar Lifecycle 24 | 25 | The following animation demonstrates the lifecycle of a scrollbar instance: 26 | 27 |  28 | 29 | 1. a DOM event called, and 30 | 2. the event wants to change the momentum of scrollbar, 31 | 2. delta values are sent to `transformDelta` hooks, 32 | 3. transformed delta values are applied to scrollbar, and caused scrolling position to change, 33 | 4. new position rendered, sends the remain momentum to `onRender` hooks. 34 | 35 | 36 | ## Plugin System 37 | 38 | Typings overview: 39 | 40 | ```ts 41 | type Data2d = { 42 | x: number, 43 | y: number, 44 | } 45 | 46 | abstract class ScrollbarPlugin { 47 | static pluginName: string; 48 | static defaultOptions: object; 49 | 50 | readonly scrollbar: Scrollbar; 51 | readonly options: any; 52 | 53 | onInit(): void; 54 | 55 | onUpdate(): void; 56 | 57 | transformDelta(delta: Data2d, fromEvent: any): Data2d; 58 | 59 | onRender(remainMomentum: Data2d): void; 60 | 61 | onDestroy(): void; 62 | } 63 | ``` 64 | 65 | `ScrollbarPlugin` is an abstract class so you can't use it directly with `Scrollbar`. Normally you would subclass it with at least a `pluginName` property: 66 | 67 | ```js 68 | import { ScrollbarPlugin } from 'smooth-scrollbar'; 69 | 70 | class MyPlugin extends ScrollbarPlugin { 71 | static pluginName = 'myPlugin'; 72 | } 73 | ``` 74 | 75 | `pluginName` property will be used to obtain plugin options later. 76 | 77 | Each plugin has several hooks that bring you into the scrollbar lifecycle. 78 | 79 | ### onInit() 80 | 81 | ```ts 82 | class MyPlugin extends ScrollbarPlugin { 83 | static pluginName = 'myPlugin'; 84 | 85 | onInit() { 86 | console.log('hello world!'); 87 | 88 | this._mount(); 89 | } 90 | } 91 | ``` 92 | 93 | `onInit()` is invoked right **after** a scrollbar instance is constructed. You can do some initialization here. 94 | 95 | ### onUpdate() 96 | 97 | ```ts 98 | class MyPlugin extends ScrollbarPlugin { 99 | static pluginName = 'myPlugin'; 100 | 101 | onUpdate() { 102 | console.log('scrollbar updated'); 103 | 104 | this._update(); 105 | } 106 | } 107 | ``` 108 | 109 | `onUpdate()` is invoked **after** scrollbar is updated (ie `scrollbar.update()` method called). It may be a good time to update your plugin itself :). 110 | 111 | ### transformDelta() 112 | 113 | ```ts 114 | type Delta = { 115 | x: number, 116 | y: number, 117 | }; 118 | 119 | class MyPlugin extends ScrollbarPlugin { 120 | static pluginName = 'myPlugin'; 121 | 122 | transformDelta(delta: Delta, fromEvent: Event): Delta { 123 | return { 124 | x: delta.x * 2, 125 | y: delta.y * 2, 126 | }; 127 | } 128 | } 129 | ``` 130 | 131 | `transformDelta()` is the most powerful method in plugin system. Let's say every scrolling is caused by a DOM event. Whenever an event called, it will update the momentum of scrollbar by a `Delta`. 132 | 133 | So this hook will be invoked **immediately after DOM event occurs, and before the final `Delta` is applied to scrollbar**. `transformDelta()` offers a possibility to break the default mechanism so you can almost do any thing from simple delta scaling to overscroll effect! And all you need is to analyze the delta value then return a new delta to the lifecycle. 134 | 135 | ### onRender() 136 | 137 | ```ts 138 | type Momentum = { 139 | x: number, 140 | y: number, 141 | }; 142 | 143 | class MyPlugin extends ScrollbarPlugin { 144 | static pluginName = 'myPlugin'; 145 | 146 | onRender(remainMomentum: Momentum) { 147 | this._remain = { 148 | ...remainMomentum, 149 | }; 150 | 151 | this.scrollbar.setMomentum(0, 0); 152 | this._render(); 153 | } 154 | } 155 | ``` 156 | 157 | `onRender()` hook is invoked everytime render loop runs. You will be informed of the remain momentum of the scrollbar. Through the `scrollbar.addMomentum()` and `scrollbar.setMomentum()` method, this is the last chance to modify the momentum in a lifecycle. 158 | 159 | Scrollbar is render in a `requestAnimationFrame` loop, so **DO NOT** perform any heavy operation in this hook, otherwise you might block the whole UI of your poor browser. 160 | 161 | ### onDestroy() 162 | 163 | ```ts 164 | class MyPlugin extends ScrollbarPlugin { 165 | static pluginName = 'myPlugin'; 166 | 167 | onDestroy() { 168 | console.log('goodbye'); 169 | this._unmount(); 170 | } 171 | } 172 | ``` 173 | 174 | As the name shows, `onDestroy()` will be called **after** a scrollbar instance is destroyed, so you should do some cleaning jobs here. 175 | 176 | ## Plugin Options 177 | 178 | Your lovely `pluginName` property is the only tunnel that connects your plugin and users. For example, suppose that we have a plugin named `meow`: 179 | 180 | ```ts 181 | class MeowPlugin extends ScrollbarPlugin { 182 | static pluginName = 'meow'; 183 | 184 | onInit() { 185 | console.log('meow', this.options); 186 | } 187 | } 188 | ``` 189 | 190 | When someone wants to use the `MeowPlugin`, he or she needs: 191 | 192 | ```ts 193 | import Scrollbar from 'smooth-scrollbar'; 194 | import MeowPlugin from 'meow-plugin'; 195 | 196 | Scrollbar.use(MeowPlugin); 197 | 198 | Scrollbar.init(elem, { 199 | plugins: { 200 | meow: { 201 | age: '10m', 202 | }, 203 | }, 204 | }); 205 | 206 | // > 'meow' { age: '10m' } 207 | ``` 208 | 209 | You can provide default options through `defaultOptions` property: 210 | 211 | ```ts 212 | class MeowPlugin extends ScrollbarPlugin { 213 | static pluginName = 'meow'; 214 | 215 | static defaultOptions = { 216 | age: '0d', 217 | }; 218 | } 219 | ``` 220 | 221 | ### Update Plugin Options 222 | 223 | Plugin options is a read-only object, so you should avoid the following operation: 224 | 225 | ```ts 226 | // ❌ wrong 227 | scrollbar.options.plugins = { 228 | overscroll: { 229 | effect: 'glow', 230 | }, 231 | }; 232 | ``` 233 | 234 | Instead, you can update plugin options through `scrollbar.updatePluginOptions` API (available since `8.1.0`): 235 | 236 | ```ts 237 | scrollbar.updatePluginOptions('overscroll', { 238 | effect: 'glow', 239 | }); 240 | ``` 241 | 242 | ## Disable Specific Plugins 243 | 244 | If you want to disable the plugin, simply set `plugin[pluginName]=false`: 245 | 246 | ```ts 247 | Scrollbar.init(devil, { 248 | plugins: { 249 | meow: false, 250 | }, 251 | }); 252 | 253 | // MeowPlugin will NEVER be constructed on this scrollbar instance! 254 | ``` 255 | 256 | ## Plugin Order 257 | 258 | Scrollbar plugins are invoked from left to right (FIFO): 259 | 260 | ```ts 261 | Scrollbar.use(PluginA, PluginB, PluginC); 262 | 263 | // hooks executing order: 264 | // PluginA.transformDelta() -> PluginA.transformDelta() -> PluginC.transformDelta() 265 | ``` 266 | 267 | Let's say we have multiple plugins: 268 | 269 | ```ts 270 | class ScaleDeltaPlugin extends ScrollbarPlugin { 271 | static pluginName = 'scaleDelta'; 272 | 273 | transformDelta(delta, fromEvent) { 274 | return { 275 | x: delta.x * 2, 276 | y: delta.y * 2, 277 | } 278 | } 279 | } 280 | 281 | class NoopPlugin extends ScrollbarPlugin { 282 | static pluginName = 'noop'; 283 | 284 | transformDelta(delta, fromEvent) { 285 | console.log(delta); 286 | return { ...delta }; 287 | } 288 | } 289 | 290 | ``` 291 | 292 | Now let's apply `delta = { x: 100, y: 100 }` to the scrollbar: 293 | 294 | ```ts 295 | Scrollbar.use(ScaleDeltaPlugin, NoopPlugin); 296 | 297 | // apply delta... 298 | 299 | // > { x: 200, y: 200 } 300 | ``` 301 | 302 | Delta is first transformed by `ScaleDeltaPlugin` and then the `NoopPlugin`. What if we change the order? 303 | 304 | ```ts 305 | Scrollbar.use(NoopPlugin, ScaleDeltaPlugin); 306 | 307 | // apply delta... 308 | 309 | // > { x: 100, y: 100 } 310 | ``` 311 | 312 | Ah, `NoopPlugin` is invoked first. 313 | 314 | As the above section demonstrated, if you are using multiple plugins, be care of the loading order! Usually plugins like `OverscrollPlugin` that will change the layout are supposed to be the last man: 315 | 316 | ```ts 317 | Scrollbar.use(PluginA, PluginB, PluginC, ..., OverscrollPlugin); 318 | ``` 319 | 320 | ### Example: invert delta 321 | 322 | This plugin allows you to invert delta for particular events. 323 | 324 | ```ts 325 | import Scrollbar, { ScrollbarPlugin } from 'smooth-scrollbar'; 326 | 327 | class InvertDeltaPlugin extends ScrollbarPlugin { 328 | static pluginName = 'invertDelta'; 329 | 330 | static defaultOptions = { 331 | events: [], 332 | }; 333 | 334 | transformDelta(delta, fromEvent) { 335 | if (this.shouldInvertDelta(fromEvent)) { 336 | return { 337 | x: delta.y, 338 | y: delta.x, 339 | }; 340 | } 341 | 342 | return delta; 343 | } 344 | 345 | shouldInvertDelta(fromEvent) { 346 | return this.options.events.some(rule => fromEvent.type.match(rule)); 347 | } 348 | } 349 | 350 | Scrollbar.use(InvertDeltaPlugin); 351 | 352 | const scrollbar = Scrollbar.init(elem, { 353 | plugins: { 354 | invertDelta: { 355 | events: [/wheel/], 356 | }, 357 | }, 358 | }); 359 | ``` 360 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smooth-scrollbar", 3 | "version": "8.8.4", 4 | "description": "Customize scrollbar in modern browsers with smooth scrolling experience.", 5 | "main": "dist/smooth-scrollbar.js", 6 | "jsnext:main": "index.js", 7 | "module": "index.js", 8 | "types": "index.d.ts", 9 | "scripts": { 10 | "start": "node ./scripts/serve.js", 11 | "lint": "tslint --type-check -p . -t stylish {src,demo}/**/*.ts", 12 | "clean:compile": "rimraf ./build", 13 | "clean:dist": "rimraf ./dist", 14 | "clean": "npm-run-all clean:compile clean:dist", 15 | "copy:src": "cpx './src/**' ./build/src", 16 | "copy:conf": "cpx ./tsconfig.json ./build", 17 | "copy": "npm-run-all copy:src copy:conf", 18 | "precompile": "npm-run-all clean:compile copy", 19 | "compile": "tsc -p ./build", 20 | "postcompile": "rimraf ./build/tsconfig.json", 21 | "prebundle": "npm-run-all clean:dist", 22 | "bundle:main": "webpack --config ./scripts/webpack.prod.js", 23 | "bundle:plugin": "webpack --config scripts/webpack.prod.plugins.js", 24 | "bundle": "npm-run-all bundle:main bundle:plugin", 25 | "preghpages": "rimraf ./ghpages", 26 | "ghpages": "webpack --config ./scripts/webpack.ghpages.js", 27 | "postghpages": "cpx 'demo/{images/*,index.html}' ghpages -v", 28 | "deploy": "./scripts/deploy.sh", 29 | "test": "npm run lint && tsc --noEmit", 30 | "release": "node ./scripts/release.js" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/idiotWu/smooth-scrollbar.git" 35 | }, 36 | "keywords": [ 37 | "scrollbar", 38 | "customize", 39 | "acceleration", 40 | "performance" 41 | ], 42 | "author": "Dolphin Wood <dolphin.w.e@gmail.com>", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/idiotWu/smooth-scrollbar/issues" 46 | }, 47 | "homepage": "https://github.com/idiotWu/smooth-scrollbar#readme", 48 | "devDependencies": { 49 | "@types/dat-gui": "^0.6.3", 50 | "@types/prismjs": "^1.16.0", 51 | "autoprefixer": "^7.2.6", 52 | "chalk": "^2.4.2", 53 | "circular-dependency-plugin": "^5.2.0", 54 | "cpx": "^1.5.0", 55 | "css-loader": "^0.28.11", 56 | "dat-gui": "^0.5.0", 57 | "execa": "^0.8.0", 58 | "inquirer": "^6.5.2", 59 | "listr": "^0.12.0", 60 | "npm-run-all": "^4.1.5", 61 | "postcss-loader": "^2.1.6", 62 | "prismjs": "^1.17.1", 63 | "rimraf": "^2.7.1", 64 | "rollup": "^0.50.1", 65 | "rollup-plugin-typescript": "^0.8.1", 66 | "semver": "^5.7.1", 67 | "style-loader": "^0.19.1", 68 | "stylus": "^0.54.7", 69 | "stylus-loader": "^3.0.2", 70 | "ts-loader": "^4.5.0", 71 | "tslint": "^5.20.0", 72 | "tslint-config-standard": "^6.0.1", 73 | "tslint-loader": "^3.6.0", 74 | "typescript": "^3.6.3", 75 | "uglifyjs-webpack-plugin": "^1.3.0", 76 | "webpack": "^4.40.2", 77 | "webpack-cli": "^3.3.8", 78 | "webpack-dev-server": "^3.11.2", 79 | "webpack-merge": "^4.2.2" 80 | }, 81 | "dependencies": { 82 | "core-js": "^3.6.4", 83 | "tslib": "^1.10.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scripts/public-url.js: -------------------------------------------------------------------------------- 1 | const ip = require('ip'); 2 | const child_process = require('child_process'); 3 | 4 | function publicUrl(port) { 5 | return process.env.GITPOD_WORKSPACE_ID ? 6 | child_process.execSync(`gp url ${port}`).toString() 7 | : `http://${ip.address()}:${port}`; 8 | } 9 | 10 | module.exports = publicUrl; 11 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const cpx = require('cpx'); 3 | const path = require('path'); 4 | const execa = require('execa'); 5 | const Listr = require('listr'); 6 | const chalk = require('chalk'); 7 | const semver = require('semver'); 8 | const inquirer = require('inquirer'); 9 | 10 | const pkg = require('../package.json'); 11 | const bowerPkg = require('../bower.json'); 12 | 13 | const joinRoot = path.join.bind(path, __dirname, '..'); 14 | 15 | const BUILD_DIR = joinRoot('build'); 16 | 17 | function checkBranch() { 18 | return execa.shell('git rev-parse --abbrev-ref HEAD').then((result) => { 19 | if (result.stdout !== 'master') { 20 | throw new Error(chalk.bold.red('Please run release script on master branch.')); 21 | } 22 | }); 23 | } 24 | 25 | function compareWithDevelop() { 26 | return execa.shell('git rev-list --count master..develop').then((result) => { 27 | if (result.stdout !== '0') { 28 | throw new Error(chalk.bold.red('master branch is not up-to-date with develop branch')); 29 | } 30 | }); 31 | } 32 | 33 | function checkWorkingTree() { 34 | return execa.shell('git status -s').then((result) => { 35 | if (result.stdout !== '') { 36 | throw new Error(chalk.bold.red('Please commit local changes before releasing.')); 37 | } 38 | }); 39 | } 40 | 41 | function prompt() { 42 | const questions = [{ 43 | type: 'list', 44 | name: 'version', 45 | message: 'Which type of release is this?', 46 | choices: ['patch', 'minor', 'major', 'beta'].map((type) => { 47 | const version = type === 'beta' ? 48 | semver.inc(pkg.version, 'prerelease', 'beta') : 49 | semver.inc(pkg.version, type); 50 | 51 | return { 52 | name: `${type} ${chalk.dim.magenta(version)}`, 53 | value: version, 54 | }; 55 | }).concat([ 56 | new inquirer.Separator(), 57 | { 58 | name: 'others', 59 | value: null, 60 | }, 61 | ]), 62 | }, { 63 | type: 'input', 64 | name: 'version', 65 | message: `Please enter the version (current: ${pkg.version}):`, 66 | when: answers => !answers.version, 67 | validate(input) { 68 | if (!semver.valid(input)) { 69 | return 'Please enter a valid semver like `a.b.c`.'; 70 | } 71 | 72 | if (!semver.gt(input, pkg.version)) { 73 | return `New version must be greater than ${pkg.version}.`; 74 | } 75 | 76 | return true; 77 | }, 78 | }, { 79 | type: 'confirm', 80 | name: 'confirm', 81 | default: false, 82 | message: answers => `Releasing version:${answers.version} - are you sure?`, 83 | }]; 84 | 85 | return inquirer.prompt(questions); 86 | } 87 | 88 | function runTask(options) { 89 | if (!options.confirm) { 90 | process.exit(0); 91 | } 92 | 93 | const tasks = new Listr([{ 94 | title: 'Create bundle', 95 | task: () => execa.shell('npm run bundle', { 96 | env: { 97 | SCROLLBAR_VERSION: options.version, 98 | }, 99 | }), 100 | }, { 101 | title: 'Compile TypeScript', 102 | task: async () => { 103 | await execa.shell('npm run compile'); 104 | 105 | const entry = `${BUILD_DIR}/index.js`; 106 | const content = fs.readFileSync(entry, 'utf8'); 107 | 108 | fs.writeFileSync(entry, 109 | content.replace('__SCROLLBAR_VERSION__', JSON.stringify(options.version)), 110 | ); 111 | }, 112 | }, { 113 | title: `Bump Bower version: ${pkg.version} -> ${options.version}`, 114 | task: () => { 115 | bowerPkg.version = options.version; 116 | 117 | fs.writeFileSync(joinRoot('bower.json'), JSON.stringify(bowerPkg, null, 2)); 118 | }, 119 | }, { 120 | title: 'Commit changes', 121 | task: async () => { 122 | await execa.shell('git add --all'); 123 | await execa.shell(`git commit -m "[build] ${options.version}"`); 124 | } 125 | }, { 126 | title: `Bump NPM version: ${pkg.version} -> ${options.version}`, 127 | task: () => execa.shell(`npm version ${options.version}`), 128 | }, { 129 | title: 'Copy files to working directory', 130 | task: () => { 131 | cpx.copySync(joinRoot('dist/**'), `${BUILD_DIR}/dist`); 132 | cpx.copySync(joinRoot('package.json'), BUILD_DIR); 133 | cpx.copySync(joinRoot('README.md'), BUILD_DIR); 134 | cpx.copySync(joinRoot('CHANGELOG.md'), BUILD_DIR); 135 | cpx.copySync(joinRoot('LICENSE'), BUILD_DIR); 136 | }, 137 | }, { 138 | title: `Publish ${options.version}`, 139 | task: () => { 140 | return semver.prerelease(options.version) ? 141 | execa.shell(`cd ${BUILD_DIR} && npm publish --tag beta`) : 142 | execa.shell(`cd ${BUILD_DIR} && npm publish`); 143 | }, 144 | }, { 145 | title: 'Push to GitHub', 146 | task: async () => { 147 | await execa.shell('git push'); 148 | await execa.shell('git push --tags'); 149 | }, 150 | }]); 151 | 152 | return tasks.run(); 153 | } 154 | 155 | checkBranch() 156 | .then(checkWorkingTree) 157 | .then(compareWithDevelop) 158 | .then(prompt) 159 | .then(runTask) 160 | .catch((err) => { 161 | console.error(err.message); 162 | process.exit(1); 163 | }); 164 | -------------------------------------------------------------------------------- /scripts/serve.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const Server = require('webpack-dev-server'); 5 | const config = require('./webpack.dev'); 6 | const publicUrl = require('./public-url'); 7 | 8 | new Server(webpack(config), { 9 | disableHostCheck: true, 10 | contentBase: path.join(__dirname, '..', 'demo'), 11 | publicPath: config.output.publicPath, 12 | public: publicUrl(3000), 13 | stats: { 14 | modules: false, 15 | }, 16 | }).listen(3000, '0.0.0.0', (err) => { 17 | if (err) { 18 | console.log(err); 19 | } 20 | 21 | console.log('Listening at http://localhost:3000'); 22 | console.log(`Remote access: ${publicUrl(3000)}`); 23 | }); 24 | -------------------------------------------------------------------------------- /scripts/webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CircularDependencyPlugin = require('circular-dependency-plugin'); 4 | 5 | const joinRoot = path.join.bind(path, __dirname, '..'); 6 | 7 | module.exports = { 8 | resolve: { 9 | extensions: ['.js', '.ts', '.css', '.styl'], 10 | alias: { 11 | 'smooth-scrollbar': joinRoot('src'), 12 | }, 13 | }, 14 | module: { 15 | rules: [{ 16 | test: /\.ts$/, 17 | use: [{ 18 | loader: 'ts-loader', 19 | options: { 20 | compilerOptions: { 21 | declaration: false, 22 | }, 23 | }, 24 | }], 25 | include: [ 26 | joinRoot('src'), 27 | joinRoot('demo'), 28 | ], 29 | }], 30 | }, 31 | plugins: [ 32 | new webpack.DefinePlugin({ 33 | __SCROLLBAR_VERSION__: JSON.stringify( 34 | process.env.SCROLLBAR_VERSION || require('../package.json').version, 35 | ), 36 | }), 37 | new CircularDependencyPlugin({ 38 | exclude: /node_modules/, 39 | failOnError: true, 40 | }), 41 | ], 42 | stats: { 43 | modules: false, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /scripts/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseConfig = require('./webpack.base'); 4 | const publicUrl = require('./public-url'); 5 | 6 | const joinRoot = path.join.bind(path, __dirname, '..'); 7 | 8 | module.exports = merge(baseConfig, { 9 | mode: 'development', 10 | devtool: 'cheap-module-source-map', 11 | entry: [ 12 | `webpack-dev-server/client?${publicUrl(3000)}`, 13 | joinRoot('demo/scripts/index.ts'), 14 | ], 15 | output: { 16 | path: joinRoot('.tmp'), 17 | filename: 'app.js', 18 | publicPath: '/', 19 | }, 20 | module: { 21 | rules: [{ 22 | test: /\.ts$/, 23 | enforce: 'pre', 24 | use: [{ 25 | loader: 'tslint-loader', 26 | options: { 27 | // type check is slow, see 28 | // https://github.com/wbuchwalter/tslint-loader/issues/76 29 | // typeCheck: true, 30 | formatter: 'stylish', 31 | }, 32 | }], 33 | include: [ 34 | joinRoot('src'), 35 | joinRoot('demo'), 36 | ], 37 | }, { 38 | test: /\.css$/, 39 | use: [ 40 | 'style-loader', 41 | 'css-loader', 42 | ], 43 | }, { 44 | test: /\.styl$/, 45 | use: [ 46 | 'style-loader', 47 | 'css-loader', 48 | { 49 | loader: 'postcss-loader', 50 | options: { 51 | sourceMap: true, 52 | plugins: () => [ require('autoprefixer') ], 53 | }, 54 | }, 55 | 'stylus-loader', 56 | ], 57 | include: [ 58 | joinRoot('demo'), 59 | ], 60 | }], 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /scripts/webpack.ghpages.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | const baseConfig = require('./webpack.base'); 7 | 8 | const joinRoot = path.join.bind(path, __dirname, '..'); 9 | 10 | module.exports = merge(baseConfig, { 11 | mode: 'production', 12 | entry: [ 13 | joinRoot('demo/scripts/index.ts'), 14 | ], 15 | output: { 16 | path: joinRoot('ghpages'), 17 | filename: 'app.js', 18 | publicPath: '/', 19 | }, 20 | module: { 21 | rules: [{ 22 | test: /\.css$/, 23 | use: [ 24 | 'style-loader', 25 | 'css-loader', 26 | ], 27 | }, { 28 | test: /\.styl$/, 29 | use: [ 30 | 'style-loader', 31 | 'css-loader', 32 | { 33 | loader: 'postcss-loader', 34 | options: { 35 | sourceMap: false, 36 | plugins: () => [ require('autoprefixer') ], 37 | }, 38 | }, 39 | 'stylus-loader', 40 | ], 41 | include: [ 42 | joinRoot('demo'), 43 | ], 44 | }], 45 | }, 46 | plugins: [ 47 | new UglifyJSPlugin(), 48 | new webpack.optimize.ModuleConcatenationPlugin(), 49 | ], 50 | }); 51 | -------------------------------------------------------------------------------- /scripts/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | const baseConfig = require('./webpack.base'); 7 | 8 | const joinRoot = path.join.bind(path, __dirname, '..'); 9 | 10 | module.exports = merge(baseConfig, { 11 | mode: 'production', 12 | entry: [ 13 | joinRoot('src/index.ts'), 14 | ], 15 | output: { 16 | path: joinRoot('dist/'), 17 | filename: 'smooth-scrollbar.js', 18 | library: 'Scrollbar', 19 | libraryTarget: 'umd', 20 | libraryExport: 'default', 21 | globalObject: 'this' 22 | }, 23 | plugins: [ 24 | new UglifyJSPlugin(), 25 | new webpack.optimize.ModuleConcatenationPlugin(), 26 | ], 27 | }); 28 | -------------------------------------------------------------------------------- /scripts/webpack.prod.plugins.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | const baseConfig = require('./webpack.base'); 7 | 8 | const joinRoot = path.join.bind(path, __dirname, '..'); 9 | 10 | module.exports = merge(baseConfig, { 11 | mode: 'production', 12 | entry: [ 13 | joinRoot('src/plugins/overscroll/index.ts'), 14 | ], 15 | output: { 16 | path: joinRoot('dist/plugins/'), 17 | filename: 'overscroll.js', 18 | library: 'OverscrollPlugin', 19 | libraryTarget: 'umd', 20 | libraryExport: 'default', 21 | globalObject: 'this' 22 | }, 23 | externals: { 24 | 'smooth-scrollbar': { 25 | commonjs: 'smooth-scrollbar', 26 | commonjs2: 'smooth-scrollbar', 27 | amd: 'smooth-scrollbar', 28 | root: 'Scrollbar', 29 | }, 30 | }, 31 | plugins: [ 32 | new UglifyJSPlugin(), 33 | new webpack.optimize.ModuleConcatenationPlugin(), 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /src/decorators/boolean.ts: -------------------------------------------------------------------------------- 1 | export function boolean(proto: any, key: string) { 2 | const alias = `_${key}`; 3 | 4 | Object.defineProperty(proto, key, { 5 | get() { 6 | return this[alias]; 7 | }, 8 | set(val?: boolean) { 9 | Object.defineProperty(this, alias, { 10 | value: !!val, 11 | enumerable: false, 12 | writable: true, 13 | configurable: true, 14 | }); 15 | }, 16 | enumerable: true, 17 | configurable: true, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/decorators/debounce.ts: -------------------------------------------------------------------------------- 1 | import { debounce as $debounce } from '../utils'; 2 | 3 | export function debounce(...options) { 4 | return (_proto: any, key: string, descriptor: PropertyDescriptor) => { 5 | const fn = descriptor.value; 6 | 7 | return { 8 | get() { 9 | if (!this.hasOwnProperty(key)) { 10 | Object.defineProperty(this, key, { 11 | value: $debounce(fn, ...options), 12 | }); 13 | } 14 | 15 | return this[key]; 16 | }, 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './range'; 2 | export * from './boolean'; 3 | export * from './debounce'; 4 | -------------------------------------------------------------------------------- /src/decorators/range.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../utils'; 2 | 3 | export function range(min = -Infinity, max = Infinity) { 4 | return (proto: any, key: string) => { 5 | const alias = `_${key}`; 6 | 7 | Object.defineProperty(proto, key, { 8 | get() { 9 | return this[alias]; 10 | }, 11 | set(val: number) { 12 | Object.defineProperty(this, alias, { 13 | value: clamp(val, min, max), 14 | enumerable: false, 15 | writable: true, 16 | configurable: true, 17 | }); 18 | }, 19 | enumerable: true, 20 | configurable: true, 21 | }); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keyboard'; 2 | export * from './mouse'; 3 | export * from './resize'; 4 | export * from './select'; 5 | export * from './touch'; 6 | export * from './wheel'; 7 | -------------------------------------------------------------------------------- /src/events/keyboard.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | 3 | import { 4 | eventScope, 5 | } from '../utils/'; 6 | 7 | enum KEY_CODE { 8 | TAB = 9, 9 | SPACE = 32, 10 | PAGE_UP, 11 | PAGE_DOWN, 12 | END, 13 | HOME, 14 | LEFT, 15 | UP, 16 | RIGHT, 17 | DOWN, 18 | } 19 | 20 | export function keyboardHandler(scrollbar: I.Scrollbar) { 21 | const addEvent = eventScope(scrollbar); 22 | const container = scrollbar.containerEl; 23 | 24 | addEvent(container, 'keydown', (evt: KeyboardEvent) => { 25 | const { activeElement } = document; 26 | 27 | if (activeElement !== container && !container.contains(activeElement)) { 28 | return; 29 | } 30 | 31 | if (isEditable(activeElement)) { 32 | return; 33 | } 34 | 35 | const delta = getKeyDelta(scrollbar, evt.keyCode || evt.which); 36 | 37 | if (!delta) { 38 | return; 39 | } 40 | 41 | const [x, y] = delta; 42 | 43 | scrollbar.addTransformableMomentum(x, y, evt, (willScroll) => { 44 | if (willScroll) { 45 | evt.preventDefault(); 46 | } else { 47 | scrollbar.containerEl.blur(); 48 | 49 | if (scrollbar.parent) { 50 | scrollbar.parent.containerEl.focus(); 51 | } 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | function getKeyDelta(scrollbar: I.Scrollbar, keyCode: number) { 58 | const { 59 | size, 60 | limit, 61 | offset, 62 | } = scrollbar; 63 | 64 | switch (keyCode) { 65 | case KEY_CODE.TAB: 66 | return handleTabKey(scrollbar); 67 | case KEY_CODE.SPACE: 68 | return [0, 200]; 69 | case KEY_CODE.PAGE_UP: 70 | return [0, -size.container.height + 40]; 71 | case KEY_CODE.PAGE_DOWN: 72 | return [0, size.container.height - 40]; 73 | case KEY_CODE.END: 74 | return [0, limit.y - offset.y]; 75 | case KEY_CODE.HOME: 76 | return [0, -offset.y]; 77 | case KEY_CODE.LEFT: 78 | return [-40, 0]; 79 | case KEY_CODE.UP: 80 | return [0, -40]; 81 | case KEY_CODE.RIGHT: 82 | return [40, 0]; 83 | case KEY_CODE.DOWN: 84 | return [0, 40]; 85 | default: 86 | return null; 87 | } 88 | } 89 | 90 | function handleTabKey(scrollbar: I.Scrollbar) { 91 | // handle in next frame 92 | requestAnimationFrame(() => { 93 | scrollbar.scrollIntoView(document.activeElement as HTMLElement, { 94 | offsetTop: scrollbar.size.container.height / 2, 95 | offsetLeft: scrollbar.size.container.width / 2, 96 | onlyScrollIfNeeded: true, 97 | }); 98 | }); 99 | } 100 | 101 | function isEditable(elem: any): boolean { 102 | if (elem.tagName === 'INPUT' || 103 | elem.tagName === 'SELECT' || 104 | elem.tagName === 'TEXTAREA' || 105 | elem.isContentEditable) { 106 | return !elem.disabled; 107 | } 108 | 109 | return false; 110 | } 111 | -------------------------------------------------------------------------------- /src/events/mouse.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../utils'; 2 | import * as I from '../interfaces/'; 3 | 4 | import { 5 | isOneOf, 6 | getPosition, 7 | eventScope, 8 | setStyle, 9 | } from '../utils/'; 10 | 11 | enum Direction { X, Y } 12 | 13 | export function mouseHandler(scrollbar: I.Scrollbar) { 14 | const addEvent = eventScope(scrollbar); 15 | const container = scrollbar.containerEl; 16 | const { xAxis, yAxis } = scrollbar.track; 17 | 18 | function calcMomentum( 19 | direction: Direction, 20 | clickPosition: number, 21 | ): number { 22 | const { 23 | size, 24 | limit, 25 | offset, 26 | } = scrollbar; 27 | 28 | if (direction === Direction.X) { 29 | const totalWidth = size.container.width + (xAxis.thumb.realSize - xAxis.thumb.displaySize); 30 | 31 | return clamp(clickPosition / totalWidth * size.content.width, 0, limit.x) - offset.x; 32 | } 33 | 34 | if (direction === Direction.Y) { 35 | const totalHeight = size.container.height + (yAxis.thumb.realSize - yAxis.thumb.displaySize); 36 | 37 | return clamp(clickPosition / totalHeight * size.content.height, 0, limit.y) - offset.y; 38 | } 39 | 40 | return 0; 41 | } 42 | 43 | function getTrackDirection( 44 | elem: HTMLElement, 45 | ): Direction | undefined { 46 | if (isOneOf(elem, [xAxis.element, xAxis.thumb.element])) { 47 | return Direction.X; 48 | } 49 | 50 | if (isOneOf(elem, [yAxis.element, yAxis.thumb.element])) { 51 | return Direction.Y; 52 | } 53 | 54 | return void 0; 55 | } 56 | 57 | let isMouseDown: boolean; 58 | let isMouseMoving: boolean; 59 | let startOffsetToThumb: { x: number, y: number }; 60 | let trackDirection: Direction | undefined; 61 | let containerRect: ClientRect; 62 | 63 | addEvent(container, 'click', (evt: MouseEvent) => { 64 | if (isMouseMoving || !isOneOf(evt.target, [xAxis.element, yAxis.element])) { 65 | return; 66 | } 67 | 68 | const track = evt.target as HTMLElement; 69 | const direction = getTrackDirection(track); 70 | const rect = track.getBoundingClientRect(); 71 | const clickPos = getPosition(evt); 72 | 73 | if (direction === Direction.X) { 74 | const offsetOnTrack = clickPos.x - rect.left - xAxis.thumb.displaySize / 2; 75 | scrollbar.setMomentum(calcMomentum(direction, offsetOnTrack), 0); 76 | } 77 | 78 | if (direction === Direction.Y) { 79 | const offsetOnTrack = clickPos.y - rect.top - yAxis.thumb.displaySize / 2; 80 | scrollbar.setMomentum(0, calcMomentum(direction, offsetOnTrack)); 81 | } 82 | }); 83 | 84 | addEvent(container, 'mousedown', (evt: MouseEvent) => { 85 | if (!isOneOf(evt.target, [xAxis.thumb.element, yAxis.thumb.element])) { 86 | return; 87 | } 88 | 89 | isMouseDown = true; 90 | 91 | const thumb = evt.target as HTMLElement; 92 | const cursorPos = getPosition(evt); 93 | const thumbRect = thumb.getBoundingClientRect(); 94 | 95 | trackDirection = getTrackDirection(thumb); 96 | 97 | // pointer offset to thumb 98 | startOffsetToThumb = { 99 | x: cursorPos.x - thumbRect.left, 100 | y: cursorPos.y - thumbRect.top, 101 | }; 102 | 103 | // container bounding rectangle 104 | containerRect = container.getBoundingClientRect(); 105 | 106 | // prevent selection, see: 107 | // https://github.com/idiotWu/smooth-scrollbar/issues/48 108 | setStyle(scrollbar.containerEl, { 109 | '-user-select': 'none', 110 | }); 111 | }); 112 | 113 | addEvent(window, 'mousemove', (evt) => { 114 | if (!isMouseDown) return; 115 | 116 | isMouseMoving = true; 117 | 118 | const cursorPos = getPosition(evt); 119 | 120 | if (trackDirection === Direction.X) { 121 | // get percentage of pointer position in track 122 | // then tranform to px 123 | // don't need easing 124 | const offsetOnTrack = cursorPos.x - startOffsetToThumb.x - containerRect.left; 125 | scrollbar.setMomentum(calcMomentum(trackDirection, offsetOnTrack), 0); 126 | } 127 | 128 | if (trackDirection === Direction.Y) { 129 | const offsetOnTrack = cursorPos.y - startOffsetToThumb.y - containerRect.top; 130 | scrollbar.setMomentum(0, calcMomentum(trackDirection, offsetOnTrack)); 131 | } 132 | }); 133 | 134 | addEvent(window, 'mouseup blur', () => { 135 | isMouseDown = isMouseMoving = false; 136 | 137 | setStyle(scrollbar.containerEl, { 138 | '-user-select': '', 139 | }); 140 | }); 141 | } 142 | -------------------------------------------------------------------------------- /src/events/resize.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | import { debounce } from '../utils'; 3 | 4 | import { 5 | eventScope, 6 | } from '../utils/'; 7 | 8 | export function resizeHandler(scrollbar: I.Scrollbar) { 9 | const addEvent = eventScope(scrollbar); 10 | 11 | addEvent( 12 | window, 13 | 'resize', 14 | debounce(scrollbar.update.bind(scrollbar), 300), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/events/select.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../utils'; 2 | import * as I from '../interfaces/'; 3 | 4 | import { 5 | eventScope, 6 | getPosition, 7 | } from '../utils/'; 8 | 9 | export function selectHandler(scrollbar: I.Scrollbar) { 10 | const addEvent = eventScope(scrollbar); 11 | const { containerEl, contentEl } = scrollbar; 12 | 13 | let isSelected = false; 14 | let isContextMenuOpened = false; // flag to prevent selection when context menu is opened 15 | let animationID: number; 16 | 17 | function scroll({ x, y }) { 18 | if (!x && !y) return; 19 | 20 | const { offset, limit } = scrollbar; 21 | // DISALLOW delta transformation 22 | scrollbar.setMomentum( 23 | clamp(offset.x + x, 0, limit.x) - offset.x, 24 | clamp(offset.y + y, 0, limit.y) - offset.y, 25 | ); 26 | 27 | animationID = requestAnimationFrame(() => { 28 | scroll({ x, y }); 29 | }); 30 | } 31 | 32 | addEvent(window, 'mousemove', (evt: MouseEvent) => { 33 | if (!isSelected) return; 34 | 35 | cancelAnimationFrame(animationID); 36 | 37 | const dir = calcMomentum(scrollbar, evt); 38 | 39 | scroll(dir); 40 | }); 41 | 42 | // prevent scrolling when context menu is opened 43 | // NOTE: `contextmenu` event may be fired 44 | // 1. BEFORE `selectstart`: when user right-clicks on the text content -> prevent future scrolling, 45 | // 2. AFTER `selectstart`: when user right-clicks on the blank area -> cancel current scrolling, 46 | // so we need to both set the flag and cancel current scrolling 47 | addEvent(contentEl, 'contextmenu', () => { 48 | // set the flag to prevent future scrolling 49 | isContextMenuOpened = true; 50 | 51 | // stop current scrolling 52 | cancelAnimationFrame(animationID); 53 | isSelected = false; 54 | }); 55 | 56 | // reset context menu flag on mouse down 57 | // to ensure the scrolling is allowed in the next selection 58 | addEvent(contentEl, 'mousedown', () => { 59 | isContextMenuOpened = false; 60 | }); 61 | 62 | addEvent(contentEl, 'selectstart', () => { 63 | if (isContextMenuOpened) { 64 | return; 65 | } 66 | 67 | cancelAnimationFrame(animationID); 68 | 69 | isSelected = true; 70 | }); 71 | 72 | addEvent(window, 'mouseup blur', () => { 73 | cancelAnimationFrame(animationID); 74 | 75 | isSelected = false; 76 | isContextMenuOpened = false; 77 | }); 78 | 79 | // patch for touch devices 80 | addEvent(containerEl, 'scroll', (evt: Event) => { 81 | evt.preventDefault(); 82 | containerEl.scrollTop = containerEl.scrollLeft = 0; 83 | }); 84 | } 85 | 86 | function calcMomentum( 87 | scrollbar: I.Scrollbar, 88 | evt: MouseEvent, 89 | ) { 90 | const { top, right, bottom, left } = scrollbar.bounding; 91 | const { x, y } = getPosition(evt); 92 | 93 | const res = { 94 | x: 0, 95 | y: 0, 96 | }; 97 | 98 | const padding = 20; 99 | 100 | if (x === 0 && y === 0) return res; 101 | 102 | if (x > right - padding) { 103 | res.x = (x - right + padding); 104 | } else if (x < left + padding) { 105 | res.x = (x - left - padding); 106 | } 107 | 108 | if (y > bottom - padding) { 109 | res.y = (y - bottom + padding); 110 | } else if (y < top + padding) { 111 | res.y = (y - top - padding); 112 | } 113 | 114 | res.x *= 2; 115 | res.y *= 2; 116 | 117 | return res; 118 | } 119 | -------------------------------------------------------------------------------- /src/events/touch.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | 3 | import { 4 | eventScope, 5 | TouchRecord, 6 | } from '../utils/'; 7 | 8 | let activeScrollbar: I.Scrollbar | null; 9 | 10 | export function touchHandler(scrollbar: I.Scrollbar) { 11 | const target = scrollbar.options.delegateTo || scrollbar.containerEl; 12 | const touchRecord = new TouchRecord(); 13 | const addEvent = eventScope(scrollbar); 14 | 15 | let damping: number; 16 | let pointerCount = 0; 17 | 18 | addEvent(target, 'touchstart', (evt: TouchEvent) => { 19 | // start records 20 | touchRecord.track(evt); 21 | 22 | // stop scrolling 23 | scrollbar.setMomentum(0, 0); 24 | 25 | // save damping 26 | if (pointerCount === 0) { 27 | damping = scrollbar.options.damping; 28 | scrollbar.options.damping = Math.max(damping, 0.5); // less frames on touchmove 29 | } 30 | 31 | pointerCount++; 32 | }); 33 | 34 | addEvent(target, 'touchmove', (evt: TouchEvent) => { 35 | if (activeScrollbar && activeScrollbar !== scrollbar) return; 36 | 37 | touchRecord.update(evt); 38 | 39 | const { x, y } = touchRecord.getDelta(); 40 | 41 | scrollbar.addTransformableMomentum(x, y, evt, (willScroll) => { 42 | if (willScroll && evt.cancelable) { 43 | evt.preventDefault(); 44 | activeScrollbar = scrollbar; 45 | } 46 | }); 47 | }); 48 | 49 | addEvent(target, 'touchcancel touchend', (evt: TouchEvent) => { 50 | const delta = touchRecord.getEasingDistance(damping); 51 | 52 | scrollbar.addTransformableMomentum( 53 | delta.x, 54 | delta.y, 55 | evt, 56 | ); 57 | 58 | pointerCount--; 59 | 60 | // restore damping 61 | if (pointerCount === 0) { 62 | scrollbar.options.damping = damping; 63 | } 64 | 65 | touchRecord.release(evt); 66 | activeScrollbar = null; 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/events/wheel.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | 3 | import { 4 | eventScope, 5 | } from '../utils/'; 6 | 7 | export function wheelHandler(scrollbar: I.Scrollbar) { 8 | const addEvent = eventScope(scrollbar); 9 | 10 | const target = scrollbar.options.delegateTo || scrollbar.containerEl; 11 | 12 | const eventName = ('onwheel' in window || document.implementation.hasFeature('Events.wheel', '3.0')) ? 'wheel' : 'mousewheel'; 13 | 14 | addEvent(target, eventName, (evt: WheelEvent) => { 15 | const { x, y } = normalizeDelta(evt); 16 | 17 | scrollbar.addTransformableMomentum(x, y, evt, (willScroll) => { 18 | if (willScroll) { 19 | evt.preventDefault(); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | // Normalizing wheel delta 26 | 27 | const DELTA_SCALE = { 28 | STANDARD: 1, 29 | OTHERS: -3, 30 | }; 31 | 32 | const DELTA_MODE = [1.0, 28.0, 500.0]; 33 | 34 | const getDeltaMode = (mode) => DELTA_MODE[mode] || DELTA_MODE[0]; 35 | 36 | function normalizeDelta(evt: any) { 37 | if ('deltaX' in evt) { 38 | const mode = getDeltaMode(evt.deltaMode); 39 | 40 | return { 41 | x: evt.deltaX / DELTA_SCALE.STANDARD * mode, 42 | y: evt.deltaY / DELTA_SCALE.STANDARD * mode, 43 | }; 44 | } 45 | 46 | if ('wheelDeltaX' in evt) { 47 | return { 48 | x: evt.wheelDeltaX / DELTA_SCALE.OTHERS, 49 | y: evt.wheelDeltaY / DELTA_SCALE.OTHERS, 50 | }; 51 | } 52 | 53 | // ie with touchpad 54 | return { 55 | x: 0, 56 | y: evt.wheelDelta / DELTA_SCALE.OTHERS, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/geometry/get-size.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | 3 | export function getSize(scrollbar: I.Scrollbar): I.ScrollbarSize { 4 | const { 5 | containerEl, 6 | contentEl, 7 | } = scrollbar; 8 | 9 | const containerStyles = getComputedStyle(containerEl); 10 | const paddings = [ 11 | 'paddingTop', 12 | 'paddingBottom', 13 | 'paddingLeft', 14 | 'paddingRight', 15 | ].map(prop => { 16 | return containerStyles[prop] ? parseFloat(containerStyles[prop]) : 0; 17 | }); 18 | const verticalPadding = paddings[0] + paddings[1]; 19 | const horizontalPadding = paddings[2] + paddings[3]; 20 | 21 | return { 22 | container: { 23 | // requires `overflow: hidden` 24 | width: containerEl.clientWidth, 25 | height: containerEl.clientHeight, 26 | }, 27 | content: { 28 | // border width and paddings should be included 29 | width: contentEl.offsetWidth - contentEl.clientWidth + contentEl.scrollWidth + horizontalPadding, 30 | height: contentEl.offsetHeight - contentEl.clientHeight + contentEl.scrollHeight + verticalPadding, 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/geometry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-size'; 2 | export * from './is-visible'; 3 | export * from './update'; 4 | -------------------------------------------------------------------------------- /src/geometry/is-visible.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | 3 | export function isVisible(scrollbar: I.Scrollbar, elem: HTMLElement): boolean { 4 | const { bounding } = scrollbar; 5 | const targetBounding = elem.getBoundingClientRect(); 6 | 7 | // check overlapping 8 | const top = Math.max(bounding.top, targetBounding.top); 9 | const left = Math.max(bounding.left, targetBounding.left); 10 | const right = Math.min(bounding.right, targetBounding.right); 11 | const bottom = Math.min(bounding.bottom, targetBounding.bottom); 12 | 13 | return top < bottom && left < right; 14 | } 15 | -------------------------------------------------------------------------------- /src/geometry/update.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Scrollbar, 3 | } from '../interfaces/'; 4 | 5 | export function update(scrollbar: Scrollbar) { 6 | const newSize = scrollbar.getSize(); 7 | 8 | const limit = { 9 | x: Math.max(newSize.content.width - newSize.container.width, 0), 10 | y: Math.max(newSize.content.height - newSize.container.height, 0), 11 | }; 12 | 13 | // metrics 14 | const containerBounding = scrollbar.containerEl.getBoundingClientRect(); 15 | 16 | const bounding = { 17 | top: Math.max(containerBounding.top, 0), 18 | right: Math.min(containerBounding.right, window.innerWidth), 19 | bottom: Math.min(containerBounding.bottom, window.innerHeight), 20 | left: Math.max(containerBounding.left, 0), 21 | }; 22 | 23 | // assign props 24 | scrollbar.size = newSize; 25 | scrollbar.limit = limit; 26 | scrollbar.bounding = bounding; 27 | 28 | // update tracks 29 | scrollbar.track.update(); 30 | 31 | // re-positioning 32 | scrollbar.setPosition(); 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import * as I from './interfaces/'; 3 | 4 | import { 5 | scrollbarMap, 6 | Scrollbar, 7 | } from './scrollbar'; 8 | 9 | import { 10 | addPlugins, 11 | ScrollbarPlugin, 12 | } from './plugin'; 13 | 14 | import { 15 | attachStyle, 16 | detachStyle, 17 | } from './style'; 18 | 19 | export { ScrollbarPlugin }; 20 | 21 | declare var __SCROLLBAR_VERSION__: string; 22 | 23 | /** 24 | * cast `I.Scrollbar` to `Scrollbar` to avoid error 25 | * 26 | * `I.Scrollbar` is not assignable to `Scrollbar`: 27 | * "privateProp" is missing in `I.Scrollbar` 28 | * 29 | * @see https://github.com/Microsoft/TypeScript/issues/2672 30 | */ 31 | 32 | export default class SmoothScrollbar extends Scrollbar { 33 | static version = __SCROLLBAR_VERSION__; 34 | 35 | static ScrollbarPlugin = ScrollbarPlugin; 36 | 37 | /** 38 | * Initializes a scrollbar on the given element. 39 | * 40 | * @param elem The DOM element that you want to initialize scrollbar to 41 | * @param [options] Initial options 42 | */ 43 | static init(elem: HTMLElement, options?: Partial<I.ScrollbarOptions>): Scrollbar { 44 | if (!elem || elem.nodeType !== 1) { 45 | throw new TypeError(`expect element to be DOM Element, but got ${elem}`); 46 | } 47 | 48 | // attach stylesheet 49 | attachStyle(); 50 | 51 | if (scrollbarMap.has(elem)) { 52 | return scrollbarMap.get(elem) as Scrollbar; 53 | } 54 | 55 | return new Scrollbar(elem, options); 56 | } 57 | 58 | /** 59 | * Automatically init scrollbar on all elements base on the selector `[data-scrollbar]` 60 | * 61 | * @param options Initial options 62 | */ 63 | static initAll(options?: Partial<I.ScrollbarOptions>): Scrollbar[] { 64 | return Array.from(document.querySelectorAll('[data-scrollbar]'), (elem: HTMLElement) => { 65 | return SmoothScrollbar.init(elem, options); 66 | }); 67 | } 68 | 69 | /** 70 | * Check if there is a scrollbar on given element 71 | * 72 | * @param elem The DOM element that you want to check 73 | */ 74 | static has(elem: HTMLElement): boolean { 75 | return scrollbarMap.has(elem); 76 | } 77 | 78 | /** 79 | * Gets scrollbar on the given element. 80 | * If no scrollbar instance exsits, returns `undefined` 81 | * 82 | * @param elem The DOM element that you want to check. 83 | */ 84 | static get(elem: HTMLElement): Scrollbar | undefined { 85 | return scrollbarMap.get(elem) as (Scrollbar | undefined); 86 | } 87 | 88 | /** 89 | * Returns an array that contains all scrollbar instances 90 | */ 91 | static getAll(): Scrollbar[] { 92 | return Array.from(scrollbarMap.values()) as Scrollbar[]; 93 | } 94 | 95 | /** 96 | * Removes scrollbar on the given element 97 | */ 98 | static destroy(elem: HTMLElement) { 99 | const scrollbar = scrollbarMap.get(elem); 100 | 101 | if (scrollbar) { 102 | scrollbar.destroy(); 103 | } 104 | } 105 | 106 | /** 107 | * Removes all scrollbar instances from current document 108 | */ 109 | static destroyAll() { 110 | scrollbarMap.forEach((scrollbar) => { 111 | scrollbar.destroy(); 112 | }); 113 | } 114 | 115 | /** 116 | * Attaches plugins to scrollbars 117 | * 118 | * @param ...Plugins Scrollbar plugin classes 119 | */ 120 | static use(...Plugins: (typeof ScrollbarPlugin)[]) { 121 | return addPlugins(...Plugins); 122 | } 123 | 124 | /** 125 | * Attaches default style sheets to current document. 126 | * You don't need to call this method manually unless 127 | * you removed the default styles via `Scrollbar.detachStyle()` 128 | */ 129 | static attachStyle() { 130 | return attachStyle(); 131 | } 132 | 133 | /** 134 | * Removes default styles from current document. 135 | * Use this method when you want to use your own css for scrollbars. 136 | */ 137 | static detachStyle() { 138 | return detachStyle(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/interfaces/data-2d.ts: -------------------------------------------------------------------------------- 1 | // 2-dimension data set 2 | export type Data2d = { 3 | x: number, 4 | y: number, 5 | }; 6 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scrollbar'; 2 | export * from './track'; 3 | export * from './plugin'; 4 | export * from './data-2d'; 5 | -------------------------------------------------------------------------------- /src/interfaces/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Scrollbar, 3 | } from './scrollbar'; 4 | 5 | import { Data2d } from './data-2d'; 6 | 7 | // Scrollbar.Plugin 8 | export interface ScrollbarPlugin { 9 | readonly scrollbar: Scrollbar; 10 | readonly options: any; 11 | readonly name: string; 12 | 13 | onInit(): void; 14 | onDestroy(): void; 15 | 16 | onUpdate(): void; 17 | onRender(remainMomentum: Data2d): void; 18 | 19 | transformDelta(delta: Data2d, fromEvent: any): Data2d; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/scrollbar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TrackController, 3 | } from './track'; 4 | 5 | import { Data2d } from './data-2d'; 6 | 7 | // Scrollbar.options 8 | export type ScrollbarOptions = { 9 | /** 10 | * Momentum reduction damping factor, a float value between `(0, 1)`. The lower the value is, the more smooth the scrolling will be (also the more paint frames). 11 | * @default 0.1 12 | */ 13 | damping: number, 14 | /** 15 | * Minimal size for scrollbar thumbs. 16 | * @default 20 17 | */ 18 | thumbMinSize: number, 19 | /** 20 | * Render every frame in integer pixel values, set to `true` to **improve** scrolling performance. 21 | * @default true 22 | */ 23 | renderByPixels: boolean, 24 | /** 25 | * Keep scrollbar tracks visible. 26 | * @default false 27 | */ 28 | alwaysShowTracks: boolean, 29 | /** 30 | * Set to `true` to allow outer scrollbars continue scrolling when current scrollbar reaches edge. 31 | * @default true 32 | */ 33 | continuousScrolling: boolean, 34 | /** 35 | * Delegate wheel events and touch events to the given element. By default, the container element is used. This option will be useful for dealing with fixed elements. 36 | * @default null 37 | */ 38 | delegateTo: EventTarget | null, 39 | /** 40 | * @deprecated `wheelEventTarget` is deprecated and will be removed in the future, use `delegateTo` instead. 41 | */ 42 | wheelEventTarget: EventTarget | null, 43 | /** 44 | * Options for plugins, see {@link https://github.com/idiotWu/smooth-scrollbar/blob/develop/docs/plugin.md Plugin System}. 45 | */ 46 | plugins: any, 47 | }; 48 | 49 | // Scrollbar.size 50 | export type Metrics = { 51 | width: number, 52 | height: number, 53 | }; 54 | 55 | export type ScrollbarSize = { 56 | container: Metrics, 57 | content: Metrics, 58 | }; 59 | 60 | // Scrollbar.bounding 61 | export type ScrollbarBounding = { 62 | top: number, 63 | right: number, 64 | bottom: number, 65 | left: number, 66 | }; 67 | 68 | // Scrolling Listener 69 | export type ScrollStatus = { 70 | offset: Data2d, 71 | limit: Data2d, 72 | }; 73 | 74 | export interface ScrollListener { 75 | (this: Scrollbar, status: ScrollStatus): void; 76 | } 77 | 78 | // `scrollTo` options 79 | export type ScrollToOptions = { 80 | callback: (this: Scrollbar) => void, 81 | easing: (percent: number) => number, 82 | }; 83 | 84 | // `setPosition` options 85 | export type SetPositionOptions = { 86 | withoutCallbacks: boolean, 87 | }; 88 | 89 | // `scrollIntoView` options 90 | export type ScrollIntoViewOptions = { 91 | alignToTop: boolean, 92 | onlyScrollIfNeeded: boolean, 93 | offsetTop: number, 94 | offsetLeft: number, 95 | offsetBottom: number, 96 | }; 97 | 98 | export interface AddTransformableMomentumCallback { 99 | (this: Scrollbar, willScroll: boolean): void; 100 | } 101 | 102 | // Scrollbar Class 103 | export interface Scrollbar { 104 | readonly parent: Scrollbar | null; 105 | 106 | readonly containerEl: HTMLElement; 107 | readonly contentEl: HTMLElement; 108 | 109 | readonly track: TrackController; 110 | 111 | readonly options: ScrollbarOptions; 112 | 113 | bounding: ScrollbarBounding; 114 | size: ScrollbarSize; 115 | 116 | offset: Data2d; 117 | limit: Data2d; 118 | 119 | scrollTop: number; 120 | scrollLeft: number; 121 | 122 | destroy(): void; 123 | 124 | update(): void; 125 | getSize(): ScrollbarSize; 126 | isVisible(elem: HTMLElement): boolean; 127 | 128 | addListener(fn: ScrollListener): void; 129 | removeListener(fn: ScrollListener): void; 130 | 131 | addTransformableMomentum(x: number, y: number, fromEvent: Event, callback?: AddTransformableMomentumCallback): void; 132 | addMomentum(x: number, y: number): void; 133 | setMomentum(x: number, y: number): void; 134 | 135 | scrollTo(x?: number, y?: number, duration?: number, options?: Partial<ScrollToOptions>): void; 136 | setPosition(x?: number, y?: number, options?: Partial<SetPositionOptions>): void; 137 | scrollIntoView(elem: HTMLElement, options?: Partial<ScrollIntoViewOptions>): void; 138 | 139 | updatePluginOptions(pluginName: string, options?: any): void; 140 | } 141 | -------------------------------------------------------------------------------- /src/interfaces/track.ts: -------------------------------------------------------------------------------- 1 | export interface ScrollbarThumb { 2 | readonly element: HTMLElement; 3 | displaySize: number; 4 | realSize: number; 5 | offset: number; 6 | 7 | attachTo(track: HTMLElement): void; 8 | update(scrollOffset: number, containerSize: number, pageSize: number): void; 9 | } 10 | 11 | export interface ScrollbarTrack { 12 | readonly element: HTMLElement; 13 | readonly thumb: ScrollbarThumb; 14 | 15 | attachTo(container: HTMLElement): void; 16 | show(): void; 17 | hide(): void; 18 | update(scrollOffset: number, containerSize: number, pageSize: number): void; 19 | } 20 | 21 | export interface TrackController { 22 | readonly xAxis: ScrollbarTrack; 23 | readonly yAxis: ScrollbarTrack; 24 | 25 | update(): void; 26 | autoHideOnIdle(): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | range, 3 | boolean, 4 | } from './decorators/'; 5 | 6 | import { 7 | ScrollbarOptions, 8 | } from './interfaces/'; 9 | 10 | export class Options { 11 | /** 12 | * Momentum reduction damping factor, a float value between `(0, 1)`. 13 | * The lower the value is, the more smooth the scrolling will be 14 | * (also the more paint frames). 15 | */ 16 | @range(0, 1) 17 | damping = 0.1; 18 | 19 | /** 20 | * Minimal size for scrollbar thumbs. 21 | */ 22 | @range(0, Infinity) 23 | thumbMinSize = 20; 24 | 25 | /** 26 | * Render every frame in integer pixel values 27 | * set to `true` to improve scrolling performance. 28 | */ 29 | @boolean 30 | renderByPixels = true; 31 | 32 | /** 33 | * Keep scrollbar tracks visible 34 | */ 35 | @boolean 36 | alwaysShowTracks = false; 37 | 38 | /** 39 | * Set to `true` to allow outer scrollbars continue scrolling 40 | * when current scrollbar reaches edge. 41 | */ 42 | @boolean 43 | continuousScrolling = true; 44 | 45 | /** 46 | * Delegate wheel events and touch events to the given element. 47 | * By default, the container element is used. 48 | * This option will be useful for dealing with fixed elements. 49 | */ 50 | delegateTo: EventTarget | null = null; 51 | 52 | get wheelEventTarget() { 53 | return this.delegateTo; 54 | } 55 | 56 | set wheelEventTarget(el: EventTarget | null) { 57 | console.warn('[smooth-scrollbar]: `options.wheelEventTarget` is deprecated and will be removed in the future, use `options.delegateTo` instead.'); 58 | 59 | this.delegateTo = el; 60 | } 61 | 62 | /** 63 | * Options for plugins. Syntax: 64 | * plugins[pluginName] = pluginOptions: any 65 | */ 66 | readonly plugins: any = {}; 67 | 68 | constructor(config: Partial<ScrollbarOptions> = {}) { 69 | Object.keys(config).forEach((prop) => { 70 | this[prop] = config[prop]; 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as I from './interfaces/'; 2 | 3 | import { Scrollbar } from './scrollbar'; // used as type annotations 4 | 5 | export class ScrollbarPlugin implements I.ScrollbarPlugin { 6 | static pluginName = ''; 7 | static defaultOptions: any = {}; 8 | 9 | readonly scrollbar: Scrollbar; 10 | readonly options: any; 11 | readonly name: string; 12 | 13 | constructor( 14 | scrollbar: Scrollbar, 15 | options?: any, 16 | ) { 17 | this.scrollbar = scrollbar; 18 | this.name = new.target.pluginName; 19 | 20 | this.options = { 21 | ...new.target.defaultOptions, 22 | ...options, 23 | }; 24 | } 25 | 26 | onInit() {} 27 | onDestroy() {} 28 | 29 | onUpdate() {} 30 | onRender(_remainMomentum: I.Data2d) {} 31 | 32 | transformDelta(delta: I.Data2d, _evt: Event): I.Data2d { 33 | return { ...delta }; 34 | } 35 | } 36 | 37 | export type PluginMap = { 38 | order: Set<string>, 39 | constructors: { 40 | [name: string]: typeof ScrollbarPlugin, 41 | }, 42 | }; 43 | 44 | export const globalPlugins: PluginMap = { 45 | order: new Set<string>(), 46 | constructors: {}, 47 | }; 48 | 49 | export function addPlugins( 50 | ...Plugins: (typeof ScrollbarPlugin)[] 51 | ): void { 52 | Plugins.forEach((P) => { 53 | const { pluginName } = P; 54 | 55 | if (!pluginName) { 56 | throw new TypeError(`plugin name is required`); 57 | } 58 | 59 | globalPlugins.order.add(pluginName); 60 | globalPlugins.constructors[pluginName] = P; 61 | }); 62 | } 63 | 64 | export function initPlugins( 65 | scrollbar: Scrollbar, 66 | options: any, 67 | ): ScrollbarPlugin[] { 68 | return Array.from(globalPlugins.order) 69 | .filter((pluginName: string) => { 70 | return options[pluginName] !== false; 71 | }) 72 | .map((pluginName: string) => { 73 | const Plugin = globalPlugins.constructors[pluginName]; 74 | 75 | const instance = new Plugin(scrollbar, options[pluginName]); 76 | 77 | // bind plugin options to `scrollbar.options` 78 | options[pluginName] = instance.options; 79 | 80 | return instance; 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/plugins/overscroll/bounce.ts: -------------------------------------------------------------------------------- 1 | import Scrollbar from 'smooth-scrollbar'; 2 | 3 | import { setStyle } from '../../utils/set-style'; 4 | 5 | export class Bounce { 6 | constructor( 7 | private _scrollbar: Scrollbar, 8 | ) {} 9 | 10 | render({ x = 0, y = 0 }) { 11 | const { 12 | size, 13 | track, 14 | offset, 15 | contentEl, 16 | } = this._scrollbar; 17 | 18 | setStyle(contentEl, { 19 | '-transform': `translate3d(${-(offset.x + x)}px, ${-(offset.y + y)}px, 0)`, 20 | }); 21 | 22 | if (x) { 23 | track.xAxis.show(); 24 | 25 | const scaleRatio = size.container.width / (size.container.width + Math.abs(x)); 26 | 27 | setStyle(track.xAxis.thumb.element, { 28 | '-transform': `translate3d(${track.xAxis.thumb.offset}px, 0, 0) scale3d(${scaleRatio}, 1, 1)`, 29 | '-transform-origin': x < 0 ? 'left' : 'right', 30 | }); 31 | } 32 | 33 | if (y) { 34 | track.yAxis.show(); 35 | 36 | const scaleRatio = size.container.height / (size.container.height + Math.abs(y)); 37 | 38 | setStyle(track.yAxis.thumb.element, { 39 | '-transform': `translate3d(0, ${track.yAxis.thumb.offset}px, 0) scale3d(1, ${scaleRatio}, 1)`, 40 | '-transform-origin': y < 0 ? 'top' : 'bottom', 41 | }); 42 | } 43 | 44 | track.autoHideOnIdle(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/overscroll/glow.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../../utils'; 2 | import Scrollbar from 'smooth-scrollbar'; 3 | 4 | import { setStyle } from '../../utils/set-style'; 5 | 6 | const GLOW_MAX_OPACITY = 0.75; 7 | const GLOW_MAX_OFFSET = 0.25; 8 | 9 | export class Glow { 10 | private _canvas = document.createElement('canvas'); 11 | private _ctx = this._canvas.getContext('2d') as CanvasRenderingContext2D; 12 | 13 | private _touchX: number; 14 | private _touchY: number; 15 | 16 | constructor( 17 | private _scrollbar: Scrollbar, 18 | ) { 19 | setStyle(this._canvas, { 20 | position: 'absolute', 21 | top: 0, 22 | left: 0, 23 | width: '100%', 24 | height: '100%', 25 | display: 'none', 26 | }); 27 | } 28 | 29 | mount() { 30 | this._scrollbar.containerEl.appendChild(this._canvas); 31 | } 32 | 33 | unmount() { 34 | if (this._canvas.parentNode) { 35 | this._canvas.parentNode.removeChild(this._canvas); 36 | } 37 | } 38 | 39 | adjust() { 40 | const { 41 | size, 42 | } = this._scrollbar; 43 | 44 | const DPR = window.devicePixelRatio || 1; 45 | 46 | const nextWidth = size.container.width * DPR; 47 | const nextHeight = size.container.height * DPR; 48 | 49 | if (nextWidth === this._canvas.width && nextHeight === this._canvas.height) { 50 | return; 51 | } 52 | 53 | this._canvas.width = nextWidth; 54 | this._canvas.height = nextHeight; 55 | 56 | this._ctx.scale(DPR, DPR); 57 | } 58 | 59 | recordTouch(event: TouchEvent) { 60 | const touch = event.touches[event.touches.length - 1]; 61 | 62 | this._touchX = touch.clientX; 63 | this._touchY = touch.clientY; 64 | } 65 | 66 | render({ x = 0, y = 0 }, color: string) { 67 | if (!x && !y) { 68 | setStyle(this._canvas, { 69 | display: 'none', 70 | }); 71 | 72 | return; 73 | } 74 | 75 | setStyle(this._canvas, { 76 | display: 'block', 77 | }); 78 | 79 | const { 80 | size, 81 | } = this._scrollbar; 82 | 83 | this._ctx.clearRect(0, 0, size.container.width, size.container.height); 84 | this._ctx.fillStyle = color; 85 | 86 | this._renderX(x); 87 | this._renderY(y); 88 | } 89 | 90 | private _getMaxOverscroll(): number { 91 | const options = this._scrollbar.options.plugins.overscroll; 92 | 93 | return options && options.maxOverscroll ? options.maxOverscroll : 150; 94 | } 95 | 96 | private _renderX(strength: number) { 97 | const { 98 | size, 99 | } = this._scrollbar; 100 | 101 | const maxOverscroll = this._getMaxOverscroll(); 102 | const { width, height } = size.container; 103 | const ctx = this._ctx; 104 | 105 | ctx.save(); 106 | 107 | if (strength > 0) { 108 | // glow on right side 109 | // horizontally flip 110 | ctx.transform(-1, 0, 0, 1, width, 0); 111 | } 112 | 113 | const opacity = clamp(Math.abs(strength) / maxOverscroll, 0, GLOW_MAX_OPACITY); 114 | const startOffset = clamp(opacity, 0, GLOW_MAX_OFFSET) * width; 115 | 116 | // controll point 117 | const x = Math.abs(strength); 118 | const y = this._touchY || (height / 2); 119 | 120 | ctx.globalAlpha = opacity; 121 | ctx.beginPath(); 122 | ctx.moveTo(0, -startOffset); 123 | ctx.quadraticCurveTo(x, y, 0, height + startOffset); 124 | ctx.fill(); 125 | ctx.closePath(); 126 | ctx.restore(); 127 | } 128 | 129 | private _renderY(strength: number) { 130 | const { 131 | size, 132 | } = this._scrollbar; 133 | 134 | const maxOverscroll = this._getMaxOverscroll(); 135 | const { width, height } = size.container; 136 | const ctx = this._ctx; 137 | 138 | ctx.save(); 139 | 140 | if (strength > 0) { 141 | // glow on bottom side 142 | // vertically flip 143 | ctx.transform(1, 0, 0, -1, 0, height); 144 | } 145 | 146 | const opacity = clamp(Math.abs(strength) / maxOverscroll, 0, GLOW_MAX_OPACITY); 147 | const startOffset = clamp(opacity, 0, GLOW_MAX_OFFSET) * width; 148 | 149 | // controll point 150 | const x = this._touchX || (width / 2); 151 | const y = Math.abs(strength); 152 | 153 | ctx.globalAlpha = opacity; 154 | ctx.beginPath(); 155 | ctx.moveTo(-startOffset, 0); 156 | ctx.quadraticCurveTo(x, y, width + startOffset, 0); 157 | ctx.fill(); 158 | ctx.closePath(); 159 | ctx.restore(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/plugins/overscroll/index.ts: -------------------------------------------------------------------------------- 1 | import { clamp, debounce } from '../../utils'; 2 | import { ScrollbarPlugin } from 'smooth-scrollbar'; 3 | import { Bounce } from './bounce'; 4 | import { Glow } from './glow'; 5 | 6 | export enum OverscrollEffect { 7 | BOUNCE = 'bounce', 8 | GLOW = 'glow', 9 | } 10 | 11 | export type Data2d = { 12 | x: number, 13 | y: number, 14 | }; 15 | 16 | export type OnScrollCallback = (this: OverscrollPlugin, position: Data2d) => void; 17 | 18 | export type OverscrollOptions = { 19 | effect?: OverscrollEffect, 20 | onScroll?: OnScrollCallback, 21 | damping: number, 22 | maxOverscroll: number, 23 | glowColor: string, 24 | }; 25 | 26 | const ALLOWED_EVENTS = /wheel|touch/; 27 | 28 | export default class OverscrollPlugin extends ScrollbarPlugin { 29 | static pluginName = 'overscroll'; 30 | 31 | static defaultOptions: OverscrollOptions = { 32 | effect: OverscrollEffect.BOUNCE, 33 | onScroll: undefined, 34 | damping: 0.2, 35 | maxOverscroll: 150, 36 | glowColor: '#87ceeb', 37 | }; 38 | 39 | options: OverscrollOptions; 40 | 41 | private _glow = new Glow(this.scrollbar); 42 | private _bounce = new Bounce(this.scrollbar); 43 | 44 | private _wheelScrollBack = { 45 | x: false, 46 | y: false, 47 | }; 48 | private _lockWheel = { 49 | x: false, 50 | y: false, 51 | }; 52 | 53 | private get _isWheelLocked() { 54 | return this._lockWheel.x || this._lockWheel.y; 55 | } 56 | 57 | private _touching = false; 58 | 59 | private _lastEventType: string; 60 | 61 | private _amplitude = { 62 | x: 0, 63 | y: 0, 64 | }; 65 | 66 | private _position = { 67 | x: 0, 68 | y: 0, 69 | }; 70 | 71 | private get _enabled() { 72 | return !!this.options.effect; 73 | } 74 | 75 | // since we can't detect whether user release touchpad 76 | // handle it with debounce is the best solution now, as a trade-off 77 | private _releaseWheel = debounce(() => { 78 | this._lockWheel.x = false; 79 | this._lockWheel.y = false; 80 | }, 30); 81 | 82 | onInit() { 83 | const { 84 | _glow, 85 | options, 86 | scrollbar, 87 | } = this; 88 | 89 | // observe 90 | let effect = options.effect; 91 | 92 | Object.defineProperty(options, 'effect', { 93 | get() { 94 | return effect; 95 | }, 96 | set(val) { 97 | if (!val) { 98 | effect = undefined; 99 | return; 100 | } 101 | 102 | if (val !== OverscrollEffect.BOUNCE && val !== OverscrollEffect.GLOW) { 103 | throw new TypeError(`unknow overscroll effect: ${val}`); 104 | } 105 | 106 | effect = val; 107 | 108 | scrollbar.options.continuousScrolling = false; 109 | 110 | if (val === OverscrollEffect.GLOW) { 111 | _glow.mount(); 112 | _glow.adjust(); 113 | } else { 114 | _glow.unmount(); 115 | } 116 | }, 117 | }); 118 | 119 | options.effect = effect; // init 120 | } 121 | 122 | onUpdate() { 123 | if (this.options.effect === OverscrollEffect.GLOW) { 124 | this._glow.adjust(); 125 | } 126 | } 127 | 128 | onRender(remainMomentum: Data2d) { 129 | if (!this._enabled) { 130 | return; 131 | } 132 | 133 | if (this.scrollbar.options.continuousScrolling) { 134 | // turn off continuous scrolling 135 | this.scrollbar.options.continuousScrolling = false; 136 | } 137 | 138 | let { x: nextX, y: nextY } = remainMomentum; 139 | 140 | // transfer remain momentum to overscroll 141 | if (!this._amplitude.x && 142 | this._willOverscroll('x', remainMomentum.x) 143 | ) { 144 | nextX = 0; 145 | 146 | this._absorbMomentum('x', remainMomentum.x); 147 | } 148 | 149 | if (!this._amplitude.y && 150 | this._willOverscroll('y', remainMomentum.y) 151 | ) { 152 | nextY = 0; 153 | 154 | this._absorbMomentum('y', remainMomentum.y); 155 | } 156 | 157 | this.scrollbar.setMomentum(nextX, nextY); 158 | this._render(); 159 | } 160 | 161 | transformDelta(delta: Data2d, fromEvent: Event): Data2d { 162 | this._lastEventType = fromEvent.type; 163 | 164 | if (!this._enabled || !ALLOWED_EVENTS.test(fromEvent.type)) { 165 | return delta; 166 | } 167 | 168 | if (this._isWheelLocked && /wheel/.test(fromEvent.type)) { 169 | this._releaseWheel(); 170 | 171 | if (this._willOverscroll('x', delta.x)) { 172 | delta.x = 0; 173 | } 174 | 175 | if (this._willOverscroll('y', delta.y)) { 176 | delta.y = 0; 177 | } 178 | } 179 | 180 | let { x: nextX, y: nextY } = delta; 181 | 182 | if (this._willOverscroll('x', delta.x)) { 183 | nextX = 0; 184 | this._addAmplitude('x', delta.x); 185 | } 186 | 187 | if (this._willOverscroll('y', delta.y)) { 188 | nextY = 0; 189 | this._addAmplitude('y', delta.y); 190 | } 191 | 192 | switch (fromEvent.type) { 193 | case 'touchstart': 194 | case 'touchmove': 195 | this._touching = true; 196 | this._glow.recordTouch(fromEvent as TouchEvent); 197 | break; 198 | 199 | case 'touchcancel': 200 | case 'touchend': 201 | this._touching = false; 202 | break; 203 | } 204 | 205 | return { 206 | x: nextX, 207 | y: nextY, 208 | }; 209 | } 210 | 211 | private _willOverscroll(direction: 'x' | 'y', delta: number): boolean { 212 | if (!delta) { 213 | return false; 214 | } 215 | 216 | // away from origin 217 | if (this._position[direction]) { 218 | return true; 219 | } 220 | 221 | const offset = this.scrollbar.offset[direction]; 222 | const limit = this.scrollbar.limit[direction]; 223 | 224 | if (limit === 0) { 225 | return false; 226 | } 227 | 228 | // cond: 229 | // 1. next scrolling position is supposed to stay unchange 230 | // 2. current position is on the edge 231 | return clamp(offset + delta, 0, limit) === offset && 232 | (offset === 0 || offset === limit); 233 | } 234 | 235 | private _absorbMomentum(direction: 'x' | 'y', remainMomentum: number) { 236 | const { 237 | options, 238 | _lastEventType, 239 | _amplitude, 240 | } = this; 241 | 242 | if (!ALLOWED_EVENTS.test(_lastEventType)) { 243 | return; 244 | } 245 | 246 | _amplitude[direction] = clamp(remainMomentum, -options.maxOverscroll, options.maxOverscroll); 247 | } 248 | 249 | private _addAmplitude(direction: 'x' | 'y', delta: number) { 250 | const { 251 | options, 252 | scrollbar, 253 | _amplitude, 254 | _position, 255 | } = this; 256 | 257 | const currentAmp = _amplitude[direction]; 258 | 259 | const isOpposite = delta * currentAmp < 0; 260 | 261 | let friction: number; 262 | 263 | if (isOpposite) { 264 | // opposite direction 265 | friction = 0; 266 | } else { 267 | friction = this._wheelScrollBack[direction] ? 268 | 1 : Math.abs(currentAmp / options.maxOverscroll); 269 | } 270 | 271 | const amp = currentAmp + delta * (1 - friction); 272 | 273 | _amplitude[direction] = scrollbar.offset[direction] === 0 ? 274 | /* top | left */ clamp(amp, -options.maxOverscroll, 0) : 275 | /* bottom | right */ clamp(amp, 0, options.maxOverscroll); 276 | 277 | if (isOpposite) { 278 | // scroll back 279 | _position[direction] = _amplitude[direction]; 280 | } 281 | } 282 | 283 | private _render() { 284 | const { 285 | options, 286 | _amplitude, 287 | _position, 288 | } = this; 289 | 290 | if (this._enabled && 291 | (_amplitude.x || _amplitude.y || _position.x || _position.y) 292 | ) { 293 | const nextX = this._nextAmp('x'); 294 | const nextY = this._nextAmp('y'); 295 | 296 | _amplitude.x = nextX.amplitude; 297 | _position.x = nextX.position; 298 | 299 | _amplitude.y = nextY.amplitude; 300 | _position.y = nextY.position; 301 | 302 | switch (options.effect) { 303 | case OverscrollEffect.BOUNCE: 304 | this._bounce.render(_position); 305 | break; 306 | 307 | case OverscrollEffect.GLOW: 308 | this._glow.render(_position, this.options.glowColor); 309 | break; 310 | } 311 | 312 | if (typeof options.onScroll === 'function') { 313 | options.onScroll.call(this, { ..._position }); 314 | } 315 | } 316 | } 317 | 318 | private _nextAmp(direction: 'x' | 'y'): { amplitude: number, position: number } { 319 | const { 320 | options, 321 | _amplitude, 322 | _position, 323 | } = this; 324 | 325 | const t = 1 - options.damping; 326 | const amp = _amplitude[direction]; 327 | const pos = _position[direction]; 328 | 329 | const nextAmp = this._touching ? amp : (amp * t | 0); 330 | const distance = nextAmp - pos; 331 | const nextPos = pos + distance - (distance * t | 0); 332 | 333 | if (!this._touching && Math.abs(nextPos) < Math.abs(pos)) { 334 | this._wheelScrollBack[direction] = true; 335 | } 336 | 337 | if (this._wheelScrollBack[direction] && Math.abs(nextPos) <= 1) { 338 | this._wheelScrollBack[direction] = false; 339 | this._lockWheel[direction] = true; 340 | } 341 | 342 | return { 343 | amplitude: nextAmp, 344 | position: nextPos, 345 | }; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es/map'; 2 | import 'core-js/es/set'; 3 | import 'core-js/es/weak-map'; 4 | import 'core-js/es/array/from'; 5 | import 'core-js/es/object/assign'; 6 | -------------------------------------------------------------------------------- /src/scrollbar.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from './utils'; 2 | 3 | import { Options } from './options'; 4 | 5 | import { 6 | setStyle, 7 | clearEventsOn, 8 | } from './utils/'; 9 | 10 | import { 11 | debounce, 12 | } from './decorators/'; 13 | 14 | import { 15 | TrackController, 16 | } from './track/'; 17 | 18 | import { 19 | getSize, 20 | update, 21 | isVisible, 22 | } from './geometry/'; 23 | 24 | import { 25 | scrollTo, 26 | setPosition, 27 | scrollIntoView, 28 | } from './scrolling/'; 29 | 30 | import { 31 | initPlugins, 32 | } from './plugin'; 33 | 34 | import * as eventHandlers from './events/'; 35 | 36 | import * as I from './interfaces/'; 37 | 38 | // DO NOT use WeakMap here 39 | // .getAll() methods requires `scrollbarMap.values()` 40 | export const scrollbarMap = new Map<HTMLElement, Scrollbar>(); 41 | 42 | export class Scrollbar implements I.Scrollbar { 43 | /** 44 | * Options for current scrollbar instancs 45 | */ 46 | readonly options: Options; 47 | 48 | readonly track: TrackController; 49 | 50 | /** 51 | * The element that you initialized scrollbar to 52 | */ 53 | readonly containerEl: HTMLElement; 54 | 55 | /** 56 | * The wrapper element that contains your contents 57 | */ 58 | readonly contentEl: HTMLElement; 59 | 60 | /** 61 | * Geometry infomation for current scrollbar instance 62 | */ 63 | size: I.ScrollbarSize; 64 | 65 | /** 66 | * Current scrolling offsets 67 | */ 68 | offset = { 69 | x: 0, 70 | y: 0, 71 | }; 72 | 73 | /** 74 | * Max-allowed scrolling offsets 75 | */ 76 | limit = { 77 | x: Infinity, 78 | y: Infinity, 79 | }; 80 | 81 | /** 82 | * Container bounding rect 83 | */ 84 | bounding = { 85 | top: 0, 86 | right: 0, 87 | bottom: 0, 88 | left: 0, 89 | }; 90 | 91 | /** 92 | * Parent scrollbar 93 | */ 94 | get parent() { 95 | let elem = this.containerEl.parentElement; 96 | 97 | while (elem) { 98 | const parentScrollbar = scrollbarMap.get(elem); 99 | 100 | if (parentScrollbar) { 101 | return parentScrollbar; 102 | } 103 | 104 | elem = elem.parentElement; 105 | } 106 | 107 | return null; 108 | } 109 | 110 | /** 111 | * Gets or sets `scrollbar.offset.y` 112 | */ 113 | get scrollTop() { 114 | return this.offset.y; 115 | } 116 | set scrollTop(y: number) { 117 | this.setPosition(this.scrollLeft, y); 118 | } 119 | 120 | /** 121 | * Gets or sets `scrollbar.offset.x` 122 | */ 123 | get scrollLeft() { 124 | return this.offset.x; 125 | } 126 | set scrollLeft(x: number) { 127 | this.setPosition(x, this.scrollTop); 128 | } 129 | 130 | private _renderID: number; 131 | private _observer: any; // FIXME: we need to update typescript version to support `ResizeObserver` 132 | // private _observer: ResizeObserver; 133 | private _plugins: I.ScrollbarPlugin[] = []; 134 | 135 | private _momentum = { x: 0, y: 0 }; 136 | private _listeners = new Set<I.ScrollListener>(); 137 | 138 | constructor( 139 | containerEl: HTMLElement, 140 | options?: Partial<I.ScrollbarOptions>, 141 | ) { 142 | this.containerEl = containerEl; 143 | const contentEl = this.contentEl = document.createElement('div'); 144 | 145 | this.options = new Options(options); 146 | 147 | // mark as a scroll element 148 | containerEl.setAttribute('data-scrollbar', 'true'); 149 | 150 | // make container focusable 151 | containerEl.setAttribute('tabindex', '-1'); 152 | setStyle(containerEl, { 153 | overflow: 'hidden', 154 | outline: 'none', 155 | }); 156 | 157 | // enable touch event capturing in IE, see: 158 | // https://github.com/idiotWu/smooth-scrollbar/issues/39 159 | if (window.navigator.msPointerEnabled) { 160 | containerEl.style.msTouchAction = 'none'; 161 | } 162 | 163 | // mount content 164 | contentEl.className = 'scroll-content'; 165 | 166 | Array.from(containerEl.childNodes).forEach((node) => { 167 | contentEl.appendChild(node); 168 | }); 169 | 170 | containerEl.appendChild(contentEl); 171 | 172 | // attach track 173 | this.track = new TrackController(this); 174 | 175 | // initial measuring 176 | this.size = this.getSize(); 177 | 178 | // init plugins 179 | this._plugins = initPlugins(this, this.options.plugins); 180 | 181 | // preserve scroll offset 182 | const { scrollLeft, scrollTop } = containerEl; 183 | containerEl.scrollLeft = containerEl.scrollTop = 0; 184 | this.setPosition(scrollLeft, scrollTop, { 185 | withoutCallbacks: true, 186 | }); 187 | 188 | // FIXME: update typescript 189 | const ResizeObserver = (window as any).ResizeObserver; 190 | 191 | // observe 192 | if (typeof ResizeObserver === 'function') { 193 | this._observer = new ResizeObserver(() => { 194 | this.update(); 195 | }); 196 | 197 | this._observer.observe(contentEl); 198 | } 199 | 200 | scrollbarMap.set(containerEl, this); 201 | 202 | // wait for DOM ready 203 | requestAnimationFrame(() => { 204 | this._init(); 205 | }); 206 | } 207 | 208 | /** 209 | * Returns the size of the scrollbar container element 210 | * and the content wrapper element 211 | */ 212 | getSize(): I.ScrollbarSize { 213 | return getSize(this); 214 | } 215 | 216 | /** 217 | * Forces scrollbar to update geometry infomation. 218 | * 219 | * By default, scrollbars are automatically updated with `100ms` debounce (or `MutationObserver` fires). 220 | * You can call this method to force an update when you modified contents 221 | */ 222 | update() { 223 | update(this); 224 | 225 | this._plugins.forEach((plugin) => { 226 | plugin.onUpdate(); 227 | }); 228 | } 229 | 230 | /** 231 | * Checks if an element is visible in the current view area 232 | */ 233 | isVisible(elem: HTMLElement): boolean { 234 | return isVisible(this, elem); 235 | } 236 | 237 | /** 238 | * Sets the scrollbar to the given offset without easing 239 | */ 240 | setPosition( 241 | x = this.offset.x, 242 | y = this.offset.y, 243 | options: Partial<I.SetPositionOptions> = {}, 244 | ) { 245 | const status = setPosition(this, x, y); 246 | 247 | if (!status || options.withoutCallbacks) { 248 | return; 249 | } 250 | 251 | this._listeners.forEach((fn) => { 252 | fn.call(this, status); 253 | }); 254 | } 255 | 256 | /** 257 | * Scrolls to given position with easing function 258 | */ 259 | scrollTo( 260 | x = this.offset.x, 261 | y = this.offset.y, 262 | duration = 0, 263 | options: Partial<I.ScrollToOptions> = {}, 264 | ) { 265 | scrollTo(this, x, y, duration, options); 266 | } 267 | 268 | /** 269 | * Scrolls the target element into visible area of scrollbar, 270 | * likes the DOM method `element.scrollIntoView(). 271 | */ 272 | scrollIntoView( 273 | elem: HTMLElement, 274 | options: Partial<I.ScrollIntoViewOptions> = {}, 275 | ) { 276 | scrollIntoView(this, elem, options); 277 | } 278 | 279 | /** 280 | * Adds scrolling listener 281 | */ 282 | addListener(fn: I.ScrollListener) { 283 | if (typeof fn !== 'function') { 284 | throw new TypeError('[smooth-scrollbar] scrolling listener should be a function'); 285 | } 286 | 287 | this._listeners.add(fn); 288 | } 289 | 290 | /** 291 | * Removes listener previously registered with `scrollbar.addListener()` 292 | */ 293 | removeListener(fn: I.ScrollListener) { 294 | this._listeners.delete(fn); 295 | } 296 | 297 | /** 298 | * Adds momentum and applys delta transformers. 299 | */ 300 | addTransformableMomentum( 301 | x: number, 302 | y: number, 303 | fromEvent: Event, 304 | callback?: I.AddTransformableMomentumCallback, 305 | ) { 306 | this._updateDebounced(); 307 | 308 | const finalDelta = this._plugins.reduce((delta, plugin) => { 309 | return plugin.transformDelta(delta, fromEvent) || delta; 310 | }, { x, y }); 311 | 312 | const willScroll = !this._shouldPropagateMomentum(finalDelta.x, finalDelta.y); 313 | 314 | if (willScroll) { 315 | this.addMomentum(finalDelta.x, finalDelta.y); 316 | } 317 | 318 | if (callback) { 319 | callback.call(this, willScroll); 320 | } 321 | } 322 | 323 | /** 324 | * Increases scrollbar's momentum 325 | */ 326 | addMomentum(x: number, y: number) { 327 | this.setMomentum( 328 | this._momentum.x + x, 329 | this._momentum.y + y, 330 | ); 331 | } 332 | 333 | /** 334 | * Sets scrollbar's momentum to given value 335 | */ 336 | setMomentum(x: number, y: number) { 337 | if (this.limit.x === 0) { 338 | x = 0; 339 | } 340 | if (this.limit.y === 0) { 341 | y = 0; 342 | } 343 | 344 | if (this.options.renderByPixels) { 345 | x = Math.round(x); 346 | y = Math.round(y); 347 | } 348 | 349 | this._momentum.x = x; 350 | this._momentum.y = y; 351 | } 352 | 353 | /** 354 | * Update options for specific plugin 355 | * 356 | * @param pluginName Name of the plugin 357 | * @param [options] An object includes the properties that you want to update 358 | */ 359 | updatePluginOptions(pluginName: string, options?: any) { 360 | this._plugins.forEach((plugin) => { 361 | if (plugin.name === pluginName) { 362 | Object.assign(plugin.options, options); 363 | } 364 | }); 365 | } 366 | 367 | destroy() { 368 | const { 369 | containerEl, 370 | contentEl, 371 | } = this; 372 | 373 | clearEventsOn(this); 374 | this._listeners.clear(); 375 | 376 | this.setMomentum(0, 0); 377 | cancelAnimationFrame(this._renderID); 378 | 379 | if (this._observer) { 380 | this._observer.disconnect(); 381 | } 382 | 383 | scrollbarMap.delete(this.containerEl); 384 | 385 | // restore contents 386 | const childNodes = Array.from(contentEl.childNodes); 387 | 388 | while (containerEl.firstChild) { 389 | containerEl.removeChild(containerEl.firstChild); 390 | } 391 | 392 | childNodes.forEach((el) => { 393 | containerEl.appendChild(el); 394 | }); 395 | 396 | // reset scroll position 397 | setStyle(containerEl, { 398 | overflow: '', 399 | }); 400 | containerEl.scrollTop = this.scrollTop; 401 | containerEl.scrollLeft = this.scrollLeft; 402 | 403 | // invoke plugin.onDestroy 404 | this._plugins.forEach((plugin) => { 405 | plugin.onDestroy(); 406 | }); 407 | this._plugins.length = 0; 408 | } 409 | 410 | private _init() { 411 | this.update(); 412 | 413 | // init evet handlers 414 | Object.keys(eventHandlers).forEach((prop) => { 415 | eventHandlers[prop](this); 416 | }); 417 | 418 | // invoke `plugin.onInit` 419 | this._plugins.forEach((plugin) => { 420 | plugin.onInit(); 421 | }); 422 | 423 | this._render(); 424 | } 425 | 426 | @debounce(100, true) 427 | private _updateDebounced() { 428 | this.update(); 429 | } 430 | 431 | // check whether to propagate monmentum to parent scrollbar 432 | // the following situations are considered as `true`: 433 | // 1. continuous scrolling is enabled (automatically disabled when overscroll is enabled) 434 | // 2. scrollbar reaches one side and is not about to scroll on the other direction 435 | private _shouldPropagateMomentum(deltaX = 0, deltaY = 0): boolean { 436 | const { 437 | options, 438 | offset, 439 | limit, 440 | } = this; 441 | 442 | if (!options.continuousScrolling) return false; 443 | 444 | // force an update when scrollbar is "unscrollable", see #106 445 | if (limit.x === 0 && limit.y === 0) { 446 | this._updateDebounced(); 447 | } 448 | 449 | const destX = clamp(deltaX + offset.x, 0, limit.x); 450 | const destY = clamp(deltaY + offset.y, 0, limit.y); 451 | let res = true; 452 | 453 | // offsets are not about to change 454 | // `&=` operator is not allowed for boolean types 455 | res = res && (destX === offset.x); 456 | res = res && (destY === offset.y); 457 | 458 | // current offsets are on the edge 459 | res = res && (offset.x === limit.x || offset.x === 0 || offset.y === limit.y || offset.y === 0); 460 | 461 | return res; 462 | } 463 | 464 | private _render() { 465 | const { 466 | _momentum, 467 | } = this; 468 | 469 | if (_momentum.x || _momentum.y) { 470 | const nextX = this._nextTick('x'); 471 | const nextY = this._nextTick('y'); 472 | 473 | _momentum.x = nextX.momentum; 474 | _momentum.y = nextY.momentum; 475 | 476 | this.setPosition(nextX.position, nextY.position); 477 | } 478 | 479 | const remain = { ...this._momentum }; 480 | 481 | this._plugins.forEach((plugin) => { 482 | plugin.onRender(remain); 483 | }); 484 | 485 | this._renderID = requestAnimationFrame(this._render.bind(this)); 486 | } 487 | 488 | private _nextTick(direction: 'x' | 'y'): { momentum: number, position: number } { 489 | const { 490 | options, 491 | offset, 492 | _momentum, 493 | } = this; 494 | 495 | const current = offset[direction]; 496 | const remain = _momentum[direction]; 497 | 498 | if (Math.abs(remain) <= 0.1) { 499 | return { 500 | momentum: 0, 501 | position: current + remain, 502 | }; 503 | } 504 | 505 | let nextMomentum = remain * (1 - options.damping); 506 | 507 | if (options.renderByPixels) { 508 | nextMomentum |= 0; 509 | } 510 | 511 | return { 512 | momentum: nextMomentum, 513 | position: current + remain - nextMomentum, 514 | }; 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /src/scrolling/index.ts: -------------------------------------------------------------------------------- 1 | export * from './set-position'; 2 | export * from './scroll-to'; 3 | export * from './scroll-into-view'; 4 | -------------------------------------------------------------------------------- /src/scrolling/scroll-into-view.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../utils'; 2 | 3 | import * as I from '../interfaces/'; 4 | 5 | export function scrollIntoView( 6 | scrollbar: I.Scrollbar, 7 | elem: HTMLElement, 8 | { 9 | alignToTop = true, 10 | onlyScrollIfNeeded = false, 11 | offsetTop = 0, 12 | offsetLeft = 0, 13 | offsetBottom = 0, 14 | }: Partial<I.ScrollIntoViewOptions> = {}, 15 | ) { 16 | const { 17 | containerEl, 18 | bounding, 19 | offset, 20 | limit, 21 | } = scrollbar; 22 | 23 | if (!elem || !containerEl.contains(elem)) return; 24 | 25 | const targetBounding = elem.getBoundingClientRect(); 26 | 27 | if (onlyScrollIfNeeded && scrollbar.isVisible(elem)) return; 28 | 29 | const delta = alignToTop ? targetBounding.top - bounding.top - offsetTop : targetBounding.bottom - bounding.bottom + offsetBottom; 30 | 31 | scrollbar.setMomentum( 32 | targetBounding.left - bounding.left - offsetLeft, 33 | clamp(delta, -offset.y, limit.y - offset.y), 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/scrolling/scroll-to.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../utils'; 2 | 3 | import * as I from '../interfaces/'; 4 | 5 | const animationIDStorage = new WeakMap<I.Scrollbar, number>(); 6 | 7 | export function scrollTo( 8 | scrollbar: I.Scrollbar, 9 | x: number, 10 | y: number, 11 | duration = 0, 12 | { easing = defaultEasing, callback }: Partial<I.ScrollToOptions> = {}, 13 | ) { 14 | const { 15 | options, 16 | offset, 17 | limit, 18 | } = scrollbar; 19 | 20 | if (options.renderByPixels) { 21 | // ensure resolved with integer 22 | x = Math.round(x); 23 | y = Math.round(y); 24 | } 25 | 26 | const startX = offset.x; 27 | const startY = offset.y; 28 | 29 | const disX = clamp(x, 0, limit.x) - startX; 30 | const disY = clamp(y, 0, limit.y) - startY; 31 | 32 | const start = Date.now(); 33 | 34 | function scroll() { 35 | const elapse = Date.now() - start; 36 | const progress = duration ? easing(Math.min(elapse / duration, 1)) : 1; 37 | 38 | scrollbar.setPosition( 39 | startX + disX * progress, 40 | startY + disY * progress, 41 | ); 42 | 43 | if (elapse >= duration) { 44 | if (typeof callback === 'function') { 45 | callback.call(scrollbar); 46 | } 47 | } else { 48 | const animationID = requestAnimationFrame(scroll); 49 | animationIDStorage.set(scrollbar, animationID); 50 | } 51 | } 52 | 53 | cancelAnimationFrame(animationIDStorage.get(scrollbar) as number); 54 | scroll(); 55 | } 56 | 57 | /** 58 | * easeOutCubic 59 | */ 60 | function defaultEasing(t: number): number { 61 | return (t - 1) ** 3 + 1; 62 | } 63 | -------------------------------------------------------------------------------- /src/scrolling/set-position.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from '../utils'; 2 | import * as I from '../interfaces/'; 3 | 4 | import { 5 | setStyle, 6 | } from '../utils/'; 7 | 8 | export function setPosition( 9 | scrollbar: I.Scrollbar, 10 | x: number, 11 | y: number, 12 | ): I.ScrollStatus | null { 13 | const { 14 | options, 15 | offset, 16 | limit, 17 | track, 18 | contentEl, 19 | } = scrollbar; 20 | 21 | if (options.renderByPixels) { 22 | x = Math.round(x); 23 | y = Math.round(y); 24 | } 25 | 26 | x = clamp(x, 0, limit.x); 27 | y = clamp(y, 0, limit.y); 28 | 29 | // position changed -> show track for 300ms 30 | if (x !== offset.x) track.xAxis.show(); 31 | if (y !== offset.y) track.yAxis.show(); 32 | 33 | if (!options.alwaysShowTracks) { 34 | track.autoHideOnIdle(); 35 | } 36 | 37 | if (x === offset.x && y === offset.y) { 38 | return null; 39 | } 40 | 41 | offset.x = x; 42 | offset.y = y; 43 | 44 | setStyle(contentEl, { 45 | '-transform': `translate3d(${-x}px, ${-y}px, 0)`, 46 | }); 47 | 48 | track.update(); 49 | 50 | return { 51 | offset: { ...offset }, 52 | limit: { ...limit }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/style.ts: -------------------------------------------------------------------------------- 1 | const TRACK_BG = 'rgba(222, 222, 222, .75)'; 2 | const THUMB_BG = 'rgba(0, 0, 0, .5)'; 3 | 4 | // sets content's display type to `flow-root` to suppress margin collapsing 5 | const SCROLLBAR_STYLE = ` 6 | [data-scrollbar] { 7 | display: block; 8 | position: relative; 9 | } 10 | 11 | .scroll-content { 12 | display: flow-root; 13 | -webkit-transform: translate3d(0, 0, 0); 14 | transform: translate3d(0, 0, 0); 15 | } 16 | 17 | .scrollbar-track { 18 | position: absolute; 19 | opacity: 0; 20 | z-index: 1; 21 | background: ${TRACK_BG}; 22 | -webkit-user-select: none; 23 | -moz-user-select: none; 24 | -ms-user-select: none; 25 | user-select: none; 26 | -webkit-transition: opacity 0.5s 0.5s ease-out; 27 | transition: opacity 0.5s 0.5s ease-out; 28 | } 29 | .scrollbar-track.show, 30 | .scrollbar-track:hover { 31 | opacity: 1; 32 | -webkit-transition-delay: 0s; 33 | transition-delay: 0s; 34 | } 35 | 36 | .scrollbar-track-x { 37 | bottom: 0; 38 | left: 0; 39 | width: 100%; 40 | height: 8px; 41 | } 42 | .scrollbar-track-y { 43 | top: 0; 44 | right: 0; 45 | width: 8px; 46 | height: 100%; 47 | } 48 | .scrollbar-thumb { 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | width: 8px; 53 | height: 8px; 54 | background: ${THUMB_BG}; 55 | border-radius: 4px; 56 | } 57 | `; 58 | 59 | const STYLE_ID = 'smooth-scrollbar-style'; 60 | let isStyleAttached = false; 61 | 62 | export function attachStyle() { 63 | if (isStyleAttached || typeof window === 'undefined') { 64 | return; 65 | } 66 | 67 | const styleEl = document.createElement('style'); 68 | styleEl.id = STYLE_ID; 69 | styleEl.textContent = SCROLLBAR_STYLE; 70 | 71 | if (document.head) { 72 | document.head.appendChild(styleEl); 73 | } 74 | 75 | isStyleAttached = true; 76 | } 77 | 78 | export function detachStyle() { 79 | if (!isStyleAttached || typeof window === 'undefined') { 80 | return; 81 | } 82 | 83 | const styleEl = document.getElementById(STYLE_ID); 84 | 85 | if (!styleEl || !styleEl.parentNode) { 86 | return; 87 | } 88 | 89 | styleEl.parentNode.removeChild(styleEl); 90 | 91 | isStyleAttached = false; 92 | } 93 | -------------------------------------------------------------------------------- /src/track/direction.ts: -------------------------------------------------------------------------------- 1 | export enum TrackDirection { 2 | X = 'x', 3 | Y = 'y', 4 | } 5 | -------------------------------------------------------------------------------- /src/track/index.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | 3 | import { ScrollbarTrack } from './track'; 4 | import { TrackDirection } from './direction'; 5 | 6 | import { 7 | debounce, 8 | } from '../decorators/'; 9 | 10 | export class TrackController implements I.TrackController { 11 | readonly xAxis: ScrollbarTrack; 12 | readonly yAxis: ScrollbarTrack; 13 | 14 | constructor( 15 | private _scrollbar: I.Scrollbar, 16 | ) { 17 | const thumbMinSize = _scrollbar.options.thumbMinSize; 18 | 19 | this.xAxis = new ScrollbarTrack(TrackDirection.X, thumbMinSize); 20 | this.yAxis = new ScrollbarTrack(TrackDirection.Y, thumbMinSize); 21 | 22 | this.xAxis.attachTo(_scrollbar.containerEl); 23 | this.yAxis.attachTo(_scrollbar.containerEl); 24 | 25 | if (_scrollbar.options.alwaysShowTracks) { 26 | this.xAxis.show(); 27 | this.yAxis.show(); 28 | } 29 | } 30 | 31 | /** 32 | * Updates track appearance 33 | */ 34 | update() { 35 | const { 36 | size, 37 | offset, 38 | } = this._scrollbar; 39 | 40 | this.xAxis.update(offset.x, size.container.width, size.content.width); 41 | this.yAxis.update(offset.y, size.container.height, size.content.height); 42 | } 43 | 44 | /** 45 | * Automatically hide tracks when scrollbar is in idle state 46 | */ 47 | @debounce(300) 48 | autoHideOnIdle() { 49 | if (this._scrollbar.options.alwaysShowTracks) { 50 | return; 51 | } 52 | 53 | this.xAxis.hide(); 54 | this.yAxis.hide(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/track/thumb.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | import { TrackDirection } from './direction'; 3 | import { setStyle } from '../utils/'; 4 | 5 | export class ScrollbarThumb implements I.ScrollbarThumb { 6 | /** 7 | * Thumb element 8 | */ 9 | readonly element = document.createElement('div'); 10 | 11 | /** 12 | * Display size of the thumb 13 | * will always be greater than `scrollbar.options.thumbMinSize` 14 | */ 15 | displaySize = 0; 16 | 17 | /** 18 | * Actual size of the thumb 19 | */ 20 | realSize = 0; 21 | 22 | /** 23 | * Thumb offset to the top 24 | */ 25 | offset = 0; 26 | 27 | constructor( 28 | private _direction: TrackDirection, 29 | private _minSize = 0, 30 | ) { 31 | this.element.className = `scrollbar-thumb scrollbar-thumb-${_direction}`; 32 | } 33 | 34 | /** 35 | * Attach to track element 36 | * 37 | * @param trackEl Track element 38 | */ 39 | attachTo(trackEl: HTMLElement) { 40 | trackEl.appendChild(this.element); 41 | } 42 | 43 | update( 44 | scrollOffset: number, 45 | containerSize: number, 46 | pageSize: number, 47 | ) { 48 | // calculate thumb size 49 | // pageSize > containerSize -> scrollable 50 | this.realSize = Math.min(containerSize / pageSize, 1) * containerSize; 51 | this.displaySize = Math.max(this.realSize, this._minSize); 52 | 53 | // calculate thumb offset 54 | this.offset = scrollOffset / pageSize * (containerSize + (this.realSize - this.displaySize)); 55 | 56 | setStyle(this.element, this._getStyle()); 57 | } 58 | 59 | private _getStyle() { 60 | switch (this._direction) { 61 | case TrackDirection.X: 62 | return { 63 | width: `${this.displaySize}px`, 64 | '-transform': `translate3d(${this.offset}px, 0, 0)`, 65 | }; 66 | 67 | case TrackDirection.Y: 68 | return { 69 | height: `${this.displaySize}px`, 70 | '-transform': `translate3d(0, ${this.offset}px, 0)`, 71 | }; 72 | 73 | default: 74 | return null; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/track/track.ts: -------------------------------------------------------------------------------- 1 | import * as I from '../interfaces/'; 2 | import { TrackDirection } from './direction'; 3 | import { ScrollbarThumb } from './thumb'; 4 | 5 | import { 6 | setStyle, 7 | } from '../utils/'; 8 | 9 | export class ScrollbarTrack implements I.ScrollbarTrack { 10 | readonly thumb: ScrollbarThumb; 11 | 12 | /** 13 | * Track element 14 | */ 15 | readonly element = document.createElement('div'); 16 | 17 | private _isShown = false; 18 | 19 | constructor( 20 | direction: TrackDirection, 21 | thumbMinSize: number = 0, 22 | ) { 23 | this.element.className = `scrollbar-track scrollbar-track-${direction}`; 24 | 25 | this.thumb = new ScrollbarThumb( 26 | direction, 27 | thumbMinSize, 28 | ); 29 | 30 | this.thumb.attachTo(this.element); 31 | } 32 | 33 | /** 34 | * Attach to scrollbar container element 35 | * 36 | * @param scrollbarContainer Scrollbar container element 37 | */ 38 | attachTo(scrollbarContainer: HTMLElement) { 39 | scrollbarContainer.appendChild(this.element); 40 | } 41 | 42 | /** 43 | * Show track immediately 44 | */ 45 | show() { 46 | if (this._isShown) { 47 | return; 48 | } 49 | 50 | this._isShown = true; 51 | this.element.classList.add('show'); 52 | } 53 | 54 | /** 55 | * Hide track immediately 56 | */ 57 | hide() { 58 | if (!this._isShown) { 59 | return; 60 | } 61 | 62 | this._isShown = false; 63 | this.element.classList.remove('show'); 64 | } 65 | 66 | update( 67 | scrollOffset: number, 68 | containerSize: number, 69 | pageSize: number, 70 | ) { 71 | setStyle(this.element, { 72 | display: pageSize <= containerSize ? 'none' : 'block', 73 | }); 74 | 75 | this.thumb.update(scrollOffset, containerSize, pageSize); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/clamp.ts: -------------------------------------------------------------------------------- 1 | export function clamp(value: number, lower: number, upper: number): number { 2 | return Math.max(lower, Math.min(upper, value)); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce<T extends (...args: any[]) => void>(fn: T, wait: number = 0, leading?: boolean) { 2 | let timer: ReturnType<typeof setTimeout>; 3 | let lastCalledAt = -Infinity; 4 | 5 | return function debouncedFn(this: unknown, ...args: Parameters<T>) { 6 | if (leading) { 7 | const now = Date.now(); 8 | const elapsed = now - lastCalledAt; 9 | lastCalledAt = now; 10 | 11 | if (elapsed >= wait) { 12 | fn.apply(this, args); 13 | } 14 | } 15 | 16 | clearTimeout(timer); 17 | 18 | timer = setTimeout(() => { 19 | fn.apply(this, args); 20 | }, wait); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/event-hub.ts: -------------------------------------------------------------------------------- 1 | import { Scrollbar } from '../interfaces/'; 2 | 3 | export interface EventHandler { 4 | (event: any): void; 5 | } 6 | 7 | type EventConfig = { 8 | elem: EventTarget, 9 | eventName: string, 10 | handler: EventHandler, 11 | }; 12 | 13 | let eventListenerOptions: boolean | EventListenerOptions; 14 | 15 | const eventMap = new WeakMap<Scrollbar, EventConfig[]>(); 16 | 17 | function getOptions(): typeof eventListenerOptions { 18 | if (eventListenerOptions !== undefined) { 19 | return eventListenerOptions; 20 | } 21 | 22 | let supportPassiveEvent = false; 23 | 24 | try { 25 | const noop = () => {}; 26 | const options = Object.defineProperty({}, 'passive', { 27 | enumerable: true, 28 | get() { 29 | supportPassiveEvent = true; 30 | return true; 31 | }, 32 | }); 33 | window.addEventListener('testPassive', noop, options); 34 | window.removeEventListener('testPassive', noop, options); 35 | } catch (e) {} 36 | 37 | eventListenerOptions = supportPassiveEvent ? { passive: false } as EventListenerOptions : false; 38 | 39 | return eventListenerOptions; 40 | } 41 | 42 | export function eventScope(scrollbar: Scrollbar) { 43 | const configs = eventMap.get(scrollbar) || []; 44 | 45 | eventMap.set(scrollbar, configs); 46 | 47 | return function addEvent( 48 | elem: EventTarget, 49 | events: string, 50 | fn: EventHandler, 51 | ) { 52 | function handler(event: any) { 53 | // ignore default prevented events 54 | if (event.defaultPrevented) { 55 | return; 56 | } 57 | 58 | fn(event); 59 | } 60 | 61 | events.split(/\s+/g).forEach((eventName) => { 62 | configs.push({ elem, eventName, handler }); 63 | 64 | elem.addEventListener(eventName, handler, getOptions()); 65 | }); 66 | }; 67 | } 68 | 69 | export function clearEventsOn(scrollbar: Scrollbar) { 70 | const configs = eventMap.get(scrollbar); 71 | 72 | if (!configs) { 73 | return; 74 | } 75 | 76 | configs.forEach(({ elem, eventName, handler }) => { 77 | elem.removeEventListener(eventName, handler, getOptions()); 78 | }); 79 | 80 | eventMap.delete(scrollbar); 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/get-pointer-data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get pointer/touch data 3 | */ 4 | export function getPointerData(evt: any) { 5 | // if is touch event, return last item in touchList 6 | // else return original event 7 | return evt.touches ? evt.touches[evt.touches.length - 1] : evt; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/get-position.ts: -------------------------------------------------------------------------------- 1 | import { getPointerData } from './get-pointer-data'; 2 | 3 | /** 4 | * Get pointer/finger position 5 | */ 6 | export function getPosition(evt: any) { 7 | const data = getPointerData(evt); 8 | 9 | return { 10 | x: data.clientX, 11 | y: data.clientY, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-hub'; 2 | export * from './get-pointer-data'; 3 | export * from './get-position'; 4 | export * from './is-one-of'; 5 | export * from './set-style'; 6 | export * from './touch-record'; 7 | export { clamp } from './clamp'; 8 | export { debounce } from './debounce'; 9 | -------------------------------------------------------------------------------- /src/utils/is-one-of.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if `a` is one of `[...b]` 3 | */ 4 | export function isOneOf(a: any, b: any[] = []): boolean { 5 | return b.some(v => a === v); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/set-style.ts: -------------------------------------------------------------------------------- 1 | const VENDOR_PREFIX = [ 2 | 'webkit', 3 | 'moz', 4 | 'ms', 5 | 'o', 6 | ]; 7 | 8 | const RE = new RegExp(`^-(?!(?:${VENDOR_PREFIX.join('|')})-)`); 9 | 10 | function autoPrefix(styles: any) { 11 | const res = {}; 12 | 13 | Object.keys(styles).forEach((prop) => { 14 | if (!RE.test(prop)) { 15 | res[prop] = styles[prop]; 16 | return; 17 | } 18 | 19 | const val = styles[prop]; 20 | 21 | prop = prop.replace(/^-/, ''); 22 | res[prop] = val; 23 | 24 | VENDOR_PREFIX.forEach((prefix) => { 25 | res[`-${prefix}-${prop}`] = val; 26 | }); 27 | }); 28 | 29 | return res; 30 | } 31 | 32 | export function setStyle(elem: HTMLElement, styles: any) { 33 | styles = autoPrefix(styles); 34 | 35 | Object.keys(styles).forEach((prop) => { 36 | const cssProp = prop.replace(/^-/, '').replace(/-([a-z])/g, (_, $1) => $1.toUpperCase()); 37 | elem.style[cssProp] = styles[prop]; 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/touch-record.ts: -------------------------------------------------------------------------------- 1 | import { getPosition } from './get-position'; 2 | 3 | export class Tracker { 4 | readonly velocityMultiplier = window.devicePixelRatio; 5 | 6 | updateTime = Date.now(); 7 | delta = { x: 0, y: 0 }; 8 | velocity = { x: 0, y: 0 }; 9 | lastPosition = { x: 0, y: 0 }; 10 | 11 | constructor(touch: Touch) { 12 | this.lastPosition = getPosition(touch); 13 | } 14 | 15 | update(touch: Touch) { 16 | const { 17 | velocity, 18 | updateTime, 19 | lastPosition, 20 | } = this; 21 | 22 | const now = Date.now(); 23 | const position = getPosition(touch); 24 | 25 | const delta = { 26 | x: -(position.x - lastPosition.x), 27 | y: -(position.y - lastPosition.y), 28 | }; 29 | 30 | const duration = (now - updateTime) || 16.7; 31 | const vx = delta.x / duration * 16.7; 32 | const vy = delta.y / duration * 16.7; 33 | velocity.x = vx * this.velocityMultiplier; 34 | velocity.y = vy * this.velocityMultiplier; 35 | 36 | this.delta = delta; 37 | this.updateTime = now; 38 | this.lastPosition = position; 39 | } 40 | } 41 | 42 | export class TouchRecord { 43 | private _activeTouchID: number; 44 | private _touchList: { [id: number]: Tracker } = {}; 45 | 46 | private get _primitiveValue() { 47 | return { x: 0, y: 0 }; 48 | } 49 | 50 | isActive() { 51 | return this._activeTouchID !== undefined; 52 | } 53 | 54 | getDelta() { 55 | const tracker = this._getActiveTracker(); 56 | 57 | if (!tracker) { 58 | return this._primitiveValue; 59 | } 60 | 61 | return { ...tracker.delta }; 62 | } 63 | 64 | getVelocity() { 65 | const tracker = this._getActiveTracker(); 66 | 67 | if (!tracker) { 68 | return this._primitiveValue; 69 | } 70 | 71 | return { ...tracker.velocity }; 72 | } 73 | 74 | getEasingDistance(damping: number) { 75 | const deAcceleration = 1 - damping; 76 | 77 | let distance = { 78 | x: 0, 79 | y: 0, 80 | }; 81 | 82 | const vel = this.getVelocity(); 83 | 84 | Object.keys(vel).forEach(dir => { 85 | // ignore small velocity 86 | let v = Math.abs(vel[dir]) <= 10 ? 0 : vel[dir]; 87 | 88 | while (v !== 0) { 89 | distance[dir] += v; 90 | v = (v * deAcceleration) | 0; 91 | } 92 | }); 93 | 94 | return distance; 95 | } 96 | 97 | track(evt: TouchEvent) { 98 | const { 99 | targetTouches, 100 | } = evt; 101 | 102 | Array.from(targetTouches).forEach(touch => { 103 | this._add(touch); 104 | }); 105 | 106 | return this._touchList; 107 | } 108 | 109 | update(evt: TouchEvent) { 110 | const { 111 | touches, 112 | changedTouches, 113 | } = evt; 114 | 115 | Array.from(touches).forEach(touch => { 116 | this._renew(touch); 117 | }); 118 | 119 | this._setActiveID(changedTouches); 120 | 121 | return this._touchList; 122 | } 123 | 124 | release(evt: TouchEvent) { 125 | delete this._activeTouchID; 126 | 127 | Array.from(evt.changedTouches).forEach(touch => { 128 | this._delete(touch); 129 | }); 130 | } 131 | 132 | private _add(touch: Touch) { 133 | if (this._has(touch)) { 134 | // reset tracker 135 | this._delete(touch); 136 | } 137 | 138 | const tracker = new Tracker(touch); 139 | 140 | this._touchList[touch.identifier] = tracker; 141 | } 142 | 143 | private _renew(touch: Touch) { 144 | if (!this._has(touch)) { 145 | return; 146 | } 147 | 148 | const tracker = this._touchList[touch.identifier]; 149 | 150 | tracker.update(touch); 151 | } 152 | 153 | private _delete(touch: Touch) { 154 | delete this._touchList[touch.identifier]; 155 | } 156 | 157 | private _has(touch: Touch): boolean { 158 | return this._touchList.hasOwnProperty(touch.identifier); 159 | } 160 | 161 | private _setActiveID(touches: TouchList) { 162 | this._activeTouchID = touches[touches.length - 1].identifier; 163 | } 164 | 165 | private _getActiveTracker(): Tracker { 166 | const { 167 | _touchList, 168 | _activeTouchID, 169 | } = this; 170 | 171 | return _touchList[_activeTouchID]; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": ".", 4 | "baseUrl": ".", 5 | "target": "es5", 6 | "module": "es6", 7 | "lib": [ 8 | "dom", 9 | "es6" 10 | ], 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "noUnusedParameters": true, 14 | "noImplicitReturns": true, 15 | "strictNullChecks": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "importHelpers": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "paths": { 22 | "smooth-scrollbar": [ "src/index" ], 23 | "smooth-scrollbar/*": [ "src/*" ] 24 | } 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "build", 29 | "dist" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard", 3 | "rules": { 4 | "ter-indent": [true, 2, { "SwitchCase": 1 }], 5 | "semicolon": [true, "always"], 6 | "align": [true, "parameters", "statements"], 7 | "trailing-comma": [true, {"multiline": "always", "singleline": "never", "esSpecCompliant": true}], 8 | "no-empty": false, 9 | "no-unused-variable": false, 10 | "strict-type-predicates": false, 11 | "space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}] 12 | } 13 | } 14 | --------------------------------------------------------------------------------