This commit is contained in:
Dan Percic 2023-09-15 19:07:19 +03:00
parent a4132b82f5
commit 9fba181eb9

View File

@ -3,13 +3,18 @@ import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators'; import { map, switchMap, tap } from 'rxjs/operators';
import { FilterService, getFilteredEntities } from '../../filtering'; import { FilterService, getFilteredEntities } from '../../filtering';
import { SearchService } from '../../search'; import { SearchService } from '../../search';
import { SortingService } from '../../sorting';
import { getLength, shareDistinctLast, shareLast, some } from '../../utils';
import { Id, IListable } from '../models'; import { Id, IListable } from '../models';
import { EntitiesService } from './entities.service'; import { EntitiesService } from './entities.service';
import { getLength, shareDistinctLast, shareLast, some } from '../../utils';
import { SortingService } from '../../sorting';
@Injectable() @Injectable()
export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> { export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> {
#displayed: Class[] = [];
readonly #selected$ = new BehaviorSubject<PrimaryKey[]>([]);
#anchor: Class | undefined = undefined;
#focus: Class | undefined = undefined;
#sortedDisplayed: Class[] = [];
readonly displayed$: Observable<Class[]>; readonly displayed$: Observable<Class[]>;
readonly displayedLength$: Observable<number>; readonly displayedLength$: Observable<number>;
readonly areAllSelected$: Observable<boolean>; readonly areAllSelected$: Observable<boolean>;
@ -20,13 +25,6 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
readonly selectedLength$: Observable<number>; readonly selectedLength$: Observable<number>;
readonly sortedDisplayedEntities$: Observable<Class[]>; readonly sortedDisplayedEntities$: Observable<Class[]>;
private _displayed: Class[] = [];
private readonly _selected$ = new BehaviorSubject<PrimaryKey[]>([]);
private _anchor: Class | undefined = undefined;
private _focus: Class | undefined = undefined;
private _sortedDisplayed: Class[] = [];
constructor( constructor(
protected readonly _filterService: FilterService, protected readonly _filterService: FilterService,
protected readonly _searchService: SearchService<Class>, protected readonly _searchService: SearchService<Class>,
@ -36,12 +34,12 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
this.displayed$ = this._getDisplayed$; this.displayed$ = this._getDisplayed$;
this.displayedLength$ = this.displayed$.pipe(getLength, shareDistinctLast()); this.displayedLength$ = this.displayed$.pipe(getLength, shareDistinctLast());
this.selected$ = this._selected$.asObservable().pipe(shareDistinctLast()); this.selected$ = this.#selected$.asObservable().pipe(shareDistinctLast());
this.selectedEntities$ = combineLatest([this._selected$.asObservable(), this._entitiesService.all$]).pipe( this.selectedEntities$ = combineLatest([this.#selected$.asObservable(), this._entitiesService.all$]).pipe(
map(([selectedIds, all]) => all.filter(a => selectedIds.includes(a.id))), map(([selectedIds, all]) => all.filter(a => selectedIds.includes(a.id))),
shareLast(), shareLast(),
); );
this.selectedLength$ = this._selected$.pipe(getLength, shareDistinctLast()); this.selectedLength$ = this.#selected$.pipe(getLength, shareDistinctLast());
this.areAllSelected$ = this._areAllSelected$; this.areAllSelected$ = this._areAllSelected$;
this.areSomeSelected$ = this._areSomeSelected$; this.areSomeSelected$ = this._areSomeSelected$;
@ -56,7 +54,7 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
} }
get selectedIds(): PrimaryKey[] { get selectedIds(): PrimaryKey[] {
return this._selected$.getValue(); return this.#selected$.getValue();
} }
private get _getDisplayed$(): Observable<Class[]> { private get _getDisplayed$(): Observable<Class[]> {
@ -67,7 +65,7 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)), map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)),
map(entities => this._searchService.searchIn(entities)), map(entities => this._searchService.searchIn(entities)),
tap(displayed => { tap(displayed => {
this._displayed = displayed; this.#displayed = displayed;
this._updateSelection(); this._updateSelection();
}), }),
shareLast(), shareLast(),
@ -96,15 +94,16 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
} }
private get _allSelected() { private get _allSelected() {
return this._displayed.length !== 0 && this._displayed.length === this.selected.length; return this.#displayed.length !== 0 && this.#displayed.length === this.selected.length;
} }
setSelected(newEntities: Class[]): void { setSelected(newEntities: Class[]): void {
console.trace('setSelected', newEntities);
const selectedIds = newEntities.map(e => e.id); const selectedIds = newEntities.map(e => e.id);
this._selected$.next(selectedIds); this.#selected$.next(selectedIds);
if (this._anchor && !newEntities.includes(this._anchor)) { if (this.#anchor && !newEntities.includes(this.#anchor)) {
this._anchor = undefined; this.#anchor = undefined;
} }
} }
@ -113,7 +112,7 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
} }
isSelected$(entity: Class): Observable<boolean> { isSelected$(entity: Class): Observable<boolean> {
return this._selected$.pipe( return this.#selected$.pipe(
some(selectedId => selectedId === entity.id), some(selectedId => selectedId === entity.id),
shareLast(), shareLast(),
); );
@ -123,7 +122,7 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
if (this._allSelected) { if (this._allSelected) {
return this.setSelected([]); return this.setSelected([]);
} }
this.setSelected(this._displayed); this.setSelected(this.#displayed);
} }
select(entity: Class, withShift = false): void { select(entity: Class, withShift = false): void {
@ -134,8 +133,8 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
if (!withShift) { if (!withShift) {
if (!isCurrentlySelected) { if (!isCurrentlySelected) {
this.setSelected([...currentlySelected, entity]); this.setSelected([...currentlySelected, entity]);
this._anchor = entity; this.#anchor = entity;
this._focus = entity; this.#focus = entity;
} else { } else {
// Entity is previously selected, deselect it // Entity is previously selected, deselect it
this.setSelected(currentlySelected.slice(0, currentEntityIdx).concat(currentlySelected.slice(currentEntityIdx + 1))); this.setSelected(currentlySelected.slice(0, currentEntityIdx).concat(currentlySelected.slice(currentEntityIdx + 1)));
@ -154,52 +153,52 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
/** Move anchor & focus to next selected, or previous selected, or undefined */ /** Move anchor & focus to next selected, or previous selected, or undefined */
#moveAnchorAfterDeselect(entity: Class): void { #moveAnchorAfterDeselect(entity: Class): void {
const entityIdx = this._sortedDisplayed.indexOf(entity); const entityIdx = this.#sortedDisplayed.indexOf(entity);
let newAnchorIdx = entityIdx + 1; let newAnchorIdx = entityIdx + 1;
let increment = 1; let increment = 1;
do { do {
if (this.isSelected(this._sortedDisplayed[newAnchorIdx])) { if (this.isSelected(this.#sortedDisplayed[newAnchorIdx])) {
break; break;
} }
newAnchorIdx += increment; newAnchorIdx += increment;
if (newAnchorIdx === this._sortedDisplayed.length) { if (newAnchorIdx === this.#sortedDisplayed.length) {
newAnchorIdx = entityIdx - 1; newAnchorIdx = entityIdx - 1;
increment = -1; increment = -1;
} }
} while (newAnchorIdx > 0); } while (newAnchorIdx > 0);
this._anchor = this._sortedDisplayed[newAnchorIdx]; this.#anchor = this.#sortedDisplayed[newAnchorIdx];
this._focus = this._sortedDisplayed[newAnchorIdx]; this.#focus = this.#sortedDisplayed[newAnchorIdx];
} }
#shiftClick(entity: Class): void { #shiftClick(entity: Class): void {
const entityIdx = this._sortedDisplayed.indexOf(entity); const entityIdx = this.#sortedDisplayed.indexOf(entity);
if (!this._anchor || !this._focus) { if (!this.#anchor || !this.#focus) {
this._anchor = this._sortedDisplayed[0]; this.#anchor = this.#sortedDisplayed[0];
this._focus = this._sortedDisplayed[0]; this.#focus = this.#sortedDisplayed[0];
} }
const anchorIdx = this._sortedDisplayed.indexOf(this._anchor); const anchorIdx = this.#sortedDisplayed.indexOf(this.#anchor);
const focusIdx = this._sortedDisplayed.indexOf(this._focus); const focusIdx = this.#sortedDisplayed.indexOf(this.#focus);
// Deselect entities between anchor and previous focus // Deselect entities between anchor and previous focus
const remove = this._sortedDisplayed.slice(Math.min(anchorIdx, focusIdx), Math.max(anchorIdx, focusIdx) + 1); const remove = this.#sortedDisplayed.slice(Math.min(anchorIdx, focusIdx), Math.max(anchorIdx, focusIdx) + 1);
// Update focus // Update focus
this._focus = entity; this.#focus = entity;
// Select entities between anchor and new focus // Select entities between anchor and new focus
const intervalEntities = this._sortedDisplayed.slice(Math.min(anchorIdx, entityIdx), Math.max(anchorIdx, entityIdx) + 1); const intervalEntities = this.#sortedDisplayed.slice(Math.min(anchorIdx, entityIdx), Math.max(anchorIdx, entityIdx) + 1);
const newSelected = [...intervalEntities, ...this.selected.filter(e => !intervalEntities.includes(e) && !remove.includes(e))]; const newSelected = [...intervalEntities, ...this.selected.filter(e => !intervalEntities.includes(e) && !remove.includes(e))];
this.setSelected(newSelected); this.setSelected(newSelected);
} }
private _updateSelection(): void { private _updateSelection(): void {
const items = this._displayed.filter(item => this.selected.includes(item)); const items = this.#displayed.filter(item => this.selected.includes(item));
this.setSelected(items); this.setSelected(items);
} }
@ -209,7 +208,7 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
return this._sortingService.sortingOption$.pipe( return this._sortingService.sortingOption$.pipe(
switchMap(() => sortedEntities$), switchMap(() => sortedEntities$),
tap(sortedEntities => { tap(sortedEntities => {
this._sortedDisplayed = sortedEntities; this.#sortedDisplayed = sortedEntities;
}), }),
shareDistinctLast(), shareDistinctLast(),
); );