The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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&#39;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&#39;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 &#39;smooth-scrollbar&#39;;
 95 | 
 96 | Scrollbar.init(document.querySelector(&#39;#my-scrollbar&#39;), 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">&lt;script src=&quot;dist/smooth-scrollbar.js&quot;&gt;&lt;/script&gt;
 99 | 
100 | &lt;script&gt;
101 |   var Scrollbar = window.Scrollbar;
102 | 
103 |   Scrollbar.init(document.querySelector(&#39;#my-scrollbar&#39;), options);
104 | &lt;/script&gt;</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&#39;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">&lt;scrollbar&gt;
184 |     &lt;div class=&quot;scroll-content&quot;&gt;
185 |         your contents here...
186 |     &lt;/div&gt;
187 |     &lt;div class=&quot;scrollbar-track scrollbar-track-x&quot;&gt;
188 |         &lt;div class=&quot;scrollbar-thumb scrollbar-thumb-x&quot;&gt;&lt;/div&gt;
189 |     &lt;/div&gt;
190 |     &lt;div class=&quot;scrollbar-track scrollbar-track-y&quot;&gt;
191 |         &lt;div class=&quot;scrollbar-thumb scrollbar-thumb-y&quot;&gt;&lt;/div&gt;
192 |     &lt;/div&gt;
193 | &lt;/scrollbar&gt;</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>&lt;section data-scrollbar&gt;
208 |   &lt;img src="xxx.jpg"&gt;
209 | &lt;/section&gt;
210 | 
211 | &lt;script&gt; Scrollbar.initAll(); &lt;/script&gt;</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'` &#124; `'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 | ![diagram](assets/diagram.gif)
 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 | 


--------------------------------------------------------------------------------