mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
fix(isa-app-scroll-container): Fixed issue with reloading on several lists
Ref: #5237
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { debounceTime, filter } from 'rxjs/operators';
|
||||
|
||||
@Directive({
|
||||
selector: '[uiScrollContainer]',
|
||||
@@ -27,30 +27,48 @@ export class UiScrollContainerDirective implements OnChanges, OnInit {
|
||||
@Input()
|
||||
deltaEnd = 0;
|
||||
|
||||
private scrollEvent$ = new Subject<Event>();
|
||||
private scrollEvent$ = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Tracks the last scrollHeight when reachEnd was emitted.
|
||||
* This prevents duplicate emissions at the same scroll position after content loads.
|
||||
*/
|
||||
private lastEmittedScrollHeight = 0;
|
||||
|
||||
@Output()
|
||||
reachStart = this.scrollEvent$.pipe(
|
||||
filter((event) => {
|
||||
debounceTime(100),
|
||||
filter(() => {
|
||||
if (this.direction === 'vertical') {
|
||||
const top = this.nativeElement.scrollTop;
|
||||
return top <= this.deltaStart;
|
||||
} else {
|
||||
throw new Error('not implemented');
|
||||
return this.nativeElement.scrollTop <= this.deltaStart;
|
||||
}
|
||||
throw new Error('Horizontal scroll not implemented');
|
||||
}),
|
||||
);
|
||||
|
||||
@Output()
|
||||
reachEnd = this.scrollEvent$.pipe(
|
||||
filter((event) => {
|
||||
debounceTime(100),
|
||||
filter(() => {
|
||||
if (this.direction === 'vertical') {
|
||||
const top = this.nativeElement.scrollTop;
|
||||
const height = this.nativeElement.scrollHeight - this.nativeElement.clientHeight - this.deltaEnd;
|
||||
return top >= height;
|
||||
} else {
|
||||
throw new Error('not implemented');
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.nativeElement;
|
||||
const threshold = scrollHeight - clientHeight - this.deltaEnd;
|
||||
const isAtEnd = scrollTop >= threshold;
|
||||
|
||||
if (!isAtEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only emit if scrollHeight changed (new content loaded)
|
||||
// This prevents re-emitting when user is still at end after a load
|
||||
if (scrollHeight !== this.lastEmittedScrollHeight) {
|
||||
this.lastEmittedScrollHeight = scrollHeight;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
throw new Error('Horizontal scroll not implemented');
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -71,17 +89,33 @@ export class UiScrollContainerDirective implements OnChanges, OnInit {
|
||||
ngOnChanges({ direction }: SimpleChanges): void {
|
||||
if (direction) {
|
||||
if (this.direction === 'horizontal') {
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-x', 'auto');
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-y', 'auto');
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-x',
|
||||
'auto',
|
||||
);
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-y',
|
||||
'auto',
|
||||
);
|
||||
} else {
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-y', 'auto');
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow-x', 'hidden');
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-y',
|
||||
'auto',
|
||||
);
|
||||
this.renderer.setStyle(
|
||||
this.elementRef.nativeElement,
|
||||
'overflow-x',
|
||||
'hidden',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('scroll', ['$event'])
|
||||
onScroll(event: Event) {
|
||||
this.scrollEvent$.next(event);
|
||||
@HostListener('scroll')
|
||||
onScroll() {
|
||||
this.scrollEvent$.next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,27 @@
|
||||
(reachEnd)="reachedEnd()"
|
||||
(reachStart)="reachedStart()"
|
||||
[deltaEnd]="deltaEnd"
|
||||
>
|
||||
@if (!loading) {
|
||||
>
|
||||
@if (!loading || itemLength > 0) {
|
||||
<ng-content></ng-content>
|
||||
} @else {
|
||||
@if (useLoadAnimation) {
|
||||
}
|
||||
|
||||
@if (loading && useLoadAnimation) {
|
||||
@if (itemLength === 0 || itemLength === undefined) {
|
||||
<!-- Initial load: show multiple skeletons -->
|
||||
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
|
||||
@for (skeletons of createSkeletons(); track skeletons) {
|
||||
@for (skeleton of createSkeletons(); track skeleton) {
|
||||
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
|
||||
}
|
||||
} @else {
|
||||
<ui-content-loader [loading]="loading"></ui-content-loader>
|
||||
<!-- Load more: show single skeleton at the end -->
|
||||
<ui-skeleton-loader [template]="skeletonTemplate"></ui-skeleton-loader>
|
||||
}
|
||||
}
|
||||
|
||||
@if (loading && !useLoadAnimation) {
|
||||
<ui-content-loader [loading]="loading"></ui-content-loader>
|
||||
}
|
||||
|
||||
@if (showSpacer && !loading) {
|
||||
<div class="spacer"></div>
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UiScrollContainerComponent implements OnInit {
|
||||
export class UiScrollContainerComponent implements OnInit, OnChanges {
|
||||
@ViewChild('scrollContainer', { read: ElementRef, static: true })
|
||||
scrollContainer: ElementRef;
|
||||
|
||||
@@ -61,14 +63,49 @@ export class UiScrollContainerComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
createSkeletons() {
|
||||
if (this.itemLength && this.itemLength !== 0) {
|
||||
return Array.from(Array(this.itemLength - 1), (_, i) => i);
|
||||
} else {
|
||||
return [];
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// When new items are loaded, adjust scroll position so user can scroll down again
|
||||
if (changes['itemLength']) {
|
||||
const prevLength = changes['itemLength'].previousValue ?? 0;
|
||||
const newLength = changes['itemLength'].currentValue ?? 0;
|
||||
|
||||
// Only adjust if items were added (not on initial load or reset)
|
||||
if (newLength > prevLength && prevLength > 0) {
|
||||
this.adjustScrollPositionAfterLoad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After new items are loaded, adjust scroll position so user is not at the very end.
|
||||
* This allows them to scroll down again to trigger the next load.
|
||||
*/
|
||||
private adjustScrollPositionAfterLoad(): void {
|
||||
const el = this.scrollContainer?.nativeElement;
|
||||
if (!el) return;
|
||||
|
||||
// Wait for DOM to update with new items
|
||||
setTimeout(() => {
|
||||
const maxScroll = el.scrollHeight - el.clientHeight;
|
||||
const currentScroll = el.scrollTop;
|
||||
|
||||
// Only adjust if we're at or very near the end
|
||||
if (currentScroll >= maxScroll - this.deltaEnd - 20) {
|
||||
// Move scroll position up by deltaEnd + buffer so user has room to scroll
|
||||
const offset = this.deltaEnd + 100;
|
||||
const targetScroll = Math.max(0, maxScroll - offset);
|
||||
el.scrollTop = targetScroll;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
createSkeletons(): number[] {
|
||||
if (this.itemLength && this.itemLength !== 0) {
|
||||
return Array.from({ length: this.itemLength - 1 }, (_, i) => i);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
reachedEnd() {
|
||||
this.reachEnd.emit();
|
||||
}
|
||||
@@ -79,7 +116,8 @@ export class UiScrollContainerComponent implements OnInit {
|
||||
|
||||
get scrollPersantage() {
|
||||
const scrollHeight =
|
||||
this.scrollContainer?.nativeElement?.scrollHeight - this.scrollContainer?.nativeElement?.clientHeight;
|
||||
this.scrollContainer?.nativeElement?.scrollHeight -
|
||||
this.scrollContainer?.nativeElement?.clientHeight;
|
||||
if (scrollHeight === 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -95,7 +133,10 @@ export class UiScrollContainerComponent implements OnInit {
|
||||
|
||||
scrollTo(top: number) {
|
||||
setTimeout(() => {
|
||||
this.scrollContainer?.nativeElement?.scrollTo({ top, behavior: 'smooth' });
|
||||
this.scrollContainer?.nativeElement?.scrollTo({
|
||||
top,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user