refactor: move event select popup to a modal

This commit is contained in:
Thea Schöbl
2022-09-23 12:24:22 +00:00
committed by Rainer Killinger
parent 8a04a43903
commit 4a3f79ca20
22 changed files with 788 additions and 495 deletions

View File

@@ -1,286 +0,0 @@
/*
* Copyright (C) 2022 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-explicit-any */
import {
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import {ModalController, PopoverController} from '@ionic/angular';
import {SCDateSeries} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {
DateSeriesRelevantData,
ScheduleProvider,
toDateSeriesRelevantData,
} from '../../calendar/schedule.provider';
import {CalendarService} from '../../calendar/calendar.service';
import {AddEventReviewModalComponent} from '../../calendar/add-event-review-modal.component';
import {ThingTranslatePipe} from '../../../translation/thing-translate.pipe';
import {groupBy, groupByProperty} from '../../../_helpers/collections/group-by';
import {mapValues} from '../../../_helpers/collections/map-values';
import {stringSortBy} from '../../../_helpers/collections/string-sort';
import {uniqBy} from '../../../_helpers/collections/uniq';
import {differenceBy} from '../../../_helpers/collections/difference';
enum Selection {
ON = 2,
PARTIAL = 1,
OFF = 1,
}
/**
* A tree
*
* The generic is to preserve type safety of how deep the tree goes.
*/
class TreeNode<T extends TreeNode<any> | SelectionValue> {
/**
* Value of this node
*/
checked: boolean;
/**
* If items are partially selected
*/
indeterminate: boolean;
/**
* Parent of this node
*/
parent?: TreeNode<TreeNode<T>>;
constructor(readonly children: T[], readonly ref: ChangeDetectorRef) {
this.updateParents();
this.accumulateApplyValues();
}
/**
* Accumulate values of children to set current value
*/
private accumulateApplyValues() {
const selections: number[] = this.children.map(it =>
it instanceof TreeNode
? it.checked
? Selection.ON
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
? Selection.ON
: Selection.OFF,
);
this.checked = selections.every(it => it === Selection.ON);
this.indeterminate = this.checked
? false
: selections.some(it => it > Selection.OFF);
}
/**
* Apply the value of this node to all child nodes
*/
private applyValueDownwards() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.checked = this.checked;
child.indeterminate = false;
// tslint:disable-next-line:no-any
(child as TreeNode<any>).applyValueDownwards();
} else {
(child as SelectionValue).selected = this.checked;
}
}
}
/**
* Set all children's parent to this
*/
private updateParents() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.parent = this as TreeNode<TreeNode<T>>;
}
}
}
/**
* Update values to all parents upwards
*/
private updateValueUpwards() {
this.parent?.accumulateApplyValues();
this.parent?.updateValueUpwards();
}
/**
* Click on this node
*/
click() {
this.checked = !this.checked;
this.indeterminate = false;
this.applyValueDownwards();
this.updateValueUpwards();
}
/**
* Notify that a child's value has changed
*/
notifyChildChanged() {
this.accumulateApplyValues();
this.updateValueUpwards();
}
}
interface SelectionValue {
/**
* Item that was selected
*/
item: SCDateSeries;
/**
* Selection
*/
selected: boolean;
}
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-add-event-popover-component',
templateUrl: 'add-event-popover.html',
styleUrls: ['add-event-popover.scss'],
})
export class AddEventPopoverComponent implements OnInit, OnDestroy {
/**
* The item the action belongs to
*/
@Input() items: SCDateSeries[];
/**
* Selection of the item
*/
selection: TreeNode<TreeNode<SelectionValue>>;
/**
* Uuids
*/
partialDateSeries: DateSeriesRelevantData[];
/**
* Uuid Subscription
*/
uuidSubscription: Subscription;
constructor(
readonly ref: ChangeDetectorRef,
readonly scheduleProvider: ScheduleProvider,
readonly popoverController: PopoverController,
readonly calendar: CalendarService,
readonly modalController: ModalController,
readonly thingTranslatePipe: ThingTranslatePipe,
) {}
/**
* Destroy
*/
ngOnDestroy() {
this.uuidSubscription.unsubscribe();
}
/**
* Init
*/
ngOnInit() {
this.uuidSubscription = this.scheduleProvider.partialEvents$.subscribe(
async result => {
this.partialDateSeries = result;
this.selection = new TreeNode(
Object.values(
groupBy(
this.items
.map(item => ({
selected: this.partialDateSeries.some(
it => it.uid === item.uid,
),
item: item,
}))
.sort(stringSortBy(it => it.item.repeatFrequency)),
it => it.item.repeatFrequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
);
},
);
}
getSelection(): {
selected: DateSeriesRelevantData[];
unselected: DateSeriesRelevantData[];
} {
const selection = mapValues(
groupByProperty(
this.selection.children.flatMap(it => it.children),
'selected',
),
value => value.map(it => toDateSeriesRelevantData(it.item)),
);
return {selected: selection.true ?? [], unselected: selection.false ?? []};
}
async export() {
const modal = await this.modalController.create({
component: AddEventReviewModalComponent,
canDismiss: true,
cssClass: 'add-modal',
componentProps: {
dismissAction: () => {
modal.dismiss();
},
dateSeries: this.items,
},
});
await modal.present();
await modal.onWillDismiss();
}
/**
* On selection change
*/
async onCommit(save: boolean) {
if (save) {
const {selected, unselected} = this.getSelection();
this.scheduleProvider.partialEvents$.next(
uniqBy(
[
...differenceBy(this.partialDateSeries, unselected, it => it.uid),
...selected,
],
it => it.uid,
),
);
}
await this.popoverController.dismiss();
}
}

View File

@@ -1,91 +0,0 @@
<!--
~ Copyright (C) 2022 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/>.
-->
<ion-card-content>
<ion-item-group>
<ion-item-divider (click)="selection.click()">
<ion-label>{{
'data.chips.add_events.popover.ALL' | translate
}}</ion-label>
<ion-checkbox
slot="start"
[checked]="selection.checked"
[indeterminate]="selection.indeterminate"
>
</ion-checkbox>
</ion-item-divider>
<ion-item-group *ngFor="let frequency of selection.children">
<ion-item-divider (click)="frequency.click()">
<ion-label class="ion-text-wrap">{{
frequency.children[0].item.repeatFrequency
? (frequency.children[0].item.repeatFrequency
| durationLocalized: true
| sentencecase)
: ('data.chips.add_events.popover.SINGLE' | translate | titlecase)
}}</ion-label>
<ion-checkbox
slot="start"
[checked]="frequency.checked"
[indeterminate]="frequency.indeterminate"
>
</ion-checkbox>
</ion-item-divider>
<ion-item
*ngFor="let date of frequency.children"
(click)="date.selected = !date.selected; frequency.notifyChildChanged()"
>
<ion-label
class="ion-text-wrap"
*ngIf="date.item.dates.length > 1; else single_event"
>
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{ date.item.dates[0] | amDateFormat: 'HH:mm ddd' }}
{{ 'data.chips.add_events.popover.UNTIL' | translate }}
{{ date.item.dates[date.item.dates.length - 1] | amDateFormat: 'll' }}
</ion-label>
<ng-template #single_event>
<ion-label class="ion-text-wrap">
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{
date.item.dates[date.item.dates.length - 1]
| amDateFormat: 'll, HH:mm'
}}
</ion-label>
</ng-template>
<ion-checkbox slot="start" [checked]="date.selected"> </ion-checkbox>
</ion-item>
</ion-item-group>
</ion-item-group>
<div class="action-buttons">
<ion-button (click)="onCommit(false)" fill="clear">{{
'abort' | translate
}}</ion-button>
<ion-button (click)="onCommit(true)" fill="clear">{{
'ok' | translate
}}</ion-button>
</div>
<div class="download-button">
<ion-button
fill="clear"
(click)="export()"
[disabled]="!(selection.indeterminate || selection.checked)"
>
<ion-icon slot="icon-only" name="download"></ion-icon>
<!-- {{ 'export' | translate }} -->
</ion-button>
</div>
</ion-card-content>

View File

@@ -1,26 +0,0 @@
/*!
* 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/>.
*/
::ng-deep ion-item-divider {
cursor: pointer;
}
.action-buttons {
float: right;
}
.download-button {
float: left;
}

View File

@@ -1,25 +1,24 @@
/*
* Copyright (C) 2022 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 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.
* 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* tslint:disable:prefer-function-over-method */
import {Component, Input, OnDestroy} from '@angular/core';
import {PopoverController} from '@ionic/angular';
import {Component, Input, OnDestroy, ViewChild} from '@angular/core';
import {IonRouterOutlet, ModalController} from '@ionic/angular';
import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {ScheduleProvider} from '../../../calendar/schedule.provider';
import {AddEventPopoverComponent} from '../add-event-popover.component';
import {CoordinatedSearchProvider} from '../../coordinated-search.provider';
import {
chipSkeletonTransition,
@@ -29,6 +28,8 @@ import {
AddEventStates,
AddEventStatesMap,
} from './add-event-action-chip.config';
import {EditEventSelectionComponent} from '../edit-event-selection.component';
import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component';
/**
* Shows a horizontal list of action chips
@@ -82,10 +83,14 @@ export class AddEventActionChipComponent implements OnDestroy {
*/
uuidSubscription: Subscription;
@ViewChild('selection', {static: false})
selection: EditEventSelectionComponent;
constructor(
readonly popoverController: PopoverController,
readonly dataProvider: CoordinatedSearchProvider,
readonly modalController: ModalController,
readonly scheduleProvider: ScheduleProvider,
readonly routerOutlet: IonRouterOutlet,
) {}
/**
@@ -107,6 +112,24 @@ export class AddEventActionChipComponent implements OnDestroy {
this.uuidSubscription?.unsubscribe();
}
async export() {
const modal = await this.modalController.create({
component: AddEventReviewModalComponent,
canDismiss: true,
cssClass: 'add-modal',
presentingElement: await this.modalController.getTop(),
componentProps: {
dismissAction: () => {
modal.dismiss();
},
dateSeries: this.selection.items,
},
});
await modal.present();
await modal.onWillDismiss();
}
/**
* Init
*/
@@ -176,21 +199,4 @@ export class AddEventActionChipComponent implements OnDestroy {
},
);
}
/**
* Action
*/
async onClick(event: MouseEvent) {
const associatedDateSeries = await this.associatedDateSeries;
const popover = await this.popoverController.create({
component: AddEventPopoverComponent,
translucent: true,
cssClass: 'add-event-popover',
componentProps: {
items: associatedDateSeries,
},
event: event,
});
await popover.present();
}
}

View File

@@ -1,16 +1,16 @@
<!--
~ Copyright (C) 2022 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 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.
~ 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/>.
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<div class="stack-children">
@@ -18,10 +18,45 @@
*ngIf="associatedDateSeries | async as associatedDateSeries; else loading"
@chipTransition
[disabled]="disabled"
(click)="$event.stopPropagation(); onClick($event)"
(click)="$event.stopPropagation(); editModal.present()"
>
<ion-icon [name]="icon" [fill]="iconFill"></ion-icon>
<ion-label>{{ label | translate }}</ion-label>
<stapps-edit-modal #editModal (save)="selection.save()">
<ng-template>
<ion-content class="ion-padding modal-content">
<div>
<stapps-edit-event-selection
#selection
[items]="associatedDateSeries"
(modified)="editModal.pendingChanges = true"
></stapps-edit-event-selection>
</div>
</ion-content>
<ion-footer mode="ios">
<ion-toolbar color="light">
<ion-button
slot="end"
fill="clear"
(click)="export()"
[disabled]="
!(
selection.selection.indeterminate ||
selection.selection.checked
)
"
>
{{
'schedule.toCalendar.reviewModal.DOWNLOAD'
| translate
| titlecase
}}
<ion-icon slot="end" name="download"></ion-icon>
</ion-button>
</ion-toolbar>
</ion-footer>
</ng-template>
</stapps-edit-modal>
</ion-chip>
<ng-template #loading>
<ion-chip @chipSkeletonTransition>

View File

@@ -1,3 +1,19 @@
/*!
* Copyright (C) 2022 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 "src/theme/common/ion-content-parallax";
:host {
display: block;
padding: var(--spacing-sm);
@@ -17,3 +33,14 @@
grid-column-start: 1;
grid-row-start: 1;
}
.modal-content {
--background: var(--ion-color-primary);
--color: var(--ion-color-primary-contrast);
@include ion-content-parallax($content-size: 160px)
}
ion-footer > ion-toolbar {
--border-color: var(--ion-color-light-shade);
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) 2022 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 {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import {ModalController, PopoverController} from '@ionic/angular';
import {SCDateSeries} from '@openstapps/core';
import {
DateSeriesRelevantData,
ScheduleProvider,
toDateSeriesRelevantData,
} from '../../calendar/schedule.provider';
import {CalendarService} from '../../calendar/calendar.service';
import {ThingTranslatePipe} from '../../../translation/thing-translate.pipe';
import {groupBy, groupByProperty} from '../../../_helpers/collections/group-by';
import {mapValues} from '../../../_helpers/collections/map-values';
import {stringSortBy} from '../../../_helpers/collections/string-sort';
import {uniqBy} from '../../../_helpers/collections/uniq';
import {differenceBy} from '../../../_helpers/collections/difference';
import {SelectionValue, TreeNode} from './tree-node';
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-edit-event-selection',
templateUrl: 'edit-event-selection.html',
styleUrls: ['edit-event-selection.scss'],
})
export class EditEventSelectionComponent implements OnInit {
/**
* The item the action belongs to
*/
@Input() items: SCDateSeries[];
/**
* Selection of the item
*/
selection: TreeNode<TreeNode<SelectionValue>>;
/**
* Uuids
*/
partialDateSeries: DateSeriesRelevantData[];
@Output()
modified = new EventEmitter();
constructor(
readonly ref: ChangeDetectorRef,
readonly scheduleProvider: ScheduleProvider,
readonly popoverController: PopoverController,
readonly calendar: CalendarService,
readonly modalController: ModalController,
readonly thingTranslatePipe: ThingTranslatePipe,
) {}
ngOnInit() {
this.partialDateSeries = this.scheduleProvider.partialEvents$.value;
this.reset();
}
private getSelection(): {
selected: DateSeriesRelevantData[];
unselected: DateSeriesRelevantData[];
} {
const selection = mapValues(
groupByProperty(
this.selection.children.flatMap(it => it.children),
'selected',
),
value => value.map(it => toDateSeriesRelevantData(it.item)),
);
return {selected: selection.true ?? [], unselected: selection.false ?? []};
}
getModifiedEvents(): DateSeriesRelevantData[] {
const {selected, unselected} = this.getSelection();
return uniqBy(
[
...differenceBy(this.partialDateSeries, unselected, it => it.uid),
...selected,
],
it => it.uid,
);
}
reset() {
this.selection = new TreeNode(
Object.values(
groupBy(
this.items
.map(item => ({
selected: this.partialDateSeries.some(it => it.uid === item.uid),
item: item,
}))
.sort(stringSortBy(it => it.item.repeatFrequency)),
it => it.item.repeatFrequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
);
}
/**
* Save selection
*/
save() {
this.scheduleProvider.partialEvents$.next(this.getModifiedEvents());
}
}

View File

@@ -0,0 +1,86 @@
<!--
~ Copyright (C) 2022 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/>.
-->
<ion-item
(click)="modified.emit(); selection.click()"
class="list-header"
lines="none"
>
<ion-list-header>
<ion-label>{{ 'data.chips.add_events.popover.ALL' | translate }}</ion-label>
</ion-list-header>
<ion-checkbox
slot="end"
[checked]="selection.checked"
[indeterminate]="selection.indeterminate"
>
</ion-checkbox>
</ion-item>
<ng-container *ngFor="let frequency of selection.children">
<ion-list inset="true" lines="full">
<ion-item
lines="none"
(click)="modified.emit(); frequency.click()"
class="list-header"
>
<ion-list-header>
<ion-label>{{
frequency.children[0].item.repeatFrequency
? (frequency.children[0].item.repeatFrequency
| durationLocalized: true
| sentencecase)
: ('data.chips.add_events.popover.SINGLE' | translate | titlecase)
}}</ion-label>
<ion-button></ion-button>
</ion-list-header>
<ion-checkbox
slot="end"
[checked]="frequency.checked"
[indeterminate]="frequency.indeterminate"
>
</ion-checkbox>
</ion-item>
<ion-item
*ngFor="let date of frequency.children"
(click)="
modified.emit();
date.selected = !date.selected;
frequency.notifyChildChanged()
"
>
<ion-label
*ngIf="date.item.dates.length > 1; else single_event"
class="ion-text-wrap"
>
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{ date.item.dates[0] | amDateFormat: 'HH:mm ddd' }}
{{ 'data.chips.add_events.popover.UNTIL' | translate }}
{{ date.item.dates[date.item.dates.length - 1] | amDateFormat: 'll' }}
</ion-label>
<ng-template #single_event>
<ion-label class="ion-text-wrap">
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{
date.item.dates[date.item.dates.length - 1]
| amDateFormat: 'll, HH:mm'
}}
</ion-label>
</ng-template>
<ion-checkbox slot="end" [checked]="date.selected"> </ion-checkbox>
</ion-item>
</ion-list>
</ng-container>

View File

@@ -0,0 +1,45 @@
/*!
* Copyright (C) 2022 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/>.
*/
ion-item-divider.ios > ion-checkbox {
margin-right: 8px;
}
.list-header {
--padding-start: 0;
--background: var(--ion-color-primary-shade);
> ion-list-header {
--color: var(--ion-color-primary-contrast);
--background: none;
}
> ion-checkbox {
--background: none;
--border-color: rgba(var(--ion-color-primary-contrast-rgb), 0.77);
--background-checked: var(--ion-color-primary-contrast);
--border-color-checked: var(--ion-color-primary-contrast);
--checkmark-color: var(--ion-color-primary)
}
}
:host > .list-header {
--background: none;
}
ion-list.md {
padding-top: 0;
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2022 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 {ChangeDetectorRef} from '@angular/core';
import {SCDateSeries} from '@openstapps/core';
export enum Selection {
ON = 2,
PARTIAL = 1,
OFF = 1,
}
/**
* A tree
*
* The generic is to preserve type safety of how deep the tree goes.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class TreeNode<T extends TreeNode<any> | SelectionValue> {
/**
* Value of this node
*/
checked: boolean;
/**
* If items are partially selected
*/
indeterminate: boolean;
/**
* Parent of this node
*/
parent?: TreeNode<TreeNode<T>>;
constructor(readonly children: T[], readonly ref: ChangeDetectorRef) {
this.updateParents();
this.accumulateApplyValues();
}
/**
* Accumulate values of children to set current value
*/
private accumulateApplyValues() {
const selections: number[] = this.children.map(it =>
it instanceof TreeNode
? it.checked
? Selection.ON
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
? Selection.ON
: Selection.OFF,
);
this.checked = selections.every(it => it === Selection.ON);
this.indeterminate = this.checked
? false
: selections.some(it => it > Selection.OFF);
}
/**
* Apply the value of this node to all child nodes
*/
private applyValueDownwards() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.checked = this.checked;
child.indeterminate = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(child as TreeNode<any>).applyValueDownwards();
} else {
(child as SelectionValue).selected = this.checked;
}
}
}
/**
* Set all children's parent to this
*/
private updateParents() {
for (const child of this.children) {
if (child instanceof TreeNode) {
child.parent = this as TreeNode<TreeNode<T>>;
}
}
}
/**
* Update values to all parents upwards
*/
private updateValueUpwards() {
this.parent?.accumulateApplyValues();
this.parent?.updateValueUpwards();
}
/**
* Click on this node
*/
click() {
this.checked = !this.checked;
this.indeterminate = false;
this.applyValueDownwards();
this.updateValueUpwards();
}
/**
* Notify that a child's value has changed
*/
notifyChildChanged() {
this.accumulateApplyValues();
this.updateValueUpwards();
}
}
export interface SelectionValue {
/**
* Item that was selected
*/
item: SCDateSeries;
/**
* Selection
*/
selected: boolean;
}

View File

@@ -1,16 +1,16 @@
/*
* Copyright (C) 2022 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 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.
* 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/>.
* 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 {ScrollingModule} from '@angular/cdk/scrolling';
import {CommonModule} from '@angular/common';
@@ -26,7 +26,7 @@ import {MenuModule} from '../menu/menu.module';
import {ScheduleProvider} from '../calendar/schedule.provider';
import {StorageModule} from '../storage/storage.module';
import {ActionChipListComponent} from './chips/action-chip-list.component';
import {AddEventPopoverComponent} from './chips/add-event-popover.component';
import {EditEventSelectionComponent} from './chips/edit-event-selection.component';
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
import {LocateActionChipComponent} from './chips/data/locate-action-chip.component';
import {DataFacetsProvider} from './data-facets.provider';
@@ -99,7 +99,7 @@ import {ExternalLinkComponent} from './elements/external-link.component';
declarations: [
ActionChipListComponent,
AddEventActionChipComponent,
AddEventPopoverComponent,
EditEventSelectionComponent,
AddressDetailComponent,
ArticleDetailContentComponent,
ArticleListItemComponent,