{
styleUrls: ['data-list.scss'],
})
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
*/
@@ -86,7 +81,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
*/
subscriptions: Subscription[] = [];
- @ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
+ @ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll;
/**
* Signalizes that the data is being loaded
@@ -113,6 +108,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
if (Array.isArray(this.items) && typeof changes.items !== 'undefined') {
this.itemStream.next(this.items);
+ this.infiniteScroll.complete();
}
}
@@ -127,7 +123,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
if (typeof this.resetToTop !== 'undefined') {
this.subscriptions.push(
this.resetToTop.subscribe(() => {
- this.viewPort.scrollToIndex(0);
+ // this.viewPort.scrollToIndex(0);
}),
);
}
@@ -139,18 +135,4 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
notifyLoadMore() {
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();
- }
- }
}
diff --git a/src/app/modules/data/list/data-list.html b/src/app/modules/data/list/data-list.html
index b6569188..a0f8c0ea 100644
--- a/src/app/modules/data/list/data-list.html
+++ b/src/app/modules/data/list/data-list.html
@@ -15,29 +15,23 @@
-
-
+
+
-
+
+
+
+
{{ 'search.nothing_found' | translate | titlecase }}
-
-
-
+
diff --git a/src/app/modules/data/list/data-list.scss b/src/app/modules/data/list/data-list.scss
index 086552c5..6fb09920 100644
--- a/src/app/modules/data/list/data-list.scss
+++ b/src/app/modules/data/list/data-list.scss
@@ -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
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
@@ -13,17 +13,10 @@
* this program. If not, see .
*/
-cdk-virtual-scroll-viewport {
+ion-list {
+ background: none;
+}
+
+skeleton-list {
height: 100%;
- width: 100%;
-}
-
-::ng-deep {
- .cdk-virtual-scroll-content-wrapper {
- width: 100%;
- }
-}
-
-.virtual-scroll-expander {
- clear: both;
}
diff --git a/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.directive.ts b/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.directive.ts
deleted file mode 100644
index c56468f0..00000000
--- a/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.directive.ts
+++ /dev/null
@@ -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 .
- */
-
-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;
-
- @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;
- }
-}
diff --git a/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.ts b/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.ts
deleted file mode 100644
index 0b5aea51..00000000
--- a/src/app/modules/data/list/sc-thing-list-item-virtual-scroll-strategy.ts
+++ /dev/null
@@ -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 .
- */
-
-/* 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;
-
- private index$ = new Subject();
-
- private heights = new Map();
-
- private groupHeights = new Map();
-
- 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 = new Map([[SCThingType.AcademicEvent, 139]]);
-
- buffer = 4000;
-
- gap = 8;
-
- itemRenderTimeout = 1000;
-
- heightUpdateDebounceTime = 25;
-
- heightSetDebounceTime = 100;
-
- loadMore = new Subject();
-
- // 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();
- 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);
- }
-}
diff --git a/src/app/modules/data/list/skeleton-list.component.ts b/src/app/modules/data/list/skeleton-list.component.ts
new file mode 100644
index 00000000..ed122e41
--- /dev/null
+++ b/src/app/modules/data/list/skeleton-list.component.ts
@@ -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 .
+ */
+
+import {Component} from '@angular/core';
+
+@Component({
+ selector: 'skeleton-list',
+ templateUrl: 'skeleton-list.html',
+ styleUrls: ['skeleton-list.scss'],
+})
+export class SkeletonListComponent {}
diff --git a/src/app/modules/data/list/skeleton-list.html b/src/app/modules/data/list/skeleton-list.html
new file mode 100644
index 00000000..5348bcba
--- /dev/null
+++ b/src/app/modules/data/list/skeleton-list.html
@@ -0,0 +1,43 @@
+
+
+
diff --git a/src/app/modules/data/list/skeleton-list.scss b/src/app/modules/data/list/skeleton-list.scss
new file mode 100644
index 00000000..6b525fd6
--- /dev/null
+++ b/src/app/modules/data/list/skeleton-list.scss
@@ -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 .
+ */
+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);
+}