mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Enhanced return details and search components with new features and improvements.
- ✨ **Feature**: Added InViewport directive for element visibility detection - ✨ **Feature**: Introduced new button for navigation in return details - 🛠️ **Refactor**: Improved scroll position restoration logic and removed deprecated files - 📚 **Docs**: Updated README with usage instructions for new directives Ref: #5034
This commit is contained in:
1
.github/review-instructions.md
vendored
1
.github/review-instructions.md
vendored
@@ -37,6 +37,7 @@ When conducting a code review, follow these steps to ensure a thorough and const
|
|||||||
## Additional Informations
|
## Additional Informations
|
||||||
|
|
||||||
- Treat missing tests and JSDocs as warnings
|
- Treat missing tests and JSDocs as warnings
|
||||||
|
- Tread missing unit test as warnings
|
||||||
|
|
||||||
### Review Template
|
### Review Template
|
||||||
|
|
||||||
|
|||||||
17
docs/templates/readme-library-template.md
vendored
Normal file
17
docs/templates/readme-library-template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# [Library Name]
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<!-- Brief description of the library and its purpose -->
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
<!-- Key features or modules -->
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
<!-- List Functions, Components, Services, etc. with their api and their purpose -->
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
<!-- Sample code snippet or usage instructions -->
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
@let receipt = receiptResult().data;
|
@let receipt = receiptResult().data;
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="fixed bottom-6 right-6"
|
class="fixed bottom-6 right-6"
|
||||||
uiButton
|
uiButton
|
||||||
@@ -11,49 +10,64 @@
|
|||||||
Rückgabe starten
|
Rückgabe starten
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (receipt) {
|
<div>
|
||||||
<oms-feature-return-details-header [buyer]="receipt.buyer"></oms-feature-return-details-header>
|
<button
|
||||||
|
uiButton
|
||||||
|
color="tertiary"
|
||||||
|
size="small"
|
||||||
|
class="px-[0.875rem] py-1 min-w-0 bg-white gap-1"
|
||||||
|
(click)="location.back()"
|
||||||
|
>
|
||||||
|
<ng-icon name="isaActionChevronLeft" size="1.5rem" class="-ml-2"></ng-icon>
|
||||||
|
<span>zurück</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (showMore()) {
|
@if (receipt) {
|
||||||
<oms-feature-return-details-order-group-data
|
<div class="flex flex-col items-start justify-stretch gap-6 rounded-2xl bg-isa-white px-4 py-6">
|
||||||
[receipt]="receipt"
|
<oms-feature-return-details-header [buyer]="receipt.buyer"></oms-feature-return-details-header>
|
||||||
></oms-feature-return-details-order-group-data>
|
|
||||||
<button
|
@if (showMore()) {
|
||||||
class="-ml-3"
|
<oms-feature-return-details-order-group-data
|
||||||
uiTextButton
|
[receipt]="receipt"
|
||||||
type="button"
|
></oms-feature-return-details-order-group-data>
|
||||||
color="strong"
|
<button
|
||||||
size="small"
|
class="-ml-3"
|
||||||
(click)="showMore.set(false)"
|
uiTextButton
|
||||||
>
|
type="button"
|
||||||
<ng-icon name="isaActionMinus"></ng-icon>
|
color="strong"
|
||||||
Weniger anzeigen
|
size="small"
|
||||||
</button>
|
(click)="showMore.set(false)"
|
||||||
} @else {
|
>
|
||||||
<oms-feature-return-details-data [receipt]="receipt"></oms-feature-return-details-data>
|
<ng-icon name="isaActionMinus"></ng-icon>
|
||||||
<button
|
Weniger anzeigen
|
||||||
class="-ml-3"
|
</button>
|
||||||
uiTextButton
|
} @else {
|
||||||
type="button"
|
<oms-feature-return-details-data [receipt]="receipt"></oms-feature-return-details-data>
|
||||||
color="strong"
|
<button
|
||||||
size="small"
|
class="-ml-3"
|
||||||
(click)="showMore.set(true)"
|
uiTextButton
|
||||||
>
|
type="button"
|
||||||
<ng-icon name="isaActionPlus"></ng-icon>
|
color="strong"
|
||||||
Bestelldetails anzeigen
|
size="small"
|
||||||
</button>
|
(click)="showMore.set(true)"
|
||||||
}
|
>
|
||||||
<div></div>
|
<ng-icon name="isaActionPlus"></ng-icon>
|
||||||
<oms-feature-return-details-order-group
|
Bestelldetails anzeigen
|
||||||
[items]="receiptItems()"
|
</button>
|
||||||
[(selectedItems)]="selectedItems"
|
}
|
||||||
></oms-feature-return-details-order-group>
|
<div></div>
|
||||||
@for (item of receipt.items; track item.id; let last = $last) {
|
<oms-feature-return-details-order-group
|
||||||
<oms-feature-return-details-order-group-item
|
[items]="receiptItems()"
|
||||||
class="border-b border-solid border-isa-neutral-300 last:border-none"
|
[(selectedItems)]="selectedItems"
|
||||||
[item]="item.data"
|
></oms-feature-return-details-order-group>
|
||||||
(selectedChange)="selectItemById(item.id, $event)"
|
@for (item of receipt.items; track item.id; let last = $last) {
|
||||||
[selected]="selectedItemIds().includes(item.id)"
|
<oms-feature-return-details-order-group-item
|
||||||
></oms-feature-return-details-order-group-item>
|
class="border-b border-solid border-isa-neutral-300 last:border-none"
|
||||||
}
|
[item]="item.data"
|
||||||
|
(selectedChange)="selectItemById(item.id, $event)"
|
||||||
|
[selected]="selectedItemIds().includes(item.id)"
|
||||||
|
></oms-feature-return-details-order-group-item>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
:host {
|
:host {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: stretch;
|
||||||
gap: 1.5rem;
|
justify-content: stretch;
|
||||||
align-self: stretch;
|
gap: 1rem;
|
||||||
|
|
||||||
border-radius: 1rem;
|
|
||||||
background: #fff;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
import { isaActionPlus, isaActionMinus } from '@isa/icons';
|
import { isaActionPlus, isaActionMinus, isaActionChevronLeft } from '@isa/icons';
|
||||||
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
|
||||||
import { ReceiptItem, ReturnDetailsStore, ReturnProcessStore } from '@isa/oms/data-access';
|
import { ReceiptItem, ReturnDetailsStore, ReturnProcessStore } from '@isa/oms/data-access';
|
||||||
import { injectActivatedProcessId } from '@isa/core/process';
|
import { injectActivatedProcessId } from '@isa/core/process';
|
||||||
@@ -21,6 +21,7 @@ import { ReturnDetailsHeaderComponent } from './return-details-header/return-det
|
|||||||
import { ReturnDetailsOrderGroupComponent } from './return-details-order-group/return-details-order-group.component';
|
import { ReturnDetailsOrderGroupComponent } from './return-details-order-group/return-details-order-group.component';
|
||||||
import { ReturnDetailsOrderGroupItemComponent } from './return-details-order-group-item/return-details-order-group-item.component';
|
import { ReturnDetailsOrderGroupItemComponent } from './return-details-order-group-item/return-details-order-group-item.component';
|
||||||
import { ReturnDetailsOrderGroupDataComponent } from './return-details-order-group-data/return-details-order-group-data.component';
|
import { ReturnDetailsOrderGroupDataComponent } from './return-details-order-group-data/return-details-order-group-data.component';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'oms-feature-return-details',
|
selector: 'oms-feature-return-details',
|
||||||
@@ -37,7 +38,7 @@ import { ReturnDetailsOrderGroupDataComponent } from './return-details-order-gro
|
|||||||
ReturnDetailsOrderGroupItemComponent,
|
ReturnDetailsOrderGroupItemComponent,
|
||||||
ReturnDetailsOrderGroupDataComponent,
|
ReturnDetailsOrderGroupDataComponent,
|
||||||
],
|
],
|
||||||
providers: [provideIcons({ isaActionPlus, isaActionMinus })],
|
providers: [provideIcons({ isaActionPlus, isaActionMinus, isaActionChevronLeft })],
|
||||||
})
|
})
|
||||||
export class ReturnDetailsComponent {
|
export class ReturnDetailsComponent {
|
||||||
private processId = injectActivatedProcessId();
|
private processId = injectActivatedProcessId();
|
||||||
@@ -46,6 +47,8 @@ export class ReturnDetailsComponent {
|
|||||||
|
|
||||||
private _activatedRoute = inject(ActivatedRoute);
|
private _activatedRoute = inject(ActivatedRoute);
|
||||||
|
|
||||||
|
location = inject(Location);
|
||||||
|
|
||||||
params = toSignal(this._activatedRoute.params);
|
params = toSignal(this._activatedRoute.params);
|
||||||
|
|
||||||
selectedItems = signal<ReceiptItem[]>([]);
|
selectedItems = signal<ReceiptItem[]>([]);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
|
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
|
||||||
</div>
|
</div>
|
||||||
} @else if (renderPageTrigger()) {
|
} @else if (renderPageTrigger()) {
|
||||||
<div (utilScrolledIntoViewport)="paging($event)"></div>
|
<div (uiInViewport)="paging($event)"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else if (renderSearchLoader()) {
|
} @else if (renderSearchLoader()) {
|
||||||
|
|||||||
@@ -22,12 +22,9 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|||||||
import { isaActionSort } from '@isa/icons';
|
import { isaActionSort } from '@isa/icons';
|
||||||
import { ReceiptListItem, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
|
import { ReceiptListItem, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
|
||||||
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
|
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
|
||||||
import { BreakpointDirective } from '@isa/ui/layout';
|
import { BreakpointDirective, InViewportDirective } from '@isa/ui/layout';
|
||||||
import { CallbackResult, ListResponseArgs } from '@isa/common/result';
|
import { CallbackResult, ListResponseArgs } from '@isa/common/result';
|
||||||
import {
|
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||||
injectRestoreScrollPosition,
|
|
||||||
ScrolledIntoViewportDirective,
|
|
||||||
} from '@isa/utils/scroll-position';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'oms-feature-return-search-result',
|
selector: 'oms-feature-return-search-result',
|
||||||
@@ -44,7 +41,7 @@ import {
|
|||||||
NgIconComponent,
|
NgIconComponent,
|
||||||
FilterMenuButtonComponent,
|
FilterMenuButtonComponent,
|
||||||
BreakpointDirective,
|
BreakpointDirective,
|
||||||
ScrolledIntoViewportDirective,
|
InViewportDirective,
|
||||||
],
|
],
|
||||||
providers: [provideIcons({ isaActionSort })],
|
providers: [provideIcons({ isaActionSort })],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ The `uiBreakpoint` directive conditionally includes a template based on the view
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### InViewport Directive
|
||||||
|
|
||||||
|
Use this directive to emit an event whenever the host element enters or leaves the viewport.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<some-element uiInViewport (uiInViewport)="onInViewportChange($event)"> ... </some-element>
|
||||||
|
```
|
||||||
|
|
||||||
## Breakpoints Table
|
## Breakpoints Table
|
||||||
|
|
||||||
| Breakpoint | CSS Media Query Selector |
|
| Breakpoint | CSS Media Query Selector |
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './lib/breakpoint.directive';
|
export * from './lib/breakpoint.directive';
|
||||||
export * from './lib/breakpoint';
|
export * from './lib/breakpoint';
|
||||||
|
export * from './lib/in-viewport.directive';
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import { Directive, ElementRef, AfterViewInit, OnDestroy, output } from '@angular/core';
|
import { Directive, ElementRef, AfterViewInit, OnDestroy, output } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive that emits an event when its host element enters or leaves the viewport.
|
||||||
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[utilScrolledIntoViewport]',
|
selector: '[uiInViewport]',
|
||||||
|
exportAs: 'uiInViewport',
|
||||||
})
|
})
|
||||||
export class ScrolledIntoViewportDirective implements AfterViewInit, OnDestroy {
|
export class InViewportDirective implements AfterViewInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Emits true when the element enters the viewport and false when it leaves.
|
* Emits true when the element enters the viewport and false when it leaves.
|
||||||
*/
|
*/
|
||||||
scrolledIntoViewport = output<boolean>({ alias: 'utilScrolledIntoViewport' });
|
inViewport = output<boolean>({ alias: 'uiInViewport' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the element enters the viewport.
|
||||||
|
*/
|
||||||
|
enteredViewport = output<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the element leaves the viewport.
|
||||||
|
*/
|
||||||
|
leftViewport = output<void>();
|
||||||
|
|
||||||
private observer: IntersectionObserver | null = null;
|
private observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
@@ -18,9 +32,11 @@ export class ScrolledIntoViewportDirective implements AfterViewInit, OnDestroy {
|
|||||||
(entries) => {
|
(entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.scrolledIntoViewport.emit(true);
|
this.inViewport.emit(true);
|
||||||
|
this.enteredViewport.emit();
|
||||||
} else {
|
} else {
|
||||||
this.scrolledIntoViewport.emit(false);
|
this.inViewport.emit(false);
|
||||||
|
this.leftViewport.emit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1,7 +1,66 @@
|
|||||||
# utils-scroll-position
|
# Scroll Position Library
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev).
|
## Overview
|
||||||
|
|
||||||
## Running unit tests
|
Provides utilities for storing, restoring, and observing scroll position across navigation or component view changes.
|
||||||
|
|
||||||
Run `nx test utils-scroll-position` to execute the unit tests.
|
## Features
|
||||||
|
|
||||||
|
- Store current scroll position in session storage.
|
||||||
|
- Restore saved scroll position with an optional delay.
|
||||||
|
- Observe when an element enters or leaves the viewport.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Storing Scroll Position
|
||||||
|
|
||||||
|
Call the function to save the current scroll position:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { storeScrollPosition } from '@isa/utils/scroll-position';
|
||||||
|
|
||||||
|
storeScrollPosition();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restoring Scroll Position
|
||||||
|
|
||||||
|
Inject the restore function and call it with an optional delay:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||||
|
|
||||||
|
const restorePosition = injectRestoreScrollPosition();
|
||||||
|
await restorePosition(200);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Restoration
|
||||||
|
|
||||||
|
Provide environment initializer in your app module to auto-save scroll positions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||||
|
|
||||||
|
provideScrollPositionRestoration();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Marking a Route for Auto Restoration
|
||||||
|
|
||||||
|
To enable automatic scroll restoration on a specific route, add the “scrollPositionRestoration” property in its data:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
path: 'example',
|
||||||
|
component: ExampleComponent,
|
||||||
|
data: {
|
||||||
|
scrollPositionRestoration: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detecting Element Visibility
|
||||||
|
|
||||||
|
Apply the directive to a component template to emit true or false when entering or exiting the viewport:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div utilScrolledIntoViewport (utilScrolledIntoViewport)="onVisibilityChange($event)">...</div>
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './lib/scroll-position-restoration';
|
export * from './lib/inject-restore-scroll-position';
|
||||||
export * from './lib/scrolled-into-viewport.directive';
|
export * from './lib/provide-scroll-position-restoration';
|
||||||
|
export * from './lib/store-scroll-position';
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ViewportScroller } from '@angular/common';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { SessionStorageProvider } from '@isa/core/storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that restores the scroll position from session storage.
|
||||||
|
*
|
||||||
|
* The returned function waits for an optional delay to ensure the DOM is ready before scrolling.
|
||||||
|
*
|
||||||
|
* @returns A function accepting an optional delay (in milliseconds) and restoring scroll position.
|
||||||
|
*/
|
||||||
|
export function injectRestoreScrollPosition(): () => Promise<void> {
|
||||||
|
const router = inject(Router);
|
||||||
|
const viewportScroller = inject(ViewportScroller);
|
||||||
|
const sessionStorage = inject(SessionStorageProvider);
|
||||||
|
|
||||||
|
return async (delay = 0) => {
|
||||||
|
const url = router.url;
|
||||||
|
const position = await sessionStorage.get(url);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
// wait for the next tick to ensure the DOM is ready
|
||||||
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
sessionStorage.clear(url);
|
||||||
|
viewportScroller.scrollToPosition(position as [number, number]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { ViewportScroller } from '@angular/common';
|
||||||
|
import { EnvironmentProviders, inject, provideEnvironmentInitializer } from '@angular/core';
|
||||||
|
import { NavigationStart, Router } from '@angular/router';
|
||||||
|
import { SessionStorageProvider } from '@isa/core/storage';
|
||||||
|
import { getDeepestActivatedRoute } from './utils/get-deepest-activated-route';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an environment initializer that restores scroll position upon navigation and page unload.
|
||||||
|
*
|
||||||
|
* Listens to router navigation events and the window's beforeunload event to store the current scroll position.
|
||||||
|
*
|
||||||
|
* @returns EnvironmentProviders for scroll position restoration.
|
||||||
|
*/
|
||||||
|
export function provideScrollPositionRestoration(): EnvironmentProviders {
|
||||||
|
return provideEnvironmentInitializer(() => {
|
||||||
|
const router = inject(Router);
|
||||||
|
const viewportScroller = inject(ViewportScroller);
|
||||||
|
const sessionStorage = inject(SessionStorageProvider);
|
||||||
|
|
||||||
|
function storeScrollPosition() {
|
||||||
|
const url = router.url;
|
||||||
|
const route = getDeepestActivatedRoute(router.routerState.root);
|
||||||
|
|
||||||
|
if (route.snapshot.data?.['scrollPositionRestoration']) {
|
||||||
|
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window) {
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
storeScrollPosition();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
|
||||||
|
if (event instanceof NavigationStart) {
|
||||||
|
storeScrollPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { ViewportScroller } from '@angular/common';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import { EnvironmentProviders, inject, provideEnvironmentInitializer } from '@angular/core';
|
|
||||||
import { ActivatedRoute, NavigationStart, Router } from '@angular/router';
|
|
||||||
import { SessionStorageProvider } from '@isa/core/storage';
|
|
||||||
|
|
||||||
// const route: Route = {
|
|
||||||
// component: AnyComponent,
|
|
||||||
// data: {
|
|
||||||
// scrollPositionRestoration: true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const getDeepestActivatedRoute = (route: ActivatedRoute): ActivatedRoute => {
|
|
||||||
while (route.firstChild) {
|
|
||||||
route = route.firstChild;
|
|
||||||
}
|
|
||||||
return route;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function provideScrollPositionRestoration(): EnvironmentProviders {
|
|
||||||
return provideEnvironmentInitializer(() => {
|
|
||||||
const router = inject(Router);
|
|
||||||
const viewportScroller = inject(ViewportScroller);
|
|
||||||
const sessionStorage = inject(SessionStorageProvider);
|
|
||||||
|
|
||||||
function storeScrollPosition() {
|
|
||||||
const url = router.url;
|
|
||||||
const route = getDeepestActivatedRoute(router.routerState.root);
|
|
||||||
|
|
||||||
if (route.snapshot.data?.['scrollPositionRestoration']) {
|
|
||||||
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window) {
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
storeScrollPosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
|
|
||||||
if (event instanceof NavigationStart) {
|
|
||||||
storeScrollPosition();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function storeScrollPosition() {
|
|
||||||
const router = inject(Router);
|
|
||||||
const viewportScroller = inject(ViewportScroller);
|
|
||||||
const sessionStorage = inject(SessionStorageProvider);
|
|
||||||
|
|
||||||
const url = router.url;
|
|
||||||
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function injectRestoreScrollPosition(): () => Promise<void> {
|
|
||||||
const router = inject(Router);
|
|
||||||
const viewportScroller = inject(ViewportScroller);
|
|
||||||
const sessionStorage = inject(SessionStorageProvider);
|
|
||||||
|
|
||||||
return async (delay = 0) => {
|
|
||||||
const url = router.url;
|
|
||||||
const position = await sessionStorage.get(url);
|
|
||||||
|
|
||||||
if (position) {
|
|
||||||
// wait for the next tick to ensure the DOM is ready
|
|
||||||
await new Promise((r) => setTimeout(r, delay));
|
|
||||||
sessionStorage.clear(url);
|
|
||||||
viewportScroller.scrollToPosition(position as [number, number]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
17
libs/utils/scroll-position/src/lib/store-scroll-position.ts
Normal file
17
libs/utils/scroll-position/src/lib/store-scroll-position.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ViewportScroller } from '@angular/common';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { SessionStorageProvider } from '@isa/core/storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current scroll position in session storage.
|
||||||
|
* Uses the current router URL as the key.
|
||||||
|
*/
|
||||||
|
export async function storeScrollPosition() {
|
||||||
|
const router = inject(Router);
|
||||||
|
const viewportScroller = inject(ViewportScroller);
|
||||||
|
const sessionStorage = inject(SessionStorageProvider);
|
||||||
|
|
||||||
|
const url = router.url;
|
||||||
|
sessionStorage.set(url, viewportScroller.getScrollPosition());
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively retrieves the deepest activated route.
|
||||||
|
*
|
||||||
|
* @param route - The starting ActivatedRoute instance.
|
||||||
|
* @returns The deepest ActivatedRoute found.
|
||||||
|
*/
|
||||||
|
export const getDeepestActivatedRoute = (route: ActivatedRoute): ActivatedRoute => {
|
||||||
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user