Skip to content

Commit 5dd422b

Browse files
authored
[FEATURE] add FormEngine-style unsaved changes dialog (#69)
Show a dedicated "Unsaved changes" modal before the visual editor refreshes, changes the URL, or closes with pending edits. The dialog mirrors the FormEngine flow with three actions so editors can discard changes, stay on the page, or save before leaving.
1 parent d0221e7 commit 5dd422b

4 files changed

Lines changed: 94 additions & 16 deletions

File tree

Classes/Service/EditModeService.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use TYPO3\CMS\Core\Site\SiteFinder;
2323
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
2424
use TYPO3\CMS\Frontend\Page\PageInformation;
25-
use TYPO3\CMS\VisualEditor\Service\LocalizationService;
2625

2726
use function method_exists;
2827

@@ -146,7 +145,7 @@ public function init(ServerRequestInterface $request): void
146145
],
147146
[
148147
'useNonce' => true,
149-
]
148+
],
150149
);
151150
}
152151
}
@@ -227,15 +226,21 @@ private function isBeUser(): bool
227226

228227
private function loadLanguageLabelsInline(): void
229228
{
230-
$file = 'EXT:visual_editor/Resources/Private/Language/locallang.xlf';
231-
$languageService = $this->languageServiceFactory->create($this->localizationService->getBackendUserLanguage() ?? 'en');
232-
foreach ($languageService->getLabelsFromResource($file) as $key => $value) {
233-
$this->pageRenderer->addInlineLanguageLabel($key, $value);
229+
$files = [
230+
'EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf',
231+
'EXT:visual_editor/Resources/Private/Language/locallang.xlf',
232+
];
233+
foreach ($files as $file) {
234+
$languageService = $this->languageServiceFactory->create($this->localizationService->getBackendUserLanguage() ?? 'en');
235+
foreach ($languageService->getLabelsFromResource($file) as $key => $value) {
236+
$this->pageRenderer->addInlineLanguageLabel($key, $value);
237+
}
234238
}
235239
}
236240

237241
/**
238242
* returns the origins of all configured sites and languages
243+
*
239244
* @return list<string>
240245
*/
241246
public function getAllowedOrigins(): array

Resources/Public/JavaScript/Frontend/initialize-save-handling.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {onMessage, sendMessage} from '@typo3/visual-editor/Shared/iframe-messaging';
22
import {useDataHandler} from '@typo3/visual-editor/Frontend/use-data-handler';
33
import {dataHandlerStore} from '@typo3/visual-editor/Frontend/stores/data-handler-store';
4+
import {InterceptUserActionsGuard} from '@typo3/visual-editor/Frontend/intercept-user-actions-guard';
45

56
let saving = false;
67

@@ -58,4 +59,6 @@ export function initializeSaveHandling() {
5859
onMessage('doSave', () => {
5960
trySave();
6061
});
62+
63+
new InterceptUserActionsGuard(dataHandlerStore);
6164
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {lll} from "@typo3/core/lit-helper.js";
2+
import Modal from '@typo3/backend/modal.js';
3+
import Severity from '@typo3/backend/severity.js';
4+
import {trySave} from '@typo3/visual-editor/Frontend/initialize-save-handling';
5+
6+
7+
export class InterceptUserActionsGuard {
8+
constructor(dataHandlerStore) {
9+
this.dataHandlerStore = dataHandlerStore;
10+
11+
window.addEventListener('beforeunload', (event) => {
12+
if (this.dataHandlerStore.changesCount) {
13+
event.preventDefault();
14+
}
15+
});
16+
17+
top.TYPO3.Backend.consumerScope.attach(this);
18+
window.addEventListener('pagehide', () => top.TYPO3.Backend.consumerScope.detach(this), {once: true});
19+
}
20+
21+
/**
22+
* @param interactionRequest {{ concernsTypes(possible: string[]): any }}
23+
* @return {Promise<void>}
24+
*/
25+
async consume(interactionRequest) {
26+
if (!interactionRequest.concernsTypes(['typo3.setUrl', 'typo3.beforeSetUrl', 'typo3.refresh'])) {
27+
return;
28+
}
29+
30+
const hasChanges = this.dataHandlerStore.changesCount > 0;
31+
if (!hasChanges) {
32+
return;
33+
}
34+
35+
return new Promise((resolve, reject) => {
36+
const buttons = [
37+
{
38+
text: lll('buttons.confirm.close_without_save.no'),
39+
btnClass: 'btn-default',
40+
trigger: (_e, modal) => {
41+
modal.hideModal();
42+
reject();
43+
},
44+
},
45+
{
46+
text: lll('buttons.confirm.close_without_save.yes'),
47+
btnClass: 'btn-warning',
48+
trigger: (_e, modal) => {
49+
modal.hideModal();
50+
this.dataHandlerStore.reset();
51+
resolve();
52+
},
53+
},
54+
];
55+
56+
const noErrors = this.dataHandlerStore.invalidCount === 0;
57+
if (noErrors) {
58+
buttons.push({
59+
text: lll('buttons.confirm.save_and_close'),
60+
btnClass: 'btn-primary',
61+
trigger: async (_e, modal) => {
62+
modal.hideModal();
63+
await trySave();
64+
resolve();
65+
},
66+
});
67+
}
68+
69+
Modal.confirm(
70+
lll('label.confirm.close_without_save.title'),
71+
lll('label.confirm.close_without_save.content'),
72+
Severity.warning,
73+
buttons,
74+
);
75+
});
76+
}
77+
}

Resources/Public/JavaScript/Frontend/stores/data-handler-store.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ class DataHandlerStore extends EventTarget {
1010
#cmdArray = [];
1111
#invalidFields = {};
1212

13-
constructor() {
14-
super();
15-
16-
window.addEventListener('beforeunload', (event) => {
17-
if (this.changesCount) {
18-
event.preventDefault();
19-
}
20-
});
21-
}
22-
2313
get data() {
2414
return structuredClone(this.#data);
2515
}
@@ -127,7 +117,10 @@ class DataHandlerStore extends EventTarget {
127117
}
128118
}
129119
}
120+
this.reset();
121+
}
130122

123+
reset() {
131124
this.#data = {};
132125
this.#cmdArray = [];
133126
this.#invalidFields = {};

0 commit comments

Comments
 (0)