Skip to content

Commit df18403

Browse files
committed
feature(wysiwyg): multiselect drag and drop reorder + delete slides
1 parent 10cc2f5 commit df18403

File tree

9 files changed

+313
-37
lines changed

9 files changed

+313
-37
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export class ContentComponent {
2323
})
2424
);
2525

26-
currentSlideIndex$ = this.contentService.currentSlideIndex$;
26+
currentSlideIndex$ = this.navigationService.currentSlideIndex$;
2727

2828
currentSlide$ = combineLatest([
29-
this.navigationService.selectedSlide$,
29+
this.navigationService.currentSlideIndex$,
3030
this.presentation$
3131
]).pipe(
3232
map(([slide, presentation]) => {

apps/codelab/src/app/admin/content/presentation-editor/reducer.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ export function reducer(
3434
case 'deleteSlide':
3535
getPresentation().slides.filter(({ id }) => id !== payload.id);
3636
return presentations;
37+
case 'deleteSlides':
38+
getPresentation().slides = getPresentation().slides.filter((slide, index) => !payload.selections.includes(index));
39+
40+
return presentations;
41+
case 'reorderSlides':
42+
const toIndex = payload.toIndex;
43+
const selections = payload.selections;
44+
45+
const slides = getPresentation().slides;
46+
47+
const normalizeIndexes = (indexes, array) =>
48+
indexes.filter(index => index >= 0 && index < array.length);
49+
50+
const normalizeToIndex = (toIndex, array, moveIndexes) =>
51+
Math.min(Math.max(0, toIndex), array.length - moveIndexes.length);
52+
53+
const arrayMoveByIndex = (array, index, toIndex) => {
54+
const indexes = Array.isArray(index) ? index : [index];
55+
const normalizedIndexes = normalizeIndexes(indexes, array);
56+
const normalizedToIndex = normalizeToIndex(toIndex, array, indexes);
57+
58+
const moveValues = normalizedIndexes.map(moveIndex => array[moveIndex]);
59+
60+
const dontMoveValues = array.filter(
61+
(item, index) => normalizedIndexes.indexOf(index) === -1,
62+
);
63+
64+
dontMoveValues.splice(normalizedToIndex, 0, ...moveValues);
65+
return dontMoveValues;
66+
};
67+
68+
getPresentation().slides = [...arrayMoveByIndex(slides, selections, toIndex)];
69+
70+
return presentations;
3771

3872
case 'addBlock': {
3973
const slide = getSlide();

apps/codelab/src/app/admin/content/presentation-editor/services/content.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,29 @@ export class ContentService implements OnDestroy {
136136
this.dispatch(presentationId, action);
137137
}
138138

139+
deleteSlides(presentationId: string, selections: number[]) {
140+
const action = {
141+
type: 'deleteSlides',
142+
payload: {
143+
selections
144+
}
145+
};
146+
147+
this.dispatch(presentationId, action);
148+
}
149+
150+
reorderSlides(presentationId: string, selections: number[], toIndex: number) {
151+
const action = {
152+
type: 'reorderSlides',
153+
payload: {
154+
selections,
155+
toIndex
156+
}
157+
};
158+
159+
this.dispatch(presentationId, action);
160+
}
161+
139162
addBlock(presentationId: string, slideId: string, block: ContentBlock) {
140163
const action = {
141164
type: 'addBlock',

apps/codelab/src/app/admin/content/presentation-editor/services/navigation.service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { take } from 'rxjs/operators';
88
providedIn: 'root'
99
})
1010
export class NavigationService {
11-
private readonly selectedSlideSubject = new BehaviorSubject(0);
11+
private readonly currentSlideIndexSubject = new BehaviorSubject(0);
1212
private readonly selectedPresentationIdSubject = new BehaviorSubject<
1313
string | undefined
1414
>(undefined);
15-
public selectedSlide$ = this.selectedSlideSubject.asObservable();
15+
public currentSlideIndex$ = this.currentSlideIndexSubject.asObservable();
1616
public selectedPresentationId$ = this.selectedPresentationIdSubject.asObservable();
1717

1818
constructor(
@@ -24,7 +24,7 @@ export class NavigationService {
2424
const params =
2525
router.routerState.snapshot.root.firstChild.firstChild.firstChild.params;
2626
this.selectedPresentationIdSubject.next(params.presentation);
27-
this.selectedSlideSubject.next(params.slide);
27+
this.currentSlideIndexSubject.next(Number(params.slide));
2828
}
2929

3030
goToPresentation(presentationId: string) {
@@ -34,18 +34,18 @@ export class NavigationService {
3434

3535
goToSlide(presentationId: string, index: number) {
3636
if (index >= 0) {
37-
this.selectedSlideSubject.next(index);
37+
this.currentSlideIndexSubject.next(index);
3838
this.location.replaceState(
3939
'admin/content/' + presentationId + '/' + index
4040
);
4141
}
4242
}
4343

4444
nextSlide(presentationId: string) {
45-
this.goToSlide(presentationId, this.selectedSlideSubject.value + 1);
45+
this.goToSlide(presentationId, this.currentSlideIndexSubject.value + 1);
4646
}
4747

4848
previousSlide(presentationId: string) {
49-
this.goToSlide(presentationId, this.selectedSlideSubject.value - 1);
49+
this.goToSlide(presentationId, this.currentSlideIndexSubject.value - 1);
5050
}
5151
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Injectable } from '@angular/core';
2+
3+
import { BehaviorSubject } from 'rxjs';
4+
5+
export function isCtrlEvent(event: MouseEvent) {
6+
return event.metaKey || event.ctrlKey;
7+
}
8+
9+
export function isShiftEvent(event: MouseEvent) {
10+
return event.shiftKey;
11+
}
12+
13+
@Injectable()
14+
export class MultiSelectionService {
15+
16+
public selections$ = new BehaviorSubject<number[]>([]);
17+
public selections: number[] = [];
18+
19+
public lastSingleSelection: number | null = null;
20+
21+
constructor() {
22+
}
23+
24+
isAlreadySelected(index: number): boolean {
25+
return this.selections.indexOf(index) >= 0;
26+
}
27+
28+
normalizeSelections(indexes: number[]): number[] {
29+
return indexes.sort((a, b) => a - b);
30+
}
31+
32+
isSelectionEmpty(): boolean {
33+
return this.selections.length < 1;
34+
}
35+
36+
setSelections(indexes: number[]) {
37+
this.selections = this.normalizeSelections(indexes);
38+
this.selections$.next(this.selections);
39+
}
40+
41+
addToSelections(index: number) {
42+
if (!this.isAlreadySelected(index)) {
43+
this.setSelections([...this.selections, index]);
44+
}
45+
}
46+
47+
select(event: MouseEvent, selectedIndex: number) {
48+
const shiftSelect = isShiftEvent(event) &&
49+
(this.lastSingleSelection || this.lastSingleSelection === 0) &&
50+
this.lastSingleSelection !== selectedIndex;
51+
52+
if (this.isSelectionEmpty()) {
53+
54+
this.setSelections([selectedIndex]);
55+
this.lastSingleSelection = selectedIndex;
56+
57+
} else if (isCtrlEvent(event)) {
58+
59+
const alreadySelected = this.isAlreadySelected(selectedIndex);
60+
61+
if (alreadySelected) {
62+
const selectionsWithoutSelectedIndex = this.selections.filter((index) => index !== selectedIndex);
63+
64+
if (selectionsWithoutSelectedIndex.length > 0) {
65+
this.setSelections(selectionsWithoutSelectedIndex);
66+
} else {
67+
this.setSelections([selectedIndex]);
68+
this.lastSingleSelection = selectedIndex;
69+
}
70+
} else {
71+
this.setSelections([...this.selections, selectedIndex]);
72+
this.lastSingleSelection = selectedIndex;
73+
}
74+
75+
} else if (shiftSelect) {
76+
77+
const newSelectionBefore = selectedIndex < this.lastSingleSelection;
78+
const count = (
79+
newSelectionBefore
80+
? this.lastSingleSelection - (selectedIndex - 1)
81+
: (selectedIndex + 1) - this.lastSingleSelection
82+
);
83+
84+
const shiftSelection = new Array(count)
85+
.fill(0)
86+
.map((_, index) => newSelectionBefore
87+
? this.lastSingleSelection - index
88+
: this.lastSingleSelection + index
89+
);
90+
91+
this.setSelections([...shiftSelection]);
92+
93+
} else {
94+
this.resetSelection(selectedIndex);
95+
}
96+
}
97+
98+
resetSelection(currentIndex: number) {
99+
this.lastSingleSelection = currentIndex;
100+
101+
this.setSelections([currentIndex]);
102+
}
103+
104+
selectAll(indexes: number[]) {
105+
this.setSelections(indexes);
106+
}
107+
108+
}
Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
<div class="slides-wrapper">
22
<button (click)="addSlide()">Add</button>
33

4-
<div (cdkDropListDropped)="reorder.emit($event)" cdkDropList>
5-
<button
6-
inert
7-
(click)="this.navigationService.goToSlide(presentationId, i)"
8-
*ngFor="let slide of slides; let i = index"
9-
[class.selected]="i === currentSlideIndex"
4+
<div (cdkDropListDropped)="droppedIntoList($event)" [class.dragging]="dragging" cdkDropList>
5+
<div
6+
(cdkDragDropped)="dropped()"
7+
(cdkDragEnded)="dragEnded()"
8+
(cdkDragStarted)="dragStarted($event)"
9+
(click)="select($event, index)"
10+
*ngFor="let slide of slides; let index = index; trackBy: trackBySlideId;"
11+
[class.selected]="multiSelectionService.selections.includes(index)"
12+
[class.current]="index == currentSlideIndex"
13+
cdkDragLockAxis="y"
14+
cdkDrag
1015
class="slide-wrapper"
16+
role="button"
1117
>
12-
<div cdkDrag class="slide">
18+
<div *cdkDragPreview>
19+
<div *ngFor="let sel of multiSelectionService.selections"></div>
20+
</div>
21+
<div *cdkDragPlaceholder>
22+
<div style="background: #00a4ff; height: 1px; width: 100%; margin: 2px 0"></div>
23+
</div>
24+
25+
<div class="slide">
1326
<slides-slide-preview
14-
[class.selected]="i === currentSlideIndex"
27+
inert
1528
[slide]="slide"
1629
mode="preview"
1730
></slides-slide-preview>
1831
</div>
19-
</button>
32+
</div>
2033
</div>
2134
</div>

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55
}
66

77
.slides-wrapper {
8-
overflow: scroll;
8+
overflow-y: scroll;
99
display: flex;
1010
flex-direction: column;
11-
width: 114px;
11+
width: 200px;
1212
height: 100%;
1313
padding: 0 4px;
1414
box-shadow: 1px 0 2px 0 #ddd;
1515
background: #f8f8f8;
1616
}
1717

18+
.slide-wrapper.current {
19+
border: 1px #ddd solid;
20+
box-shadow: 0 0 2px 1px #155ad0;
21+
outline: none;
22+
}
23+
1824
.slide-wrapper.selected {
1925
background: #dde;
2026
}
@@ -23,7 +29,7 @@
2329
display: flex;
2430
justify-content: space-around;
2531
width: 100%;
26-
min-height: 40px;
32+
min-height: 150px;
2733
padding: 4px 20px;
2834
box-sizing: border-box;
2935
border: 1px solid #ddd;
@@ -53,3 +59,9 @@
5359
zoom: 0.2;
5460
}
5561
}
62+
63+
.dragging {
64+
.slide-wrapper.selected:not(.cdk-drag-placeholder) {
65+
display: none;
66+
}
67+
}

0 commit comments

Comments
 (0)