typo3-typescript-sources 14.3.6 → 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.
Files changed (54) hide show
  1. package/.last-synced-tag +1 -1
  2. package/README-TYPO3.md +1 -1
  3. package/README.md +1 -1
  4. package/backend/column-selector-button.ts +5 -2
  5. package/backend/drag-uploader.ts +78 -86
  6. package/backend/element/contextual-record-edit-trigger.ts +9 -1
  7. package/backend/form-engine/element/extra/select-box-filter.ts +8 -5
  8. package/backend/form-engine/element/online-media-form-element.ts +3 -3
  9. package/backend/form-engine/element/select-tree.ts +16 -2
  10. package/backend/form-engine/field-control/add-record.ts +27 -1
  11. package/backend/form-engine.ts +88 -13
  12. package/backend/layout-module/column-restriction.ts +61 -0
  13. package/backend/layout-module/drag-drop.ts +12 -0
  14. package/backend/layout-module/paste.ts +7 -0
  15. package/backend/localization/localization-wizard.ts +3 -0
  16. package/backend/localization/steps/handler-selection-step.ts +12 -17
  17. package/backend/localization/steps/mode-step.ts +9 -14
  18. package/backend/localization/steps/source-language-step.ts +8 -13
  19. package/backend/localization/steps/target-language-step.ts +12 -17
  20. package/backend/login-refresh.ts +1 -9
  21. package/backend/modal.ts +162 -6
  22. package/backend/notification.ts +90 -6
  23. package/backend/online-media.ts +1 -3
  24. package/backend/record-download-button.ts +4 -5
  25. package/backend/recordlist.ts +5 -0
  26. package/backend/tests/fixtures/modal-autofocus.html +1 -0
  27. package/backend/tests/form-engine/element/extra/select-box-filter-test.ts +215 -0
  28. package/backend/tests/form-engine/element/select-tree-test.ts +121 -0
  29. package/backend/tests/modal-test.ts +91 -0
  30. package/backend/tests/notification-test.ts +176 -0
  31. package/backend/tree/page-tree-element.ts +8 -1
  32. package/backend/wizard/steps/finisher-step.ts +2 -2
  33. package/backend/wizard/wizard.ts +11 -4
  34. package/core/ajax/ajax-request.ts +18 -0
  35. package/core/tests/ajax/ajax-request-test.ts +1 -1
  36. package/core/tests/utility/text-normalizer-test.ts +138 -0
  37. package/core/utility/text-normalizer.ts +98 -0
  38. package/dashboard/dashboard.ts +3 -8
  39. package/filelist/file-list-rename-handler.ts +3 -6
  40. package/filelist/file-list-replace-handler.ts +3 -6
  41. package/filelist/file-list.ts +3 -4
  42. package/form/backend/form-editor/modals-component.ts +114 -5
  43. package/form/backend/form-editor/view-model.ts +1 -1
  44. package/form/backend/form-wizard/steps/settings-step.ts +6 -3
  45. package/package.json +2 -2
  46. package/rte-ckeditor/legacy/ckeditor5-bundle.ts +0 -1
  47. package/rte-ckeditor/plugin/typo3-link.ts +7 -9
  48. package/rte-ckeditor/rte-link-browser.ts +1 -1
  49. package/scheduler/scheduler-add-group.ts +4 -12
  50. package/workspaces/backend.ts +7 -2
  51. package/workspaces/preview.ts +39 -0
  52. package/workspaces/renderable/comment-view.ts +4 -2
  53. package/workspaces/renderable/record-table.ts +134 -80
  54. package/workspaces/workspaces.ts +65 -12
@@ -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
  });
@@ -151,6 +151,9 @@ export class LocalizationWizard extends LitElement {
151
151
 
152
152
  private handleStepSummary(event: StepSummaryEvent) {
153
153
  const recordInfo = this.recordInfoTask.value;
154
+ if (recordInfo === undefined) {
155
+ return;
156
+ }
154
157
  event.detail.summaryData = [
155
158
  {
156
159
  label: recordInfo.typeName,
@@ -45,23 +45,18 @@ export class HandlerSelectionStep implements WizardStepInterface, WizardStepValu
45
45
  return [];
46
46
  }
47
47
 
48
- try {
49
- const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_handlers)
50
- .withQueryArguments({
51
- recordType: recordType,
52
- recordUid: recordUid,
53
- sourceLanguage: sourceLanguage,
54
- targetLanguage: targetLanguage,
55
- mode: mode,
56
- })
57
- .get();
58
- const handlers: LocalizationHandler[] = await response.resolve();
59
-
60
- return handlers;
61
- } catch (error) {
62
- console.warn('Failed to fetch handlers:', error);
63
- return [];
64
- }
48
+ const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_handlers)
49
+ .withQueryArguments({
50
+ recordType: recordType,
51
+ recordUid: recordUid,
52
+ sourceLanguage: sourceLanguage,
53
+ targetLanguage: targetLanguage,
54
+ mode: mode,
55
+ })
56
+ .get();
57
+ const handlers: LocalizationHandler[] = await response.resolve();
58
+
59
+ return handlers;
65
60
  },
66
61
  args: () => [this.context.recordType, this.context.recordUid, this.context.getStoreData('sourceLanguage'), this.context.getStoreData('targetLanguage'), this.context.getStoreData('localizationMode')],
67
62
  autoRun: false
@@ -46,20 +46,15 @@ export class ModeStep implements WizardStepInterface, WizardStepValueInterface,
46
46
  return [];
47
47
  }
48
48
 
49
- try {
50
- const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_modes).withQueryArguments({
51
- recordType: recordType,
52
- recordUid: recordUid,
53
- targetLanguage: targetLanguage,
54
- sourceLanguage: sourceLanguage,
55
- }).get();
56
- const modes: LocalizationMode[] = await response.resolve();
57
-
58
- return modes;
59
- } catch (error) {
60
- console.warn('Failed to fetch localization modes:', error);
61
- return [];
62
- }
49
+ const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_modes).withQueryArguments({
50
+ recordType: recordType,
51
+ recordUid: recordUid,
52
+ targetLanguage: targetLanguage,
53
+ sourceLanguage: sourceLanguage,
54
+ }).get();
55
+ const modes: LocalizationMode[] = await response.resolve();
56
+
57
+ return modes;
63
58
  },
64
59
  args: () => [this.context.recordType, this.context.recordUid, this.context.getStoreData('targetLanguage'), this.context.getStoreData('sourceLanguage')],
65
60
  autoRun: false
@@ -38,19 +38,14 @@ export class SourceLanguageStep implements WizardStepInterface, WizardStepValueI
38
38
  return [];
39
39
  }
40
40
 
41
- try {
42
- const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_sources).withQueryArguments({
43
- recordType: recordType,
44
- recordUid: recordUid,
45
- targetLanguage: targetLanguageId,
46
- }).get();
47
- const sourceLanguages: LocalizationLanguageRecord[] = await response.resolve();
48
-
49
- return sourceLanguages;
50
- } catch (error) {
51
- console.warn('Failed to fetch source languages:', error);
52
- return [];
53
- }
41
+ const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_sources).withQueryArguments({
42
+ recordType: recordType,
43
+ recordUid: recordUid,
44
+ targetLanguage: targetLanguageId,
45
+ }).get();
46
+ const sourceLanguages: LocalizationLanguageRecord[] = await response.resolve();
47
+
48
+ return sourceLanguages;
54
49
  },
55
50
  args: () => [this.context.recordType, this.context.recordUid, this.context.getStoreData('targetLanguage')],
56
51
  autoRun: false
@@ -37,24 +37,19 @@ export class TargetLanguageStep implements WizardStepInterface, WizardStepValueI
37
37
  constructor(private readonly context: LocalizationContext) {
38
38
  this.task = new Task(this.context.wizard, {
39
39
  task: async ([recordType, recordUid]: [string, number]): Promise<LocalizationLanguageRecord[]> => {
40
- try {
41
- const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_targets).withQueryArguments({
42
- recordType: recordType,
43
- recordUid: recordUid,
44
- }).get();
45
- let targetLanguages: LocalizationLanguageRecord[] = await response.resolve();
46
-
47
- // Filter to only the predefined target language if one exists vsia property
48
- const predefinedTargetLanguage = this.context.targetLanguage;
49
- if (predefinedTargetLanguage != null) {
50
- targetLanguages = targetLanguages.filter(lang => lang.uid === predefinedTargetLanguage);
51
- }
52
-
53
- return targetLanguages;
54
- } catch (error) {
55
- console.warn('Failed to fetch target languages:', error);
56
- return [];
40
+ const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_targets).withQueryArguments({
41
+ recordType: recordType,
42
+ recordUid: recordUid,
43
+ }).get();
44
+ let targetLanguages: LocalizationLanguageRecord[] = await response.resolve();
45
+
46
+ // Filter to only the predefined target language if one exists vsia property
47
+ const predefinedTargetLanguage = this.context.targetLanguage;
48
+ if (predefinedTargetLanguage != null) {
49
+ targetLanguages = targetLanguages.filter(lang => lang.uid === predefinedTargetLanguage);
57
50
  }
51
+
52
+ return targetLanguages;
58
53
  },
59
54
  args: () => [this.context.recordType, this.context.recordUid],
60
55
  autoRun: false
@@ -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
@@ -16,6 +16,7 @@ import { customElement, property, state, query } from 'lit/decorators.js';
16
16
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
17
17
  import { classMap, type ClassInfo } from 'lit/directives/class-map.js';
18
18
  import { ifDefined } from 'lit/directives/if-defined.js';
19
+ import { styleMap, type StyleInfo } from 'lit/directives/style-map.js';
19
20
  import { classesArrayToClassInfo } from '@typo3/core/lit-helper';
20
21
  import RegularEvent from '@typo3/core/event/regular-event';
21
22
  import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
@@ -23,6 +24,7 @@ import type { AbstractAction } from './action-button/abstract-action';
23
24
  import type { ModalResponseEvent } from '@typo3/backend/modal-interface';
24
25
  import { SeverityEnum } from './enum/severity';
25
26
  import AjaxRequest from '@typo3/core/ajax/ajax-request';
27
+ import Persistent from '@typo3/backend/storage/persistent';
26
28
  import Severity from './severity';
27
29
  import '@typo3/backend/element/icon-element';
28
30
  import '@typo3/backend/element/spinner-element';
@@ -121,6 +123,7 @@ export interface Configuration {
121
123
  staticBackdrop: boolean;
122
124
  hideCloseButton: boolean;
123
125
  hideHeader: boolean;
126
+ resizeIdentifier: string;
124
127
  }
125
128
 
126
129
  type PartialConfiguration = Partial<Omit<Configuration, 'buttons'> & { buttons: Array<Partial<Button>> }>;
@@ -129,6 +132,8 @@ let uniqueIdCounter = 0;
129
132
 
130
133
  @customElement('typo3-backend-modal')
131
134
  export class ModalElement extends LitElement {
135
+ private static readonly RESIZE_MIN_WIDTH = 400;
136
+
132
137
  @property({ type: String, reflect: true }) modalTitle: string = '';
133
138
  @property({ type: String, reflect: true }) content: string = '';
134
139
  @property({ type: String, reflect: true }) type: Types = Types.default;
@@ -140,10 +145,13 @@ export class ModalElement extends LitElement {
140
145
  @property({ type: Boolean }) hideHeader: boolean = false;
141
146
  @property({ type: Array }) additionalCssClasses: Array<string> = [];
142
147
  @property({ type: Array, attribute: false }) buttons: Array<Button> = [];
148
+ @property({ type: String, attribute: 'resize-identifier' }) resizeIdentifier: string = '';
143
149
 
144
150
  @state() templateResultContent: TemplateResult | JQuery | Element | DocumentFragment = null;
145
151
  @state() activeButton: Button = null;
146
152
  @query('dialog', true) dialog: HTMLDialogElement;
153
+ @state() private modalWidth?: number;
154
+ @state() private resizing: boolean = false;
147
155
 
148
156
  public callback: ModalCallbackFunction = null;
149
157
  public ajaxCallback: ModalCallbackFunction = null;
@@ -151,6 +159,7 @@ export class ModalElement extends LitElement {
151
159
  public userData: { [key: string]: any } = {};
152
160
 
153
161
  private readonly uniqueId: number;
162
+ private resizeReferencePosition: number = 0;
154
163
  #size: Sizes | SizeConfig = Sizes.default;
155
164
 
156
165
  constructor() {
@@ -196,6 +205,13 @@ export class ModalElement extends LitElement {
196
205
  }
197
206
  }
198
207
 
208
+ public override connectedCallback(): void {
209
+ super.connectedCallback();
210
+ if (this.isResizable()) {
211
+ this.loadPersistedWidth();
212
+ }
213
+ }
214
+
199
215
  public setContent(content: TemplateResult | JQuery | Element | DocumentFragment): void {
200
216
  this.templateResultContent = content;
201
217
  }
@@ -256,6 +272,7 @@ export class ModalElement extends LitElement {
256
272
  }
257
273
 
258
274
  protected override render(): TemplateResult {
275
+ const resizable = this.isResizable();
259
276
  const classes: ClassInfo = classesArrayToClassInfo([
260
277
  'modal',
261
278
  't3js-modal',
@@ -264,11 +281,18 @@ export class ModalElement extends LitElement {
264
281
  `modal-severity-${Severity.getCssClass(this.severity)}`,
265
282
  ...this.getSizeClasses(),
266
283
  `modal-position-${this.position}`,
284
+ ...(resizable ? ['modal-resizable'] : []),
285
+ ...(this.resizing ? ['modal-resizing'] : []),
267
286
  ...this.additionalCssClasses,
268
287
  ]);
288
+ const dialogStyles: StyleInfo = {};
289
+ if (resizable && this.modalWidth) {
290
+ dialogStyles['--typo3-modal-width'] = `${this.modalWidth}px`;
291
+ }
269
292
  return html`
270
293
  <dialog
271
294
  class=${classMap(classes)}
295
+ style=${styleMap(dialogStyles)}
272
296
  aria-labelledby=${ifDefined(this.hideHeader ? undefined : `t3-modal-header-${this.uniqueId}`)}
273
297
  aria-label=${ifDefined(this.hideHeader ? this.modalTitle : undefined)}
274
298
  closedby=${this.hideCloseButton ? 'none' : 'closerequest'}
@@ -276,6 +300,7 @@ export class ModalElement extends LitElement {
276
300
  @cancel=${this.handleDialogCancel}
277
301
  @click=${this.handleDialogClick}
278
302
  >
303
+ ${resizable ? html`<div class="modal-resize-handle" @pointerdown=${this.startResize}></div>` : nothing}
279
304
  ${this.hideHeader ? nothing : html`
280
305
  <div class="modal-header t3js-modal-header">
281
306
  <div class="modal-header-title t3js-modal-title" id="t3-modal-header-${this.uniqueId}">${this.modalTitle}</div>
@@ -293,6 +318,7 @@ export class ModalElement extends LitElement {
293
318
  ${this.buttons.map(button => this.renderModalButton(button))}
294
319
  </div>
295
320
  `}
321
+ <div class="alert-container"></div>
296
322
  </dialog>
297
323
  `;
298
324
  }
@@ -364,6 +390,8 @@ export class ModalElement extends LitElement {
364
390
  if (this.ajaxCallback) {
365
391
  this.ajaxCallback(this);
366
392
  }
393
+ // Native autofocus only covers content present when the dialog opens
394
+ this.querySelector<HTMLElement>(`${Identifiers.body} [autofocus]`)?.focus();
367
395
  this.dispatchEvent(new CustomEvent('modal-loaded'));
368
396
  });
369
397
  })
@@ -480,6 +508,99 @@ export class ModalElement extends LitElement {
480
508
  { transform: 'translateX(0px)' },
481
509
  ], 150);
482
510
  }
511
+
512
+ /**
513
+ * Currently only "sheet" positioned modals (drawers docked to the viewport edge)
514
+ * support resizing, as it is the only position with a single free-floating edge.
515
+ */
516
+ private isResizable(): boolean {
517
+ return this.position === Positions.sheet;
518
+ }
519
+
520
+ private getResizePersistenceKey(): string | null {
521
+ if (!this.resizeIdentifier) {
522
+ return null;
523
+ }
524
+ return `resize.${this.resizeIdentifier}.modal`;
525
+ }
526
+
527
+ private loadPersistedWidth(): void {
528
+ const key = this.getResizePersistenceKey();
529
+ if (!key) {
530
+ return;
531
+ }
532
+ const stored = Persistent.get(key);
533
+ if (stored) {
534
+ const width = parseInt(stored, 10);
535
+ if (!isNaN(width) && width > 0) {
536
+ this.modalWidth = width;
537
+ }
538
+ }
539
+ }
540
+
541
+ private persistWidth(): void {
542
+ const key = this.getResizePersistenceKey();
543
+ if (key && this.modalWidth) {
544
+ Persistent.set(key, String(this.modalWidth));
545
+ }
546
+ }
547
+
548
+ private getMaxWidth(): number {
549
+ const computedMaxWidth = parseFloat(getComputedStyle(this.dialog).maxWidth);
550
+ return Number.isNaN(computedMaxWidth) ? window.innerWidth : computedMaxWidth;
551
+ }
552
+
553
+ private isRtl(): boolean {
554
+ return getComputedStyle(this).direction === 'rtl';
555
+ }
556
+
557
+ /**
558
+ * A "sheet" is docked to the inline-end edge (via margin-inline-end: 0), so its
559
+ * free edge to grab and drag is always the inline-start edge, in both directions.
560
+ */
561
+ private readonly startResize = (event: PointerEvent) => {
562
+ if (event.button !== 0) {
563
+ return;
564
+ }
565
+ event.stopPropagation();
566
+ event.preventDefault();
567
+
568
+ const rect = this.dialog.getBoundingClientRect();
569
+ this.resizeReferencePosition = this.isRtl() ? rect.left : rect.right;
570
+
571
+ this.resizing = true;
572
+ const target = event.target as HTMLElement;
573
+ target.setPointerCapture(event.pointerId);
574
+ target.addEventListener('pointermove', this.handlePointerMove);
575
+ target.addEventListener('pointerup', this.handlePointerUp);
576
+ target.addEventListener('pointercancel', this.handlePointerUp);
577
+ target.addEventListener('lostpointercapture', this.handlePointerUp);
578
+ };
579
+
580
+ private readonly handlePointerMove = (event: PointerEvent) => {
581
+ if (!this.resizing) {
582
+ return;
583
+ }
584
+
585
+ const maxWidth = this.getMaxWidth();
586
+ let width = this.isRtl()
587
+ ? Math.round(event.clientX - this.resizeReferencePosition)
588
+ : Math.round(this.resizeReferencePosition - event.clientX);
589
+
590
+ width = Math.max(ModalElement.RESIZE_MIN_WIDTH, Math.min(width, maxWidth));
591
+ this.modalWidth = width;
592
+ this.dialog.style.setProperty('--typo3-modal-width', `${width}px`);
593
+ };
594
+
595
+ private readonly handlePointerUp = (event: PointerEvent) => {
596
+ const target = event.currentTarget as HTMLElement;
597
+ target.removeEventListener('pointermove', this.handlePointerMove);
598
+ target.removeEventListener('pointerup', this.handlePointerUp);
599
+ target.removeEventListener('pointercancel', this.handlePointerUp);
600
+ target.removeEventListener('lostpointercapture', this.handlePointerUp);
601
+ this.resizing = false;
602
+ this.persistWidth();
603
+ };
483
604
  }
484
605
 
485
606
  declare global {
@@ -517,7 +638,8 @@ class Modal {
517
638
  ajaxCallback: null,
518
639
  staticBackdrop: false,
519
640
  hideCloseButton: false,
520
- hideHeader: false
641
+ hideHeader: false,
642
+ resizeIdentifier: '',
521
643
  };
522
644
 
523
645
  constructor() {
@@ -691,6 +813,9 @@ class Modal {
691
813
  configuration.hideCloseButton = configuration.hideCloseButton || this.defaultConfiguration.hideCloseButton;
692
814
  configuration.staticBackdrop = configuration.staticBackdrop || this.defaultConfiguration.staticBackdrop;
693
815
  configuration.hideHeader = configuration.hideHeader || this.defaultConfiguration.hideHeader;
816
+ configuration.resizeIdentifier = typeof configuration.resizeIdentifier === 'string'
817
+ ? configuration.resizeIdentifier
818
+ : this.defaultConfiguration.resizeIdentifier;
694
819
 
695
820
  return this.generate(configuration);
696
821
  }
@@ -813,6 +938,7 @@ class Modal {
813
938
  currentModal.hideCloseButton = configuration.hideCloseButton;
814
939
  currentModal.staticBackdrop = configuration.staticBackdrop;
815
940
  currentModal.hideHeader = configuration.hideHeader;
941
+ currentModal.resizeIdentifier = configuration.resizeIdentifier;
816
942
  if (configuration.callback) {
817
943
  currentModal.callback = configuration.callback;
818
944
  }
@@ -821,11 +947,10 @@ class Modal {
821
947
  }
822
948
 
823
949
  currentModal.addEventListener('typo3-modal-shown', (): void => {
824
- // focus the button which was configured as active button
825
- const activeButton = currentModal.querySelector(`${Identifiers.footer} .t3js-active`) as HTMLInputElement | null;
826
- if (activeButton !== null) {
827
- activeButton.focus();
828
- }
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();
829
954
  });
830
955
 
831
956
  // Remove modal from Modal.instances when hidden
@@ -834,10 +959,15 @@ class Modal {
834
959
  const lastIndex = this.instances.length - 1;
835
960
  this.instances.splice(lastIndex, 1);
836
961
  this.currentModal = this.instances[lastIndex - 1];
962
+ remountAlertContainer(this.currentModal);
837
963
  }
838
964
  });
839
965
 
840
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
+ }
841
971
  currentModal.remove();
842
972
  });
843
973
 
@@ -847,12 +977,38 @@ class Modal {
847
977
  this.instances.push(currentModal);
848
978
  });
849
979
 
980
+ currentModal.addEventListener('typo3-modal-shown', (): void => {
981
+ remountAlertContainer(currentModal);
982
+ });
983
+
850
984
  document.body.appendChild(currentModal);
851
985
 
852
986
  return currentModal;
853
987
  }
854
988
  }
855
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
+
856
1012
  let modalObject: Modal = null;
857
1013
  try {
858
1014
  if (parent && parent.window.TYPO3 && parent.window.TYPO3.Modal) {