typo3-typescript-sources 14.3.10 → 14.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.last-synced-tag +1 -1
  2. package/README.md +1 -1
  3. package/adminpanel/admin-panel.ts +2 -2
  4. package/backend/bookmark/bookmark-manager.ts +2 -2
  5. package/backend/clipboard-panel.ts +13 -7
  6. package/backend/code-editor/language/typoscript.ts +1 -1
  7. package/backend/column-selector-button.ts +2 -2
  8. package/backend/context-menu-actions.ts +23 -5
  9. package/backend/context-menu.ts +33 -2
  10. package/backend/contextual-record-edit.ts +114 -33
  11. package/backend/date-time-picker.ts +1 -1
  12. package/backend/drag-uploader.ts +49 -20
  13. package/backend/element/breadcrumb.ts +6 -6
  14. package/backend/element/combobox-element.ts +2 -2
  15. package/backend/element/contextual-record-edit-trigger.ts +97 -63
  16. package/backend/enum/viewport/scaffold-identifier.ts +1 -1
  17. package/backend/form-engine/container/flex-form-container-container.ts +1 -1
  18. package/backend/form-engine/container/flex-form-section-container.ts +3 -3
  19. package/backend/form-engine/container/inline-control-container.ts +29 -29
  20. package/backend/form-engine/element/abstract-sortable-select-items.ts +1 -1
  21. package/backend/form-engine/element/category-element.ts +5 -5
  22. package/backend/form-engine/element/group-element.ts +1 -1
  23. package/backend/form-engine/element/select-tree-element.ts +4 -4
  24. package/backend/form-engine/element/select-tree.ts +1 -1
  25. package/backend/form-engine/element/slug-element.ts +9 -7
  26. package/backend/form-engine/element/suggest/result-container.ts +1 -1
  27. package/backend/form-engine/element/suggest/result-item.ts +1 -1
  28. package/backend/form-engine/element/table-permission-element.ts +4 -4
  29. package/backend/form-engine/field-control/add-record.ts +1 -1
  30. package/backend/form-engine/field-control/edit-popup.ts +2 -2
  31. package/backend/form-engine/field-control/insert-clipboard.ts +1 -1
  32. package/backend/form-engine/field-control/link-popup.ts +1 -1
  33. package/backend/form-engine/field-control/list-module.ts +1 -1
  34. package/backend/form-engine/field-control/password-generator.ts +4 -4
  35. package/backend/form-engine/field-control/reset-selection.ts +2 -2
  36. package/backend/form-engine/field-wizard/localization-state-selector.ts +2 -2
  37. package/backend/form-engine/field-wizard/value-picker.ts +1 -1
  38. package/backend/form-engine/field-wizard/value-slider.ts +2 -2
  39. package/backend/form-engine-review.ts +1 -1
  40. package/backend/form-engine-suggest.ts +2 -2
  41. package/backend/form-engine-validation.ts +55 -10
  42. package/backend/form-engine.ts +12 -11
  43. package/backend/global-event-handler.ts +1 -1
  44. package/backend/grid-editor.ts +83 -8
  45. package/backend/image-manipulation.ts +2 -2
  46. package/backend/layout-module/drag-drop.ts +15 -15
  47. package/backend/layout-module/page-layout.ts +7 -2
  48. package/backend/layout-module/paste.ts +3 -3
  49. package/backend/layout-module/toggle-hidden-element.ts +1 -1
  50. package/backend/live-search/live-search.ts +6 -6
  51. package/backend/localization/finisher/localization-submission-service.ts +1 -0
  52. package/backend/localization/localization-button.ts +3 -0
  53. package/backend/localization/localization-wizard.ts +3 -0
  54. package/backend/localization/steps/source-language-step.ts +1 -0
  55. package/backend/login/capslock-warning.ts +84 -0
  56. package/backend/login-refresh.ts +2 -2
  57. package/backend/login.ts +3 -2
  58. package/backend/mail-link-handler.ts +2 -2
  59. package/backend/modal.ts +27 -3
  60. package/backend/module-menu.ts +17 -8
  61. package/backend/module.ts +1 -1
  62. package/backend/multi-record-selection.ts +3 -3
  63. package/backend/multi-step-wizard.ts +2 -2
  64. package/backend/new-record-wizard.ts +1 -1
  65. package/backend/notification.ts +1 -1
  66. package/backend/page-wizard/steps/form-engine-step.ts +2 -2
  67. package/backend/popover.ts +1 -1
  68. package/backend/recordlist.ts +7 -7
  69. package/backend/security/element/csp-reports.ts +2 -2
  70. package/backend/settings/editor.ts +9 -9
  71. package/backend/settings/type/page.ts +2 -2
  72. package/backend/sortable-table.ts +3 -3
  73. package/backend/tab.ts +3 -3
  74. package/backend/telephone-link-handler.ts +1 -1
  75. package/backend/tests/drag-uploader-test.ts +287 -0
  76. package/backend/tests/form-engine/element/select-tree-test.ts +1 -1
  77. package/backend/tests/form-engine-validation-test.ts +75 -0
  78. package/backend/tests/grid-editor-test.ts +29 -0
  79. package/backend/tests/login/capslock-warning-test.ts +78 -0
  80. package/backend/tests/modal-test.ts +73 -0
  81. package/backend/tests/notification-test.ts +15 -15
  82. package/backend/tests/tree/tree-test.ts +68 -0
  83. package/backend/tests/utility/drag-drop-utility-test.ts +82 -0
  84. package/backend/toolbar/system-information-menu.ts +2 -2
  85. package/backend/tree/file-storage-browser.ts +2 -2
  86. package/backend/tree/page-browser.ts +2 -2
  87. package/backend/tree/page-position-select.ts +2 -2
  88. package/backend/tree/page-tree-element.ts +3 -2
  89. package/backend/tree/tree-module-state.ts +12 -0
  90. package/backend/tree/tree-toolbar.ts +1 -1
  91. package/backend/tree/tree.ts +24 -17
  92. package/backend/url-link-handler.ts +1 -1
  93. package/backend/user-pass-login.ts +0 -103
  94. package/backend/utility/collapse-state-search.ts +2 -2
  95. package/backend/utility/drag-drop-utility.ts +5 -9
  96. package/backend/viewport/content-container.ts +1 -1
  97. package/backend/viewport/content-navigation-toggle.ts +2 -2
  98. package/backend/viewport/content-navigation.ts +3 -3
  99. package/backend/viewport/navigation-container.ts +2 -2
  100. package/backend/wizard/move-content-element.ts +7 -7
  101. package/backend/wizard/move-page.ts +3 -3
  102. package/backend/wizard/steps/finisher-step.ts +1 -1
  103. package/belog/backend-log.ts +2 -2
  104. package/beuser/permissions.ts +4 -4
  105. package/core/java-script-item-handler.ts +12 -2
  106. package/core/java-script-item-processor.ts +52 -6
  107. package/core/tests/java-script-item-processor-test.ts +83 -0
  108. package/dashboard/dashboard.ts +1 -6
  109. package/extensionmanager/main.ts +1 -1
  110. package/extensionmanager/update.ts +1 -1
  111. package/extensionmanager/upload-form.ts +1 -1
  112. package/filelist/browse-files.ts +2 -2
  113. package/filelist/browse-folders.ts +1 -1
  114. package/filelist/file-list-actions.ts +1 -1
  115. package/filelist/file-list-dragdrop.ts +4 -4
  116. package/filelist/file-list.ts +5 -5
  117. package/filelist/linkbrowser-file-handler.ts +1 -1
  118. package/filelist/rename-file.ts +3 -3
  119. package/filelist/resource-creation.ts +1 -1
  120. package/form/backend/form-editor/core.ts +1 -1
  121. package/form/backend/form-editor/inspector-component.ts +4 -4
  122. package/form/backend/form-editor/stage-component.ts +10 -11
  123. package/form/backend/form-editor/view-model.ts +1 -1
  124. package/info/translation-status.ts +2 -2
  125. package/install/installer.ts +6 -6
  126. package/install/module/environment/folder-structure.ts +1 -1
  127. package/install/module/maintenance/clear-tables.ts +1 -1
  128. package/install/module/maintenance/clear-typo3temp-files.ts +1 -1
  129. package/install/module/maintenance/database-analyzer.ts +2 -2
  130. package/install/module/settings/local-configuration.ts +8 -8
  131. package/install/module/upgrade/upgrade-docs.ts +1 -1
  132. package/install/module/upgrade/upgrade-wizards.ts +8 -8
  133. package/install/router.ts +3 -3
  134. package/lowlevel/query-generator.ts +4 -4
  135. package/package.json +2 -2
  136. package/recycler/recycler.ts +17 -17
  137. package/redirects/event-handler.ts +64 -19
  138. package/redirects/short-url-generator.ts +2 -2
  139. package/redirects/tests/event-handler-test.ts +121 -0
  140. package/rte-ckeditor/ckeditor5.ts +2 -2
  141. package/rte-ckeditor/tests/ckeditor5-test.ts +1 -1
  142. package/scheduler/form-engine/element/timing-options-element.ts +2 -2
  143. package/scheduler/scheduler-add-group.ts +3 -3
  144. package/scheduler/scheduler-sortable-groups.ts +2 -2
  145. package/scheduler/scheduler.ts +1 -1
  146. package/styleguide/find-icons.ts +1 -1
  147. package/viewpage/main.ts +7 -7
  148. package/workspaces/backend.ts +25 -25
  149. package/workspaces/preview.ts +8 -8
  150. package/workspaces/workspace-state.ts +1 -1
package/.last-synced-tag CHANGED
@@ -1 +1 @@
1
- v14.3.10
1
+ v14.3.11
package/README.md CHANGED
@@ -23,7 +23,7 @@ npm install github:maikschneider/typo3-typescript-sources#main
23
23
 
24
24
  - **Original Repository:** [TYPO3/typo3](https://github.com/TYPO3/typo3)
25
25
  - **Source Path:** `Build/Sources/TypeScript/`
26
- - **Current TYPO3 Version:** `v14.3.10`
26
+ - **Current TYPO3 Version:** `v14.3.11`
27
27
 
28
28
  ## 🌿 Branches
29
29
 
@@ -36,7 +36,7 @@
36
36
  private readonly trigger: HTMLElement;
37
37
 
38
38
  constructor() {
39
- this.adminPanel = document.querySelector(AdminPanelSelectors.adminPanelRole) as HTMLFormElement;
39
+ this.adminPanel = document.querySelector<HTMLFormElement>(AdminPanelSelectors.adminPanelRole);
40
40
  this.modules = this.querySelectorAll(AdminPanelSelectors.moduleTriggerRole).map(
41
41
  (moduleTrigger) => {
42
42
  const moduleParent = moduleTrigger.closest(AdminPanelSelectors.moduleParentClass);
@@ -60,7 +60,7 @@
60
60
  new AdminPanelContentSetting(contentSettingElement, contentSettingTrigger);
61
61
  },
62
62
  );
63
- this.trigger = document.querySelector(AdminPanelSelectors.triggerRole) as HTMLElement;
63
+ this.trigger = document.querySelector<HTMLElement>(AdminPanelSelectors.triggerRole);
64
64
 
65
65
  this.initializeEvents();
66
66
  this.addBackdropListener();
@@ -1120,7 +1120,7 @@ export class BookmarkManagerContentElement extends LitElement {
1120
1120
 
1121
1121
  private async handleDropBookmark(draggedBookmark: Bookmark, target: Bookmark | BookmarkGroup | BookmarkGroupId, dropPosition?: 'before' | 'after'): Promise<void> {
1122
1122
  if (typeof target === 'object' && 'route' in target) {
1123
- const targetBookmark = target as Bookmark;
1123
+ const targetBookmark = target;
1124
1124
  if (draggedBookmark.id === targetBookmark.id) {
1125
1125
  return;
1126
1126
  }
@@ -1210,7 +1210,7 @@ export class BookmarkManagerButtonElement extends PseudoButtonLitElement {
1210
1210
  protected override buttonActivated(): void {
1211
1211
  // Create element in top frame to avoid adoptedStyleSheets cross-document issues
1212
1212
  const targetDocument = topDocument;
1213
- const managerElement = targetDocument.createElement('typo3-backend-bookmark-manager-content') as BookmarkManagerContentElement;
1213
+ const managerElement = targetDocument.createElement('typo3-backend-bookmark-manager-content');
1214
1214
  if (this.editId !== undefined) {
1215
1215
  managerElement.editId = this.editId;
1216
1216
  }
@@ -105,11 +105,17 @@ export class ClipboardPanel extends LitElement {
105
105
  const clipboardData: ClipboardData = resolvedBody.data;
106
106
  return html`
107
107
  <div class="panel panel-default" data-clipboard-panel>
108
- <div class="panel-heading">
108
+ <div class="panel-heading" id="clipboard-panel-heading">
109
109
  ${clipboardData.labels.clipboard}
110
110
  </div>
111
111
  <div class="table-fit">
112
- <table class="table">
112
+ <table class="table" aria-labelledby="clipboard-panel-heading">
113
+ <thead>
114
+ <tr>
115
+ <th scope="col" colspan="2" class="p-0 border-0"><span class="visually-hidden">${clipboardData.labels.element}</span></th>
116
+ <th scope="col" class="p-0 border-0"><span class="visually-hidden">${clipboardData.labels.control}</span></th>
117
+ </tr>
118
+ </thead>
113
119
  <tbody>
114
120
  ${clipboardData.tabs.map((tab): TemplateResult => this.renderTab(tab, clipboardData))}
115
121
  </tbody>
@@ -133,8 +139,8 @@ export class ClipboardPanel extends LitElement {
133
139
  private renderTab(tab: ClipboardTab, clipboardData: ClipboardData): TemplateResult {
134
140
  return html`
135
141
  <tr>
136
- <td colspan="2" class="nowrap">
137
- <button type="button" class="btn btn-link" aria-checked="${clipboardData.current === tab.identifier}" title="${tab.description}" data-action="setP" @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'setP': tab.identifier } })}">
142
+ <th scope="row" colspan="2" class="nowrap fw-normal">
143
+ <button type="button" class="btn btn-link" aria-current="${clipboardData.current === tab.identifier ? 'true' : nothing}" title="${tab.description}" data-action="setP" @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'setP': tab.identifier } })}">
138
144
  ${clipboardData.current === tab.identifier ? html`
139
145
  <typo3-backend-icon identifier="actions-check-circle-alt" size="small"></typo3-backend-icon>
140
146
  ${tab.title}
@@ -146,7 +152,7 @@ export class ClipboardPanel extends LitElement {
146
152
  </span>
147
153
  `}
148
154
  </button>
149
- </td>
155
+ </th>
150
156
  <td class="col-control nowrap">
151
157
  ${clipboardData.current !== tab.identifier ? nothing : html`
152
158
  <div class="btn-toolbar btn-toolbar-nowrap">
@@ -182,11 +188,11 @@ export class ClipboardPanel extends LitElement {
182
188
  <td class="col-icon nowrap ${classMap({ 'ps-4': !tabItem.identifier })}">
183
189
  ${unsafeHTML(tabItem.icon)}
184
190
  </td>
185
- <td class="col-title col-responsive nowrap">
191
+ <th scope="row" class="col-title col-responsive nowrap fw-normal">
186
192
  ${unsafeHTML(tabItem.title)}
187
193
  ${tabIdentifier === 'normal' ? html`<strong>(${clipboardData.copyMode === CopyMode.copy ? html`${clipboardData.labels.copy}` : html`${clipboardData.labels.cut}`})</strong>` : nothing}
188
194
  ${tabItem.thumb ? html`<div class="d-block">${unsafeHTML(tabItem.thumb)}</div>` : nothing}
189
- </td>
195
+ </th>
190
196
  <td class="col-control nowrap">
191
197
  <div class="btn-group">
192
198
  ${!tabItem.infoDataDispatch ? nothing : html`
@@ -47,7 +47,7 @@ export function typoscript() {
47
47
 
48
48
  const tsCodeCompletionInitializer = (async (): Promise<TsCodeCompletion> => {
49
49
  await DocumentService.ready();
50
- const effectivePid = parseInt((document.querySelector('input[name="effectivePid"]') as HTMLInputElement)?.value, 10);
50
+ const effectivePid = parseInt((document.querySelector<HTMLInputElement>('input[name="effectivePid"]'))?.value, 10);
51
51
  return new TsCodeCompletion(effectivePid);
52
52
  })();
53
53
 
@@ -168,7 +168,7 @@ export class ColumnSelectorButton extends PseudoButtonLitElement {
168
168
  }
169
169
 
170
170
  private processSelection(currentModal: HTMLElement): void {
171
- const form: HTMLFormElement = currentModal.querySelector('form') as HTMLFormElement;
171
+ const form = currentModal.querySelector('form');
172
172
  if (form === null) {
173
173
  this.abortSelection();
174
174
  return;
@@ -192,7 +192,7 @@ export class ColumnSelectorButton extends PseudoButtonLitElement {
192
192
  }
193
193
 
194
194
  private handleModalContentLoaded(currentModal: HTMLElement): void {
195
- const form: HTMLFormElement = currentModal.querySelector('form') as HTMLFormElement;
195
+ const form = currentModal.querySelector('form');
196
196
  if (form === null) {
197
197
  // Early return if modal content does not include a form
198
198
  return;
@@ -15,6 +15,8 @@ import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
15
15
  import { SeverityEnum } from './enum/severity';
16
16
  import AjaxDataHandler from './ajax-data-handler';
17
17
  import AjaxRequest from '@typo3/core/ajax/ajax-request';
18
+ import ClientRequest from './event/client-request';
19
+ import { isContextualRecordEditOpen, openContextualRecordEdit } from './element/contextual-record-edit-trigger';
18
20
  import InfoWindow from './info-window';
19
21
  import Modal, { type ModalElement } from './modal';
20
22
  import ModuleMenu from './module-menu';
@@ -52,13 +54,17 @@ class ContextMenuActions {
52
54
  overrideVals = '&overrideVals[pages][sys_language_uid]=' + pageLanguageId;
53
55
  }
54
56
 
55
- Viewport.ContentContainer.setUrl(
56
- topWindow.TYPO3.settings.FormEngine.moduleUrl
57
- + '&edit[' + table + '][' + uid + ']=edit'
57
+ const editParameters = '&edit[' + table + '][' + uid + ']=edit'
58
58
  + overrideVals
59
59
  + '&module=' + encodeURIComponent(topWindow.TYPO3.ModuleMenu.App.getCurrentModule())
60
- + '&returnUrl=' + ContextMenuActions.getReturnUrl(),
61
- );
60
+ + '&returnUrl=' + ContextMenuActions.getReturnUrl();
61
+
62
+ if (isContextualRecordEditOpen()) {
63
+ openContextualRecordEdit(topWindow.TYPO3.settings.FormEngine.contextualUrl + editParameters, table);
64
+ return;
65
+ }
66
+
67
+ Viewport.ContentContainer.setUrl(topWindow.TYPO3.settings.FormEngine.moduleUrl + editParameters);
62
68
  }
63
69
 
64
70
  public static viewRecord(table: string, uid: number, dataset: DOMStringMap): void {
@@ -197,12 +203,21 @@ class ContextMenuActions {
197
203
  }
198
204
  }
199
205
 
206
+ /**
207
+ * Marks a navigation that commits a record and returns to the current
208
+ * module, so it is not handled as leaving the current view.
209
+ */
210
+ public static createRecordCommitRequest(): ClientRequest {
211
+ return new ClientRequest('typo3.recordCommit');
212
+ }
213
+
200
214
  public static disableRecord(table: string, uid: number, dataset: DOMStringMap): void {
201
215
  const disableFieldName = dataset.disableField || 'hidden';
202
216
  Viewport.ContentContainer.setUrl(
203
217
  topWindow.TYPO3.settings.RecordCommit.moduleUrl
204
218
  + '&data[' + table + '][' + uid + '][' + disableFieldName + ']=1'
205
219
  + '&redirect=' + ContextMenuActions.getReturnUrl(),
220
+ ContextMenuActions.createRecordCommitRequest(),
206
221
  );
207
222
  }
208
223
 
@@ -212,6 +227,7 @@ class ContextMenuActions {
212
227
  topWindow.TYPO3.settings.RecordCommit.moduleUrl
213
228
  + '&data[' + table + '][' + uid + '][' + disableFieldName + ']=0'
214
229
  + '&redirect=' + ContextMenuActions.getReturnUrl(),
230
+ ContextMenuActions.createRecordCommitRequest(),
215
231
  );
216
232
  }
217
233
 
@@ -220,6 +236,7 @@ class ContextMenuActions {
220
236
  topWindow.TYPO3.settings.RecordCommit.moduleUrl
221
237
  + '&data[' + table + '][' + uid + '][nav_hide]=0'
222
238
  + '&redirect=' + ContextMenuActions.getReturnUrl(),
239
+ ContextMenuActions.createRecordCommitRequest(),
223
240
  );
224
241
  }
225
242
 
@@ -228,6 +245,7 @@ class ContextMenuActions {
228
245
  topWindow.TYPO3.settings.RecordCommit.moduleUrl
229
246
  + '&data[' + table + '][' + uid + '][nav_hide]=1'
230
247
  + '&redirect=' + ContextMenuActions.getReturnUrl(),
248
+ ContextMenuActions.createRecordCommitRequest(),
231
249
  );
232
250
  }
233
251
 
@@ -155,6 +155,9 @@ export class ContextMenuElement extends LitElement {
155
155
  @queryAll('.context-menu') contextMenuElements: HTMLDivElement[];
156
156
  @queryAll('.context-menu-item') contextMenuItemElements: HTMLButtonElement[];
157
157
  private readonly rootIdentifier: string = 'root';
158
+ // A modal <dialog> makes everything outside of it inert, so the menu is rendered
159
+ // into a popover that is mounted into the topmost open dialog while it is shown.
160
+ private readonly popoverHost: HTMLDivElement = document.createElement('div');
158
161
 
159
162
  private focusFirstElement: boolean = false;
160
163
 
@@ -211,6 +214,8 @@ export class ContextMenuElement extends LitElement {
211
214
  window.addEventListener('resize', this.hide);
212
215
  document.addEventListener(ContextMenuEvent.open, this.show);
213
216
  document.addEventListener(ContextMenuEvent.close, this.hide);
217
+ document.addEventListener('typo3-modal-show', this.hide);
218
+ document.addEventListener('typo3-modal-hide', this.hide);
214
219
  }
215
220
 
216
221
  public override disconnectedCallback(): void {
@@ -218,6 +223,8 @@ export class ContextMenuElement extends LitElement {
218
223
  window.removeEventListener('resize', this.hide);
219
224
  document.removeEventListener(ContextMenuEvent.open, this.show);
220
225
  document.removeEventListener(ContextMenuEvent.close, this.hide);
226
+ document.removeEventListener('typo3-modal-show', this.hide);
227
+ document.removeEventListener('typo3-modal-hide', this.hide);
221
228
  }
222
229
 
223
230
  protected override updated(): void {
@@ -232,7 +239,10 @@ export class ContextMenuElement extends LitElement {
232
239
  }
233
240
 
234
241
  protected override createRenderRoot(): HTMLElement | ShadowRoot {
235
- return this;
242
+ this.popoverHost.classList.add('context-menu-host');
243
+ this.popoverHost.setAttribute('popover', 'manual');
244
+ this.append(this.popoverHost);
245
+ return this.popoverHost;
236
246
  }
237
247
 
238
248
  protected override render() {
@@ -265,6 +275,7 @@ export class ContextMenuElement extends LitElement {
265
275
  this.uid = detail.uid;
266
276
  this.context = detail.context;
267
277
  this.eventSource = detail.eventSource;
278
+ this.mountPopoverHost();
268
279
 
269
280
  if (detail.originalEvent !== null) {
270
281
  const offset = this.calculateIframeOffset(detail.originalEvent.view, window);
@@ -295,11 +306,31 @@ export class ContextMenuElement extends LitElement {
295
306
  return;
296
307
  }
297
308
  this.open = false;
309
+ this.unmountPopoverHost();
298
310
  this.fetchTask.run();
299
311
  await this.updateComplete;
300
312
  this.eventSource?.focus();
301
313
  };
302
314
 
315
+ private mountPopoverHost(): void {
316
+ const openDialogs = document.querySelectorAll<HTMLDialogElement>('typo3-backend-modal > dialog[open]');
317
+ const target: HTMLElement = openDialogs.item(openDialogs.length - 1) ?? this;
318
+ if (this.popoverHost.matches(':popover-open')) {
319
+ this.popoverHost.hidePopover();
320
+ }
321
+ if (this.popoverHost.parentElement !== target) {
322
+ target.append(this.popoverHost);
323
+ }
324
+ this.popoverHost.showPopover();
325
+ }
326
+
327
+ private unmountPopoverHost(): void {
328
+ if (this.popoverHost.matches(':popover-open')) {
329
+ this.popoverHost.hidePopover();
330
+ }
331
+ this.append(this.popoverHost);
332
+ }
333
+
303
334
  private renderOverlay(): TemplateResult {
304
335
  return html`
305
336
  <div
@@ -594,7 +625,7 @@ export class ContextMenuElement extends LitElement {
594
625
  }
595
626
 
596
627
  private getElementFromNode(node: MenuItemInterface): HTMLElement | null {
597
- return this.querySelector('[data-contextmenu-id="' + this.getNodeIdentifier(node) + '"]');
628
+ return this.renderRoot.querySelector<HTMLElement>('[data-contextmenu-id="' + this.getNodeIdentifier(node) + '"]');
598
629
  }
599
630
 
600
631
  private enhanceNodes(parentIdentifier: string, nodes: RawMenuItemInterface[]): MenuItemInterface[] {
@@ -12,12 +12,15 @@
12
12
  */
13
13
 
14
14
  import FormEngine from '@typo3/backend/form-engine';
15
- import Modal from '@typo3/backend/modal';
15
+ import type { Consumable } from '@typo3/backend/event/consumable';
16
+ import type TriggerRequest from '@typo3/backend/event/trigger-request';
17
+ import Modal, { type ModalElement } from '@typo3/backend/modal';
16
18
  import Severity from '@typo3/backend/severity';
17
19
  import DocumentService from '@typo3/core/document-service';
18
- import { topWindow } from '@typo3/core/utility/top-window';
20
+ import { topDocument, topWindow } from '@typo3/core/utility/top-window';
19
21
  import '@typo3/backend/element/icon-element';
20
22
  import labels from '~labels/backend.alt_doc';
23
+ import coreCommonLabels from '~labels/core.common';
21
24
 
22
25
  type ContextualRecordEditOptions = {
23
26
  justSaved?: boolean;
@@ -38,6 +41,8 @@ class ContextualRecordEdit {
38
41
  private closeButton: HTMLButtonElement | null = null;
39
42
  private fullscreenButton: HTMLAnchorElement | null = null;
40
43
  private readonly options: ContextualRecordEditOptions;
44
+ // "<table>:<uid>" of the edited record
45
+ private readonly record: string = ContextualRecordEdit.resolveRecord();
41
46
 
42
47
  constructor(options: ContextualRecordEditOptions = {}) {
43
48
  this.options = options;
@@ -48,11 +53,22 @@ class ContextualRecordEdit {
48
53
  });
49
54
  }
50
55
 
56
+ private static resolveRecord(): string {
57
+ for (const key of new URL(window.location.href).searchParams.keys()) {
58
+ const match = key.match(/^edit\[([^\]]+)]\[(\d+)]$/);
59
+ if (match !== null) {
60
+ return match[1] + ':' + match[2];
61
+ }
62
+ }
63
+ return '';
64
+ }
65
+
51
66
  private initialize(): void {
52
67
  this.initializeCloseButton();
53
68
  this.initializeFullscreenButton();
54
69
  this.initializeEscapeKey();
55
70
  this.initializeParentMessages();
71
+ this.initializeNavigationGuard();
56
72
  this.handlePostSaveState();
57
73
  if (!this.options.justSaved) {
58
74
  this.focusFirstElement();
@@ -74,42 +90,101 @@ class ContextualRecordEdit {
74
90
  }
75
91
  this.fullscreenButton.addEventListener('click', (e: Event): void => {
76
92
  e.preventDefault();
77
- const navigate = (): void => {
78
- // Clear FormEngine's dirty tracking so it doesn't show its own confirmation after ours
79
- document.querySelectorAll('.has-change').forEach((el) => el.classList.remove('has-change'));
80
- // Navigate the content frame directly (same-origin access) — using
81
- // postMessage for this failed silently in Firefox.
93
+ // Navigate the content frame directly (same-origin access), using
94
+ // postMessage for this failed silently in Firefox. The navigation
95
+ // guard asks to discard unsaved changes and closes the sheet.
96
+ FormEngine.ready().then((): void => {
82
97
  topWindow.TYPO3?.Backend?.ContentContainer?.setUrl(this.fullscreenButton.href);
83
- // Tell the parent to close the modal
84
- window.parent.postMessage(
85
- { actionName: 'typo3:editform:navigate' },
86
- window.location.origin
87
- );
88
- };
89
- if (FormEngine.hasChange()) {
90
- Modal.confirm(
91
- labels.get('label.confirm.close_without_save.title'),
92
- labels.get('label.confirm.close_without_save.content'),
93
- Severity.warning,
94
- [
95
- {
96
- text: labels.get('buttons.confirm.close_without_save.no'),
97
- btnClass: 'btn-default',
98
- trigger: (_e, modal): void => { modal.hideModal(); },
99
- },
100
- {
101
- text: labels.get('buttons.confirm.close_without_save.yes'),
102
- btnClass: 'btn-warning',
103
- trigger: (_e, modal): void => { modal.hideModal(); navigate(); },
104
- },
105
- ]
106
- );
107
- } else {
108
- navigate();
98
+ });
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Navigating the content frame while the sheet is open (e.g. via a context
104
+ * menu action) would load the target behind the sheet. The sheet therefore
105
+ * replaces FormEngine as navigation guard and closes itself instead.
106
+ */
107
+ private initializeNavigationGuard(): void {
108
+ const consumerScope = topWindow.TYPO3?.Backend?.consumerScope;
109
+ if (!consumerScope) {
110
+ return;
111
+ }
112
+ const navigationGuard: Consumable = {
113
+ consume: (interactionRequest: TriggerRequest): Promise<void> | null => {
114
+ if (!interactionRequest.concernsTypes(['typo3.setUrl'])) {
115
+ return null;
116
+ }
117
+ // Stays pending if the user keeps editing, which cancels the navigation
118
+ return new Promise<void>((resolve): void => {
119
+ if (interactionRequest.concernsTypes(['typo3.recordCommit'])) {
120
+ // A record commit returns to the current module, so the sheet stays
121
+ // open and is reloaded once the commit is done to show its result.
122
+ this.confirmDiscardChanges((): void => {
123
+ topDocument.addEventListener('typo3-module-loaded', () => this.reload(), { once: true });
124
+ resolve();
125
+ });
126
+ return;
127
+ }
128
+ this.confirmDiscardChanges((): void => {
129
+ resolve();
130
+ window.parent.postMessage(
131
+ { actionName: 'typo3:editform:navigate' },
132
+ window.location.origin
133
+ );
134
+ });
135
+ });
136
+ },
137
+ };
138
+ let pageHidden = false;
139
+ window.addEventListener('pagehide', (): void => {
140
+ pageHidden = true;
141
+ consumerScope.detach(navigationGuard);
142
+ }, { once: true });
143
+ FormEngine.ready().then((): void => {
144
+ if (pageHidden) {
145
+ return;
109
146
  }
147
+ consumerScope.detach(FormEngine);
148
+ consumerScope.attach(navigationGuard);
110
149
  });
111
150
  }
112
151
 
152
+ private reload(): void {
153
+ const url = new URL(window.location.href);
154
+ url.searchParams.delete('justSaved');
155
+ window.location.replace(url.toString());
156
+ }
157
+
158
+ private confirmDiscardChanges(onDiscard: () => void, onSave: (() => void) | null = null): void {
159
+ if (!FormEngine.hasChange()) {
160
+ onDiscard();
161
+ return;
162
+ }
163
+ Modal.confirm(
164
+ labels.get('label.confirm.close_without_save.title'),
165
+ labels.get('label.confirm.close_without_save.content'),
166
+ Severity.warning,
167
+ [
168
+ {
169
+ text: labels.get('buttons.confirm.close_without_save.no'),
170
+ btnClass: 'btn-default',
171
+ trigger: (_e, modal): void => { modal.hideModal(); },
172
+ },
173
+ {
174
+ text: labels.get('buttons.confirm.close_without_save.yes'),
175
+ btnClass: 'btn-default',
176
+ trigger: (_e, modal): void => { modal.hideModal(); onDiscard(); },
177
+ },
178
+ ...(onSave !== null && document.querySelector('.has-error') === null ? [{
179
+ text: coreCommonLabels.get('save'),
180
+ btnClass: 'btn-primary',
181
+ active: true,
182
+ trigger: (_e: Event, modal: ModalElement): void => { modal.hideModal(); onSave(); },
183
+ }] : []),
184
+ ]
185
+ );
186
+ }
187
+
113
188
  private initializeEscapeKey(): void {
114
189
  document.addEventListener('keydown', (e: KeyboardEvent): void => {
115
190
  if (e.key === 'Escape') {
@@ -127,6 +202,11 @@ class ContextualRecordEdit {
127
202
  if (event.data?.actionName === 'typo3:editform:requestclose') {
128
203
  this.requestClose();
129
204
  }
205
+ if (event.data?.actionName === 'typo3:editform:reload') {
206
+ // Saving the stale form of the record that was just saved elsewhere would revert that change
207
+ const canSave = event.data.savedRecord !== this.record;
208
+ this.confirmDiscardChanges((): void => this.reload(), canSave ? (): void => FormEngine.saveDocument() : null);
209
+ }
130
210
  });
131
211
  }
132
212
 
@@ -139,6 +219,7 @@ class ContextualRecordEdit {
139
219
  {
140
220
  actionName: 'typo3:editform:saved',
141
221
  recordTitle: this.options.savedRecordTitle ?? '',
222
+ record: this.record,
142
223
  },
143
224
  window.location.origin
144
225
  );
@@ -145,7 +145,7 @@ class DateTimePicker {
145
145
  */
146
146
  private getScrollEvent(): ThrottleEvent {
147
147
  return new ThrottleEvent('scroll', (): void => {
148
- const activeFlatpickrElement = document.querySelector('.flatpickr-input.active') as FlatpickrInputElement;
148
+ const activeFlatpickrElement = document.querySelector<FlatpickrInputElement>('.flatpickr-input.active');
149
149
  if (activeFlatpickrElement === null) {
150
150
  return;
151
151
  }