|
|
@ -1,8 +1,10 @@ |
|
|
import { |
|
|
import { |
|
|
|
|
|
afterNextRender, |
|
|
AfterViewInit, |
|
|
AfterViewInit, |
|
|
ChangeDetectionStrategy, |
|
|
ChangeDetectionStrategy, |
|
|
Component, |
|
|
Component, |
|
|
computed, |
|
|
computed, |
|
|
|
|
|
DestroyRef, |
|
|
inject, |
|
|
inject, |
|
|
Injector, |
|
|
Injector, |
|
|
LOCALE_ID, |
|
|
LOCALE_ID, |
|
|
@ -17,9 +19,10 @@ import { |
|
|
contentChild, |
|
|
contentChild, |
|
|
viewChild, |
|
|
viewChild, |
|
|
} from '@angular/core'; |
|
|
} from '@angular/core'; |
|
|
|
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
|
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; |
|
|
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; |
|
|
|
|
|
|
|
|
import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; |
|
|
import { Observable, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; |
|
|
|
|
|
|
|
|
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; |
|
|
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; |
|
|
import { |
|
|
import { |
|
|
@ -92,6 +95,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; |
|
|
}) |
|
|
}) |
|
|
export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestroy { |
|
|
export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestroy { |
|
|
readonly #injector = inject(Injector); |
|
|
readonly #injector = inject(Injector); |
|
|
|
|
|
readonly #destroyRef = inject(DestroyRef); |
|
|
readonly getInjected = this.#injector.get.bind(this.#injector); |
|
|
readonly getInjected = this.#injector.get.bind(this.#injector); |
|
|
protected readonly locale = inject(LOCALE_ID); |
|
|
protected readonly locale = inject(LOCALE_ID); |
|
|
protected readonly config = inject(ConfigStateService); |
|
|
protected readonly config = inject(ConfigStateService); |
|
|
@ -186,6 +190,10 @@ export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestr |
|
|
}, |
|
|
}, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
private horizontalScrollOffset = 0; |
|
|
|
|
|
|
|
|
|
|
|
private hasPendingHorizontalScrollOffset = false; |
|
|
|
|
|
|
|
|
hasAtLeastOnePermittedAction: boolean; |
|
|
hasAtLeastOnePermittedAction: boolean; |
|
|
|
|
|
|
|
|
readonly propList: EntityPropList<R>; |
|
|
readonly propList: EntityPropList<R>; |
|
|
@ -232,6 +240,7 @@ export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestr |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this._data.set(dataValue.map((record, index) => this.prepareRecord(record, index))); |
|
|
this._data.set(dataValue.map((record, index) => this.prepareRecord(record, index))); |
|
|
|
|
|
this.restoreHorizontalScrollOffset(); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -363,13 +372,66 @@ export class ExtensibleTableComponent<R = any> implements AfterViewInit, OnDestr |
|
|
ngAfterViewInit(): void { |
|
|
ngAfterViewInit(): void { |
|
|
if (!this.infiniteScroll()) { |
|
|
if (!this.infiniteScroll()) { |
|
|
this.list() |
|
|
this.list() |
|
|
?.requestStatus$?.pipe(filter(status => status === 'loading')) |
|
|
?.requestStatus$?.pipe(takeUntilDestroyed(this.#destroyRef)) |
|
|
.subscribe(() => { |
|
|
.subscribe(status => { |
|
|
this._data.set([]); |
|
|
if (status === 'loading') { |
|
|
|
|
|
this.rememberHorizontalScrollOffset(); |
|
|
|
|
|
this._data.set([]); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// A failed request never reaches the data input, restore from here instead
|
|
|
|
|
|
if (status === 'error') { |
|
|
|
|
|
this.restoreHorizontalScrollOffset(); |
|
|
|
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private getBodyElement(): HTMLElement | null { |
|
|
|
|
|
if (!this.isBrowser) { |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return this.table()?.element?.querySelector('datatable-body') ?? null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// The element carrying the column width is gone while loading without rows, so the browser
|
|
|
|
|
|
// resets the horizontal scroll position and the header keeps the offset it had before
|
|
|
|
|
|
private rememberHorizontalScrollOffset(): void { |
|
|
|
|
|
const body = this.getBodyElement(); |
|
|
|
|
|
this.hasPendingHorizontalScrollOffset = true; |
|
|
|
|
|
|
|
|
|
|
|
// Not scrollable while a previous request is still in flight, keep the offset taken back then
|
|
|
|
|
|
if (body && body.scrollWidth > body.clientWidth) { |
|
|
|
|
|
this.horizontalScrollOffset = body.scrollLeft; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private restoreHorizontalScrollOffset(): void { |
|
|
|
|
|
if (!this.hasPendingHorizontalScrollOffset) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.hasPendingHorizontalScrollOffset = false; |
|
|
|
|
|
|
|
|
|
|
|
afterNextRender( |
|
|
|
|
|
() => { |
|
|
|
|
|
const body = this.getBodyElement(); |
|
|
|
|
|
|
|
|
|
|
|
if (!body) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
body.scrollLeft = this.horizontalScrollOffset; |
|
|
|
|
|
|
|
|
|
|
|
// The header only follows a scroll event, scrolling to the same position does not raise one
|
|
|
|
|
|
body.dispatchEvent(new Event('scroll')); |
|
|
|
|
|
}, |
|
|
|
|
|
{ injector: this.#injector }, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
ngOnDestroy(): void { |
|
|
ngOnDestroy(): void { |
|
|
this.loadMoreSubscription.unsubscribe(); |
|
|
this.loadMoreSubscription.unsubscribe(); |
|
|
} |
|
|
} |
|
|
|