Skip to content
This repository was archived by the owner on Feb 6, 2024. It is now read-only.

Commit 4315304

Browse files
feat: "undo / redo" support for attribute changes (#1175)
* feat: undo redo attributes and deck transition animation * feat: input undo redo working together with custom undo redo * feat: undo and redo with content editable / input events * refactor: dedicated store for undo and redo * refactor: rename element innerHTML value used to be pushed to undo on changes * feat: set color and style undo redo * chore: prettier * feat: remove stack for inner html when about to style * feat: propagate undo redo element for saving purpose * fix: do not trigger update on undo redo color * refactor: move helper to utils * fix: use slotted element as reference (element can be a child) * feat: reset undo redo stack on leaving editor * fix: stack undo redo on element changes * feat: undo redo font family * feat: undo redo text scale * feat: undo redo letter spacing and align * feat: undo redo block * feat: redo undo border radius * feat: redo undo box shadow * feat: redo undo code color * feat: redo undo reset code color * feat: redo events throw in component for attributes too * refactor: duplicate code * feat: redo undo color * refactor: update multiple styles at once * refactor: update multiple properties and styles at once * fix: definition * feat: undo redo image style * feat: undo redo list * fix: reset undo redo store * feat: do not stack if same value
1 parent 0eac94d commit 4315304

File tree

23 files changed

+817
-244
lines changed

23 files changed

+817
-244
lines changed

studio/src/app/components/editor/actions/element/app-actions-element/app-actions-element.tsx

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {isSlide} from '@deckdeckgo/deck-utils';
66

77
import store from '../../../../../stores/busy.store';
88
import i18n from '../../../../../stores/i18n.store';
9+
import undoRedoStore from '../../../../../stores/undo-redo.store';
910

1011
import {ImageHelper} from '../../../../../helpers/editor/image.helper';
1112
import {ShapeHelper} from '../../../../../helpers/editor/shape.helper';
@@ -67,6 +68,26 @@ export class AppActionsElement {
6768

6869
@Event() private resetted: EventEmitter<void>;
6970

71+
private observeElementMutations = () => {
72+
if (undoRedoStore.state.elementInnerHTML === undefined) {
73+
return;
74+
}
75+
76+
if (undoRedoStore.state.elementInnerHTML === this.selectedElement.element.innerHTML) {
77+
return;
78+
}
79+
80+
if (undoRedoStore.state.undo === undefined) {
81+
undoRedoStore.state.undo = [];
82+
}
83+
84+
undoRedoStore.state.undo.push({type: 'input', target: this.selectedElement.element, data: {innerHTML: undoRedoStore.state.elementInnerHTML}});
85+
86+
undoRedoStore.state.elementInnerHTML = this.selectedElement.element.innerHTML;
87+
};
88+
89+
private observer: MutationObserver = new MutationObserver(this.observeElementMutations);
90+
7091
constructor() {
7192
this.debounceResizeSlideContent = debounce(async () => {
7293
await this.resizeSlideContent();
@@ -133,17 +154,27 @@ export class AppActionsElement {
133154
}
134155

135156
@Method()
136-
touch(element: HTMLElement, autoOpen: boolean = true): Promise<void> {
137-
return new Promise<void>(async (resolve) => {
138-
await this.unSelect();
139-
await this.select(element, autoOpen);
157+
async touch(element: HTMLElement | undefined, autoOpen: boolean = true) {
158+
await this.unSelect();
159+
await this.select(element, autoOpen);
140160

141-
if (element) {
142-
element.focus();
143-
}
161+
if (!element) {
162+
return;
163+
}
144164

145-
resolve();
146-
});
165+
element.focus();
166+
167+
if (this.selectedElement?.type === 'element') {
168+
this.observer.takeRecords();
169+
this.observer.observe(this.selectedElement.element, {attributes: true, childList: true, subtree: true});
170+
171+
undoRedoStore.state.elementInnerHTML = this.selectedElement.element.innerHTML;
172+
return;
173+
}
174+
175+
this.observer.disconnect();
176+
177+
undoRedoStore.state.elementInnerHTML = undefined;
147178
}
148179

149180
@Method()

studio/src/app/components/editor/actions/footer/app-actions-editor/app-actions-editor.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {Component, Element, Event, Watch, EventEmitter, Fragment, h, Host, JSX,
33
import {isSlide} from '@deckdeckgo/deck-utils';
44

55
import editorStore from '../../../../../stores/editor.store';
6+
import undoRedoStore from '../../../../../stores/undo-redo.store';
67

78
import {BreadcrumbsStep} from '../../../../../types/editor/breadcrumbs-step';
89

@@ -57,6 +58,27 @@ export class AppActionsEditor {
5758

5859
private actionsElementRef!: HTMLAppActionsElementElement;
5960

61+
private destroyUndoRedoListener;
62+
63+
componentDidLoad() {
64+
this.destroyUndoRedoListener = undoRedoStore.onChange('elementInnerHTML', (elementInnerHTML: string | undefined) => {
65+
if (elementInnerHTML === undefined) {
66+
this.el.removeEventListener('click', this.resetElementInnerHTML, false);
67+
return;
68+
}
69+
70+
this.el.addEventListener('click', this.resetElementInnerHTML, {once: true});
71+
});
72+
}
73+
74+
disconnectedCallback() {
75+
this.destroyUndoRedoListener?.();
76+
}
77+
78+
private resetElementInnerHTML = () => {
79+
undoRedoStore.state.elementInnerHTML = undefined;
80+
};
81+
6082
@Watch('fullscreen')
6183
onFullscreenChange() {
6284
this.hideBottomSheet = true;

studio/src/app/components/editor/styles/app-color-text-background/app-color-text-background.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import i18n from '../../../../stores/i18n.store';
55

66
import {ColorUtils, InitStyleColor} from '../../../../utils/editor/color.utils';
77
import {SettingsUtils} from '../../../../utils/core/settings.utils';
8+
import {setStyle} from '../../../../utils/editor/undo-redo.utils';
89

910
import {Expanded} from '../../../../types/core/settings';
1011

@@ -26,6 +27,8 @@ export class AppColorTextBackground {
2627
@Prop()
2728
colorType: 'text' | 'background' = 'text';
2829

30+
private colorRef!: HTMLAppColorElement;
31+
2932
@Event() colorChange: EventEmitter<void>;
3033

3134
private initBackground = async (): Promise<InitStyleColor> => {
@@ -85,11 +88,11 @@ export class AppColorTextBackground {
8588
return;
8689
}
8790

88-
if (this.deck || this.slide) {
89-
this.selectedElement.style.setProperty('--color', selectedColor);
90-
} else {
91-
this.selectedElement.style.color = selectedColor;
92-
}
91+
setStyle(this.selectedElement, {
92+
properties: [{property: this.deck || this.slide ? '--color' : 'color', value: selectedColor}],
93+
type: this.deck ? 'deck' : this.slide ? 'slide' : 'element',
94+
updateUI: async (_value: string) => await this.colorRef.loadColor(),
95+
});
9396

9497
this.colorChange.emit();
9598
}
@@ -99,11 +102,11 @@ export class AppColorTextBackground {
99102
return;
100103
}
101104

102-
if (this.deck || this.slide) {
103-
this.selectedElement.style.setProperty('--background', selectedColor);
104-
} else {
105-
this.selectedElement.style.background = selectedColor;
106-
}
105+
setStyle(this.selectedElement, {
106+
properties: [{value: selectedColor, property: this.deck || this.slide ? '--background' : 'background'}],
107+
type: this.deck ? 'deck' : this.slide ? 'slide' : 'element',
108+
updateUI: async (_value: string) => await this.colorRef.loadColor(),
109+
});
107110

108111
this.colorChange.emit();
109112
}
@@ -116,6 +119,7 @@ export class AppColorTextBackground {
116119
<ion-label slot="title">{i18n.state.editor.color}</ion-label>
117120

118121
<app-color
122+
ref={(el) => (this.colorRef = el as HTMLAppColorElement)}
119123
initColor={this.colorType === 'background' ? this.initBackground : this.initColor}
120124
onResetColor={() => this.resetColor()}
121125
defaultColor={this.colorType === 'background' ? '#fff' : '#000'}

studio/src/app/components/editor/styles/app-color/app-color.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export class AppColor {
3333
@State()
3434
private colorCSS: string;
3535

36+
private skipNextColorCSSEmit: boolean = false;
37+
3638
@Event()
3739
colorDidChange: EventEmitter<string>;
3840

@@ -79,6 +81,8 @@ export class AppColor {
7981

8082
this.opacity = opacity ? opacity : 100;
8183

84+
this.skipNextColorCSSEmit = true;
85+
8286
await this.initColorCSS();
8387
}
8488

@@ -127,6 +131,8 @@ export class AppColor {
127131

128132
$event.stopPropagation();
129133

134+
this.skipNextColorCSSEmit = true;
135+
130136
await this.initColorStateRgb(color.rgb);
131137
await this.initColorCSS();
132138

@@ -218,6 +224,11 @@ export class AppColor {
218224
}
219225

220226
private async updateColorCSS() {
227+
if (this.skipNextColorCSSEmit) {
228+
this.skipNextColorCSSEmit = false;
229+
return;
230+
}
231+
221232
this.colorDidChange.emit(this.colorCSS);
222233
}
223234

studio/src/app/components/editor/styles/deck/app-deck-fonts/app-deck-fonts.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import i18n from '../../../../../stores/i18n.store';
44

55
import {FontsService} from '../../../../../services/editor/fonts/fonts.service';
66

7+
import {setStyle} from '../../../../../utils/editor/undo-redo.utils';
8+
79
@Component({
810
tag: 'app-deck-fonts',
911
styleUrl: 'app-deck-fonts.scss',
@@ -51,15 +53,13 @@ export class AppDeckFonts {
5153
return;
5254
}
5355

54-
if (!font) {
55-
this.deckElement.style.removeProperty('font-family');
56-
} else {
57-
this.deckElement.style.setProperty('font-family', font.family);
58-
}
56+
setStyle(this.deckElement, {
57+
properties: [{value: !font ? null : font.family, property: 'font-family'}],
58+
type: 'deck',
59+
updateUI: async () => await this.initSelectedFont(),
60+
});
5961

6062
this.fontsChange.emit();
61-
62-
await this.initSelectedFont();
6363
}
6464

6565
render() {

studio/src/app/components/editor/styles/deck/app-deck-transition/app-deck-transition.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import i18n from '../../../../../stores/i18n.store';
55

66
import {DeckAction} from '../../../../../types/editor/deck-action';
77

8+
import {setAttribute} from '../../../../../utils/editor/undo-redo.utils';
9+
810
@Component({
911
tag: 'app-deck-transition',
1012
styleUrl: 'app-deck-transition.scss',
@@ -141,9 +143,16 @@ export class AppDeckTransition {
141143

142144
await this.goToFirstSlide();
143145

144-
this.deckElement.setAttribute(this.device === 'mobile' ? 'direction-mobile' : 'direction', direction);
146+
const resetDevice: 'desktop' | 'mobile' = this.device;
145147

146-
this.selectedDirection = direction;
148+
setAttribute(this.deckElement, {
149+
attribute: this.device === 'mobile' ? 'direction-mobile' : 'direction',
150+
value: direction,
151+
updateUI: (value: string) => {
152+
this.device = resetDevice;
153+
this.selectedDirection = value as 'horizontal' | 'vertical' | 'papyrus';
154+
},
155+
});
147156

148157
this.transitionChange.emit();
149158

@@ -171,9 +180,11 @@ export class AppDeckTransition {
171180
return;
172181
}
173182

174-
this.deckElement.setAttribute('animation', animation);
175-
176-
this.selectedAnimation = animation;
183+
setAttribute(this.deckElement, {
184+
attribute: 'animation',
185+
value: animation,
186+
updateUI: (value: string) => (this.selectedAnimation = value as 'slide' | 'fade' | 'none'),
187+
});
177188

178189
this.transitionChange.emit();
179190
}

0 commit comments

Comments
 (0)