mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 17:12:43 +00:00
refactor: remove virtual scroll
This commit is contained in:
committed by
Rainer Killinger
parent
e90286fc68
commit
e8dee1fd47
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2023 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -40,7 +40,6 @@ describe('favorites', function () {
|
|||||||
cy.get('.title').should('contain', text);
|
cy.get('.title').should('contain', text);
|
||||||
cy.get('stapps-favorite-button').click();
|
cy.get('stapps-favorite-button').click();
|
||||||
});
|
});
|
||||||
cy.get('cdk-virtual-scroll-viewport').should('be.not.visible');
|
|
||||||
cy.get('stapps-data-list').contains('Keine Ergebnisse').should('be.visible');
|
cy.get('stapps-data-list').contains('Keine Ergebnisse').should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,11 +95,11 @@ import {BookDetailContentComponent} from './types/book/book-detail-content.compo
|
|||||||
import {BookListItemComponent} from './types/book/book-list-item.component';
|
import {BookListItemComponent} from './types/book/book-list-item.component';
|
||||||
import {PeriodicalListItemComponent} from './types/periodical/periodical-list-item.component';
|
import {PeriodicalListItemComponent} from './types/periodical/periodical-list-item.component';
|
||||||
import {PeriodicalDetailContentComponent} from './types/periodical/periodical-detail-content.component';
|
import {PeriodicalDetailContentComponent} from './types/periodical/periodical-detail-content.component';
|
||||||
import {SCThingListItemVirtualScrollStrategyDirective} from './list/sc-thing-list-item-virtual-scroll-strategy.directive';
|
|
||||||
import {DataListItemHostDirective} from './list/data-list-item-host.directive';
|
import {DataListItemHostDirective} from './list/data-list-item-host.directive';
|
||||||
import {DataListItemHostDefaultComponent} from './list/data-list-item-host-default.component';
|
import {DataListItemHostDefaultComponent} from './list/data-list-item-host-default.component';
|
||||||
import {browserFactory, SimpleBrowser} from '../../util/browser.factory';
|
import {browserFactory, SimpleBrowser} from '../../util/browser.factory';
|
||||||
import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component';
|
import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.component';
|
||||||
|
import {SkeletonListComponent} from './list/skeleton-list.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module for handling data
|
* Module for handling data
|
||||||
@@ -128,6 +128,7 @@ import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.co
|
|||||||
EventListItemComponent,
|
EventListItemComponent,
|
||||||
FavoriteButtonComponent,
|
FavoriteButtonComponent,
|
||||||
FavoriteDetailContentComponent,
|
FavoriteDetailContentComponent,
|
||||||
|
SkeletonListComponent,
|
||||||
FavoriteListItemComponent,
|
FavoriteListItemComponent,
|
||||||
FoodDataListComponent,
|
FoodDataListComponent,
|
||||||
LocateActionChipComponent,
|
LocateActionChipComponent,
|
||||||
@@ -147,7 +148,6 @@ import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.co
|
|||||||
PlaceListItemComponent,
|
PlaceListItemComponent,
|
||||||
PlaceMensaDetailComponent,
|
PlaceMensaDetailComponent,
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
SCThingListItemVirtualScrollStrategyDirective,
|
|
||||||
SemesterDetailContentComponent,
|
SemesterDetailContentComponent,
|
||||||
SemesterListItemComponent,
|
SemesterListItemComponent,
|
||||||
DataListItemHostDirective,
|
DataListItemHostDirective,
|
||||||
@@ -209,7 +209,6 @@ import {DishCharacteristicsComponent} from './types/dish/dish-characteristics.co
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
SCThingListItemVirtualScrollStrategyDirective,
|
|
||||||
DataDetailComponent,
|
DataDetailComponent,
|
||||||
DataDetailContentComponent,
|
DataDetailContentComponent,
|
||||||
DataIconPipe,
|
DataIconPipe,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2023 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ContentChild,
|
ContentChild,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {SCThings} from '@openstapps/core';
|
import {SCThings} from '@openstapps/core';
|
||||||
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
||||||
|
import {IonInfiniteScroll} from '@ionic/angular';
|
||||||
|
|
||||||
export interface DataListContext<T> {
|
export interface DataListContext<T> {
|
||||||
$implicit: T;
|
$implicit: T;
|
||||||
@@ -43,11 +43,6 @@ export interface DataListContext<T> {
|
|||||||
styleUrls: ['data-list.scss'],
|
styleUrls: ['data-list.scss'],
|
||||||
})
|
})
|
||||||
export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
||||||
/**
|
|
||||||
* Amount of list items left to show (in percent) that should trigger a data reload
|
|
||||||
*/
|
|
||||||
private readonly reloadThreshold = 0.2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All SCThings to display
|
* All SCThings to display
|
||||||
*/
|
*/
|
||||||
@@ -86,7 +81,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
subscriptions: Subscription[] = [];
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
@ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
|
@ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signalizes that the data is being loaded
|
* Signalizes that the data is being loaded
|
||||||
@@ -113,6 +108,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (Array.isArray(this.items) && typeof changes.items !== 'undefined') {
|
if (Array.isArray(this.items) && typeof changes.items !== 'undefined') {
|
||||||
this.itemStream.next(this.items);
|
this.itemStream.next(this.items);
|
||||||
|
this.infiniteScroll.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +123,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
if (typeof this.resetToTop !== 'undefined') {
|
if (typeof this.resetToTop !== 'undefined') {
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.resetToTop.subscribe(() => {
|
this.resetToTop.subscribe(() => {
|
||||||
this.viewPort.scrollToIndex(0);
|
// this.viewPort.scrollToIndex(0);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -139,18 +135,4 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
|||||||
notifyLoadMore() {
|
notifyLoadMore() {
|
||||||
this.loadMore.emit();
|
this.loadMore.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to call whenever scroll view visible range changed
|
|
||||||
*/
|
|
||||||
scrolled(index: number) {
|
|
||||||
if (
|
|
||||||
// first condition prevents "load more" to be executed even before scrolling
|
|
||||||
index > 0 &&
|
|
||||||
(this.items?.length ?? 0) - this.viewPort.getRenderedRange().end <=
|
|
||||||
(this.items?.length ?? 0) * this.reloadThreshold
|
|
||||||
) {
|
|
||||||
this.notifyLoadMore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,29 +15,23 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="itemStream | async as items">
|
<ng-container *ngIf="itemStream | async as items">
|
||||||
<ng-content select="[header]"></ng-content>
|
<ng-content select="[header]"></ng-content>
|
||||||
<cdk-virtual-scroll-viewport
|
<ion-list [style.display]="items && items.length ? 'block' : 'none'">
|
||||||
scThingListItemVirtualScrollStrategy
|
<ng-container *ngFor="let item of items">
|
||||||
[style.display]="items && items.length ? 'block' : 'none'"
|
|
||||||
(loadMore)="notifyLoadMore()"
|
|
||||||
>
|
|
||||||
<ng-container *cdkVirtualFor="let item of items; trackBy: identifyItem">
|
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngTemplateOutlet="listItemTemplateRef || defaultListItem; context: {$implicit: item}"
|
*ngTemplateOutlet="listItemTemplateRef || defaultListItem; context: {$implicit: item}"
|
||||||
></ng-container>
|
></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</cdk-virtual-scroll-viewport>
|
<ion-infinite-scroll (ionInfinite)="notifyLoadMore()">
|
||||||
|
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||||
|
</ion-infinite-scroll>
|
||||||
|
</ion-list>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div [style.display]="!loading && items && items.length === 0 ? 'block' : 'none'">
|
<div [style.display]="!loading && items && items.length === 0 ? 'block' : 'none'">
|
||||||
<ion-label class="centeredMessageContainer">
|
<ion-label class="centeredMessageContainer">
|
||||||
{{ 'search.nothing_found' | translate | titlecase }}
|
{{ 'search.nothing_found' | translate | titlecase }}
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</div>
|
</div>
|
||||||
<ion-list [style.display]="loading ? 'block' : 'none'">
|
<skeleton-list [style.display]="loading ? 'block' : 'none'"></skeleton-list>
|
||||||
<stapps-skeleton-list-item
|
|
||||||
[hideThumbnail]="singleType"
|
|
||||||
*ngFor="let skeleton of [].constructor(skeletonItems)"
|
|
||||||
></stapps-skeleton-list-item>
|
|
||||||
</ion-list>
|
|
||||||
|
|
||||||
<ng-template let-item #defaultListItem>
|
<ng-template let-item #defaultListItem>
|
||||||
<stapps-data-list-item [item]="item" [hideThumbnail]="singleType"></stapps-data-list-item>
|
<stapps-data-list-item [item]="item" [hideThumbnail]="singleType"></stapps-data-list-item>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*!
|
/*!
|
||||||
* Copyright (C) 2022 StApps
|
* Copyright (C) 2023 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -13,17 +13,10 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
cdk-virtual-scroll-viewport {
|
ion-list {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
skeleton-list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
::ng-deep {
|
|
||||||
.cdk-virtual-scroll-content-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtual-scroll-expander {
|
|
||||||
clear: both;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2023 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Directive, forwardRef, Input, Output, ViewChild} from '@angular/core';
|
|
||||||
import {CdkVirtualForOf, VIRTUAL_SCROLL_STRATEGY} from '@angular/cdk/scrolling';
|
|
||||||
import {ScThingListItemVirtualScrollStrategy} from './sc-thing-list-item-virtual-scroll-strategy';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function factory(directive: SCThingListItemVirtualScrollStrategyDirective) {
|
|
||||||
return directive.scrollStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: 'cdk-virtual-scroll-viewport[scThingListItemVirtualScrollStrategy]',
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: VIRTUAL_SCROLL_STRATEGY,
|
|
||||||
useFactory: factory,
|
|
||||||
deps: [forwardRef(() => SCThingListItemVirtualScrollStrategyDirective)],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class SCThingListItemVirtualScrollStrategyDirective {
|
|
||||||
scrollStrategy = new ScThingListItemVirtualScrollStrategy();
|
|
||||||
|
|
||||||
@ViewChild(CdkVirtualForOf) virtualForOf: CdkVirtualForOf<unknown>;
|
|
||||||
|
|
||||||
@Output() readonly loadMore = this.scrollStrategy.loadMore;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
@Input() set trackGroupBy(value: (item: any) => unknown) {
|
|
||||||
this.scrollStrategy.trackGroupBy = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input() set minimumHeight(value: number) {
|
|
||||||
this.scrollStrategy.approximateItemHeight = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input() set buffer(value: number) {
|
|
||||||
this.scrollStrategy.buffer = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input() set gap(value: number) {
|
|
||||||
this.scrollStrategy.gap = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input() set itemRenderTimeout(value: number) {
|
|
||||||
this.scrollStrategy.itemRenderTimeout = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2023 StApps
|
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
|
||||||
* Software Foundation, version 3.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
||||||
import {CdkVirtualForOf, CdkVirtualScrollViewport, VirtualScrollStrategy} from '@angular/cdk/scrolling';
|
|
||||||
import {BehaviorSubject, Subject, Subscription, takeUntil, timer} from 'rxjs';
|
|
||||||
import {debounceTime, distinctUntilChanged, tap} from 'rxjs/operators';
|
|
||||||
import {SCThingType} from '@openstapps/core';
|
|
||||||
|
|
||||||
export class ScThingListItemVirtualScrollStrategy implements VirtualScrollStrategy {
|
|
||||||
private viewport?: CdkVirtualScrollViewport;
|
|
||||||
|
|
||||||
private virtualForOf?: CdkVirtualForOf<unknown>;
|
|
||||||
|
|
||||||
private index$ = new Subject<number>();
|
|
||||||
|
|
||||||
private heights = new Map<unknown, number | undefined>();
|
|
||||||
|
|
||||||
private groupHeights = new Map<unknown, number | undefined>();
|
|
||||||
|
|
||||||
private offsets: number[] = [];
|
|
||||||
|
|
||||||
private totalHeight = 0;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
private _items?: readonly unknown[];
|
|
||||||
|
|
||||||
private _groups?: readonly unknown[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We use this to track loadMore
|
|
||||||
*/
|
|
||||||
private currentLength = 0;
|
|
||||||
|
|
||||||
private dataStreamSubscription?: Subscription;
|
|
||||||
|
|
||||||
private mutationObserver?: MutationObserver;
|
|
||||||
|
|
||||||
approximateItemHeight = 67;
|
|
||||||
|
|
||||||
approximateGroupSizes: Map<unknown, number> = new Map([[SCThingType.AcademicEvent, 139]]);
|
|
||||||
|
|
||||||
buffer = 4000;
|
|
||||||
|
|
||||||
gap = 8;
|
|
||||||
|
|
||||||
itemRenderTimeout = 1000;
|
|
||||||
|
|
||||||
heightUpdateDebounceTime = 25;
|
|
||||||
|
|
||||||
heightSetDebounceTime = 100;
|
|
||||||
|
|
||||||
loadMore = new Subject<void>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
trackGroupBy: (value: any) => unknown = it => it.type;
|
|
||||||
|
|
||||||
attach(viewport: CdkVirtualScrollViewport): void {
|
|
||||||
this.viewport = viewport;
|
|
||||||
// @ts-expect-error private property
|
|
||||||
this.virtualForOf = viewport._forOf;
|
|
||||||
this.dataStreamSubscription = this.virtualForOf?.dataStream.subscribe(items => {
|
|
||||||
this.items = items;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mutationObserver = new MutationObserver(() => {
|
|
||||||
const renderedItems = this.renderedItems;
|
|
||||||
if (!renderedItems) {
|
|
||||||
this.mutationObserver?.disconnect();
|
|
||||||
return this.onPrematureDisconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect();
|
|
||||||
this.intersectionObserver = new IntersectionObserver(this.observeIntersection.bind(this), {
|
|
||||||
rootMargin: `${this.buffer - 64}px`,
|
|
||||||
threshold: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const node of renderedItems) {
|
|
||||||
const [item, group] = this.getItemByNode(node, renderedItems);
|
|
||||||
|
|
||||||
if (!this.heights.has(item)) {
|
|
||||||
this.intersectionObserver.observe(node);
|
|
||||||
} else {
|
|
||||||
node.style.height = `${this.getHeight(item, group)}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentWrapper = this.contentWrapper;
|
|
||||||
if (!contentWrapper) return this.onPrematureDisconnect();
|
|
||||||
this.mutationObserver.observe(contentWrapper, {childList: true, subtree: true});
|
|
||||||
|
|
||||||
this.setTotalContentSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
detach(): void {
|
|
||||||
this.index$.complete();
|
|
||||||
this.dataStreamSubscription?.unsubscribe();
|
|
||||||
this.mutationObserver?.disconnect();
|
|
||||||
delete this.viewport;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHeight(item: unknown, group: unknown) {
|
|
||||||
return (
|
|
||||||
this.heights.get(item) ||
|
|
||||||
this.groupHeights.get(group) ||
|
|
||||||
this.approximateGroupSizes.get(group) ||
|
|
||||||
this.approximateItemHeight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
private set items(value: readonly unknown[]) {
|
|
||||||
const trackBy = this.virtualForOf?.cdkVirtualForTrackBy;
|
|
||||||
const tracks = value.map((it, i) => (trackBy ? trackBy(i, it) : it));
|
|
||||||
if (
|
|
||||||
this._items &&
|
|
||||||
tracks.length === this._items.length &&
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
tracks.every((it, i) => it === this._items![i])
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._items = tracks;
|
|
||||||
this._groups = value.map(this.trackGroupBy);
|
|
||||||
this.currentLength = tracks.length;
|
|
||||||
|
|
||||||
this.updateHeights();
|
|
||||||
}
|
|
||||||
|
|
||||||
scrolledIndexChange = this.index$.pipe(distinctUntilChanged());
|
|
||||||
|
|
||||||
private getOffsetFromIndex(index: number): number {
|
|
||||||
return this.offsets[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getIndexFromOffset(offset: number): number {
|
|
||||||
return Math.max(
|
|
||||||
0,
|
|
||||||
this.offsets.findIndex((it, i, array) => it >= offset || i === array.length - 1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateHeights() {
|
|
||||||
if (!this._items) return;
|
|
||||||
const heights = this._items.map((it, i) => this.getHeight(it, this._groups![i]) + this.gap);
|
|
||||||
this.offsets = Array.from({length: heights.length}, () => 0);
|
|
||||||
this.totalHeight = heights.reduce((a, b, index) => (this.offsets[index + 1] = a + b), 0) + this.gap;
|
|
||||||
|
|
||||||
this.setTotalContentSize();
|
|
||||||
this.updateRenderedRange();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateRenderedRange() {
|
|
||||||
if (!this.viewport) return;
|
|
||||||
const offset = this.viewport.measureScrollOffset('top');
|
|
||||||
const viewportSize = this.viewport.getViewportSize();
|
|
||||||
const firstVisibleIndex = Math.max(0, this.getIndexFromOffset(offset) - 1);
|
|
||||||
const range = {
|
|
||||||
start: this.getIndexFromOffset(Math.max(0, offset - this.buffer)),
|
|
||||||
end: this.getIndexFromOffset(offset + viewportSize + this.buffer),
|
|
||||||
};
|
|
||||||
const {start, end} = this.viewport.getRenderedRange();
|
|
||||||
if (range.start === start && range.end === end) return;
|
|
||||||
|
|
||||||
if (this.currentLength !== 0 && range.end === this.currentLength) {
|
|
||||||
this.currentLength++;
|
|
||||||
this.loadMore.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.viewport.setRenderedRange(range);
|
|
||||||
this.viewport.setRenderedContentOffset(this.getOffsetFromIndex(range.start), 'to-start');
|
|
||||||
|
|
||||||
this.index$.next(firstVisibleIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTotalContentSize() {
|
|
||||||
this.viewport?.setTotalContentSize(this.totalHeight);
|
|
||||||
// @ts-expect-error TODO
|
|
||||||
this.viewport?._measureViewportSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
observeIntersection(entries: IntersectionObserverEntry[], observer: IntersectionObserver) {
|
|
||||||
const renderedItems = this.renderedItems;
|
|
||||||
if (!renderedItems) return this.onPrematureDisconnect();
|
|
||||||
|
|
||||||
const update = new Subject<void>();
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isIntersecting) continue;
|
|
||||||
|
|
||||||
const outerNode = entry.target as HTMLElement;
|
|
||||||
const [item] = this.getItemByNode(outerNode, renderedItems);
|
|
||||||
const node = outerNode.firstChild! as HTMLElement;
|
|
||||||
|
|
||||||
const height = new BehaviorSubject(node.offsetHeight);
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
const renderedItems = this.renderedItems;
|
|
||||||
if (!renderedItems) {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
return this.onPrematureDisconnect();
|
|
||||||
}
|
|
||||||
const [newItem] = this.getItemByNode(node, renderedItems);
|
|
||||||
if (newItem !== item) {
|
|
||||||
this.heights.delete(item);
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
height.next(node.offsetHeight);
|
|
||||||
});
|
|
||||||
resizeObserver.observe(node);
|
|
||||||
height
|
|
||||||
.pipe(
|
|
||||||
distinctUntilChanged(),
|
|
||||||
debounceTime(this.heightSetDebounceTime),
|
|
||||||
takeUntil(timer(this.itemRenderTimeout)),
|
|
||||||
tap({complete: () => resizeObserver.disconnect()}),
|
|
||||||
)
|
|
||||||
.subscribe(height => {
|
|
||||||
this.heights.set(item, height);
|
|
||||||
outerNode.style.height = `${height}px`;
|
|
||||||
update.next();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.unobserve(node);
|
|
||||||
}
|
|
||||||
update
|
|
||||||
.pipe(debounceTime(this.heightUpdateDebounceTime), takeUntil(timer(this.itemRenderTimeout)))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.updateHeights();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
intersectionObserver?: IntersectionObserver;
|
|
||||||
|
|
||||||
get contentWrapper() {
|
|
||||||
return this.viewport?._contentWrapper.nativeElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
get renderedItems() {
|
|
||||||
const contentWrapper = this.contentWrapper;
|
|
||||||
return contentWrapper
|
|
||||||
? // eslint-disable-next-line unicorn/prefer-spread
|
|
||||||
(Array.from(contentWrapper.children).filter(it => it instanceof HTMLElement) as HTMLElement[])
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getItemByNode(node: HTMLElement, renderedItems: HTMLElement[]) {
|
|
||||||
const {start} = this.viewport!.getRenderedRange();
|
|
||||||
const index = renderedItems.indexOf(node) + start;
|
|
||||||
return [this._items![index], this._groups![index]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
onContentRendered(): void {}
|
|
||||||
|
|
||||||
onContentScrolled() {
|
|
||||||
this.updateRenderedRange();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPrematureDisconnect() {
|
|
||||||
console.warn('Virtual Scroll strategy was disconnected unexpectedly', new Error('foo').stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDataLengthChanged(): void {
|
|
||||||
this.setTotalContentSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
onRenderedOffsetChanged(): void {}
|
|
||||||
|
|
||||||
scrollToIndex(index: number, behavior: ScrollBehavior): void {
|
|
||||||
this.viewport?.scrollToOffset(this.getOffsetFromIndex(index), behavior);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
src/app/modules/data/list/skeleton-list.component.ts
Normal file
23
src/app/modules/data/list/skeleton-list.component.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'skeleton-list',
|
||||||
|
templateUrl: 'skeleton-list.html',
|
||||||
|
styleUrls: ['skeleton-list.scss'],
|
||||||
|
})
|
||||||
|
export class SkeletonListComponent {}
|
||||||
43
src/app/modules/data/list/skeleton-list.html
Normal file
43
src/app/modules/data/list/skeleton-list.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2023 StApps
|
||||||
|
~ This program is free software: you can redistribute it and/or modify it
|
||||||
|
~ under the terms of the GNU General Public License as published by the Free
|
||||||
|
~ Software Foundation, version 3.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
~ more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with
|
||||||
|
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<pattern id="list-item" width="100%" height="75" patternUnits="userSpaceOnUse">
|
||||||
|
<rect width="100%" height="67" class="item"></rect>
|
||||||
|
<mask id="label-mask">
|
||||||
|
<rect rx="8" x="24" y="24" width="36" height="36" fill="white"></rect>
|
||||||
|
<rect rx="6" x="72" y="24" width="180" height="12" fill="white"></rect>
|
||||||
|
<rect rx="6" x="72" y="48" width="92" height="12" fill="white"></rect>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#label-mask)">
|
||||||
|
<rect class="label" width="100%" height="100%"></rect>
|
||||||
|
<rect fill="white" width="16" height="100%" style="filter: blur(8px)">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="translate"
|
||||||
|
from="0 0"
|
||||||
|
to="1000 0"
|
||||||
|
dur="2s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
></animateTransform>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="100%" height="100%" fill="url(#list-item)"></rect>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
34
src/app/modules/data/list/skeleton-list.scss
Normal file
34
src/app/modules/data/list/skeleton-list.scss
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright (C) 2023 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item-height: 92;
|
||||||
|
$gap: 4;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
fill: var(--ion-color-medium);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
rx: var(--border-radius-default);
|
||||||
|
fill: var(--ion-item-background, var(--ion-background-color, #fff));
|
||||||
|
x: var(--spacing-sm);
|
||||||
|
y: var(--spacing-sm);
|
||||||
|
width: calc(100% - var(--spacing-sm) * 2);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user