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.
- package/.last-synced-tag +1 -1
- package/README-TYPO3.md +1 -1
- package/README.md +1 -1
- package/backend/column-selector-button.ts +5 -2
- package/backend/drag-uploader.ts +78 -86
- package/backend/element/contextual-record-edit-trigger.ts +9 -1
- package/backend/form-engine/element/extra/select-box-filter.ts +8 -5
- package/backend/form-engine/element/online-media-form-element.ts +3 -3
- package/backend/form-engine/element/select-tree.ts +16 -2
- package/backend/form-engine/field-control/add-record.ts +27 -1
- package/backend/form-engine.ts +88 -13
- package/backend/layout-module/column-restriction.ts +61 -0
- package/backend/layout-module/drag-drop.ts +12 -0
- package/backend/layout-module/paste.ts +7 -0
- package/backend/localization/localization-wizard.ts +3 -0
- package/backend/localization/steps/handler-selection-step.ts +12 -17
- package/backend/localization/steps/mode-step.ts +9 -14
- package/backend/localization/steps/source-language-step.ts +8 -13
- package/backend/localization/steps/target-language-step.ts +12 -17
- package/backend/login-refresh.ts +1 -9
- package/backend/modal.ts +162 -6
- package/backend/notification.ts +90 -6
- package/backend/online-media.ts +1 -3
- package/backend/record-download-button.ts +4 -5
- package/backend/recordlist.ts +5 -0
- package/backend/tests/fixtures/modal-autofocus.html +1 -0
- package/backend/tests/form-engine/element/extra/select-box-filter-test.ts +215 -0
- package/backend/tests/form-engine/element/select-tree-test.ts +121 -0
- package/backend/tests/modal-test.ts +91 -0
- package/backend/tests/notification-test.ts +176 -0
- package/backend/tree/page-tree-element.ts +8 -1
- package/backend/wizard/steps/finisher-step.ts +2 -2
- package/backend/wizard/wizard.ts +11 -4
- package/core/ajax/ajax-request.ts +18 -0
- package/core/tests/ajax/ajax-request-test.ts +1 -1
- package/core/tests/utility/text-normalizer-test.ts +138 -0
- package/core/utility/text-normalizer.ts +98 -0
- package/dashboard/dashboard.ts +3 -8
- package/filelist/file-list-rename-handler.ts +3 -6
- package/filelist/file-list-replace-handler.ts +3 -6
- package/filelist/file-list.ts +3 -4
- package/form/backend/form-editor/modals-component.ts +114 -5
- package/form/backend/form-editor/view-model.ts +1 -1
- package/form/backend/form-wizard/steps/settings-step.ts +6 -3
- package/package.json +2 -2
- package/rte-ckeditor/legacy/ckeditor5-bundle.ts +0 -1
- package/rte-ckeditor/plugin/typo3-link.ts +7 -9
- package/rte-ckeditor/rte-link-browser.ts +1 -1
- package/scheduler/scheduler-add-group.ts +4 -12
- package/workspaces/backend.ts +7 -2
- package/workspaces/preview.ts +39 -0
- package/workspaces/renderable/comment-view.ts +4 -2
- package/workspaces/renderable/record-table.ts +134 -80
- 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
package/backend/login-refresh.ts
CHANGED
|
@@ -284,15 +284,7 @@ class LoginRefresh {
|
|
|
284
284
|
active: false,
|
|
285
285
|
btnClass: 'btn-primary',
|
|
286
286
|
name: 'refreshSession',
|
|
287
|
-
|
|
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
|
-
//
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
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) {
|