typo3-typescript-sources 14.3.7 → 14.3.8

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.
package/.last-synced-tag CHANGED
@@ -1 +1 @@
1
- v14.3.7
1
+ v14.3.8
package/README.md CHANGED
@@ -16,7 +16,7 @@ npm install typo3-typescript-sources
16
16
 
17
17
  - **Original Repository:** [TYPO3/typo3](https://github.com/TYPO3/typo3)
18
18
  - **Source Path:** `Build/Sources/TypeScript/`
19
- - **Current TYPO3 Version:** `v14.3.7`
19
+ - **Current TYPO3 Version:** `v14.3.8`
20
20
 
21
21
  ## 🌿 Branches
22
22
 
@@ -160,7 +160,7 @@ export class ColumnSelectorButton extends PseudoButtonLitElement {
160
160
  text: this.buttonOk,
161
161
  btnClass: 'btn-primary',
162
162
  name: 'update',
163
- trigger: (e: Event, modal: ModalElement): void => this.processSelection(modal)
163
+ form: 'columnSelectorForm',
164
164
  }
165
165
  ],
166
166
  ajaxCallback: (): void => this.handleModalContentLoaded(modal)
@@ -198,7 +198,10 @@ export class ColumnSelectorButton extends PseudoButtonLitElement {
198
198
  return;
199
199
  }
200
200
  // Prevent the form from being submitted as the form data will be send via an ajax request
201
- form.addEventListener('submit', (e: Event): void => { e.preventDefault(); });
201
+ form.addEventListener('submit', (e: Event): void => {
202
+ e.preventDefault();
203
+ this.processSelection(currentModal);
204
+ });
202
205
 
203
206
  const columns: NodeListOf<HTMLInputElement> = currentModal.querySelectorAll(Selectors.columnsSelector);
204
207
  const columnsFilter: HTMLInputElement = currentModal.querySelector(Selectors.columnsFilterSelector);
@@ -371,7 +371,21 @@ export default class DragUploader {
371
371
  * @param {FileList} files
372
372
  */
373
373
  public processFiles(files: FileList): void {
374
- this.queueLength = files.length;
374
+ const validFiles: File[] = [];
375
+ for (const file of Array.from(files)) {
376
+ const error = this.validateFile(file);
377
+ if (error === null) {
378
+ validFiles.push(file);
379
+ } else {
380
+ Notification.error(coreLabels.get('file_upload.notUploaded.title'), error);
381
+ }
382
+ }
383
+ this.fileInput.value = '';
384
+ if (validFiles.length === 0) {
385
+ return;
386
+ }
387
+
388
+ this.queueLength = validFiles.length;
375
389
 
376
390
  if (this.fileList.parentElement.hasAttribute('hidden')) {
377
391
  // Show the filelist (table)
@@ -385,11 +399,11 @@ export default class DragUploader {
385
399
  this.overallProgressBar = document.createElement('typo3-backend-progress-bar');
386
400
  document.body.appendChild(this.overallProgressBar);
387
401
  this.overallProgressBar.start();
388
- this.percentagePerFile = 1 / files.length;
402
+ this.percentagePerFile = 1 / validFiles.length;
389
403
 
390
404
  // Check for each file if is already exist before adding it to the queue
391
405
  const ajaxCalls: Promise<void>[] = [];
392
- Array.from(files).forEach((file: File) => {
406
+ validFiles.forEach((file: File) => {
393
407
  const request = new AjaxRequest(TYPO3.settings.ajaxUrls.file_exists).withQueryArguments({
394
408
  fileName: file.name,
395
409
  fileTarget: this.target,
@@ -418,8 +432,6 @@ export default class DragUploader {
418
432
  this.overallProgressBar.done();
419
433
  }
420
434
  });
421
-
422
- this.fileInput.value = '';
423
435
  }
424
436
 
425
437
  public fileInDropzone = (): void => {
@@ -631,6 +643,37 @@ export default class DragUploader {
631
643
  document.dispatchEvent(new CustomEvent(uploadFinishedEventName));
632
644
  });
633
645
  }
646
+
647
+ /**
648
+ * @returns the reason why the file cannot be uploaded, null if it can be uploaded
649
+ */
650
+ private validateFile(file: File): string | null {
651
+ const extension = file.name.split('.').pop().toLowerCase();
652
+ if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
653
+ return coreLabels.get('file_upload.maxFileSizeExceeded', {
654
+ '0': file.name,
655
+ '1': FormatUtility.fileSizeAsString(this.maxFileSize),
656
+ });
657
+ }
658
+ if (this.fileDenyPattern && file.name.match(this.fileDenyPattern)) {
659
+ return coreLabels.get('file_upload.fileNotAllowed', {
660
+ '0': file.name,
661
+ });
662
+ }
663
+ if (this.filesExtensionsAllowed && !this.filesExtensionsAllowed.split(',').includes(extension)) {
664
+ return coreLabels.get('file_upload.notUploaded.extensionExpected', {
665
+ '0': file.name,
666
+ '1': this.filesExtensionsAllowed.split(',').join(', '),
667
+ });
668
+ }
669
+ if (this.filesExtensionsDisallowed && this.filesExtensionsDisallowed.split(',').includes(extension)) {
670
+ return coreLabels.get('file_upload.notUploaded.extensionDisallowed', {
671
+ '0': file.name,
672
+ '1': extension,
673
+ });
674
+ }
675
+ return null;
676
+ }
634
677
  }
635
678
 
636
679
  class FileQueueItem {
@@ -697,72 +740,41 @@ class FileQueueItem {
697
740
  // set dummy file icon
698
741
  this.iconCol.innerHTML = '<typo3-backend-icon identifier="mimetypes-other-other" />';
699
742
 
700
- // check file size
701
- if (this.dragUploader.maxFileSize > 0 && this.file.size > this.dragUploader.maxFileSize) {
702
- this.updateMessage(coreLabels.get('file_upload.maxFileSizeExceeded', {
703
- '0': this.file.name,
704
- '1': FormatUtility.fileSizeAsString(this.dragUploader.maxFileSize),
705
- }));
706
- this.progressBar.value = 100;
707
- this.progressBar.severity = SeverityEnum.error;
708
-
709
- // check filename/extension against deny pattern
710
- } else if (this.dragUploader.fileDenyPattern && this.file.name.match(this.dragUploader.fileDenyPattern)) {
711
- this.updateMessage(coreLabels.get('file_upload.fileNotAllowed', {
712
- '0': this.file.name,
713
- }));
714
- this.progressBar.value = 100;
715
- this.progressBar.severity = SeverityEnum.error;
716
-
717
- } else if (this.isProhibitedByAllowedExtensionsList()) {
718
- this.updateMessage(coreLabels.get('file_upload.fileExtensionExpected', {
719
- '0': this.dragUploader.filesExtensionsAllowed,
720
- }));
721
- this.progressBar.value = 100;
722
- this.progressBar.severity = SeverityEnum.error;
723
- } else if (this.isProhibitedByDisallowedExtensionsList()) {
724
- this.updateMessage(coreLabels.get('file_upload.fileExtensionDisallowed', {
725
- '0': this.dragUploader.filesExtensionsDisallowed,
726
- }));
727
- this.progressBar.value = 100;
728
- this.progressBar.severity = SeverityEnum.error;
729
- } else {
730
- this.updateMessage('- ' + FormatUtility.fileSizeAsString(this.file.size));
731
-
732
- const formData = new FormData();
733
- formData.append('data[upload][1][target]', this.dragUploader.target);
734
- formData.append('data[upload][1][data]', '1');
735
- formData.append('overwriteExistingFiles', this.override);
736
- formData.append('redirect', '');
737
- formData.append('upload_1', this.file);
738
-
739
- // We use XMLHttpRequest as we need the `progress` event which isn't supported by fetch()
740
- const xhr = new XMLHttpRequest();
741
- xhr.onreadystatechange = (): void => {
742
- if (xhr.readyState === XMLHttpRequest.DONE) {
743
- if (xhr.status === 200) {
744
- try {
745
- const response = JSON.parse(xhr.responseText);
746
- if (!response.hasErrors) {
747
- this.uploadSuccess(response);
748
- } else {
749
- this.uploadError(xhr);
750
- }
751
- }
752
- catch {
753
- // In case JSON can not be parsed, the upload failed due to server errors,
754
- // e.g. "POST Content-Length exceeds limit". Just handle as upload error.
743
+ this.updateMessage('- ' + FormatUtility.fileSizeAsString(this.file.size));
744
+
745
+ const formData = new FormData();
746
+ formData.append('data[upload][1][target]', this.dragUploader.target);
747
+ formData.append('data[upload][1][data]', '1');
748
+ formData.append('overwriteExistingFiles', this.override);
749
+ formData.append('redirect', '');
750
+ formData.append('upload_1', this.file);
751
+
752
+ // We use XMLHttpRequest as we need the `progress` event which isn't supported by fetch()
753
+ const xhr = new XMLHttpRequest();
754
+ xhr.onreadystatechange = (): void => {
755
+ if (xhr.readyState === XMLHttpRequest.DONE) {
756
+ if (xhr.status === 200) {
757
+ try {
758
+ const response = JSON.parse(xhr.responseText);
759
+ if (!response.hasErrors) {
760
+ this.uploadSuccess(response);
761
+ } else {
755
762
  this.uploadError(xhr);
756
763
  }
757
- } else {
764
+ }
765
+ catch {
766
+ // In case JSON can not be parsed, the upload failed due to server errors,
767
+ // e.g. "POST Content-Length exceeds limit". Just handle as upload error.
758
768
  this.uploadError(xhr);
759
769
  }
770
+ } else {
771
+ this.uploadError(xhr);
760
772
  }
761
- };
762
- xhr.upload.addEventListener('progress', (e: ProgressEvent) => this.updateProgress(e));
763
- xhr.open('POST', TYPO3.settings.ajaxUrls.file_process);
764
- xhr.send(formData);
765
- }
773
+ }
774
+ };
775
+ xhr.upload.addEventListener('progress', (e: ProgressEvent) => this.updateProgress(e));
776
+ xhr.open('POST', TYPO3.settings.ajaxUrls.file_process);
777
+ xhr.send(formData);
766
778
  }
767
779
 
768
780
  /**
@@ -925,26 +937,6 @@ class FileQueueItem {
925
937
  this.row.append(document.createElement('td'));
926
938
  }
927
939
  }
928
-
929
- public isProhibitedByAllowedExtensionsList(): boolean {
930
- if (!this.dragUploader.filesExtensionsAllowed) {
931
- return false;
932
- }
933
- const extension = this.file.name.split('.').pop();
934
- const allowed = this.dragUploader.filesExtensionsAllowed.split(',');
935
-
936
- return !allowed.includes(extension.toLowerCase());
937
- }
938
-
939
- public isProhibitedByDisallowedExtensionsList(): boolean {
940
- if (!this.dragUploader.filesExtensionsDisallowed) {
941
- return false;
942
- }
943
- const extension = this.file.name.split('.').pop();
944
- const disallowed = this.dragUploader.filesExtensionsDisallowed.split(',');
945
-
946
- return disallowed.includes(extension.toLowerCase());
947
- }
948
940
  }
949
941
 
950
942
  DragUploader.init();
@@ -30,14 +30,14 @@ export class OnlineMediaFormElement extends LitElement {
30
30
 
31
31
  protected override render(): TemplateResult {
32
32
  return html`
33
- <form @submit="${ this.dispatchSubmitEvent }">
33
+ <form id="online-media-form" @submit="${ this.dispatchSubmitEvent }">
34
34
  <div class="form-control-wrap">
35
- <input type="text" class="form-control" name="online-media-url" placeholder="${this.placeholder}" required>
35
+ <input type="text" class="form-control" name="online-media-url" placeholder="${this.placeholder}" required autofocus>
36
36
  <div class="form-text">
37
37
  ${this.allowedExtensionsHelpText}<br>
38
38
  <ul class="badge-list">
39
39
  ${this.allowedExtensions.split(',').map((ext: string) => html`
40
- <li><span class="badge badge-secondary">${ext.trim().toUpperCase()}</span></li>
40
+ <li><span class="badge badge-secondary">${ext.trim().toLowerCase()}</span></li>
41
41
  `)}
42
42
  </ul>
43
43
  </div>
@@ -1033,8 +1033,12 @@ export default (function() {
1033
1033
  };
1034
1034
 
1035
1035
  /**
1036
- * Enables all doc header buttons.
1037
- * Called after FormEngine initialization is complete to restore button interactivity.
1036
+ * Releases the doc header button bar, which is rendered "inert" as long as the
1037
+ * ui block is shown, to prevent unintended operations while FormEngine is loading.
1038
+ * Called after FormEngine initialization is complete.
1039
+ *
1040
+ * The block is applied to the button bar as a whole, never to the single buttons,
1041
+ * so that the disabled state of individual buttons remains untouched.
1038
1042
  */
1039
1043
  FormEngine.enableDocHeaderButtons = function(): void {
1040
1044
  const docHeaderBar = document.querySelector('.t3js-module-docheader-buttons');
@@ -1042,16 +1046,8 @@ export default (function() {
1042
1046
  return;
1043
1047
  }
1044
1048
 
1045
- // Enable all buttons and links in the doc header
1046
- docHeaderBar.querySelectorAll('button, a, input[type="submit"]').forEach((element: HTMLElement): void => {
1047
- if (element instanceof HTMLButtonElement || element instanceof HTMLInputElement) {
1048
- element.disabled = false;
1049
- } else if (element instanceof HTMLAnchorElement) {
1050
- // For anchor elements - remove TYPO3 disabled state
1051
- element.classList.remove('disabled');
1052
- element.removeAttribute('aria-disabled');
1053
- }
1054
- });
1049
+ docHeaderBar.removeAttribute('inert');
1050
+ docHeaderBar.removeAttribute('aria-busy');
1055
1051
  };
1056
1052
 
1057
1053
  /**
@@ -0,0 +1,61 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+
14
+ /**
15
+ * Module: @typo3/backend/layout-module/column-restriction
16
+ *
17
+ * Client-side mirror of the server-side backend layout colPos restriction
18
+ * (\TYPO3\CMS\Backend\View\BackendLayoutView::isCTypeAllowedInColPosByPage()).
19
+ * Used by drag-drop and paste in the page layout module to avoid offering
20
+ * actions that the DataHandler would reject anyway.
21
+ *
22
+ * @internal
23
+ */
24
+
25
+ function toList(value: string | undefined): string[] {
26
+ return (value ?? '')
27
+ .split(',')
28
+ .map((item: string): string => item.trim())
29
+ .filter((item: string): boolean => item !== '');
30
+ }
31
+
32
+ /**
33
+ * Evaluates whether the given CType is allowed in the column described by the
34
+ * "data-allowed-content-types" / "data-disallowed-content-types" attributes.
35
+ * Mirrors the server-side logic: disallowed wins, an empty allow list allows all.
36
+ */
37
+ export function isContentTypeAllowedInColumn(cType: string, columnElement: HTMLElement | null): boolean {
38
+ if (columnElement === null) {
39
+ return true;
40
+ }
41
+
42
+ const disallowed = toList(columnElement.dataset.disallowedContentTypes);
43
+ if (disallowed.includes(cType)) {
44
+ return false;
45
+ }
46
+
47
+ const allowed = toList(columnElement.dataset.allowedContentTypes);
48
+ if (allowed.length > 0 && !allowed.includes(cType)) {
49
+ return false;
50
+ }
51
+
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Resolves the column of the given element (nearest ancestor carrying a colPos)
57
+ * and evaluates whether the CType is allowed there.
58
+ */
59
+ export function isContentTypeAllowedForTarget(cType: string, target: HTMLElement): boolean {
60
+ return isContentTypeAllowedInColumn(cType, target.closest('[data-colpos]'));
61
+ }
@@ -22,6 +22,7 @@ import RegularEvent from '@typo3/core/event/regular-event';
22
22
  import { DataTransferTypes } from '@typo3/backend/enum/data-transfer-types';
23
23
  import BroadcastService from '@typo3/backend/broadcast-service';
24
24
  import { BroadcastMessage } from '@typo3/backend/broadcast-message';
25
+ import { isContentTypeAllowedInColumn } from '@typo3/backend/layout-module/column-restriction';
25
26
  import type ResponseInterface from '../ajax-data-handler/response-interface';
26
27
  import type { DragDropThumbnail, DragTooltipMetadata } from '@typo3/backend/drag-tooltip';
27
28
  import DragDropUtility from '@typo3/backend/utility/drag-drop-utility';
@@ -36,6 +37,7 @@ export type ContentElementDragDropData = {
36
37
  pid: number;
37
38
  uid: number;
38
39
  language: number;
40
+ cType: string;
39
41
  content: string;
40
42
  moveElementUrl: string;
41
43
  };
@@ -54,6 +56,8 @@ enum Classes {
54
56
  }
55
57
 
56
58
  class DragDrop {
59
+ private draggedContentType: string = '';
60
+
57
61
  constructor() {
58
62
  DocumentService.ready().then((): void => {
59
63
  this.initialize();
@@ -116,10 +120,13 @@ class DragDrop {
116
120
  protected onDragStart(event: DragEvent, target: HTMLElement): void {
117
121
  const content = target.closest(Identifiers.content) as HTMLElement;
118
122
 
123
+ this.draggedContentType = content.dataset.ctype ?? '';
124
+
119
125
  event.dataTransfer.setData(DataTransferTypes.content, JSON.stringify({
120
126
  pid: this.getCurrentPageId(),
121
127
  uid: parseInt(content.dataset.uid, 10),
122
128
  language: parseInt(content.dataset.languageUid, 10),
129
+ cType: this.draggedContentType,
123
130
  content: content.outerHTML,
124
131
  moveElementUrl: content.dataset.moveElementUrl,
125
132
  } as ContentElementDragDropData));
@@ -133,6 +140,7 @@ class DragDrop {
133
140
  }
134
141
 
135
142
  protected onDragEnd(): void {
143
+ this.draggedContentType = '';
136
144
  this.hideDropZones();
137
145
  }
138
146
 
@@ -176,6 +184,7 @@ class DragDrop {
176
184
 
177
185
  // the dragged elements language uid
178
186
  let language = contentElementDragDropData.language;
187
+ // Keep in sync with LanguageMarker::ALL_LANGUAGES on the PHP side.
179
188
  if (language !== -1) {
180
189
  // new elements language must be the same as the column the element is dropped in if element is not -1
181
190
  language = parseInt((dropContainer.closest('[data-language-uid]') as HTMLElement).dataset.languageUid, 10);
@@ -335,6 +344,9 @@ class DragDrop {
335
344
 
336
345
  protected showDropZones(): void {
337
346
  document.querySelectorAll(Identifiers.dropZone).forEach((element: HTMLElement): void => {
347
+ if (!isContentTypeAllowedInColumn(this.draggedContentType, element.closest('[data-colpos]'))) {
348
+ return;
349
+ }
338
350
  element.hidden = false;
339
351
  const addContentButton = element.parentElement.querySelector(Identifiers.addContent) as HTMLElement;
340
352
  if (addContentButton !== null) {
@@ -24,18 +24,21 @@ import Severity from '../severity';
24
24
  import '@typo3/backend/element/icon-element';
25
25
  import { SeverityEnum } from '../enum/severity';
26
26
  import RegularEvent from '@typo3/core/event/regular-event';
27
+ import { isContentTypeAllowedForTarget } from '@typo3/backend/layout-module/column-restriction';
27
28
  import type ResponseInterface from '../ajax-data-handler/response-interface';
28
29
  import layoutLabels from '~labels/backend.layout';
29
30
 
30
31
  type PasteOptions = {
31
32
  itemOnClipboardUid: number;
32
33
  itemOnClipboardTitle: string;
34
+ itemOnClipboardCType: string;
33
35
  copyMode: string;
34
36
  };
35
37
 
36
38
  class Paste {
37
39
  private readonly itemOnClipboardUid: number = 0;
38
40
  private readonly itemOnClipboardTitle: string = '';
41
+ private readonly itemOnClipboardCType: string = '';
39
42
  private readonly copyMode: string = '';
40
43
  private readonly elementIdentifier: string = '.t3js-page-ce';
41
44
  private pasteAfterLinkTemplate: string = '';
@@ -47,6 +50,7 @@ class Paste {
47
50
  constructor(args: PasteOptions) {
48
51
  this.itemOnClipboardUid = args.itemOnClipboardUid;
49
52
  this.itemOnClipboardTitle = args.itemOnClipboardTitle;
53
+ this.itemOnClipboardCType = args.itemOnClipboardCType;
50
54
  this.copyMode = args.copyMode;
51
55
 
52
56
  DocumentService.ready().then((): void => {
@@ -96,6 +100,9 @@ class Paste {
96
100
  private activatePasteIcons(): void {
97
101
  if (this.pasteAfterLinkTemplate && this.pasteIntoLinkTemplate) {
98
102
  document.querySelectorAll('.t3js-page-new-ce').forEach((el: HTMLElement): void => {
103
+ if (!isContentTypeAllowedForTarget(this.itemOnClipboardCType, el)) {
104
+ return;
105
+ }
99
106
  const template = el.parentElement.dataset.page ? this.pasteIntoLinkTemplate : this.pasteAfterLinkTemplate;
100
107
  el.append(document.createRange().createContextualFragment(template));
101
108
  });
@@ -284,15 +284,7 @@ class LoginRefresh {
284
284
  active: false,
285
285
  btnClass: 'btn-primary',
286
286
  name: 'refreshSession',
287
- trigger: async (e: Event, modal: ModalElement) => {
288
- modal.querySelector('form').requestSubmit();
289
- const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.login_refresh).get();
290
- const data = await response.resolve();
291
- modal.hideModal();
292
- if (!data.refresh.success) {
293
- modal.dispatchEvent(new Event('show-login-form'));
294
- }
295
- }
287
+ form: 'beLoginRefresh',
296
288
  }
297
289
  ],
298
290
  content: html`
package/backend/modal.ts CHANGED
@@ -318,6 +318,7 @@ export class ModalElement extends LitElement {
318
318
  ${this.buttons.map(button => this.renderModalButton(button))}
319
319
  </div>
320
320
  `}
321
+ <div class="alert-container"></div>
321
322
  </dialog>
322
323
  `;
323
324
  }
@@ -389,6 +390,8 @@ export class ModalElement extends LitElement {
389
390
  if (this.ajaxCallback) {
390
391
  this.ajaxCallback(this);
391
392
  }
393
+ // Native autofocus only covers content present when the dialog opens
394
+ this.querySelector<HTMLElement>(`${Identifiers.body} [autofocus]`)?.focus();
392
395
  this.dispatchEvent(new CustomEvent('modal-loaded'));
393
396
  });
394
397
  })
@@ -944,11 +947,10 @@ class Modal {
944
947
  }
945
948
 
946
949
  currentModal.addEventListener('typo3-modal-shown', (): void => {
947
- // focus the button which was configured as active button
948
- const activeButton = currentModal.querySelector(`${Identifiers.footer} .t3js-active`) as HTMLInputElement | null;
949
- if (activeButton !== null) {
950
- activeButton.focus();
951
- }
950
+ // Prefer an autofocus field over the active button
951
+ const focusTarget = currentModal.querySelector<HTMLElement>(`${Identifiers.body} [autofocus]`)
952
+ ?? currentModal.querySelector<HTMLElement>(`${Identifiers.footer} .t3js-active`);
953
+ focusTarget?.focus();
952
954
  });
953
955
 
954
956
  // Remove modal from Modal.instances when hidden
@@ -957,10 +959,15 @@ class Modal {
957
959
  const lastIndex = this.instances.length - 1;
958
960
  this.instances.splice(lastIndex, 1);
959
961
  this.currentModal = this.instances[lastIndex - 1];
962
+ remountAlertContainer(this.currentModal);
960
963
  }
961
964
  });
962
965
 
963
966
  currentModal.addEventListener('typo3-modal-hidden', (): void => {
967
+ // A notification shown while the modal closes is placed in it, and must not be removed along with it
968
+ if (currentModal.querySelector('#alert-container') !== null) {
969
+ remountAlertContainer(this.currentModal);
970
+ }
964
971
  currentModal.remove();
965
972
  });
966
973
 
@@ -970,12 +977,38 @@ class Modal {
970
977
  this.instances.push(currentModal);
971
978
  });
972
979
 
980
+ currentModal.addEventListener('typo3-modal-shown', (): void => {
981
+ remountAlertContainer(currentModal);
982
+ });
983
+
973
984
  document.body.appendChild(currentModal);
974
985
 
975
986
  return currentModal;
976
987
  }
977
988
  }
978
989
 
990
+ const remountAlertContainer = (modal: ModalElement | undefined) => {
991
+ const alertContainer = document.querySelector<HTMLDivElement>('#alert-container');
992
+ if (alertContainer) {
993
+ const isOpen = alertContainer.matches(':popover-open');
994
+ let target: HTMLElement = document.body;
995
+ if (modal) {
996
+ target = modal.querySelector('.alert-container') ?? target;
997
+ }
998
+
999
+ if ('moveBefore' in target) {
1000
+ target.moveBefore(alertContainer, null);
1001
+ } else {
1002
+ (target as HTMLElement).appendChild(alertContainer);
1003
+ }
1004
+
1005
+ if (isOpen) {
1006
+ alertContainer.hidePopover();
1007
+ alertContainer.showPopover();
1008
+ }
1009
+ }
1010
+ };
1011
+
979
1012
  let modalObject: Modal = null;
980
1013
  try {
981
1014
  if (parent && parent.window.TYPO3 && parent.window.TYPO3.Modal) {