Skip to content

Commit 0049afe

Browse files
committed
feature(wysiwyg): refactor service into multiselect-model
1 parent f2297d6 commit 0049afe

File tree

7 files changed

+163
-175
lines changed

7 files changed

+163
-175
lines changed

apps/codelab/src/app/admin/content/presentation-editor/side-panel/side-panel.component.html

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,29 @@
66
[class.dragging]="dragging"
77
cdkDropList
88
multiselectList
9-
[items]="slides"
10-
[(select)]="selectedSlideIndexes"
9+
[msModel]="selectionModel"
10+
[msItems]="slideIds"
1111
>
1212
<div
1313
(cdkDragDropped)="dropped()"
1414
(cdkDragEnded)="dragEnded()"
1515
(cdkDragStarted)="dragStarted($event)"
1616
(click)="select($event, index)"
1717
*ngFor="let slide of slides; let index = index; trackBy: trackBySlideId"
18-
[class.selected]="selectedSlideIndexes.includes(index)"
18+
[class.selected]="selectionModel.isSelected(slide.id)"
1919
[class.current]="index == currentSlideIndex"
2020
cdkDragLockAxis="y"
21-
multiselectItem
22-
[item]="index"
2321
cdkDrag
22+
multiselectItem
23+
[msItem]="slide.id"
2424
class="slide-wrapper"
2525
role="button"
2626
>
2727
<div *cdkDragPreview>
28-
<div *ngFor="let sel of selectedSlideIndexes"></div>
28+
<div *ngFor="let selectedItem of selectionModel.selected"></div>
2929
</div>
3030
<div *cdkDragPlaceholder>
31-
<div
32-
style="background: #00a4ff; height: 1px; width: 100%; margin: 2px 0"
33-
></div>
31+
<div class="drag-placeholder"></div>
3432
</div>
3533

3634
<div class="slide">

apps/codelab/src/app/admin/content/presentation-editor/side-panel/side-panel.component.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,10 @@
6565
display: none;
6666
}
6767
}
68+
69+
.drag-placeholder {
70+
height: 1px;
71+
width: 100%;
72+
margin: 2px 0;
73+
background: #00a4ff;
74+
}

apps/codelab/src/app/admin/content/presentation-editor/side-panel/side-panel.component.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
ElementRef,
44
HostListener,
55
Input,
6-
OnInit
6+
OnChanges,
7+
OnInit,
8+
SimpleChanges
79
} from '@angular/core';
810
import { Location } from '@angular/common';
911
import { CdkDragDrop, CdkDragStart, DragRef } from '@angular/cdk/drag-drop';
@@ -12,24 +14,35 @@ import { ActivatedRoute, Router } from '@angular/router';
1214
import { ContentService } from '../services/content.service';
1315
import { NavigationService } from '../services/navigation.service';
1416
import { ContentSlide } from '../types';
15-
import {
16-
isCtrlEvent,
17-
isShiftEvent
18-
} from '../../../../multiselect/multiselect.service';
17+
18+
import { MultiselectModel } from '../../../../multiselect/multiselect-model';
19+
20+
function normalizeSelectionIndexes(indexes: number[]): number[] {
21+
return indexes.filter((index) => index >= 0);
22+
}
23+
24+
function selectionModelToIndexes<T>(items: T[], model: MultiselectModel<T>): number[] {
25+
return normalizeSelectionIndexes(model.selected.map((item) => items.indexOf(item)));
26+
}
27+
28+
function slideIdsMapper(slides: ContentSlide[]): string[] {
29+
return slides.map(slide => slide.id);
30+
}
1931

2032
@Component({
2133
selector: 'slides-side-panel',
2234
templateUrl: './side-panel.component.html',
2335
styleUrls: ['./side-panel.component.scss']
2436
})
25-
export class SidePanelComponent implements OnInit {
37+
export class SidePanelComponent implements OnInit, OnChanges {
2638
@Input() slides: ContentSlide[];
2739
@Input() currentSlideIndex = 0;
2840
@Input() presentationId!: string;
2941

3042
public dragging: DragRef = null;
3143
public sidePanelInFocus = false;
32-
public selectedSlideIndexes: number[] = [];
44+
public selectionModel: MultiselectModel<string> = new MultiselectModel();
45+
public slideIds: string[] = [];
3346

3447
constructor(
3548
readonly el: ElementRef,
@@ -44,6 +57,14 @@ export class SidePanelComponent implements OnInit {
4457
this.resetSelected();
4558
}
4659

60+
ngOnChanges(changes: SimpleChanges) {
61+
if (changes.slides) {
62+
this.slideIds = slideIdsMapper(changes.slides.currentValue);
63+
64+
this.resetSelected();
65+
}
66+
}
67+
4768
trackBySlideId(index: number, slide: ContentSlide) {
4869
return slide.id;
4970
}
@@ -82,11 +103,9 @@ export class SidePanelComponent implements OnInit {
82103
} else if (event.key === 'Delete') {
83104
this.contentService.deleteSlides(
84105
this.presentationId,
85-
this.selectedSlideIndexes
106+
selectionModelToIndexes(this.slideIds, this.selectionModel)
86107
);
87108

88-
this.resetSelected();
89-
90109
event.preventDefault();
91110
}
92111
}
@@ -109,21 +128,25 @@ export class SidePanelComponent implements OnInit {
109128
}
110129

111130
droppedIntoList(event: CdkDragDrop<any, any>) {
131+
const selectedSlideIndexes = selectionModelToIndexes(this.slideIds, this.selectionModel);
132+
112133
this.contentService.reorderSlides(
113134
this.presentationId,
114-
this.selectedSlideIndexes,
115-
event.currentIndex - this.selectedSlideIndexes.length + 1
135+
selectedSlideIndexes,
136+
event.currentIndex - selectedSlideIndexes.length + 1
116137
);
117-
118-
this.resetSelected();
119138
}
120139

121140
resetSelected() {
122-
this.selectedSlideIndexes = [this.currentSlideIndex];
141+
this.selectSingle(this.slides[this.currentSlideIndex].id);
142+
}
143+
144+
selectSingle(id: string) {
145+
this.selectionModel.selectSingle(id);
123146
}
124147

125148
select(event: MouseEvent, index: number) {
126-
if (!(isShiftEvent(event) || isCtrlEvent(event)) || !this.dragging) {
149+
if (!this.dragging) {
127150
this.navigationService.goToSlide(this.presentationId, index);
128151
}
129152
}

apps/codelab/src/app/multiselect/multiselect-item.directive.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@ import { MULTISELECT_LIST, MultiselectListDirective } from './multiselect-list.d
44
@Directive({
55
selector: '[multiselectItem]'
66
})
7-
export class MultiselectItemDirective {
7+
export class MultiselectItemDirective<T> {
88

9-
@Input() item: number;
9+
@Input() msItem: T;
1010

1111
constructor(
12-
@Inject(MULTISELECT_LIST) public parentList: MultiselectListDirective
12+
@Inject(MULTISELECT_LIST) public parentList: MultiselectListDirective<T>
1313
) {
1414

1515
}
1616

1717
@HostListener('click', ['$event'])
1818
private handleClickEvent(event: MouseEvent) {
19-
this.parentList.multiselectService.select(event, this.item);
19+
if (event.ctrlKey || event.metaKey) {
20+
this.parentList.msModel.toggleSingle(this.msItem);
21+
} else if (event.shiftKey) {
22+
this.parentList.msModel.toggleContinuous(this.msItem);
23+
} else {
24+
this.parentList.msModel.selectSingle(this.msItem);
25+
}
2026
}
2127

2228
}
Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { Directive, ElementRef, EventEmitter, HostListener, InjectionToken, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
1+
import { Directive, ElementRef, HostListener, InjectionToken, Input, OnChanges, SimpleChanges } from '@angular/core';
22

3-
import { Subject } from 'rxjs';
4-
import { takeUntil } from 'rxjs/operators';
3+
import { MultiselectModel } from './multiselect-model';
54

6-
import { MultiselectService } from './multiselect.service';
7-
8-
export const MULTISELECT_LIST = new InjectionToken<MultiselectListDirective>('MultiselectList');
5+
export const MULTISELECT_LIST = new InjectionToken<MultiselectListDirective<any>>('MultiselectList');
96

107
@Directive({
118
selector: '[multiselectList]',
@@ -14,57 +11,23 @@ export const MULTISELECT_LIST = new InjectionToken<MultiselectListDirective>('Mu
1411
provide: MULTISELECT_LIST,
1512
useExisting: MultiselectListDirective
1613
},
17-
MultiselectService
1814
]
1915
})
20-
export class MultiselectListDirective implements OnInit, OnChanges, OnDestroy {
16+
export class MultiselectListDirective<T> implements OnChanges {
2117
elementHasFocus = false;
22-
@Input() items = [];
23-
@Input() select = [];
24-
@Output() selectChange = new EventEmitter<number[]>();
25-
@Output() reset = new EventEmitter();
26-
private readonly onDestroy = new Subject<void>();
18+
19+
@Input() msItems = [];
20+
@Input() msModel: MultiselectModel<T>;
2721

2822
constructor(
29-
public readonly multiselectService: MultiselectService,
3023
private readonly el: ElementRef
3124
) {
3225

3326
}
3427

35-
ngOnInit() {
36-
this.multiselectService.selection$
37-
.pipe(takeUntil(this.onDestroy))
38-
.subscribe((selection) => {
39-
this.selectChange.emit(selection);
40-
});
41-
}
42-
43-
ngOnChanges(changes: SimpleChanges) {
44-
if ('select' in changes) {
45-
if (changes.select.firstChange && changes.select.currentValue[0] >= 0) {
46-
const firstSelectIndex = changes.select.currentValue[0];
47-
48-
this.multiselectService.addToSelection(firstSelectIndex);
49-
this.multiselectService.lastSingleSelection = firstSelectIndex;
50-
}
51-
}
52-
}
53-
54-
ngOnDestroy() {
55-
this.onDestroy.next();
56-
this.onDestroy.complete();
57-
}
58-
5928
@HostListener('document:click', ['$event'])
6029
private handleClickEvent(event: MouseEvent) {
6130
this.elementHasFocus = this.el.nativeElement.contains(event.target);
62-
63-
64-
// TODO need a better way to reset
65-
// if (!this.elementHasFocus) {
66-
// this.multiselectService.resetSelection(this.currentSlideIndex);
67-
// }
6831
}
6932

7033
@HostListener('document:keydown', ['$event'])
@@ -74,11 +37,16 @@ export class MultiselectListDirective implements OnInit, OnChanges, OnDestroy {
7437
}
7538

7639
if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
77-
const allIndexes = this.items.map((_, index) => index);
78-
this.multiselectService.selectAll(allIndexes);
40+
this.msModel.toggleAllItems(this.msItems, true);
7941

8042
event.preventDefault();
8143
}
8244
}
8345

46+
ngOnChanges(changes: SimpleChanges) {
47+
if (changes.msItems) {
48+
this.msModel.setValues(changes.msItems.currentValue);
49+
}
50+
}
51+
8452
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Subject } from 'rxjs';
2+
3+
export class MultiselectModel<T> {
4+
5+
private selection = new Set<T>();
6+
7+
private stack: T[] = [];
8+
9+
private values: T[] = [];
10+
11+
changed = new Subject<T[]>();
12+
13+
get selected(): T[] {
14+
return Array.from(this.selection.values());
15+
}
16+
17+
setValues(values: T[]) {
18+
this.values = values;
19+
}
20+
21+
findBetween(item1: T, item2: T) {
22+
const index1 = this.values.indexOf(item1);
23+
const index2 = this.values.indexOf(item2);
24+
25+
const numberOfItems = Math.abs(index1 - index2) + 1;
26+
const firstIndex = index1 < index2 ? index1 : index2;
27+
28+
return this.values.slice(firstIndex, firstIndex + numberOfItems);
29+
}
30+
31+
toggleAllItems(items: T[], shouldSelect: boolean) {
32+
for (const item of items) {
33+
if (shouldSelect) {
34+
this.selection.add(item);
35+
} else {
36+
this.selection.delete(item);
37+
}
38+
}
39+
40+
this.emitChangeEvent();
41+
}
42+
43+
toggleSingle(item: T): void {
44+
if (this.isSelected(item)) {
45+
this.toggleAllItems([item], false);
46+
this.stack = this.stack.filter(v => v !== item);
47+
} else {
48+
this.toggleAllItems([item], true);
49+
this.stack.push(item);
50+
}
51+
}
52+
53+
toggleContinuous(item: T): void {
54+
const between = this.findBetween(this.stack[this.stack.length - 1], item);
55+
56+
this.toggleAllItems(between, !this.isSelected(item));
57+
58+
this.stack.push(item);
59+
}
60+
61+
selectSingle(item: T): void {
62+
this.selection.clear();
63+
64+
this.toggleAllItems([item], true);
65+
66+
this.stack.push(item);
67+
}
68+
69+
isSelected(item: T): boolean {
70+
return this.selection.has(item);
71+
}
72+
73+
clear(): void {
74+
this.selection.clear();
75+
this.emitChangeEvent();
76+
}
77+
78+
isEmpty(): boolean {
79+
return this.selection.size === 0;
80+
}
81+
82+
emitChangeEvent(): void {
83+
this.changed.next(this.selected);
84+
}
85+
86+
}

0 commit comments

Comments
 (0)