├── .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 |
Logo
4 | 5 | 6 | 7 | 8 | 9 | > A polyfill for the 'scroll-behavior' CSS-property 10 | 11 | 12 | 13 | 14 | 15 | Downloads per month 16 | NPM version 17 | Dependencies 18 | Contributors 19 | code style: prettier 20 | License: MIT 21 | Support on Patreon 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 | | Frederik Wessberg | 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 | Backers on Patreon 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 | --------------------------------------------------------------------------------