mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 08:33:11 +00:00
feat: add catalog module
This commit is contained in:
committed by
Jovan Krunić
parent
e628f396e2
commit
03084b1c96
@@ -81,6 +81,7 @@ import {OriginInListComponent} from './elements/origin-in-list.component';
|
||||
import {CoordinatedSearchProvider} from './coordinated-search.provider';
|
||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||
import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
||||
import {SimpleDataListComponent} from './list/simple-data-list.component';
|
||||
|
||||
/**
|
||||
* Module for handling data
|
||||
@@ -135,8 +136,9 @@ import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
||||
SkeletonSimpleCardComponent,
|
||||
VideoDetailContentComponent,
|
||||
VideoListItemComponent,
|
||||
SimpleDataListComponent,
|
||||
],
|
||||
entryComponents: [DataListComponent],
|
||||
entryComponents: [DataListComponent, SimpleDataListComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
DataRoutingModule,
|
||||
@@ -177,6 +179,7 @@ import {FavoriteButtonComponent} from './elements/favorite-button.component';
|
||||
SkeletonListItemComponent,
|
||||
SkeletonSimpleCardComponent,
|
||||
SearchPageComponent,
|
||||
SimpleDataListComponent,
|
||||
],
|
||||
})
|
||||
export class DataModule {}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
<span
|
||||
>{{ text | slice: 0:size }}<span *ngIf="text.length > size">...</span></span
|
||||
>
|
||||
<span class="ion-hide-sm-up">
|
||||
{{ text | slice: 0:size }}
|
||||
<span *ngIf="text.length > size" class="ion-hide-sm-up">…</span>
|
||||
</span>
|
||||
<span class="ion-hide-sm-down ion-hide-md-up">
|
||||
{{ text | slice: 0:size * 2 }}
|
||||
<span *ngIf="text.length > size * 2" class="ion-hide-sm-down ion-hide-md-up">
|
||||
…
|
||||
</span>
|
||||
</span>
|
||||
<span class="ion-hide-md-down">
|
||||
{{ text | slice: 0:size * 3 }}
|
||||
<span *ngIf="text.length > size * 3" class="ion-hide-md-down">…</span>
|
||||
</span>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 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';
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
/**
|
||||
* A placeholder to show when a list item is being loaded
|
||||
@@ -22,4 +22,6 @@ import {Component} from '@angular/core';
|
||||
templateUrl: 'skeleton-list-item.html',
|
||||
styleUrls: ['skeleton-list-item.scss'],
|
||||
})
|
||||
export class SkeletonListItemComponent {}
|
||||
export class SkeletonListItemComponent {
|
||||
@Input() hideThumbnail = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ion-item>
|
||||
<ion-thumbnail slot="start" class="ion-margin-end">
|
||||
<ion-thumbnail *ngIf="!hideThumbnail" slot="start" class="ion-margin-end">
|
||||
<ion-skeleton-text animated></ion-skeleton-text>
|
||||
</ion-thumbnail>
|
||||
<ion-grid>
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {SCThings} from '@openstapps/core';
|
||||
import {ceil} from 'lodash-es';
|
||||
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
||||
|
||||
/**
|
||||
@@ -72,7 +71,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
||||
/**
|
||||
* Items that display the skeleton list
|
||||
*/
|
||||
skeletonItems: number[];
|
||||
skeletonItems: number;
|
||||
|
||||
/**
|
||||
* Array of all subscriptions to Observables
|
||||
@@ -84,12 +83,10 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
|
||||
/**
|
||||
* Calculate how many items would fill the screen
|
||||
*/
|
||||
@HostListener('window.resize', ['$event'])
|
||||
@HostListener('window:resize', ['$event'])
|
||||
calcSkeletonItems() {
|
||||
const itemHeight = 122;
|
||||
this.skeletonItems = Array.from({
|
||||
length: ceil(window.innerHeight / itemHeight),
|
||||
});
|
||||
const itemHeight = 40;
|
||||
this.skeletonItems = Math.ceil(window.innerHeight / itemHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</div>
|
||||
<ion-list [style.display]="items ? 'none' : 'block'">
|
||||
<stapps-skeleton-list-item
|
||||
*ngFor="let skeleton of skeletonItems"
|
||||
[hideThumbnail]="singleType"
|
||||
*ngFor="let skeleton of [].constructor(skeletonItems)"
|
||||
></stapps-skeleton-list-item>
|
||||
</ion-list>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
cdk-virtual-scroll-viewport {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -8,3 +9,7 @@ cdk-virtual-scroll-viewport {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-scroll-expander {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 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, Input} from '@angular/core';
|
||||
import {Component, Input, OnInit, OnDestroy} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {AlertController} from '@ionic/angular';
|
||||
import {
|
||||
@@ -39,7 +39,7 @@ import {PositionService} from '../../map/position.service';
|
||||
templateUrl: 'search-page.html',
|
||||
providers: [ContextMenuService],
|
||||
})
|
||||
export class SearchPageComponent {
|
||||
export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Api query filter
|
||||
*/
|
||||
@@ -126,9 +126,7 @@ export class SearchPageComponent {
|
||||
protected dataRoutingService: DataRoutingService,
|
||||
protected router: Router,
|
||||
protected positionService: PositionService,
|
||||
) {
|
||||
this.initialize();
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetches items with set query configuration
|
||||
@@ -230,7 +228,8 @@ export class SearchPageComponent {
|
||||
this.contextMenuService.updateContextFilter(facets);
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
ngOnInit() {
|
||||
this.initialize();
|
||||
this.contextMenuService.setContextSort({
|
||||
name: 'sort',
|
||||
reversed: false,
|
||||
@@ -290,7 +289,7 @@ export class SearchPageComponent {
|
||||
);
|
||||
}
|
||||
|
||||
ionViewWillLeave() {
|
||||
ngOnDestroy() {
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
|
||||
76
src/app/modules/data/list/simple-data-list.component.ts
Normal file
76
src/app/modules/data/list/simple-data-list.component.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {SCThings} from '@openstapps/core';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Router} from '@angular/router';
|
||||
import {DataRoutingService} from '../data-routing.service';
|
||||
|
||||
/**
|
||||
* Shows the list of items
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-simple-data-list',
|
||||
templateUrl: 'simple-data-list.html',
|
||||
styleUrls: ['simple-data-list.scss'],
|
||||
})
|
||||
export class SimpleDataListComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* All SCThings to display
|
||||
*/
|
||||
@Input() items?: SCThings[];
|
||||
|
||||
/**
|
||||
* Indicates whether or not the list is to display SCThings of a single type
|
||||
*/
|
||||
@Input() singleType = false;
|
||||
|
||||
/**
|
||||
* List header
|
||||
*/
|
||||
@Input() listHeader?: string;
|
||||
|
||||
/**
|
||||
* Items that display the skeleton list
|
||||
*/
|
||||
skeletonItems = 6;
|
||||
|
||||
/**
|
||||
* Array of all subscriptions to Observables
|
||||
*/
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
private readonly dataRoutingService: DataRoutingService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscriptions.push(
|
||||
this.dataRoutingService.itemSelectListener().subscribe(item => {
|
||||
void this.router.navigate(['data-detail', item.uid]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove subscriptions when the component is removed
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
for (const sub of this.subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/app/modules/data/list/simple-data-list.html
Normal file
28
src/app/modules/data/list/simple-data-list.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<ng-container *ngIf="items | async as items; else loading">
|
||||
<ion-list>
|
||||
<ng-container *ngIf="!listHeader; else header"></ng-container>
|
||||
<stapps-data-list-item
|
||||
*ngFor="let item of items"
|
||||
[item]="item"
|
||||
[hideThumbnail]="singleType"
|
||||
></stapps-data-list-item>
|
||||
</ion-list>
|
||||
</ng-container>
|
||||
<ng-template #loading>
|
||||
<ion-list>
|
||||
<stapps-skeleton-list-item
|
||||
[hideThumbnail]="singleType"
|
||||
*ngFor="let skeleton of [].constructor(skeletonItems)"
|
||||
>
|
||||
</stapps-skeleton-list-item>
|
||||
</ion-list>
|
||||
</ng-template>
|
||||
<ng-template #header>
|
||||
<ion-list-header lines="inset">
|
||||
<ion-text color="dark">
|
||||
<h3>
|
||||
{{ listHeader }}
|
||||
</h3>
|
||||
</ion-text>
|
||||
</ion-list-header>
|
||||
</ng-template>
|
||||
0
src/app/modules/data/list/simple-data-list.scss
Normal file
0
src/app/modules/data/list/simple-data-list.scss
Normal file
@@ -12,19 +12,159 @@
|
||||
* 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, Input} from '@angular/core';
|
||||
import {SCCatalog} from '@openstapps/core';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
OnDestroy,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
import {SCCatalog, SCSearchBooleanFilter} from '@openstapps/core';
|
||||
import {SearchPageComponent} from '../../list/search-page.component';
|
||||
|
||||
enum AccordionButtonState {
|
||||
collapsed = 'chevron-down',
|
||||
expanded = 'chevron-up',
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-catalog-detail-content',
|
||||
templateUrl: 'catalog-detail-content.html',
|
||||
styleUrls: ['catalog-detail-content.scss'],
|
||||
})
|
||||
export class CatalogDetailContentComponent {
|
||||
export class CatalogDetailContentComponent
|
||||
extends SearchPageComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
/**
|
||||
* TODO
|
||||
* SCCatalog to display
|
||||
*/
|
||||
@Input() item: SCCatalog;
|
||||
|
||||
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
|
||||
|
||||
buttonState = AccordionButtonState.collapsed;
|
||||
|
||||
buttonShown = true;
|
||||
|
||||
descriptionLinesShown: number;
|
||||
|
||||
descriptionLinesTotal: number;
|
||||
|
||||
descriptionPreviewLines = 3;
|
||||
|
||||
descriptionLinesToDisplay = 0;
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
if (this.item.description) {
|
||||
this.descriptionLinesToDisplay = this.descriptionPreviewLines;
|
||||
setTimeout(() => this.checkTextElipsis(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.checkTextElipsis();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.pageSize = 100;
|
||||
this.sortQuery = {
|
||||
arguments: {field: 'name'},
|
||||
order: 'asc',
|
||||
type: 'ducet',
|
||||
};
|
||||
|
||||
const subCatalogFilter: SCSearchBooleanFilter = {
|
||||
arguments: {
|
||||
operation: 'and',
|
||||
filters: [
|
||||
{
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: 'catalog',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'superCatalog.uid',
|
||||
value: this.item.uid,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
const subEventsFilter: SCSearchBooleanFilter = {
|
||||
arguments: {
|
||||
operation: 'and',
|
||||
filters: [
|
||||
{
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: 'academic event',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'catalogs.uid',
|
||||
value: this.item.uid,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
this.forcedFilter = {
|
||||
arguments: {
|
||||
filters: [subCatalogFilter, subEventsFilter],
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
checkTextElipsis() {
|
||||
if (typeof this.accordionTextArea === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const element = this.accordionTextArea.nativeElement as HTMLElement;
|
||||
|
||||
const lineHeight = Number.parseInt(
|
||||
getComputedStyle(element).getPropertyValue('line-height'),
|
||||
);
|
||||
this.descriptionLinesTotal = element?.scrollHeight / lineHeight;
|
||||
this.descriptionLinesShown = element?.offsetHeight / lineHeight;
|
||||
if (this.buttonState === AccordionButtonState.expanded) {
|
||||
this.descriptionLinesToDisplay = this.descriptionLinesTotal;
|
||||
}
|
||||
const isElipsed = element?.offsetHeight < element?.scrollHeight;
|
||||
this.buttonShown =
|
||||
(isElipsed && this.buttonState === AccordionButtonState.collapsed) ||
|
||||
(!isElipsed && this.buttonState === AccordionButtonState.expanded);
|
||||
}
|
||||
|
||||
toggleDescriptionAccordion() {
|
||||
if (this.descriptionLinesToDisplay > 0) {
|
||||
this.descriptionLinesToDisplay =
|
||||
this.descriptionLinesToDisplay === this.descriptionPreviewLines
|
||||
? this.descriptionLinesTotal
|
||||
: this.descriptionPreviewLines;
|
||||
}
|
||||
this.buttonState =
|
||||
this.buttonState === AccordionButtonState.collapsed
|
||||
? AccordionButtonState.expanded
|
||||
: AccordionButtonState.collapsed;
|
||||
setTimeout(() => this.checkTextElipsis(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
<stapps-simple-card
|
||||
[title]="'categories' | propertyNameTranslate: item | titlecase"
|
||||
[content]="'categories' | thingTranslate: item"
|
||||
<ion-card>
|
||||
<ion-card-content>
|
||||
<ion-text color="dark">
|
||||
<h1>
|
||||
{{ 'name' | thingTranslate: item }}
|
||||
</h1>
|
||||
</ion-text>
|
||||
<div *ngIf="item.description">
|
||||
<br />
|
||||
<div
|
||||
class="text-accordion"
|
||||
[style.-webkit-line-clamp]="descriptionLinesToDisplay"
|
||||
#accordionTextArea
|
||||
>
|
||||
{{ 'description' | thingTranslate: item }}
|
||||
</div>
|
||||
</div>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
<ion-button
|
||||
expand="full"
|
||||
fill="clear"
|
||||
*ngIf="item.description && buttonShown"
|
||||
(click)="toggleDescriptionAccordion()"
|
||||
>
|
||||
</stapps-simple-card>
|
||||
<ion-icon [name]="buttonState" size="large"></ion-icon>
|
||||
</ion-button>
|
||||
<stapps-simple-data-list
|
||||
id="simple-data-list"
|
||||
[items]="items"
|
||||
[singleType]="true"
|
||||
[listHeader]="'type' | thingTranslate: item | titlecase"
|
||||
></stapps-simple-data-list>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.text-accordion {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<div class="ion-text-wrap">
|
||||
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
|
||||
<p *ngIf="item.description">
|
||||
<stapps-long-inline-text
|
||||
[text]="'description' | thingTranslate: item"
|
||||
[size]="80"
|
||||
></stapps-long-inline-text>
|
||||
</p>
|
||||
<p *ngIf="item.academicTerm">{{ item.academicTerm.name }}</p>
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{ 'name' | thingTranslate: item }}
|
||||
</h2>
|
||||
</ion-label>
|
||||
<ion-note *ngIf="item.academicTerm">{{
|
||||
item.academicTerm.name
|
||||
}}</ion-note>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
Reference in New Issue
Block a user