Skip to content

Commit 2d4c213

Browse files
committed
feature(wysiwyg): refactor
1 parent acd09e8 commit 2d4c213

File tree

9 files changed

+114
-80
lines changed

9 files changed

+114
-80
lines changed

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
<div class="slides-wrapper">
1+
<div class="slides-wrapper" tabindex="0">
22
<button (click)="addSlide()">Add</button>
33

44
<div
55
(cdkDropListDropped)="droppedIntoList($event)"
66
[class.dragging]="dragging"
77
cdkDropList
8-
multiselectList
9-
[msModel]="selectionModel"
8+
[multiselectList]="selectionModel"
109
>
1110
<div
1211
(cdkDragDropped)="dropped()"
1312
(cdkDragEnded)="dragEnded()"
1413
(cdkDragStarted)="dragStarted($event)"
15-
(click)="select($event, index)"
14+
(click)="makeCurrent($event, index)"
1615
*ngFor="let slide of slides; let index = index; trackBy: trackBySlideId"
1716
[class.selected]="selectionModel.isSelected(slide.id)"
1817
[class.current]="index == currentSlideIndex"
1918
cdkDragLockAxis="y"
2019
cdkDrag
21-
multiselectItem
22-
[msItem]="slide.id"
20+
[multiselectItem]="slide.id"
2321
class="slide-wrapper"
2422
role="button"
2523
>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
background: #f8f8f8;
1616
}
1717

18+
.slides-wrapper:focus {
19+
outline: 0;
20+
}
21+
1822
.slide-wrapper.current {
1923
border: 1px #ddd solid;
2024
box-shadow: 0 0 2px 1px #155ad0;

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

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,10 @@ import { NavigationService } from '../services/navigation.service';
1616
import { ContentSlide } from '../types';
1717

1818
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>(
25-
items: T[],
26-
model: MultiselectModel<T>
27-
): number[] {
28-
return normalizeSelectionIndexes(
29-
model.selected.map(item => items.indexOf(item))
30-
);
31-
}
19+
import {
20+
isFromContext,
21+
KeyboardEventWithTarget
22+
} from '../../../../shared/helpers/helpers';
3223

3324
function slideIdsMapper(slides: ContentSlide[]): string[] {
3425
return slides.map(slide => slide.id);
@@ -45,7 +36,6 @@ export class SidePanelComponent implements OnInit, OnChanges {
4536
@Input() presentationId!: string;
4637

4738
public dragging: DragRef = null;
48-
public elementHasFocus = false;
4939
public selectionModel: MultiselectModel<string> = new MultiselectModel();
5040
public slideIds: string[] = [];
5141

@@ -79,48 +69,69 @@ export class SidePanelComponent implements OnInit, OnChanges {
7969
}
8070

8171
@HostListener('document:keydown.arrowdown', ['$event'])
82-
nextSlide(event: KeyboardEvent) {
83-
if (this.elementHasFocus) {
72+
nextSlide(event: KeyboardEventWithTarget<HTMLElement>) {
73+
if (this.isFromSidePanelContext(event.target)) {
8474
this.navigationService.nextSlide(this.presentationId);
75+
8576
event.preventDefault();
8677
}
8778
}
8879

8980
@HostListener('document:keydown.arrowup', ['$event'])
90-
prevSlide(event: KeyboardEvent) {
91-
if (this.elementHasFocus) {
81+
prevSlide(event: KeyboardEventWithTarget<HTMLElement>) {
82+
if (this.isFromSidePanelContext(event.target)) {
9283
this.navigationService.previousSlide(this.presentationId);
84+
9385
event.preventDefault();
9486
}
9587
}
9688

97-
@HostListener('document:keydown', ['$event'])
98-
private handleKeyboardEvent(event: KeyboardEvent) {
99-
if (!this.elementHasFocus) {
100-
return;
101-
}
102-
103-
if (event.key === 'Escape' && this.dragging) {
89+
@HostListener('document:keydown.Escape', ['$event'])
90+
private handleEscapeEvent(event: KeyboardEventWithTarget<HTMLElement>) {
91+
if (this.isFromSidePanelContext(event.target) && this.dragging) {
10492
this.dragging.reset();
93+
94+
// This is a workaround to completely reset dragging
95+
// https://stackoverflow.com/a/62537983
10596
document.dispatchEvent(new Event('mouseup'));
10697

10798
event.preventDefault();
108-
} else if (event.key === 'Delete') {
99+
}
100+
}
101+
102+
@HostListener('document:keydown.Delete', ['$event'])
103+
private handleDeleteEvent(event: KeyboardEventWithTarget<HTMLElement>) {
104+
if (this.isFromSidePanelContext(event.target)) {
105+
const selectedSlideIndexes = this.getSelectedSlideIndexes();
106+
109107
this.contentService.deleteSlides(
110108
this.presentationId,
111-
selectionModelToIndexes(this.slideIds, this.selectionModel)
109+
selectedSlideIndexes
112110
);
113111

114112
event.preventDefault();
115113
}
116114
}
117115

118-
@HostListener('document:click', ['$event'])
119-
private handleClickEvent(event: MouseEvent) {
120-
this.elementHasFocus = this.el.nativeElement.contains(event.target);
116+
@HostListener('document:keydown.control.a', ['$event'])
117+
@HostListener('document:keydown.meta.a', ['$event'])
118+
private handleCtrlMetaEvent(event: KeyboardEventWithTarget<HTMLElement>) {
119+
if (this.isFromSidePanelContext(event.target)) {
120+
this.selectionModel.toggleAll();
121+
122+
event.preventDefault();
123+
}
124+
}
125+
126+
private isFromSidePanelContext(target: HTMLElement): boolean {
127+
return isFromContext(target, target =>
128+
target.classList.contains('slides-wrapper')
129+
);
121130
}
122131

123132
dragStarted(event: CdkDragStart) {
133+
// Undocumented reference to the underlying drag instance.
134+
// Its needed to reset dragging
124135
this.dragging = event.source._dragRef;
125136
}
126137

@@ -132,11 +143,8 @@ export class SidePanelComponent implements OnInit, OnChanges {
132143
this.dragging = null;
133144
}
134145

135-
droppedIntoList(event: CdkDragDrop<any, any>) {
136-
const selectedSlideIndexes = selectionModelToIndexes(
137-
this.slideIds,
138-
this.selectionModel
139-
);
146+
droppedIntoList(event: CdkDragDrop<ElementRef<HTMLElement>>) {
147+
const selectedSlideIndexes = this.getSelectedSlideIndexes();
140148

141149
this.contentService.reorderSlides(
142150
this.presentationId,
@@ -149,11 +157,15 @@ export class SidePanelComponent implements OnInit, OnChanges {
149157
this.selectSingle(this.slides[this.currentSlideIndex].id);
150158
}
151159

160+
getSelectedSlideIndexes(): number[] {
161+
return this.selectionModel.getSelectedIndexes();
162+
}
163+
152164
selectSingle(id: string) {
153165
this.selectionModel.selectSingle(id);
154166
}
155167

156-
select(event: MouseEvent, index: number) {
168+
makeCurrent(event: MouseEvent, index: number) {
157169
if (!this.dragging) {
158170
this.navigationService.goToSlide(this.presentationId, index);
159171
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
selector: '[multiselectItem]'
1515
})
1616
export class MultiselectItemDirective<T> {
17-
@Input() msItem: T;
17+
@Input() multiselectItem: T;
1818

1919
constructor(
2020
public element: ElementRef<HTMLElement>,
@@ -24,11 +24,17 @@ export class MultiselectItemDirective<T> {
2424
@HostListener('click', ['$event'])
2525
private handleClickEvent(event: MouseEvent) {
2626
if (event.ctrlKey || event.metaKey) {
27-
this.parentList.msModel.toggleSingle(this.msItem);
28-
} else if (event.shiftKey) {
29-
this.parentList.msModel.toggleContinuous(this.msItem);
30-
} else {
31-
this.parentList.msModel.selectSingle(this.msItem);
27+
this.parentList.multiselectList.toggleSingle(this.multiselectItem);
28+
29+
return;
3230
}
31+
32+
if (event.shiftKey) {
33+
this.parentList.multiselectList.toggleContinuous(this.multiselectItem);
34+
35+
return;
36+
}
37+
38+
this.parentList.multiselectList.selectSingle(this.multiselectItem);
3339
}
3440
}
Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
Directive,
3-
ElementRef,
4-
HostListener,
5-
InjectionToken,
6-
Input
7-
} from '@angular/core';
1+
import { Directive, InjectionToken, Input } from '@angular/core';
82

93
import { MultiselectModel } from './multiselect-model';
104

@@ -22,27 +16,5 @@ export const MULTISELECT_LIST = new InjectionToken<
2216
]
2317
})
2418
export class MultiselectListDirective<T> {
25-
elementHasFocus = false;
26-
27-
@Input() msModel: MultiselectModel<T>;
28-
29-
constructor(private readonly el: ElementRef) {}
30-
31-
@HostListener('document:click', ['$event'])
32-
private handleClickEvent(event: MouseEvent) {
33-
this.elementHasFocus = this.el.nativeElement.contains(event.target);
34-
}
35-
36-
@HostListener('document:keydown', ['$event'])
37-
private handleKeyboardEvent(event: KeyboardEvent) {
38-
if (!this.elementHasFocus) {
39-
return;
40-
}
41-
42-
if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
43-
this.msModel.toggleAll();
44-
45-
event.preventDefault();
46-
}
47-
}
19+
@Input() multiselectList: MultiselectModel<T>;
4820
}

apps/codelab/src/app/multiselect/multiselect-model.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,23 @@ describe('MultiselectModel', () => {
140140
});
141141
});
142142

143+
describe('getSelectedIndexes', () => {
144+
it('returns selected indexes', () => {
145+
model.toggleSingle(item1);
146+
147+
expect(model.getSelectedIndexes()).toEqual([0]);
148+
149+
model.clear();
150+
151+
expect(model.getSelectedIndexes()).toEqual([]);
152+
153+
model.toggleSingle('e');
154+
model.toggleContinuous('c');
155+
156+
expect(model.getSelectedIndexes()).toEqual([4, 2, 3]);
157+
});
158+
});
159+
143160
describe('emitChangeEvent', () => {
144161
it('should emit change event after selection', done => {
145162
const subscription = model.changed.subscribe(changes => {

apps/codelab/src/app/multiselect/multiselect-model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ export class MultiselectModel<T> {
7777
return this.selection.has(item);
7878
}
7979

80+
getSelectedIndexes(): number[] {
81+
return this.selected.map(item => this.items.indexOf(item));
82+
}
83+
8084
clear(): void {
8185
this.selection.clear();
8286
this.emitChangeEvent();

apps/codelab/src/app/multiselect/multiselect.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { MultiselectItemDirective } from './multiselect-item.directive';
1414

1515
@Component({
1616
template: `
17-
<div [msModel]="selectionModel" multiselectList>
18-
<div *ngFor="let item of items" [msItem]="item" multiselectItem>
17+
<div [multiselectList]="selectionModel">
18+
<div *ngFor="let item of items" [multiselectItem]="item">
1919
{{ item }}
2020
</div>
2121
</div>

apps/codelab/src/app/shared/helpers/helpers.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,24 @@ export function arrayMoveByIndex<T>(
394394
remainingValues.splice(normalizedToIndex, 0, ...moveValues);
395395
return remainingValues;
396396
}
397+
398+
export type KeyboardEventWithTarget<T> = KeyboardEvent & {
399+
target: T;
400+
};
401+
402+
export function isFromContext(
403+
target: HTMLElement,
404+
compareFn: (target: HTMLElement) => boolean
405+
): boolean {
406+
let parent = target;
407+
408+
while (parent) {
409+
if (compareFn(parent)) {
410+
return true;
411+
}
412+
413+
parent = parent.parentElement;
414+
}
415+
416+
return false;
417+
}

0 commit comments

Comments
 (0)