├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── documentation
└── asset
│ ├── logo.png
│ └── logo.svg
├── package.json
├── prettier.config.js
├── rollup.config.js
├── scaffold.config.js
├── src
├── adjustable-element
│ └── i-adjustable-element.ts
├── index.ts
├── original
│ ├── element
│ │ ├── scroll-by.ts
│ │ ├── scroll-into-view.ts
│ │ ├── scroll-left.ts
│ │ ├── scroll-to.ts
│ │ ├── scroll-top.ts
│ │ └── scroll.ts
│ └── window
│ │ ├── scroll-by.ts
│ │ ├── scroll-to.ts
│ │ └── scroll.ts
├── patch
│ ├── anchor
│ │ └── catch-navigation.ts
│ ├── element
│ │ ├── compute-scroll-into-view.ts
│ │ ├── scroll-by.ts
│ │ ├── scroll-into-view.ts
│ │ ├── scroll-left.ts
│ │ ├── scroll-to.ts
│ │ ├── scroll-top.ts
│ │ └── scroll.ts
│ ├── patch.ts
│ ├── shared.ts
│ └── window
│ │ ├── scroll-by.ts
│ │ ├── scroll-to.ts
│ │ └── scroll.ts
├── scroll-method
│ ├── get-original-scroll-method-for-kind.ts
│ └── scroll-method-name.ts
├── smooth-scroll
│ ├── get-smooth-scroll-options
│ │ └── get-smooth-scroll-options.ts
│ ├── smooth-scroll-options
│ │ └── i-smooth-scroll-options.ts
│ └── smooth-scroll
│ │ └── smooth-scroll.ts
├── support
│ ├── supports-element-prototype-scroll-methods.ts
│ ├── supports-scroll-behavior.ts
│ └── unsupported-environment.ts
└── util
│ ├── attribute.ts
│ ├── disable-scroll-snap.ts
│ ├── easing.ts
│ ├── ensure-numeric.ts
│ ├── find-nearest-ancestor-with-scroll-behavior.ts
│ ├── find-nearest-root.ts
│ ├── get-location-origin.ts
│ ├── get-parent.ts
│ ├── get-scroll-behavior.ts
│ ├── is-scroll-to-options.ts
│ ├── now.ts
│ ├── scroll-snappable.ts
│ └── scrolling-element.ts
├── tsconfig.dist.json
├── tsconfig.json
└── tslint.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /compiled/
3 | /dist/
4 | /typings/
5 | package-lock.json
6 | /.idea/
7 | /.cache/
8 | /.vscode/
9 | *.log
10 | /logs/
11 | npm-debug.log*
12 | /lib-cov/
13 | /coverage/
14 | /.nyc_output/
15 | /.grunt/
16 | *.7z
17 | *.dmg
18 | *.gz
19 | *.iso
20 | *.jar
21 | *.rar
22 | *.tar
23 | *.zip
24 | .tgz
25 | .env
26 | .DS_Store
27 | .DS_Store?
28 | ._*
29 | .Spotlight-V100
30 | .Trashes
31 | ehthumbs.db
32 | Thumbs.db
33 | *.pem
34 | *.p12
35 | *.crt
36 | *.csr
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [2.0.13](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.12...v2.0.13) (2019-10-16)
2 |
3 | ### Features
4 |
5 | - **compatibility:** Support IE 11. [#4](https://github.com/wessberg/scroll-behavior-polyfill/issues/4) ([71089d7](https://github.com/wessberg/scroll-behavior-polyfill/commit/71089d72a3bded13d9cd47d749028666490fa62d))
6 |
7 | ## [2.0.12](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.11...v2.0.12) (2019-09-09)
8 |
9 | ### Bug Fixes
10 |
11 | - anchor detection if the link also contains a path fails. Closes [#9](https://github.com/wessberg/scroll-behavior-polyfill/issues/9) ([4b14f30](https://github.com/wessberg/scroll-behavior-polyfill/commit/4b14f309d6b243850f71aeed82e0698f7cd786ad))
12 |
13 | ## [2.0.11](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.10...v2.0.11) (2019-08-12)
14 |
15 | ### Bug Fixes
16 |
17 | - **declarations:** marks declarations as a module to help with async imports. Closes [#8](https://github.com/wessberg/scroll-behavior-polyfill/issues/8) ([ac1e99e](https://github.com/wessberg/scroll-behavior-polyfill/commit/ac1e99eee72db6525fe0b7f7bf6df2723ca08a81))
18 |
19 | ## [2.0.10](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.9...v2.0.10) (2019-07-18)
20 |
21 | ### Bug Fixes
22 |
23 | - **bug:** makes it possible to use the polyfill without native WeakMap support ([012b486](https://github.com/wessberg/scroll-behavior-polyfill/commit/012b486b4f79559a5b2bfb7fc2e861144d927040))
24 |
25 | ## [2.0.9](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.8...v2.0.9) (2019-07-18)
26 |
27 | ### Bug Fixes
28 |
29 | - **bug:** fixes an issue when using this polyfill along with css-scroll-snap. Closes [#5](https://github.com/wessberg/scroll-behavior-polyfill/issues/5) ([c50582b](https://github.com/wessberg/scroll-behavior-polyfill/commit/c50582b3a4d64e8621e05ce247dc8199c2a5c5dd)), closes [#7](https://github.com/wessberg/scroll-behavior-polyfill/issues/7)
30 |
31 | ## [2.0.8](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.7...v2.0.8) (2019-06-21)
32 |
33 | ### Bug Fixes
34 |
35 | - **typings:** fixes issue that would lead to Typescript errors due to global namespace annotations ([75fd236](https://github.com/wessberg/scroll-behavior-polyfill/commit/75fd236b4e80b2eba9f56ef4029f2e7f108f06bb))
36 |
37 | ## [2.0.7](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.6...v2.0.7) (2019-02-23)
38 |
39 | ### Bug Fixes
40 |
41 | - **bug:** fixes an issue where anchor scrolling could lead to the wrong coordinates under some circumstances ([2f399f4](https://github.com/wessberg/scroll-behavior-polyfill/commit/2f399f4243f5cea82cc7d291f74fd689ab771f81))
42 |
43 | ## [2.0.6](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.5...v2.0.6) (2019-02-09)
44 |
45 | ## [2.0.5](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.4...v2.0.5) (2019-02-07)
46 |
47 | ## [2.0.4](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.3...v2.0.4) (2019-01-24)
48 |
49 | ### Bug Fixes
50 |
51 | - **package.json:** updates 'engine' field to reflect support for older Node.js versions. Fixes [#3](https://github.com/wessberg/scroll-behavior-polyfill/issues/3) ([8b49156](https://github.com/wessberg/scroll-behavior-polyfill/commit/8b4915634292f94c6a7a2bab5cf867283c3ae215))
52 |
53 | ## [2.0.3](https://github.com/wessberg/scroll-behavior-polyfill/compare/2.0.3...v2.0.3) (2019-01-11)
54 |
55 | ## [2.0.2](https://github.com/wessberg/scroll-behavior-polyfill/compare/v2.0.1...v2.0.2) (2019-01-11)
56 |
57 | ### Features
58 |
59 | - **improvements:** Bug fixes, support for scrolling via the scrollTop and scrollLeft setters, and more ([4989d15](https://github.com/wessberg/scroll-behavior-polyfill/commit/4989d15ef53196e8619a161211214e412bbd09bc))
60 |
61 | ## [2.0.1](https://github.com/wessberg/scroll-behavior-polyfill/compare/2.0.1...v2.0.1) (2019-01-09)
62 |
63 | # [2.0.0](https://github.com/wessberg/scroll-behavior-polyfill/compare/v1.0.2...v2.0.0) (2019-01-09)
64 |
65 | ### Features
66 |
67 | - **release:** new major version and rewritten from scratch. ([5647eb3](https://github.com/wessberg/scroll-behavior-polyfill/commit/5647eb3fc041d613f2ce8290c8a7733ca081ca8b))
68 |
69 | ### BREAKING CHANGES
70 |
71 | - **release:** CSS Stylesheets will no longer be parsed. Instead, you must either set inline styles, an attribute with the same name, or set it imperatively. Of course, you can still use the imperative API.
72 |
73 | ## [1.0.2](https://github.com/wessberg/scroll-behavior-polyfill/compare/v1.0.1...v1.0.2) (2017-09-08)
74 |
75 | ## 1.0.1 (2017-09-08)
76 |
--------------------------------------------------------------------------------
/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 any of the code of conduct enforcers: [Frederik Wessberg](mailto:frederikwessberg@hotmail.com) ([@FredWessberg](https://twitter.com/FredWessberg)) ([Website](https://github.com/wessberg)).
59 | All 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, version 1.4,
71 | available at http://contributor-covenant.org/version/1/4/
72 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | You are more than welcome to contribute to `scroll-behavior-polyfill` in any way you please, including:
2 |
3 | - Updating documentation.
4 | - Fixing spelling and grammar
5 | - Adding tests
6 | - Fixing issues and suggesting new features
7 | - Blogging, tweeting, and creating tutorials about `scroll-behavior-polyfill`
8 | - Reaching out to [@FredWessberg](https://twitter.com/FredWessberg) on Twitter
9 | - Submit an issue or a Pull Request
10 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2019 [Frederik Wessberg](mailto:frederikwessberg@hotmail.com) ([@FredWessberg](https://twitter.com/FredWessberg)) ([Website](https://github.com/wessberg))
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 |
8 |
9 | > A polyfill for the 'scroll-behavior' CSS-property
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ## Description
28 |
29 |
30 |
31 | The [`scroll-behavior`](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior) CSS-property as well as the extensions to the Element interface in the [CSSOM View Module](https://drafts.csswg.org/cssom-view/#dom-element-scrollto-options-options) CSS property sets the behavior for a scrolling box when scrolling is triggered by the navigation or CSSOM scrolling APIs.
32 | This polyfill brings this new feature to all browsers.
33 |
34 | It is very efficient, tiny, and works with the latest browser technologies such as Shadow DOM.
35 |
36 | This polyfill also implements the extensions to the Element interface in the [CSSOM View Module](https://drafts.csswg.org/cssom-view/#dom-element-scrollto-options-options) such as `Element.prototype.scroll`, `Element.prototype.scrollTo`, `Element.protype.scrollBy`, and `Element.prototype.scrollIntoView`.
37 |
38 |
39 |
40 | ### Features
41 |
42 |
43 |
44 | - Spec-compliant
45 | - Tiny
46 | - Efficient
47 | - Works with the latest browser technologies, including Shadow DOM
48 | - Seamless
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | ## Table of Contents
57 |
58 | - [Description](#description)
59 | - [Features](#features)
60 | - [Table of Contents](#table-of-contents)
61 | - [Install](#install)
62 | - [NPM](#npm)
63 | - [Yarn](#yarn)
64 | - [Applying the polyfill](#applying-the-polyfill)
65 | - [Usage](#usage)
66 | - [Declarative API](#declarative-api)
67 | - [Imperative API](#imperative-api)
68 | - [Dependencies & Browser support](#dependencies--browser-support)
69 | - [Contributing](#contributing)
70 | - [Maintainers](#maintainers)
71 | - [Backers](#backers)
72 | - [Patreon](#patreon)
73 | - [FAQ](#faq)
74 | - [Are there any known quirks?](#are-there-any-known-quirks)
75 | - [License](#license)
76 |
77 |
78 |
79 |
80 |
81 | ## Install
82 |
83 | ### NPM
84 |
85 | ```
86 | $ npm install scroll-behavior-polyfill
87 | ```
88 |
89 | ### Yarn
90 |
91 | ```
92 | $ yarn add scroll-behavior-polyfill
93 | ```
94 |
95 |
96 |
97 | ## Applying the polyfill
98 |
99 | The polyfill will be feature detected and applied if and only if the browser doesn't support the property already.
100 | To include it, add this somewhere:
101 |
102 | ```typescript
103 | import "scroll-behavior-polyfill";
104 | ```
105 |
106 | However, it is strongly suggested that you only include the polyfill for browsers that doesn't already support `scroll-behavior`.
107 | One way to do so is with an async import:
108 |
109 | ```typescript
110 | if (!("scrollBehavior" in document.documentElement.style)) {
111 | await import("scroll-behavior-polyfill");
112 | }
113 | ```
114 |
115 | Alternatively, you can use [Polyfill.app](https://github.com/wessberg/Polyfiller) which uses this polyfill and takes care of only loading the polyfill if needed as well as adding the language features that the polyfill depends on (See [dependencies](#dependencies--browser-support)).
116 |
117 |
118 |
119 | ## Usage
120 |
121 |
122 |
123 | ### Declarative API
124 |
125 | You can define the `scroll-behavior` of Elements via one of the following approaches:
126 |
127 | - A style attribute including a `scroll-behavior` property.
128 | - An element with a `scroll-behavior` attribute.
129 | - Or, an element with a `CSSStyleDeclaration` with a `scrollBehavior` property.
130 |
131 | This means that either of the following approaches will work:
132 |
133 | ```html
134 |
135 |
136 |
137 |
138 |
139 |
143 | ```
144 |
145 | See [this section](#are-there-any-known-quirks) for information about why `scroll-behavior` values provided in stylesheets won't be discovered by the polyfill.
146 |
147 | ### Imperative API
148 |
149 | You can of course also use the imperative `scroll()`, `scrollTo`, `scrollBy`, and `scrollIntoView` APIs and provide `scroll-behavior` options.
150 |
151 | For example:
152 |
153 | ```typescript
154 | // Works for the window object
155 | window.scroll({
156 | behavior: "smooth",
157 | top: 100,
158 | left: 0
159 | });
160 |
161 | // Works for any element (and supports all options)
162 | myElement.scrollIntoView();
163 |
164 | myElement.scrollBy({
165 | behavior: "smooth",
166 | top: 50,
167 | left: 0
168 | });
169 | ```
170 |
171 | You can also use the `scrollTop` and `scrollLeft` setters, both of which works with the polyfill too:
172 |
173 | ```typescript
174 | element.scrollTop += 100;
175 | element.scrollLeft += 50;
176 | ```
177 |
178 | ## Dependencies & Browser support
179 |
180 | This polyfill is distributed in ES3-compatible syntax, but is using some modern APIs and language features which must be available:
181 |
182 | - `requestAnimationFrame`
183 | - `Object.getOwnPropertyDescriptor`
184 | - `Object.defineProperty`
185 |
186 | For by far the most browsers, these features will already be natively available.
187 | Generally, I would highly recommend using something like [Polyfill.app](https://github.com/wessberg/Polyfiller) which takes care of this stuff automatically.
188 |
189 |
190 |
191 | ## Contributing
192 |
193 | Do you want to contribute? Awesome! Please follow [these recommendations](./CONTRIBUTING.md).
194 |
195 |
196 |
197 |
198 |
199 | ## Maintainers
200 |
201 | |
|
202 | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
203 | | [Frederik Wessberg](mailto:frederikwessberg@hotmail.com)
Twitter: [@FredWessberg](https://twitter.com/FredWessberg)
_Lead Developer_ |
204 |
205 |
206 |
207 |
208 |
209 | ## Backers
210 |
211 | ### Patreon
212 |
213 | [Become a backer](https://www.patreon.com/bePatron?u=11315442) and get your name, avatar, and Twitter handle listed here.
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 | ## FAQ
222 |
223 |
224 |
225 | ### Are there any known quirks?
226 |
227 | - `scroll-behavior` properties declared only in stylesheets won't be discovered. This is because [polyfilling CSS is hard and really bad for performance](https://philipwalton.com/articles/the-dark-side-of-polyfilling-css/).
228 |
229 |
230 |
231 | ## License
232 |
233 | MIT © [Frederik Wessberg](mailto:frederikwessberg@hotmail.com) ([@FredWessberg](https://twitter.com/FredWessberg)) ([Website](https://github.com/wessberg))
234 |
235 |
236 |
--------------------------------------------------------------------------------
/documentation/asset/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wessberg/scroll-behavior-polyfill/0e4973c514f0fc713dbe6a617273c2ce1db29e42/documentation/asset/logo.png
--------------------------------------------------------------------------------
/documentation/asset/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scroll-behavior-polyfill",
3 | "version": "2.0.13",
4 | "description": "A polyfill for the 'scroll-behavior' CSS-property",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/wessberg/scroll-behavior-polyfill.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/wessberg/scroll-behavior-polyfill/issues"
11 | },
12 | "scripts": {
13 | "generate:readme": "scaffold readme --yes",
14 | "generate:license": "scaffold license --yes",
15 | "generate:contributing": "scaffold contributing --yes",
16 | "generate:coc": "scaffold coc --yes",
17 | "generate:changelog": "standard-changelog --first-release",
18 | "generate:all": "npm run generate:license & npm run generate:contributing & npm run generate:coc & npm run generate:readme && npm run generate:changelog",
19 | "clean:dist": "rm -rf dist",
20 | "clean:compiled": "rm -rf compiled",
21 | "clean": "npm run clean:dist && npm run clean:compiled",
22 | "lint": "tsc --noEmit && tslint -c tslint.json --project tsconfig.json",
23 | "prettier": "prettier --write '{src,documentation}/**/*.{js,ts,json,html,xml,css,md}'",
24 | "prebuild": "npm run clean:dist",
25 | "build": "npm run rollup",
26 | "rollup": "rollup -c rollup.config.js",
27 | "preversion": "npm run lint && NODE_ENV=production npm run build",
28 | "version": "npm run generate:all && git add .",
29 | "release": "np --no-cleanup --no-yarn"
30 | },
31 | "files": [
32 | "dist/**/*.*"
33 | ],
34 | "keywords": [
35 | "scroll-behavior",
36 | "smooth-scrolling",
37 | "polyfill",
38 | "css",
39 | "smooth",
40 | "scroll behavior"
41 | ],
42 | "contributors": [
43 | {
44 | "name": "Frederik Wessberg",
45 | "email": "frederikwessberg@hotmail.com",
46 | "url": "https://github.com/wessberg",
47 | "imageUrl": "https://avatars2.githubusercontent.com/u/20454213?s=460&v=4",
48 | "role": "Lead Developer",
49 | "twitter": "FredWessberg"
50 | }
51 | ],
52 | "license": "MIT",
53 | "devDependencies": {
54 | "surge": "0.21.3",
55 | "@wessberg/rollup-plugin-ts": "1.1.72",
56 | "@wessberg/scaffold": "1.0.19",
57 | "@wessberg/ts-config": "^0.0.41",
58 | "rollup": "^1.24.0",
59 | "rollup-plugin-node-resolve": "^5.2.0",
60 | "tslib": "^1.10.0",
61 | "tslint": "^5.20.0",
62 | "typescript": "^3.6.4",
63 | "standard-changelog": "^2.0.15",
64 | "prettier": "^1.18.2",
65 | "pretty-quick": "^2.0.0",
66 | "husky": "^3.0.9",
67 | "np": "^5.1.1"
68 | },
69 | "dependencies": {},
70 | "main": "./dist/index.js",
71 | "module": "./dist/index.js",
72 | "browser": "./dist/index.js",
73 | "types": "./dist/index.d.ts",
74 | "typings": "./dist/index.d.ts",
75 | "es2015": "./dist/index.js",
76 | "engines": {
77 | "node": ">=4.0.0"
78 | },
79 | "husky": {
80 | "hooks": {
81 | "pre-commit": "pretty-quick --staged"
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require("@wessberg/ts-config/prettier.config");
2 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import ts from "@wessberg/rollup-plugin-ts";
2 | import resolve from "rollup-plugin-node-resolve";
3 | import packageJson from "./package.json";
4 |
5 | export default {
6 | input: "src/index.ts",
7 | output: [
8 | {
9 | file: packageJson.main,
10 | format: "iife",
11 | sourcemap: true
12 | }
13 | ],
14 | context: "window",
15 | treeshake: true,
16 | plugins: [
17 | ts({
18 | tsconfig: process.env.NODE_ENV === "production" ? "tsconfig.dist.json" : "tsconfig.json"
19 | }),
20 | resolve()
21 | ]
22 | };
23 |
--------------------------------------------------------------------------------
/scaffold.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require("@wessberg/ts-config/scaffold.config"),
3 | logo: {
4 | url:
5 | "https://raw.githubusercontent.com/wessberg/scroll-behavior-polyfill/master/documentation/asset/logo.png",
6 | height: 60
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/adjustable-element/i-adjustable-element.ts:
--------------------------------------------------------------------------------
1 | export interface IAdjustableElement extends Element {
2 | __adjustingScrollPosition?: boolean;
3 | }
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {SUPPORTS_SCROLL_BEHAVIOR} from "./support/supports-scroll-behavior";
2 | import {patch} from "./patch/patch";
3 | import {SUPPORTS_ELEMENT_PROTOTYPE_SCROLL_METHODS} from "./support/supports-element-prototype-scroll-methods";
4 | import {UNSUPPORTED_ENVIRONMENT} from "./support/unsupported-environment";
5 |
6 | if (!UNSUPPORTED_ENVIRONMENT && (!SUPPORTS_SCROLL_BEHAVIOR || !SUPPORTS_ELEMENT_PROTOTYPE_SCROLL_METHODS)) {
7 | patch();
8 | }
9 |
--------------------------------------------------------------------------------
/src/original/element/scroll-by.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const ELEMENT_ORIGINAL_SCROLL_BY = UNSUPPORTED_ENVIRONMENT ? undefined : Element.prototype.scrollBy;
4 |
--------------------------------------------------------------------------------
/src/original/element/scroll-into-view.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const ELEMENT_ORIGINAL_SCROLL_INTO_VIEW = UNSUPPORTED_ENVIRONMENT ? undefined : Element.prototype.scrollIntoView;
4 |
--------------------------------------------------------------------------------
/src/original/element/scroll-left.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const ELEMENT_ORIGINAL_SCROLL_LEFT_SET_DESCRIPTOR = UNSUPPORTED_ENVIRONMENT
4 | ? undefined
5 | : Object.getOwnPropertyDescriptor(Element.prototype, "scrollLeft")!.set!;
6 |
--------------------------------------------------------------------------------
/src/original/element/scroll-to.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const ELEMENT_ORIGINAL_SCROLL_TO = UNSUPPORTED_ENVIRONMENT ? undefined : Element.prototype.scrollTo;
4 |
--------------------------------------------------------------------------------
/src/original/element/scroll-top.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const ELEMENT_ORIGINAL_SCROLL_TOP_SET_DESCRIPTOR = UNSUPPORTED_ENVIRONMENT
4 | ? undefined
5 | : Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop")!.set!;
6 |
--------------------------------------------------------------------------------
/src/original/element/scroll.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const ELEMENT_ORIGINAL_SCROLL = UNSUPPORTED_ENVIRONMENT ? undefined : Element.prototype.scroll;
4 |
--------------------------------------------------------------------------------
/src/original/window/scroll-by.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const WINDOW_ORIGINAL_SCROLL_BY = UNSUPPORTED_ENVIRONMENT ? undefined : window.scrollBy;
4 |
--------------------------------------------------------------------------------
/src/original/window/scroll-to.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const WINDOW_ORIGINAL_SCROLL_TO = UNSUPPORTED_ENVIRONMENT ? undefined : window.scrollTo;
4 |
--------------------------------------------------------------------------------
/src/original/window/scroll.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "../../support/unsupported-environment";
2 |
3 | export const WINDOW_ORIGINAL_SCROLL = UNSUPPORTED_ENVIRONMENT ? undefined : window.scroll;
4 |
--------------------------------------------------------------------------------
/src/patch/anchor/catch-navigation.ts:
--------------------------------------------------------------------------------
1 | import {findNearestAncestorsWithScrollBehavior} from "../../util/find-nearest-ancestor-with-scroll-behavior";
2 | import {findNearestRoot} from "../../util/find-nearest-root";
3 | import {getLocationOrigin} from "../../util/get-location-origin";
4 |
5 | /**
6 | * A Regular expression that matches id's of the form "#[digit]"
7 | * @type {RegExp}
8 | */
9 | const ID_WITH_LEADING_DIGIT_REGEXP = /^#\d/;
10 |
11 | /**
12 | * Catches anchor navigation to IDs within the same root and ensures that they can be smooth-scrolled
13 | * if the scroll behavior is smooth in the first rooter within that context
14 | */
15 | export function catchNavigation(): void {
16 | // Listen for 'click' events globally
17 | window.addEventListener("click", e => {
18 | // Only work with trusted events on HTMLAnchorElements
19 | if (!e.isTrusted || !(e.target instanceof HTMLAnchorElement)) return;
20 |
21 | const {pathname, search, hash} = e.target;
22 | const pointsToCurrentPage =
23 | getLocationOrigin(e.target) === getLocationOrigin(location) && pathname === location.pathname && search === location.search;
24 |
25 | // Only work with HTMLAnchorElements that navigates to a specific ID on the current page
26 | if (!pointsToCurrentPage || hash == null || hash.length < 1) {
27 | return;
28 | }
29 |
30 | // Find the nearest root, whether it be a ShadowRoot or the document itself
31 | const root = findNearestRoot(e.target);
32 |
33 | // Attempt to match the selector from that root. querySelector' doesn't support IDs that start with a digit, so work around that limitation
34 | const elementMatch = hash.match(ID_WITH_LEADING_DIGIT_REGEXP) != null ? root.getElementById(hash.slice(1)) : root.querySelector(hash);
35 |
36 | // If no selector could be found, don't proceed
37 | if (elementMatch == null) return;
38 |
39 | // Find the nearest ancestor that can be scrolled
40 | const [, behavior] = findNearestAncestorsWithScrollBehavior(elementMatch);
41 |
42 | // If the behavior isn't smooth, don't proceed
43 | if (behavior !== "smooth") return;
44 |
45 | // Otherwise, first prevent the default action.
46 | e.preventDefault();
47 |
48 | // Now, scroll to the element with that ID
49 | elementMatch.scrollIntoView({
50 | behavior
51 | });
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/src/patch/element/compute-scroll-into-view.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The majority of this file is based on https://github.com/stipsan/compute-scroll-into-view (MIT license),
3 | * but has been rewritten to accept a scroller as an argument.
4 | */
5 | import {getScrollingElement} from "../../util/scrolling-element";
6 |
7 | // tslint:disable
8 |
9 | interface IVisualViewport {
10 | height: number;
11 | width: number;
12 | }
13 |
14 | declare var visualViewport: IVisualViewport;
15 |
16 | interface VisualViewportable {
17 | visualViewport?: {
18 | height: number;
19 | width: number;
20 | };
21 | }
22 |
23 | /**
24 | * Find out which edge to align against when logical scroll position is "nearest"
25 | * Interesting fact: "nearest" works similarly to "if-needed", if the element is fully visible it will not scroll it
26 | *
27 | * Legends:
28 | * ┌────────┐ ┏ ━ ━ ━ ┓
29 | * │ target │ frame
30 | * └────────┘ ┗ ━ ━ ━ ┛
31 | */
32 | function alignNearest(
33 | scrollingEdgeStart: number,
34 | scrollingEdgeEnd: number,
35 | scrollingSize: number,
36 | scrollingBorderStart: number,
37 | scrollingBorderEnd: number,
38 | elementEdgeStart: number,
39 | elementEdgeEnd: number,
40 | elementSize: number
41 | ) {
42 | /**
43 | * If element edge A and element edge B are both outside scrolling box edge A and scrolling box edge B
44 | *
45 | * ┌──┐
46 | * ┏━│━━│━┓
47 | * │ │
48 | * ┃ │ │ ┃ do nothing
49 | * │ │
50 | * ┗━│━━│━┛
51 | * └──┘
52 | *
53 | * If element edge C and element edge D are both outside scrolling box edge C and scrolling box edge D
54 | *
55 | * ┏ ━ ━ ━ ━ ┓
56 | * ┌───────────┐
57 | * │┃ ┃│ do nothing
58 | * └───────────┘
59 | * ┗ ━ ━ ━ ━ ┛
60 | */
61 | if (
62 | (elementEdgeStart < scrollingEdgeStart && elementEdgeEnd > scrollingEdgeEnd) ||
63 | (elementEdgeStart > scrollingEdgeStart && elementEdgeEnd < scrollingEdgeEnd)
64 | ) {
65 | return 0;
66 | }
67 |
68 | /**
69 | * If element edge A is outside scrolling box edge A and element height is less than scrolling box height
70 | *
71 | * ┌──┐
72 | * ┏━│━━│━┓ ┏━┌━━┐━┓
73 | * └──┘ │ │
74 | * from ┃ ┃ to ┃ └──┘ ┃
75 | *
76 | * ┗━ ━━ ━┛ ┗━ ━━ ━┛
77 | *
78 | * If element edge B is outside scrolling box edge B and element height is greater than scrolling box height
79 | *
80 | * ┏━ ━━ ━┓ ┏━┌━━┐━┓
81 | * │ │
82 | * from ┃ ┌──┐ ┃ to ┃ │ │ ┃
83 | * │ │ │ │
84 | * ┗━│━━│━┛ ┗━│━━│━┛
85 | * │ │ └──┘
86 | * │ │
87 | * └──┘
88 | *
89 | * If element edge C is outside scrolling box edge C and element width is less than scrolling box width
90 | *
91 | * from to
92 | * ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
93 | * ┌───┐ ┌───┐
94 | * │ ┃ │ ┃ ┃ │ ┃
95 | * └───┘ └───┘
96 | * ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
97 | *
98 | * If element edge D is outside scrolling box edge D and element width is greater than scrolling box width
99 | *
100 | * from to
101 | * ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
102 | * ┌───────────┐ ┌───────────┐
103 | * ┃ │ ┃ │ ┃ ┃ │
104 | * └───────────┘ └───────────┘
105 | * ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
106 | */
107 | if (
108 | (elementEdgeStart <= scrollingEdgeStart && elementSize <= scrollingSize) ||
109 | (elementEdgeEnd >= scrollingEdgeEnd && elementSize >= scrollingSize)
110 | ) {
111 | return elementEdgeStart - scrollingEdgeStart - scrollingBorderStart;
112 | }
113 |
114 | /**
115 | * If element edge B is outside scrolling box edge B and element height is less than scrolling box height
116 | *
117 | * ┏━ ━━ ━┓ ┏━ ━━ ━┓
118 | *
119 | * from ┃ ┃ to ┃ ┌──┐ ┃
120 | * ┌──┐ │ │
121 | * ┗━│━━│━┛ ┗━└━━┘━┛
122 | * └──┘
123 | *
124 | * If element edge A is outside scrolling box edge A and element height is greater than scrolling box height
125 | *
126 | * ┌──┐
127 | * │ │
128 | * │ │ ┌──┐
129 | * ┏━│━━│━┓ ┏━│━━│━┓
130 | * │ │ │ │
131 | * from ┃ └──┘ ┃ to ┃ │ │ ┃
132 | * │ │
133 | * ┗━ ━━ ━┛ ┗━└━━┘━┛
134 | *
135 | * If element edge C is outside scrolling box edge C and element width is greater than scrolling box width
136 | *
137 | * from to
138 | * ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
139 | * ┌───────────┐ ┌───────────┐
140 | * │ ┃ │ ┃ │ ┃ ┃
141 | * └───────────┘ └───────────┘
142 | * ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
143 | *
144 | * If element edge D is outside scrolling box edge D and element width is less than scrolling box width
145 | *
146 | * from to
147 | * ┏ ━ ━ ━ ━ ┓ ┏ ━ ━ ━ ━ ┓
148 | * ┌───┐ ┌───┐
149 | * ┃ │ ┃ │ ┃ │ ┃
150 | * └───┘ └───┘
151 | * ┗ ━ ━ ━ ━ ┛ ┗ ━ ━ ━ ━ ┛
152 | *
153 | */
154 | if ((elementEdgeEnd > scrollingEdgeEnd && elementSize < scrollingSize) || (elementEdgeStart < scrollingEdgeStart && elementSize > scrollingSize)) {
155 | return elementEdgeEnd - scrollingEdgeEnd + scrollingBorderEnd;
156 | }
157 |
158 | return 0;
159 | }
160 |
161 | export function computeScrollIntoView(target: Element, scroller: Element, options: ScrollIntoViewOptions): Pick {
162 | const {block, inline} = options;
163 |
164 | // Used to handle the top most element that can be scrolled
165 | const scrollingElement = getScrollingElement();
166 |
167 | // Support pinch-zooming properly, making sure elements scroll into the visual viewport
168 | // Browsers that don't support visualViewport will report the layout viewport dimensions on document.documentElement.clientWidth/Height
169 | // and viewport dimensions on window.innerWidth/Height
170 | // https://www.quirksmode.org/mobile/viewports2.html
171 | // https://bokand.github.io/viewport/index.html
172 | const viewportWidth = (window as VisualViewportable).visualViewport != null ? visualViewport.width : innerWidth;
173 | const viewportHeight = (window as VisualViewportable).visualViewport != null ? visualViewport.height : innerHeight;
174 |
175 | const viewportX = window.scrollX != null ? window.scrollX : window.pageXOffset;
176 | const viewportY = window.scrollY != null ? window.scrollY : window.pageYOffset;
177 |
178 | const {
179 | height: targetHeight,
180 | width: targetWidth,
181 | top: targetTop,
182 | right: targetRight,
183 | bottom: targetBottom,
184 | left: targetLeft
185 | } = target.getBoundingClientRect();
186 |
187 | // These values mutate as we loop through and generate scroll coordinates
188 | const targetBlock: number = block === "start" || block === "nearest" ? targetTop : block === "end" ? targetBottom : targetTop + targetHeight / 2; // block === 'center
189 | const targetInline: number = inline === "center" ? targetLeft + targetWidth / 2 : inline === "end" ? targetRight : targetLeft; // inline === 'start || inline === 'nearest
190 |
191 | const {height, width, top, right, bottom, left} = scroller.getBoundingClientRect();
192 |
193 | const frameStyle = getComputedStyle(scroller);
194 | const borderLeft = parseInt(frameStyle.borderLeftWidth as string, 10);
195 | const borderTop = parseInt(frameStyle.borderTopWidth as string, 10);
196 | const borderRight = parseInt(frameStyle.borderRightWidth as string, 10);
197 | const borderBottom = parseInt(frameStyle.borderBottomWidth as string, 10);
198 |
199 | let blockScroll: number = 0;
200 | let inlineScroll: number = 0;
201 |
202 | // The property existance checks for offset[Width|Height] is because only HTMLElement objects have them, but any Element might pass by here
203 | // @TODO find out if the "as HTMLElement" overrides can be dropped
204 | const scrollbarWidth =
205 | "offsetWidth" in scroller ? (scroller as HTMLElement).offsetWidth - (scroller as HTMLElement).clientWidth - borderLeft - borderRight : 0;
206 | const scrollbarHeight =
207 | "offsetHeight" in scroller ? (scroller as HTMLElement).offsetHeight - (scroller as HTMLElement).clientHeight - borderTop - borderBottom : 0;
208 |
209 | if (scrollingElement === scroller) {
210 | // Handle viewport logic (document.documentElement or document.body)
211 |
212 | if (block === "start") {
213 | blockScroll = targetBlock;
214 | } else if (block === "end") {
215 | blockScroll = targetBlock - viewportHeight;
216 | } else if (block === "nearest") {
217 | blockScroll = alignNearest(
218 | viewportY,
219 | viewportY + viewportHeight,
220 | viewportHeight,
221 | borderTop,
222 | borderBottom,
223 | viewportY + targetBlock,
224 | viewportY + targetBlock + targetHeight,
225 | targetHeight
226 | );
227 | } else {
228 | // block === 'center' is the default
229 | blockScroll = targetBlock - viewportHeight / 2;
230 | }
231 |
232 | if (inline === "start") {
233 | inlineScroll = targetInline;
234 | } else if (inline === "center") {
235 | inlineScroll = targetInline - viewportWidth / 2;
236 | } else if (inline === "end") {
237 | inlineScroll = targetInline - viewportWidth;
238 | } else {
239 | // inline === 'nearest' is the default
240 | inlineScroll = alignNearest(
241 | viewportX,
242 | viewportX + viewportWidth,
243 | viewportWidth,
244 | borderLeft,
245 | borderRight,
246 | viewportX + targetInline,
247 | viewportX + targetInline + targetWidth,
248 | targetWidth
249 | );
250 | }
251 |
252 | // Apply scroll position offsets and ensure they are within bounds
253 | // @TODO add more test cases to cover this 100%
254 | blockScroll = Math.max(0, blockScroll + viewportY);
255 | inlineScroll = Math.max(0, inlineScroll + viewportX);
256 | } else {
257 | // Handle each scrolling frame that might exist between the target and the viewport
258 |
259 | if (block === "start") {
260 | blockScroll = targetBlock - top - borderTop;
261 | } else if (block === "end") {
262 | blockScroll = targetBlock - bottom + borderBottom + scrollbarHeight;
263 | } else if (block === "nearest") {
264 | blockScroll = alignNearest(
265 | top,
266 | bottom,
267 | height,
268 | borderTop,
269 | borderBottom + scrollbarHeight,
270 | targetBlock,
271 | targetBlock + targetHeight,
272 | targetHeight
273 | );
274 | } else {
275 | // block === 'center' is the default
276 | blockScroll = targetBlock - (top + height / 2) + scrollbarHeight / 2;
277 | }
278 |
279 | if (inline === "start") {
280 | inlineScroll = targetInline - left - borderLeft;
281 | } else if (inline === "center") {
282 | inlineScroll = targetInline - (left + width / 2) + scrollbarWidth / 2;
283 | } else if (inline === "end") {
284 | inlineScroll = targetInline - right + borderRight + scrollbarWidth;
285 | } else {
286 | // inline === 'nearest' is the default
287 | inlineScroll = alignNearest(
288 | left,
289 | right,
290 | width,
291 | borderLeft,
292 | borderRight + scrollbarWidth,
293 | targetInline,
294 | targetInline + targetWidth,
295 | targetWidth
296 | );
297 | }
298 |
299 | const {scrollLeft, scrollTop} = scroller;
300 | // Ensure scroll coordinates are not out of bounds while applying scroll offsets
301 | blockScroll = Math.max(0, Math.min(scrollTop + blockScroll, scroller.scrollHeight - height + scrollbarHeight));
302 | inlineScroll = Math.max(0, Math.min(scrollLeft + inlineScroll, scroller.scrollWidth - width + scrollbarWidth));
303 | }
304 |
305 | return {
306 | top: blockScroll,
307 | left: inlineScroll
308 | };
309 | }
310 |
--------------------------------------------------------------------------------
/src/patch/element/scroll-by.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 |
3 | /**
4 | * Patches the 'scrollBy' method on the Element prototype
5 | */
6 | export function patchElementScrollBy(): void {
7 | Element.prototype.scrollBy = function(this: Element, optionsOrX?: number | ScrollToOptions, y?: number): void {
8 | handleScrollMethod(this, "scrollBy", optionsOrX, y);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/patch/element/scroll-into-view.ts:
--------------------------------------------------------------------------------
1 | import {findNearestAncestorsWithScrollBehavior} from "../../util/find-nearest-ancestor-with-scroll-behavior";
2 | import {ELEMENT_ORIGINAL_SCROLL_INTO_VIEW} from "../../original/element/scroll-into-view";
3 | import {computeScrollIntoView} from "./compute-scroll-into-view";
4 | import {getOriginalScrollMethodForKind} from "../../scroll-method/get-original-scroll-method-for-kind";
5 |
6 | /**
7 | * Patches the 'scrollIntoView' method on the Element prototype
8 | */
9 | export function patchElementScrollIntoView(): void {
10 | Element.prototype.scrollIntoView = function(this: Element, arg?: boolean | ScrollIntoViewOptions): void {
11 | const normalizedOptions: ScrollIntoViewOptions =
12 | arg == null || arg === true
13 | ? {
14 | block: "start",
15 | inline: "nearest"
16 | }
17 | : arg === false
18 | ? {
19 | block: "end",
20 | inline: "nearest"
21 | }
22 | : arg;
23 |
24 | // Find the nearest ancestor that can be scrolled
25 | const [ancestorWithScroll, ancestorWithScrollBehavior] = findNearestAncestorsWithScrollBehavior(this);
26 |
27 | const behavior = normalizedOptions.behavior != null ? normalizedOptions.behavior : ancestorWithScrollBehavior;
28 |
29 | // If the behavior isn't smooth, simply invoke the original implementation and do no more
30 | if (behavior !== "smooth") {
31 | // Assert that 'scrollIntoView' is actually defined
32 | if (ELEMENT_ORIGINAL_SCROLL_INTO_VIEW != null) {
33 | ELEMENT_ORIGINAL_SCROLL_INTO_VIEW.call(this, normalizedOptions);
34 | }
35 |
36 | // Otherwise, invoke 'scrollTo' instead and provide the scroll coordinates
37 | else {
38 | const {top, left} = computeScrollIntoView(this, ancestorWithScroll, normalizedOptions);
39 | getOriginalScrollMethodForKind("scrollTo", this).call(this, left, top);
40 | }
41 | return;
42 | }
43 |
44 | ancestorWithScroll.scrollTo({
45 | behavior,
46 | ...computeScrollIntoView(this, ancestorWithScroll, normalizedOptions)
47 | });
48 | };
49 |
50 | // On IE11, HTMLElement has its own declaration of scrollIntoView and does not inherit this from the prototype chain, so we'll need to patch that one too.
51 | if (HTMLElement.prototype.scrollIntoView != null && HTMLElement.prototype.scrollIntoView !== Element.prototype.scrollIntoView) {
52 | HTMLElement.prototype.scrollIntoView = Element.prototype.scrollIntoView;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/patch/element/scroll-left.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 | import {ELEMENT_ORIGINAL_SCROLL_LEFT_SET_DESCRIPTOR} from "../../original/element/scroll-left";
3 |
4 | /**
5 | * Patches the 'scrollLeft' property descriptor on the Element prototype
6 | */
7 | export function patchElementScrollLeft(): void {
8 | Object.defineProperty(Element.prototype, "scrollLeft", {
9 | set(scrollLeft: number) {
10 | if (this.__adjustingScrollPosition) {
11 | return ELEMENT_ORIGINAL_SCROLL_LEFT_SET_DESCRIPTOR!.call(this, scrollLeft);
12 | }
13 |
14 | handleScrollMethod(this, "scrollTo", scrollLeft, this.scrollTop);
15 | return scrollLeft;
16 | }
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/patch/element/scroll-to.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 |
3 | /**
4 | * Patches the 'scrollTo' method on the Element prototype
5 | */
6 | export function patchElementScrollTo(): void {
7 | Element.prototype.scrollTo = function(this: Element, optionsOrX?: number | ScrollToOptions, y?: number): void {
8 | handleScrollMethod(this, "scrollTo", optionsOrX, y);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/patch/element/scroll-top.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 | import {ELEMENT_ORIGINAL_SCROLL_TOP_SET_DESCRIPTOR} from "../../original/element/scroll-top";
3 |
4 | /**
5 | * Patches the 'scrollTop' property descriptor on the Element prototype
6 | */
7 | export function patchElementScrollTop(): void {
8 | Object.defineProperty(Element.prototype, "scrollTop", {
9 | set(scrollTop: number) {
10 | if (this.__adjustingScrollPosition) {
11 | return ELEMENT_ORIGINAL_SCROLL_TOP_SET_DESCRIPTOR!.call(this, scrollTop);
12 | }
13 |
14 | handleScrollMethod(this, "scrollTo", this.scrollLeft, scrollTop);
15 | return scrollTop;
16 | }
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/patch/element/scroll.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 |
3 | /**
4 | * Patches the 'scroll' method on the Element prototype
5 | */
6 | export function patchElementScroll(): void {
7 | Element.prototype.scroll = function(this: Element, optionsOrX?: number | ScrollToOptions, y?: number): void {
8 | handleScrollMethod(this, "scroll", optionsOrX, y);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/patch/patch.ts:
--------------------------------------------------------------------------------
1 | import {patchElementScroll} from "./element/scroll";
2 | import {patchElementScrollBy} from "./element/scroll-by";
3 | import {patchElementScrollTo} from "./element/scroll-to";
4 | import {patchWindowScroll} from "./window/scroll";
5 | import {patchWindowScrollBy} from "./window/scroll-by";
6 | import {patchWindowScrollTo} from "./window/scroll-to";
7 | import {catchNavigation} from "./anchor/catch-navigation";
8 | import {patchElementScrollIntoView} from "./element/scroll-into-view";
9 | import {patchElementScrollTop} from "./element/scroll-top";
10 | import {patchElementScrollLeft} from "./element/scroll-left";
11 |
12 | /**
13 | * Applies the polyfill
14 | */
15 | export function patch(): void {
16 | // Element.prototype methods
17 | patchElementScroll();
18 | patchElementScrollBy();
19 | patchElementScrollTo();
20 | patchElementScrollIntoView();
21 |
22 | // Element.prototype descriptors
23 | patchElementScrollLeft();
24 | patchElementScrollTop();
25 |
26 | // window methods
27 | patchWindowScroll();
28 | patchWindowScrollBy();
29 | patchWindowScrollTo();
30 |
31 | // Navigation
32 | catchNavigation();
33 | }
34 |
--------------------------------------------------------------------------------
/src/patch/shared.ts:
--------------------------------------------------------------------------------
1 | import {getScrollBehavior} from "../util/get-scroll-behavior";
2 | import {smoothScroll} from "../smooth-scroll/smooth-scroll/smooth-scroll";
3 | import {getSmoothScrollOptions} from "../smooth-scroll/get-smooth-scroll-options/get-smooth-scroll-options";
4 | import {ensureNumeric} from "../util/ensure-numeric";
5 | import {isScrollToOptions} from "../util/is-scroll-to-options";
6 | import {ScrollMethodName} from "../scroll-method/scroll-method-name";
7 | import {getOriginalScrollMethodForKind} from "../scroll-method/get-original-scroll-method-for-kind";
8 |
9 | /**
10 | * Handles a scroll method
11 | * @param {Element|Window} element
12 | * @param {ScrollMethodName} kind
13 | * @param {number | ScrollToOptions} optionsOrX
14 | * @param {number} y
15 | */
16 | export function handleScrollMethod(element: Element | Window, kind: ScrollMethodName, optionsOrX?: number | ScrollToOptions, y?: number): void {
17 | onScrollWithOptions(getScrollToOptionsWithValidation(optionsOrX, y), element, kind);
18 | }
19 |
20 | /**
21 | * Invoked when a 'ScrollToOptions' dict is provided to 'scroll()' as the first argument
22 | * @param {ScrollToOptions} options
23 | * @param {Element|Window} element
24 | * @param {ScrollMethodName} kind
25 | */
26 | function onScrollWithOptions(options: Required, element: Element | Window, kind: ScrollMethodName): void {
27 | const behavior = getScrollBehavior(element, options);
28 |
29 | // If the behavior is 'auto' apply instantaneous scrolling
30 | if (behavior == null || behavior === "auto") {
31 | getOriginalScrollMethodForKind(kind, element).call(element, options.left, options.top);
32 | } else {
33 | smoothScroll(getSmoothScrollOptions(element, options.left, options.top, kind));
34 | }
35 | }
36 |
37 | /**
38 | * Normalizes the given scroll coordinates
39 | * @param {number?} x
40 | * @param {number?} y
41 | * @return {Required>}
42 | */
43 | function normalizeScrollCoordinates(x: number | undefined, y: number | undefined): Required> {
44 | return {
45 | left: ensureNumeric(x),
46 | top: ensureNumeric(y)
47 | };
48 | }
49 |
50 | /**
51 | * Gets ScrollToOptions based on the given arguments. Will throw if validation fails
52 | * @param {number | ScrollToOptions} optionsOrX
53 | * @param {number} y
54 | * @return {Required}
55 | */
56 | function getScrollToOptionsWithValidation(optionsOrX?: number | ScrollToOptions, y?: number): Required {
57 | // If only one argument is given, and it isn't an options object, throw a TypeError
58 | if (y === undefined && !isScrollToOptions(optionsOrX)) {
59 | throw new TypeError("Failed to execute 'scroll' on 'Element': parameter 1 ('options') is not an object.");
60 | }
61 |
62 | // Scroll based on the primitive values given as arguments
63 | if (!isScrollToOptions(optionsOrX)) {
64 | return {
65 | ...normalizeScrollCoordinates(optionsOrX, y),
66 | behavior: "auto"
67 | };
68 | }
69 |
70 | // Scroll based on the received options object
71 | else {
72 | return {
73 | ...normalizeScrollCoordinates(optionsOrX.left, optionsOrX.top),
74 | behavior: optionsOrX.behavior == null ? "auto" : optionsOrX.behavior
75 | };
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/patch/window/scroll-by.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 |
3 | /**
4 | * Patches the 'scrollBy' method on the Window prototype
5 | */
6 | export function patchWindowScrollBy(): void {
7 | window.scrollBy = function(this: Window, optionsOrX?: number | ScrollToOptions, y?: number): void {
8 | handleScrollMethod(this, "scrollBy", optionsOrX, y);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/patch/window/scroll-to.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 |
3 | /**
4 | * Patches the 'scrollTo' method on the Window prototype
5 | */
6 | export function patchWindowScrollTo(): void {
7 | window.scrollTo = function(this: Window, optionsOrX?: number | ScrollToOptions, y?: number): void {
8 | handleScrollMethod(this, "scrollTo", optionsOrX, y);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/patch/window/scroll.ts:
--------------------------------------------------------------------------------
1 | import {handleScrollMethod} from "../shared";
2 |
3 | /**
4 | * Patches the 'scroll' method on the Window prototype
5 | */
6 | export function patchWindowScroll(): void {
7 | window.scroll = function(this: Window, optionsOrX?: number | ScrollToOptions, y?: number): void {
8 | handleScrollMethod(this, "scroll", optionsOrX, y);
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/scroll-method/get-original-scroll-method-for-kind.ts:
--------------------------------------------------------------------------------
1 | import {ScrollMethodName} from "./scroll-method-name";
2 | import {ELEMENT_ORIGINAL_SCROLL} from "../original/element/scroll";
3 | import {WINDOW_ORIGINAL_SCROLL} from "../original/window/scroll";
4 | import {ELEMENT_ORIGINAL_SCROLL_BY} from "../original/element/scroll-by";
5 | import {WINDOW_ORIGINAL_SCROLL_BY} from "../original/window/scroll-by";
6 | import {ELEMENT_ORIGINAL_SCROLL_TO} from "../original/element/scroll-to";
7 | import {WINDOW_ORIGINAL_SCROLL_TO} from "../original/window/scroll-to";
8 | import {IAdjustableElement} from "../adjustable-element/i-adjustable-element";
9 |
10 | /**
11 | * A fallback if Element.prototype.scroll is not defined
12 | * @param {number} x
13 | * @param {number} y
14 | */
15 | function elementPrototypeScrollFallback(this: IAdjustableElement, x: number, y: number): void {
16 | this.__adjustingScrollPosition = true;
17 | this.scrollLeft = x;
18 | this.scrollTop = y;
19 | delete this.__adjustingScrollPosition;
20 | }
21 |
22 | /**
23 | * A fallback if Element.prototype.scrollTo is not defined
24 | * @param {number} x
25 | * @param {number} y
26 | */
27 | function elementPrototypeScrollToFallback(this: IAdjustableElement, x: number, y: number): void {
28 | return elementPrototypeScrollFallback.call(this, x, y);
29 | }
30 |
31 | /**
32 | * A fallback if Element.prototype.scrollBy is not defined
33 | * @param {number} x
34 | * @param {number} y
35 | */
36 | function elementPrototypeScrollByFallback(this: IAdjustableElement, x: number, y: number): void {
37 | this.__adjustingScrollPosition = true;
38 | this.scrollLeft += x;
39 | this.scrollTop += y;
40 | delete this.__adjustingScrollPosition;
41 | }
42 |
43 | /**
44 | * Gets the original non-patched prototype method for the given kind
45 | * @param {ScrollMethodName} kind
46 | * @param {Element|Window} element
47 | * @return {Function}
48 | */
49 | export function getOriginalScrollMethodForKind(kind: ScrollMethodName, element: Element | Window): Function {
50 | switch (kind) {
51 | case "scroll":
52 | if (element instanceof Element) {
53 | if (ELEMENT_ORIGINAL_SCROLL != null) {
54 | return ELEMENT_ORIGINAL_SCROLL;
55 | } else {
56 | return elementPrototypeScrollFallback;
57 | }
58 | } else {
59 | return WINDOW_ORIGINAL_SCROLL!;
60 | }
61 |
62 | case "scrollBy":
63 | if (element instanceof Element) {
64 | if (ELEMENT_ORIGINAL_SCROLL_BY != null) {
65 | return ELEMENT_ORIGINAL_SCROLL_BY;
66 | } else {
67 | return elementPrototypeScrollByFallback;
68 | }
69 | } else {
70 | return WINDOW_ORIGINAL_SCROLL_BY!;
71 | }
72 |
73 | case "scrollTo":
74 | if (element instanceof Element) {
75 | if (ELEMENT_ORIGINAL_SCROLL_TO != null) {
76 | return ELEMENT_ORIGINAL_SCROLL_TO;
77 | } else {
78 | return elementPrototypeScrollToFallback;
79 | }
80 | } else {
81 | return WINDOW_ORIGINAL_SCROLL_TO!;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/scroll-method/scroll-method-name.ts:
--------------------------------------------------------------------------------
1 | export type ScrollMethodName = "scroll" | "scrollBy" | "scrollTo";
2 |
--------------------------------------------------------------------------------
/src/smooth-scroll/get-smooth-scroll-options/get-smooth-scroll-options.ts:
--------------------------------------------------------------------------------
1 | import {ISmoothScrollOptions} from "../smooth-scroll-options/i-smooth-scroll-options";
2 | import {now} from "../../util/now";
3 | import {ScrollMethodName} from "../../scroll-method/scroll-method-name";
4 | import {getOriginalScrollMethodForKind} from "../../scroll-method/get-original-scroll-method-for-kind";
5 | import {getScrollingElement} from "../../util/scrolling-element";
6 | import {ScrollSnappable} from "../../util/scroll-snappable";
7 |
8 | /**
9 | * Gets the Smooth Scroll Options to use for the step function
10 | * @param {Element|Window} element
11 | * @param {number} x
12 | * @param {number} y
13 | * @param {ScrollMethodName} kind
14 | * @returns {ISmoothScrollOptions}
15 | */
16 | export function getSmoothScrollOptions(element: Element | Window, x: number, y: number, kind: ScrollMethodName): ISmoothScrollOptions {
17 | const startTime = now();
18 |
19 | if (!(element instanceof Element)) {
20 | // Use window as the scroll container
21 | const {scrollX, pageXOffset, scrollY, pageYOffset} = window;
22 | const startX = scrollX == null || scrollX === 0 ? pageXOffset : scrollX;
23 | const startY = scrollY == null || scrollY === 0 ? pageYOffset : scrollY;
24 | return {
25 | startTime,
26 | startX,
27 | startY,
28 | endX: Math.floor(kind === "scrollBy" ? startX + x : x),
29 | endY: Math.floor(kind === "scrollBy" ? startY + y : y),
30 | method: getOriginalScrollMethodForKind("scrollTo", window).bind(window),
31 | scroller: getScrollingElement() as ScrollSnappable
32 | };
33 | } else {
34 | const {scrollLeft, scrollTop} = element;
35 | const startX = scrollLeft;
36 | const startY = scrollTop;
37 | return {
38 | startTime,
39 | startX,
40 | startY,
41 | endX: Math.floor(kind === "scrollBy" ? startX + x : x),
42 | endY: Math.floor(kind === "scrollBy" ? startY + y : y),
43 | method: getOriginalScrollMethodForKind("scrollTo", element).bind(element),
44 | scroller: element as ScrollSnappable
45 | };
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/smooth-scroll/smooth-scroll-options/i-smooth-scroll-options.ts:
--------------------------------------------------------------------------------
1 | import {ScrollSnappable} from "../../util/scroll-snappable";
2 |
3 | export interface ISmoothScrollOptions {
4 | startX: number;
5 | startY: number;
6 | endX: number;
7 | endY: number;
8 | startTime: number;
9 | scroller: ScrollSnappable;
10 | method(x: number, y: number): void;
11 | }
12 |
--------------------------------------------------------------------------------
/src/smooth-scroll/smooth-scroll/smooth-scroll.ts:
--------------------------------------------------------------------------------
1 | import {ISmoothScrollOptions} from "../smooth-scroll-options/i-smooth-scroll-options";
2 | import {ease} from "../../util/easing";
3 | import {disableScrollSnap, DisableScrollSnapResult} from "../../util/disable-scroll-snap";
4 |
5 | /**
6 | * The duration of a smooth scroll
7 | * @type {number}
8 | */
9 | const SCROLL_TIME = 15000;
10 |
11 | /**
12 | * Performs a smooth repositioning of the scroll
13 | * @param {ISmoothScrollOptions} options
14 | */
15 | export function smoothScroll(options: ISmoothScrollOptions): void {
16 | const {startTime, startX, startY, endX, endY, method, scroller} = options;
17 |
18 | let timeLapsed = 0;
19 | let start: number | undefined;
20 |
21 | const distanceX = endX - startX;
22 | const distanceY = endY - startY;
23 | const speed = Math.max(Math.abs((distanceX / 1000) * SCROLL_TIME), Math.abs((distanceY / 1000) * SCROLL_TIME));
24 |
25 | // Temporarily disables any scroll snapping that may be active since it fights for control over the scroller with this polyfill
26 | let scrollSnapFix: DisableScrollSnapResult | undefined = disableScrollSnap(scroller);
27 |
28 | requestAnimationFrame(function animate(timestamp: number) {
29 | if (start == null) {
30 | start = timestamp;
31 | }
32 | timeLapsed += timestamp - startTime;
33 | const percentage = Math.max(0, Math.min(1, speed === 0 ? 0 : timeLapsed / speed));
34 | const positionX = Math.floor(startX + distanceX * ease(percentage));
35 | const positionY = Math.floor(startY + distanceY * ease(percentage));
36 |
37 | method(positionX, positionY);
38 |
39 | if (positionX !== endX || positionY !== endY) {
40 | requestAnimationFrame(animate);
41 | start = timestamp;
42 | } else {
43 | if (scrollSnapFix != null) {
44 | scrollSnapFix.reset();
45 | scrollSnapFix = undefined;
46 | }
47 | }
48 | });
49 | }
50 |
--------------------------------------------------------------------------------
/src/support/supports-element-prototype-scroll-methods.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "./unsupported-environment";
2 |
3 | /**
4 | * Is true if the browser natively supports the Element.prototype.[scroll|scrollTo|scrollBy|scrollIntoView] methods
5 | * @type {boolean}
6 | */
7 | export const SUPPORTS_ELEMENT_PROTOTYPE_SCROLL_METHODS = UNSUPPORTED_ENVIRONMENT
8 | ? false
9 | : "scroll" in Element.prototype && "scrollTo" in Element.prototype && "scrollBy" in Element.prototype && "scrollIntoView" in Element.prototype;
10 |
--------------------------------------------------------------------------------
/src/support/supports-scroll-behavior.ts:
--------------------------------------------------------------------------------
1 | import {UNSUPPORTED_ENVIRONMENT} from "./unsupported-environment";
2 |
3 | /**
4 | * Is true if the browser natively supports the 'scroll-behavior' CSS-property.
5 | * @type {boolean}
6 | */
7 | export const SUPPORTS_SCROLL_BEHAVIOR = UNSUPPORTED_ENVIRONMENT ? false : "scrollBehavior" in document.documentElement.style;
8 |
--------------------------------------------------------------------------------
/src/support/unsupported-environment.ts:
--------------------------------------------------------------------------------
1 | export const UNSUPPORTED_ENVIRONMENT = typeof window === "undefined";
2 |
--------------------------------------------------------------------------------
/src/util/attribute.ts:
--------------------------------------------------------------------------------
1 | const STYLE_ATTRIBUTE_PROPERTY_NAME = "scroll-behavior";
2 | const STYLE_ATTRIBUTE_PROPERTY_REGEXP = new RegExp(`${STYLE_ATTRIBUTE_PROPERTY_NAME}:\\s*([^;]*)`);
3 |
4 | /**
5 | * Given an Element, this function appends the given ScrollBehavior CSS property value to the elements' 'style' attribute.
6 | * If it doesnt already have one, it will add it.
7 | * @param {Element} element
8 | * @param {ScrollBehavior} behavior
9 | */
10 | export function appendScrollBehaviorToStyleAttribute(element: Element, behavior: ScrollBehavior): void {
11 | const addition = `${STYLE_ATTRIBUTE_PROPERTY_NAME}:${behavior}`;
12 | let attributeValue = element.getAttribute("style");
13 | if (attributeValue == null || attributeValue === "") {
14 | element.setAttribute("style", addition);
15 | return;
16 | }
17 |
18 | // The style attribute may already include a 'scroll-behavior:' in which case that should be replaced
19 | const existingValueForProperty = parseScrollBehaviorFromStyleAttribute(element);
20 | if (existingValueForProperty != null) {
21 | const replacementProperty = `${STYLE_ATTRIBUTE_PROPERTY_NAME}:${existingValueForProperty}`;
22 | // Replace the variant that ends with a semi-colon which it may
23 | attributeValue = attributeValue.replace(`${replacementProperty};`, "");
24 | // Replace the variant that *doesn't* end with a semi-colon
25 | attributeValue = attributeValue.replace(replacementProperty, "");
26 | }
27 |
28 | // Now, append the behavior to the string.
29 | element.setAttribute("style", attributeValue.endsWith(";") ? `${attributeValue}${addition}` : `;${attributeValue}${addition}`);
30 | }
31 |
32 | /**
33 | * Given an Element, this function attempts to parse its 'style' attribute (if it has one)' to extract
34 | * a value for the 'scroll-behavior' CSS property (if it is given within that style attribute)
35 | * @param {Element} element
36 | * @returns {ScrollBehavior?}
37 | */
38 | export function parseScrollBehaviorFromStyleAttribute(element: Element): ScrollBehavior | undefined {
39 | const styleAttributeValue = element.getAttribute("style");
40 | if (styleAttributeValue != null && styleAttributeValue.includes(STYLE_ATTRIBUTE_PROPERTY_NAME)) {
41 | const match = styleAttributeValue.match(STYLE_ATTRIBUTE_PROPERTY_REGEXP);
42 | if (match != null) {
43 | const [, behavior] = match;
44 | if (behavior != null && behavior !== "") {
45 | return behavior as ScrollBehavior;
46 | }
47 | }
48 | }
49 | return undefined;
50 | }
51 |
--------------------------------------------------------------------------------
/src/util/disable-scroll-snap.ts:
--------------------------------------------------------------------------------
1 | import {ScrollSnappable} from "./scroll-snappable";
2 | import {SUPPORTS_SCROLL_BEHAVIOR} from "../support/supports-scroll-behavior";
3 | import {appendScrollBehaviorToStyleAttribute, parseScrollBehaviorFromStyleAttribute} from "./attribute";
4 | import {getScrollingElement} from "./scrolling-element";
5 |
6 | export interface DisableScrollSnapResult {
7 | reset(): void;
8 | }
9 |
10 | export interface DisableScrollSnapReleaser {
11 | cachedScrollSnapValue: string | null;
12 | cachedScrollBehaviorStyleAttributeValue: ScrollBehavior | undefined;
13 | secondaryScroller: ScrollSnappable | undefined;
14 | secondaryScrollerCachedScrollSnapValue: string | null | undefined;
15 | secondaryScrollerCachedScrollBehaviorStyleAttributeValue: ScrollBehavior | undefined;
16 | release(): void;
17 | }
18 |
19 | const NOOP: DisableScrollSnapResult = {
20 | reset: () => {}
21 | };
22 |
23 | const map = typeof WeakMap === "undefined" ? undefined : new WeakMap();
24 |
25 | export function disableScrollSnap(scroller: ScrollSnappable): DisableScrollSnapResult {
26 | // If scroll-behavior is natively supported, or if there is no native WeakMap support, there's no need for this fix
27 | if (SUPPORTS_SCROLL_BEHAVIOR || map == null) {
28 | return NOOP;
29 | }
30 |
31 | const scrollingElement = getScrollingElement();
32 |
33 | let cachedScrollSnapValue: string | null;
34 | let cachedScrollBehaviorStyleAttributeValue: ScrollBehavior | undefined;
35 | let secondaryScroller: ScrollSnappable | undefined;
36 | let secondaryScrollerCachedScrollSnapValue: string | null | undefined;
37 | let secondaryScrollerCachedScrollBehaviorStyleAttributeValue: ScrollBehavior | undefined;
38 | const existingResult = map.get(scroller);
39 | if (existingResult != null) {
40 | cachedScrollSnapValue = existingResult.cachedScrollSnapValue;
41 | cachedScrollBehaviorStyleAttributeValue = existingResult.cachedScrollBehaviorStyleAttributeValue;
42 | secondaryScroller = existingResult.secondaryScroller;
43 | secondaryScrollerCachedScrollSnapValue = existingResult.secondaryScrollerCachedScrollSnapValue;
44 | secondaryScrollerCachedScrollBehaviorStyleAttributeValue = existingResult.secondaryScrollerCachedScrollBehaviorStyleAttributeValue;
45 | existingResult.release();
46 | } else {
47 | cachedScrollSnapValue = scroller.style.scrollSnapType === "" ? null : scroller.style.scrollSnapType;
48 | cachedScrollBehaviorStyleAttributeValue = parseScrollBehaviorFromStyleAttribute(scroller);
49 | secondaryScroller = scroller === scrollingElement && scrollingElement !== document.body ? (document.body as ScrollSnappable) : undefined;
50 | secondaryScrollerCachedScrollSnapValue =
51 | secondaryScroller == null ? undefined : secondaryScroller.style.scrollSnapType === "" ? null : secondaryScroller.style.scrollSnapType;
52 | secondaryScrollerCachedScrollBehaviorStyleAttributeValue =
53 | secondaryScroller == null ? undefined : parseScrollBehaviorFromStyleAttribute(secondaryScroller);
54 |
55 | const cachedComputedScrollSnapValue = getComputedStyle(scroller).getPropertyValue("scroll-snap-type");
56 | const secondaryScrollerCachedComputedScrollSnapValue =
57 | secondaryScroller == null ? undefined : getComputedStyle(secondaryScroller).getPropertyValue("scroll-snap-type");
58 |
59 | // If it just so happens that there actually isn't any scroll snapping going on, there's no point in performing any additional work here.
60 | if (cachedComputedScrollSnapValue === "none" && secondaryScrollerCachedComputedScrollSnapValue === "none") {
61 | return NOOP;
62 | }
63 | }
64 |
65 | scroller.style.scrollSnapType = "none";
66 | if (secondaryScroller !== undefined) {
67 | secondaryScroller.style.scrollSnapType = "none";
68 | }
69 | if (cachedScrollBehaviorStyleAttributeValue !== undefined) {
70 | appendScrollBehaviorToStyleAttribute(scroller, cachedScrollBehaviorStyleAttributeValue);
71 | }
72 |
73 | if (secondaryScroller !== undefined && secondaryScrollerCachedScrollBehaviorStyleAttributeValue !== undefined) {
74 | appendScrollBehaviorToStyleAttribute(secondaryScroller, secondaryScrollerCachedScrollBehaviorStyleAttributeValue);
75 | }
76 |
77 | let hasReleased = false;
78 |
79 | const eventTarget = scroller === scrollingElement ? window : scroller;
80 |
81 | function release() {
82 | eventTarget.removeEventListener("scroll", resetHandler);
83 | if (map != null) {
84 | map.delete(scroller);
85 | }
86 | hasReleased = true;
87 | }
88 |
89 | function resetHandler() {
90 | scroller.style.scrollSnapType = cachedScrollSnapValue;
91 |
92 | if (secondaryScroller != null && secondaryScrollerCachedScrollSnapValue !== undefined) {
93 | secondaryScroller.style.scrollSnapType = secondaryScrollerCachedScrollSnapValue;
94 | }
95 |
96 | if (cachedScrollBehaviorStyleAttributeValue !== undefined) {
97 | appendScrollBehaviorToStyleAttribute(scroller, cachedScrollBehaviorStyleAttributeValue);
98 | }
99 |
100 | if (secondaryScroller !== undefined && secondaryScrollerCachedScrollBehaviorStyleAttributeValue !== undefined) {
101 | appendScrollBehaviorToStyleAttribute(secondaryScroller, secondaryScrollerCachedScrollBehaviorStyleAttributeValue);
102 | }
103 |
104 | release();
105 | }
106 |
107 | function reset() {
108 | setTimeout(() => {
109 | if (hasReleased) return;
110 | eventTarget.addEventListener("scroll", resetHandler);
111 | });
112 | }
113 |
114 | map.set(scroller, {
115 | release,
116 | cachedScrollSnapValue,
117 | cachedScrollBehaviorStyleAttributeValue,
118 | secondaryScroller,
119 | secondaryScrollerCachedScrollSnapValue,
120 | secondaryScrollerCachedScrollBehaviorStyleAttributeValue
121 | });
122 |
123 | return {
124 | reset
125 | };
126 | }
127 |
--------------------------------------------------------------------------------
/src/util/easing.ts:
--------------------------------------------------------------------------------
1 | const HALF = 0.5;
2 |
3 | /**
4 | * The easing function to use when applying the smooth scrolling
5 | * @param {number} k
6 | * @returns {number}
7 | */
8 | export function ease(k: number) {
9 | return HALF * (1 - Math.cos(Math.PI * k));
10 | }
11 |
--------------------------------------------------------------------------------
/src/util/ensure-numeric.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Ensures that the given value is numeric
3 | * @param {number} value
4 | * @return {number}
5 | */
6 | export function ensureNumeric(value: unknown): number {
7 | if (value == null) return 0;
8 | else if (typeof value === "number") {
9 | return value;
10 | } else if (typeof value === "string") {
11 | return parseFloat(value);
12 | } else {
13 | return 0;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/util/find-nearest-ancestor-with-scroll-behavior.ts:
--------------------------------------------------------------------------------
1 | import {getParent} from "./get-parent";
2 | import {getScrollBehavior} from "./get-scroll-behavior";
3 | import {getScrollingElement} from "./scrolling-element";
4 |
5 | /**
6 | * Returns true if the given overflow property represents a scrollable overflow value
7 | * @param {string | null} overflow
8 | * @return {boolean}
9 | */
10 | function canOverflow(overflow: string | null): boolean {
11 | return overflow !== "visible" && overflow !== "clip";
12 | }
13 |
14 | /**
15 | * Returns true if the given element is scrollable
16 | * @param {Element} element
17 | * @return {boolean}
18 | */
19 | function isScrollable(element: Element) {
20 | if (element.clientHeight < element.scrollHeight || element.clientWidth < element.scrollWidth) {
21 | const style = getComputedStyle(element, null);
22 | return canOverflow(style.overflowY) || canOverflow(style.overflowX);
23 | }
24 |
25 | return false;
26 | }
27 |
28 | /**
29 | * Finds the nearest ancestor of an element that can scroll
30 | * @param {Element} target
31 | * @returns {Element|Window?}
32 | */
33 | export function findNearestAncestorsWithScrollBehavior(target: Element | HTMLElement): [Element | HTMLElement, ScrollBehavior] {
34 | let currentElement: Element | HTMLElement = target;
35 | const scrollingElement = getScrollingElement();
36 |
37 | while (currentElement != null) {
38 | const behavior = getScrollBehavior(currentElement);
39 | if (behavior != null && (currentElement === scrollingElement || isScrollable(currentElement))) {
40 | return [currentElement, behavior];
41 | }
42 |
43 | const parent = getParent(currentElement);
44 | currentElement = parent as Element;
45 | }
46 |
47 | // No such element could be found. Start over, but this time find the nearest ancestor that can simply scroll
48 | currentElement = target;
49 |
50 | while (currentElement != null) {
51 | if (currentElement === scrollingElement || isScrollable(currentElement)) {
52 | return [currentElement, "auto"];
53 | }
54 |
55 | const parent = getParent(currentElement);
56 | currentElement = parent as Element;
57 | }
58 |
59 | // Default to the scrolling element
60 | return [scrollingElement, "auto"];
61 | }
62 |
--------------------------------------------------------------------------------
/src/util/find-nearest-root.ts:
--------------------------------------------------------------------------------
1 | import {getParent} from "./get-parent";
2 |
3 | // tslint:disable:no-any
4 |
5 | /**
6 | * Finds the nearest root from an element
7 | * @param {Element} target
8 | * @returns {Document|ShadowRoot}
9 | */
10 | export function findNearestRoot(target: Element): Document | ShadowRoot {
11 | let currentElement: EventTarget | null = target;
12 | while (currentElement != null) {
13 | if ("ShadowRoot" in window && currentElement instanceof (window as any).ShadowRoot) {
14 | // Assume this is a ShadowRoot
15 | return currentElement as ShadowRoot;
16 | }
17 |
18 | const parent = getParent(currentElement);
19 |
20 | if (parent === currentElement) {
21 | return document;
22 | }
23 |
24 | currentElement = parent;
25 | }
26 | return document;
27 | }
28 |
--------------------------------------------------------------------------------
/src/util/get-location-origin.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the origin of the given Location or HTMLAnchorElement if available in the runtime, and otherwise shims it. (it's a one-liner)
3 | * @returns {string}
4 | */
5 | export function getLocationOrigin(locationLike: Location | HTMLAnchorElement = location): string {
6 | if ("origin" in locationLike && locationLike.origin != null) {
7 | return locationLike.origin;
8 | }
9 |
10 | let port = locationLike.port != null && locationLike.port.length > 0 ? `:${locationLike.port}` : "";
11 |
12 | if (locationLike.protocol === "http:" && port === ":80") {
13 | port = "";
14 | } else if (locationLike.protocol === "https:" && port === ":443") {
15 | port = "";
16 | }
17 |
18 | return `${locationLike.protocol}//${locationLike.hostname}${port}`;
19 | }
20 |
--------------------------------------------------------------------------------
/src/util/get-parent.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-any
2 |
3 | /**
4 | * Gets the parent of an element, taking into account DocumentFragments, ShadowRoots, as well as the root context (window)
5 | * @param {EventTarget} currentElement
6 | * @returns {EventTarget | null}
7 | */
8 | export function getParent(currentElement: EventTarget): Element | Window | Node | null {
9 | if ("nodeType" in currentElement && (currentElement).nodeType === 1) {
10 | return (currentElement).parentNode;
11 | }
12 |
13 | if ("ShadowRoot" in window && currentElement instanceof (window).ShadowRoot) {
14 | return (currentElement).host;
15 | } else if (currentElement === document) {
16 | return window;
17 | } else if (currentElement instanceof Node) return currentElement.parentNode;
18 |
19 | return null;
20 | }
21 |
--------------------------------------------------------------------------------
/src/util/get-scroll-behavior.ts:
--------------------------------------------------------------------------------
1 | import {getScrollingElement} from "./scrolling-element";
2 | import {parseScrollBehaviorFromStyleAttribute} from "./attribute";
3 |
4 | const styleDeclarationPropertyName = "scrollBehavior" as keyof CSSStyleDeclaration;
5 | export type ScrollBehaviorRawValue = ScrollBehavior | null | "";
6 |
7 | /**
8 | * Determines the scroll behavior to use, depending on the given ScrollOptions and the position of the Element
9 | * within the DOM
10 | * @param {Element|HTMLElement|Window} inputTarget
11 | * @param {ScrollOptions} [options]
12 | * @returns {ScrollBehavior}
13 | */
14 | export function getScrollBehavior(inputTarget: Element | HTMLElement | Window, options?: ScrollOptions): ScrollBehavior | undefined {
15 | // If the given 'behavior' is 'smooth', apply smooth scrolling no matter what
16 | if (options != null && options.behavior === "smooth") return "smooth";
17 |
18 | const target: HTMLElement = "style" in inputTarget ? inputTarget : getScrollingElement();
19 |
20 | let value: ScrollBehavior | undefined;
21 |
22 | if ("style" in target) {
23 | // Check if scroll-behavior is set as a property on the CSSStyleDeclaration
24 | const scrollBehaviorPropertyValue = target.style[styleDeclarationPropertyName] as ScrollBehaviorRawValue;
25 | // Return it if it is given and has a proper value
26 | if (scrollBehaviorPropertyValue != null && scrollBehaviorPropertyValue !== "") {
27 | value = scrollBehaviorPropertyValue;
28 | }
29 | }
30 |
31 | if (value == null) {
32 | const attributeValue = target.getAttribute("scroll-behavior");
33 | if (attributeValue != null && attributeValue !== "") {
34 | value = attributeValue as ScrollBehavior;
35 | }
36 | }
37 |
38 | if (value == null) {
39 | // Otherwise, check if it is set as an inline style
40 | value = parseScrollBehaviorFromStyleAttribute(target);
41 | }
42 |
43 | if (value == null) {
44 | // Take the computed style for the element and see if it contains a specific 'scroll-behavior' value
45 | const computedStyle = getComputedStyle(target);
46 | const computedStyleValue = computedStyle.getPropertyValue("scrollBehavior") as ScrollBehaviorRawValue;
47 | if (computedStyleValue != null && computedStyleValue !== "") {
48 | value = computedStyleValue;
49 | }
50 | }
51 |
52 | // In all other cases, use the value from the CSSOM
53 | return value;
54 | }
55 |
--------------------------------------------------------------------------------
/src/util/is-scroll-to-options.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns true if the given value is some ScrollToOptions
3 | * @param {number | ScrollToOptions} value
4 | * @return {value is ScrollToOptions}
5 | */
6 | export function isScrollToOptions(value: number | ScrollToOptions | undefined): value is ScrollToOptions {
7 | return value != null && typeof value === "object";
8 | }
9 |
--------------------------------------------------------------------------------
/src/util/now.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns a High Resolution timestamp if possible, otherwise fallbacks to Date.now()
3 | * @returns {number}
4 | */
5 | export function now(): number {
6 | if ("performance" in window) return performance.now();
7 | return Date.now();
8 | }
9 |
--------------------------------------------------------------------------------
/src/util/scroll-snappable.ts:
--------------------------------------------------------------------------------
1 | export interface ScrollSnappable extends HTMLElement {
2 | style: HTMLElement["style"] & {scrollSnapType: string | null};
3 | }
4 |
--------------------------------------------------------------------------------
/src/util/scrolling-element.ts:
--------------------------------------------------------------------------------
1 | export function getScrollingElement(): HTMLElement {
2 | if (document.scrollingElement != null) {
3 | return document.scrollingElement as HTMLElement;
4 | } else {
5 | return document.documentElement;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.dist.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/**/*.*"],
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@wessberg/ts-config",
3 | "compilerOptions": {
4 | "target": "es3",
5 | "downlevelIteration": true
6 | }
7 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/@wessberg/ts-config/tslint.json",
3 | "rules": {
4 | "no-gratuitous-expressions": false
5 | }
6 | }
7 |
--------------------------------------------------------------------------------