├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── DEVELOPMENT.md ├── LICENSE.txt ├── README.md ├── later-package.json ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── angular │ ├── README.md │ ├── ng-package.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── scroll.component.ts │ │ ├── scroll.html │ │ └── scroll.module.ts │ └── tsconfig.json ├── core │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── rollup.base.js │ ├── rollup.config.es.js │ ├── rollup.config.umd.js │ ├── src │ │ ├── index.ts │ │ ├── scroll.ts │ │ ├── scss │ │ │ ├── _mixins.scss │ │ │ ├── _pure.import.scss │ │ │ ├── _pure.scss │ │ │ └── scroll.scss │ │ └── support.ts │ └── tsconfig.json ├── css-theming │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── scss │ │ ├── _mixins.scss │ │ ├── css-theming.import.scss │ │ └── css-theming.scss ├── react │ ├── .eslintrc │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── vue │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── scroll.vue │ │ └── shims-vue.d.ts │ ├── tsconfig.json │ └── vue.config.js └── vue2 │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── scroll.vue │ └── shims-vue.d.ts │ ├── tsconfig.json │ └── vue.config.js └── samples ├── angular ├── .browserslistrc ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── full-page │ │ │ ├── full-page.component.ts │ │ │ ├── full-page.html │ │ │ ├── full-page.scss │ │ │ └── index.ts │ │ ├── general │ │ │ ├── general.component.ts │ │ │ ├── general.html │ │ │ ├── general.scss │ │ │ └── index.ts │ │ └── styling │ │ │ ├── index.ts │ │ │ ├── styling.component.ts │ │ │ ├── styling.html │ │ │ └── styling.scss │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ └── styles.scss ├── tsconfig.app.json └── tsconfig.json ├── react ├── .gitignore ├── README.md ├── config-overrides.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── index.css │ └── index.js ├── vue ├── .browserslistrc ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── main.ts │ └── shims-vue.d.ts ├── tsconfig.json └── vue.config.js └── vue2 ├── .browserslistrc ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── main.ts └── shims-vue.d.ts ├── tsconfig.json └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.{css,scss,json,yaml,yml,html,config,md,js,ts,tsx}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "node": true 5 | }, 6 | "plugins": [ 7 | "@typescript-eslint" 8 | ], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "@typescript-eslint/explicit-module-boundary-types": "off", 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/no-non-null-assertion": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.ai binary 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup Node.js 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: '16' 14 | - name: Build 15 | run: | 16 | npm i 17 | npx lerna bootstrap 18 | npx lerna run build 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | packages/*/docs-* 4 | **/.angular/cache/ 5 | 6 | npm-debug.log 7 | yarn-error.log 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "printWidth": 100, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Angular.ng-template", 4 | "dbaeumer.vscode-eslint", 5 | "mrahhal.vscode-ng-tooling" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/node_modules": true 4 | }, 5 | "search.exclude": { 6 | "**/dist": true 7 | }, 8 | "htmlhint.options": { 9 | "attr-lowercase": false, 10 | "attr-no-duplication": true, 11 | "attr-value-double-quotes": true, 12 | "id-unique": true, 13 | "spec-char-escape": false, 14 | "doctype-first": false, 15 | "src-not-empty": true, 16 | "tag-pair": true, 17 | "tagname-lowercase": false, 18 | "title-require": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | 5 | Initial version. 6 | -------------------------------------------------------------------------------- /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 contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at m.r992@hotmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | We use [lerna](https://github.com/lerna/lerna) to publish our packages. We use the fixed versioning mode. 4 | 5 | ## Setting up the repo 6 | 7 | ``` 8 | npm install 9 | npx lerna bootstrap 10 | ``` 11 | 12 | ## Releasing 13 | 14 | We use https://github.com/mrahhal/release-cycle as a reference when releasing. 15 | 16 | ### What changed since last release 17 | 18 | ``` 19 | npx lerna changed 20 | ``` 21 | 22 | ### Update version 23 | 24 | If we're updating to a version (usually a major version update) that requires updates to the peer dependencies, then use this `npx lerna version ...` command to allow lerna to update what it wants to update, and then go on and update peer dependencies manually (and run `npm i` so that the lock file is updated too). After that, commit the changes, and then finally run lerna publish. 25 | 26 | ``` 27 | npx lerna version [version] --no-git-tag-version --no-push --yes 28 | ``` 29 | 30 | ### Release 31 | 32 | ``` 33 | npx lerna publish [version] --no-push 34 | ``` 35 | 36 | And if all goes well, push: 37 | ``` 38 | git push --follow-tags 39 | ``` 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021, Mohammad Rahhal @mrahhal 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 | # mr-scroll 2 | 3 | [![CI](https://github.com/mrahhal/mr-scroll/actions/workflows/ci.yml/badge.svg)](https://github.com/mrahhal/mr-scroll/actions/workflows/ci.yml) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt) 5 | 6 | The best custom scroll for the web. 7 | 8 | [Live demo here.](https://mr-scroll-demo.mrahhal.net) 9 | 10 | ## Features 11 | 12 | - Custom and easy to style scrollbar 13 | - Uses the native browser scrollbar behind the scenes -> smooth scrolling, middle mouse click drag works, follows the behavior you're used to 14 | - Behaves exactly like a native scrollbar (detects content size changes, so it's always visually in sync unlike every other custom scrollbar) 15 | - Different modes: scroll, auto, overlay, hidden 16 | - Show on hover 17 | - Hidden content fade (shows a fading effect to indicate there's hidden content out of view) 18 | - Emits various useful events you can handle (scrolled, state changed, position changed, top/bottom reached with configurable thresholds, etc) 19 | - Great for implementing infinite paging 20 | - Works exactly the same across supported browsers 21 | - Supported on all modern browsers, including mobile browsers 22 | 23 | And finally, we have efficient idiomatic wrapper packages for popular frameworks (angular, react, vue2, vue3). 24 | 25 | ## Packages 26 | 27 | - [@mr-scroll/core](./packages/core): The core package. This does the heavy lifting. 28 | - [@mr-scroll/angular](./packages/angular): The wrapper package for Angular. 29 | - [@mr-scroll/react](./packages/react): The wrapper package for React. 30 | - [@mr-scroll/vue2](./packages/vue2): The wrapper package for Vue 2. 31 | - [@mr-scroll/vue](./packages/vue): The wrapper package for Vue 3. 32 | 33 | Can't find your framework? This means we don't have a wrapper for it just yet. Feel free to suggest/contribute one. 34 | 35 | ### Support packages 36 | 37 | We also have support packages: 38 | 39 | - [@mr-scroll/css-theming](./packages/css-theming): A support package for styling the scrollbar according to the active theme when using [css-theming](https://github.com/mrahhal/css-theming). 40 | 41 | All packages in the @mr-scroll org here: https://www.npmjs.com/org/mr-scroll 42 | 43 | ## Config 44 | 45 | > Check the respective wrapper package and samples for an example usage. 46 | 47 | | Name | Type | Default | Description | 48 | |------|------|---------|-------------| 49 | | mode | 'auto' \| 'overlay' \| 'hidden' | 'auto' | The mode that the scroll will adapt. | 50 | | topThreshold | number | 50 | The top threshold in px. Affects when the topReached event is raised. | 51 | | bottomThreshold | number | 50 | The bottom threshold in px. Affects when the bottomReached event is raised. | 52 | | leftThreshold | number | 50 | The left threshold in px. Affects when the leftReached event is raised. | 53 | | rightThreshold | number | 50 | The right threshold in px. Affects when the rightReached event is raised. | 54 | | showOnHover | boolean | false | Respresents whether or not to show the scrollbar only on hover. | 55 | 56 | ## Events 57 | 58 | > Check the respective wrapper package or sample for an example usage. 59 | 60 | | Name | Event data | Description | 61 | |------|------------|-------------| 62 | | scrolled | { left: number; top: number } | Raised whenever the scrollbar is scrolled. | 63 | | topReached | N/A | Raised when the top is reached, taken into account the topThreshold config. | 64 | | bottomReached | N/A | Raised when the bottom is reached, taken into account the bottomThreshold config. | 65 | | leftReached | N/A | Raised when the left is reached, taken into account the leftThreshold config. | 66 | | rightReached | N/A | Raised when the right is reached, taken into account the rightThreshold config. | 67 | | positionHChanged | 'start' \| 'middle' \| 'end' \| 'full' | Raised when the horizontal position is changed. | 68 | | positionAbsoluteHChanged | 'start' \| 'middle' \| 'end' \| 'full' | Raised when the horizontal position is changed without taking thresholds into account. | 69 | | stateHChanged | 'hidden' \| 'scrolling' | Raised when the horizontal state is changed. | 70 | | positionVChanged | 'start' \| 'middle' \| 'end' \| 'full' | Raised when the vertical position is changed. | 71 | | positionAbsoluteVChanged | 'start' \| 'middle' \| 'end' \| 'full' | Raised when the vertical position is changed without taking thresholds into account. | 72 | | stateVChanged | 'hidden' \| 'scrolling' | Raised when the vertical state is changed. | 73 | 74 | ## Mixins 75 | 76 | Defined [here](./packages/core/src/scss/_mixins.scss). 77 | 78 | The [@mr-scroll/core](./packages/core) package provides several helper mixins in SCSS. You'll always `@include` the mixins in the direct parent of an mr-scroll. 79 | 80 | To use them, you'll import the pure file and include any mixin you want: 81 | ```scss 82 | @import '@mr-scroll/core/src/scss/pure'; 83 | 84 | .foo { 85 | @include msc-[mixin name](...); 86 | } 87 | ``` 88 | 89 | If you're using the SCSS module system: 90 | ```scss 91 | @use '@mr-scroll/core/src/scss/pure' as msc; 92 | 93 | .foo { 94 | @include msc.[mixin name](...); 95 | } 96 | ``` 97 | 98 | As an example, using the height mixin: 99 | ```scss 100 | @use '@mr-scroll/core/src/scss/pure' as msc; 101 | 102 | .my-scroll-parent { 103 | @include msc.height(200px); 104 | } 105 | ``` 106 | 107 | ## Styling 108 | 109 | To style mr-scroll in our whole app we can set some global CSS variables. But keep in mind that this sets the styles for all scrolls in the whole hierarchy. 110 | 111 | Here is a list of the CSS variables you can override, and their default values: 112 | ```css 113 | --mr-scroll-bar-size-normal: 12px; 114 | --mr-scroll-bar-size-overlay: 8px; 115 | --mr-scroll-bar-margin: 3px; 116 | --mr-scroll-track-color: transparent; 117 | --mr-scroll-thumb-border-radius: 2px; 118 | --mr-scroll-thumb-color: #aaa; 119 | --mr-scroll-hidden-content-fade-size: 25px; 120 | ``` 121 | 122 | Sometimes you want to override a certain style just for one scroll without affecting the others. For that, you can use any of the `override-*` mixins in that mr-scroll's direct parent: 123 | 124 | | Name | Description | 125 | |------|-------------| 126 | | override-thumb-border-radius | Overrides the thumb border radius. | 127 | | override-hidden-content-fade-size | Overrides the hidden content fade size. | 128 | 129 | ## General usage 130 | 131 | Generally, we want to control our scroll in only 3 different ways: fixed height, max height, or fully adaptive. Here is how to do each. 132 | 133 | - Fixed height: You'll use the `height` mixin on the direct parent for mr-scroll (or simply just set a `height: ...px;` on an .mr-scroll). 134 | 135 | - Max height: You'll use the `max-height` mixin on the direct parent for mr-scroll (or simply just set a `max-height: ...px;` on an .mr-scroll). 136 | 137 | - Adaptive: This is a bit harder to implement when you have a complex layout, but it's still easy. In complex layouts, flexible design doesn't always work well with scrolls. The trick is to use the `flex-adaptive-container` mixin we provide on parents, all the way up to the root container (or a fixed/absolute container). This will lead to a fully working adaptive layout. For an example of this, check the the full-page page in the angular sample. 138 | 139 | ## Samples 140 | 141 | Samples contain working examples of how to use mr-scroll. 142 | 143 | - [Angular](./samples/angular) 144 | - [React](./samples/react) 145 | - [Vue 2](./samples/vue2) 146 | - [Vue 3](./samples/vue) 147 | 148 | --- 149 | 150 | [![CCSS](https://img.shields.io/badge/follows-CCSS-cc00ff.svg)](https://github.com/mrahhal/CCSS) 151 | 152 | This project follows [CCSS](https://github.com/mrahhal/CCSS) for CSS naming conventions. 153 | -------------------------------------------------------------------------------- /later-package.json: -------------------------------------------------------------------------------- 1 | // To be used later for workspaces. But right now, this for some reason 2 | // is causing weird problems locally with using ScrollModule in the 3 | // angular sample. 4 | // https://github.com/npm/rfcs/blob/latest/implemented/0026-workspaces.md 5 | // https://docs.npmjs.com/cli/v7/using-npm/workspaces#installing-workspaces 6 | { 7 | "private": true, 8 | "workspaces": [ 9 | "packages/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "1.2.0", 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "ignoreChanges": [ 8 | "**/*.md" 9 | ], 10 | "useNx": false, 11 | "useWorkspaces": false 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "lint": "run-s lint:eslint lint:prettier", 6 | "lint:eslint": "eslint packages --ext .js,.ts,.jsx,.tsx --fix --ignore-path .gitignore", 7 | "lint:prettier": "prettier packages --write" 8 | }, 9 | "devDependencies": { 10 | "@typescript-eslint/eslint-plugin": "^5.41.0", 11 | "@typescript-eslint/parser": "^5.41.0", 12 | "eslint": "^8.26.0", 13 | "eslint-config-prettier": "^8.5.0", 14 | "eslint-config-standard-react": "^12.0.0", 15 | "eslint-plugin-react": "^7.31.10", 16 | "eslint-plugin-vue": "^9.6.0", 17 | "lerna": "^5.5.4", 18 | "npm-run-all": "^4.1.5", 19 | "prettier": "^2.7.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/angular/README.md: -------------------------------------------------------------------------------- 1 | # @mr-scroll/angular 2 | 3 | [![npm](https://img.shields.io/npm/v/@mr-scroll/angular.svg)](https://www.npmjs.com/package/@mr-scroll/angular) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | The best custom scroll for the web. 7 | 8 | This is the angular wrapper. [Check here](../../README.md) (root of this repo) for an overview on mr-scroll. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm i @mr-scroll/core @mr-scroll/angular 14 | ``` 15 | 16 | Angular 10 and above is supported. 17 | 18 | Note: If you're using [css-theming](https://github.com/mrahhal/css-theming), check the [css-theming support package](../css-theming). 19 | 20 | ## Usage 21 | 22 | Import the global CSS styles in your `angular.json`, in projects>angular>architect>build>options>styles: 23 | 24 | ```json 25 | "styles": [ 26 | "node_modules/@mr-scroll/core/dist/styles.css", 27 | //... 28 | ] 29 | ``` 30 | 31 | [Example from sample here.](https://github.com/mrahhal/mr-scroll/blob/0780d36414c7032a5853daa53ec390cc9427537c/samples/angular/angular.json#L34) 32 | 33 | Import `ScrollModule` into your module. 34 | 35 | [Example from sample here.](https://github.com/mrahhal/mr-scroll/blob/0780d36414c7032a5853daa53ec390cc9427537c/samples/angular/src/app/app.module.ts#L19) 36 | 37 | Use `mr-scroll` component: 38 | 39 | ```html 40 | Content 41 | ``` 42 | 43 | > For more general usage info check the [README](../../README.md) in the root of this repo. 44 | 45 | **NOTE:** The `scrolled` event is the only event that won't trigger change detection. This is by design as it's fired a lot. If you need change detection when you react to it, you can do this easily by using `NgZone`: 46 | 47 | ```ts 48 | // Inject NgZone in your component. 49 | constructor(private _zone: NgZone) { } 50 | 51 | _onScrolled() { 52 | _zone.Run(() => { 53 | // Handle the event. 54 | }); 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "src/index.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/angular", 3 | "version": "1.2.0", 4 | "description": "The best custom scroll for the web. This is the angular wrapper.", 5 | "keywords": [ 6 | "custom", 7 | "scroll", 8 | "scrollbar", 9 | "angular", 10 | "component" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/mrahhal/mr-scroll/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/mrahhal/mr-scroll" 18 | }, 19 | "license": "MIT", 20 | "author": "Mohammad Rahhal", 21 | "files": [ 22 | "**" 23 | ], 24 | "scripts": { 25 | "compile": "ngc", 26 | "build": "ng-packagr -p ng-package.json", 27 | "check-peer-dependencies": "npx check-peer-dependencies", 28 | "prepublishOnly": "npm run build" 29 | }, 30 | "devDependencies": { 31 | "@angular/animations": "^13.0.0", 32 | "@angular/compiler": "^13.0.0", 33 | "@angular/compiler-cli": "^13.0.0", 34 | "@angular/platform-browser": "^13.0.0", 35 | "@angular/platform-browser-dynamic": "^13.0.0", 36 | "@mr-scroll/core": "file:../core", 37 | "ng-packagr": "^13.0.0", 38 | "ts-node": "9.1.1", 39 | "tslib": "^2.2.0", 40 | "typescript": "~4.4.4", 41 | "zone.js": "~0.11.4" 42 | }, 43 | "peerDependencies": { 44 | "@angular/common": ">=13.0.0", 45 | "@angular/core": ">=13.0.0", 46 | "@mr-scroll/core": "^1.0.0", 47 | "rxjs": ">=6.0.0" 48 | }, 49 | "engines": { 50 | "node": ">=6.0.0" 51 | }, 52 | "publishConfig": { 53 | "access": "public", 54 | "directory": "dist" 55 | }, 56 | "distDir": "dist" 57 | } 58 | -------------------------------------------------------------------------------- /packages/angular/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./scroll.component"; 2 | export * from "./scroll.module"; 3 | -------------------------------------------------------------------------------- /packages/angular/src/scroll.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | Component, 5 | ElementRef, 6 | EventEmitter, 7 | Input, 8 | NgZone, 9 | OnDestroy, 10 | OnInit, 11 | Output, 12 | ViewChild, 13 | ViewEncapsulation, 14 | } from "@angular/core"; 15 | import { Scroll, ScrollMode, ScrollPosition, ScrollState } from "@mr-scroll/core"; 16 | 17 | @Component({ 18 | selector: "mr-scroll", 19 | templateUrl: "./scroll.html", 20 | encapsulation: ViewEncapsulation.None, 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | }) 23 | export class ScrollComponent implements OnInit, AfterViewInit, OnDestroy { 24 | private _scroll: Scroll; 25 | 26 | @Input() mode?: ScrollMode; 27 | 28 | @Input() topThreshold?: number; 29 | 30 | @Input() bottomThreshold?: number; 31 | 32 | @Input() leftThreshold?: number; 33 | 34 | @Input() rightThreshold?: number; 35 | 36 | @Input() showOnHover?: boolean; 37 | 38 | @Output() scrolled = new EventEmitter<{ left: number; top: number }>(); 39 | 40 | @Output() topReached = new EventEmitter(); 41 | 42 | @Output() bottomReached = new EventEmitter(); 43 | 44 | @Output() leftReached = new EventEmitter(); 45 | 46 | @Output() rightReached = new EventEmitter(); 47 | 48 | @Output() positionHChanged = new EventEmitter(); 49 | 50 | @Output() positionAbsoluteHChanged = new EventEmitter(); 51 | 52 | @Output() stateHChanged = new EventEmitter(); 53 | 54 | @Output() positionVChanged = new EventEmitter(); 55 | 56 | @Output() positionAbsoluteVChanged = new EventEmitter(); 57 | 58 | @Output() stateVChanged = new EventEmitter(); 59 | 60 | @ViewChild("content", { static: true }) _content: ElementRef; 61 | 62 | constructor(private _el: ElementRef, private _zone: NgZone) {} 63 | 64 | get scroll() { 65 | return this._scroll; 66 | } 67 | 68 | ngOnInit() { 69 | this._scroll = new Scroll(this._el.nativeElement, this._content.nativeElement, { 70 | mode: this.mode, 71 | topThreshold: this.topThreshold, 72 | bottomThreshold: this.bottomThreshold, 73 | leftThreshold: this.leftThreshold, 74 | rightThreshold: this.rightThreshold, 75 | showOnHover: this.showOnHover, 76 | }); 77 | 78 | // Events that will normally trigger change detection. 79 | const delegatedEvents = [ 80 | "topReached", 81 | "bottomReached", 82 | "leftReached", 83 | "rightReached", 84 | "positionHChanged", 85 | "positionAbsoluteHChanged", 86 | "stateHChanged", 87 | "positionVChanged", 88 | "positionAbsoluteVChanged", 89 | "stateVChanged", 90 | ]; 91 | // Events that won't trigger change detection. Change detection should be handled by the consumer. 92 | const delegatedEventsOutsideNgZone = ["scrolled"]; 93 | 94 | for (const eventName of delegatedEvents) { 95 | this._scroll[eventName].subscribe((x: any) => { 96 | const e = this[eventName] as EventEmitter; 97 | // Avoid calling zone.run if there are no subscribers to avoid triggering change detection 98 | if (e.observers.length) { 99 | this._zone.run(() => { 100 | e.emit(x); 101 | }); 102 | } 103 | }); 104 | } 105 | 106 | for (const eventName of delegatedEventsOutsideNgZone) { 107 | this._scroll[eventName].subscribe((x: any) => { 108 | const e = this[eventName] as EventEmitter; 109 | e.emit(x); 110 | }); 111 | } 112 | } 113 | 114 | ngAfterViewInit() { 115 | this._zone.runOutsideAngular(() => { 116 | this._scroll.initialize(); 117 | }); 118 | } 119 | 120 | ngOnDestroy() { 121 | this._scroll.destroy(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/angular/src/scroll.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /packages/angular/src/scroll.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from "@angular/common"; 2 | import { NgModule } from "@angular/core"; 3 | 4 | import { ScrollComponent } from "./scroll.component"; 5 | 6 | const exportedDeclarations = [ScrollComponent]; 7 | 8 | @NgModule({ 9 | imports: [CommonModule], 10 | declarations: exportedDeclarations, 11 | exports: exportedDeclarations, 12 | }) 13 | export class ScrollModule {} 14 | -------------------------------------------------------------------------------- /packages/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "moduleResolution": "node", 7 | "module": "es2020", 8 | "target": "es2015", 9 | "lib": ["es6", "dom"], 10 | "allowSyntheticDefaultImports": true, 11 | "skipLibCheck": true, 12 | "rootDir": "src", 13 | "outDir": "dist", 14 | "declaration": true, 15 | "sourceMap": true, 16 | "inlineSources": true, 17 | "importHelpers": true 18 | }, 19 | "angularCompilerOptions": { 20 | "compilationMode": "partial", 21 | "enableI18nLegacyMessageIdFormat": false, 22 | "skipTemplateCodegen": true, 23 | "strictInjectionParameters": true, 24 | "strictInputAccessModifiers": true, 25 | "strictMetadataEmit": true, 26 | "strictTemplates": true 27 | }, 28 | "files": ["src/index.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @mr-scroll/core 2 | 3 | [![npm](https://img.shields.io/npm/v/@mr-scroll/core.svg)](https://www.npmjs.com/package/@mr-scroll/core) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | The best custom scroll for the web. 7 | 8 | This is the core package. [Check here](../../README.md) for a general usage guide. 9 | 10 | ## Implementation 11 | 12 | > NOTE: If you're using a framework we have a wrapper for then you don't need this. [Check here](../../README.md#packages) to see which ones we support. 13 | 14 | @mr-scroll/core has the core logic to make an mr-scroll custom scrollbar. 15 | 16 | Implementing @mr-scroll/core in a framework is simple. You'll wrap the `Scroll` class inside a component (or something similar). 17 | 18 | The `Scroll` constructor takes 3 arguments: 19 | 20 | - Host element: The html element that will act as the host. Required. 21 | - Content element: The html element that will contain the actual contents. Required. 22 | - A config object 23 | 24 | So this requires you to provide the host and content elements. Usually, you'll have this html template: 25 | 26 | ```html 27 |
28 | 29 |
30 | 31 | CONTENT 32 | 33 |
34 |
35 | ``` 36 | 37 | Use whatever is the idiomatic approach in your framework to do this. 38 | 39 | And then in your wrapper component: 40 | 41 | ```ts 42 | import { Scroll } from '@mr-scroll/core'; 43 | 44 | // Inside the wrapper 45 | 46 | // You'll want to store the reference to the scroll. 47 | this.scroll = new Scroll(hostElement, contentElement, /* config: fill from your inputs */ { ... }); 48 | 49 | // Delegate events in a way that makes sense in your framework. 50 | // For example, in angular, we add EventEmitters that delegate the inner events of Scroll. 51 | 52 | // Initialize whenever is the right time to do so in your framework. 53 | this.scroll.initialize(); 54 | 55 | // And don't forget to destroy it when your component is being destroyed. 56 | this.scroll.destroy(); 57 | ``` 58 | 59 | This package also provides the main CSS styles to be imported in your app. You can find the bundled styles at "@mr-scroll/core/dist/styles.css". 60 | 61 | Check our [angular wrapper](../angular) for an implementation example of all of this for Angular. 62 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/core", 3 | "description": "The best custom scroll for the web. This is the core package.", 4 | "version": "1.2.0", 5 | "main": "dist/umd/index.js", 6 | "module": "dist/es/index.js", 7 | "types": "dist/es/index.d.ts", 8 | "scripts": { 9 | "start": "npm run watch", 10 | "watch": "concurrently \"npm run watch:ts\" \"npm run watch:scss\"", 11 | "watch:ts": "rollup -c rollup.config.es.js -w", 12 | "watch:scss": "nodemon --watch ./src/scss -e scss -x \"npm run bundle:css\"", 13 | "build": "rollup -c rollup.config.es.js && rollup -c rollup.config.umd.js && npm run bundle:css", 14 | "bundle:css": "npx sass src/scss/scroll.scss dist/styles.css", 15 | "docs": "typedoc src/index.ts --out docs-ts && sassdoc src/scss -d docs-scss", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "keywords": [ 19 | "custom", 20 | "scroll", 21 | "scrollbar" 22 | ], 23 | "author": "Mohammad Rahhal", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/mrahhal/mr-scroll" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/mrahhal/mr-scroll/issues" 31 | }, 32 | "files": [ 33 | "dist", 34 | "src/scss" 35 | ], 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "peerDependencies": { 40 | "rxjs": ">=6.0.0" 41 | }, 42 | "devDependencies": { 43 | "@rollup/plugin-node-resolve": "^13.0.0", 44 | "@rollup/plugin-typescript": "^8.2.1", 45 | "concurrently": "^6.1.0", 46 | "nodemon": "^2.0.7", 47 | "rollup": "^2.47.0", 48 | "rollup-plugin-peer-deps-external": "^2.2.4", 49 | "rxjs": "^6.6.7", 50 | "sass": "^1.32.13", 51 | "sassdoc": "^2.7.3", 52 | "tslib": "^2.2.0", 53 | "typedoc": "^0.20.36", 54 | "typescript": "~4.2.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/rollup.base.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 4 | 5 | // eslint-disable-next-line no-undef 6 | const production = process.env.NODE_ENV === "production"; 7 | 8 | export function createRollupConfig(format, name = undefined) { 9 | const outputDir = `dist/${format}`; 10 | const typescriptOptions = 11 | format == "es" 12 | ? { 13 | declaration: true, 14 | declarationDir: outputDir, 15 | } 16 | : {}; 17 | 18 | return { 19 | input: "src/index.ts", 20 | output: { 21 | dir: outputDir, 22 | format, 23 | name, 24 | sourcemap: true, 25 | }, 26 | plugins: [ 27 | format == "es" ? peerDepsExternal() : null, 28 | typescript({ 29 | rootDir: "src/", 30 | sourceMap: !production, 31 | inlineSources: !production, 32 | ...typescriptOptions, 33 | }), 34 | nodeResolve(), 35 | ], 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/rollup.config.es.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from "./rollup.base"; 2 | 3 | const config = createRollupConfig("es"); 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/core/rollup.config.umd.js: -------------------------------------------------------------------------------- 1 | import { createRollupConfig } from "./rollup.base"; 2 | 3 | const config = createRollupConfig("umd", "mrScrollCore"); 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./scroll"; 2 | -------------------------------------------------------------------------------- /packages/core/src/scroll.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs"; 2 | 3 | import { getScrollbarWidth } from "./support"; 4 | 5 | export type ScrollMode = "auto" | "overlay" | "hidden"; 6 | export type ScrollPosition = "start" | "middle" | "end" | "full"; 7 | export type ScrollState = "hidden" | "scrolling"; 8 | export type ScrollExtremity = "start" | "end"; 9 | export type ScrollDirection = "h" | "v"; 10 | 11 | /** 12 | * Contains configuration options for Scroll. 13 | */ 14 | export interface ScrollConfig { 15 | /** 16 | * The mode that the scroll will adapt. 17 | */ 18 | mode: ScrollMode; 19 | 20 | /** 21 | * The top threshold in px. Affects when the topReached event is raised. 22 | */ 23 | topThreshold: number; 24 | 25 | /** 26 | * The bottom threshold in px. Affects when the bottomReached event is raised. 27 | */ 28 | bottomThreshold: number; 29 | 30 | /** 31 | * The left threshold in px. Affects when the leftReached event is raised. 32 | */ 33 | leftThreshold: number; 34 | 35 | /** 36 | * The right threshold in px. Affects when the rightReached event is raised. 37 | */ 38 | rightThreshold: number; 39 | 40 | /** 41 | * Respresents whether or not to show the scrollbar only on hover. 42 | */ 43 | showOnHover: boolean; 44 | } 45 | 46 | const DEFAULT_CONFIG: ScrollConfig = { 47 | mode: "auto", 48 | topThreshold: 50, 49 | bottomThreshold: 50, 50 | leftThreshold: 50, 51 | rightThreshold: 50, 52 | showOnHover: false, 53 | }; 54 | 55 | const HOST_CLASS = "mr-scroll"; 56 | const HOST_SIZE_0_MODIFIER = `${HOST_CLASS}--size-0`; 57 | const HOST_HIDDEN_CONTENT_MODIFIER = `${HOST_CLASS}--hidden-content`; 58 | const HOST_HIDDEN_CONTENT_LEFT_MODIFIER = `${HOST_HIDDEN_CONTENT_MODIFIER}-l`; 59 | const HOST_HIDDEN_CONTENT_RIGHT_MODIFIER = `${HOST_HIDDEN_CONTENT_MODIFIER}-r`; 60 | const HOST_HIDDEN_CONTENT_TOP_MODIFIER = `${HOST_HIDDEN_CONTENT_MODIFIER}-t`; 61 | const HOST_HIDDEN_CONTENT_BOTTOM_MODIFIER = `${HOST_HIDDEN_CONTENT_MODIFIER}-b`; 62 | const HOST_HIDDEN_CONTENT_FADE_CLASS = `${HOST_CLASS}_hidden-content-fade`; 63 | const CONTENT_CLASS = `${HOST_CLASS}_content`; 64 | const BAR_CLASS = `${HOST_CLASS}_bar`; 65 | const BAR_DRAGGING_MODIFIER = `${BAR_CLASS}--dragging`; 66 | const BAR_HIDDEN_MODIFIER = `${BAR_CLASS}--hidden`; 67 | const BAR_H_MODIFIER = `${BAR_CLASS}--h`; 68 | const BAR_V_MODIFIER = `${BAR_CLASS}--v`; 69 | 70 | interface Bar { 71 | barElement: HTMLDivElement; 72 | trackElement: HTMLDivElement; 73 | thumbElement: HTMLDivElement; 74 | } 75 | 76 | interface DirectionContext { 77 | bar: Bar; 78 | position: ScrollPosition; 79 | positionAbsolute: ScrollPosition; 80 | state: ScrollState; 81 | scrollRatio: number; 82 | scroll: number | null; 83 | size: number; 84 | sizePixels: number; 85 | translate: number; 86 | dragging: boolean; 87 | 88 | positionChanged: Subject; 89 | positionAbsoluteChanged: Subject; 90 | stateChanged: Subject; 91 | } 92 | 93 | function createDirectionContext(): DirectionContext { 94 | return { 95 | bar: null!, 96 | position: null!, 97 | positionAbsolute: null!, 98 | state: null!, 99 | scrollRatio: -1, 100 | scroll: null, 101 | size: -1, 102 | sizePixels: -1, 103 | translate: -1, 104 | dragging: false, 105 | 106 | positionChanged: new Subject(), 107 | positionAbsoluteChanged: new Subject(), 108 | stateChanged: new Subject(), 109 | }; 110 | } 111 | 112 | /** 113 | * The core class that implements the custom scroll logic. 114 | */ 115 | export class Scroll { 116 | private _hostElementAccessor: () => HTMLElement; 117 | private _contentElementAccessor: () => HTMLElement; 118 | private _config: ScrollConfig; 119 | private _mo: MutationObserver = null!; 120 | private _ro: ResizeObserver = null!; 121 | private _browserScrollbarSize: number; 122 | private _scrollbarSize: number; 123 | private _barTotalMargin: number; 124 | private _h = createDirectionContext(); 125 | private _v = createDirectionContext(); 126 | private _prevPageX = 0; 127 | private _prevPageY = 0; 128 | 129 | private _scrolled = new Subject<{ left: number; top: number }>(); 130 | private _topReached = new Subject(); 131 | private _bottomReached = new Subject(); 132 | private _leftReached = new Subject(); 133 | private _rightReached = new Subject(); 134 | 135 | /** 136 | * @param _hostElement The host element. If a function is provided, it'll be called to access the element each time it's needed. 137 | * @param _contentElement The content element. If a function is provided, it'll be called to access the element each time it's needed. 138 | * @param config The config object. 139 | */ 140 | constructor( 141 | hostElement: HTMLElement | (() => HTMLElement), 142 | contentElement: HTMLElement | (() => HTMLElement), 143 | config?: Partial, 144 | ) { 145 | if (!hostElement) { 146 | throw new Error("hostElement can't be null or undefined."); 147 | } 148 | if (!contentElement) { 149 | throw new Error("contentElement can't be null or undefined."); 150 | } 151 | 152 | this._hostElementAccessor = typeof hostElement == "function" ? hostElement : () => hostElement; 153 | this._contentElementAccessor = 154 | typeof contentElement == "function" ? contentElement : () => contentElement; 155 | 156 | const _hostElement = this._hostElementAccessor(); 157 | const _contentElement = this._contentElementAccessor(); 158 | 159 | this.update = this.update.bind(this); 160 | 161 | this._getThumbHWidth = this._getThumbHWidth.bind(this); 162 | this._getThumbVHeight = this._getThumbVHeight.bind(this); 163 | this._getScrollLeftForOffset = this._getScrollLeftForOffset.bind(this); 164 | this._getScrollTopForOffset = this._getScrollTopForOffset.bind(this); 165 | 166 | this._handleTrackHMouseDown = this._handleTrackHMouseDown.bind(this); 167 | this._handleTrackVMouseDown = this._handleTrackVMouseDown.bind(this); 168 | this._handleThumbHMouseDown = this._handleThumbHMouseDown.bind(this); 169 | this._handleThumbVMouseDown = this._handleThumbVMouseDown.bind(this); 170 | this._handleDrag = this._handleDrag.bind(this); 171 | this._handleDragEnd = this._handleDragEnd.bind(this); 172 | 173 | // Filter out undefined 174 | config = Object.entries(config || {}) 175 | .filter(([, value]) => value !== undefined) 176 | .reduce((obj, [key, value]) => ((obj[key] = value), obj), {} as any); 177 | 178 | this._config = config = { ...DEFAULT_CONFIG, ...config }; 179 | 180 | _hostElement.classList.add(HOST_CLASS); 181 | _contentElement.classList.add(CONTENT_CLASS); 182 | 183 | if (config.showOnHover) { 184 | this._hostElement.classList.add(`${HOST_CLASS}--show-on-hover`); 185 | } 186 | 187 | this._hostElement.classList.add(`${HOST_CLASS}--mode-` + config.mode); 188 | 189 | this._browserScrollbarSize = this._resolveBrowserScrollbarSize(); 190 | this._scrollbarSize = this._resolveScrollbarSize()!; 191 | this._barTotalMargin = this._resolveBarMargin() * 3; // Logically this should be "* 2", but this is working better. Something's off. 192 | 193 | // mr-scroll_hidden-content-fade 194 | const createFade = (t: string) => { 195 | const fadeElement = _hostElement.appendChild(document.createElement("div")); 196 | fadeElement.classList.add(HOST_HIDDEN_CONTENT_FADE_CLASS); 197 | fadeElement.classList.add(HOST_HIDDEN_CONTENT_FADE_CLASS + `--${t}`); 198 | }; 199 | ["t", "r", "b", "l"].forEach((t) => createFade(t)); 200 | 201 | // mr-scroll_bar 202 | const createBar = (): Bar => { 203 | const barElement = _hostElement.appendChild(document.createElement("div")); 204 | barElement.classList.add(`${BAR_CLASS}`); 205 | const trackElement = barElement.appendChild(document.createElement("div")); 206 | trackElement.classList.add(`${BAR_CLASS}-track`); 207 | const thumbElement = barElement.appendChild(document.createElement("div")); 208 | thumbElement.classList.add(`${BAR_CLASS}-thumb`); 209 | return { 210 | barElement, 211 | trackElement, 212 | thumbElement, 213 | }; 214 | }; 215 | 216 | const barH = (this._h.bar = createBar()); 217 | barH.barElement.classList.add(BAR_H_MODIFIER); 218 | const barV = (this._v.bar = createBar()); 219 | barV.barElement.classList.add(BAR_V_MODIFIER); 220 | 221 | if (this.mode != "auto") { 222 | // In all modes but auto, we always have spacing 223 | this._addSpacing(); 224 | } 225 | } 226 | 227 | get mode() { 228 | return this._config.mode; 229 | } 230 | 231 | get scrollLeft() { 232 | return this._h.scroll; 233 | } 234 | 235 | get scrollTop() { 236 | return this._v.scroll; 237 | } 238 | 239 | get positionH() { 240 | return this._h.position; 241 | } 242 | 243 | get positionV() { 244 | return this._v.position; 245 | } 246 | 247 | get positionAbsoluteH() { 248 | return this._h.positionAbsolute; 249 | } 250 | 251 | get positionAbsoluteV() { 252 | return this._v.positionAbsolute; 253 | } 254 | 255 | get stateH() { 256 | return this._h.state; 257 | } 258 | 259 | get stateV() { 260 | return this._v.state; 261 | } 262 | 263 | get scrolled() { 264 | return this._scrolled.asObservable(); 265 | } 266 | get topReached() { 267 | return this._topReached.asObservable(); 268 | } 269 | get bottomReached() { 270 | return this._bottomReached.asObservable(); 271 | } 272 | get leftReached() { 273 | return this._leftReached.asObservable(); 274 | } 275 | get rightReached() { 276 | return this._rightReached.asObservable(); 277 | } 278 | get positionHChanged() { 279 | return this._h.positionChanged.asObservable(); 280 | } 281 | get positionAbsoluteHChanged() { 282 | return this._h.positionAbsoluteChanged.asObservable(); 283 | } 284 | get stateHChanged() { 285 | return this._h.stateChanged.asObservable(); 286 | } 287 | get positionVChanged() { 288 | return this._v.positionChanged.asObservable(); 289 | } 290 | get positionAbsoluteVChanged() { 291 | return this._v.positionAbsoluteChanged.asObservable(); 292 | } 293 | get stateVChanged() { 294 | return this._v.stateChanged.asObservable(); 295 | } 296 | 297 | private get _hostElement() { 298 | return this._hostElementAccessor(); 299 | } 300 | private get _contentElement() { 301 | return this._contentElementAccessor(); 302 | } 303 | private get _ownHeight() { 304 | return this._contentElement.clientHeight; 305 | } 306 | private get _ownWidth() { 307 | return this._contentElement.clientWidth; 308 | } 309 | private get _totalHeight() { 310 | return this._contentElement.scrollHeight; 311 | } 312 | private get _totalWidth() { 313 | return this._contentElement.scrollWidth; 314 | } 315 | 316 | private get _hasScrollbar() { 317 | return !!this._browserScrollbarSize; 318 | } 319 | 320 | /** 321 | * Initializes the scroll. 322 | */ 323 | initialize() { 324 | // Setup subjects 325 | this.positionHChanged.subscribe((position: ScrollPosition) => { 326 | switch (position) { 327 | case "start": 328 | this._leftReached.next(); 329 | break; 330 | case "end": 331 | this._rightReached.next(); 332 | break; 333 | } 334 | }); 335 | 336 | this.positionVChanged.subscribe((position: ScrollPosition) => { 337 | switch (position) { 338 | case "start": 339 | this._topReached.next(); 340 | break; 341 | case "end": 342 | this._bottomReached.next(); 343 | break; 344 | } 345 | }); 346 | 347 | this.positionAbsoluteHChanged.subscribe(() => { 348 | this._updateHiddenContentClasses(); 349 | }); 350 | this.positionAbsoluteVChanged.subscribe(() => { 351 | this._updateHiddenContentClasses(); 352 | }); 353 | this._updateHiddenContentClasses(); 354 | 355 | if (this.mode == "auto") { 356 | // Only in auto mode, we add/remove spacing depending on the state 357 | 358 | this.stateHChanged.subscribe((state: ScrollState) => { 359 | switch (state) { 360 | case "scrolling": 361 | this._addSpacingH(); 362 | break; 363 | 364 | case "hidden": 365 | this._removeSpacingH(); 366 | break; 367 | } 368 | }); 369 | 370 | this.stateVChanged.subscribe((state: ScrollState) => { 371 | switch (state) { 372 | case "scrolling": 373 | this._addSpacingV(); 374 | break; 375 | 376 | case "hidden": 377 | this._removeSpacingV(); 378 | break; 379 | } 380 | }); 381 | } 382 | 383 | // Setup observers 384 | this._ro = new ResizeObserver(() => { 385 | this.update(); 386 | }); 387 | this._ro.observe(this._contentElement); 388 | 389 | const observeNodes = (nodes: NodeList) => { 390 | nodes.forEach((node) => { 391 | if (node.nodeType != node.ELEMENT_NODE) return; 392 | this._ro.observe(node as Element); 393 | }); 394 | }; 395 | const unobserveNodes = (nodes: NodeList) => { 396 | nodes.forEach((node) => { 397 | if (node.nodeType != node.ELEMENT_NODE) return; 398 | this._ro.unobserve(node as Element); 399 | }); 400 | }; 401 | 402 | observeNodes(this._contentElement.childNodes); 403 | 404 | this._mo = new MutationObserver((mutationsList) => { 405 | for (const mutation of mutationsList) { 406 | unobserveNodes(mutation.removedNodes); 407 | observeNodes(mutation.addedNodes); 408 | } 409 | this.update(); 410 | }); 411 | this._mo.observe(this._contentElement, { childList: true }); 412 | 413 | // 414 | this._setCssProperty("--mr-scroll-browser-bar-size", `${this._browserScrollbarSize}px`); 415 | this._setCssProperty("--mr-scroll-bar-size", `${this._scrollbarSize}px`); 416 | 417 | if (!this._hasScrollbar) { 418 | this._hostElement.classList.add(HOST_SIZE_0_MODIFIER); 419 | } 420 | 421 | this._contentElement.addEventListener("scroll", this.update); 422 | window.addEventListener("resize", this.update); 423 | this._addDraggingListeners(); 424 | 425 | this.update(); 426 | } 427 | 428 | /** 429 | * Destroys the scroll. It's required to call this after you finish using the scroll 430 | * so that it can properly deallocate resources and clean after itself. 431 | */ 432 | destroy() { 433 | this._contentElement.removeEventListener("scroll", this.update); 434 | window.removeEventListener("resize", this.update); 435 | this._removeDraggingListeners(); 436 | 437 | this._mo.disconnect(); 438 | this._ro.disconnect(); 439 | 440 | this._scrolled.complete(); 441 | this._topReached.complete(); 442 | this._bottomReached.complete(); 443 | this._leftReached.complete(); 444 | this._rightReached.complete(); 445 | this._h.positionChanged.complete(); 446 | this._h.positionAbsoluteChanged.complete(); 447 | this._h.stateChanged.complete(); 448 | this._v.positionChanged.complete(); 449 | this._v.positionAbsoluteChanged.complete(); 450 | this._v.stateChanged.complete(); 451 | } 452 | 453 | /** 454 | * Updates the scroll's state. Usually you don't need to call this manually as the scroll detects and updates 455 | * itself automatically whenever it needs to. 456 | */ 457 | update() { 458 | const ownHeight = this._ownHeight; 459 | const ownWidth = this._ownWidth; 460 | const totalHeight = this._totalHeight; 461 | const totalWidth = this._totalWidth; 462 | 463 | this._h.scrollRatio = ownWidth / totalWidth; 464 | this._v.scrollRatio = ownHeight / totalHeight; 465 | 466 | const { scrollTop, scrollLeft } = this._contentElement; 467 | const width = this._h.scrollRatio * 100; 468 | const leftRatio = scrollLeft / totalWidth; 469 | const height = this._v.scrollRatio * 100; 470 | const topRatio = scrollTop / totalHeight; 471 | 472 | if (this._h.scroll == null || this._h.scroll != scrollLeft) { 473 | this._h.scroll = scrollLeft; 474 | } 475 | if (this._v.scroll == null || this._v.scroll != scrollTop) { 476 | this._v.scroll = scrollTop; 477 | } 478 | 479 | const computePosition = ( 480 | scrollRatio: number, 481 | scroll: number, 482 | ownSize: number, 483 | totalSize: number, 484 | startThreshold: number, 485 | endThreshold: number, 486 | ) => { 487 | let p: ScrollPosition = "middle"; 488 | if (scrollRatio >= 1) { 489 | p = "full"; 490 | } else { 491 | const isStart = scroll <= startThreshold; 492 | if (isStart) { 493 | p = "start"; 494 | } 495 | 496 | if (!isStart) { 497 | const attainedSize = scroll + ownSize + endThreshold; 498 | if (attainedSize >= totalSize) { 499 | p = "end"; 500 | } 501 | } 502 | } 503 | 504 | return p; 505 | }; 506 | 507 | const computePositionH = (leftThreshold: number, rightThreshold: number) => { 508 | return computePosition( 509 | this._h.scrollRatio, 510 | scrollLeft, 511 | ownWidth, 512 | totalWidth, 513 | leftThreshold, 514 | rightThreshold, 515 | ); 516 | }; 517 | 518 | const computePositionV = (topThreshold: number, bottomThreshold: number) => { 519 | return computePosition( 520 | this._v.scrollRatio, 521 | scrollTop, 522 | ownHeight, 523 | totalHeight, 524 | topThreshold, 525 | bottomThreshold, 526 | ); 527 | }; 528 | 529 | const newPositionH = computePositionH(this._config.leftThreshold, this._config.rightThreshold); 530 | const newPositionAbsoluteH = computePositionH(0, 0); 531 | const newStateH: ScrollState = newPositionH == "full" ? "hidden" : "scrolling"; 532 | 533 | if (newPositionH == "full") { 534 | this._h.bar.barElement.classList.add(BAR_HIDDEN_MODIFIER); 535 | } else { 536 | const c = this._h; 537 | 538 | c.bar.barElement.classList.remove(BAR_HIDDEN_MODIFIER); 539 | const translate = leftRatio * (ownWidth - this._barTotalMargin); 540 | 541 | if (c.size != width) { 542 | c.bar.thumbElement.style.width = `${width}%`; 543 | } 544 | if (c.translate != translate) { 545 | c.bar.thumbElement.style.transform = `translateX(${translate}px)`; 546 | } 547 | 548 | c.size = width; 549 | c.sizePixels = (width / 100) * ownWidth; 550 | c.translate = translate; 551 | } 552 | 553 | const newPositionV = computePositionV(this._config.topThreshold, this._config.bottomThreshold); 554 | const newPositionAbsoluteV = computePositionV(0, 0); 555 | const newStateV: ScrollState = newPositionV == "full" ? "hidden" : "scrolling"; 556 | 557 | if (newPositionV == "full") { 558 | this._v.bar.barElement.classList.add(BAR_HIDDEN_MODIFIER); 559 | } else { 560 | const c = this._v; 561 | 562 | c.bar.barElement.classList.remove(BAR_HIDDEN_MODIFIER); 563 | const translate = topRatio * (ownHeight - this._barTotalMargin); 564 | 565 | if (c.size != height) { 566 | c.bar.thumbElement.style.height = `${height}%`; 567 | } 568 | if (c.translate != translate) { 569 | c.bar.thumbElement.style.transform = `translateY(${translate}px)`; 570 | } 571 | 572 | c.size = height; 573 | c.sizePixels = (height / 100) * ownHeight; 574 | c.translate = translate; 575 | } 576 | 577 | this._setPositionAndStateH(newPositionH, newPositionAbsoluteH, newStateH); 578 | this._setPositionAndStateV(newPositionV, newPositionAbsoluteV, newStateV); 579 | this._scrolled.next({ left: scrollLeft, top: scrollTop }); 580 | } 581 | 582 | scrollTo(options: ScrollToOptions) { 583 | this._contentElement.scroll(options); 584 | } 585 | 586 | scrollToTop(behavior: ScrollBehavior = "auto") { 587 | this.scrollTo({ top: 0, behavior }); 588 | } 589 | 590 | scrollToLeft(behavior: ScrollBehavior = "auto") { 591 | this.scrollTo({ left: 0, behavior }); 592 | } 593 | 594 | //#region dragging 595 | 596 | private _addDraggingListeners() { 597 | if (!this._hasScrollbar) return; 598 | this._h.bar.trackElement.addEventListener("mousedown", this._handleTrackHMouseDown); 599 | this._v.bar.trackElement.addEventListener("mousedown", this._handleTrackVMouseDown); 600 | this._h.bar.thumbElement.addEventListener("mousedown", this._handleThumbHMouseDown); 601 | this._v.bar.thumbElement.addEventListener("mousedown", this._handleThumbVMouseDown); 602 | } 603 | 604 | private _removeDraggingListeners() { 605 | if (!this._hasScrollbar) return; 606 | this._h.bar.trackElement.removeEventListener("mousedown", this._handleTrackHMouseDown); 607 | this._v.bar.trackElement.removeEventListener("mousedown", this._handleTrackVMouseDown); 608 | this._h.bar.thumbElement.removeEventListener("mousedown", this._handleThumbHMouseDown); 609 | this._v.bar.thumbElement.removeEventListener("mousedown", this._handleThumbVMouseDown); 610 | 611 | this._destroyMoveDragging(); 612 | } 613 | 614 | private _handleTrackHMouseDown(e: MouseEvent) { 615 | e.preventDefault(); 616 | const { target, clientX } = e; 617 | const { left } = (target as HTMLElement).getBoundingClientRect(); 618 | const thumbWidth = this._getThumbHWidth(); 619 | const offset = Math.abs(left - clientX) - thumbWidth / 2; 620 | this.scrollTo({ left: this._getScrollLeftForOffset(offset), behavior: "smooth" }); 621 | } 622 | 623 | private _handleTrackVMouseDown(e: MouseEvent) { 624 | e.preventDefault(); 625 | const { target, clientY } = e; 626 | const { top } = (target as HTMLElement).getBoundingClientRect(); 627 | const thumbHeight = this._getThumbVHeight(); 628 | const offset = Math.abs(top - clientY) - thumbHeight / 2; 629 | this.scrollTo({ top: this._getScrollTopForOffset(offset), behavior: "smooth" }); 630 | } 631 | 632 | private _handleThumbHMouseDown(e: MouseEvent) { 633 | e.preventDefault(); 634 | this._handleDragStart(e, "h"); 635 | const { target, clientX } = e; 636 | const { offsetWidth } = target as HTMLElement; 637 | const { left } = (target as HTMLElement).getBoundingClientRect(); 638 | this._prevPageX = offsetWidth - (clientX - left); 639 | } 640 | 641 | private _handleThumbVMouseDown(e: MouseEvent) { 642 | e.preventDefault(); 643 | this._handleDragStart(e, "v"); 644 | const { target, clientY } = e; 645 | const { offsetHeight } = target as HTMLElement; 646 | const { top } = (target as HTMLElement).getBoundingClientRect(); 647 | this._prevPageY = offsetHeight - (clientY - top); 648 | } 649 | 650 | private _setDragging(direction: ScrollDirection, dragging: boolean) { 651 | const c = this._resolveDirectionContext(direction); 652 | if (c.dragging == dragging) return; 653 | c.dragging = dragging; 654 | if (dragging) { 655 | c.bar.barElement.classList.add(BAR_DRAGGING_MODIFIER); 656 | } else { 657 | c.bar.barElement.classList.remove(BAR_DRAGGING_MODIFIER); 658 | } 659 | } 660 | 661 | private _setupMoveDragging() { 662 | document.addEventListener("mousemove", this._handleDrag); 663 | document.addEventListener("mouseup", this._handleDragEnd); 664 | document.onselectstart = () => false; 665 | } 666 | 667 | private _destroyMoveDragging() { 668 | document.removeEventListener("mousemove", this._handleDrag); 669 | document.removeEventListener("mouseup", this._handleDragEnd); 670 | document.onselectstart = null; 671 | } 672 | 673 | private _handleDragStart(e: MouseEvent, direction: ScrollDirection) { 674 | e.stopImmediatePropagation(); 675 | this._setDragging(direction, true); 676 | this._setupMoveDragging(); 677 | } 678 | 679 | private _handleDrag(e: MouseEvent) { 680 | if (this._prevPageX) { 681 | const { clientX } = e; 682 | const { left: trackLeft } = this._h.bar.trackElement.getBoundingClientRect(); 683 | const thumbWidth = this._getThumbHWidth(); 684 | const clickPosition = thumbWidth - this._prevPageX; 685 | const offset = -trackLeft + clientX - clickPosition; 686 | this._contentElement.scrollLeft = this._getScrollLeftForOffset(offset); 687 | } 688 | if (this._prevPageY) { 689 | const { clientY } = e; 690 | const { top: trackTop } = this._v.bar.trackElement.getBoundingClientRect(); 691 | const thumbHeight = this._getThumbVHeight(); 692 | const clickPosition = thumbHeight - this._prevPageY; 693 | const offset = -trackTop + clientY - clickPosition; 694 | this._contentElement.scrollTop = this._getScrollTopForOffset(offset); 695 | } 696 | return false; 697 | } 698 | 699 | private _handleDragEnd() { 700 | this._setDragging("h", false); 701 | this._setDragging("v", false); 702 | this._prevPageX = this._prevPageY = 0; 703 | this._destroyMoveDragging(); 704 | } 705 | 706 | private _getThumbHWidth() { 707 | return this._h.sizePixels; 708 | } 709 | 710 | private _getThumbVHeight() { 711 | return this._v.sizePixels; 712 | } 713 | 714 | private _getScrollLeftForOffset(offset: number) { 715 | const { scrollWidth, clientWidth } = this._contentElement; 716 | const trackWidth = this._getInnerWidth(this._h.bar.trackElement); 717 | const thumbWidth = this._getThumbHWidth(); 718 | return (offset / (trackWidth - thumbWidth)) * (scrollWidth - clientWidth); 719 | } 720 | 721 | private _getScrollTopForOffset(offset: number) { 722 | const { scrollHeight, clientHeight } = this._contentElement; 723 | const trackHeight = this._getInnerHeight(this._v.bar.trackElement); 724 | const thumbHeight = this._getThumbVHeight(); 725 | return (offset / (trackHeight - thumbHeight)) * (scrollHeight - clientHeight); 726 | } 727 | 728 | private _getInnerWidth(el: HTMLElement) { 729 | const { clientWidth } = el; 730 | const { paddingLeft, paddingRight } = getComputedStyle(el); 731 | return clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight); 732 | } 733 | 734 | private _getInnerHeight(el: HTMLElement) { 735 | const { clientHeight } = el; 736 | const { paddingTop, paddingBottom } = getComputedStyle(el); 737 | return clientHeight - parseFloat(paddingTop) - parseFloat(paddingBottom); 738 | } 739 | 740 | //#endregion 741 | 742 | private _resolveDirectionContext(direction: ScrollDirection) { 743 | switch (direction) { 744 | case "h": 745 | return this._h; 746 | case "v": 747 | return this._v; 748 | } 749 | } 750 | 751 | private _setPositionAndState( 752 | direction: ScrollDirection, 753 | position: ScrollPosition, 754 | positionAbsolute: ScrollPosition, 755 | state: ScrollState, 756 | ) { 757 | const c = this._resolveDirectionContext(direction); 758 | const positionChanged = position != c.position; 759 | const positionAbsoluteChanged = positionAbsolute != c.positionAbsolute; 760 | const stateChanged = state != c.state; 761 | 762 | if (!positionChanged && !positionAbsoluteChanged && !stateChanged) { 763 | return; 764 | } 765 | 766 | c.position = position; 767 | c.positionAbsolute = positionAbsolute; 768 | c.state = state; 769 | 770 | if (positionChanged) { 771 | c.positionChanged.next(position); 772 | } 773 | if (positionAbsoluteChanged) { 774 | c.positionAbsoluteChanged.next(positionAbsolute); 775 | } 776 | if (stateChanged) { 777 | c.stateChanged.next(state); 778 | } 779 | } 780 | 781 | private _setPositionAndStateH( 782 | position: ScrollPosition, 783 | positionAbsolute: ScrollPosition, 784 | state: ScrollState, 785 | ) { 786 | this._setPositionAndState("h", position, positionAbsolute, state); 787 | } 788 | 789 | private _setPositionAndStateV( 790 | position: ScrollPosition, 791 | positionAbsolute: ScrollPosition, 792 | state: ScrollState, 793 | ) { 794 | this._setPositionAndState("v", position, positionAbsolute, state); 795 | } 796 | 797 | private _updateHiddenContentClasses() { 798 | const setClasses = ( 799 | classes: { start: string; end: string }, 800 | extremities: ScrollExtremity[] | null, 801 | ) => { 802 | this._hostElement.classList.remove(classes.start, classes.end); 803 | if (extremities != null) 804 | for (const c of extremities) { 805 | switch (c) { 806 | case "start": 807 | this._hostElement.classList.add(classes.start); 808 | break; 809 | 810 | case "end": 811 | this._hostElement.classList.add(classes.end); 812 | break; 813 | } 814 | } 815 | }; 816 | 817 | const classesH = { 818 | start: HOST_HIDDEN_CONTENT_LEFT_MODIFIER, 819 | end: HOST_HIDDEN_CONTENT_RIGHT_MODIFIER, 820 | }; 821 | const classesV = { 822 | start: HOST_HIDDEN_CONTENT_TOP_MODIFIER, 823 | end: HOST_HIDDEN_CONTENT_BOTTOM_MODIFIER, 824 | }; 825 | 826 | [ 827 | { position: this._h.positionAbsolute, classes: classesH }, 828 | { position: this._v.positionAbsolute, classes: classesV }, 829 | ].forEach((x) => { 830 | switch (x.position) { 831 | case "full": 832 | setClasses(x.classes, null); 833 | break; 834 | 835 | case "start": 836 | setClasses(x.classes, ["end"]); 837 | break; 838 | 839 | case "middle": 840 | setClasses(x.classes, ["start", "end"]); 841 | break; 842 | 843 | case "end": 844 | setClasses(x.classes, ["start"]); 845 | break; 846 | } 847 | }); 848 | } 849 | 850 | private _addSpacing() { 851 | this._addSpacingH(); 852 | this._addSpacingV(); 853 | } 854 | 855 | private _addSpacingH() { 856 | if (!this._hasScrollbar) return; 857 | 858 | this._contentElement.style.marginBottom = `-${this._browserScrollbarSize}px`; 859 | if (this.mode == "auto") { 860 | this._contentElement.style.paddingBottom = `${this._scrollbarSize}px`; 861 | } 862 | } 863 | 864 | private _removeSpacingH() { 865 | if (!this._hasScrollbar) return; 866 | 867 | this._contentElement.style.marginBottom = ""; 868 | this._contentElement.style.paddingBottom = ""; 869 | } 870 | 871 | private _addSpacingV() { 872 | if (!this._hasScrollbar) return; 873 | 874 | this._contentElement.style.marginRight = `-${this._browserScrollbarSize}px`; 875 | if (this.mode == "auto") { 876 | this._contentElement.style.paddingRight = `${this._scrollbarSize}px`; 877 | } 878 | } 879 | 880 | private _removeSpacingV() { 881 | if (!this._hasScrollbar) return; 882 | 883 | this._contentElement.style.marginRight = ""; 884 | this._contentElement.style.paddingRight = ""; 885 | } 886 | 887 | private _getCssProperty(name: string) { 888 | return getComputedStyle(this._hostElement).getPropertyValue(name); 889 | } 890 | 891 | private _setCssProperty(name: string, value: string) { 892 | this._hostElement.style.setProperty(name, value); 893 | } 894 | 895 | private _resolveBrowserScrollbarSize() { 896 | return getScrollbarWidth(); 897 | } 898 | 899 | private _resolveScrollbarSize(mode = this.mode) { 900 | let type = "normal"; 901 | if (mode == "overlay" || mode == "hidden") { 902 | type = "overlay"; 903 | } 904 | const widthRaw = this._getCssProperty(`--mr-scroll-bar-size-${type}`).trim(); 905 | if (!widthRaw) { 906 | return null; 907 | } 908 | return parseInt(widthRaw.substring(0, widthRaw.length - 2)); 909 | } 910 | 911 | private _resolveBarMargin() { 912 | const margin = this._getCssProperty("--mr-scroll-bar-margin"); 913 | return parseInt(margin.substring(0, margin.length - 2)); 914 | } 915 | } 916 | -------------------------------------------------------------------------------- /packages/core/src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | @use "sass:meta"; 3 | 4 | /// Gets the rgb values of the color. 5 | /// @access private 6 | @function get-rgb($color) { 7 | $r: color.red($color); 8 | $g: color.green($color); 9 | $b: color.blue($color); 10 | @return $r, $g, $b; 11 | } 12 | 13 | /// Sets the element to be a column flex container and adds proper flags to disable the auto min behavior of flexbox. 14 | /// Check this for more about the auto min behavior: https://stackoverflow.com/a/36247448/2172786. 15 | /// Note that you can override this to be a row flex container. We made it column because that's the most common case. 16 | /// @access public 17 | /// @param {Boolean} $flex [true] - Whether or not to add `flex: 1` 18 | @mixin flex-adaptive-container($flex: true) { 19 | @if ($flex == true) { 20 | flex: 1; 21 | } 22 | display: flex; 23 | flex-flow: column; 24 | min-width: 0; 25 | min-height: 0; 26 | } 27 | 28 | /// Pads the content of the scroll. This makes it so the inner content is padded without affecting the placement of the scrollbar. 29 | /// @access public 30 | /// @param {Number} $padding - The padding 31 | @mixin pad-content($padding) { 32 | > .mr-scroll > .mr-scroll_content { 33 | padding: $padding; 34 | } 35 | } 36 | 37 | /// Sets a fixed height for the content. 38 | /// @access public 39 | /// @param {Number} $height - The height 40 | @mixin height($height) { 41 | > .mr-scroll { 42 | height: $height; 43 | } 44 | } 45 | 46 | /// Sets a max height for the content. 47 | /// @access public 48 | /// @param {Number} $max-height - The max height 49 | @mixin max-height($max-height) { 50 | > .mr-scroll { 51 | max-height: $max-height; 52 | } 53 | } 54 | 55 | /// Applies a hidden content fade to the scroll. 56 | /// @access public 57 | /// @param {Color|String|List} $color - The background color (can be a CSS var that points to an rgb value, a color, or an rgb value) 58 | @mixin hidden-content-fade($color: null) { 59 | @if ($color == null) { 60 | $color: white; 61 | } 62 | 63 | $type: meta.type-of($color); 64 | $color-rgb: null; 65 | 66 | @if ($type == "color") { 67 | // i.e white, #fff, etc 68 | $color-rgb: get-rgb($color); 69 | } @else if ($type == "string") { 70 | // i.e var(--rgb) 71 | $color-rgb: $color; 72 | } @else { 73 | // i.e a variable poiting to "255, 255, 255" (without quotes) 74 | $color-rgb: $color; 75 | } 76 | 77 | > .mr-scroll { 78 | > .mr-scroll_hidden-content-fade { 79 | --mr-scroll-hidden-content-fade-gradient-color: #{$color-rgb}; 80 | display: initial; 81 | } 82 | } 83 | } 84 | 85 | /// Overrides the thumb border radius. 86 | /// @access public 87 | /// @param {Number} $border-radius - The border radius 88 | @mixin override-thumb-border-radius($border-radius) { 89 | > .mr-scroll > .mr-scroll_bar > .mr-scroll_bar-thumb { 90 | border-radius: $border-radius; 91 | } 92 | } 93 | 94 | /// Overrides the hidden content fade size. 95 | /// @access public 96 | /// @param {Number} $size - The size 97 | @mixin override-hidden-content-fade-size($size) { 98 | > .mr-scroll { 99 | &::after, 100 | &::before { 101 | height: $size; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/core/src/scss/_pure.import.scss: -------------------------------------------------------------------------------- 1 | @forward "mixins" as msc-*; 2 | -------------------------------------------------------------------------------- /packages/core/src/scss/_pure.scss: -------------------------------------------------------------------------------- 1 | @forward "mixins"; 2 | -------------------------------------------------------------------------------- /packages/core/src/scss/scroll.scss: -------------------------------------------------------------------------------- 1 | @use "pure" as *; 2 | 3 | :root { 4 | --mr-scroll-bar-size-normal: 12px; 5 | --mr-scroll-bar-size-overlay: 8px; 6 | --mr-scroll-bar-margin: 3px; 7 | --mr-scroll-track-color: transparent; 8 | --mr-scroll-thumb-border-radius: 2px; 9 | --mr-scroll-thumb-color: #aaa; 10 | --mr-scroll-hidden-content-fade-size: 25px; 11 | } 12 | 13 | .mr-scroll { 14 | @include flex-adaptive-container(); 15 | overflow: hidden; 16 | position: relative; 17 | 18 | &--mode-auto { 19 | > .mr-scroll_content { 20 | overflow: auto; 21 | } 22 | } 23 | 24 | &--mode-hidden { 25 | > .mr-scroll_bar { 26 | display: none; 27 | } 28 | } 29 | 30 | &--show-on-hover { 31 | > .mr-scroll_bar { 32 | opacity: 0; 33 | transition: opacity linear 0.2s; 34 | } 35 | 36 | &:hover { 37 | > .mr-scroll_bar { 38 | opacity: 1; 39 | } 40 | } 41 | } 42 | 43 | &--size-0 { 44 | > .mr-scroll_bar { 45 | display: none; 46 | } 47 | } 48 | 49 | $sides: ("l", "r", "t", "b"); 50 | @each $side in $sides { 51 | &--hidden-content-#{$side} { 52 | > .mr-scroll_hidden-content-fade--#{$side} { 53 | opacity: 1; 54 | } 55 | } 56 | } 57 | } 58 | 59 | .mr-scroll_hidden-content-fade { 60 | --mr-scroll-hidden-content-fade-gradient-content: rgba( 61 | var(--mr-scroll-hidden-content-fade-gradient-color), 62 | 0 63 | ) 64 | 0%, 65 | rgba(var(--mr-scroll-hidden-content-fade-gradient-color), 1) 100%; 66 | display: none; 67 | pointer-events: none; 68 | position: absolute; 69 | background: var(--mr-scroll-hidden-content-fade); 70 | opacity: 0; 71 | transition: opacity linear 0.1s; 72 | 73 | &--t, 74 | &--b { 75 | left: 0; 76 | height: var(--mr-scroll-hidden-content-fade-size); 77 | width: 100%; 78 | } 79 | 80 | &--l, 81 | &--r { 82 | top: 0; 83 | height: 100%; 84 | width: var(--mr-scroll-hidden-content-fade-size); 85 | } 86 | 87 | &--t { 88 | --mr-scroll-hidden-content-fade: linear-gradient( 89 | to top, 90 | var(--mr-scroll-hidden-content-fade-gradient-content) 91 | ); 92 | top: 0; 93 | } 94 | 95 | &--b { 96 | --mr-scroll-hidden-content-fade: linear-gradient( 97 | to bottom, 98 | var(--mr-scroll-hidden-content-fade-gradient-content) 99 | ); 100 | bottom: 0; 101 | } 102 | 103 | &--l { 104 | --mr-scroll-hidden-content-fade: linear-gradient( 105 | to left, 106 | var(--mr-scroll-hidden-content-fade-gradient-content) 107 | ); 108 | left: 0; 109 | } 110 | 111 | &--r { 112 | --mr-scroll-hidden-content-fade: linear-gradient( 113 | to right, 114 | var(--mr-scroll-hidden-content-fade-gradient-content) 115 | ); 116 | right: 0; 117 | } 118 | } 119 | 120 | .mr-scroll_content { 121 | @include flex-adaptive-container(); 122 | height: 100%; 123 | overflow: scroll; 124 | } 125 | 126 | .mr-scroll_bar { 127 | position: absolute; 128 | display: flex; 129 | 130 | &:hover { 131 | > .mr-scroll_bar-track { 132 | opacity: 1; 133 | } 134 | } 135 | 136 | &--h { 137 | height: var(--mr-scroll-bar-size); 138 | right: 0; 139 | bottom: 0; 140 | left: 0; 141 | flex-flow: column; 142 | 143 | > .mr-scroll_bar-thumb { 144 | top: 0; 145 | bottom: 0; 146 | left: 0; 147 | } 148 | } 149 | 150 | &--v { 151 | width: var(--mr-scroll-bar-size); 152 | top: 0; 153 | right: 0; 154 | bottom: 0; 155 | 156 | > .mr-scroll_bar-thumb { 157 | top: 0; 158 | right: 0; 159 | left: 0; 160 | } 161 | } 162 | 163 | &--hidden { 164 | display: none; 165 | } 166 | 167 | &--dragging { 168 | > .mr-scroll_bar-track, 169 | > .mr-scroll_bar-thumb { 170 | opacity: 1; 171 | } 172 | } 173 | } 174 | 175 | .mr-scroll_bar-track { 176 | margin: 3px; 177 | flex-grow: 1; 178 | background: var(--mr-scroll-track-color); 179 | cursor: pointer; 180 | opacity: 0.7; 181 | transition: opacity linear 0.1s; 182 | } 183 | 184 | .mr-scroll_bar-thumb { 185 | background: var(--mr-scroll-thumb-color); 186 | border-radius: var(--mr-scroll-thumb-border-radius); 187 | cursor: pointer; 188 | position: absolute; 189 | margin: 3px; 190 | opacity: 0.7; 191 | transition: opacity linear 0.1s; 192 | 193 | &:hover { 194 | opacity: 1; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /packages/core/src/support.ts: -------------------------------------------------------------------------------- 1 | let _computed = false; 2 | let _scrollbarWidth: number; 3 | let _scrollbarStylingSupported: boolean; 4 | 5 | /** 6 | * Computes the browser's native scrollbar width. 7 | */ 8 | function computeScrollbarWidth() { 9 | const test = document.createElement("div"); 10 | test.style.width = "100px"; 11 | test.style.height = "100px"; 12 | test.style.overflow = "scroll"; 13 | test.style.position = "absolute"; 14 | test.style.top = "-9999px"; 15 | 16 | document.body.appendChild(test); 17 | const scrollbarWidth = test.offsetWidth - test.clientWidth; 18 | document.body.removeChild(test); 19 | 20 | return scrollbarWidth; 21 | } 22 | 23 | /** 24 | * Computes whether or not scrollbar styling is supported. 25 | */ 26 | function computeScrollbarStylingSupported() { 27 | const test = document.createElement("div"); 28 | test.className = "__mr-sb-styling-test"; 29 | test.style.overflow = "scroll"; 30 | test.style.width = "40px"; 31 | 32 | const style = document.createElement("style"); 33 | style.innerHTML = ".__mr-sb-styling-test::-webkit-scrollbar { width: 0px; }"; 34 | 35 | // Apply 36 | document.body.appendChild(test); 37 | document.body.appendChild(style); 38 | 39 | // If css scrollbar is supported, than the scrollWidth should not be impacted 40 | const supported = test.scrollWidth == 40; 41 | 42 | // Cleaning 43 | document.body.removeChild(test); 44 | document.body.removeChild(style); 45 | 46 | return supported; 47 | } 48 | 49 | function ensureComputed() { 50 | if (_computed) { 51 | return; 52 | } 53 | _computed = true; 54 | 55 | _scrollbarWidth = computeScrollbarWidth(); 56 | _scrollbarStylingSupported = computeScrollbarStylingSupported(); 57 | } 58 | 59 | /** 60 | * @returns the browser's native scrollbar width. 61 | */ 62 | export function getScrollbarWidth() { 63 | ensureComputed(); 64 | return _scrollbarWidth; 65 | } 66 | 67 | /** 68 | * @returns whether or not scrollbar styling is supported. 69 | */ 70 | export function isScrollbarStylingSupported() { 71 | ensureComputed(); 72 | return _scrollbarStylingSupported; 73 | } 74 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2015", 5 | "module": "es2020", 6 | "strict": true, 7 | "esModuleInterop": true 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/css-theming/README.md: -------------------------------------------------------------------------------- 1 | # @mr-scroll/css-theming 2 | 3 | [![npm](https://img.shields.io/npm/v/@mr-scroll/css-theming.svg)](https://www.npmjs.com/package/@mr-scroll/css-theming) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | The best custom scroll for the web. 7 | 8 | This is the [css-theming](https://github.com/mrahhal/css-theming) support package. [Check here](../../README.md) (root of this repo) for an overview on mr-scroll. 9 | 10 | This package styles the scroll (thumb, etc) according to the active theme when using [css-theming](https://github.com/mrahhal/css-theming). 11 | 12 | ## Install 13 | 14 | Assuming we're using angular (if not, install the respective wrapper package instead of @mr-scroll/angular): 15 | 16 | ``` 17 | npm i @mr-scroll/core @mr-scroll/angular @mr-scroll/css-theming 18 | ``` 19 | 20 | ## Usage 21 | 22 | You only need to import the SCSS file that this package includes in your global SCSS file, and call the mixin it provides: 23 | 24 | ```scss 25 | // For example, in styles.scss 26 | 27 | // From css-theming 28 | @import "css-theming/src/scss/css-theming"; 29 | // From @mr-scroll/css-theming 30 | @import "@mr-scroll/css-theming/src/scss/css-theming"; 31 | 32 | // You can optionally provide values here. 33 | @include msct-apply(); 34 | ``` 35 | 36 | If you're using the SCSS module system instead: 37 | 38 | ```scss 39 | // For example, in styles.scss 40 | 41 | // From css-theming 42 | @use "css-theming/src/scss/css-theming"; 43 | // From @mr-scroll/css-theming 44 | @use "@mr-scroll/css-theming/src/scss/css-theming" as msct; 45 | 46 | // You can optionally provide values here. 47 | @include msct.apply(); 48 | ``` 49 | 50 | [Example from sample here.](https://github.com/mrahhal/mr-scroll/blob/0780d36414c7032a5853daa53ec390cc9427537c/samples/angular/src/styles.scss#L3-L7) 51 | 52 | That's it. If you have css-theming set up properly, you'll see that the scroll's thumb changes colors as the user switches between light and dark themes. 53 | -------------------------------------------------------------------------------- /packages/css-theming/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/css-theming", 3 | "version": "1.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@mr-scroll/css-theming", 9 | "version": "1.2.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@mr-scroll/core": "file:../core" 13 | }, 14 | "peerDependencies": { 15 | "@mr-scroll/core": "^0.1.0", 16 | "css-theming": ">=1.5.0" 17 | } 18 | }, 19 | "../core": { 20 | "version": "0.1.0", 21 | "dev": true, 22 | "license": "MIT", 23 | "dependencies": { 24 | "resize-observer-polyfill": "^1.5.1" 25 | }, 26 | "devDependencies": { 27 | "@rollup/plugin-node-resolve": "^13.0.0", 28 | "@rollup/plugin-typescript": "^8.2.1", 29 | "@typescript-eslint/eslint-plugin": "^4.22.1", 30 | "@typescript-eslint/parser": "^4.22.1", 31 | "concurrently": "^6.1.0", 32 | "eslint": "^7.25.0", 33 | "nodemon": "^2.0.7", 34 | "rollup": "^2.47.0", 35 | "rxjs": "^6.6.7", 36 | "sass": "^1.32.13", 37 | "stylelint": "^13.13.1", 38 | "stylelint-config-standard": "^22.0.0", 39 | "tslib": "^2.2.0", 40 | "typescript": "~4.2.4" 41 | }, 42 | "peerDependencies": { 43 | "rxjs": ">=6.0.0" 44 | } 45 | }, 46 | "node_modules/@mr-scroll/core": { 47 | "resolved": "../core", 48 | "link": true 49 | }, 50 | "node_modules/css-theming": { 51 | "version": "1.5.0", 52 | "resolved": "https://registry.npmjs.org/css-theming/-/css-theming-1.5.0.tgz", 53 | "integrity": "sha512-t+9//AvlnpFFbK3acT6X7hN+Wr3EqdEshrULz3FnjBxSNHPJFb9z/gT//YgSS7x8o/bbs75U8BJcXlOlow9NJg==", 54 | "peer": true 55 | } 56 | }, 57 | "dependencies": { 58 | "@mr-scroll/core": { 59 | "version": "file:../core", 60 | "requires": { 61 | "@rollup/plugin-node-resolve": "^13.0.0", 62 | "@rollup/plugin-typescript": "^8.2.1", 63 | "@typescript-eslint/eslint-plugin": "^4.22.1", 64 | "@typescript-eslint/parser": "^4.22.1", 65 | "concurrently": "^6.1.0", 66 | "eslint": "^7.25.0", 67 | "nodemon": "^2.0.7", 68 | "resize-observer-polyfill": "^1.5.1", 69 | "rollup": "^2.47.0", 70 | "rxjs": "^6.6.7", 71 | "sass": "^1.32.13", 72 | "stylelint": "^13.13.1", 73 | "stylelint-config-standard": "^22.0.0", 74 | "tslib": "^2.2.0", 75 | "typescript": "~4.2.4" 76 | } 77 | }, 78 | "css-theming": { 79 | "version": "1.5.0", 80 | "resolved": "https://registry.npmjs.org/css-theming/-/css-theming-1.5.0.tgz", 81 | "integrity": "sha512-t+9//AvlnpFFbK3acT6X7hN+Wr3EqdEshrULz3FnjBxSNHPJFb9z/gT//YgSS7x8o/bbs75U8BJcXlOlow9NJg==", 82 | "peer": true 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/css-theming/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/css-theming", 3 | "description": "The best custom scroll for the web. This is the css-theming support package.", 4 | "version": "1.2.0", 5 | "keywords": [ 6 | "custom", 7 | "scroll", 8 | "scrollbar", 9 | "themes", 10 | "css-theming" 11 | ], 12 | "author": "Mohammad Rahhal", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/mrahhal/mr-scroll" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/mrahhal/mr-scroll/issues" 20 | }, 21 | "files": [ 22 | "src/scss" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "peerDependencies": { 28 | "@mr-scroll/core": "^1.0.0", 29 | "css-theming": ">=1.5.0" 30 | }, 31 | "devDependencies": { 32 | "@mr-scroll/core": "file:../core" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/css-theming/src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "@mr-scroll/core/src/scss/pure" as msc; 2 | @use "css-theming/src/scss/themes"; 3 | 4 | @mixin apply-thumb-color($thumb-color-light: null, $thumb-color-dark: null) { 5 | @if ($thumb-color-light == null) { 6 | $thumb-color-light: #aaa; 7 | } 8 | @if ($thumb-color-dark == null) { 9 | $thumb-color-dark: white; 10 | } 11 | 12 | @include themes.themes-apply { 13 | --mr-scroll-thumb-color: #{if( 14 | themes.$brightness == "light", 15 | $thumb-color-light, 16 | $thumb-color-dark 17 | )}; 18 | } 19 | } 20 | 21 | @mixin apply($thumb-color-light: null, $thumb-color-dark: null) { 22 | @include apply-thumb-color($thumb-color-light, $thumb-color-dark); 23 | } 24 | -------------------------------------------------------------------------------- /packages/css-theming/src/scss/css-theming.import.scss: -------------------------------------------------------------------------------- 1 | @forward "mixins" as msct-*; 2 | -------------------------------------------------------------------------------- /packages/css-theming/src/scss/css-theming.scss: -------------------------------------------------------------------------------- 1 | @forward "mixins"; 2 | -------------------------------------------------------------------------------- /packages/react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "standard-react", 5 | "plugin:prettier/recommended", 6 | "prettier/standard", 7 | "prettier/react", 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 2020, 12 | "ecmaFeatures": { 13 | "legacyDecorators": true, 14 | "jsx": true 15 | } 16 | }, 17 | "settings": { 18 | "react": { 19 | "version": "16" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # @mr-scroll/angular 2 | 3 | [![npm](https://img.shields.io/npm/v/@mr-scroll/react.svg)](https://www.npmjs.com/package/@mr-scroll/react) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | The best custom scroll for the web. 7 | 8 | This is the react wrapper. [Check here](../../README.md) (root of this repo) for an overview on mr-scroll. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm i @mr-scroll/core @mr-scroll/react 14 | ``` 15 | 16 | React 16 and above is supported. 17 | 18 | Note: If you're using [css-theming](https://github.com/mrahhal/css-theming), check the [css-theming support package](../css-theming). 19 | 20 | ## Usage 21 | 22 | Import the global CSS styles (for example in App.css): 23 | 24 | ```css 25 | @import "@mr-scroll/core/dist/styles.css"; 26 | ``` 27 | 28 | ```tsx 29 | import React, { Component } from "react"; 30 | import Scroll from "@mr-scroll/react"; 31 | 32 | class Example extends Component { 33 | render() { 34 | return Content; 35 | } 36 | } 37 | ``` 38 | 39 | > For more general usage info check the [README](../../README.md) in the root of this repo. 40 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/react", 3 | "description": "The best custom scroll for the web. This is the react wrapper.", 4 | "version": "1.2.0", 5 | "main": "dist/index.js", 6 | "module": "dist/index.modern.js", 7 | "types": "dist/index.d.ts", 8 | "engines": { 9 | "node": ">=10" 10 | }, 11 | "scripts": { 12 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 13 | "build": "microbundle-crl --no-compress --format modern,cjs", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "keywords": [ 17 | "custom", 18 | "scroll", 19 | "scrollbar", 20 | "react", 21 | "component" 22 | ], 23 | "author": "Mohammad Rahhal", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/mrahhal/mr-scroll" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/mrahhal/mr-scroll/issues" 31 | }, 32 | "publishConfig": { 33 | "access": "public" 34 | }, 35 | "files": [ 36 | "dist" 37 | ], 38 | "peerDependencies": { 39 | "@mr-scroll/core": "^1.0.0", 40 | "react": ">=16.0.0" 41 | }, 42 | "devDependencies": { 43 | "@mr-scroll/core": "file:../core", 44 | "@testing-library/jest-dom": "^4.2.4", 45 | "@testing-library/react": "^9.5.0", 46 | "@testing-library/user-event": "^7.2.1", 47 | "@types/jest": "^25.1.4", 48 | "@types/node": "^12.12.38", 49 | "@types/react": "^16.9.27", 50 | "@types/react-dom": "^16.9.7", 51 | "cross-env": "^7.0.2", 52 | "gh-pages": "^2.2.0", 53 | "microbundle-crl": "^0.13.11", 54 | "npm-run-all": "^4.1.5", 55 | "react": "^16.13.1", 56 | "react-dom": "^16.13.1", 57 | "react-scripts": "^3.4.1", 58 | "typescript": "^3.7.5" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Scroll, ScrollMode, ScrollPosition, ScrollState } from "@mr-scroll/core"; 2 | import React from "react"; 3 | 4 | interface Props { 5 | mode?: ScrollMode; 6 | topThreshold?: number; 7 | bottomThreshold?: number; 8 | leftThreshold?: number; 9 | rightThreshold?: number; 10 | showOnHover?: boolean; 11 | 12 | scrolled?: (e: { left: number; top: number }) => void; 13 | topReached?: () => void; 14 | bottomReached?: () => void; 15 | leftReached?: () => void; 16 | rightReached?: () => void; 17 | positionHChanged?: (e: ScrollPosition) => void; 18 | positionAbsoluteHChanged?: (e: ScrollPosition) => void; 19 | stateHChanged?: (e: ScrollState) => void; 20 | positionVChanged?: (e: ScrollPosition) => void; 21 | positionAbsoluteVChanged?: (e: ScrollPosition) => void; 22 | stateVChanged?: (e: ScrollState) => void; 23 | } 24 | 25 | export default class ScrollComponent extends React.Component { 26 | private _scroll: Scroll; 27 | private _hostRef: React.RefObject; 28 | private _contentRef: React.RefObject; 29 | 30 | constructor(props: Props) { 31 | super(props); 32 | this._hostRef = React.createRef(); 33 | this._contentRef = React.createRef(); 34 | } 35 | 36 | get scroll() { 37 | return this._scroll; 38 | } 39 | 40 | componentDidMount() { 41 | this._scroll = new Scroll( 42 | () => this._hostRef.current! as HTMLElement, 43 | () => this._contentRef.current! as HTMLElement, 44 | { 45 | mode: this.props.mode, 46 | topThreshold: this.props.topThreshold, 47 | bottomThreshold: this.props.bottomThreshold, 48 | leftThreshold: this.props.leftThreshold, 49 | rightThreshold: this.props.rightThreshold, 50 | showOnHover: this.props.showOnHover, 51 | }, 52 | ); 53 | 54 | const delegatedEvents = [ 55 | "scrolled", 56 | "topReached", 57 | "bottomReached", 58 | "leftReached", 59 | "rightReached", 60 | "positionHChanged", 61 | "positionAbsoluteHChanged", 62 | "stateHChanged", 63 | "positionVChanged", 64 | "positionAbsoluteVChanged", 65 | "stateVChanged", 66 | ]; 67 | for (const eventName of delegatedEvents) { 68 | this._scroll[eventName].subscribe((x: any) => { 69 | // eslint-disable-next-line @typescript-eslint/ban-types 70 | const handler = this.props[eventName] as Function; 71 | if (handler) { 72 | handler(x); 73 | } 74 | }); 75 | } 76 | 77 | this._scroll.initialize(); 78 | } 79 | 80 | componentWillUnmount() { 81 | this._scroll.destroy(); 82 | } 83 | 84 | render() { 85 | return ( 86 |
87 |
{this.props.children}
88 |
89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2015", 5 | "module": "esnext", 6 | "outDir": "dist", 7 | "lib": ["dom", "esnext"], 8 | "jsx": "react", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # @mr-scroll/vue 2 | 3 | [![npm](https://img.shields.io/npm/v/@mr-scroll/react.svg)](https://www.npmjs.com/package/@mr-scroll/react) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | The best custom scroll for the web. 7 | 8 | This is the vue wrapper. [Check here](../../README.md) (root of this repo) for an overview on mr-scroll. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm i @mr-scroll/core @mr-scroll/vue 14 | ``` 15 | 16 | Note: If you're using [css-theming](https://github.com/mrahhal/css-theming), check the [css-theming support package](../css-theming). 17 | 18 | ## Usage 19 | 20 | Register as a global component: 21 | 22 | ```js 23 | import { createApp } from 'vue'; 24 | import Scroll from '@mr-scroll/vue'; 25 | 26 | const app = createApp(..options); 27 | app.use(Scroll); 28 | ``` 29 | 30 | Register as a local component: 31 | 32 | ```js 33 | import Scroll from "@mr-scroll/vue"; 34 | 35 | export default { 36 | name: "MyComponent", 37 | components: { 38 | "mr-scroll": Scroll, 39 | }, 40 | }; 41 | ``` 42 | 43 | Import the global CSS styles (for example in your App.vue): 44 | 45 | ```vue 46 | 47 | ``` 48 | 49 | Use `mr-scroll` component: 50 | 51 | ```html 52 | Content 53 | ``` 54 | 55 | > For more general usage info check the [README](../../README.md) in the root of this repo. 56 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/vue", 3 | "description": "The best custom scroll for the web. This is the vue wrapper.", 4 | "version": "1.2.0", 5 | "main": "dist/mr-scroll.umd.js", 6 | "module": "dist/mr-scroll.common.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "lint": "vue-cli-service lint", 10 | "build": "vue-cli-service build --target lib --name mr-scroll src/index.ts", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "keywords": [ 14 | "custom", 15 | "scroll", 16 | "scrollbar", 17 | "vue", 18 | "component" 19 | ], 20 | "author": "Mohammad Rahhal", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/mrahhal/mr-scroll" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/mrahhal/mr-scroll/issues" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "files": [ 33 | "dist" 34 | ], 35 | "peerDependencies": { 36 | "@mr-scroll/core": "^1.0.0", 37 | "vue": ">=3.0.0", 38 | "vue-class-component": ">=8.0.0-0", 39 | "vue-property-decorator": ">=10.0.0-0" 40 | }, 41 | "devDependencies": { 42 | "@mr-scroll/core": "file:../core", 43 | "@vue/cli-plugin-typescript": "~4.5.0", 44 | "@vue/cli-service": "~4.5.0", 45 | "@vue/compiler-sfc": "^3.0.11", 46 | "typescript": "~4.1.5", 47 | "vue": "^3.0.0", 48 | "vue-class-component": "^8.0.0-0", 49 | "vue-property-decorator": "^10.0.0-0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | 3 | import ScrollComponent from "./scroll.vue"; 4 | 5 | (ScrollComponent as any).install = (app: App) => { 6 | app.component("mr-scroll", ScrollComponent); 7 | }; 8 | 9 | export default ScrollComponent; 10 | -------------------------------------------------------------------------------- /packages/vue/src/scroll.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 68 | -------------------------------------------------------------------------------- /packages/vue/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module "*.vue" { 3 | import type { DefineComponent } from "vue"; 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2015", 5 | "module": "es2020", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "outDir": "dist", 9 | "importHelpers": true, 10 | "strict": true, 11 | "experimentalDecorators": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.vue"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: (config) => { 3 | // These are some necessary steps changing the default webpack config of the Vue CLI 4 | // that need to be changed in order for Typescript based components to generate their 5 | // declaration (.d.ts) files. 6 | // 7 | // Discussed here https://github.com/vuejs/vue-cli/issues/1081 8 | if (process.env.NODE_ENV === "production") { 9 | config.module.rule("ts").uses.delete("cache-loader"); 10 | 11 | config.module 12 | .rule("ts") 13 | .use("ts-loader") 14 | .loader("ts-loader") 15 | .tap((opts) => { 16 | opts.transpileOnly = false; 17 | opts.happyPackMode = false; 18 | return opts; 19 | }); 20 | } 21 | }, 22 | parallel: false, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/vue2/README.md: -------------------------------------------------------------------------------- 1 | # @mr-scroll/vue2 2 | 3 | [![npm](https://img.shields.io/npm/v/@mr-scroll/react.svg)](https://www.npmjs.com/package/@mr-scroll/react) 4 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | 6 | The best custom scroll for the web. 7 | 8 | This is the vue 2 wrapper. [Check here](../../README.md) (root of this repo) for an overview on mr-scroll. 9 | 10 | ## Install 11 | 12 | ``` 13 | npm i @mr-scroll/core @mr-scroll/vue2 14 | ``` 15 | 16 | Note: If you're using [css-theming](https://github.com/mrahhal/css-theming), check the [css-theming support package](../css-theming). 17 | 18 | ## Usage 19 | 20 | Register as a global component: 21 | 22 | ```js 23 | import { createApp } from 'vue'; 24 | import Scroll from '@mr-scroll/vue2'; 25 | 26 | const app = createApp(..options); 27 | app.use(Scroll); 28 | ``` 29 | 30 | Register as a local component: 31 | 32 | ```js 33 | import Scroll from "@mr-scroll/vue2"; 34 | 35 | export default { 36 | name: "MyComponent", 37 | components: { 38 | "mr-scroll": Scroll, 39 | }, 40 | }; 41 | ``` 42 | 43 | Import the global CSS styles (for example in your App.vue): 44 | 45 | ```vue 46 | 47 | ``` 48 | 49 | Use `mr-scroll` component: 50 | 51 | ```html 52 | Content 53 | ``` 54 | 55 | > For more general usage info check the [README](../../README.md) in the root of this repo. 56 | -------------------------------------------------------------------------------- /packages/vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mr-scroll/vue2", 3 | "description": "The best custom scroll for the web. This is the vue 2 wrapper.", 4 | "version": "1.2.0", 5 | "main": "dist/mr-scroll.umd.js", 6 | "module": "dist/mr-scroll.common.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "lint": "vue-cli-service lint", 10 | "build": "vue-cli-service build --target lib --name mr-scroll src/index.ts", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "keywords": [ 14 | "custom", 15 | "scroll", 16 | "scrollbar", 17 | "vue", 18 | "component" 19 | ], 20 | "author": "Mohammad Rahhal", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/mrahhal/mr-scroll" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/mrahhal/mr-scroll/issues" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "files": [ 33 | "dist" 34 | ], 35 | "peerDependencies": { 36 | "@mr-scroll/core": "^1.0.0", 37 | "vue": ">=2.0.0 <3.0.0", 38 | "vue-class-component": "*", 39 | "vue-property-decorator": "*" 40 | }, 41 | "devDependencies": { 42 | "@mr-scroll/core": "file:../core", 43 | "@vue/cli-plugin-typescript": "~4.5.0", 44 | "@vue/cli-service": "~4.5.0", 45 | "typescript": "~4.1.5", 46 | "vue": "^2.0.0", 47 | "vue-class-component": "^7.0.0", 48 | "vue-property-decorator": "^9.0.0", 49 | "vue-template-compiler": "^2.6.11" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/vue2/src/index.ts: -------------------------------------------------------------------------------- 1 | import ScrollComponent from "./scroll.vue"; 2 | 3 | (ScrollComponent as any).install = (app: any) => { 4 | app.component("mr-scroll", ScrollComponent); 5 | }; 6 | 7 | export default ScrollComponent; 8 | -------------------------------------------------------------------------------- /packages/vue2/src/scroll.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 68 | -------------------------------------------------------------------------------- /packages/vue2/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /packages/vue2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2015", 5 | "module": "es2020", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "outDir": "dist", 9 | "importHelpers": true, 10 | "strict": true, 11 | "experimentalDecorators": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.vue"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue2/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: (config) => { 3 | // These are some necessary steps changing the default webpack config of the Vue CLI 4 | // that need to be changed in order for Typescript based components to generate their 5 | // declaration (.d.ts) files. 6 | // 7 | // Discussed here https://github.com/vuejs/vue-cli/issues/1081 8 | if (process.env.NODE_ENV === "production") { 9 | config.module.rule("ts").uses.delete("cache-loader"); 10 | 11 | config.module 12 | .rule("ts") 13 | .use("ts-loader") 14 | .loader("ts-loader") 15 | .tap((opts) => { 16 | opts.transpileOnly = false; 17 | opts.happyPackMode = false; 18 | return opts; 19 | }); 20 | } 21 | }, 22 | parallel: false, 23 | }; 24 | -------------------------------------------------------------------------------- /samples/angular/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /samples/angular/README.md: -------------------------------------------------------------------------------- 1 | # Angular 2 | 3 | Sample project using [@mr-scroll/angular](../../packages/angular). 4 | 5 | This sample also uses [@mr-scroll/css-theming](../../packages/css-theming). 6 | -------------------------------------------------------------------------------- /samples/angular/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/angular", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "node_modules/@mr-scroll/core/dist/styles.css", 34 | "src/styles.scss" 35 | ], 36 | "scripts": [], 37 | "preserveSymlinks": true, 38 | "vendorChunk": true, 39 | "extractLicenses": false, 40 | "buildOptimizer": false, 41 | "sourceMap": true, 42 | "optimization": false, 43 | "namedChunks": true 44 | }, 45 | "configurations": { 46 | "production": { 47 | "fileReplacements": [ 48 | { 49 | "replace": "src/environments/environment.ts", 50 | "with": "src/environments/environment.prod.ts" 51 | } 52 | ], 53 | "optimization": true, 54 | "outputHashing": "all", 55 | "sourceMap": false, 56 | "namedChunks": false, 57 | "extractLicenses": true, 58 | "vendorChunk": false, 59 | "buildOptimizer": true, 60 | "budgets": [ 61 | { 62 | "type": "initial", 63 | "maximumWarning": "500kb", 64 | "maximumError": "1mb" 65 | }, 66 | { 67 | "type": "anyComponentStyle", 68 | "maximumWarning": "2kb", 69 | "maximumError": "4kb" 70 | } 71 | ] 72 | } 73 | } 74 | }, 75 | "serve": { 76 | "builder": "@angular-devkit/build-angular:dev-server", 77 | "options": { 78 | "browserTarget": "angular:build" 79 | }, 80 | "configurations": { 81 | "production": { 82 | "browserTarget": "angular:build:production" 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "defaultProject": "angular" 90 | } 91 | -------------------------------------------------------------------------------- /samples/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "lint": "ng lint" 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "@angular/animations": "~13.1.0", 13 | "@angular/common": "~13.1.0", 14 | "@angular/compiler": "~13.1.0", 15 | "@angular/core": "~13.1.0", 16 | "@angular/forms": "~13.1.0", 17 | "@angular/platform-browser": "~13.1.0", 18 | "@angular/platform-browser-dynamic": "~13.1.0", 19 | "@angular/router": "~13.1.0", 20 | "@mr-scroll/angular": "file:../../packages/angular/dist", 21 | "@mr-scroll/core": "file:../../packages/core", 22 | "@mr-scroll/css-theming": "file:../../packages/css-theming", 23 | "css-theming": "^1.5.0", 24 | "lodash": "^4.17.21", 25 | "rxjs": "~6.6.0", 26 | "tslib": "^1.10.0", 27 | "zone.js": "~0.11.4" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~13.1.1", 31 | "@angular/cli": "~13.1.1", 32 | "@angular/compiler-cli": "~13.1.0", 33 | "@types/lodash": "^4.14.169", 34 | "@types/node": "^12.11.1", 35 | "ts-node": "~8.3.0", 36 | "typescript": "~4.5.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /samples/angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
General
4 |
Full page
5 |
Styling
6 |
7 |
8 |
Light
9 |
Dark
10 |
11 |
12 |
13 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /samples/angular/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import '@mr-scroll/core/src/scss/pure'; 2 | 3 | .app-root { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | width: 100%; 8 | height: 100%; 9 | @include msc-flex-adaptive-container(); 10 | } 11 | 12 | .layout-nav { 13 | display: flex; 14 | gap: 10px; 15 | border-bottom: 1px solid var(--bd); 16 | justify-content: space-between; 17 | } 18 | 19 | .layout-nav-pages { 20 | flex-shrink: 0; 21 | display: flex; 22 | flex-flow: row wrap; 23 | gap: 10px; 24 | 25 | > * { 26 | cursor: pointer; 27 | padding: 4px 8px; 28 | opacity: .5; 29 | 30 | &:hover { 31 | opacity: .8; 32 | } 33 | 34 | &.active { 35 | pointer-events: none; 36 | opacity: 1; 37 | } 38 | } 39 | } 40 | 41 | .layout-nav-theme-selector { 42 | display: flex; 43 | 44 | > * { 45 | padding: 3px 6px; 46 | cursor: pointer; 47 | opacity: .5; 48 | 49 | &:hover { 50 | opacity: .8; 51 | } 52 | 53 | &.active { 54 | pointer-events: none; 55 | opacity: 1; 56 | } 57 | } 58 | } 59 | 60 | .layout-page { 61 | flex: 1; 62 | position: relative; 63 | @include msc-flex-adaptive-container(); 64 | } 65 | -------------------------------------------------------------------------------- /samples/angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, DoCheck, ViewEncapsulation } from '@angular/core'; 2 | import { getCurrentTheme, getTheme, initializeTheming, setTheme } from 'css-theming'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'], 8 | encapsulation: ViewEncapsulation.None, 9 | host: { 10 | 'class': 'app-root', 11 | }, 12 | }) 13 | export class AppComponent implements DoCheck { 14 | page: 'general' | 'full-page' | 'styling' = 'general'; 15 | theme: string; 16 | 17 | constructor() { 18 | // css-theming stuff 19 | initializeTheming(); 20 | this.theme = getCurrentTheme().name; 21 | 22 | document.addEventListener('keypress', e => { 23 | if (e.defaultPrevented) return; 24 | 25 | if (e.key === 't') { 26 | const previousTheme = getCurrentTheme(); 27 | const newTheme = previousTheme.name == 'default' ? 'default-dark' : 'default'; 28 | this._setTheme(newTheme); 29 | } 30 | }); 31 | //------------------ 32 | } 33 | 34 | ngDoCheck() { 35 | // To test when change detection is being triggered 36 | console.log('ngDoCheck'); 37 | } 38 | 39 | setPage(page: 'general' | 'full-page' | 'styling') { 40 | this.page = page; 41 | } 42 | 43 | _setTheme(theme: string) { 44 | setTheme(getTheme(theme)); 45 | this.theme = theme; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/angular/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ScrollModule } from '@mr-scroll/angular'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { FULL_PAGE_DECLARATIONS } from './full-page'; 7 | import { GENERAL_DECLARATIONS } from './general'; 8 | import { STYLING_DECLARATIONS } from './styling'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent, 13 | GENERAL_DECLARATIONS, 14 | FULL_PAGE_DECLARATIONS, 15 | STYLING_DECLARATIONS, 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | ScrollModule, 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent], 23 | }) 24 | export class AppModule { } 25 | -------------------------------------------------------------------------------- /samples/angular/src/app/full-page/full-page.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | @Component({ 5 | selector: 'full-page', 6 | templateUrl: './full-page.html', 7 | styleUrls: ['./full-page.scss'], 8 | encapsulation: ViewEncapsulation.None, 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | host: { 11 | 'class': 'full-page', 12 | }, 13 | }) 14 | export class FullPageComponent { 15 | items = _.range(1, 31); 16 | } 17 | -------------------------------------------------------------------------------- /samples/angular/src/app/full-page/full-page.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Sidebar

4 |
Item {{item}}
5 |
6 |
7 | 8 |
9 | 10 |

Content {{item}}

11 |
12 |
13 | -------------------------------------------------------------------------------- /samples/angular/src/app/full-page/full-page.scss: -------------------------------------------------------------------------------- 1 | @import '@mr-scroll/core/src/scss/pure'; 2 | @import '@mr-scroll/css-theming/src/scss/css-theming'; 3 | 4 | .full-page { 5 | position: absolute; 6 | left: 0; 7 | top: 0; 8 | width: 100%; 9 | height: 100%; 10 | @include msc-flex-adaptive-container(); 11 | flex-flow: row; 12 | } 13 | 14 | .full-page-sidebar { 15 | @include msc-flex-adaptive-container($flex: false); 16 | width: 150px; 17 | $bg: #5700ff; 18 | background: $bg; 19 | color: white; 20 | @include msc-pad-content(10px); 21 | @include msc-hidden-content-fade($bg); 22 | } 23 | 24 | .full-page-sidebar-item { 25 | padding: 8px 4px; 26 | } 27 | 28 | .full-page-content { 29 | @include msc-flex-adaptive-container(); 30 | @include msc-pad-content(10px); 31 | } 32 | -------------------------------------------------------------------------------- /samples/angular/src/app/full-page/index.ts: -------------------------------------------------------------------------------- 1 | import { FullPageComponent } from './full-page.component'; 2 | 3 | export const FULL_PAGE_DECLARATIONS: any[] = [ 4 | FullPageComponent, 5 | ]; 6 | -------------------------------------------------------------------------------- /samples/angular/src/app/general/general.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | @Component({ 5 | selector: 'general', 6 | templateUrl: './general.html', 7 | styleUrls: ['./general.scss'], 8 | encapsulation: ViewEncapsulation.None, 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | host: { 11 | 'class': 'general', 12 | }, 13 | }) 14 | export class GeneralComponent { 15 | items = _.range(1, 11); 16 | items3 = _.range(1, 4); 17 | items24 = _.range(1, 25); 18 | 19 | _onTopReached() { 20 | console.log('_onTopReached'); 21 | } 22 | 23 | _onLeftReached() { 24 | console.log('_onLeftReached'); 25 | } 26 | 27 | _onRightReached() { 28 | console.log('_onRightReached'); 29 | } 30 | 31 | _onScrolled() { 32 | // This event doesn't trigger change detection on purpose for performance reasons. 33 | // To trigger change detection you should call `NgZone.run()`. 34 | 35 | console.log('_onScrolled'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/angular/src/app/general/general.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Auto (default)
4 |
5 | 6 | 7 | 8 |
9 |
10 | 11 |
12 |
Overlay
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 |
Hidden
22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 |
30 |
Show on hover
31 |
32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 |
Hidden content fade
40 |
41 | 42 | 43 | 44 |
45 |
46 | This shows a gradient over the top/bottom side when there's scroll and content is hidden. 47 |
48 |
49 | 50 |
51 |
Max height (overflows)
52 |
53 | 54 | 55 | 56 |
57 |
58 | 59 |
60 |
Max height
61 |
62 | 63 |
64 |
65 | Item #{{item}} 66 |
67 |
68 |
69 |
70 |
71 | 72 |
73 |
Horizontal
74 |
75 | 76 |
77 |
78 | Item #{{item}} 79 |
80 |
81 |
82 |
83 |
84 | 85 |
86 |
Both
87 |
88 | 89 |
90 |
91 | Item #{{item}} 92 |
93 |
94 |
95 |
96 |
97 |
98 | 99 | 100 |
101 |
102 | Item #{{item}} 103 |
104 |
105 |
106 | -------------------------------------------------------------------------------- /samples/angular/src/app/general/general.scss: -------------------------------------------------------------------------------- 1 | @import '@mr-scroll/core/src/scss/pure'; 2 | @import '@mr-scroll/css-theming/src/scss/css-theming'; 3 | 4 | .general { 5 | @include msc-flex-adaptive-container(); 6 | overflow-y: auto; 7 | padding: 10px; 8 | } 9 | 10 | .general-examples { 11 | display: grid; 12 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 13 | gap: 30px; 14 | } 15 | 16 | .general-example { 17 | display: flex; 18 | flex-flow: column; 19 | } 20 | 21 | .general-example-title { 22 | padding: 6px 2px; 23 | font-size: 12px; 24 | font-weight: bold; 25 | text-transform: uppercase; 26 | } 27 | 28 | .general-example-content { 29 | border: 1.5px solid var(--bd); 30 | border-radius: 3px; 31 | background: var(--fg-0); 32 | @include msc-height(200px); 33 | 34 | &--hidden-content-fade { 35 | // We're using css-theming so we can simply give this the variable for fg-0. 36 | // Of course, you can set the colors to whatever you like. 37 | @include msc-hidden-content-fade(var(--fg-0-rgb)); 38 | } 39 | 40 | &--max-height { 41 | > .mr-scroll { 42 | height: unset; 43 | } 44 | 45 | @include msc-max-height(200px); 46 | } 47 | 48 | &--h { 49 | > .mr-scroll { 50 | height: initial; 51 | } 52 | } 53 | } 54 | 55 | .general-content-hv { 56 | display: grid; 57 | grid-template-columns: 1fr 1fr 1fr 1fr; 58 | white-space: nowrap; 59 | margin: -1px; 60 | 61 | > * { 62 | padding: 10px; 63 | border: 1px dashed var(--bd); 64 | } 65 | } 66 | 67 | .general-example-note { 68 | margin-top: 10px; 69 | padding: 4px 8px; 70 | background: var(--fg-0); 71 | border-left: 2px solid var(--info); 72 | border-radius: 2px; 73 | } 74 | 75 | .general-content-item { 76 | padding: 10px; 77 | 78 | &:hover { 79 | background: var(--bg); 80 | } 81 | } 82 | 83 | .general-content-item + .general-content-item { 84 | border-top: 1px solid var(--bd); 85 | } 86 | 87 | .general-content-h { 88 | display: flex; 89 | flex-flow: row nowrap; 90 | } 91 | 92 | .general-content-h-item { 93 | padding: 10px; 94 | white-space: nowrap; 95 | } 96 | 97 | .general-content-h-item + .general-content-h-item { 98 | border-left: 1px solid var(--bd); 99 | } 100 | -------------------------------------------------------------------------------- /samples/angular/src/app/general/index.ts: -------------------------------------------------------------------------------- 1 | import { GeneralComponent } from './general.component'; 2 | 3 | export const GENERAL_DECLARATIONS: any[] = [ 4 | GeneralComponent, 5 | ]; 6 | -------------------------------------------------------------------------------- /samples/angular/src/app/styling/index.ts: -------------------------------------------------------------------------------- 1 | import { StylingComponent } from './styling.component'; 2 | 3 | export const STYLING_DECLARATIONS: any[] = [ 4 | StylingComponent, 5 | ]; 6 | -------------------------------------------------------------------------------- /samples/angular/src/app/styling/styling.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | @Component({ 5 | selector: 'styling', 6 | templateUrl: './styling.html', 7 | styleUrls: ['./styling.scss'], 8 | encapsulation: ViewEncapsulation.None, 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | host: { 11 | 'class': 'styling', 12 | }, 13 | }) 14 | export class StylingComponent { 15 | items = _.range(1, 11); 16 | } 17 | -------------------------------------------------------------------------------- /samples/angular/src/app/styling/styling.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | Item #{{item}} 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /samples/angular/src/app/styling/styling.scss: -------------------------------------------------------------------------------- 1 | @import '@mr-scroll/core/src/scss/pure'; 2 | @import '@mr-scroll/css-theming/src/scss/css-theming'; 3 | 4 | .styling { 5 | @include msc-flex-adaptive-container(); 6 | overflow-y: auto; 7 | padding: 10px; 8 | } 9 | 10 | .styling-examples { 11 | display: grid; 12 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 13 | gap: 30px; 14 | } 15 | 16 | .styling-example { 17 | border: 1.5px solid var(--bd); 18 | border-radius: 3px; 19 | background: var(--fg-0); 20 | @include msc-height(200px); 21 | 22 | &-1 { 23 | --mr-scroll-thumb-color: var(--primary); 24 | --mr-scroll-border-radius: 10px; 25 | } 26 | 27 | &-2 { 28 | --mr-scroll-thumb-color: var(--primary); 29 | } 30 | 31 | &-3 { 32 | --mr-scroll-thumb-color: var(--green); 33 | --mr-scroll-track-color: var(--green-50); 34 | } 35 | } 36 | 37 | .styling-content-item { 38 | padding: 10px; 39 | 40 | &:hover { 41 | background: var(--bg); 42 | } 43 | } 44 | 45 | .styling-content-item + .styling-content-item { 46 | border-top: 1px solid var(--bd); 47 | } 48 | -------------------------------------------------------------------------------- /samples/angular/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/angular/src/assets/.gitkeep -------------------------------------------------------------------------------- /samples/angular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /samples/angular/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /samples/angular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/angular/src/favicon.ico -------------------------------------------------------------------------------- /samples/angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/angular/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /samples/angular/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /samples/angular/src/styles.scss: -------------------------------------------------------------------------------- 1 | $ct-compute-rgb-variables: true; 2 | 3 | @import 'css-theming/src/scss/css-theming'; 4 | @import '@mr-scroll/css-theming/src/scss/css-theming'; 5 | 6 | // You can optionally provide values here 7 | @include msct-apply(); 8 | 9 | html { 10 | font-family: Camphor, Open Sans, Segoe UI, sans-serif; 11 | overflow-x: hidden; 12 | } 13 | 14 | html, 15 | body { 16 | margin: 0; 17 | } 18 | -------------------------------------------------------------------------------- /samples/angular/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /samples/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "es2015", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /samples/react/README.md: -------------------------------------------------------------------------------- 1 | # React 2 | 3 | Sample project using [@mr-scroll/react](../../packages/react). 4 | -------------------------------------------------------------------------------- /samples/react/config-overrides.js: -------------------------------------------------------------------------------- 1 | const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); 2 | 3 | module.exports = function override(config, env) { 4 | config.resolve.plugins = config.resolve.plugins.filter(plugin => !(plugin instanceof ModuleScopePlugin)); 5 | return config; 6 | }; 7 | -------------------------------------------------------------------------------- /samples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-app-rewired start", 7 | "build": "react-app-rewired build", 8 | "test": "react-app-rewired test", 9 | "eject": "react-app-rewired eject" 10 | }, 11 | "dependencies": { 12 | "@mr-scroll/core": "file:../../packages/core", 13 | "@mr-scroll/react": "file:../../packages/react", 14 | "@testing-library/jest-dom": "^5.12.0", 15 | "@testing-library/react": "^11.2.7", 16 | "@testing-library/user-event": "^12.8.3", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-scripts": "4.0.3" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "react-app-rewired": "^2.1.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/react/public/favicon.ico -------------------------------------------------------------------------------- /samples/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /samples/react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/react/public/logo192.png -------------------------------------------------------------------------------- /samples/react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/react/public/logo512.png -------------------------------------------------------------------------------- /samples/react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /samples/react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /samples/react/src/App.css: -------------------------------------------------------------------------------- 1 | @import '@mr-scroll/core/dist/styles.css'; 2 | 3 | .App { 4 | margin: 10px; 5 | display: grid; 6 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 7 | gap: 30px; 8 | } 9 | 10 | .general-example > .mr-scroll { 11 | height: 200px; 12 | border: 1px solid #aaa; 13 | border-radius: 2px; 14 | } 15 | 16 | .general-item { 17 | padding: 10px; 18 | } 19 | -------------------------------------------------------------------------------- /samples/react/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import Scroll from '@mr-scroll/react'; 3 | 4 | function App() { 5 | const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(item => 6 |
Item {item}
7 | ); 8 | 9 | function onScrolled(x) { 10 | console.log('onScrolled', x); 11 | } 12 | 13 | return ( 14 |
15 |
16 | 17 | {items} 18 | 19 |
20 | 21 |
22 | 23 | {items} 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /samples/react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /samples/react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /samples/vue/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /samples/vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue 2 | 3 | Sample project using [@mr-scroll/vue](../../packages/vue). 4 | -------------------------------------------------------------------------------- /samples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build" 9 | }, 10 | "dependencies": { 11 | "@mr-scroll/core": "file:../../packages/core", 12 | "@mr-scroll/vue": "file:../../packages/vue", 13 | "vue": "^3.0.0", 14 | "vue-class-component": "^8.0.0-0", 15 | "vue-property-decorator": "^10.0.0-0" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-typescript": "~4.5.0", 19 | "@vue/cli-service": "~4.5.0", 20 | "@vue/compiler-sfc": "^3.0.0", 21 | "typescript": "~4.1.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/vue/public/favicon.ico -------------------------------------------------------------------------------- /samples/vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 43 | 44 | 45 | 46 | 73 | -------------------------------------------------------------------------------- /samples/vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /samples/vue/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /samples/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.vue" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /samples/vue/vue.config.js: -------------------------------------------------------------------------------- 1 | // Referencing a local lib causes vue to be loaded twice. This is required to force it to load once. 2 | // https://github.com/vuejs/vue-cli/issues/4271 3 | 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | configureWebpack: { 8 | resolve: { 9 | alias: { 10 | vue$: path.resolve('./node_modules/vue/dist/vue.runtime.esm-bundler.js'), 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /samples/vue2/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /samples/vue2/README.md: -------------------------------------------------------------------------------- 1 | # Vue 2 2 | 3 | Sample project using [@mr-scroll/vue2](../../packages/vue2). 4 | -------------------------------------------------------------------------------- /samples/vue2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build" 9 | }, 10 | "dependencies": { 11 | "@mr-scroll/core": "file:../../packages/core", 12 | "@mr-scroll/vue2": "file:../../packages/vue2", 13 | "vue": "^2.0.0", 14 | "vue-class-component": "^7.0.0", 15 | "vue-property-decorator": "^9.0.0" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-typescript": "~4.5.0", 19 | "@vue/cli-service": "~4.5.0", 20 | "typescript": "~4.1.5", 21 | "vue-template-compiler": "^2.6.11" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/vue2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/mr-scroll/47bd950785480e89cca2a971e1f18f056fe98cfc/samples/vue2/public/favicon.ico -------------------------------------------------------------------------------- /samples/vue2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/vue2/src/App.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 43 | 44 | 45 | 46 | 73 | -------------------------------------------------------------------------------- /samples/vue2/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app'); 9 | -------------------------------------------------------------------------------- /samples/vue2/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /samples/vue2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.vue" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /samples/vue2/vue.config.js: -------------------------------------------------------------------------------- 1 | // Referencing a local lib causes vue to be loaded twice. This is required to force it to load once. 2 | // https://github.com/vuejs/vue-cli/issues/4271 3 | 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | configureWebpack: { 8 | resolve: { 9 | alias: { 10 | vue$: path.resolve('./node_modules/vue/dist/vue.runtime.esm.js'), 11 | }, 12 | }, 13 | }, 14 | }; 15 | --------------------------------------------------------------------------------