Skip to content

Commit 5831ab8

Browse files
committed
feature(wysiwyg): refactor multiselect into directive
1 parent df5a149 commit 5831ab8

File tree

7 files changed

+167
-56
lines changed

7 files changed

+167
-56
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@
55
(cdkDropListDropped)="droppedIntoList($event)"
66
[class.dragging]="dragging"
77
cdkDropList
8+
multiselectList
9+
[items]="slides"
10+
[(select)]="selectedSlideIndexes"
811
>
912
<div
1013
(cdkDragDropped)="dropped()"
1114
(cdkDragEnded)="dragEnded()"
1215
(cdkDragStarted)="dragStarted($event)"
1316
(click)="select($event, index)"
1417
*ngFor="let slide of slides; let index = index; trackBy: trackBySlideId"
15-
[class.selected]="multiSelectionService.selections.includes(index)"
18+
[class.selected]="selectedSlideIndexes.includes(index)"
1619
[class.current]="index == currentSlideIndex"
1720
cdkDragLockAxis="y"
21+
multiselectItem
22+
[item]="index"
1823
cdkDrag
1924
class="slide-wrapper"
2025
role="button"
2126
>
2227
<div *cdkDragPreview>
23-
<div *ngFor="let sel of multiSelectionService.selections"></div>
28+
<div *ngFor="let sel of selectedSlideIndexes"></div>
2429
</div>
2530
<div *cdkDragPlaceholder>
2631
<div

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

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import { NavigationService } from '../services/navigation.service';
1414
import { ContentSlide } from '../types';
1515
import {
1616
isCtrlEvent,
17-
isShiftEvent,
18-
MultiSelectionService
19-
} from './multi-selection.service';
17+
isShiftEvent
18+
} from '../../../../multiselect/multiselect.service';
2019

2120
@Component({
2221
selector: 'slides-side-panel',
@@ -30,24 +29,19 @@ export class SidePanelComponent implements OnInit {
3029

3130
public dragging: DragRef = null;
3231
public sidePanelInFocus = false;
32+
public selectedSlideIndexes: number[] = [];
3333

3434
constructor(
3535
readonly el: ElementRef,
3636
readonly location: Location,
3737
readonly route: ActivatedRoute,
3838
readonly router: Router,
39-
readonly multiSelectionService: MultiSelectionService,
4039
readonly contentService: ContentService,
4140
readonly navigationService: NavigationService
4241
) {}
4342

4443
ngOnInit() {
45-
this.multiSelectionService.selections$.subscribe(selections => {
46-
console.log({ selections });
47-
});
48-
49-
this.multiSelectionService.addToSelections(this.currentSlideIndex);
50-
this.multiSelectionService.lastSingleSelection = this.currentSlideIndex;
44+
this.selectedSlideIndexes = [this.currentSlideIndex];
5145
}
5246

5347
trackBySlideId(index: number, slide: ContentSlide) {
@@ -80,22 +74,18 @@ export class SidePanelComponent implements OnInit {
8074
return;
8175
}
8276

83-
if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
84-
const allSideIndexes = this.slides.map((slide, index) => index);
85-
this.multiSelectionService.selectAll(allSideIndexes);
86-
87-
event.preventDefault();
88-
} else if (event.key === 'Escape' && this.dragging) {
77+
if (event.key === 'Escape' && this.dragging) {
8978
this.dragging.reset();
9079
document.dispatchEvent(new Event('mouseup'));
9180

9281
event.preventDefault();
9382
} else if (event.key === 'Delete') {
9483
this.contentService.deleteSlides(
9584
this.presentationId,
96-
this.multiSelectionService.selections
85+
this.selectedSlideIndexes
9786
);
98-
this.multiSelectionService.resetSelection(this.currentSlideIndex);
87+
88+
this.selectedSlideIndexes = [this.currentSlideIndex];
9989

10090
event.preventDefault();
10191
}
@@ -104,10 +94,6 @@ export class SidePanelComponent implements OnInit {
10494
@HostListener('document:click', ['$event'])
10595
private handleClickEvent(event: MouseEvent) {
10696
this.sidePanelInFocus = this.el.nativeElement.contains(event.target);
107-
108-
if (!this.sidePanelInFocus) {
109-
this.multiSelectionService.resetSelection(this.currentSlideIndex);
110-
}
11197
}
11298

11399
dragStarted(event: CdkDragStart) {
@@ -125,18 +111,17 @@ export class SidePanelComponent implements OnInit {
125111
droppedIntoList(event: CdkDragDrop<any, any>) {
126112
this.contentService.reorderSlides(
127113
this.presentationId,
128-
this.multiSelectionService.selections,
129-
event.currentIndex - this.multiSelectionService.selections.length + 1
114+
this.selectedSlideIndexes,
115+
event.currentIndex - this.selectedSlideIndexes.length + 1
130116
);
131117

132-
this.multiSelectionService.resetSelection(this.currentSlideIndex);
118+
this.selectedSlideIndexes = [this.currentSlideIndex];
133119
}
134120

135121
select(event: MouseEvent, index: number) {
136-
this.multiSelectionService.select(event, index);
137-
138122
if (!(isShiftEvent(event) || isCtrlEvent(event)) || !this.dragging) {
139123
this.navigationService.goToSlide(this.presentationId, index);
140124
}
141125
}
126+
142127
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { RouterModule } from '@angular/router';
55
import { DragDropModule } from '@angular/cdk/drag-drop';
66

77
import { PreviewModule } from '../preview/preview.module';
8-
import { MultiSelectionService } from './multi-selection.service';
8+
import { MultiselectModule } from '../../../../multiselect/multiselect.module';
99

1010
@NgModule({
1111
declarations: [SidePanelComponent],
12-
providers: [MultiSelectionService],
1312
exports: [SidePanelComponent],
14-
imports: [CommonModule, RouterModule, DragDropModule, PreviewModule]
13+
imports: [CommonModule, RouterModule, DragDropModule, PreviewModule, MultiselectModule]
1514
})
1615
export class SidePanelModule {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Directive, HostListener, Inject, Input } from '@angular/core';
2+
import { MULTISELECT_LIST, MultiselectListDirective } from './multiselect-list.directive';
3+
4+
@Directive({
5+
selector: '[multiselectItem]'
6+
})
7+
export class MultiselectItemDirective {
8+
9+
@Input() item: number;
10+
11+
constructor(
12+
@Inject(MULTISELECT_LIST) public parentList: MultiselectListDirective
13+
) {
14+
15+
}
16+
17+
@HostListener('click', ['$event'])
18+
private handleClickEvent(event: MouseEvent) {
19+
this.parentList.multiselectService.select(event, this.item);
20+
}
21+
22+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Directive, ElementRef, EventEmitter, HostListener, InjectionToken, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
2+
3+
import { Subject } from 'rxjs';
4+
import { takeUntil } from 'rxjs/operators';
5+
6+
import { MultiselectService } from './multiselect.service';
7+
8+
export const MULTISELECT_LIST = new InjectionToken<MultiselectListDirective>('MultiselectList');
9+
10+
@Directive({
11+
selector: '[multiselectList]',
12+
providers: [
13+
{
14+
provide: MULTISELECT_LIST,
15+
useExisting: MultiselectListDirective
16+
},
17+
MultiselectService
18+
]
19+
})
20+
export class MultiselectListDirective implements OnInit, OnChanges, OnDestroy {
21+
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>();
27+
28+
constructor(
29+
public readonly multiselectService: MultiselectService,
30+
private readonly el: ElementRef
31+
) {
32+
33+
}
34+
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+
59+
@HostListener('document:click', ['$event'])
60+
private handleClickEvent(event: MouseEvent) {
61+
this.elementHasFocus = this.el.nativeElement.contains(event.target);
62+
63+
64+
// TODO
65+
// if (!this.elementHasFocus) {
66+
//
67+
// this.multiselectService.resetSelection(this.currentSlideIndex);
68+
// }
69+
}
70+
71+
@HostListener('document:keydown', ['$event'])
72+
private handleKeyboardEvent(event: KeyboardEvent) {
73+
if (!this.elementHasFocus) {
74+
return;
75+
}
76+
77+
if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
78+
const allIndexes = this.items.map((_, index) => index);
79+
this.multiselectService.selectAll(allIndexes);
80+
81+
event.preventDefault();
82+
}
83+
}
84+
85+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NgModule } from '@angular/core';
2+
3+
import { MultiselectItemDirective } from './multiselect-item.directive';
4+
import { MultiselectListDirective } from './multiselect-list.directive';
5+
6+
@NgModule({
7+
declarations: [
8+
MultiselectListDirective,
9+
MultiselectItemDirective
10+
],
11+
exports: [
12+
MultiselectListDirective,
13+
MultiselectItemDirective
14+
],
15+
})
16+
export class MultiselectModule { }
Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,62 +11,61 @@ export function isShiftEvent(event: MouseEvent) {
1111
}
1212

1313
@Injectable()
14-
export class MultiSelectionService {
15-
public selections$ = new BehaviorSubject<number[]>([]);
16-
public selections: number[] = [];
14+
export class MultiselectService {
15+
public selection$ = new BehaviorSubject<number[]>([]);
16+
public selection: number[] = [];
1717

1818
public lastSingleSelection: number | null = null;
1919

2020
constructor() {}
2121

2222
isAlreadySelected(index: number): boolean {
23-
return this.selections.indexOf(index) >= 0;
23+
return this.selection.indexOf(index) >= 0;
2424
}
2525

26-
normalizeSelections(indexes: number[]): number[] {
26+
normalizeSelection(indexes: number[]): number[] {
2727
return indexes.sort((a, b) => a - b);
2828
}
2929

3030
isSelectionEmpty(): boolean {
31-
return this.selections.length < 1;
31+
return this.selection.length < 1;
3232
}
3333

34-
setSelections(indexes: number[]) {
35-
this.selections = this.normalizeSelections(indexes);
36-
this.selections$.next(this.selections);
34+
setSelection(indexes: number[]) {
35+
this.selection = this.normalizeSelection(indexes);
36+
this.selection$.next(this.selection);
3737
}
3838

39-
addToSelections(index: number) {
39+
addToSelection(index: number) {
4040
if (!this.isAlreadySelected(index)) {
41-
this.setSelections([...this.selections, index]);
41+
this.setSelection([...this.selection, index]);
4242
}
4343
}
4444

4545
select(event: MouseEvent, selectedIndex: number) {
46-
const shiftSelect =
47-
isShiftEvent(event) &&
48-
(this.lastSingleSelection || this.lastSingleSelection === 0) &&
49-
this.lastSingleSelection !== selectedIndex;
46+
const shiftSelect = isShiftEvent(event)
47+
&& this.lastSingleSelection >= 0
48+
&& this.lastSingleSelection !== selectedIndex;
5049

5150
if (this.isSelectionEmpty()) {
52-
this.setSelections([selectedIndex]);
51+
this.setSelection([selectedIndex]);
5352
this.lastSingleSelection = selectedIndex;
5453
} else if (isCtrlEvent(event)) {
5554
const alreadySelected = this.isAlreadySelected(selectedIndex);
5655

5756
if (alreadySelected) {
58-
const selectionsWithoutSelectedIndex = this.selections.filter(
57+
const selectionWithoutSelectedIndex = this.selection.filter(
5958
index => index !== selectedIndex
6059
);
6160

62-
if (selectionsWithoutSelectedIndex.length > 0) {
63-
this.setSelections(selectionsWithoutSelectedIndex);
61+
if (selectionWithoutSelectedIndex.length > 0) {
62+
this.setSelection(selectionWithoutSelectedIndex);
6463
} else {
65-
this.setSelections([selectedIndex]);
64+
this.setSelection([selectedIndex]);
6665
this.lastSingleSelection = selectedIndex;
6766
}
6867
} else {
69-
this.setSelections([...this.selections, selectedIndex]);
68+
this.setSelection([...this.selection, selectedIndex]);
7069
this.lastSingleSelection = selectedIndex;
7170
}
7271
} else if (shiftSelect) {
@@ -83,7 +82,7 @@ export class MultiSelectionService {
8382
: this.lastSingleSelection + index
8483
);
8584

86-
this.setSelections([...shiftSelection]);
85+
this.setSelection([...shiftSelection]);
8786
} else {
8887
this.resetSelection(selectedIndex);
8988
}
@@ -92,10 +91,10 @@ export class MultiSelectionService {
9291
resetSelection(currentIndex: number) {
9392
this.lastSingleSelection = currentIndex;
9493

95-
this.setSelections([currentIndex]);
94+
this.setSelection([currentIndex]);
9695
}
9796

9897
selectAll(indexes: number[]) {
99-
this.setSelections(indexes);
98+
this.setSelection(indexes);
10099
}
101100
}

0 commit comments

Comments
 (0)