diff --git a/CHANGELOG.md b/CHANGELOG.md index bb59f333..e20b31d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 16.0.0-beta.2 + +- feat: `scrollViewport` directive can be a descent child, not necessarily a direct child of ``. +- feat: Add `start`, `end`, `right`, `bottom` and `center` options to `scrollToElement` (supports RTL), closes [#637](https://github.com/MurhafSousli/ngx-scrollbar/issues/637). +- feat: Add `syncSpacer` directive that is applied on `` component to sync spacer element with content dimension changes. +- fix: `ResizeObserver` loop completed with undelivered notifications, closes [#650](https://github.com/MurhafSousli/ngx-scrollbar/issues/650). +- enhance: Improve scrollbar rendering speed that uses `externalViewport` such as integrating with 3rd party library. +- enhance: Use the shared resize observer from the CDK to do the necessary calculation. +- enhance: `asyncDetection` Use the content observer service from the CDK to detect viewport and content wrapper elements. +- enhance: use `--scrollbar-thumb-hover-color` not only when hovered but also when active. +- enhance: `track` and `thumb` controls uses more accurate measures using `getBoundingClientRect` instead of direct size properties. +- refactor: Switch to `InputSignal` instead of standards inputs across components. +- refactor: Use effects instead of `ngOnInit` to initialize the scrollbar component. +- refactor: Remove `scrollViewport` directive from `ng-scrollbar` hostDirectives because it's redundant. +- refactor: the `TrackAdapter` now uses viewport & content dimension signals to update the track size instead of `ResizeObserver` events. + +### Breaking changes + +- Minimum compatibility is Angular v17.3.0 + ## 15.1.3 - enhance: Use injection factory for `NG_SCROLLBAR_OPTIONS` and `NG_SCROLLBAR_POLYFILL` to avoid merging custom options with the default options for every scrollbar component. diff --git a/package-lock.json b/package-lock.json index 9aad7dab..9cd0427e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,47 +8,47 @@ "name": "ngx-scrollbar-project", "version": "0.0.0", "dependencies": { - "@angular/animations": "^18.2.1", - "@angular/cdk": "^18.2.1", - "@angular/common": "^18.2.1", - "@angular/compiler": "^18.2.1", - "@angular/core": "^18.2.1", - "@angular/forms": "^18.2.1", - "@angular/material": "^18.2.1", - "@angular/platform-browser": "^18.2.1", - "@angular/platform-browser-dynamic": "^18.2.1", - "@angular/platform-server": "^18.2.1", - "@angular/router": "^18.2.1", - "@angular/ssr": "^18.2.1", + "@angular/animations": "^18.2.5", + "@angular/cdk": "^18.2.5", + "@angular/common": "^18.2.5", + "@angular/compiler": "^18.2.5", + "@angular/core": "^18.2.5", + "@angular/forms": "^18.2.5", + "@angular/material": "^18.2.5", + "@angular/platform-browser": "^18.2.5", + "@angular/platform-browser-dynamic": "^18.2.5", + "@angular/platform-server": "^18.2.5", + "@angular/router": "^18.2.5", + "@angular/ssr": "^18.2.5", "@swimlane/ngx-datatable": "^20.1.0", - "ag-grid-angular": "^32.1.0", - "ag-grid-community": "^32.1.0", + "ag-grid-angular": "^32.2.0", + "ag-grid-community": "^32.2.0", "chance": "^1.1.12", - "express": "^4.18.2", "ng-zorro-antd": "^18.1.1", "ngx-color-picker": "^17.0.0", "ngx-infinite-scroll": "^18.0.0", - "primeng": "^17.18.9", + "primeng": "^17.18.10", "rxjs": "~7.8.0", "tslib": "^2.7.0", "zone.js": "^0.14.10" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.2.1", - "@angular-eslint/builder": "18.3.0", - "@angular-eslint/eslint-plugin": "18.3.0", - "@angular-eslint/eslint-plugin-template": "18.3.0", - "@angular-eslint/schematics": "18.3.0", - "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "^18.2.1", - "@angular/compiler-cli": "^18.2.1", + "@angular-devkit/build-angular": "^18.2.5", + "@angular-eslint/builder": "18.3.1", + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", + "@angular-eslint/schematics": "18.3.1", + "@angular-eslint/template-parser": "18.3.1", + "@angular/cli": "^18.2.5", + "@angular/compiler-cli": "^18.2.5", "@types/express": "^4.17.17", "@types/jasmine": "~5.1.0", - "@types/node": "^22.5.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "^9.8.0", - "jasmine-core": "^5.2.0", + "@types/node": "^22.6.1", + "@typescript-eslint/eslint-plugin": "^8.7.0", + "@typescript-eslint/parser": "^8.7.0", + "eslint": "^9.11.1", + "express": "^4.21.0", + "jasmine-core": "^5.3.0", "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", @@ -81,12 +81,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.1.tgz", - "integrity": "sha512-XTnJfCBMDQl3xF4w/eNrq821gbj2Ig1cqbzpRflhz4pqrANTAfHfPoIC7piWEZ60FNlHapzb6fvh6tJUGXG9og==", + "version": "0.1802.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.5.tgz", + "integrity": "sha512-c7sVoW85Yqj7IYvNKxtNSGS5I7gWpORorg/xxLZX3OkHWXDrwYbb5LN/2p5/Aytxyb0aXl4o5fFOu6CUwcaLUw==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.5", "rxjs": "7.8.1" }, "engines": { @@ -96,16 +96,16 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.1.tgz", - "integrity": "sha512-ANsTWKjIlEvJ6s276TbwnDhkoHhQDfsNiRFUDRGBZu94UNR78ImQZSyKYGHJOeQQH6jpBtraA1rvW5WKozAtlw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.5.tgz", + "integrity": "sha512-dIvb0AHoRIMM6tLuG4t6lDDslSAYP77wqytodsN317UzFOuuCPernXbO8NJs+QHxj09nPsem1T5vnvpO2E/PVQ==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/build-webpack": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular/build": "18.2.1", + "@angular-devkit/architect": "0.1802.5", + "@angular-devkit/build-webpack": "0.1802.5", + "@angular-devkit/core": "18.2.5", + "@angular/build": "18.2.5", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -116,7 +116,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.1", + "@ngtools/webpack": "18.2.5", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -156,10 +156,10 @@ "terser": "5.31.6", "tree-kill": "1.2.2", "tslib": "2.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1", - "webpack": "5.93.0", - "webpack-dev-middleware": "7.3.0", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" @@ -223,31 +223,20 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.5.tgz", + "integrity": "sha512-L0n4eHObeqEOYRfSP+e4SeF/dmwxOIFy9xYvYCOUwOLrW4b3+a1+kkT30pqyfL72LFtpf0cmUwaWEFIcWl5PCg==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", + "webpack": "^5.54.0" } }, "node_modules/@angular-devkit/build-angular/node_modules/istanbul-lib-instrument": { @@ -266,24 +255,6 @@ "node": ">=10" } }, - "node_modules/@angular-devkit/build-angular/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -291,12 +262,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.1.tgz", - "integrity": "sha512-xOP9Hxkj/mWYdMTa/8uNxFTv7z+3UiGdt4VAO7vetV5qkU/S9rRq8FEKviCc2llXfwkhInSgeeHpWKdATa+YIQ==", + "version": "0.1802.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.5.tgz", + "integrity": "sha512-6qkcrWBdkxojCVHGWcdJaz4G+7QTjFvmc+3g8xvLc9sYvJq1I059gfXhDnC0FxiA0MT4cY/26ECYWUHTD5CJLQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/architect": "0.1802.5", "rxjs": "7.8.1" }, "engines": { @@ -310,9 +281,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.1.tgz", - "integrity": "sha512-fSuGj6CxiTFR+yjuVcaWqaVb5Wts39CSBYRO1BlsOlbuWFZ2NKC/BAb5bdxpB31heCBJi7e3XbPvcMMJIcnKlA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.5.tgz", + "integrity": "sha512-r9TumPlJ8PvA2+yz4sp+bUHgtznaVKzhvXTN5qL1k4YP8LJ7iZWMR2FOP+HjukHZOTsenzmV9pszbogabqwoZQ==", "dev": true, "dependencies": { "ajv": "8.17.1", @@ -354,12 +325,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.1.tgz", - "integrity": "sha512-2t/q0Jcv7yqhAzEdNgsxoGSCmPgD4qfnVOJ7EJw3LNIA+kX1CmtN4FESUS0i49kN4AyNJFAI5O2pV8iJiliKaw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.5.tgz", + "integrity": "sha512-NUmz2UQ1Xl4cf4j1AgkwIfsCjBzAPgfeC3IBrD29hSOBE1Y3j6auqjBkvw50v6mbSPxESND995Xy13HpK1Xflw==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.1", + "@angular-devkit/core": "18.2.5", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -372,9 +343,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.0.tgz", - "integrity": "sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.3.1.tgz", + "integrity": "sha512-cPc7Ye9zDs5M4i+feL6vob+mh7yX5vxvOS5KQIhneUrp5e9D+IGuNFMmBLlOPpmklSc9XJBtuvI5Zjuh4z1ETw==", "dev": true, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", @@ -382,19 +353,19 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.0.tgz", - "integrity": "sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.1.tgz", + "integrity": "sha512-sikmkjfsXPpPTku1aQkQ1MNNEKGBgGGRvUN/WeNS9dhCJ4dxU3O7dZctt1aQWj+W3nbuUtDiimAWF5fZHGFE2Q==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.0.tgz", - "integrity": "sha512-Vl7gfPMXxvtHTjYdlzR161aj5xrqW6T57wd8ToQ7Gqzm0qHGfY6kE4SQobUa2LCYckTNSlv+zXe48C4ah/dSjw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.3.1.tgz", + "integrity": "sha512-MP4Nm+SHboF8KdnN0KpPEGAaTTzDLPm3+S/4W3Mg8onqWCyadyd4mActh9mK/pvCj8TVlb/SW1zeTtdMYhwonw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", - "@angular-eslint/utils": "18.3.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -403,13 +374,13 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.0.tgz", - "integrity": "sha512-ddR/qwYbUeq9IpyVKrPbfZyRBTy6V8uc5I0JcBKttQ4CZ4joXhqsVgWFsI+JAMi8E66uNj1VC7NuKCOjDINv2Q==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.3.1.tgz", + "integrity": "sha512-hBJ3+f7VSidvrtYaXH7Vp0sWvblA9jLK2c6uQzhYGWdEDUcTg7g7VI9ThW39WvMbHqkyzNE4PPOynK69cBEDGg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", - "@angular-eslint/utils": "18.3.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", + "@angular-eslint/utils": "18.3.1", "aria-query": "5.3.0", "axobject-query": "4.1.0" }, @@ -420,13 +391,13 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.3.0.tgz", - "integrity": "sha512-rQ4DEWwf3f5n096GAK6JvXD0SRzRJ52WRaIyKg8MMkk6qvUDfZI8seOkcbjDtZoIe6Ds7DfqSfJgNVte75qvPQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.3.1.tgz", + "integrity": "sha512-BTsQHDu7LjvXannJTb5BqMPCFIHRNN94eRyb60VfjJxB/ZFtsbAQDFFOi5lEZsRsd4mBeUMuL9mW4IMcPtUQ9Q==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "18.3.0", - "@angular-eslint/eslint-plugin-template": "18.3.0", + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", "ignore": "5.3.2", "semver": "7.6.3", "strip-json-comments": "3.1.1" @@ -437,12 +408,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.0.tgz", - "integrity": "sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.3.1.tgz", + "integrity": "sha512-JUUkfWH1G+u/Uk85ZYvJSt/qwN/Ko+jlXFtzBEcknJZsTWTwBcp36v77gPZe5FmKSziJZpyPUd+7Kiy6tuSCTw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0", + "@angular-eslint/bundled-angular-compiler": "18.3.1", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -476,12 +447,12 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.0.tgz", - "integrity": "sha512-sCrkHkpxBJZLuCikdboZoawCfc2UgbJv+T14tu2uQCv+Vwzeadnu04vkeY2vTkA8GeBdBij/G9/N/nvwmwVw3g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.3.1.tgz", + "integrity": "sha512-sd9niZI7h9H2FQ7OLiQsLFBhjhRQTASh+Q0+4+hyjv9idbSHBJli8Gsi2fqj9zhtMKpAZFTrWzuLUpubJ9UYbA==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "18.3.0" + "@angular-eslint/bundled-angular-compiler": "18.3.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -490,9 +461,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.1.tgz", - "integrity": "sha512-jit452yuE6DMVV09E6RAjgapgw64mMVH31ccpPvMDekzPsTuP3KNKtgRFU/k2DFhYJvyczM1AqqlgccE/JGaRw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.5.tgz", + "integrity": "sha512-IlXtW/Nj48ZzjHUzH1TykZcSR64ScJx39T3IHnjV2z/bVATzZ36JGoadQHdqpJNKBodYJNgtJCGLCbgAvGWY2g==", "dependencies": { "tslib": "^2.3.0" }, @@ -500,17 +471,17 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.1" + "@angular/core": "18.2.5" } }, "node_modules/@angular/build": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.1.tgz", - "integrity": "sha512-HwzjB+I31cAtjTTbbS2NbayzfcWthaKaofJlSmZIst3PN+GwLZ8DU0DRpd/xu5AXkk+DoAIWd+lzUIaqngz6ow==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.5.tgz", + "integrity": "sha512-XWkmjzgeUga0SJ0lYSYcTuYOWTyqcln2mNfBp7Ae/GZ+/7+APbedsIZEiZGZwveOIyOpTM5wguNSoe9khDl5Ig==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", + "@angular-devkit/architect": "0.1802.5", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -532,7 +503,7 @@ "rollup": "4.20.0", "sass": "1.77.6", "semver": "7.6.3", - "vite": "5.4.0", + "vite": "5.4.6", "watchpack": "2.4.1" }, "engines": { @@ -572,9 +543,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.1.tgz", - "integrity": "sha512-6y4MmpEPXze6igUHkLsBUPkxw32F8+rmW0xVXZchkSyGlFgqfh53ueXoryWb0qL4s5enkNY6AzXnKAqHfPNkVQ==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.5.tgz", + "integrity": "sha512-HLg5cfrIrgNIJJ+0v3kLieHeLPJLFNOBO359holXOrKUPRG+XQ3CT8EzSvREFm1XkaSEsDC0+dnG0ouNhOPFpQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -588,17 +559,17 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz", - "integrity": "sha512-SomUFDHanY4o7k3XBGf1eFt4z1h05IGJHfcbl2vxoc0lY59VN13m/pZsD2AtpqtJTzLQT02XQOUP4rmBbGoQ+Q==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.5.tgz", + "integrity": "sha512-97uNs0HsOdnMaTlNJKFjIBUXw0wz43uYvSSKmIpBt7eq1LaPLju1G/qpDIHx2YwhMClPrXXrW2H/xdvqZiIw+w==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", + "@angular-devkit/architect": "0.1802.5", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.1", + "@schematics/angular": "18.2.5", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -621,9 +592,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.1.tgz", - "integrity": "sha512-N0ZJO1/iU9UhprplZRPvBcdRgA/i6l6Ng5gXs5ymHBJ0lxsB+mDVCmC4jISjR9gAWc426xXwLaOpuP5Gv3f/yg==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.5.tgz", + "integrity": "sha512-m+KJrtbFXTE36jP/po6UAMeUR/enQxRHpVGLCRcIcE7VWVH1ZcOvoW1yqh2A6k+KxWXeajlq/Z04nnMhcoxMRw==", "dependencies": { "tslib": "^2.3.0" }, @@ -631,14 +602,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.1", + "@angular/core": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.1.tgz", - "integrity": "sha512-5e9ygKEcsBoV6xpaGKVrtsLxLETlrM0oB7twl4qG/xuKYqCLj8cRQMcAKSqDfTPzWMOAQc7pHdk+uFVo/8dWHA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.5.tgz", + "integrity": "sha512-vcqe9x4dGGAnMfPhEpcZyiSVgAiqJeK80LqP1vWoAmBR+HeOqAilSv6SflcLAtuTzwgzMMAvD2T+SMCgUvaqww==", "dependencies": { "tslib": "^2.3.0" }, @@ -646,7 +617,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.1" + "@angular/core": "18.2.5" }, "peerDependenciesMeta": { "@angular/core": { @@ -655,9 +626,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.1.tgz", - "integrity": "sha512-D+Qba0r6RfHfffzrebGYp54h05AxpkagLjit/GczKNgWSP1gIgZxSfi88D+GvFmeWvZxWN1ecAQ+yqft9hJqWg==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.5.tgz", + "integrity": "sha512-CCCtZobUTUfId/RTYtuDCw5R1oK0w65hdAUMRP1MdGmd8bb8DKJA86u1QCWwozL3rbXlIIX4ognQ6urQ43k/Gw==", "dev": true, "dependencies": { "@babel/core": "7.25.2", @@ -678,14 +649,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.1", + "@angular/compiler": "18.2.5", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.1.tgz", - "integrity": "sha512-9KrSpJ65UlJZNXrE18NszcfOwb5LZgG+LYi5Doe7amt218R1bzb3trvuAm0ZzMaoKh4ugtUCkzEOd4FALPEX6w==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.5.tgz", + "integrity": "sha512-5BLVc5gXxzanQkADNS9WPsor3vNF5nQcyIHBi5VScErwM5vVZ7ATH1iZwaOg1ykDEVTFVhKDwD0X1aaqGDbhmQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -698,9 +669,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.1.tgz", - "integrity": "sha512-T7z8KUuj2PoPxrMrAruQVJha+x4a9Y6IrKYtArgOQQlTwCEJuqpVYuOk5l3fwWpHE9bVEjvgkAMI1D5YXA/U6w==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.5.tgz", + "integrity": "sha512-ohKeH+EZCCIyGSiFYlraWLzssGAZc13P92cuYpXB62322PkcA5u0IT72mML9JWGKRqF2zteVsw4koWHVxXM5mA==", "dependencies": { "tslib": "^2.3.0" }, @@ -708,22 +679,22 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.1.tgz", - "integrity": "sha512-DBSJGqLttT9vYpLGWTuuRoOKd1mNelS0jnNo7jNZyMpjcGfuhNzmPtYiBkXfNsAl7YoXoUmX8+4uh1JZspQGqA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.5.tgz", + "integrity": "sha512-+Yz8ayKz1ALz2UvPrM33FHSUmrE0GKHn+Gg79l6NdC4eSrzAAYBVdLfQvCBWCgtdvs7IiegbCnnAJiqXVC1DDg==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.1", + "@angular/cdk": "18.2.5", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -732,9 +703,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.1.tgz", - "integrity": "sha512-hQABX7QotGmCIR3EhCBCDh5ZTvQao+JkuK5CCw2G1PkRfJMBwEpjNqnyhz41hZhWiGlucp9jgbeypppW+mIQEw==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.5.tgz", + "integrity": "sha512-PoX9idwnOpTJBlujzZ2nFGOsmCnZzOH7uNSWIR7trdoq0b1AFXfrxlCQ36qWamk7bbhJI4H28L8YTmKew/nXDA==", "dependencies": { "tslib": "^2.3.0" }, @@ -742,9 +713,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.1", - "@angular/common": "18.2.1", - "@angular/core": "18.2.1" + "@angular/animations": "18.2.5", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -753,9 +724,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.1.tgz", - "integrity": "sha512-tYJHtshbaKrtnRA15k3vrveSVBqkVUGhINvGugFA2vMtdTOfhfPw+hhzYrcwJibgU49rHogCfI9mkIbpNRYntA==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.5.tgz", + "integrity": "sha512-5u0IuAt1r5e2u2vSKhp3phnaf6hH89B/q7GErfPse1sdDfNI6wHVppxai28PAfAj9gwooJun6MjFWhJFLzS44A==", "dependencies": { "tslib": "^2.3.0" }, @@ -763,16 +734,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/compiler": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1" + "@angular/common": "18.2.5", + "@angular/compiler": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5" } }, "node_modules/@angular/platform-server": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-18.2.1.tgz", - "integrity": "sha512-xU/7EGYk/HXAY2V7VEzBx4YcVQe3rPuojXPubdgKJ8ueQ7XVtwumv/LHM72/Yn8ChvYYaoGLtM7nI2rG1MVAag==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-18.2.5.tgz", + "integrity": "sha512-or7FA4IfNOcxPLgoA4XISuOTFM00ScKX9+34EZL3TBPBoAirt1xb/6QPJjiH30BPx7lqhE4F2pd9gEJu0sKaPA==", "dependencies": { "tslib": "^2.3.0", "xhr2": "^0.2.0" @@ -781,17 +752,17 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.1", - "@angular/common": "18.2.1", - "@angular/compiler": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1" + "@angular/animations": "18.2.5", + "@angular/common": "18.2.5", + "@angular/compiler": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5" } }, "node_modules/@angular/router": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.1.tgz", - "integrity": "sha512-gVyqW6fYnG7oq1DlZSXJMQ2Py2dJQB7g6XVtRcYB1gR4aeowx5N9ws7PjqAi0ih91ASq2MmP4OlSSWLq+eaMGg==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.5.tgz", + "integrity": "sha512-OjZV1PTiSwT0ytmR0ykveLYzs4uQWf0EuIclZmWqM/bb8Q4P+gJl7/sya05nGnZsj6nHGOL0e/LhSZ3N+5p6qg==", "dependencies": { "tslib": "^2.3.0" }, @@ -799,16 +770,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1", + "@angular/common": "18.2.5", + "@angular/core": "18.2.5", + "@angular/platform-browser": "18.2.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/ssr": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-18.2.1.tgz", - "integrity": "sha512-e+/RZZmUAUVv22JpOQ64z7RzzlCbyx2spDoKJgopp+LmfWqdR99LEvZ6H01yd8ZzynOEQtFIilPcrEWKQ1kSXg==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-18.2.5.tgz", + "integrity": "sha512-23YFcnIdcEIxr4ezzqGypxAYNeLmSM1YqY5DofQlYQIJmoORJkWFnv49pahcWYnysF8kl+BIhirGGKaOZWjxyg==", "dependencies": { "critters": "0.0.24", "tslib": "^2.3.0" @@ -3052,6 +3023,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", @@ -3110,9 +3090,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", - "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", + "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3127,6 +3107,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -3767,22 +3759,6 @@ "win32" ] }, - "node_modules/@ngtools/webpack": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.1.tgz", - "integrity": "sha512-v86U3jOoy5R9ZWe9Q0LbHRx/IBw1lbn0ldBU+gIIepREyVvb9CcH/vAyIb2Fw1zaYvvfG1OyzdrHyW8iGXjdnQ==", - "dev": true, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^18.0.0", - "typescript": ">=5.4 <5.6", - "webpack": "^5.54.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4399,13 +4375,13 @@ } }, "node_modules/@schematics/angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.1.tgz", - "integrity": "sha512-bBV7I+MCbdQmBPUFF4ECg37VReM0+AdQsxgwkjBBSYExmkErkDoDgKquwL/tH7stDCc5IfTd0g9BMeosRgDMug==", + "version": "18.2.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.5.tgz", + "integrity": "sha512-tBXhk9OGT4U6VsBNbuCNl2ITDOF3NYdGrEieIHU+lHSkpJNGZUIGxCgXCETXkmXDq1pe4wFZSKelWjeqYDfX0g==", "dev": true, "dependencies": { - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", + "@angular-devkit/core": "18.2.5", + "@angular-devkit/schematics": "18.2.5", "jsonc-parser": "3.3.1" }, "engines": { @@ -4619,26 +4595,6 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", - "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4712,9 +4668,9 @@ } }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", "dev": true, "dependencies": { "undici-types": "~6.19.2" @@ -4808,16 +4764,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", - "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz", + "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/type-utils": "8.2.0", - "@typescript-eslint/utils": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/type-utils": "8.7.0", + "@typescript-eslint/utils": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4841,15 +4797,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", - "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz", + "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4" }, "engines": { @@ -4869,13 +4825,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", - "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", + "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0" + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4886,13 +4842,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", - "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz", + "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/typescript-estree": "8.7.0", + "@typescript-eslint/utils": "8.7.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4910,9 +4866,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", - "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", + "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4923,15 +4879,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", - "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", + "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -4959,26 +4915,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -4994,25 +4930,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", - "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", + "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0" + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5026,12 +4953,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", - "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", + "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/types": "8.7.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -5231,6 +5158,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -5297,29 +5225,29 @@ } }, "node_modules/ag-charts-types": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-10.1.0.tgz", - "integrity": "sha512-pk9ft8hbgTXJ/thI/SEUR1BoauNplYExpcHh7tMOqVikoDsta1O15TB1ZL4XWnl4TPIzROBmONKsz7d8a2HBuQ==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-10.2.0.tgz", + "integrity": "sha512-PUqH1QtugpYLnlbMdeSZVf5PpT1XZVsP69qN1JXhetLtQpVC28zaj7ikwu9CMA9N9b+dBboA9QcjUQUJZVUokQ==" }, "node_modules/ag-grid-angular": { - "version": "32.1.0", - "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-32.1.0.tgz", - "integrity": "sha512-d21XbuSUvuy8Hp3m/ltU1dIjPRRB082+tNeKodf2Mwt0zNCY5H+Y1kSimRDnhU5KLK/3yIiCzpgAzwz6AmnxtQ==", + "version": "32.2.0", + "resolved": "https://registry.npmjs.org/ag-grid-angular/-/ag-grid-angular-32.2.0.tgz", + "integrity": "sha512-KqipDccT92EzZsduBi7H0uk2RH96DYhry48uy8iI8Y7injPDKAVFx7ZnGbxkwNEOFEiidi8wa6KrU8Z6yH2M3Q==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": ">= 16.0.0", "@angular/core": ">= 16.0.0", - "ag-grid-community": "32.1.0" + "ag-grid-community": "32.2.0" } }, "node_modules/ag-grid-community": { - "version": "32.1.0", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-32.1.0.tgz", - "integrity": "sha512-RVvkjRH61nuCXwIqTKQPqNbKR+8cGBKw7S1qmmMXsy0pCBAJaQn4kL3v31hKHxDtV4bPscBXLFKGnKzHuss0GQ==", + "version": "32.2.0", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-32.2.0.tgz", + "integrity": "sha512-zr29DGo6U4JR6pSOgZrZWuN1CTWdpUCfZWWpF47us6MzdFWGN5gcdiXGw40wg9XMGacShNX1aW8o33S4yVkzMw==", "dependencies": { - "ag-charts-types": "10.1.0" + "ag-charts-types": "10.2.0" } }, "node_modules/agent-base": { @@ -5492,16 +5420,8 @@ "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true }, "node_modules/autoprefixer": { "version": "10.4.20", @@ -5685,9 +5605,10 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -5697,7 +5618,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5711,6 +5632,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -5718,7 +5640,8 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/bonjour-service": { "version": "1.2.1", @@ -5850,6 +5773,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -5936,6 +5860,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -6431,6 +6356,7 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -6442,6 +6368,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -6456,6 +6383,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -6463,7 +6391,8 @@ "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true }, "node_modules/copy-anything": { "version": "2.0.6", @@ -6869,6 +6798,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -6881,10 +6811,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -6911,6 +6854,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -6937,18 +6881,6 @@ "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7033,7 +6965,8 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true }, "node_modules/electron-to-chromium": { "version": "1.5.13", @@ -7060,6 +6993,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -7209,6 +7143,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -7220,6 +7155,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -7293,7 +7229,8 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true }, "node_modules/escape-string-regexp": { "version": "1.0.5", @@ -7305,19 +7242,23 @@ } }, "node_modules/eslint": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", - "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", + "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.9.1", + "@eslint/js": "9.11.1", + "@eslint/plugin-kit": "^0.2.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -7337,7 +7278,6 @@ "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", @@ -7388,6 +7328,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, "node_modules/eslint/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -7713,6 +7659,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -7762,36 +7709,37 @@ "dev": true }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -7806,14 +7754,25 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/extend": { "version": "3.0.2", @@ -7933,12 +7892,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -7953,14 +7913,25 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/find-cache-dir": { "version": "4.0.0", @@ -8071,6 +8042,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -8092,6 +8064,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -8146,6 +8119,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8184,6 +8158,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -8293,6 +8268,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -8331,6 +8307,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -8342,6 +8319,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8353,6 +8331,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8364,6 +8343,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -8490,6 +8470,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -8586,6 +8567,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -8745,7 +8727,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/ini": { "version": "4.1.3", @@ -8788,6 +8771,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "engines": { "node": ">= 0.10" } @@ -8837,6 +8821,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8885,21 +8884,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -9005,6 +8989,21 @@ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", "dev": true }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -9162,9 +9161,9 @@ } }, "node_modules/jasmine-core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", - "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.3.0.tgz", + "integrity": "sha512-zsOmeBKESky4toybvWEikRiZ0jHoBEu79wNArLfMdSnlLMZx3Xcp6CSm2sUcYyoJC+Uyj8LBJap/MUbVSfJ27g==", "dev": true }, "node_modules/jest-worker": { @@ -9534,9 +9533,9 @@ } }, "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -10217,14 +10216,15 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", - "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.12.0.tgz", + "integrity": "sha512-74wDsex5tQDSClVkeK1vtxqYCAgCoXxx+K4NSHzgU/muYVYByFqa+0RnrPO9NM6naWm1+G9JmZ0p6QHhXmeYfA==", "dev": true, "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", @@ -10241,9 +10241,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10264,6 +10268,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -10309,6 +10314,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -10317,6 +10323,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -10716,6 +10723,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -11229,6 +11237,7 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11243,6 +11252,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "dependencies": { "ee-first": "1.1.1" }, @@ -11283,6 +11293,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -11609,6 +11637,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -11672,23 +11701,15 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "4.0.2", @@ -11957,9 +11978,9 @@ } }, "node_modules/primeng": { - "version": "17.18.9", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.9.tgz", - "integrity": "sha512-1FT0B8wtgvs/joduB1DDOLe2IsP1pegOiEfSPAHSbc6otgNx/6iLR0k2M/xr2c9Ur1aC7tAikkVfH3FGpWof3w==", + "version": "17.18.10", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.10.tgz", + "integrity": "sha512-P3UskInOZ7qYICxSYvf0K8nUEb7DmndiXmyvLGU1wch+XcVWmVs4FZsWKNfdvK7TUdxxYj8WW44nodNV/epr3A==", "dependencies": { "tslib": "^2.3.0" }, @@ -12009,6 +12030,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -12043,11 +12065,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12089,6 +12112,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -12097,6 +12121,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -12442,6 +12467,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -12460,7 +12486,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "node_modules/sass": { "version": "1.77.6", @@ -12577,9 +12604,10 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -12603,6 +12631,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -12610,12 +12639,14 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, "bin": { "mime": "cli.js" }, @@ -12626,7 +12657,8 @@ "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -12716,23 +12748,34 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-function-length": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dev": true, "dependencies": { "define-data-property": "^1.1.2", "es-errors": "^1.3.0", @@ -12748,7 +12791,8 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -12796,6 +12840,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -12984,9 +13029,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -13120,6 +13165,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -13497,6 +13543,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "engines": { "node": ">=0.6" } @@ -13585,6 +13632,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -13730,6 +13778,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -13783,6 +13832,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, "engines": { "node": ">= 0.4.0" } @@ -13819,19 +13869,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "engines": { "node": ">= 0.8" } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -14288,6 +14339,34 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -14335,12 +14414,11 @@ "dev": true }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -14349,7 +14427,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -14382,9 +14460,9 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -14478,18 +14556,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-dev-server/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -14543,21 +14609,6 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-dev-server/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -14573,24 +14624,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "dev": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webpack-dev-server/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", diff --git a/package.json b/package.json index 3b6758ef..2e5b5083 100644 --- a/package.json +++ b/package.json @@ -14,47 +14,47 @@ }, "private": true, "dependencies": { - "@angular/animations": "^18.2.1", - "@angular/cdk": "^18.2.1", - "@angular/common": "^18.2.1", - "@angular/compiler": "^18.2.1", - "@angular/core": "^18.2.1", - "@angular/forms": "^18.2.1", - "@angular/material": "^18.2.1", - "@angular/platform-browser": "^18.2.1", - "@angular/platform-browser-dynamic": "^18.2.1", - "@angular/platform-server": "^18.2.1", - "@angular/router": "^18.2.1", - "@angular/ssr": "^18.2.1", + "@angular/animations": "^18.2.5", + "@angular/cdk": "^18.2.5", + "@angular/common": "^18.2.5", + "@angular/compiler": "^18.2.5", + "@angular/core": "^18.2.5", + "@angular/forms": "^18.2.5", + "@angular/material": "^18.2.5", + "@angular/platform-browser": "^18.2.5", + "@angular/platform-browser-dynamic": "^18.2.5", + "@angular/platform-server": "^18.2.5", + "@angular/router": "^18.2.5", + "@angular/ssr": "^18.2.5", "@swimlane/ngx-datatable": "^20.1.0", - "ag-grid-angular": "^32.1.0", - "ag-grid-community": "^32.1.0", + "ag-grid-angular": "^32.2.0", + "ag-grid-community": "^32.2.0", "chance": "^1.1.12", - "express": "^4.18.2", "ng-zorro-antd": "^18.1.1", "ngx-color-picker": "^17.0.0", "ngx-infinite-scroll": "^18.0.0", - "primeng": "^17.18.9", + "primeng": "^17.18.10", "rxjs": "~7.8.0", "tslib": "^2.7.0", "zone.js": "^0.14.10" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.2.1", - "@angular-eslint/builder": "18.3.0", - "@angular-eslint/eslint-plugin": "18.3.0", - "@angular-eslint/eslint-plugin-template": "18.3.0", - "@angular-eslint/schematics": "18.3.0", - "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "^18.2.1", - "@angular/compiler-cli": "^18.2.1", + "@angular-devkit/build-angular": "^18.2.5", + "@angular-eslint/builder": "18.3.1", + "@angular-eslint/eslint-plugin": "18.3.1", + "@angular-eslint/eslint-plugin-template": "18.3.1", + "@angular-eslint/schematics": "18.3.1", + "@angular-eslint/template-parser": "18.3.1", + "@angular/cli": "^18.2.5", + "@angular/compiler-cli": "^18.2.5", "@types/express": "^4.17.17", "@types/jasmine": "~5.1.0", - "@types/node": "^22.5.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "^9.8.0", - "jasmine-core": "^5.2.0", + "@types/node": "^22.6.1", + "@typescript-eslint/eslint-plugin": "^8.7.0", + "@typescript-eslint/parser": "^8.7.0", + "eslint": "^9.11.1", + "express": "^4.21.0", + "jasmine-core": "^5.3.0", "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", diff --git a/projects/ngx-scrollbar-demo/src/app/app.config.ts b/projects/ngx-scrollbar-demo/src/app/app.config.ts index da09d50b..da89a3ec 100644 --- a/projects/ngx-scrollbar-demo/src/app/app.config.ts +++ b/projects/ngx-scrollbar-demo/src/app/app.config.ts @@ -1,10 +1,10 @@ import { ApplicationConfig } from '@angular/core'; import { provideRouter, withHashLocation } from '@angular/router'; import { provideClientHydration } from '@angular/platform-browser'; -import { provideAnimations } from '@angular/platform-browser/animations'; import { provideHttpClient, withFetch } from '@angular/common/http'; import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle'; import { routes } from './app.routes'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; export const appConfig: ApplicationConfig = { providers: [ @@ -17,6 +17,6 @@ export const appConfig: ApplicationConfig = { provideRouter(routes, withHashLocation()), provideClientHydration(), provideHttpClient(withFetch()), - provideAnimations() + provideAnimationsAsync() ] }; diff --git a/projects/ngx-scrollbar-demo/src/app/example-ag-grid-table/example-ag-grid-table.component.html b/projects/ngx-scrollbar-demo/src/app/example-ag-grid-table/example-ag-grid-table.component.html index ba859677..410eed94 100644 --- a/projects/ngx-scrollbar-demo/src/app/example-ag-grid-table/example-ag-grid-table.component.html +++ b/projects/ngx-scrollbar-demo/src/app/example-ag-grid-table/example-ag-grid-table.component.html @@ -1,6 +1,5 @@ + externalContentWrapper=".ag-center-cols-container"> - - {{ i }} - + @for (i of array; track i) { + {{ i }} + } diff --git a/projects/ngx-scrollbar-demo/src/app/example-infinite-scroll/example-infinite-scroll.component.ts b/projects/ngx-scrollbar-demo/src/app/example-infinite-scroll/example-infinite-scroll.component.ts index 09e36e07..008a7564 100644 --- a/projects/ngx-scrollbar-demo/src/app/example-infinite-scroll/example-infinite-scroll.component.ts +++ b/projects/ngx-scrollbar-demo/src/app/example-infinite-scroll/example-infinite-scroll.component.ts @@ -1,7 +1,7 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatListModule } from '@angular/material/list'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { InfiniteScrollDirective } from 'ngx-infinite-scroll'; import { MatCardModule } from '@angular/material/card'; import { NgScrollbarModule } from 'ngx-scrollbar'; import { Chance } from 'chance'; @@ -15,7 +15,7 @@ import { Chance } from 'chance'; '[class.example-component]': 'true' }, standalone: true, - imports: [MatCardModule, NgScrollbarModule, InfiniteScrollModule, MatListModule, CommonModule] + imports: [MatCardModule, NgScrollbarModule, MatListModule, CommonModule, InfiniteScrollDirective] }) export class ExampleInfiniteScrollComponent { chance = new Chance(); diff --git a/projects/ngx-scrollbar-demo/src/app/example-mat-select/example-mat-select.component.ts b/projects/ngx-scrollbar-demo/src/app/example-mat-select/example-mat-select.component.ts index f93d4a19..333b4aa6 100644 --- a/projects/ngx-scrollbar-demo/src/app/example-mat-select/example-mat-select.component.ts +++ b/projects/ngx-scrollbar-demo/src/app/example-mat-select/example-mat-select.component.ts @@ -13,7 +13,6 @@ import { Chance } from 'chance'; imports: [ MatOptionModule, MatSelectModule, - NgForOf, NgScrollbar, MatCardModule, NgScrollbarMatSelectViewport diff --git a/projects/ngx-scrollbar-demo/src/app/example-nested-virtual-scroll/example-nested-virtual-scroll.component.html b/projects/ngx-scrollbar-demo/src/app/example-nested-virtual-scroll/example-nested-virtual-scroll.component.html index 37fa0766..3ecf83f0 100644 --- a/projects/ngx-scrollbar-demo/src/app/example-nested-virtual-scroll/example-nested-virtual-scroll.component.html +++ b/projects/ngx-scrollbar-demo/src/app/example-nested-virtual-scroll/example-nested-virtual-scroll.component.html @@ -11,6 +11,8 @@ disableSensor class="outer-ng-scrollbar">
diff --git a/projects/ngx-scrollbar-demo/src/app/example-ngx-datatable/example-ngx-datatable.component.html b/projects/ngx-scrollbar-demo/src/app/example-ngx-datatable/example-ngx-datatable.component.html index 4107aa77..32802e6d 100644 --- a/projects/ngx-scrollbar-demo/src/app/example-ngx-datatable/example-ngx-datatable.component.html +++ b/projects/ngx-scrollbar-demo/src/app/example-ngx-datatable/example-ngx-datatable.component.html @@ -1,8 +1,7 @@
+ externalContentWrapper="datatable-selection"> + appearance="compact" + disableSensor> - {{item}} + {{ item }} diff --git a/projects/ngx-scrollbar-demo/src/app/example2/example2.component.html b/projects/ngx-scrollbar-demo/src/app/example2/example2.component.html index e4d6097d..dec40dbc 100644 --- a/projects/ngx-scrollbar-demo/src/app/example2/example2.component.html +++ b/projects/ngx-scrollbar-demo/src/app/example2/example2.component.html @@ -11,9 +11,9 @@
  • -
  • {{item.title}}
  • + @for (item of list; track item.title) { +
  • {{ item.title }}
  • + }
diff --git a/projects/ngx-scrollbar-demo/src/app/integration-page/integration-page.component.html b/projects/ngx-scrollbar-demo/src/app/integration-page/integration-page.component.html index 32c59248..d45e4de4 100644 --- a/projects/ngx-scrollbar-demo/src/app/integration-page/integration-page.component.html +++ b/projects/ngx-scrollbar-demo/src/app/integration-page/integration-page.component.html @@ -3,7 +3,7 @@

Ngx-datatable Example

-

Ag-grid table Example

+

Ag-grid Table Example

diff --git a/projects/ngx-scrollbar-demo/src/app/lab/lab.component.html b/projects/ngx-scrollbar-demo/src/app/lab/lab.component.html index 7de502a9..305930f1 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/lab.component.html +++ b/projects/ngx-scrollbar-demo/src/app/lab/lab.component.html @@ -184,9 +184,11 @@ [style.width.px]="(component.nativeElement.offsetWidth - (appearance === 'native' ? +variables.thickness + (+variables.trackOffset * 2) : 0)) * slider.contentWidth" [innerHTML]="content">
-
- -
+ @if (scrollToElementSelected) { +
+ +
+ } diff --git a/projects/ngx-scrollbar-demo/src/app/lab/lab.component.scss b/projects/ngx-scrollbar-demo/src/app/lab/lab.component.scss index f0a7194b..e81a18ff 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/lab.component.scss +++ b/projects/ngx-scrollbar-demo/src/app/lab/lab.component.scss @@ -61,3 +61,11 @@ nz-resize-handle { right: -20px; bottom: -20px; } + +#target { + position: absolute; + top: 300px; + left: 500px; + width: 100px; + height: 100px; +} diff --git a/projects/ngx-scrollbar-demo/src/app/lab/lab.component.ts b/projects/ngx-scrollbar-demo/src/app/lab/lab.component.ts index bbc2e745..c7b10f07 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/lab.component.ts +++ b/projects/ngx-scrollbar-demo/src/app/lab/lab.component.ts @@ -140,6 +140,7 @@ export class LabComponent { const options: Partial = { [event.axisXProperty]: event.axisXValue, [event.axisYProperty]: event.axisYValue, + center: event.center, duration: event.duration }; // This shows effect on play button when scrollTo has reached diff --git a/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.html b/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.html index 292b4d6e..c3ab2c76 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.html +++ b/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.html @@ -1,15 +1,21 @@
Content length - - + +
-
Content width - +
diff --git a/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.ts b/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.ts index 0ff30d09..c07eacb3 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.ts +++ b/projects/ngx-scrollbar-demo/src/app/lab/resize-form/resize-form.component.ts @@ -1,4 +1,4 @@ -import { AfterViewChecked, ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, model, ModelSignal, ChangeDetectionStrategy } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatSliderModule } from '@angular/material/slider'; import { MatDividerModule } from '@angular/material/divider'; @@ -11,14 +11,9 @@ import { MatDividerModule } from '@angular/material/divider'; standalone: true, imports: [MatSliderModule, FormsModule, MatDividerModule] }) -export class ResizeFormComponent implements AfterViewChecked { +export class ResizeFormComponent { - @Input() value: ResizeChange; - @Output() valueChange: EventEmitter = new EventEmitter(); - - ngAfterViewChecked() { - this.valueChange.emit(this.value); - } + value: ModelSignal = model() contentWidthDisplayWith(value: number) { return `x${ value }`; diff --git a/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.html b/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.html index 3e6b70a1..1f271414 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.html +++ b/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.html @@ -2,10 +2,8 @@ Function + [(ngModel)]="scrollFunc"> Scroll To Scroll To Element @@ -14,11 +12,11 @@
Axis-Y + [(ngModel)]="axisYProperty" + [disabled]="center()"> Top - Bottom + Bottom Unset
@@ -28,48 +26,65 @@ Axis-X - Start - End + [(ngModel)]="axisXProperty" + [disabled]="center()"> + Start + End Left - Right + Right Unset
+ +@if (scrollFunc() === 'scrollToElement') { +
+ Center + + True + False + +
+} +
Duration - + - {{ options.axisYProperty === 'unset' ? null : scrollToElementSelected ? options.axisYProperty+ ' offset' : options.axisYProperty }} + @if (axisYProperty() !== 'unset') { + {{ axisYProperty() }} + } + [(ngModel)]="axisYValue" + [disabled]="axisYProperty() === 'unset' || center()"> - {{ options.axisXProperty === 'unset' ? null : scrollToElementSelected ? options.axisXProperty+ ' offset' : options.axisXProperty }} + @if (axisXProperty() !== 'unset') { + {{ axisXProperty() }} + } + [(ngModel)]="axisXValue" + [disabled]="axisXProperty() === 'unset' || center()">
Result -
{{displayFunction}}
+
{{ displayFunction() }}
-
diff --git a/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.ts b/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.ts index 21b6d94e..a295e218 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.ts +++ b/projects/ngx-scrollbar-demo/src/app/lab/smooth-scroll-form/smooth-scroll-form.component.ts @@ -1,4 +1,16 @@ -import { Component, ChangeDetectionStrategy, Output, EventEmitter, DoCheck, Input } from '@angular/core'; +import { + Component, + output, + signal, + effect, + computed, + input, + Signal, + InputSignal, + WritableSignal, + OutputEmitterRef, + ChangeDetectionStrategy +} from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; @@ -14,48 +26,55 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle'; changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatButtonToggleModule, FormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule] }) -export class SmoothScrollFormComponent implements DoCheck { - displayFunction: string; +export class SmoothScrollFormComponent { scrollToElementSelected: boolean; - options: SmoothScrollOptionsForm = { - scrollFunc: 'scrollTo', - duration: 800, - axisYProperty: 'bottom', - axisYValue: 0, - axisXProperty: 'end', - axisXValue: 0 - }; + scrollFunc: WritableSignal = signal('scrollTo'); + duration: WritableSignal = signal(800); + axisYProperty: WritableSignal = signal('bottom'); + axisYValue: WritableSignal = signal(0); + axisXProperty: WritableSignal = signal('end'); + axisXValue: WritableSignal = signal(0); + center: WritableSignal = signal(false); - @Input() reached: boolean; - @Output('scrollToElementSelected') scrollToElement: EventEmitter = new EventEmitter(); - @Output() scrollTo: EventEmitter = new EventEmitter(); - - ngDoCheck() { - const axisX: string = this.options.axisXProperty === 'unset' ? '' : `${ this.options.axisXProperty }: ${ this.options.axisXValue }`; - const axisY: string = this.options.axisYProperty === 'unset' ? '' : `${ this.options.axisYProperty }: ${ this.options.axisYValue }`; - const comma: string = this.options.axisXProperty !== 'unset' && this.options.axisYProperty !== 'unset' ? ', ' : ''; - const durationComma: string = this.options.axisXProperty !== 'unset' || this.options.axisYProperty !== 'unset' ? ', ' : ''; - if (this.options.scrollFunc === 'scrollToElement') { - this.displayFunction = - `scrollToElement('#target', {${ axisY }${ comma }${ axisX }${ durationComma }duration: ${ this.options.duration }})`; - } else { - this.displayFunction = - `scrollTo({${ axisY }${ comma }${ axisX }${ durationComma }duration: ${ this.options.duration }})`; + options: Signal = computed(() => { + return { + scrollFunc: this.scrollFunc(), + duration: this.duration(), + axisYProperty: this.axisYProperty(), + axisYValue: this.axisYValue(), + axisXProperty: this.axisXProperty(), + axisXValue: this.axisXValue(), + center: this.center() } - } + }); - scrollFuncChanged(e: string) { - this.scrollToElementSelected = e === 'scrollToElement'; - if (this.scrollToElementSelected) { - this.options.axisXProperty = 'unset'; - this.options.axisYProperty = 'unset'; + displayFunction: Signal = computed(() => { + const center: boolean = this.center(); + if (center) { + return `scrollToElement('#target', { center: true , duration: ${ this.duration() }})`; } - this.scrollToElement.emit(this.scrollToElementSelected); - } + const axisX: string = this.axisXProperty() === 'unset' ? '' : `${ this.axisXProperty() }: ${ this.axisXValue() }`; + const axisY: string = this.axisYProperty() === 'unset' ? '' : `${ this.axisYProperty() }: ${ this.axisYValue() }`; + const comma: string = this.axisXProperty() !== 'unset' && this.axisYProperty() !== 'unset' ? ', ' : ''; + const durationComma: string = this.axisXProperty() !== 'unset' || this.axisYProperty() !== 'unset' ? ', ' : ''; + if (this.scrollFunc() === 'scrollToElement') { + return `scrollToElement('#target', {${ axisY }${ comma }${ axisX }${ durationComma }duration: ${ this.duration() }})`; + } + return `scrollTo({${ axisY }${ comma }${ axisX }${ durationComma }duration: ${ this.duration() }})`; + }); + + reached: InputSignal = input(); + + scrollToElement: OutputEmitterRef = output({ alias: 'scrollToElementSelected' }); + + scrollTo: OutputEmitterRef = output(); - play() { - this.scrollTo.emit(this.options); + constructor() { + effect(() => { + this.scrollToElementSelected = this.scrollFunc() === 'scrollToElement'; + this.scrollToElement.emit(this.scrollToElementSelected); + }); } } @@ -66,4 +85,5 @@ export interface SmoothScrollOptionsForm { axisYValue: number; axisXProperty: string; axisXValue: number; + center: boolean; } diff --git a/projects/ngx-scrollbar-demo/src/app/lab/toggle-form/toggle-form.component.ts b/projects/ngx-scrollbar-demo/src/app/lab/toggle-form/toggle-form.component.ts index 5265b0e0..67ac262c 100644 --- a/projects/ngx-scrollbar-demo/src/app/lab/toggle-form/toggle-form.component.ts +++ b/projects/ngx-scrollbar-demo/src/app/lab/toggle-form/toggle-form.component.ts @@ -1,29 +1,22 @@ -import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, AfterViewChecked } from '@angular/core'; +import { Component, model, ModelSignal, ChangeDetectionStrategy } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; @Component({ + standalone: true, selector: 'app-toggle-form', template: ` - Disable Resize Sensor - Disable Reached Events - RTL - Highlight + Disable Resize Sensor + Disable Reached Events + RTL + Highlight `, - styleUrls: ['./toggle-form.component.scss'], + styleUrl: './toggle-form.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, imports: [MatSlideToggleModule, FormsModule] }) -export class ToggleFormComponent implements AfterViewChecked { - - @Input() value: ToggleChange; - @Output() valueChange = new EventEmitter(); - - ngAfterViewChecked() { - this.valueChange.emit(this.value); - } - +export class ToggleFormComponent { + value: ModelSignal = model(); } export interface ToggleChange { diff --git a/projects/ngx-scrollbar-demo/src/app/prime-ng-table/prime-ng.component.html b/projects/ngx-scrollbar-demo/src/app/prime-ng-table/prime-ng.component.html index a71660f5..d3935f01 100644 --- a/projects/ngx-scrollbar-demo/src/app/prime-ng-table/prime-ng.component.html +++ b/projects/ngx-scrollbar-demo/src/app/prime-ng-table/prime-ng.component.html @@ -42,8 +42,7 @@ + appearance="compact"> - - {{ col.header }} - + @for (col of columns; track col.header) { + {{ col.header }} + } - - {{ rowData[col.field] }} - + @for (col of columns; track col.header) { + {{ rowData[col.field] }} + } @@ -80,8 +79,7 @@ + appearance="compact"> Custom overlay-scrollbars with native scrolling mechanism codecov @@ -53,7 +53,7 @@

Custom overlay-scrollbars with native scrolling mechanism

npm diff --git a/projects/ngx-scrollbar-demo/src/main.ts b/projects/ngx-scrollbar-demo/src/main.ts index 35b00f34..01007918 100644 --- a/projects/ngx-scrollbar-demo/src/main.ts +++ b/projects/ngx-scrollbar-demo/src/main.ts @@ -2,5 +2,9 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; +window.addEventListener('error', function (e) { + console.error('🍄 ResizeObserver Error occurred: ', e); +}); + bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); diff --git a/projects/ngx-scrollbar/cdk/src/cdk-virtual-scroll.ts b/projects/ngx-scrollbar/cdk/src/cdk-virtual-scroll.ts index 6405f63e..36b48dc3 100644 --- a/projects/ngx-scrollbar/cdk/src/cdk-virtual-scroll.ts +++ b/projects/ngx-scrollbar/cdk/src/cdk-virtual-scroll.ts @@ -1,13 +1,4 @@ -import { - Directive, - Input, - inject, - effect, - contentChild, - Signal, - EffectCleanupRegisterFn -} from '@angular/core'; -import { Platform } from '@angular/cdk/platform'; +import { Directive, inject, effect, untracked, contentChild, Signal } from '@angular/core'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { NgScrollbarExt } from 'ngx-scrollbar'; @@ -19,44 +10,26 @@ export class NgScrollbarCdkVirtualScroll { private readonly scrollbar: NgScrollbarExt = inject(NgScrollbarExt, { self: true }); - private readonly platform: Platform = inject(Platform); - private readonly virtualScrollViewportRef: Signal = contentChild(CdkVirtualScrollViewport); - @Input() cdkVirtualScrollViewport: '' | 'auto'; - constructor() { - this.scrollbar.externalViewport = '.cdk-virtual-scroll-viewport'; - this.scrollbar.externalContentWrapper = '.cdk-virtual-scroll-content-wrapper'; - this.scrollbar.externalSpacer = '.cdk-virtual-scroll-spacer'; + this.scrollbar.skipInit = true; - effect((onCleanup: EffectCleanupRegisterFn) => { - // If content width is bigger than the viewport, we need to update the spacer width to display horizontal scrollbar - let resizeObserver: ResizeObserver; + effect(() => { const virtualScrollViewport: CdkVirtualScrollViewport = this.virtualScrollViewportRef(); - const spacer: HTMLElement = virtualScrollViewport.elementRef.nativeElement.querySelector(this.scrollbar.externalSpacer); - - if (this.platform.isBrowser && virtualScrollViewport) { - resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => { - entries.forEach((entry: ResizeObserverEntry) => { - if (virtualScrollViewport.orientation === 'vertical') { - spacer.style.setProperty('width', `${ entry.contentRect.width }px`); - } else { - spacer.style.setProperty('height', `${ entry.contentRect.height }px`); - } - }); - - // Disconnect after first change if directive is not set to auto - if (this.cdkVirtualScrollViewport !== 'auto') { - resizeObserver.disconnect(); - } - // Observe content wrapper for size changes - resizeObserver.observe(virtualScrollViewport._contentWrapper.nativeElement); - }); - } - - onCleanup(() => resizeObserver?.disconnect()); + untracked(() => { + if (virtualScrollViewport) { + const viewport: HTMLElement = virtualScrollViewport.elementRef.nativeElement; + const contentWrapper: HTMLElement = virtualScrollViewport._contentWrapper.nativeElement; + const spacer: HTMLElement = virtualScrollViewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-spacer'); + + this.scrollbar.skipInit = false; + this.scrollbar.altViewport.set(viewport); + this.scrollbar.altContentWrapper.set(contentWrapper); + this.scrollbar.altSpacer.set(spacer); + } + }); }); } } diff --git a/projects/ngx-scrollbar/karma.conf.js b/projects/ngx-scrollbar/karma.conf.js index 67690c11..b54fbf60 100644 --- a/projects/ngx-scrollbar/karma.conf.js +++ b/projects/ngx-scrollbar/karma.conf.js @@ -35,7 +35,13 @@ module.exports = function (config) { ] }, reporters: ['progress', 'kjhtml'], - browsers: ['Chrome'], + browsers: ["MyChromeWithoutSearchSelect"], + customLaunchers: { + MyChromeWithoutSearchSelect: { + base: "Chrome", + flags: ["-disable-search-engine-choice-screen"], + }, + }, restartOnFileChange: true }); }; diff --git a/projects/ngx-scrollbar/mat/src/mat-select-viewport.ts b/projects/ngx-scrollbar/mat/src/mat-select-viewport.ts index 6692afe0..ae606e4a 100644 --- a/projects/ngx-scrollbar/mat/src/mat-select-viewport.ts +++ b/projects/ngx-scrollbar/mat/src/mat-select-viewport.ts @@ -1,4 +1,4 @@ -import { Directive, effect, inject } from '@angular/core'; +import { Directive, effect, inject, untracked } from '@angular/core'; import { MatOption, MatSelect } from '@angular/material/select'; import { _NgScrollbar, NG_SCROLLBAR } from 'ngx-scrollbar'; @@ -14,14 +14,18 @@ export class NgScrollbarMatSelectViewport { constructor() { effect(() => { - if (this.scrollbar.isVerticallyScrollable() && this.matSelect.panelOpen) { - const selected: MatOption | MatOption[] = this.matSelect.selected; - if (selected) { - const element: HTMLElement = Array.isArray(selected) ? selected[0]._getHostElement() : selected._getHostElement(); - const height: number = this.scrollbar.nativeElement.clientHeight; - this.scrollbar.viewport.scrollYTo(element.offsetTop + element.offsetHeight - height); + const isVerticallyScrollable: boolean = this.scrollbar.isVerticallyScrollable(); + + untracked(() => { + if (isVerticallyScrollable && this.matSelect.panelOpen) { + const selected: MatOption | MatOption[] = this.matSelect.selected; + if (selected) { + const element: HTMLElement = Array.isArray(selected) ? selected[0]._getHostElement() : selected._getHostElement(); + const height: number = this.scrollbar.nativeElement.clientHeight; + this.scrollbar.viewport.scrollYTo(element.offsetTop + element.offsetHeight - height); + } } - } + }); }); } } diff --git a/projects/ngx-scrollbar/package.json b/projects/ngx-scrollbar/package.json index 255f441a..9e97c9dd 100644 --- a/projects/ngx-scrollbar/package.json +++ b/projects/ngx-scrollbar/package.json @@ -1,6 +1,6 @@ { "name": "ngx-scrollbar", - "version": "15.1.3", + "version": "16.0.0-beta.2", "license": "MIT", "homepage": "https://ngx-scrollbar.netlify.app/", "author": { @@ -20,9 +20,9 @@ "scroll-reached" ], "peerDependencies": { - "@angular/common": ">=17.1.0", - "@angular/core": ">=17.1.0", - "@angular/cdk": ">=17.1.0", + "@angular/common": ">=17.3.0", + "@angular/core": ">=17.3.0", + "@angular/cdk": ">=17.3.0", "rxjs": ">=7.0.0" }, "dependencies": { diff --git a/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll-manager.ts b/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll-manager.ts index 5352a02b..a99c13b8 100644 --- a/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll-manager.ts +++ b/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll-manager.ts @@ -194,11 +194,11 @@ export class SmoothScrollManager { const options: SmoothScrollToOptions = { ...this._defaultOptions, ...customOptions, - ...({ + ...{ // Rewrite start & end offsets as right or left offsets. left: customOptions.left == null ? (isRtl ? customOptions.end : customOptions.start) : customOptions.left, right: customOptions.right == null ? (isRtl ? customOptions.start : customOptions.end) : customOptions.right - }) + } }; // Rewrite the bottom offset as a top offset. @@ -226,20 +226,66 @@ export class SmoothScrollManager { scrollToElement(scrollable: SmoothScrollElement, target: SmoothScrollElement, customOptions: SmoothScrollToElementOptions = {}): Promise { const scrollableEl: Element = this.getElement(scrollable); const targetEl: Element = this.getElement(target, scrollableEl); + const isRtl: boolean = getComputedStyle(scrollableEl).direction === 'rtl'; + + if (!targetEl || !scrollableEl) { + return Promise.resolve(); + } + + const scrollableRect: DOMRect = scrollableEl.getBoundingClientRect(); + const targetRect: DOMRect = targetEl.getBoundingClientRect(); + + const options: SmoothScrollToOptions = { + ...this._defaultOptions, + ...customOptions, + ...{ + top: targetRect.top + scrollableEl.scrollTop - scrollableRect.top + (customOptions.top || 0), + // Rewrite start & end offsets as right or left offsets. + left: customOptions.left == null ? (isRtl ? customOptions.end : customOptions.start) : customOptions.left, + right: customOptions.right == null ? (isRtl ? customOptions.start : customOptions.end) : customOptions.right + } + }; - if (targetEl && scrollableEl) { - const scrollableRect: DOMRect = scrollableEl.getBoundingClientRect(); - const targetRect: DOMRect = targetEl.getBoundingClientRect(); + if (customOptions.center) { + // Calculate the center of the container + const containerCenterX = scrollableRect.left + scrollableRect.width / 2; + const containerCenterY = scrollableRect.top + scrollableRect.height / 2; - const options: SmoothScrollToOptions = { - ...customOptions, - left: targetRect.left + scrollableEl.scrollLeft - scrollableRect.left + (customOptions.left || 0), - top: targetRect.top + scrollableEl.scrollTop - scrollableRect.top + (customOptions.top || 0) - }; + // Calculate the target's position relative to the container + const targetCenterX = targetRect.left + targetRect.width / 2; + const targetCenterY = targetRect.top + targetRect.height / 2; + + // Calculate the scroll position to center the target element in the container + options.left = targetCenterX - containerCenterX + scrollableEl.scrollLeft; + options.top = targetCenterY - containerCenterY + scrollableEl.scrollTop; + return this.applyScrollToOptions(scrollableEl, options); + } + + if (options.bottom != null) { + const bottomEdge: number = scrollableRect.height - targetRect.height; + options.top = targetRect.top + scrollableEl.scrollTop - scrollableRect.top - bottomEdge + (customOptions.bottom || 0); + } + + // Rewrite the right offset as a left offset. + if (isRtl) { + options.left = targetRect.left - scrollableRect.left + scrollableEl.scrollLeft + (options.left || 0); + if (options.right != null) { + options.left = targetRect.right - scrollableRect.left + scrollableEl.scrollLeft - scrollableRect.width + (options.right || 0); + } + } else { + options.left = targetRect.left - scrollableRect.left + scrollableEl.scrollLeft + (options.left || 0); + if (options.right != null) { + options.left = targetRect.right - scrollableRect.left + scrollableEl.scrollLeft - scrollableRect.width + (options.right || 0); + } + } - return this.scrollTo(scrollableEl, options); + const computedOptions: SmoothScrollToOptions = { + top: options.top, + left: options.left, + easing: options.easing, + duration: options.duration } - return Promise.resolve(); + return this.applyScrollToOptions(scrollableEl, computedOptions); } } diff --git a/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll.model.ts b/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll.model.ts index 5ee61ca4..7718657d 100644 --- a/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll.model.ts +++ b/projects/ngx-scrollbar/smooth-scroll/src/smooth-scroll.model.ts @@ -1,5 +1,5 @@ import { ElementRef, InjectionToken, Provider } from '@angular/core'; -import { _Left, _Top, _XAxis, _YAxis } from '@angular/cdk/scrolling'; +import { _XAxis, _YAxis } from '@angular/cdk/scrolling'; import { defaultSmoothScrollOptions } from './smooth-scroll.default'; export const SMOOTH_SCROLL_OPTIONS: InjectionToken = new InjectionToken('SMOOTH_SCROLL_OPTIONS', { @@ -29,7 +29,7 @@ export type SmoothScrollToOptions = Partial & Pick<_Y /** * Interface for options provided for smooth scrolling to an element. */ -export type SmoothScrollToElementOptions = _Top & _Left & SmoothScrollOptions; +export type SmoothScrollToElementOptions = SmoothScrollToOptions & { center?: boolean }; export interface SmoothScrollStep { scrollable: Element; diff --git a/projects/ngx-scrollbar/src/lib/async-detection.ts b/projects/ngx-scrollbar/src/lib/async-detection.ts index 4750598a..7b51a137 100644 --- a/projects/ngx-scrollbar/src/lib/async-detection.ts +++ b/projects/ngx-scrollbar/src/lib/async-detection.ts @@ -1,66 +1,79 @@ -import { Directive, Input, inject, OnInit, OnDestroy, NgZone } from '@angular/core'; +import { + Directive, + inject, + effect, + untracked, + input, + NgZone, + InputSignal, + EffectCleanupRegisterFn +} from '@angular/core'; +import { ContentObserver } from '@angular/cdk/observers'; import { Subscription } from 'rxjs'; import { NgScrollbarExt } from './ng-scrollbar-ext'; -import { mutationObserver } from './viewport'; +import { getThrottledStream } from './utils/common'; @Directive({ standalone: true, selector: 'ng-scrollbar[externalViewport][asyncDetection]' }) -export class AsyncDetection implements OnInit, OnDestroy { +export class AsyncDetection { private readonly scrollbar: NgScrollbarExt = inject(NgScrollbarExt, { self: true }); private readonly zone: NgZone = inject(NgZone); - private subscription: Subscription; + private readonly contentObserver: ContentObserver = inject(ContentObserver); - @Input() asyncDetection: 'auto' | ''; + asyncDetection: InputSignal<'auto' | ''> = input(); constructor() { this.scrollbar.skipInit = true; - } + let sub$: Subscription; - ngOnInit(): void { - this.zone.runOutsideAngular(() => { - this.subscription = mutationObserver(this.scrollbar.nativeElement, 100).subscribe(() => { - if (!this.scrollbar.viewport.initialized()) { - // Search for external viewport - const viewportElement: HTMLElement = this.scrollbar.nativeElement.querySelector(this.scrollbar.externalViewport); + effect((onCleanup: EffectCleanupRegisterFn) => { + const init: boolean = this.scrollbar.viewport.initialized(); + const externalViewport: string = this.scrollbar.externalViewport(); + const externalContentWrapper: string = this.scrollbar.externalContentWrapper(); + const externalSpacer: string = this.scrollbar.externalSpacer(); + const asyncDetection: 'auto' | '' = this.asyncDetection(); - // Search for external content wrapper - const contentWrapperElement: HTMLElement = this.scrollbar.nativeElement.querySelector(this.scrollbar.externalContentWrapper); + untracked(() => { + let viewportElement: HTMLElement; + let contentWrapperElement: HTMLElement; - if (viewportElement && contentWrapperElement) { - // If an external spacer selector is provided, search for it - let spacerElement: HTMLElement; - if (this.scrollbar.externalSpacer) { - spacerElement = this.scrollbar.nativeElement.querySelector(this.scrollbar.externalSpacer); - } + this.zone.runOutsideAngular(() => { + sub$ = getThrottledStream(this.contentObserver.observe(this.scrollbar.nativeElement), 100).subscribe(() => { + // Search for external viewport + viewportElement = this.scrollbar.nativeElement.querySelector(externalViewport); + + // Search for external content wrapper + contentWrapperElement = this.scrollbar.nativeElement.querySelector(externalContentWrapper); - // Initialize viewport - this.scrollbar.viewport.init(viewportElement, contentWrapperElement, spacerElement); - // Attach scrollbars - this.scrollbar.attachScrollbars(); + this.zone.run(() => { + if (!init && viewportElement && contentWrapperElement) { + // If an external spacer selector is provided, search for it + let spacerElement: HTMLElement; + if (externalSpacer) { + spacerElement = this.scrollbar.nativeElement.querySelector(externalSpacer); + } - if (!this.asyncDetection) { - this.subscription.unsubscribe(); + this.scrollbar.skipInit = false; + this.scrollbar.altViewport.set(viewportElement); + this.scrollbar.altContentWrapper.set(contentWrapperElement); + this.scrollbar.altSpacer.set(spacerElement); + } else if (!viewportElement || !contentWrapperElement) { + this.scrollbar.viewport.reset(); + } + }); + + if (asyncDetection !== 'auto') { + sub$.unsubscribe(); } - } - } else { - const viewportElement: HTMLElement = this.scrollbar.nativeElement.querySelector(this.scrollbar.externalViewport); - const contentWrapperElement: HTMLElement = this.scrollbar.nativeElement.querySelector(this.scrollbar.externalContentWrapper); + }); + }); - if (!viewportElement || !contentWrapperElement) { - this.scrollbar.viewport.nativeElement = null; - this.scrollbar.viewport.contentWrapperElement = null; - this.scrollbar.viewport.initialized.set(false); - } - } + onCleanup(() => sub$?.unsubscribe()); }); }); } - - ngOnDestroy(): void { - this.subscription?.unsubscribe(); - } } diff --git a/projects/ngx-scrollbar/src/lib/button/scrollbar-button.component.ts b/projects/ngx-scrollbar/src/lib/button/scrollbar-button.component.ts index cecef92f..4c83c189 100644 --- a/projects/ngx-scrollbar/src/lib/button/scrollbar-button.component.ts +++ b/projects/ngx-scrollbar/src/lib/button/scrollbar-button.component.ts @@ -1,13 +1,4 @@ -import { - Component, - Input, - inject, - effect, - runInInjectionContext, - OnInit, - Injector, - ChangeDetectionStrategy -} from '@angular/core'; +import { Component, effect, untracked, input, InputSignal, ChangeDetectionStrategy, } from '@angular/core'; import { Direction } from '@angular/cdk/bidi'; import { Observable, @@ -24,6 +15,29 @@ import { import { enableSelection, preventSelection, stopPropagation } from '../utils/common'; import { PointerEventsAdapter } from '../utils/pointer-events-adapter'; +type CanScrollFn = (offset: number, scrollMax?: number) => boolean; + +type ScrollStepFn = (scrollBy: number, offset: number, scrollMax: number) => number; + +// canScroll function can work for y-axis and x-axis for both LTR and RTL directions +const canScrollFunc: Record<'forward' | 'backward', CanScrollFn> = { + forward: (scrollOffset: number, scrollMax: number): boolean => scrollOffset < scrollMax, + backward: (scrollOffset: number): boolean => scrollOffset > 0 +} + +const scrollStepFunc: Record<'forward' | 'backward', ScrollStepFn> = { + forward: (scrollBy: number, offset: number) => offset + scrollBy, + backward: (scrollBy: number, offset: number) => offset - scrollBy +}; + +const horizontalScrollStepFunc: Record<'ltr' | 'rtl', Record<'forward' | 'backward', ScrollStepFn>> = { + rtl: { + forward: (scrollBy: number, offset: number, scrollMax: number) => scrollMax - offset - scrollBy, + backward: (scrollBy: number, offset: number, scrollMax: number) => scrollMax - offset + scrollBy + }, + ltr: scrollStepFunc +} + @Component({ standalone: true, selector: 'button[scrollbarButton]', @@ -31,12 +45,10 @@ import { PointerEventsAdapter } from '../utils/pointer-events-adapter'; styleUrl: './scrollbar-button.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class ScrollbarButton extends PointerEventsAdapter implements OnInit { +export class ScrollbarButton extends PointerEventsAdapter { - private readonly injector: Injector = inject(Injector); - - @Input({ required: true }) scrollbarButton: 'top' | 'bottom' | 'start' | 'end'; - @Input({ required: true }) scrollDirection: 'forward' | 'backward'; + scrollbarButton: InputSignal<'top' | 'bottom' | 'start' | 'end'> = input.required(); + scrollDirection: InputSignal<'forward' | 'backward'> = input.required(); private afterFirstClickDelay: number = 120; private firstClickDuration: number = 100; @@ -46,25 +58,6 @@ export class ScrollbarButton extends PointerEventsAdapter implements OnInit { private nextStep: (scrollBy: number, offset: number, scrollMax: number) => number; private canScroll: (offset: number, scrollMax: number) => boolean; - // canScroll function can work for y-axis and x-axis for both LTR and RTL directions - private readonly canScrollFunc: Record<'forward' | 'backward', (offset: number, scrollMax?: number) => boolean> = { - forward: (scrollOffset: number, scrollMax: number): boolean => scrollOffset < scrollMax, - backward: (scrollOffset: number): boolean => scrollOffset > 0 - } - - private scrollStepFunc: Record<'forward' | 'backward', (scrollBy: number, offset: number, scrollMax: number) => number> = { - forward: (scrollBy: number, offset: number) => offset + scrollBy, - backward: (scrollBy: number, offset: number) => offset - scrollBy - }; - - private readonly horizontalScrollStepFunc: Record<'ltr' | 'rtl', typeof this.scrollStepFunc> = { - rtl: { - forward: (scrollBy: number, offset: number, scrollMax: number) => scrollMax - offset - scrollBy, - backward: (scrollBy: number, offset: number, scrollMax: number) => scrollMax - offset + scrollBy - }, - ltr: this.scrollStepFunc - } - get pointerEvents(): Observable { const pointerDown$: Observable = fromEvent(this.nativeElement, 'pointerdown').pipe( stopPropagation(), @@ -88,22 +81,25 @@ export class ScrollbarButton extends PointerEventsAdapter implements OnInit { ); } - ngOnInit(): void { - // Get the canScroll function according to scroll direction (forward/backward) - this.canScroll = this.canScrollFunc[this.scrollDirection]; + constructor() { + effect(() => { + const scrollDirection: 'forward' | 'backward' = this.scrollDirection(); + const dir: Direction = this.cmp.direction(); + + untracked(() => { + // Get the canScroll function according to scroll direction (forward/backward) + this.canScroll = canScrollFunc[scrollDirection]; - if (this.control.axis === 'x') { - runInInjectionContext(this.injector, () => { - effect(() => { - const dir: Direction = this.cmp.direction(); + if (this.control.axis === 'x') { // Get the nextStep function according to scroll direction (forward/backward) and layout direction (LTR/RTL) - this.nextStep = this.horizontalScrollStepFunc[dir][this.scrollDirection]; - }); + this.nextStep = horizontalScrollStepFunc[dir][scrollDirection]; + } else { + // Get the nextStep function according to scroll direction (forward/backward) + this.nextStep = scrollStepFunc[scrollDirection]; + } }); - } else { - // Get the nextStep function according to scroll direction (forward/backward) - this.nextStep = this.scrollStepFunc[this.scrollDirection]; - } + }); + super(); } private firstScrollStep(): Observable { diff --git a/projects/ngx-scrollbar/src/lib/ng-scrollbar-core.ts b/projects/ngx-scrollbar/src/lib/ng-scrollbar-core.ts index e9087227..9202a123 100644 --- a/projects/ngx-scrollbar/src/lib/ng-scrollbar-core.ts +++ b/projects/ngx-scrollbar/src/lib/ng-scrollbar-core.ts @@ -1,31 +1,28 @@ import { Directive, - Input, - Output, inject, signal, effect, + output, computed, + untracked, numberAttribute, booleanAttribute, - runInInjectionContext, input, - EventEmitter, - OnInit, - AfterViewInit, - ElementRef, NgZone, Signal, + ElementRef, InputSignal, - Injector, WritableSignal, + OutputEmitterRef, EffectCleanupRegisterFn, - InputSignalWithTransform + InputSignalWithTransform, } from '@angular/core'; import { Platform } from '@angular/cdk/platform'; import { Direction, Directionality } from '@angular/cdk/bidi'; +import { SharedResizeObserver } from '@angular/cdk/observers/private'; import { toSignal } from '@angular/core/rxjs-interop'; -import { Subscription, map, tap } from 'rxjs'; +import { combineLatest, Subscription } from 'rxjs'; import { SmoothScrollElement, SmoothScrollManager, @@ -34,16 +31,16 @@ import { } from 'ngx-scrollbar/smooth-scroll'; import { Scrollbars } from './scrollbars/scrollbars'; import { _NgScrollbar, NG_SCROLLBAR } from './utils/scrollbar-base'; -import { resizeObserver, ViewportAdapter } from './viewport'; -import { ScrollbarDragging, ViewportBoundaries } from './utils/common'; +import { ViewportAdapter } from './viewport'; +import { ElementDimension, ScrollbarDragging, getThrottledStream } from './utils/common'; import { + NG_SCROLLBAR_OPTIONS, + NgScrollbarOptions, ScrollbarAppearance, - ScrollbarPosition, ScrollbarOrientation, + ScrollbarPosition, ScrollbarUpdateReason, - ScrollbarVisibility, - NgScrollbarOptions, - NG_SCROLLBAR_OPTIONS + ScrollbarVisibility } from './ng-scrollbar.model'; interface ViewportState { @@ -62,36 +59,48 @@ interface ViewportState { '[attr.isHorizontallyScrollable]': 'isHorizontallyScrollable()', '[attr.mobile]': 'isMobile', '[attr.dir]': 'direction()', - '[attr.position]': 'position', + '[attr.position]': 'position()', '[attr.dragging]': 'dragging()', - '[attr.appearance]': 'appearance', + '[attr.appearance]': 'appearance()', '[attr.visibility]': 'visibility()', '[attr.orientation]': 'orientation()', - '[attr.disableInteraction]': 'disableInteraction()' + '[attr.disableInteraction]': 'disableInteraction()', + '[style.--content-height]': 'contentDimension().height', + '[style.--content-width]': 'contentDimension().width', + '[style.--viewport-height]': 'viewportDimension().height', + '[style.--viewport-width]': 'viewportDimension().width' }, - providers: [{ provide: NG_SCROLLBAR, useExisting: NgScrollbarCore }] + providers: [ + { provide: NG_SCROLLBAR, useExisting: NgScrollbarCore } + ] }) -export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterViewInit { +export abstract class NgScrollbarCore implements _NgScrollbar { /** Global options */ private readonly options: NgScrollbarOptions = inject(NG_SCROLLBAR_OPTIONS); + private readonly sharedResizeObserver: SharedResizeObserver = inject(SharedResizeObserver); + private readonly zone: NgZone = inject(NgZone); + private readonly platform: Platform = inject(Platform); - private readonly injector: Injector = inject(Injector); /** A flag that indicates if the platform is mobile */ readonly isMobile: boolean = this.platform.IOS || this.platform.ANDROID; - dir: Directionality = inject(Directionality); - smoothScroll: SmoothScrollManager = inject(SmoothScrollManager); + readonly dir: Directionality = inject(Directionality); - nativeElement: HTMLElement = inject(ElementRef).nativeElement; + readonly smoothScroll: SmoothScrollManager = inject(SmoothScrollManager); + + /** Viewport adapter instance */ + readonly viewport: ViewportAdapter = inject(ViewportAdapter, { self: true }); + + readonly nativeElement: HTMLElement = inject(ElementRef).nativeElement; /** * Indicates if the direction is 'ltr' or 'rtl' */ - direction: Signal; + direction: Signal = toSignal(this.dir.change, { initialValue: this.dir.value }); /** * Indicates when scrollbar thumb is being dragged @@ -141,14 +150,13 @@ export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterView transform: booleanAttribute }); - viewportDimension: WritableSignal = signal({ - contentHeight: 0, - contentWidth: 0, - offsetHeight: 0, - offsetWidth: 0 - }); + /** Viewport dimension */ + viewportDimension: WritableSignal = signal({ width: 0, height: 0 }); - state: Signal = computed(() => { + /** Content dimension */ + contentDimension: WritableSignal = signal({ width: 0, height: 0 }); + + private state: Signal = computed(() => { let verticalUsed: boolean = false; let horizontalUsed: boolean = false; let isVerticallyScrollable: boolean = false; @@ -156,16 +164,18 @@ export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterView const orientation: ScrollbarOrientation = this.orientation(); const visibility: ScrollbarVisibility = this.visibility(); - const viewport: ViewportBoundaries = this.viewportDimension(); + + const viewportDimensions: ElementDimension = this.viewportDimension(); + const contentDimensions: ElementDimension = this.contentDimension(); // Check if vertical scrollbar should be displayed if (orientation === 'auto' || orientation === 'vertical') { - isVerticallyScrollable = viewport.contentHeight > viewport.offsetHeight; + isVerticallyScrollable = contentDimensions.height > viewportDimensions.height; verticalUsed = visibility === 'visible' || isVerticallyScrollable; } // Check if horizontal scrollbar should be displayed if (orientation === 'auto' || orientation === 'horizontal') { - isHorizontallyScrollable = viewport.contentWidth > viewport.offsetWidth; + isHorizontallyScrollable = contentDimensions.width > viewportDimensions.width; horizontalUsed = visibility === 'visible' || isHorizontallyScrollable; } @@ -183,7 +193,9 @@ export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterView horizontalUsed: Signal = computed(() => this.state().horizontalUsed); /** Scroll duration when the scroll track is clicked */ - @Input({ transform: numberAttribute }) trackScrollDuration: number = this.options.trackScrollDuration; + trackScrollDuration: InputSignalWithTransform = input(this.options.trackScrollDuration, { + transform: numberAttribute + }); /** * Sets the appearance of the scrollbar, there are 2 options: @@ -191,7 +203,7 @@ export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterView * - `native` (default) scrollbar space will be reserved just like with native scrollbar. * - `compact` scrollbar doesn't reserve any space, they are placed over the viewport. */ - @Input() appearance: ScrollbarAppearance = this.options.appearance; + appearance: InputSignal = input(this.options.appearance); /** * Sets the position of each scrollbar, there are 4 options: * @@ -200,94 +212,81 @@ export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterView * - `invertX` Inverts Horizontal scrollbar position * - `invertAll` Inverts both scrollbar positions */ - @Input() position: ScrollbarPosition = this.options.position; + position: InputSignal = input(this.options.position); /** A class forwarded to the scrollbar track element */ - @Input() trackClass: string = this.options.trackClass; + trackClass: InputSignal = input(this.options.trackClass); /** A class forwarded to the scrollbar thumb element */ - @Input() thumbClass: string = this.options.thumbClass; + thumbClass: InputSignal = input(this.options.thumbClass); /** A class forwarded to the scrollbar button element */ - @Input() buttonClass: string = this.options.thumbClass; + buttonClass: InputSignal = input(this.options.thumbClass); /** Steam that emits when scrollbar is initialized */ - @Output() afterInit: EventEmitter = new EventEmitter(); + afterInit: OutputEmitterRef = output(); /** Steam that emits when scrollbar is updated */ - @Output() afterUpdate: EventEmitter = new EventEmitter(); - - /** Resize observer subscription */ - private sizeChangeSub: Subscription; - - /** Viewport adapter instance */ - viewport: ViewportAdapter = new ViewportAdapter(); + afterUpdate: OutputEmitterRef = output(); /** The scrollbars component instance used for testing purpose */ - abstract _scrollbars: Scrollbars; - - ngOnInit(): void { - runInInjectionContext(this.injector, () => { - // The direction signal cannot be initialized in the constructor - // Because it initially returns 'ltr' even if dir.value is 'rtl`, the map function here is crucial - this.direction = toSignal(this.dir.change.pipe(map(() => this.dir.value)), { initialValue: this.dir.value }); - - effect((onCleanup: EffectCleanupRegisterFn) => { - // Check whether sensor should be enabled - if (this.disableSensor()) { - // If sensor is disabled update manually - this.sizeChangeSub?.unsubscribe(); - } else { - if (this.platform.isBrowser && this.viewport.initialized()) { - this.sizeChangeSub?.unsubscribe(); - + abstract _scrollbars: Signal; + + protected constructor() { + let resizeSub$: Subscription; + let hasInitialized: boolean; + + effect((onCleanup: EffectCleanupRegisterFn) => { + const disableSensor: boolean = this.disableSensor(); + const throttleDuration: number = this.sensorThrottleTime(); + const viewportInit: boolean = this.viewport.initialized(); + + untracked(() => { + if (viewportInit) { + // If resize sensor is disabled, update manually the first time + if (disableSensor) { + requestAnimationFrame(() => this.update(ScrollbarUpdateReason.AfterInit)); + } else { + // Observe size changes for viewport and content wrapper this.zone.runOutsideAngular(() => { - this.sizeChangeSub = resizeObserver({ - element: this.viewport.nativeElement, - contentWrapper: this.viewport.contentWrapperElement, - throttleDuration: this.sensorThrottleTime() - }).pipe( - tap((reason: ScrollbarUpdateReason) => this.update(reason)) - ).subscribe(); + resizeSub$ = getThrottledStream( + combineLatest([ + this.sharedResizeObserver.observe(this.viewport.nativeElement), + this.sharedResizeObserver.observe(this.viewport.contentWrapperElement) + ]), + throttleDuration + ).subscribe(() => { + // After deep investigation, it appears that setting the dimension directly from the element properties + // is much faster than to set them from resize callback values + this.zone.run(() => { + this.updateDimensions(); + + if (hasInitialized) { + this.afterUpdate.emit(); + } else { + this.afterInit.emit(); + } + hasInitialized = true; + }); + }); }); } } - onCleanup(() => this.sizeChangeSub?.unsubscribe()); + onCleanup(() => resizeSub$?.unsubscribe()); }); }); } - ngAfterViewInit(): void { - // If sensor is disabled, update to evaluate the state - if (this.platform.isBrowser && this.disableSensor()) { - // In case of 3rd party library, need to wait for content to be rendered - requestAnimationFrame(() => { - this.update(ScrollbarUpdateReason.AfterInit); - }); - } - } - /** - * Update local state and the internal scrollbar controls + * Manual update */ update(reason?: ScrollbarUpdateReason): void { - this.updateCSSVariables(); - - this.zone.run(() => { - this.viewportDimension.set({ - contentHeight: this.viewport.contentHeight, - contentWidth: this.viewport.contentWidth, - offsetHeight: this.viewport.offsetHeight, - offsetWidth: this.viewport.offsetWidth - }); + this.updateDimensions(); - // After the upgrade to Angular 18, the effect functions in the inner directives are executed after "afterInit" is emitted, - // causing the tests to fail. A tiny delay is needed before emitting to the output as a workaround. - if (reason === ScrollbarUpdateReason.AfterInit) { - this.afterInit.emit(); - } else { - this.afterUpdate.emit(); - } - }); + if (reason === ScrollbarUpdateReason.AfterInit) { + this.afterInit.emit(); + } else { + this.afterUpdate.emit(); + } } /** @@ -304,13 +303,8 @@ export abstract class NgScrollbarCore implements _NgScrollbar, OnInit, AfterView return this.smoothScroll.scrollToElement(this.viewport.nativeElement, target, options); } - /** - * Update Essential CSS variables - */ - private updateCSSVariables(): void { - this.nativeElement.style.setProperty('--content-height', `${ this.viewport.contentHeight }`); - this.nativeElement.style.setProperty('--content-width', `${ this.viewport.contentWidth }`); - this.nativeElement.style.setProperty('--viewport-height', `${ this.viewport.offsetHeight }`); - this.nativeElement.style.setProperty('--viewport-width', `${ this.viewport.offsetWidth }`); + private updateDimensions(): void { + this.viewportDimension.set({ width: this.viewport.offsetWidth, height: this.viewport.offsetHeight }); + this.contentDimension.set({ width: this.viewport.contentWidth, height: this.viewport.contentHeight }); } } diff --git a/projects/ngx-scrollbar/src/lib/ng-scrollbar-ext.ts b/projects/ngx-scrollbar/src/lib/ng-scrollbar-ext.ts index 4966bfa9..7f51c0f4 100644 --- a/projects/ngx-scrollbar/src/lib/ng-scrollbar-ext.ts +++ b/projects/ngx-scrollbar/src/lib/ng-scrollbar-ext.ts @@ -1,19 +1,26 @@ import { Component, Input, - ContentChild, inject, + signal, + effect, + computed, + untracked, createComponent, booleanAttribute, - OnInit, - OnDestroy, + input, + contentChild, + Signal, + InputSignal, + WritableSignal, Injector, Renderer2, ComponentRef, ApplicationRef, - ChangeDetectionStrategy + ChangeDetectionStrategy, + EffectCleanupRegisterFn } from '@angular/core'; -import { ScrollViewport } from './viewport'; +import { ScrollViewport, ViewportAdapter } from './viewport'; import { NgScrollbar } from './ng-scrollbar'; import { NgScrollbarCore } from './ng-scrollbar-core'; import { NG_SCROLLBAR } from './utils/scrollbar-base'; @@ -35,10 +42,11 @@ import { Scrollbars } from './scrollbars/scrollbars'; }, providers: [ { provide: NG_SCROLLBAR, useExisting: NgScrollbarExt }, - { provide: NgScrollbarCore, useExisting: NgScrollbar } + { provide: NgScrollbarCore, useExisting: NgScrollbar }, + ViewportAdapter ], }) -export class NgScrollbarExt extends NgScrollbarCore implements OnInit, OnDestroy { +export class NgScrollbarExt extends NgScrollbarCore { private readonly renderer: Renderer2 = inject(Renderer2); @@ -46,17 +54,17 @@ export class NgScrollbarExt extends NgScrollbarCore implements OnInit, OnDestroy _scrollbarsRef: ComponentRef; - _scrollbars: Scrollbars; + _scrollbars: WritableSignal = signal(null); /** * Selector used to query the viewport element. */ - @Input() externalViewport: string; + externalViewport: InputSignal = input(); /** * Selector used to query the content wrapper element. */ - @Input() externalContentWrapper: string; + externalContentWrapper: InputSignal = input(); /** * Selector used to query the spacer element (virtual scroll integration). @@ -64,7 +72,58 @@ export class NgScrollbarExt extends NgScrollbarCore implements OnInit, OnDestroy * a spacer element is typically created to match the real size of the content. * The scrollbar will use the size of this spacer element for calculations instead of the content wrapper size. */ - @Input() externalSpacer: string; + externalSpacer: InputSignal = input(); + + // At the time being, InputSignal value cannot be overridden programmatically from another directive, + altViewport: WritableSignal = signal(null); + altContentWrapper: WritableSignal = signal(null); + altSpacer: WritableSignal = signal(null); + + viewportElement: Signal = computed(() => { + if (this.customViewport()) { + return this.customViewport().nativeElement; + } + if (this.altViewport()) { + return this.altViewport(); + } + // If viewport selector was defined, query the element + const selector: string = this.externalViewport(); + return selector ? this.nativeElement.querySelector(selector) : null; + }); + + viewportError: Signal = computed(() => { + return !this.viewportElement() + ? `[NgScrollbar]: Could not find the viewport element for the provided selector "${ this.externalViewport() }"` + : null; + }); + + contentWrapperElement: Signal = computed(() => { + if (this.altContentWrapper()) { + return this.altContentWrapper(); + } + const selector: string = this.externalContentWrapper(); + return selector ? this.nativeElement.querySelector(selector) : null; + }); + + contentWrapperError: Signal = computed(() => { + return !this.contentWrapperElement() && this.externalContentWrapper() + ? `[NgScrollbar]: Content wrapper element not found for the provided selector "${ this.externalContentWrapper() }"` + : null; + }); + + spacerElement: Signal = computed(() => { + if (this.altSpacer()) { + return this.altSpacer(); + } + const selector: string = this.externalSpacer(); + return selector ? this.nativeElement.querySelector(selector) : null + }); + + spacerError: Signal = computed(() => { + return !this.spacerElement() && this.externalSpacer() + ? `[NgScrollbar]: Spacer element not found for the provided selector "${ this.externalSpacer() }"` + : null; + }); /** * Skip initializing the viewport and the scrollbar @@ -75,77 +134,55 @@ export class NgScrollbarExt extends NgScrollbarCore implements OnInit, OnDestroy /** * Reference to the external viewport directive if used */ - @ContentChild(ScrollViewport, { static: true }) customViewport: ScrollViewport; - - override ngOnInit(): void { - if (!this.skipInit) { - this.detectExternalSelectors(); - } - super.ngOnInit(); - } - - ngOnDestroy(): void { - // Destroy the attached scrollbars to avoid memory leak - this._scrollbarsRef?.hostView.destroy(); - } - - private detectExternalSelectors(): void { - let viewportElement: HTMLElement; - if (this.customViewport) { - viewportElement = this.customViewport.nativeElement; - } else { - // If viewport selector was defined, query the element - if (this.externalViewport) { - viewportElement = this.nativeElement.querySelector(this.externalViewport); - } - if (!viewportElement) { - console.error(`[NgScrollbar]: Could not find the viewport element for the provided selector "${ this.externalViewport }"`); - return; - } - } - - // If an external spacer selector is provided, attempt to query for it - let spacerElement: HTMLElement; - if (this.externalSpacer) { - spacerElement = this.nativeElement.querySelector(this.externalSpacer); - if (!spacerElement) { - console.error(`[NgScrollbar]: Spacer element not found for the provided selector "${ this.externalSpacer }"`); - return; - } - } - - // If an external content wrapper selector is provided, attempt to query for it - let contentWrapperElement: HTMLElement; - if (this.externalContentWrapper && !this.skipInit) { - contentWrapperElement = this.nativeElement.querySelector(this.externalContentWrapper); - if (!contentWrapperElement) { - console.error(`[NgScrollbar]: Content wrapper element not found for the provided selector "${ this.externalContentWrapper }"`); - return; - } - } - - // Make sure viewport element is defined to proceed - if (viewportElement) { - // If no external spacer or content wrapper is provided, create a content wrapper element - if (!this.externalSpacer && !this.externalContentWrapper) { - contentWrapperElement = this.renderer.createElement('div'); - - // Move all content of the viewport into the content wrapper - const childNodes: ChildNode[] = Array.from(viewportElement.childNodes); - childNodes.forEach((node: ChildNode) => this.renderer.appendChild(contentWrapperElement, node)); - - // Append the content wrapper to the viewport - this.renderer.appendChild(viewportElement, contentWrapperElement); - } - - // Make sure content wrapper element is defined to proceed - if (contentWrapperElement) { - // Initialize viewport - this.viewport.init(viewportElement, contentWrapperElement, spacerElement); - // Attach scrollbars - this.attachScrollbars(); - } - } + customViewport: Signal = contentChild(ScrollViewport, { descendants: true }); + + constructor() { + effect((onCleanup: EffectCleanupRegisterFn) => { + const viewportElement: HTMLElement = this.viewportElement(); + const spacerElement: HTMLElement = this.spacerElement(); + let contentWrapperElement: HTMLElement = this.contentWrapperElement(); + + const viewportError: string = this.viewportError(); + const contentWrapperError: string = this.contentWrapperError(); + const spacerError: string = this.spacerError(); + + untracked(() => { + if (!this.skipInit) { + const error: string = viewportError || contentWrapperError || spacerError; + if (error) { + console.error(error); + } else { + // If no external spacer or content wrapper is provided, create a content wrapper element + if (!spacerElement && !contentWrapperElement) { + contentWrapperElement = this.renderer.createElement('div'); + + // Move all content of the viewport into the content wrapper + const childNodes: ChildNode[] = Array.from(viewportElement.childNodes); + childNodes.forEach((node: ChildNode) => this.renderer.appendChild(contentWrapperElement, node)); + + // Append the content wrapper to the viewport + this.renderer.appendChild(viewportElement, contentWrapperElement); + } + + // Make sure content wrapper element is defined to proceed + if (contentWrapperElement) { + // Initialize viewport + this.viewport.init(viewportElement, contentWrapperElement, spacerElement); + // Attach scrollbars + this.attachScrollbars(); + } + } + } + + onCleanup(() => { + if (this._scrollbarsRef) { + this.appRef.detachView(this._scrollbarsRef.hostView); + this._scrollbarsRef.destroy(); + } + }) + }); + }); + super(); } attachScrollbars(): void { @@ -155,10 +192,10 @@ export class NgScrollbarExt extends NgScrollbarCore implements OnInit, OnDestroy elementInjector: Injector.create({ providers: [{ provide: NG_SCROLLBAR, useValue: this }] }) }); // Attach scrollbar to the content wrapper - this.viewport.contentWrapperElement.appendChild(this._scrollbarsRef.location.nativeElement); + this.renderer.appendChild(this.viewport.contentWrapperElement, this._scrollbarsRef.location.nativeElement) // Attach the host view of the component to the main change detection tree, so that its lifecycle hooks run. this.appRef.attachView(this._scrollbarsRef.hostView); // Set the scrollbars instance - this._scrollbars = this._scrollbarsRef.instance; + this._scrollbars.set(this._scrollbarsRef.instance); } } diff --git a/projects/ngx-scrollbar/src/lib/ng-scrollbar.model.ts b/projects/ngx-scrollbar/src/lib/ng-scrollbar.model.ts index c0209c5a..64ebb785 100644 --- a/projects/ngx-scrollbar/src/lib/ng-scrollbar.model.ts +++ b/projects/ngx-scrollbar/src/lib/ng-scrollbar.model.ts @@ -76,3 +76,7 @@ export interface NgScrollbarOptions { /** A flag used to activate hover effect on the offset area around the scrollbar */ hoverOffset?: boolean; } + +export function filterResizeEntries(entries: ResizeObserverEntry[], target: Element): DOMRectReadOnly { + return entries.filter((entry: ResizeObserverEntry) => entry.target === target)[0]?.contentRect; +} diff --git a/projects/ngx-scrollbar/src/lib/ng-scrollbar.module.ts b/projects/ngx-scrollbar/src/lib/ng-scrollbar.module.ts index c6ce6665..02969bee 100644 --- a/projects/ngx-scrollbar/src/lib/ng-scrollbar.module.ts +++ b/projects/ngx-scrollbar/src/lib/ng-scrollbar.module.ts @@ -5,19 +5,22 @@ import { NgScrollbarExt } from './ng-scrollbar-ext'; import { NG_SCROLLBAR_OPTIONS, NG_SCROLLBAR_POLYFILL, NgScrollbarOptions } from './ng-scrollbar.model'; import { AsyncDetection } from './async-detection'; import { defaultOptions } from './ng-scrollbar.default'; +import { SyncSpacer } from './sync-spacer'; @NgModule({ imports: [ NgScrollbar, ScrollViewport, NgScrollbarExt, - AsyncDetection + AsyncDetection, + SyncSpacer ], exports: [ NgScrollbar, ScrollViewport, NgScrollbarExt, - AsyncDetection + AsyncDetection, + SyncSpacer ] }) export class NgScrollbarModule { diff --git a/projects/ngx-scrollbar/src/lib/ng-scrollbar.scss b/projects/ngx-scrollbar/src/lib/ng-scrollbar.scss index a05c7a47..5ee8229e 100644 --- a/projects/ngx-scrollbar/src/lib/ng-scrollbar.scss +++ b/projects/ngx-scrollbar/src/lib/ng-scrollbar.scss @@ -3,6 +3,7 @@ @use "styles/position" as position; @use "styles/viewport" as viewport; @use "styles/content" as content; +@use "styles/spacer" as spacer; @use "styles/hide-native-scrollbars" as nativeScrollbars; @use "styles/reached-events" as reachedEvents; @use "styles/selectors" as selector; @@ -110,6 +111,7 @@ :host { @include viewport.SetViewportStyles(); @include content.SetContentStyles(); + @include spacer.SetSpacerStyles(); @include nativeScrollbars.HideNativeScrollbars(); @include position.SetPositionStuff(); @@ -146,16 +148,6 @@ --_viewport-user-select: none; } - @include selector.HorizontalUsed { - // Calculate the horizontal thumb size - --thumb-x-length: max(calc(var(--viewport-width) * var(--track-x-length) / var(--content-width)), var(--INTERNAL-scrollbar-thumb-min-size)); - } - - @include selector.VerticalUsed { - // Calculate the vertical thumb size - --thumb-y-length: max(calc(var(--viewport-height) * var(--track-y-length) / var(--content-height)), var(--INTERNAL-scrollbar-thumb-min-size)); - } - @include selector.thumbXDragging { --_track-x-thickness: calc(var(--INTERNAL-scrollbar-hover-thickness) * 1px); --_thumb-x-color: var(var(--INTERNAL-scrollbar-thumb-min-size)); diff --git a/projects/ngx-scrollbar/src/lib/ng-scrollbar.ts b/projects/ngx-scrollbar/src/lib/ng-scrollbar.ts index f2239437..40744049 100644 --- a/projects/ngx-scrollbar/src/lib/ng-scrollbar.ts +++ b/projects/ngx-scrollbar/src/lib/ng-scrollbar.ts @@ -1,5 +1,5 @@ -import { Component, ViewChild, OnInit, ElementRef, ChangeDetectionStrategy } from '@angular/core'; -import { ScrollViewport } from './viewport'; +import { Component, effect, untracked, viewChild, Signal, ElementRef, ChangeDetectionStrategy } from '@angular/core'; +import { ViewportAdapter } from './viewport'; import { NgScrollbarCore } from './ng-scrollbar-core'; import { NG_SCROLLBAR } from './utils/scrollbar-base'; import { Scrollbars } from './scrollbars/scrollbars'; @@ -9,7 +9,6 @@ import { Scrollbars } from './scrollbars/scrollbars'; selector: 'ng-scrollbar:not([externalViewport])', exportAs: 'ngScrollbar', imports: [Scrollbars], - hostDirectives: [ScrollViewport], template: `
@@ -19,17 +18,23 @@ import { Scrollbars } from './scrollbars/scrollbars'; styleUrls: ['./ng-scrollbar.scss'], changeDetection: ChangeDetectionStrategy.OnPush, providers: [ - { provide: NG_SCROLLBAR, useExisting: NgScrollbar } + { provide: NG_SCROLLBAR, useExisting: NgScrollbar }, + ViewportAdapter ] }) -export class NgScrollbar extends NgScrollbarCore implements OnInit { +export class NgScrollbar extends NgScrollbarCore { - @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef; + contentWrapper: Signal> = viewChild.required('contentWrapper'); - @ViewChild(Scrollbars, { static: true }) _scrollbars: Scrollbars; + _scrollbars: Signal = viewChild.required(Scrollbars); - override ngOnInit(): void { - this.viewport.init(this.nativeElement, this.contentWrapper.nativeElement); - super.ngOnInit(); + constructor() { + effect(() => { + const contentWrapper: HTMLElement = this.contentWrapper().nativeElement; + untracked(() => { + this.viewport.init(this.nativeElement, contentWrapper); + }); + }); + super(); } } diff --git a/projects/ngx-scrollbar/src/lib/scrollbar/horizontal.scss b/projects/ngx-scrollbar/src/lib/scrollbar/horizontal.scss index 62dc0c67..3922f06e 100644 --- a/projects/ngx-scrollbar/src/lib/scrollbar/horizontal.scss +++ b/projects/ngx-scrollbar/src/lib/scrollbar/horizontal.scss @@ -14,11 +14,16 @@ --_scrollbar-track-right: var(--_horizontal-right); --_scrollbar-track-left: var(--_horizontal-left); + // Calculate the horizontal thumb size + --thumb-size: max(calc(var(--viewport-width) * var(--track-size) / var(--content-width)), var(--INTERNAL-scrollbar-thumb-min-size)); + // Scrollbar thumb variables --_thumb-height: 100%; - --_thumb-width: calc(var(--thumb-x-length) * 1px); + --_thumb-width: calc(var(--thumb-size) * 1px); // Calculate thumb position + --_scrollbar-x-thumb-transform-to-value: calc(var(--track-size) - var(--thumb-size)); + --_scrollbar-thumb-transform-from: 0; --_scrollbar-thumb-transform-to: calc(var(--_scrollbar-x-thumb-transform-to-value) * 1px); @@ -58,9 +63,11 @@ flex-direction: row; } -.ng-scrollbar-hover:hover { - --_track-x-thickness: var(--_scrollbar-hover-thickness-px); - --_thumb-x-color: var(--INTERNAL-scrollbar-thumb-hover-color); +.ng-scrollbar-hover { + &:hover, &:active { + --_track-x-thickness: var(--_scrollbar-hover-thickness-px); + --_thumb-x-color: var(--INTERNAL-scrollbar-thumb-hover-color); + } } .ng-scrollbar-thumb { diff --git a/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar-adapter.ts b/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar-adapter.ts index 8280c7a5..14d3094d 100644 --- a/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar-adapter.ts +++ b/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar-adapter.ts @@ -1,4 +1,4 @@ -import { Directive, inject, InjectionToken } from '@angular/core'; +import { Directive, inject, signal, WritableSignal, InjectionToken } from '@angular/core'; import { Observable } from 'rxjs'; import { NG_SCROLLBAR, _NgScrollbar } from '../utils/scrollbar-base'; @@ -7,7 +7,11 @@ export const SCROLLBAR_CONTROL: InjectionToken = new Injection @Directive() export abstract class ScrollbarAdapter { - abstract readonly clientRectProperty: 'left' | 'top'; + trackSize: WritableSignal = signal(0); + + abstract readonly rectOffsetProperty: 'left' | 'top'; + + abstract readonly rectSizeProperty: 'width' | 'height'; abstract readonly sizeProperty: 'offsetWidth' | 'offsetHeight'; diff --git a/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar.ts b/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar.ts index b8ca462b..1ccb3bae 100644 --- a/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar.ts +++ b/projects/ngx-scrollbar/src/lib/scrollbar/scrollbar.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; +import { Component, effect, inject, ChangeDetectionStrategy } from '@angular/core'; import { Observable } from 'rxjs'; import { fromPromise } from 'rxjs/internal/observable/innerFrom'; import { TrackXDirective, TrackYDirective } from '../track/track'; @@ -15,14 +15,14 @@ import { ScrollbarManager } from '../utils/scrollbar-manager'; [class.ng-scrollbar-hover]="cmp.hoverOffset()">
-
-
+
+
@if (cmp.buttons()) { - - } @@ -34,11 +34,16 @@ import { ScrollbarManager } from '../utils/scrollbar-manager'; providers: [ { provide: SCROLLBAR_CONTROL, useExisting: ScrollbarY } ], + host: { + '[style.--track-size]': 'trackSize()' + }, changeDetection: ChangeDetectionStrategy.OnPush }) export class ScrollbarY extends ScrollbarAdapter { - readonly clientRectProperty: 'left' | 'top' = 'top'; + readonly rectOffsetProperty: 'left' | 'top' = 'top'; + + readonly rectSizeProperty: 'width' | 'height' = 'height'; readonly sizeProperty: 'offsetWidth' | 'offsetHeight' = 'offsetHeight'; @@ -68,20 +73,19 @@ export class ScrollbarY extends ScrollbarAdapter { @Component({ standalone: true, selector: 'scrollbar-x', - host: { '[attr.dir]': 'cmp.direction()' }, template: `
-
-
+
+
@if (cmp.buttons()) { - - } @@ -93,13 +97,19 @@ export class ScrollbarY extends ScrollbarAdapter { providers: [ { provide: SCROLLBAR_CONTROL, useExisting: ScrollbarX } ], + host: { + '[attr.dir]': 'cmp.direction()', + '[style.--track-size]': 'trackSize()' + }, changeDetection: ChangeDetectionStrategy.OnPush }) export class ScrollbarX extends ScrollbarAdapter { readonly manager: ScrollbarManager = inject(ScrollbarManager); - readonly clientRectProperty: 'left' | 'top' = 'left'; + readonly rectOffsetProperty: 'left' | 'top' = 'left'; + + readonly rectSizeProperty: 'width' | 'height' = 'width'; readonly sizeProperty: 'offsetWidth' | 'offsetHeight' = 'offsetWidth'; diff --git a/projects/ngx-scrollbar/src/lib/scrollbar/vertical.scss b/projects/ngx-scrollbar/src/lib/scrollbar/vertical.scss index 3fec1278..6533bc80 100644 --- a/projects/ngx-scrollbar/src/lib/scrollbar/vertical.scss +++ b/projects/ngx-scrollbar/src/lib/scrollbar/vertical.scss @@ -12,11 +12,16 @@ --_scrollbar-track-right: var(--_vertical-right); --_scrollbar-track-left: var(--_vertical-left); + // Calculate the vertical thumb size + --thumb-size: max(calc(var(--viewport-height) * var(--track-size) / var(--content-height)), var(--INTERNAL-scrollbar-thumb-min-size)); + // Scrollbar thumb variables - --_thumb-height: calc(var(--thumb-y-length) * 1px); + --_thumb-height: calc(var(--thumb-size) * 1px); --_thumb-width: 100%; // Calculate thumb position + --_scrollbar-y-thumb-transform-to-value: calc(var(--track-size) - var(--thumb-size)); + --_scrollbar-thumb-transform-from: 0 0; --_scrollbar-thumb-transform-to: 0 calc(var(--_scrollbar-y-thumb-transform-to-value) * 1px); } @@ -26,9 +31,11 @@ flex-direction: column; } -.ng-scrollbar-hover:hover { - --_track-y-thickness: var(--_scrollbar-hover-thickness-px); - --_thumb-y-color: var(--INTERNAL-scrollbar-thumb-hover-color); +.ng-scrollbar-hover { + &:hover, &:active { + --_track-y-thickness: var(--_scrollbar-hover-thickness-px); + --_thumb-y-color: var(--INTERNAL-scrollbar-thumb-hover-color); + } } .ng-scrollbar-thumb { diff --git a/projects/ngx-scrollbar/src/lib/styles/scroll.scss b/projects/ngx-scrollbar/src/lib/styles/scroll.scss index d860af5c..50ea55b3 100644 --- a/projects/ngx-scrollbar/src/lib/styles/scroll.scss +++ b/projects/ngx-scrollbar/src/lib/styles/scroll.scss @@ -14,16 +14,12 @@ --_timeline-scope: --scrollerY; --_animation-timeline-y: --scrollerY; --_viewport_scroll-timeline: --scrollerY y; - - --_scrollbar-y-thumb-transform-to-value: calc(var(--track-y-length) - var(--thumb-y-length)); } @include selector.HorizontalUsed { --_timeline-scope: --scrollerX; --_animation-timeline-x: --scrollerX; --_viewport_scroll-timeline: --scrollerX x; - - --_scrollbar-x-thumb-transform-to-value: calc(var(--track-x-length) - var(--thumb-x-length)); } @include selector.BothUsed { diff --git a/projects/ngx-scrollbar/src/lib/styles/selectors.scss b/projects/ngx-scrollbar/src/lib/styles/selectors.scss index 524b41f2..e09c4f9b 100644 --- a/projects/ngx-scrollbar/src/lib/styles/selectors.scss +++ b/projects/ngx-scrollbar/src/lib/styles/selectors.scss @@ -26,6 +26,14 @@ } } +@mixin Spacer { + @include Viewport { + >.ng-scroll-spacer { + @content; + } + } +} + @mixin BothUsed { &[verticalUsed="true"][horizontalUsed="true"] { @content; diff --git a/projects/ngx-scrollbar/src/lib/styles/spacer.scss b/projects/ngx-scrollbar/src/lib/styles/spacer.scss new file mode 100644 index 00000000..deec1a66 --- /dev/null +++ b/projects/ngx-scrollbar/src/lib/styles/spacer.scss @@ -0,0 +1,20 @@ +@use './selectors' as selector; + +@mixin SetSpacerStyles() { + --_spacer-width: var(--spacer-width); + --_spacer-height: var(--spacer-height); + + &[appearance="native"] { + --_spacer-width: calc(var(--spacer-width) + var(--_scrollbar-thickness)); + --_spacer-height: calc(var(--spacer-height) + var(--_scrollbar-thickness)); + } + + @include selector.Spacer { + // Set relative position on the spacer element to enable the functionality of sticky for the scrollbars + position: relative; + + // In virtual scroll, in vertical mode, the spacer element width should match the content wrapper width and same goes in horizontal mode. + width: calc(var(--_spacer-width) * 1px); + height: calc(var(--_spacer-height) * 1px); + } +} diff --git a/projects/ngx-scrollbar/src/lib/sync-spacer.ts b/projects/ngx-scrollbar/src/lib/sync-spacer.ts new file mode 100644 index 00000000..a6e93aa2 --- /dev/null +++ b/projects/ngx-scrollbar/src/lib/sync-spacer.ts @@ -0,0 +1,70 @@ +import { + Directive, + inject, + signal, + effect, + untracked, + NgZone, + WritableSignal, + EffectCleanupRegisterFn +} from '@angular/core'; +import { SharedResizeObserver } from '@angular/cdk/observers/private'; +import { Subscription, map } from 'rxjs'; +import { NgScrollbarExt } from './ng-scrollbar-ext'; +import { filterResizeEntries } from './ng-scrollbar.model'; +import { ElementDimension, getThrottledStream } from './utils/common'; + +@Directive({ + standalone: true, + selector: 'ng-scrollbar[externalViewport][syncSpacer]', + host: { + '[style.--spacer-width]': 'spacerDimension().width', + '[style.--spacer-height]': 'spacerDimension().height' + } +}) +export class SyncSpacer { + + private readonly sharedResizeObserver: SharedResizeObserver = inject(SharedResizeObserver); + + private readonly scrollbar: NgScrollbarExt = inject(NgScrollbarExt, { self: true }); + + private readonly zone: NgZone = inject(NgZone); + + /** + * A signal used to sync spacer dimension when content dimension changes + */ + spacerDimension: WritableSignal = signal({}); + + constructor() { + let sub$: Subscription; + + effect((onCleanup: EffectCleanupRegisterFn) => { + const spacerElement: HTMLElement = this.scrollbar.spacerElement(); + const contentWrapperElement: HTMLElement = this.scrollbar.contentWrapperElement(); + const throttleDuration: number = this.scrollbar.sensorThrottleTime(); + const disableSensor: boolean = this.scrollbar.disableSensor(); + + untracked(() => { + if (!disableSensor && contentWrapperElement && spacerElement) { + // Sync spacer dimension with content wrapper dimensions to allow both scrollbars to be displayed + this.zone.runOutsideAngular(() => { + sub$ = getThrottledStream(this.sharedResizeObserver.observe(contentWrapperElement), throttleDuration).pipe( + map((entries: ResizeObserverEntry[]) => filterResizeEntries(entries, contentWrapperElement)), + ).subscribe(() => { + this.zone.run(() => { + // Use animation frame to avoid "ResizeObserver loop completed with undelivered notifications." error + requestAnimationFrame(() => { + this.spacerDimension.set({ + width: contentWrapperElement.offsetWidth, + height: contentWrapperElement.offsetHeight + }); + }); + }); + }); + }); + } + onCleanup(() => sub$?.unsubscribe()) + }); + }); + } +} diff --git a/projects/ngx-scrollbar/src/lib/tests/appearance.spec.ts b/projects/ngx-scrollbar/src/lib/tests/appearance.spec.ts index 3c172430..5d4981f3 100644 --- a/projects/ngx-scrollbar/src/lib/tests/appearance.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/appearance.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { Directionality } from '@angular/cdk/bidi'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { NgScrollbar } from 'ngx-scrollbar'; import { setDimensions } from './common-test.'; @@ -24,6 +25,7 @@ describe('Appearance [native / compact] styles', () => { }).compileComponents(); directionalityMock.value = 'ltr'; + directionalityMock.change.next('ltr'); fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; @@ -33,8 +35,6 @@ describe('Appearance [native / compact] styles', () => { }); it('should set appearance="native" attribute by default and 0px padding', () => { - component.ngOnInit(); - component.ngAfterViewInit(); const appearanceAttr: string = component.nativeElement.getAttribute('appearance'); const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(appearanceAttr).toBe('native'); @@ -43,9 +43,7 @@ describe('Appearance [native / compact] styles', () => { it('should set appearance="native" attribute when [appearance]="native"', () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 500, contentWidth: 500 }); - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); + fixture.componentRef.setInput('appearance', 'native'); fixture.detectChanges(); const appearanceAttr: string = component.nativeElement.getAttribute('appearance'); @@ -54,12 +52,11 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-right" and "padding-bottom" when its scrollable in both directions', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 500, contentWidth: 500 }); - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); + console.log(styles.paddingBottom) expect(styles.paddingRight).toBe(scrollbarSize); expect(styles.paddingBottom).toBe(scrollbarSize); expect(styles.paddingTop).toBe('0px'); @@ -68,10 +65,8 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-right" when its vertically scrollable', async () => { setDimensions(component, { cmpHeight: 300, contentHeight: 1000 }); - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingRight).toBe(scrollbarSize); @@ -82,10 +77,8 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-bottom" when its horizontally scrollable', async () => { setDimensions(component, { cmpHeight: 100, contentHeight: 100, cmpWidth: 300, contentWidth: 1000 }); - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingBottom).toBe(scrollbarSize); @@ -96,11 +89,9 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-top" when its horizontally scrollable and [position]="invertX"', async () => { setDimensions(component, { cmpHeight: 100, contentHeight: 100, cmpWidth: 300, contentWidth: 1000 }); - component.position = 'invertX'; - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + fixture.componentRef.setInput('position', 'invertX'); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingTop).toBe(scrollbarSize); @@ -111,11 +102,9 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-left" when its vertically scrollable and [position="invertY"]', async () => { setDimensions(component, { cmpHeight: 300, contentHeight: 1000 }); - component.position = 'invertY'; - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + fixture.componentRef.setInput('position', 'invertY'); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingLeft).toBe(scrollbarSize); @@ -126,11 +115,9 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-left" and "padding-top" when its scrollbar in both directions and [position="invertAll"]', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 400, contentWidth: 400 }); - component.position = 'invertAll'; - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + fixture.componentRef.setInput('position', 'invertAll'); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingTop).toBe(scrollbarSize); @@ -142,10 +129,9 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-left" when its vertically scrollable and [dir="rtl"]', async () => { setDimensions(component, { cmpHeight: 300, contentHeight: 1000, cmpWidth: 100, contentWidth: 100 }); directionalityMock.value = 'rtl'; - component.appearance = 'native'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + directionalityMock.change.next('rtl'); + fixture.componentRef.setInput('appearance', 'native'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingLeft).toBe(scrollbarSize); @@ -157,11 +143,10 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-right" when its vertically scrollable, [dir="rtl"] and [position]="invertY"', async () => { setDimensions(component, { cmpHeight: 300, contentHeight: 1000 }); directionalityMock.value = 'rtl'; - component.appearance = 'native'; - component.position = 'invertY'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + directionalityMock.change.next('rtl'); + fixture.componentRef.setInput('appearance', 'native'); + fixture.componentRef.setInput('position', 'invertY'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingRight).toBe(scrollbarSize); @@ -173,11 +158,10 @@ describe('Appearance [native / compact] styles', () => { it('should have "padding-right" and "padding-top" when its scrollable in both directions, [dir="rtl"] and [position]="invertAll"', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 400, contentWidth: 400 }); directionalityMock.value = 'rtl'; - component.appearance = 'native'; - component.position = 'invertAll'; - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + directionalityMock.change.next('rtl'); + fixture.componentRef.setInput('appearance', 'native'); + fixture.componentRef.setInput('position', 'invertAll'); + await firstValueFrom(outputToObservable(component.afterInit)) const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.paddingRight).toBe(scrollbarSize); diff --git a/projects/ngx-scrollbar/src/lib/tests/button.spec.ts b/projects/ngx-scrollbar/src/lib/tests/button.spec.ts index 4e856d1c..13a2c7e5 100644 --- a/projects/ngx-scrollbar/src/lib/tests/button.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/button.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/ import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; import { Directionality } from '@angular/cdk/bidi'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { NgScrollbar, NgScrollbarModule } from 'ngx-scrollbar'; import { provideSmoothScrollOptions } from 'ngx-scrollbar/smooth-scroll'; @@ -35,19 +36,20 @@ describe('Buttons', () => { }).compileComponents(); directionalityMock.value = 'ltr'; + directionalityMock.change.next('ltr'); fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; fixture.componentRef.setInput('appearance', 'compact'); + + TestBed.flushEffects(); setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 200, contentHeight: 200 }); }); it('should not display the scrollbar buttons when [buttons]="false"', async () => { fixture.componentRef.setInput('buttons', false); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const buttons: DebugElement[] = fixture.debugElement.queryAll(By.directive(ScrollbarButton)); expect(buttons.length).toBeFalsy(); @@ -55,9 +57,7 @@ describe('Buttons', () => { it('should display buttons when [buttons]="true"', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const buttons: DebugElement[] = fixture.debugElement.queryAll(By.directive(ScrollbarButton)); expect(buttons.length).toBeTruthy(); @@ -65,9 +65,7 @@ describe('Buttons', () => { it('should scroll to bottom on arrow-down button click', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const button: DebugElement = fixture.debugElement.query(By.css('button[scrollbarButton="bottom"]')); @@ -97,9 +95,7 @@ describe('Buttons', () => { it('should scroll to top on arrow-up button click', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)); TestBed.flushEffects(); await component.scrollTo({ bottom: 0, duration: 0 }); @@ -130,9 +126,7 @@ describe('Buttons', () => { it('should scroll to right on arrow-right button click', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const button: DebugElement = fixture.debugElement.query(By.css('button[scrollbarButton="end"]')); @@ -162,9 +156,7 @@ describe('Buttons', () => { it('should scroll to left on arrow-left button click', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ end: 0, duration: 0 }); @@ -196,10 +188,9 @@ describe('Buttons', () => { it('[RTL] should scroll to left on arrow-left button click', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const button: DebugElement = fixture.debugElement.query(By.css('button[scrollbarButton="end"]')); @@ -228,10 +219,9 @@ describe('Buttons', () => { it('[RTL] should scroll to right on arrow-right button click', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ end: 0, duration: 0 }); @@ -263,9 +253,7 @@ describe('Buttons', () => { it('should stop scrolling when pointer is up', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const button: DebugElement = fixture.debugElement.query(By.css('button[scrollbarButton="bottom"]')); @@ -283,9 +271,7 @@ describe('Buttons', () => { it('should stop scrolling when pointer leaves the button', async () => { fixture.componentRef.setInput('buttons', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const button: DebugElement = fixture.debugElement.query(By.css('button[scrollbarButton="bottom"]')); diff --git a/projects/ngx-scrollbar/src/lib/tests/common-test..ts b/projects/ngx-scrollbar/src/lib/tests/common-test..ts index 77529627..cbd4ea91 100644 --- a/projects/ngx-scrollbar/src/lib/tests/common-test..ts +++ b/projects/ngx-scrollbar/src/lib/tests/common-test..ts @@ -12,7 +12,6 @@ export async function afterTimeout(timeout: number): Promise { await new Promise((resolve) => setTimeout(resolve, timeout)); } - export function setDimensions(comp: NgScrollbar, d: TestDimension): void { comp.nativeElement.style.width = `${ d.cmpWidth }px`; comp.nativeElement.style.height = `${ d.cmpHeight }px`; diff --git a/projects/ngx-scrollbar/src/lib/tests/content.spec.ts b/projects/ngx-scrollbar/src/lib/tests/content.spec.ts index 5f4efa46..2fb118e6 100644 --- a/projects/ngx-scrollbar/src/lib/tests/content.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/content.spec.ts @@ -19,8 +19,6 @@ describe('Content scrollbars styles', () => { it('should have the default content styles', () => { setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 50, contentHeight: 50 }); - component.ngOnInit(); - component.ngAfterViewInit(); const styles: CSSStyleDeclaration = getComputedStyle(component.viewport.contentWrapperElement); expect(styles.display).toBe('block'); expect(styles.minWidth).toBe('100%'); diff --git a/projects/ngx-scrollbar/src/lib/tests/disable-interactions.spec.ts b/projects/ngx-scrollbar/src/lib/tests/disable-interactions.spec.ts index fda5f31a..e70ee318 100644 --- a/projects/ngx-scrollbar/src/lib/tests/disable-interactions.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/disable-interactions.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { NgScrollbar, NgScrollbarModule } from 'ngx-scrollbar'; import { firstValueFrom } from 'rxjs'; import { setDimensions } from './common-test.'; @@ -94,9 +95,7 @@ describe('disableInteraction option', () => { it('should disable interactions for track and thumb', async () => { setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); trackY = fixture.debugElement.query(By.css('scrollbar-y .ng-scrollbar-track')).injector.get(TrackAdapter); diff --git a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-async.spec.ts b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-async.spec.ts index df096e85..857fcc32 100644 --- a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-async.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-async.spec.ts @@ -1,7 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Component, ViewChild, DebugElement, ElementRef } from '@angular/core'; -import { NgScrollbarExt, NgScrollbarModule, AsyncDetection, } from 'ngx-scrollbar'; +import { outputToObservable } from '@angular/core/rxjs-interop'; +import { NgScrollbarExt, NgScrollbarModule } from 'ngx-scrollbar'; import { firstValueFrom } from 'rxjs'; import { Scrollbars } from '../scrollbars/scrollbars'; import { afterTimeout } from './common-test.'; @@ -52,13 +53,11 @@ describe('External viewport via classes [AsyncDetection]', () => { let fixture: ComponentFixture; let component: ViewportClassExampleComponent; let scrollbar: NgScrollbarExt; - let asyncDetection: AsyncDetection; beforeEach(() => { fixture = TestBed.createComponent(ViewportClassExampleComponent); component = fixture.componentInstance; scrollbar = component.scrollbar; - asyncDetection = fixture.debugElement.query(By.directive(AsyncDetection)).componentInstance; }); it('[Viewport + content wrapper classes] should initialize viewport and attach scrollbars', async () => { @@ -66,27 +65,22 @@ describe('External viewport via classes [AsyncDetection]', () => { component.externalContentWrapper = '.my-custom-content-wrapper'; fixture.detectChanges(); - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); - - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.externalViewport).toBeTruthy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeFalsy(); + expect(scrollbar.customViewport()).toBeFalsy(); + expect(scrollbar.externalViewport()).toBeTruthy(); + expect(scrollbar.externalContentWrapper()).toBeTruthy(); + expect(scrollbar.externalSpacer()).toBeFalsy(); expect(scrollbar.skipInit).toBeTruthy(); expect(scrollbar.viewport.initialized()).toBeFalsy(); - asyncDetection.ngOnInit(); - // Mock library render after the scrollbar has initialized component.library.show = true; fixture.detectChanges(); // Verify afterInit is called - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbar.afterInit)); - const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport))?.nativeElement; - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; + const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport()))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper()))?.nativeElement; // Verify the viewport expect(scrollbar.viewport.nativeElement).toBe(viewportElement); @@ -96,15 +90,15 @@ describe('External viewport via classes [AsyncDetection]', () => { expect(component.library.content.nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbar._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbar._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); @@ -115,27 +109,22 @@ describe('External viewport via classes [AsyncDetection]', () => { component.externalSpacer = '.my-custom-spacer'; fixture.detectChanges(); - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); - - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.externalViewport).toBeTruthy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeTruthy(); + expect(scrollbar.customViewport()).toBeFalsy(); + expect(scrollbar.externalViewport()).toBeTruthy(); + expect(scrollbar.externalContentWrapper()).toBeTruthy(); + expect(scrollbar.externalSpacer()).toBeTruthy(); expect(scrollbar.skipInit).toBeTruthy(); expect(scrollbar.viewport.initialized()).toBeFalsy(); - asyncDetection.ngOnInit(); - // Mock library render after the scrollbar has initialized component.library.show = true; fixture.detectChanges(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbar.afterInit)); - const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport))?.nativeElement; - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; - const spacerElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalSpacer))?.nativeElement; + const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport()))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper()))?.nativeElement; + const spacerElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalSpacer()))?.nativeElement; // Verify the viewport expect(scrollbar.viewport.nativeElement).toBe(viewportElement); @@ -145,15 +134,15 @@ describe('External viewport via classes [AsyncDetection]', () => { expect(component.library.content.nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbar._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbar._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); @@ -163,27 +152,22 @@ describe('External viewport via classes [AsyncDetection]', () => { component.asyncDetection = 'auto'; fixture.detectChanges(); - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); - - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.externalViewport).toBeTruthy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeFalsy(); + expect(scrollbar.customViewport()).toBeFalsy(); + expect(scrollbar.externalViewport()).toBeTruthy(); + expect(scrollbar.externalContentWrapper()).toBeTruthy(); + expect(scrollbar.externalSpacer()).toBeFalsy(); expect(scrollbar.skipInit).toBeTruthy(); expect(scrollbar.viewport.initialized()).toBeFalsy(); - asyncDetection.ngOnInit(); - // Mock library render after the scrollbar has initialized component.library.show = true; fixture.detectChanges(); // Verify afterInit is called - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbar.afterInit)); - const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport))?.nativeElement; - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; + const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport()))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper()))?.nativeElement; // Verify the viewport expect(scrollbar.viewport.nativeElement).toBe(viewportElement); @@ -193,10 +177,10 @@ describe('External viewport via classes [AsyncDetection]', () => { expect(component.library.content.nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbar._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbar._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); @@ -205,15 +189,15 @@ describe('External viewport via classes [AsyncDetection]', () => { // Mock library removes the content (such as dropdown) component.library.show = false; fixture.detectChanges(); - // Wait 100ms for change to take effect - await afterTimeout(100); + // Wait a bit more than 100ms for change to take effect + await afterTimeout(110); expect(scrollbar.viewport.initialized()).toBeFalse(); expect(scrollbar.viewport.nativeElement).toBeFalsy(); expect(scrollbar.viewport.contentWrapperElement).toBeFalsy(); const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); }); diff --git a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-class.spec.ts b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-class.spec.ts index 2653f06b..ae728f0c 100644 --- a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-class.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-class.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { Component, DebugElement, ElementRef, ViewChild } from '@angular/core'; +import { Component, DebugElement, ElementRef, Signal, viewChild, ViewChild } from '@angular/core'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { NgScrollbarExt, NgScrollbarModule, } from 'ngx-scrollbar'; import { firstValueFrom } from 'rxjs'; import { Scrollbars } from '../scrollbars/scrollbars'; @@ -40,177 +41,165 @@ class SampleWithoutContentWrapperComponent { standalone: true, imports: [NgScrollbarModule, SampleContentComponent, SampleWithoutContentWrapperComponent], template: ` - @if (withContentWrapper) { - - - - } @else { - - - - } + + + ` }) -class ViewportClassExampleComponent { +class WithViewportDirectiveComponent { + externalViewport: string; + scrollbar: Signal = viewChild(NgScrollbarExt); + sample1: Signal = viewChild(SampleWithoutContentWrapperComponent); +} + +@Component({ + standalone: true, + imports: [NgScrollbarModule, SampleContentComponent, SampleWithoutContentWrapperComponent], + template: ` + + + + ` +}) +class WithViewportDirectiveAndInputsComponent { externalViewport: string; externalContentWrapper: string; externalSpacer: string; - - withContentWrapper: boolean; - - @ViewChild(NgScrollbarExt) scrollbar: NgScrollbarExt; - @ViewChild(SampleContentComponent) sample2: SampleContentComponent; - @ViewChild(SampleWithoutContentWrapperComponent) sample1: SampleWithoutContentWrapperComponent; + scrollbar: Signal = viewChild(NgScrollbarExt); + sample2: Signal = viewChild(SampleContentComponent); } describe('External viewport via classes', () => { - let fixture: ComponentFixture; - let component: ViewportClassExampleComponent; - let scrollbar: NgScrollbarExt; - - beforeEach(() => { - fixture = TestBed.createComponent(ViewportClassExampleComponent); - component = fixture.componentInstance; - }); it('[Viewport class] should initialize viewport and attach scrollbars', async () => { - component.externalViewport = '.my-custom-viewport'; - fixture.detectChanges(); + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveComponent); + const component: WithViewportDirectiveComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); - scrollbar = component.scrollbar; - - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); + component.externalViewport = '.my-custom-viewport'; - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.externalViewport).toBeTruthy(); - expect(scrollbar.externalContentWrapper).toBeFalsy(); - expect(scrollbar.externalSpacer).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeTruthy(); + expect(scrollbarCmp.customViewport()).toBeFalsy(); + expect(scrollbarCmp.externalViewport()).toBeTruthy(); + expect(scrollbarCmp.externalContentWrapper()).toBeFalsy(); + expect(scrollbarCmp.externalSpacer()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeTruthy(); - const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport))?.nativeElement; + const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalViewport()))?.nativeElement; expect(viewportInitSpy).toHaveBeenCalledOnceWith( viewportElement, viewportElement.firstElementChild as HTMLElement, - undefined + null ); expect(attachScrollbarSpy).toHaveBeenCalled(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + expect(scrollbarCmp._scrollbarsRef).toBeTruthy(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); // Verify the viewport - expect(scrollbar.viewport.nativeElement).toBe(viewportElement); + expect(scrollbarCmp.viewport.nativeElement).toBe(viewportElement); // // Verify that the content is a direct child of the content wrapper element - expect(scrollbar.viewport.contentWrapperElement).toEqual(component.sample1.content.nativeElement.parentElement); + expect(scrollbarCmp.viewport.contentWrapperElement).toEqual(component.sample1().content.nativeElement.parentElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbarCmp._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbarCmp._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element - expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); - const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbarCmp._scrollbarsRef.hostView, 'destroy'); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); it('[Viewport + content wrapper classes] should initialize viewport and attach scrollbars', async () => { + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); + + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); component.externalViewport = '.my-custom-viewport'; component.externalContentWrapper = '.my-custom-content-wrapper'; - component.withContentWrapper = true; - fixture.detectChanges(); - - scrollbar = component.scrollbar; - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); - - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.externalViewport).toBeTruthy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeTruthy(); + expect(scrollbarCmp.customViewport()).toBeFalsy(); + expect(scrollbarCmp.externalViewport()).toBeTruthy(); + expect(scrollbarCmp.externalContentWrapper()).toBeTruthy(); + expect(scrollbarCmp.externalSpacer()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeTruthy(); - const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport))?.nativeElement; - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; + const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalViewport()))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalContentWrapper()))?.nativeElement; expect(viewportInitSpy).toHaveBeenCalledOnceWith( viewportElement, contentWrapperElement, - undefined + null ); expect(attachScrollbarSpy).toHaveBeenCalled(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + expect(scrollbarCmp._scrollbarsRef).toBeTruthy(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); // Verify the viewport - expect(scrollbar.viewport.nativeElement).toBe(viewportElement); + expect(scrollbarCmp.viewport.nativeElement).toBe(viewportElement); // Verify that the content wrapper - expect(scrollbar.viewport.contentWrapperElement).toBe(contentWrapperElement); + expect(scrollbarCmp.viewport.contentWrapperElement).toBe(contentWrapperElement); // Verify that the content is a direct child of the content wrapper element - expect(component.sample2.content.nativeElement.parentElement).toBe(contentWrapperElement); + expect(component.sample2().content.nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbarCmp._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbarCmp._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element - expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); - const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbarCmp._scrollbarsRef.hostView, 'destroy'); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); - it('[Viewport + content wrapper + spacer classes] should initialize viewport and attach scrollbars', async () => { + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); + + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); component.externalViewport = '.my-custom-viewport'; component.externalContentWrapper = '.my-custom-content-wrapper'; component.externalSpacer = '.my-custom-spacer'; - component.withContentWrapper = true; - fixture.detectChanges(); - - scrollbar = component.scrollbar; - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); - - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.externalViewport).toBeTruthy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeTruthy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeTruthy(); + expect(scrollbarCmp.customViewport()).toBeFalsy(); + expect(scrollbarCmp.externalViewport()).toBeTruthy(); + expect(scrollbarCmp.externalContentWrapper()).toBeTruthy(); + expect(scrollbarCmp.externalSpacer()).toBeTruthy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeTruthy(); - const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalViewport))?.nativeElement; - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; - const spacerElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalSpacer))?.nativeElement; + const viewportElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalViewport()))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalContentWrapper()))?.nativeElement; + const spacerElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalSpacer()))?.nativeElement; expect(viewportInitSpy).toHaveBeenCalledOnceWith( viewportElement, @@ -218,102 +207,99 @@ describe('External viewport via classes', () => { spacerElement ); expect(attachScrollbarSpy).toHaveBeenCalled(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + expect(scrollbarCmp._scrollbarsRef).toBeTruthy(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); // Verify the viewport - expect(scrollbar.viewport.nativeElement).toBe(viewportElement); + expect(scrollbarCmp.viewport.nativeElement).toBe(viewportElement); // Verify that the content wrapper to be the spacer element - expect(scrollbar.viewport.contentWrapperElement).toBe(spacerElement); + expect(scrollbarCmp.viewport.contentWrapperElement).toBe(spacerElement); // Verify that the content is a direct child of the content wrapper element - expect(component.sample2.content.nativeElement.parentElement).toBe(contentWrapperElement); + expect(component.sample2().content.nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbarCmp._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbarCmp._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element - expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); - const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbarCmp._scrollbarsRef.hostView, 'destroy'); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); it(`[Error handling - content wrapper doesn't exist] should NOT initialize viewport or attach scrollbars`, async () => { - component.externalViewport = '.not-existing-viewport'; - fixture.detectChanges(); - scrollbar = component.scrollbar; + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); - const consoleSpy: jasmine.Spy = spyOn(console, 'error'); - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); + const consoleSpy: jasmine.Spy = spyOn(console, 'error').and.callThrough(); + + component.externalViewport = '.not-existing-viewport'; - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeFalsy(); - expect(scrollbar._scrollbars).toBeFalsy(); + expect(scrollbarCmp.customViewport()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeFalsy(); + expect(scrollbarCmp._scrollbars()).toBeFalsy(); expect(viewportInitSpy).not.toHaveBeenCalled(); expect(attachScrollbarSpy).not.toHaveBeenCalled(); - expect(consoleSpy).toHaveBeenCalledOnceWith(`[NgScrollbar]: Could not find the viewport element for the provided selector "${ scrollbar.externalViewport }"`) + expect(consoleSpy).toHaveBeenCalledOnceWith(`[NgScrollbar]: Could not find the viewport element for the provided selector "${ scrollbarCmp.externalViewport() }"`) }); it(`[Error handling - content wrapper doesn't exist] should NOT initialize viewport or attach scrollbars`, async () => { + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); + + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); + const consoleSpy: jasmine.Spy = spyOn(console, 'error').and.callThrough(); + component.externalViewport = '.my-custom-viewport'; component.externalContentWrapper = '.not-existing-content-wrapper'; - component.withContentWrapper = true; - fixture.detectChanges(); - scrollbar = component.scrollbar; - const consoleSpy: jasmine.Spy = spyOn(console, 'error'); - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); - - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeFalsy(); - expect(scrollbar._scrollbars).toBeFalsy(); + expect(scrollbarCmp.customViewport()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeFalsy(); + expect(scrollbarCmp._scrollbars()).toBeFalsy(); expect(viewportInitSpy).not.toHaveBeenCalled(); expect(attachScrollbarSpy).not.toHaveBeenCalled(); - expect(consoleSpy).toHaveBeenCalledOnceWith(`[NgScrollbar]: Content wrapper element not found for the provided selector "${ scrollbar.externalContentWrapper }"`) + expect(consoleSpy).toHaveBeenCalledOnceWith(`[NgScrollbar]: Content wrapper element not found for the provided selector "${ scrollbarCmp.externalContentWrapper() }"`) }); - it(`[Error handling - spacer doesn't exist] should NOT initialize viewport or attach scrollbars`, async () => { + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); + + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); + const consoleSpy: jasmine.Spy = spyOn(console, 'error').and.callThrough(); + component.externalViewport = '.my-custom-viewport'; component.externalContentWrapper = '.my-custom-content-wrapper'; component.externalSpacer = '.not-existing-spacer'; - component.withContentWrapper = true; - fixture.detectChanges(); - scrollbar = component.scrollbar; - - const consoleSpy: jasmine.Spy = spyOn(console, 'error'); - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeFalsy(); - expect(scrollbar._scrollbars).toBeFalsy(); + expect(scrollbarCmp.customViewport()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeFalsy(); + expect(scrollbarCmp._scrollbars()).toBeFalsy(); expect(viewportInitSpy).not.toHaveBeenCalled(); expect(attachScrollbarSpy).not.toHaveBeenCalled(); - expect(consoleSpy).toHaveBeenCalledOnceWith(`[NgScrollbar]: Spacer element not found for the provided selector "${ scrollbar.externalSpacer }"`) + expect(consoleSpy).toHaveBeenCalledOnceWith(`[NgScrollbar]: Spacer element not found for the provided selector "${ scrollbarCmp.externalSpacer() }"`) }); }); diff --git a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-directive.spec.ts b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-directive.spec.ts index f528264f..e7f16f7b 100644 --- a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-directive.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-directive.spec.ts @@ -1,212 +1,198 @@ -import { Component, DebugElement, ElementRef, ViewChild } from '@angular/core'; +import { Component, DebugElement, ElementRef, Signal, viewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { outputToObservable } from '@angular/core/rxjs-interop'; +import { firstValueFrom } from 'rxjs'; import { NgScrollbarExt, NgScrollbarModule } from 'ngx-scrollbar'; import { Scrollbars } from '../scrollbars/scrollbars'; -import { firstValueFrom } from 'rxjs'; @Component({ standalone: true, imports: [NgScrollbarModule], template: ` - @if (withContentWrapper) { - -
-
-
Content Sample
-
-
-
-
- } @else { - -
+ +
+
Content Sample
+
+
+ ` +}) +class WithViewportDirectiveComponent { + scrollbar: Signal = viewChild(NgScrollbarExt); + content: Signal> = viewChild('sample'); +} + +@Component({ + standalone: true, + imports: [NgScrollbarModule], + template: ` + +
+
Content Sample
- - } - +
+
+
` }) -class ViewportDirectiveExampleComponent { +class WithViewportDirectiveAndInputsComponent { externalContentWrapper: string; externalSpacer: string; - withContentWrapper: boolean; - - @ViewChild(NgScrollbarExt) scrollbar: NgScrollbarExt; - @ViewChild('sample') content: ElementRef; + scrollbar: Signal = viewChild(NgScrollbarExt); + content: Signal> = viewChild('sample'); } - describe('External viewport via scrollViewportDirective', () => { - let fixture: ComponentFixture; - let component: ViewportDirectiveExampleComponent; - let scrollbar: NgScrollbarExt; - - beforeEach(() => { - fixture = TestBed.createComponent(ViewportDirectiveExampleComponent); - component = fixture.componentInstance; - }); it('[Via scrollViewport directive] should initialize viewport and attach scrollbars', async () => { - fixture.detectChanges(); - - scrollbar = component.scrollbar; - - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveComponent); + const component: WithViewportDirectiveComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); fixture.detectChanges(); - expect(scrollbar.customViewport).toBeTruthy(); - expect(scrollbar.externalViewport).toBeFalsy(); - expect(scrollbar.externalContentWrapper).toBeFalsy(); - expect(scrollbar.externalSpacer).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeTruthy(); + expect(scrollbarCmp.customViewport()).toBeTruthy(); + expect(scrollbarCmp.externalViewport()).toBeFalsy(); + expect(scrollbarCmp.externalContentWrapper()).toBeFalsy(); + expect(scrollbarCmp.externalSpacer()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeTruthy(); expect(viewportInitSpy).toHaveBeenCalledOnceWith( - scrollbar.customViewport.nativeElement, - scrollbar.customViewport.nativeElement.firstElementChild as HTMLElement, - undefined + scrollbarCmp.customViewport().nativeElement, + scrollbarCmp.customViewport().nativeElement.firstElementChild, + null ); expect(attachScrollbarSpy).toHaveBeenCalled(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + expect(scrollbarCmp._scrollbarsRef).toBeTruthy(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); // Verify the viewport to be the scrollViewport directive - expect(scrollbar.viewport.nativeElement).toBe(scrollbar.customViewport.nativeElement); + expect(scrollbarCmp.viewport.nativeElement).toBe(scrollbarCmp.customViewport().nativeElement); // Verify that the content is a direct child of the content wrapper element - expect(component.content.nativeElement.parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect(component.content().nativeElement.parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbarCmp._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbarCmp._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element - expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); - const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbarCmp._scrollbarsRef.hostView, 'destroy'); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); it('[Via scrollViewport directive + content wrapper classes] should initialize viewport and attach scrollbars', async () => { - component.externalContentWrapper = '.my-custom-content-wrapper'; - component.withContentWrapper = true; - fixture.detectChanges(); - - scrollbar = component.scrollbar; - - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); + component.externalContentWrapper = '.my-custom-content-wrapper'; fixture.detectChanges(); - expect(scrollbar.customViewport).toBeTruthy(); - expect(scrollbar.externalViewport).toBeFalsy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeFalsy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeTruthy(); + expect(scrollbarCmp.customViewport()).toBeTruthy(); + expect(scrollbarCmp.externalViewport()).toBeFalsy(); + expect(scrollbarCmp.externalContentWrapper()).toBeTruthy(); + expect(scrollbarCmp.externalSpacer()).toBeFalsy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeTruthy(); - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalContentWrapper()))?.nativeElement; expect(viewportInitSpy).toHaveBeenCalledOnceWith( - scrollbar.customViewport.nativeElement, + scrollbarCmp.customViewport().nativeElement, contentWrapperElement, - undefined + null ); expect(attachScrollbarSpy).toHaveBeenCalled(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + expect(scrollbarCmp._scrollbarsRef).toBeTruthy(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); // Verify the viewport to be the scrollViewport directive - expect(scrollbar.viewport.nativeElement).toBe(scrollbar.customViewport.nativeElement); + expect(scrollbarCmp.viewport.nativeElement).toBe(scrollbarCmp.customViewport().nativeElement); // Verify that the content wrapper - expect(scrollbar.viewport.contentWrapperElement).toBe(contentWrapperElement); + expect(scrollbarCmp.viewport.contentWrapperElement).toBe(contentWrapperElement); // Verify that the content is a direct child of the content wrapper element - expect(component.content.nativeElement.parentElement).toBe(contentWrapperElement); + expect(component.content().nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbarCmp._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbarCmp._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element - expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); - const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbarCmp._scrollbarsRef.hostView, 'destroy'); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); it('[Via scrollViewport Directive + content wrapper classes + spacer classes] should initialize viewport and attach scrollbars', async () => { - component.externalContentWrapper = '.my-custom-content-wrapper'; - component.externalSpacer = '.my-custom-spacer'; - component.withContentWrapper = true; - fixture.detectChanges(); + const fixture: ComponentFixture = TestBed.createComponent(WithViewportDirectiveAndInputsComponent); + const component: WithViewportDirectiveAndInputsComponent = fixture.componentInstance; + const scrollbarCmp: NgScrollbarExt = component.scrollbar(); - scrollbar = component.scrollbar; + const viewportInitSpy: jasmine.Spy = spyOn(scrollbarCmp.viewport, 'init').and.callThrough(); + const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbarCmp, 'attachScrollbars').and.callThrough(); - const viewportInitSpy: jasmine.Spy = spyOn(scrollbar.viewport, 'init'); - const attachScrollbarSpy: jasmine.Spy = spyOn(scrollbar, 'attachScrollbars'); - - scrollbar.ngOnInit(); - scrollbar.ngAfterViewInit(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + component.externalContentWrapper = '.my-custom-content-wrapper'; + component.externalSpacer = '.my-custom-spacer'; fixture.detectChanges(); - expect(scrollbar.customViewport).toBeTruthy(); - expect(scrollbar.externalViewport).toBeFalsy(); - expect(scrollbar.externalContentWrapper).toBeTruthy(); - expect(scrollbar.externalSpacer).toBeTruthy(); - expect(scrollbar.skipInit).toBeFalsy(); - expect(scrollbar.viewport.initialized()).toBeTruthy(); + expect(scrollbarCmp.customViewport()).toBeTruthy(); + expect(scrollbarCmp.externalViewport()).toBeFalsy(); + expect(scrollbarCmp.externalContentWrapper()).toBeTruthy(); + expect(scrollbarCmp.externalSpacer()).toBeTruthy(); + expect(scrollbarCmp.skipInit).toBeFalsy(); + expect(scrollbarCmp.viewport.initialized()).toBeTruthy(); - const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalContentWrapper))?.nativeElement; - const spacerElement: HTMLElement = fixture.debugElement.query(By.css(scrollbar.externalSpacer))?.nativeElement; + const contentWrapperElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalContentWrapper()))?.nativeElement; + const spacerElement: HTMLElement = fixture.debugElement.query(By.css(scrollbarCmp.externalSpacer()))?.nativeElement; expect(viewportInitSpy).toHaveBeenCalledOnceWith( - scrollbar.customViewport.nativeElement, + scrollbarCmp.customViewport().nativeElement, contentWrapperElement, spacerElement ); expect(attachScrollbarSpy).toHaveBeenCalled(); - expect(component.scrollbar._scrollbarsRef).toBeTruthy(); + expect(scrollbarCmp._scrollbarsRef).toBeTruthy(); - await firstValueFrom(scrollbar.afterInit); + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); // Verify the viewport to be the scrollViewport directive - expect(scrollbar.viewport.nativeElement).toBe(scrollbar.customViewport.nativeElement); + expect(scrollbarCmp.viewport.nativeElement).toBe(scrollbarCmp.customViewport().nativeElement); // Verify that the content wrapper to be the spacer element - expect(scrollbar.viewport.contentWrapperElement).toBe(spacerElement); + expect(scrollbarCmp.viewport.contentWrapperElement).toBe(spacerElement); // Verify that the content is a direct child of the content wrapper element - expect(component.content.nativeElement.parentElement).toBe(contentWrapperElement); + expect(component.content().nativeElement.parentElement).toBe(contentWrapperElement); // Check if the scrollbars component is created - expect(scrollbar._scrollbars).toBeTruthy(); + expect(scrollbarCmp._scrollbars()).toBeTruthy(); const scrollbarsDebugElement: DebugElement = fixture.debugElement.query(By.directive(Scrollbars)); // Verify if the created scrollbars component is the same component instance queried - expect(scrollbar._scrollbars).toBe(scrollbarsDebugElement.componentInstance); + expect(scrollbarCmp._scrollbars()).toBe(scrollbarsDebugElement.componentInstance); // Check if the created scrollbars component is the direct child of content wrapper element - expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbar.viewport.contentWrapperElement); + expect((scrollbarsDebugElement.nativeElement as Element).parentElement).toBe(scrollbarCmp.viewport.contentWrapperElement); - const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbar._scrollbarsRef.hostView, 'destroy'); - scrollbar.ngOnDestroy(); + const hostViewDestroySpy: jasmine.Spy = spyOn(scrollbarCmp._scrollbarsRef.hostView, 'destroy'); + fixture.destroy(); expect(hostViewDestroySpy).toHaveBeenCalled(); }); diff --git a/projects/ngx-scrollbar/src/lib/tests/ext-viewport-spacer-resize.spec.ts b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-spacer-resize.spec.ts new file mode 100644 index 00000000..596763d0 --- /dev/null +++ b/projects/ngx-scrollbar/src/lib/tests/ext-viewport-spacer-resize.spec.ts @@ -0,0 +1,84 @@ +import { Component, ElementRef, Signal, viewChild, ViewChild } from '@angular/core'; +import { outputToObservable } from '@angular/core/rxjs-interop'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { firstValueFrom } from 'rxjs'; +import { NgScrollbarExt, SyncSpacer } from 'ngx-scrollbar'; +import { afterTimeout } from './common-test.'; + +@Component({ + standalone: true, + selector: 'sample-content', + imports: [NgScrollbarExt, SyncSpacer], + template: ` + +
+
+
+
+
+
+
+ ` +}) +class SampleComponent { + @ViewChild('sample') content: ElementRef; + scrollbar: Signal = viewChild(NgScrollbarExt); + sample: Signal> = viewChild('sample'); + externalViewport: string; + externalContentWrapper: string; + externalSpacer: string; +} + +describe('External viewport with [SyncSpacer]', () => { + let fixture: ComponentFixture; + let component: SampleComponent; + let scrollbarCmp: NgScrollbarExt; + + beforeEach(() => { + fixture = TestBed.createComponent(SampleComponent); + component = fixture.componentInstance; + scrollbarCmp = component.scrollbar(); + fixture.detectChanges(); + }); + + it('[SyncSpacer] should sync spacer dimension with content dimension', async () => { + await firstValueFrom(outputToObservable(scrollbarCmp.afterInit)); + + // Verify that only the vertical scrollbar is shown + expect(scrollbarCmp.verticalUsed()).toBeTrue(); + expect(scrollbarCmp.isVerticallyScrollable()).toBeTrue(); + expect(scrollbarCmp.horizontalUsed()).toBeFalse(); + expect(scrollbarCmp.isHorizontallyScrollable()).toBeFalse(); + + // Change the content size to trigger spacer resize event + scrollbarCmp.contentWrapperElement().style.setProperty('width', '200px'); + // Wait for content wrapper resize observer to pick the change + await afterTimeout(30); + // Set the new spacer dimension + fixture.detectChanges(); + // Wait for spacer resize observer to pick the change + await afterTimeout(30); + + expect(scrollbarCmp.horizontalUsed()).toBeTrue(); + expect(scrollbarCmp.isHorizontallyScrollable()).toBeTrue(); + expect(scrollbarCmp.spacerElement().clientWidth).toBe(200); + + // Change the content size to trigger spacer resize event + scrollbarCmp.contentWrapperElement().style.setProperty('width', '100px'); + // Wait for content wrapper resize observer to pick the change + await afterTimeout(30); + // Set the new spacer dimension + fixture.detectChanges(); + // Wait for spacer resize observer to pick the change + await afterTimeout(30); + + expect(scrollbarCmp.horizontalUsed()).toBeFalse(); + expect(scrollbarCmp.isHorizontallyScrollable()).toBeFalse(); + expect(scrollbarCmp.spacerElement().clientWidth).toBe(100); + }); +}); diff --git a/projects/ngx-scrollbar/src/lib/tests/fit.spec.ts b/projects/ngx-scrollbar/src/lib/tests/fit.spec.ts index 2dee77b3..4c603b08 100644 --- a/projects/ngx-scrollbar/src/lib/tests/fit.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/fit.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { Directionality } from '@angular/cdk/bidi'; import { By } from '@angular/platform-browser'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { NgScrollbar } from 'ngx-scrollbar'; import { setDimensions } from './common-test.'; @@ -23,6 +24,7 @@ describe('Fit styles', () => { ] }).compileComponents(); directionalityMock.value = 'ltr'; + directionalityMock.change.next('ltr'); fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; @@ -34,9 +36,7 @@ describe('Fit styles', () => { it('should fit both _scrollbars only if both of them are displayed', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 500, contentWidth: 500 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const trackYElement: Element = fixture.debugElement.query(By.css('scrollbar-y .ng-scrollbar-track')).nativeElement; const trackXElement: Element = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-track')).nativeElement; @@ -50,9 +50,7 @@ describe('Fit styles', () => { it('should not fit vertical scrollbar if horizontal is not displayed', async () => { setDimensions(component, { cmpHeight: 200, contentHeight: 500, cmpWidth: 200, contentWidth: 200 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const trackYElement: Element = fixture.debugElement.query(By.css('scrollbar-y .ng-scrollbar-track')).nativeElement; @@ -61,9 +59,7 @@ describe('Fit styles', () => { it('should not fit horizontal scrollbar if vertical is not displayed', async () => { setDimensions(component, { cmpWidth: 200, contentWidth: 500, cmpHeight: 200, contentHeight: 200 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const trackXElement: Element = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-track')).nativeElement; expect(trackXElement.clientWidth).toBe(200 - scrollbarOffset); diff --git a/projects/ngx-scrollbar/src/lib/tests/functions.spec.ts b/projects/ngx-scrollbar/src/lib/tests/functions.spec.ts new file mode 100644 index 00000000..878af43d --- /dev/null +++ b/projects/ngx-scrollbar/src/lib/tests/functions.spec.ts @@ -0,0 +1,34 @@ +import { fromEvent, Observable } from 'rxjs'; +import { preventSelection } from '../utils/common'; + +describe('Common functions', () => { + let mockDoc: Document; + + beforeEach(() => { + mockDoc = document.implementation.createHTMLDocument(); + }); + + it('preventSelection should set onselectstart to prevent selection', () => { + const pointerEvent$: Observable = fromEvent(mockDoc, 'pointerdown').pipe( + preventSelection(mockDoc) + ); + + // Subscribe to trigger the tap operator + pointerEvent$.subscribe(); + + // Simulate a pointer down event + const event: PointerEvent = new PointerEvent('pointerdown'); + mockDoc.dispatchEvent(event); + + expect(mockDoc.onselectstart).toBeDefined(); + expect(mockDoc.onselectstart).toBeInstanceOf(Function); + + // Create a mock event to pass to the onselectstart handler + const selectEvent: Event = new Event('selectstart'); + + // Call the onselectstart handler with the mock event + const result = mockDoc.onselectstart!(selectEvent); // Use non-null assertion + + expect(result).toBe(false); + }); +}); diff --git a/projects/ngx-scrollbar/src/lib/tests/global-options.spec.ts b/projects/ngx-scrollbar/src/lib/tests/global-options.spec.ts index ee14d201..dbd2581b 100644 --- a/projects/ngx-scrollbar/src/lib/tests/global-options.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/global-options.spec.ts @@ -21,7 +21,7 @@ describe('Global options', () => { }); it('should override default options', () => { - expect(component.appearance).toBe('compact'); + expect(component.appearance()).toBe('compact'); expect(component.visibility()).toBe('visible'); expect(component.disableInteraction()).toBe(true); }); diff --git a/projects/ngx-scrollbar/src/lib/tests/hide-native-scrollbar.spec.ts b/projects/ngx-scrollbar/src/lib/tests/hide-native-scrollbar.spec.ts index bf4d918a..b3d8e6dc 100644 --- a/projects/ngx-scrollbar/src/lib/tests/hide-native-scrollbar.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/hide-native-scrollbar.spec.ts @@ -19,7 +19,7 @@ describe('Native scrollbars', () => { it('should hide the native scrollbars by default', () => { setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 200, contentHeight: 200 }); - component.ngOnInit(); + // component.ngOnInit(); const pseudoStyle: CSSStyleDeclaration = getComputedStyle(component.viewport.nativeElement, '::-webkit-scrollbar'); const scrollbarDisplay: string = pseudoStyle.getPropertyValue('display'); diff --git a/projects/ngx-scrollbar/src/lib/tests/hover-effect.spec.ts b/projects/ngx-scrollbar/src/lib/tests/hover-effect.spec.ts index b9087a9a..16124763 100644 --- a/projects/ngx-scrollbar/src/lib/tests/hover-effect.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/hover-effect.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { firstValueFrom } from 'rxjs'; import { NgScrollbar } from 'ngx-scrollbar'; import { setDimensions } from './common-test.'; @@ -26,9 +27,7 @@ describe('Hover effect', () => { it('Should activate hover effect only when mouse is over the scrollbar in case [hoverOffset]="false"', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 400, contentWidth: 400 }); fixture.componentRef.setInput('hoverOffset', false); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const stickyYElement: Element = fixture.debugElement.query(By.css('scrollbar-y .ng-scrollbar-sticky')).nativeElement; const stickyXElement: Element = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-sticky')).nativeElement; @@ -45,9 +44,7 @@ describe('Hover effect', () => { it('Should activate hover effect when mouse is over the offset area in case [hoverOffset]="true"', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 400, contentWidth: 400 }); fixture.componentRef.setInput('hoverOffset', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) const stickyYElement: Element = fixture.debugElement.query(By.css('scrollbar-y .ng-scrollbar-sticky')).nativeElement; const stickyXElement: Element = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-sticky')).nativeElement; diff --git a/projects/ngx-scrollbar/src/lib/tests/ng-scrollbar.spec.ts b/projects/ngx-scrollbar/src/lib/tests/ng-scrollbar.spec.ts index feca5f92..f2fd049b 100644 --- a/projects/ngx-scrollbar/src/lib/tests/ng-scrollbar.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/ng-scrollbar.spec.ts @@ -1,7 +1,8 @@ import { ComponentFixture, TestBed, } from '@angular/core/testing'; import { DebugElement } from '@angular/core'; -import { NgScrollbar } from 'ngx-scrollbar'; import { By } from '@angular/platform-browser'; +import { outputToObservable } from '@angular/core/rxjs-interop'; +import { NgScrollbar } from 'ngx-scrollbar'; import { firstValueFrom } from 'rxjs'; import { setDimensions } from './common-test.'; @@ -24,7 +25,7 @@ describe('NgScrollbar Component', () => { }); it('should initialize component and viewport', () => { - component.ngOnInit(); + // component.ngOnInit(); expect(component).toBeDefined(); expect(component.viewport).toBeDefined(); }); @@ -32,8 +33,6 @@ describe('NgScrollbar Component', () => { it('should emit afterUpdate after update function is called', async () => { const afterUpdateEmitSpy: jasmine.Spy = spyOn(component.afterUpdate, 'emit'); - component.ngOnInit(); - component.ngAfterViewInit(); component.update(); expect(afterUpdateEmitSpy).toHaveBeenCalled(); }); @@ -43,10 +42,8 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'vertical'); fixture.componentRef.setInput('visibility', 'native'); setDimensions(component, { cmpHeight: 300, contentHeight: 1000 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.verticalUsed()).toBeTrue(); expect(component.isVerticallyScrollable()).toBeTrue(); @@ -58,10 +55,8 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'vertical'); fixture.componentRef.setInput('visibility', 'visible'); setDimensions(component, { cmpHeight: 1000, contentHeight: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.verticalUsed()).toBeTrue(); expect(component.isVerticallyScrollable()).toBeFalse(); @@ -73,8 +68,6 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'vertical'); fixture.componentRef.setInput('visibility', 'native'); setDimensions(component, { cmpWidth: 1000, cmpHeight: 1000, contentHeight: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); expect(component.verticalUsed()).toBeFalse(); expect(component.isVerticallyScrollable()).toBeFalse(); @@ -86,10 +79,8 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'horizontal'); fixture.componentRef.setInput('visibility', 'native'); setDimensions(component, { cmpWidth: 300, contentHeight: 300, contentWidth: 1000 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.horizontalUsed()).toBeTrue(); expect(component.isHorizontallyScrollable()).toBeTrue(); @@ -102,10 +93,8 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'horizontal'); fixture.componentRef.setInput('visibility', 'visible'); setDimensions(component, { cmpWidth: 1000, contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.horizontalUsed()).toBeTrue(); expect(component.isHorizontallyScrollable()).toBeFalse(); @@ -117,8 +106,6 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'horizontal'); fixture.componentRef.setInput('visibility', 'native'); setDimensions(component, { cmpWidth: 1000, contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); expect(component.horizontalUsed()).toBeFalse(); expect(component.isHorizontallyScrollable()).toBeFalse(); @@ -130,10 +117,8 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'auto'); fixture.componentRef.setInput('visibility', 'visible'); setDimensions(component, { cmpWidth: 1000, cmpHeight: 1000, contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.horizontalUsed()).toBeTrue(); expect(component.isHorizontallyScrollable()).toBeFalse(); @@ -145,10 +130,8 @@ describe('NgScrollbar Component', () => { fixture.componentRef.setInput('orientation', 'auto'); fixture.componentRef.setInput('visibility', 'visible'); setDimensions(component, { cmpWidth: 200, cmpHeight: 200, contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.horizontalUsed()).toBeTrue(); expect(component.isHorizontallyScrollable()).toBeTrue(); @@ -159,8 +142,6 @@ describe('NgScrollbar Component', () => { it('[Auto-height]: component height and width should match content size by default', () => { fixture.componentRef.setInput('orientation', 'auto'); setDimensions(component, { contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); const scrollbarY: DebugElement = fixture.debugElement.query(By.css('scrollbar-y')); const scrollbarX: DebugElement = fixture.debugElement.query(By.css('scrollbar-x')); @@ -174,12 +155,10 @@ describe('NgScrollbar Component', () => { it('should forward scrollToElement function call to SmoothScrollManager service', async () => { setDimensions(component, { contentHeight: 300, contentWidth: 300 }); - component.ngOnInit(); - component.ngAfterViewInit(); const smoothScrollSpy: jasmine.Spy = spyOn(component.smoothScroll, 'scrollToElement'); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) component.scrollToElement('.fake-child-element', { top: 100, duration: 500 }) diff --git a/projects/ngx-scrollbar/src/lib/tests/resize-sensor.spec.ts b/projects/ngx-scrollbar/src/lib/tests/resize-sensor.spec.ts index eda20c60..d6e3b0c3 100644 --- a/projects/ngx-scrollbar/src/lib/tests/resize-sensor.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/resize-sensor.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { firstValueFrom } from 'rxjs'; import { NgScrollbar } from 'ngx-scrollbar'; import { afterTimeout, setDimensions } from './common-test.'; @@ -23,70 +24,68 @@ describe('Resize Sensor', () => { it('[Init] should update as soon as it gets initialized', async () => { setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 500, contentWidth: 500 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)); expect(component.disableSensor()).toEqual(false); - expect(component['sizeChangeSub'].closed).toEqual(false); + expect(component.viewportDimension()).toEqual({ - offsetWidth: 200, - offsetHeight: 200, - contentWidth: 500, - contentHeight: 500 + width: 200, + height: 200 + }); + expect(component.contentDimension()).toEqual({ + width: 500, + height: 500 }); }); it('[Resize] should update when component size changes', async () => { setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 400, contentWidth: 400 }); - component.appearance = 'compact'; - component.ngOnInit(); - component.ngAfterViewInit(); + fixture.componentRef.setInput('appearance', 'compact'); await afterTimeout(16); // Change component size setDimensions(component, { cmpHeight: 200, cmpWidth: 200, contentHeight: 400, contentWidth: 400 }); - await firstValueFrom(component.afterUpdate); + await firstValueFrom(outputToObservable(component.afterUpdate)); expect(component.viewportDimension()).toEqual({ - offsetWidth: 200, - offsetHeight: 200, - contentWidth: 400, - contentHeight: 400 + width: 200, + height: 200 + }); + expect(component.contentDimension()).toEqual({ + width: 400, + height: 400 }); }); it('[Resize] should update when content size changes', async () => { setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 400, contentWidth: 400 }); - component.appearance = 'compact'; - component.ngOnInit(); - component.ngAfterViewInit(); + fixture.componentRef.setInput('appearance', 'compact'); await afterTimeout(16); // Change content size setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 500, contentWidth: 500 }); - await firstValueFrom(component.afterUpdate); + await firstValueFrom(outputToObservable(component.afterUpdate)); expect(component.viewportDimension()).toEqual({ - offsetWidth: 100, - offsetHeight: 100, - contentWidth: 500, - contentHeight: 500 + width: 100, + height: 100 + }); + expect(component.contentDimension()).toEqual({ + width: 500, + height: 500 }); }); it('[Resize + sensorThrottleTime] should throttle sensor', async () => { setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 400, contentWidth: 400 }); - component.appearance = 'compact'; + fixture.componentRef.setInput('appearance', 'compact'); fixture.componentRef.setInput('sensorThrottleTime', 200); - component.ngOnInit(); - component.ngAfterViewInit(); - await afterTimeout(16); + await firstValueFrom(outputToObservable(component.afterInit)); // Change content size setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 500, contentWidth: 500 }); @@ -96,10 +95,12 @@ describe('Resize Sensor', () => { // Verify viewport dimension haven't been updated expect(component.viewportDimension()).toEqual({ - offsetWidth: 100, - offsetHeight: 100, - contentWidth: 400, - contentHeight: 400 + width: 100, + height: 100 + }); + expect(component.contentDimension()).toEqual({ + width: 400, + height: 400 }); // Wait for another 200ms @@ -107,27 +108,28 @@ describe('Resize Sensor', () => { // Verify that viewport been updated expect(component.viewportDimension()).toEqual({ - offsetWidth: 100, - offsetHeight: 100, - contentWidth: 500, - contentHeight: 500 + width: 100, + height: 100 + }); + expect(component.contentDimension()).toEqual({ + width: 500, + height: 500 }); }); it('[Init] should call update afterViewInit and disable the resize sensor when "disableSensor" is used', async () => { setDimensions(component, { cmpHeight: 100, cmpWidth: 100, contentHeight: 400, contentWidth: 400 }); fixture.componentRef.setInput('disableSensor', true); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) expect(component.disableSensor()).toEqual(true); - expect(component['sizeChangeSub'].closed).toEqual(true); expect(component.viewportDimension()).toEqual({ - offsetWidth: 100, - offsetHeight: 100, - contentWidth: 400, - contentHeight: 400 + width: 100, + height: 100 + }); + expect(component.contentDimension()).toEqual({ + width: 400, + height: 400 }); }); }); diff --git a/projects/ngx-scrollbar/src/lib/tests/scroll-viewport.spec.ts b/projects/ngx-scrollbar/src/lib/tests/scroll-viewport.spec.ts index b0088c8e..97a1be12 100644 --- a/projects/ngx-scrollbar/src/lib/tests/scroll-viewport.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/scroll-viewport.spec.ts @@ -6,25 +6,25 @@ import { setDimensions } from './common-test.'; describe('Viewport Adapter', () => { let component: NgScrollbar; let fixture: ComponentFixture; + let viewportInitSpy: jasmine.Spy; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NgScrollbar], - providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }] + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true } + ] }).compileComponents(); fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; + viewportInitSpy = spyOn(component.viewport, 'init').and.callThrough(); }); it('should initialize viewport and add the proper classes to viewport and content wrapper', () => { expect(component.viewport).toBeDefined(); - const viewportInitSpy: jasmine.Spy = spyOn(component.viewport, 'init'); - component.ngOnInit(); - - expect(viewportInitSpy).toHaveBeenCalledOnceWith(component.nativeElement, component.contentWrapper.nativeElement); - component.ngAfterViewInit(); + expect(viewportInitSpy).toHaveBeenCalledOnceWith(component.nativeElement, component.contentWrapper().nativeElement); expect(component.viewport.nativeElement).toBeDefined(); expect(component.viewport.contentWrapperElement).toBeDefined(); @@ -36,9 +36,8 @@ describe('Viewport Adapter', () => { }); it('should instantly jump to scroll position when using scrollYTo and scrollXTo', () => { + TestBed.flushEffects(); setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 400, contentHeight: 400 }); - component.ngOnInit(); - component.ngAfterViewInit(); component.viewport.scrollYTo(200); expect(component.viewport.scrollTop).toBe(200); diff --git a/projects/ngx-scrollbar/src/lib/tests/thumb.spec.ts b/projects/ngx-scrollbar/src/lib/tests/thumb.spec.ts index 1e74643e..5a726d22 100644 --- a/projects/ngx-scrollbar/src/lib/tests/thumb.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/thumb.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/ import { signal } from '@angular/core'; import { By } from '@angular/platform-browser'; import { Directionality } from '@angular/cdk/bidi'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { NgScrollbar } from 'ngx-scrollbar'; import { ThumbAdapter } from '../thumb/thumb-adapter'; @@ -32,6 +33,7 @@ describe('Scrollbar thumb', () => { }).compileComponents(); directionalityMock.value = 'ltr'; + directionalityMock.change.next('ltr'); fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; @@ -43,9 +45,7 @@ describe('Scrollbar thumb', () => { it('should set "isDragging" to true and scroll accordingly when vertical scrollbar thumb is being dragged', async () => { setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 100, contentHeight: 400 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const thumbY: Element = fixture.debugElement.query(By.css('scrollbar-y .ng-scrollbar-thumb')).nativeElement; @@ -73,9 +73,7 @@ describe('Scrollbar thumb', () => { it('should set "isDragging" to true and scroll accordingly when horizontal scrollbar is being dragged', async () => { setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 400, contentHeight: 100 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const thumbX: Element = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-thumb')).nativeElement; @@ -102,11 +100,10 @@ describe('Scrollbar thumb', () => { it('[RTL] should set "isDragging" to true and scroll accordingly when horizontal scrollbar is being dragged', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 400, contentHeight: 100 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const thumbX: Element = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-thumb')).nativeElement; @@ -134,9 +131,7 @@ describe('Scrollbar thumb', () => { it('should set the animation when polyfill script is loaded', async () => { setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 400, contentHeight: 100 }); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const thumbAdapter: ThumbAdapter = fixture.debugElement.query(By.css('scrollbar-x .ng-scrollbar-thumb')).injector.get(ThumbAdapter); diff --git a/projects/ngx-scrollbar/src/lib/tests/track.spec.ts b/projects/ngx-scrollbar/src/lib/tests/track.spec.ts index 1374a77d..b9a3003e 100644 --- a/projects/ngx-scrollbar/src/lib/tests/track.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/track.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/ import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { Directionality } from '@angular/cdk/bidi'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { NgScrollbar, NgScrollbarModule, } from 'ngx-scrollbar'; import { provideSmoothScrollOptions } from 'ngx-scrollbar/smooth-scroll'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; @@ -36,18 +37,19 @@ describe('Scrollbar track', () => { }).compileComponents(); directionalityMock.value = 'ltr'; + directionalityMock.change.next('ltr'); fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; fixture.componentRef.setInput('appearance', 'compact'); + + TestBed.flushEffects(); setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 500, contentHeight: 500 }); }); it('[Vertical] should scroll to bottom progressively when mousedown on the bottom edge of the track', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const trackYDebugElement: DebugElement = fixture.debugElement.query(By.directive(TrackYDirective)); @@ -76,9 +78,7 @@ describe('Scrollbar track', () => { }); it('[Vertical] should scroll to top progressively when mousedown on the top edge of the track', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ bottom: 0, duration: 0 }); @@ -110,10 +110,9 @@ describe('Scrollbar track', () => { it('[RTL Vertical] should scroll to bottom progressively when mousedown on the bottom edge of the track', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const trackYDebugElement: DebugElement = fixture.debugElement.query(By.directive(TrackYDirective)); @@ -143,10 +142,9 @@ describe('Scrollbar track', () => { it('[RTL Vertical] should scroll to top progressively when mousedown on the top edge of the track', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ bottom: 0, duration: 0 }); @@ -177,9 +175,7 @@ describe('Scrollbar track', () => { }); it('[Horizontal] should scroll to end progressively when mousedown on the right edge of the track', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const trackXDebugElement: DebugElement = fixture.debugElement.query(By.directive(TrackXDirective)); @@ -208,9 +204,7 @@ describe('Scrollbar track', () => { }); it('[Horizontal] should scroll to start progressively when mousedown on the left edge of the track', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ end: 0, duration: 0 }); @@ -241,10 +235,9 @@ describe('Scrollbar track', () => { it('[RTL Horizontal] should scroll to end progressively when mousedown on the left edge of the track in RTL', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ start: 0, duration: 0 }); @@ -275,10 +268,9 @@ describe('Scrollbar track', () => { it('[RTL Horizontal] should scroll to start progressively when mousedown on the right edge of the track in RTL', async () => { directionalityMock.value = 'rtl'; + directionalityMock.change.next('rtl'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); await component.scrollTo({ end: 0, duration: 0 }); @@ -309,9 +301,7 @@ describe('Scrollbar track', () => { it('should scroll to bottom with one step on first click if incremental position exceeds scroll maximum', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); // Make current scroll position close to bottom, so it triggers only one scroll to the end @@ -334,9 +324,7 @@ describe('Scrollbar track', () => { it('should scroll to top with one step on first click if incremental position exceeds scroll maximum', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); // Make current scroll position close to top, so it triggers only one scroll step to finish @@ -358,9 +346,7 @@ describe('Scrollbar track', () => { }); it('should not scroll when mouse is down and moves away', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const trackYDebugElement: DebugElement = fixture.debugElement.query(By.directive(TrackYDirective)); @@ -402,9 +388,7 @@ describe('Scrollbar track', () => { }); it('should scroll only once one if destination is one step below the thumb position', async () => { - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)) TestBed.flushEffects(); const trackYDebugElement: DebugElement = fixture.debugElement.query(By.directive(TrackYDirective)); diff --git a/projects/ngx-scrollbar/src/lib/tests/visibility.spec.ts b/projects/ngx-scrollbar/src/lib/tests/visibility.spec.ts index 57e452ef..5f99fcfd 100644 --- a/projects/ngx-scrollbar/src/lib/tests/visibility.spec.ts +++ b/projects/ngx-scrollbar/src/lib/tests/visibility.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; +import { outputToObservable } from '@angular/core/rxjs-interop'; import { NgScrollbar } from 'ngx-scrollbar'; import { firstValueFrom } from 'rxjs'; import { setDimensions } from './common-test.'; @@ -21,14 +22,12 @@ describe('Visibility styles', () => { fixture = TestBed.createComponent(NgScrollbar); component = fixture.componentInstance; fixture.detectChanges(); + setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 100, contentHeight: 200 }); }); it('[Visibility] should be hidden when visibility="hover"', async () => { - setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 100, contentHeight: 200 }); fixture.componentRef.setInput('visibility', 'hover'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)); const stickyDebugElement: DebugElement = fixture.debugElement.query(By.css('.ng-scrollbar-sticky')); @@ -43,15 +42,12 @@ describe('Visibility styles', () => { }); it('[Visibility] should be able to override styles related to sticky container using CSS variables', async () => { - setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 100, contentHeight: 200 }); fixture.componentRef.setInput('visibility', 'hover'); // Override track color and transition using CSS variables component.nativeElement.style.setProperty('--scrollbar-hover-opacity-transition-enter-duration', '200ms'); component.nativeElement.style.setProperty('--scrollbar-hover-opacity-transition-leave-duration', '500ms'); component.nativeElement.style.setProperty('--scrollbar-hover-opacity-transition-leave-delay', '3s'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)); const stickyDebugElement: DebugElement = fixture.debugElement.query(By.css('.ng-scrollbar-sticky')); const stickyStyles: CSSStyleDeclaration = getComputedStyle(stickyDebugElement.nativeElement); @@ -62,29 +58,10 @@ describe('Visibility styles', () => { expect(stickyStyles.transitionProperty).toBe('opacity'); }); - // it('[Visibility] should set default styles and hover effect', async () => { - // setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 100, contentHeight: 200 }); - // component.ngOnInit(); - // component.ngAfterViewInit(); - // await firstValueFrom(component.afterInit); - // - // const trackStyles: CSSStyleDeclaration = getComputedStyle(component._scrollbars.y.track.nativeElement); - // - // expect(component.nativeElement.getAttribute('visibility')).toBe('native'); - // expect(trackStyles.position).toBe('absolute'); - // - // expect(trackStyles.opacity).toBe('1'); - // expect(trackStyles.transition).toBe('height 0.15s ease-out 0s, width 0.15s ease-out 0s'); - // expect(trackStyles.backgroundColor).toBe('rgba(0, 0, 0, 0)'); - // }); - // it('[Visibility] should be able to override styles related to scrollbar track using CSS variables', async () => { - setDimensions(component, { cmpWidth: 100, cmpHeight: 100, contentWidth: 100, contentHeight: 200 }); // Override track color and transition using CSS variables component.nativeElement.style.setProperty('--scrollbar-track-color', 'red'); - component.ngOnInit(); - component.ngAfterViewInit(); - await firstValueFrom(component.afterInit); + await firstValueFrom(outputToObservable(component.afterInit)); const trackDebugElement: DebugElement = fixture.debugElement.query(By.directive(TrackYDirective)); const trackStyles: CSSStyleDeclaration = getComputedStyle(trackDebugElement.nativeElement); diff --git a/projects/ngx-scrollbar/src/lib/thumb/thumb-adapter.ts b/projects/ngx-scrollbar/src/lib/thumb/thumb-adapter.ts index 7354dc6f..c263167f 100644 --- a/projects/ngx-scrollbar/src/lib/thumb/thumb-adapter.ts +++ b/projects/ngx-scrollbar/src/lib/thumb/thumb-adapter.ts @@ -1,4 +1,4 @@ -import { Directive, inject, effect } from '@angular/core'; +import { Directive, inject, effect, untracked } from '@angular/core'; import { Observable, of, fromEvent, map, takeUntil, tap, switchMap } from 'rxjs'; import { ScrollbarDragging, @@ -23,7 +23,7 @@ export abstract class ThumbAdapter extends PointerEventsAdapter { // Returns thumb size get size(): number { - return this.nativeElement[this.control.sizeProperty]; + return this.nativeElement.getBoundingClientRect()[this.control.rectSizeProperty]; } // The maximum space available for scrolling. @@ -78,9 +78,11 @@ export abstract class ThumbAdapter extends PointerEventsAdapter { constructor() { effect(() => { const script: ScrollTimelineFunc = this.manager.scrollTimelinePolyfill(); - if (script && !this._animation) { - this._animation = startPolyfill(script, this.nativeElement, this.cmp.viewport.nativeElement, this.control.axis); - } + untracked(() => { + if (script && !this._animation) { + this._animation = startPolyfill(script, this.nativeElement, this.cmp.viewport.nativeElement, this.control.axis); + } + }) }); super(); } @@ -102,6 +104,6 @@ function startPolyfill(ScrollTimeline: ScrollTimelineFunc, element: HTMLElement, fill: 'both', easing: 'linear', timeline: new ScrollTimeline({ source, axis }) - } as unknown + } ); } diff --git a/projects/ngx-scrollbar/src/lib/track/track-adapter.ts b/projects/ngx-scrollbar/src/lib/track/track-adapter.ts index be8d10e1..eb07b82a 100644 --- a/projects/ngx-scrollbar/src/lib/track/track-adapter.ts +++ b/projects/ngx-scrollbar/src/lib/track/track-adapter.ts @@ -1,29 +1,23 @@ -import { ContentChild, Directive, effect, EffectCleanupRegisterFn } from '@angular/core'; +import { Directive, effect, untracked } from '@angular/core'; import { Observable, - Subscription, - tap, - map, delay, + fromEvent, + map, merge, startWith, switchMap, - fromEvent, takeUntil, takeWhile, + tap, EMPTY } from 'rxjs'; import { enableSelection, preventSelection, stopPropagation } from '../utils/common'; -import { ThumbAdapter } from '../thumb/thumb-adapter'; -import { resizeObserver } from '../viewport'; import { PointerEventsAdapter } from '../utils/pointer-events-adapter'; @Directive() export abstract class TrackAdapter extends PointerEventsAdapter { - // Subscription for resize observer - private sizeChangeSub: Subscription; - // The current position of the mouse during track dragging private currMousePosition: number; @@ -33,11 +27,8 @@ export abstract class TrackAdapter extends PointerEventsAdapter { // The maximum scroll position until the end is reached protected scrollMax: number; - // The CSS variable name used to set the length value - protected abstract readonly cssLengthProperty: string; - // Returns viewport scroll size - protected abstract get viewportScrollSize(): number; + protected abstract get contentSize(): number; // Returns viewport client size protected get viewportSize(): number { @@ -60,12 +51,12 @@ export abstract class TrackAdapter extends PointerEventsAdapter { // Scrollbar track offset get offset(): number { - return this.clientRect[this.control.clientRectProperty]; + return this.clientRect[this.control.rectOffsetProperty]; } // Scrollbar track length get size(): number { - return this.nativeElement[this.control.sizeProperty]; + return this.clientRect[this.control.rectSizeProperty]; } // Observable for track dragging events @@ -119,27 +110,13 @@ export abstract class TrackAdapter extends PointerEventsAdapter { ); } - // Reference to the ThumbAdapter component - @ContentChild(ThumbAdapter) protected thumb: ThumbAdapter; - constructor() { - effect((onCleanup: EffectCleanupRegisterFn) => { - if (this.cmp.disableSensor()) { - this.update(); - this.sizeChangeSub?.unsubscribe(); - } else { - this.zone.runOutsideAngular(() => { - // Update styles with real track size - this.sizeChangeSub = resizeObserver({ - element: this.nativeElement, - throttleDuration: this.cmp.sensorThrottleTime() - }).pipe( - tap(() => this.update()) - ).subscribe(); - }); - } - - onCleanup(() => this.sizeChangeSub?.unsubscribe()); + effect(() => { + this.cmp.viewportDimension(); + this.cmp.contentDimension(); + untracked(() => { + requestAnimationFrame(() => this.control.trackSize.set(this.size)); + }); }); super(); } @@ -148,10 +125,6 @@ export abstract class TrackAdapter extends PointerEventsAdapter { protected abstract getScrollBackwardStep(): number; - private update(): void { - this.cmp.nativeElement.style.setProperty(this.cssLengthProperty, `${ this.size }`); - } - /** * Callback when mouse is first clicked on the track */ diff --git a/projects/ngx-scrollbar/src/lib/track/track.ts b/projects/ngx-scrollbar/src/lib/track/track.ts index 26e9e70d..8a7317cb 100644 --- a/projects/ngx-scrollbar/src/lib/track/track.ts +++ b/projects/ngx-scrollbar/src/lib/track/track.ts @@ -10,9 +10,7 @@ import { TrackAdapter } from './track-adapter'; }) export class TrackXDirective extends TrackAdapter { - protected readonly cssLengthProperty: string = '--track-x-length'; - - protected get viewportScrollSize(): number { + protected get contentSize(): number { return this.cmp.viewport.contentWidth; } @@ -24,15 +22,15 @@ export class TrackXDirective extends TrackAdapter { effect(() => { if (this.cmp.direction() === 'rtl') { this.getCurrPosition = (): number => { - const offset: number = this.viewportScrollSize - this.viewportSize - this.control.viewportScrollOffset; - return offset * this.size / this.viewportScrollSize; + const offset: number = this.contentSize - this.viewportSize - this.control.viewportScrollOffset; + return offset * this.size / this.contentSize; }; this.getScrollDirection = (position: number): 'forward' | 'backward' => { return position < this.getCurrPosition() ? 'forward' : 'backward'; }; } else { this.getCurrPosition = (): number => { - return this.control.viewportScrollOffset * this.size / this.viewportScrollSize + return this.control.viewportScrollOffset * this.size / this.contentSize; }; this.getScrollDirection = (position: number): 'forward' | 'backward' => { return position > this.getCurrPosition() ? 'forward' : 'backward'; @@ -43,7 +41,7 @@ export class TrackXDirective extends TrackAdapter { } protected scrollTo(start: number): Observable { - return fromPromise(this.cmp.scrollTo({ start, duration: this.cmp.trackScrollDuration })); + return fromPromise(this.cmp.scrollTo({ start, duration: this.cmp.trackScrollDuration() })); } protected getScrollForwardStep(): number { @@ -62,14 +60,12 @@ export class TrackXDirective extends TrackAdapter { }) export class TrackYDirective extends TrackAdapter { - protected readonly cssLengthProperty: string = '--track-y-length'; - - protected get viewportScrollSize(): number { + protected get contentSize(): number { return this.cmp.viewport.contentHeight; } protected getCurrPosition(): number { - return this.control.viewportScrollOffset * this.size / this.viewportScrollSize; + return this.control.viewportScrollOffset * this.size / this.contentSize; } protected getScrollDirection(position: number): 'forward' | 'backward' { @@ -77,7 +73,7 @@ export class TrackYDirective extends TrackAdapter { } protected scrollTo(top: number): Observable { - return fromPromise(this.cmp.scrollTo({ top, duration: this.cmp.trackScrollDuration })); + return fromPromise(this.cmp.scrollTo({ top, duration: this.cmp.trackScrollDuration() })); } protected getScrollForwardStep(): number { diff --git a/projects/ngx-scrollbar/src/lib/utils/common.ts b/projects/ngx-scrollbar/src/lib/utils/common.ts index 9a27375c..69b1e174 100644 --- a/projects/ngx-scrollbar/src/lib/utils/common.ts +++ b/projects/ngx-scrollbar/src/lib/utils/common.ts @@ -1,4 +1,4 @@ -import { MonoTypeOperatorFunction, tap } from 'rxjs'; +import { MonoTypeOperatorFunction, Observable, tap, throttleTime } from 'rxjs'; export function preventSelection(doc: Document): MonoTypeOperatorFunction { return tap(() => doc.onselectstart = () => false); @@ -17,11 +17,26 @@ export function stopPropagation(): MonoTypeOperatorFunction { }); } +export function getThrottledStream(stream: Observable, duration: number): Observable { + return stream.pipe( + throttleTime(duration || 0, null, { + leading: false, + trailing: true + }) + ); +} + +export interface ElementDimension { + width?: number; + height?: number; +} + export type ScrollbarDragging = 'x' | 'y' | 'none'; export enum ViewportClasses { Viewport = 'ng-scroll-viewport', - Content = 'ng-scroll-content' + Content = 'ng-scroll-content', + Spacer = 'ng-scroll-spacer' } export interface ViewportBoundaries { diff --git a/projects/ngx-scrollbar/src/lib/utils/pointer-events-adapter.ts b/projects/ngx-scrollbar/src/lib/utils/pointer-events-adapter.ts index 98baa260..9155a8f9 100644 --- a/projects/ngx-scrollbar/src/lib/utils/pointer-events-adapter.ts +++ b/projects/ngx-scrollbar/src/lib/utils/pointer-events-adapter.ts @@ -1,4 +1,4 @@ -import { Directive, effect, inject, ElementRef, NgZone, EffectCleanupRegisterFn } from '@angular/core'; +import { Directive, effect, inject, untracked, ElementRef, NgZone, EffectCleanupRegisterFn } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { Observable, Subscription } from 'rxjs'; import { _NgScrollbar, NG_SCROLLBAR } from '../utils/scrollbar-base'; @@ -29,13 +29,15 @@ export abstract class PointerEventsAdapter { constructor() { effect((onCleanup: EffectCleanupRegisterFn) => { - if (this.cmp.disableInteraction()) { - this._pointerEventsSub?.unsubscribe(); - } else { - this.zone.runOutsideAngular(() => { - this._pointerEventsSub = this.pointerEvents.subscribe(); - }); - } + const disableInteraction: boolean = this.cmp.disableInteraction(); + + untracked(() => { + if (!disableInteraction) { + this.zone.runOutsideAngular(() => { + this._pointerEventsSub = this.pointerEvents.subscribe(); + }); + } + }); onCleanup(() => this._pointerEventsSub?.unsubscribe()); }); diff --git a/projects/ngx-scrollbar/src/lib/utils/scrollbar-base.ts b/projects/ngx-scrollbar/src/lib/utils/scrollbar-base.ts index d19a4132..8e6e0ae9 100644 --- a/projects/ngx-scrollbar/src/lib/utils/scrollbar-base.ts +++ b/projects/ngx-scrollbar/src/lib/utils/scrollbar-base.ts @@ -2,7 +2,7 @@ import { InjectionToken, Signal, WritableSignal } from '@angular/core'; import { Direction } from '@angular/cdk/bidi'; import { SmoothScrollToOptions } from 'ngx-scrollbar/smooth-scroll'; import { ViewportAdapter } from '../viewport'; -import { ScrollbarDragging, ViewportBoundaries } from './common'; +import { ElementDimension, ScrollbarDragging } from './common'; /** @@ -15,7 +15,7 @@ export interface _NgScrollbar { nativeElement: HTMLElement; dragging: WritableSignal; direction: Signal; - trackScrollDuration: number; + trackScrollDuration: Signal; hoverOffset: Signal; buttons: Signal; disableSensor: Signal; @@ -25,10 +25,11 @@ export interface _NgScrollbar { isHorizontallyScrollable: Signal; verticalUsed: Signal; horizontalUsed: Signal; - viewportDimension: Signal; - thumbClass: string; - trackClass: string; - buttonClass: string; + thumbClass: Signal; + trackClass: Signal; + buttonClass: Signal; + viewportDimension: WritableSignal; + contentDimension: WritableSignal; get viewport(): ViewportAdapter; diff --git a/projects/ngx-scrollbar/src/lib/viewport/index.ts b/projects/ngx-scrollbar/src/lib/viewport/index.ts index 94f8e7fd..89ee638a 100644 --- a/projects/ngx-scrollbar/src/lib/viewport/index.ts +++ b/projects/ngx-scrollbar/src/lib/viewport/index.ts @@ -1,3 +1,2 @@ export * from './scroll-viewport'; -export * from './observer'; export * from './viewport-adapter'; diff --git a/projects/ngx-scrollbar/src/lib/viewport/observer.ts b/projects/ngx-scrollbar/src/lib/viewport/observer.ts deleted file mode 100644 index 0fff9d6e..00000000 --- a/projects/ngx-scrollbar/src/lib/viewport/observer.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Observable, Observer, throttleTime } from 'rxjs'; -import { ScrollbarUpdateReason } from '../ng-scrollbar.model'; - -interface ResizeArgs { - element: HTMLElement; - throttleDuration: number; - contentWrapper?: HTMLElement; -} - -export function resizeObserver({ element, contentWrapper, throttleDuration }: ResizeArgs): Observable { - // The first time the observer is triggered as soon as the element is observed, - // So we need to differentiate the reason of the event fired - let reason: ScrollbarUpdateReason = ScrollbarUpdateReason.AfterInit; - - let resizeObserver: ResizeObserver; - - const stream: Observable = new Observable((observer: Observer) => { - resizeObserver = new ResizeObserver(() => { - observer.next(reason); - // After first init event, mark the reason to be a resize from now on. - reason = ScrollbarUpdateReason.Resized; - }); - resizeObserver.observe(element); - - // If a content element has a supporting content scrollbars, observe it! - if (contentWrapper) { - resizeObserver.observe(contentWrapper); - } - - return () => { - resizeObserver?.disconnect(); - }; - }); - - return throttleDuration ? stream.pipe(throttleTime(throttleDuration, null, { - leading: true, - trailing: true - })) : stream; -} - - -export function mutationObserver(element: HTMLElement, throttleDuration: number): Observable { - let mutationObserver: MutationObserver; - - const stream: Observable = new Observable((observer: Observer) => { - mutationObserver = new MutationObserver(() => { - observer.next(); - }); - mutationObserver.observe(element, { childList: true, subtree: true }); - - return () => { - mutationObserver?.disconnect(); - }; - }); - - return stream.pipe(throttleTime(throttleDuration, null, { - leading: true, - trailing: true - })); -} diff --git a/projects/ngx-scrollbar/src/lib/viewport/scroll-viewport.ts b/projects/ngx-scrollbar/src/lib/viewport/scroll-viewport.ts index 64c66c77..0b60e01e 100644 --- a/projects/ngx-scrollbar/src/lib/viewport/scroll-viewport.ts +++ b/projects/ngx-scrollbar/src/lib/viewport/scroll-viewport.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, inject, } from '@angular/core'; +import { Directive, inject, ElementRef } from '@angular/core'; @Directive({ standalone: true, diff --git a/projects/ngx-scrollbar/src/lib/viewport/viewport-adapter.ts b/projects/ngx-scrollbar/src/lib/viewport/viewport-adapter.ts index 192bc0ec..26054274 100644 --- a/projects/ngx-scrollbar/src/lib/viewport/viewport-adapter.ts +++ b/projects/ngx-scrollbar/src/lib/viewport/viewport-adapter.ts @@ -1,10 +1,11 @@ -import { signal, WritableSignal } from '@angular/core'; +import { Injectable, signal, WritableSignal } from '@angular/core'; import { ViewportClasses } from '../utils/common'; /** * Class representing a viewport adapter. * Provides methods and properties to interact with a viewport and its content. */ +@Injectable() export class ViewportAdapter { /** @@ -77,8 +78,7 @@ export class ViewportAdapter { // and a spacer element will have the real size // Therefore, if spaceElement is provided, it will be observed instead of the content wrapper if (spacerElement) { - // Set relative position on the spacer element to enable the functionality of sticky for the scrollbars - spacerElement.style.position = 'relative'; + spacerElement.classList.add(ViewportClasses.Spacer); this.contentWrapperElement = spacerElement; } else { // If spacer is not provided, set it as the content wrapper @@ -87,6 +87,12 @@ export class ViewportAdapter { this.initialized.set(true); } + reset(): void { + this.nativeElement = null; + this.contentWrapperElement = null; + this.initialized.set(false); + } + /** * Scrolls the viewport vertically to the specified value. */ diff --git a/projects/ngx-scrollbar/src/public-api.ts b/projects/ngx-scrollbar/src/public-api.ts index c450d1ab..0f05e202 100644 --- a/projects/ngx-scrollbar/src/public-api.ts +++ b/projects/ngx-scrollbar/src/public-api.ts @@ -3,5 +3,6 @@ export * from './lib/ng-scrollbar.model'; export * from './lib/ng-scrollbar'; export * from './lib/ng-scrollbar-ext'; export * from './lib/async-detection'; +export * from './lib/sync-spacer'; export * from './lib/utils/scrollbar-base'; export * from './lib/viewport/scroll-viewport';