From baad3e1dac5467fc5a8488f4bbe67706c606e13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Wed, 8 Feb 2023 17:20:13 +0200 Subject: [PATCH] Shift click selection logic --- src/lib/listing/services/listing.service.ts | 75 +++++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/src/lib/listing/services/listing.service.ts b/src/lib/listing/services/listing.service.ts index de765de..740771c 100644 --- a/src/lib/listing/services/listing.service.ts +++ b/src/lib/listing/services/listing.service.ts @@ -19,9 +19,13 @@ export class ListingService, PrimaryKey exte readonly selectedEntities$: Observable; readonly selectedLength$: Observable; readonly sortedDisplayedEntities$: Observable; + private _displayed: Class[] = []; private readonly _selected$ = new BehaviorSubject([]); - readonly #sortedDisplayedEntities$ = new BehaviorSubject([]); + + private _anchor: Class | undefined = undefined; + private _focus: Class | undefined = undefined; + private _sortedDisplayed: Class[] = []; constructor( protected readonly _filterService: FilterService, @@ -98,6 +102,10 @@ export class ListingService, PrimaryKey exte setSelected(newEntities: Class[]): void { const selectedIds = newEntities.map(e => e.id); this._selected$.next(selectedIds); + + if (this._anchor && !newEntities.includes(this._anchor)) { + this._anchor = undefined; + } } isSelected(entity: Class): boolean { @@ -121,13 +129,20 @@ export class ListingService, PrimaryKey exte select(entity: Class, withShift = false): void { const currentlySelected = this.selected; const currentEntityIdx = currentlySelected.indexOf(entity); + const isCurrentlySelected = currentEntityIdx !== -1; - if (currentEntityIdx === -1) { - // Entity is not previously selected, select it - this.setSelected([...currentlySelected, entity]); + if (!withShift) { + if (!isCurrentlySelected) { + this.setSelected([...currentlySelected, entity]); + this._anchor = entity; + this._focus = entity; + } else { + // Entity is previously selected, deselect it + this.setSelected(currentlySelected.slice(0, currentEntityIdx).concat(currentlySelected.slice(currentEntityIdx + 1))); + this.#moveAnchorAfterDeselect(entity); + } } else { - // Entity is previously selected, deselect it - this.setSelected(currentlySelected.slice(0, currentEntityIdx).concat(currentlySelected.slice(currentEntityIdx + 1))); + this.#shiftClick(entity); } } @@ -137,6 +152,52 @@ export class ListingService, PrimaryKey exte this.setSelected(this.selected.filter(el => !entitiesIds.includes(el.id))); } + /** Move anchor & focus to next selected, or previous selected, or undefined */ + #moveAnchorAfterDeselect(entity: Class): void { + const entityIdx = this._sortedDisplayed.indexOf(entity); + + let newAnchorIdx = entityIdx + 1; + let increment = 1; + do { + if (this.isSelected(this._sortedDisplayed[newAnchorIdx])) { + break; + } + + newAnchorIdx += increment; + if (newAnchorIdx === this._sortedDisplayed.length) { + newAnchorIdx = entityIdx - 1; + increment = -1; + } + } while (newAnchorIdx > 0); + + this._anchor = this._sortedDisplayed[newAnchorIdx]; + this._focus = this._sortedDisplayed[newAnchorIdx]; + } + + #shiftClick(entity: Class): void { + const entityIdx = this._sortedDisplayed.indexOf(entity); + + if (!this._anchor || !this._focus) { + this._anchor = this._sortedDisplayed[0]; + this._focus = this._sortedDisplayed[0]; + } + + const anchorIdx = this._sortedDisplayed.indexOf(this._anchor); + const focusIdx = this._sortedDisplayed.indexOf(this._focus); + + // Deselect entities between anchor and previous focus + const remove = this._sortedDisplayed.slice(Math.min(anchorIdx, focusIdx), Math.max(anchorIdx, focusIdx) + 1); + + // Update focus + this._focus = entity; + + // Select entities between anchor and new focus + 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))]; + this.setSelected(newSelected); + } + private _updateSelection(): void { const items = this._displayed.filter(item => this.selected.includes(item)); this.setSelected(items); @@ -148,7 +209,7 @@ export class ListingService, PrimaryKey exte return this._sortingService.sortingOption$.pipe( switchMap(() => sortedEntities$), tap(sortedEntities => { - this.#sortedDisplayedEntities$.next(sortedEntities); + this._sortedDisplayed = sortedEntities; }), shareDistinctLast(), );