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 +1 -1
- package/README.md +1 -1
- package/backend/column-selector-button.ts +5 -2
- package/backend/drag-uploader.ts +78 -86
- package/backend/form-engine/element/online-media-form-element.ts +3 -3
- package/backend/form-engine.ts +8 -12
- 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/login-refresh.ts +1 -9
- package/backend/modal.ts +38 -5
- 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/modal-test.ts +91 -0
- package/backend/tests/notification-test.ts +176 -0
- package/backend/tree/page-tree-element.ts +8 -1
- 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/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/preview.ts +39 -0
- package/workspaces/renderable/record-table.ts +94 -77
package/backend/notification.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* The TYPO3 project - inspiring people to share!
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { LitElement, html, type TemplateResult } from 'lit';
|
|
14
|
+
import { LitElement, html, nothing, type TemplateResult } from 'lit';
|
|
15
15
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
16
16
|
import { classMap } from 'lit/directives/class-map.js';
|
|
17
17
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
@@ -119,16 +119,20 @@ class Notification {
|
|
|
119
119
|
if (this.messageContainer === null || document.getElementById('alert-container') === null) {
|
|
120
120
|
this.messageContainer = document.createElement('div');
|
|
121
121
|
this.messageContainer.setAttribute('id', 'alert-container');
|
|
122
|
+
this.messageContainer.setAttribute('popover', 'manual');
|
|
122
123
|
this.notificationList = document.createElement('div');
|
|
123
124
|
this.notificationList.setAttribute('class', 'alert-list');
|
|
124
125
|
// Enable focusing for keyboard scrolling (accessibility)
|
|
125
126
|
this.notificationList.setAttribute('tabindex', '0');
|
|
127
|
+
this.notificationList.setAttribute('aria-live', 'polite');
|
|
126
128
|
this.messageContainer.appendChild(this.notificationList);
|
|
127
129
|
|
|
128
130
|
this.clearAllButton = <ClearNotificationMessages>document.createElement('typo3-notification-clear-all');
|
|
129
131
|
this.containerItemVisibility();
|
|
130
132
|
this.messageContainer.prepend(this.clearAllButton);
|
|
131
|
-
document.
|
|
133
|
+
const target = document.querySelector('typo3-backend-modal:last-of-type .alert-container') ?? document.body;
|
|
134
|
+
target.appendChild(this.messageContainer);
|
|
135
|
+
this.messageContainer.showPopover();
|
|
132
136
|
|
|
133
137
|
document.addEventListener('typo3-notification-open', () => {
|
|
134
138
|
this.totalNotifications++;
|
|
@@ -165,7 +169,20 @@ class Notification {
|
|
|
165
169
|
|
|
166
170
|
protected static containerItemVisibility() {
|
|
167
171
|
this.clearAllButton.hidden = this.totalNotifications < this.showClearAllButtonCount;
|
|
168
|
-
|
|
172
|
+
try {
|
|
173
|
+
// Always hide first to ensure we are moved into the foremost top-layer
|
|
174
|
+
this.messageContainer.hidePopover();
|
|
175
|
+
} catch (e: unknown) {
|
|
176
|
+
if (e instanceof DOMException && e.name === 'InvalidStateError') {
|
|
177
|
+
// Ignored, since popover does not signal showing state,
|
|
178
|
+
// but may throw when `hidePopover()` is called on a hidden element
|
|
179
|
+
} else {
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (this.totalNotifications > 0) {
|
|
184
|
+
this.messageContainer.showPopover();
|
|
185
|
+
}
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
|
|
@@ -204,6 +221,13 @@ export class NotificationMessage extends LitElement {
|
|
|
204
221
|
|
|
205
222
|
@state() executingAction: number = -1;
|
|
206
223
|
|
|
224
|
+
private dismissTimer: number | null = null;
|
|
225
|
+
private remainingDuration: number = 0;
|
|
226
|
+
private timerStartedAt: number = 0;
|
|
227
|
+
private dismissalArmed: boolean = false;
|
|
228
|
+
private pointerInside: boolean = false;
|
|
229
|
+
private focusInside: boolean = false;
|
|
230
|
+
|
|
207
231
|
public override async firstUpdated(): Promise<void> {
|
|
208
232
|
document.addEventListener('typo3-notification-clear-all', async () => {
|
|
209
233
|
this.clear();
|
|
@@ -212,15 +236,42 @@ export class NotificationMessage extends LitElement {
|
|
|
212
236
|
const event = new CustomEvent('typo3-notification-open', { bubbles: true, composed: true });
|
|
213
237
|
this.dispatchEvent(event);
|
|
214
238
|
|
|
239
|
+
if (this.notificationDuration > 0) {
|
|
240
|
+
// Reading a message or reaching its actions takes the time it takes, so the countdown
|
|
241
|
+
// pauses while the notification is pointed at or focused. This is tracked from the start,
|
|
242
|
+
// the pointer may already rest where the notification appears.
|
|
243
|
+
this.remainingDuration = this.notificationDuration * 1000;
|
|
244
|
+
this.addEventListener('mouseenter', (): void => {
|
|
245
|
+
this.pointerInside = true;
|
|
246
|
+
this.pauseDismissal();
|
|
247
|
+
});
|
|
248
|
+
this.addEventListener('focusin', (): void => {
|
|
249
|
+
this.focusInside = true;
|
|
250
|
+
this.pauseDismissal();
|
|
251
|
+
});
|
|
252
|
+
this.addEventListener('mouseleave', (): void => {
|
|
253
|
+
this.pointerInside = false;
|
|
254
|
+
this.resumeDismissal();
|
|
255
|
+
});
|
|
256
|
+
this.addEventListener('focusout', (event: FocusEvent): void => {
|
|
257
|
+
this.focusInside = event.relatedTarget instanceof Node && this.contains(event.relatedTarget);
|
|
258
|
+
this.resumeDismissal();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
215
262
|
await new Promise(resolve => window.setTimeout(resolve, 200));
|
|
216
263
|
await this.requestUpdate();
|
|
217
264
|
if (this.notificationDuration > 0) {
|
|
218
|
-
|
|
219
|
-
this.
|
|
265
|
+
this.dismissalArmed = true;
|
|
266
|
+
this.resumeDismissal();
|
|
220
267
|
}
|
|
221
268
|
}
|
|
222
269
|
|
|
223
270
|
public async clear(): Promise<void> {
|
|
271
|
+
if (this.dismissTimer !== null) {
|
|
272
|
+
window.clearTimeout(this.dismissTimer);
|
|
273
|
+
this.dismissTimer = null;
|
|
274
|
+
}
|
|
224
275
|
this.dispatchEvent(
|
|
225
276
|
new CustomEvent('typo3-notification-clear', { bubbles: true, composed: true })
|
|
226
277
|
);
|
|
@@ -307,12 +358,18 @@ export class NotificationMessage extends LitElement {
|
|
|
307
358
|
${this.actions.map((action, index) => html`
|
|
308
359
|
<a href="#"
|
|
309
360
|
title="${action.label}"
|
|
361
|
+
aria-disabled="${this.executingAction >= 0 ? 'true' : nothing}"
|
|
310
362
|
@click="${async (event: PointerEvent) => {
|
|
311
363
|
event.preventDefault();
|
|
364
|
+
// Pointer events are disabled while executing, keyboard activation is not
|
|
365
|
+
if (this.executingAction >= 0) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const target = event.currentTarget as HTMLAnchorElement;
|
|
312
369
|
this.executingAction = index;
|
|
313
370
|
await this.updateComplete;
|
|
314
371
|
if ('action' in action) {
|
|
315
|
-
await action.action.execute(
|
|
372
|
+
await action.action.execute(target);
|
|
316
373
|
}
|
|
317
374
|
this.clear();
|
|
318
375
|
}}"
|
|
@@ -328,6 +385,33 @@ export class NotificationMessage extends LitElement {
|
|
|
328
385
|
`;
|
|
329
386
|
/* eslint-enable @stylistic/indent */
|
|
330
387
|
}
|
|
388
|
+
|
|
389
|
+
private pauseDismissal(): void {
|
|
390
|
+
if (this.dismissTimer === null) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
window.clearTimeout(this.dismissTimer);
|
|
394
|
+
this.dismissTimer = null;
|
|
395
|
+
this.remainingDuration -= Date.now() - this.timerStartedAt;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private resumeDismissal(): void {
|
|
399
|
+
if (!this.dismissalArmed || this.dismissTimer !== null || !this.isConnected || this.pointerInside || this.focusInside) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.startDismissal();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private startDismissal(): void {
|
|
406
|
+
this.timerStartedAt = Date.now();
|
|
407
|
+
this.dismissTimer = window.setTimeout((): void => {
|
|
408
|
+
this.dismissTimer = null;
|
|
409
|
+
// An action being executed clears the notification once it is done
|
|
410
|
+
if (this.executingAction < 0) {
|
|
411
|
+
this.clear();
|
|
412
|
+
}
|
|
413
|
+
}, Math.max(this.remainingDuration, 0));
|
|
414
|
+
}
|
|
331
415
|
}
|
|
332
416
|
|
|
333
417
|
declare global {
|
package/backend/online-media.ts
CHANGED
|
@@ -65,17 +65,16 @@ export class RecordDownloadButton extends PseudoButtonLitElement {
|
|
|
65
65
|
text: this.ok || listLabels.get('button.ok'),
|
|
66
66
|
btnClass: 'btn-primary',
|
|
67
67
|
name: 'download',
|
|
68
|
-
|
|
69
|
-
const form: HTMLFormElement = modal.querySelector('form');
|
|
70
|
-
form?.submit();
|
|
71
|
-
modal.hideModal();
|
|
72
|
-
}
|
|
68
|
+
form: 'downloadSettingsForm',
|
|
73
69
|
}
|
|
74
70
|
],
|
|
75
71
|
ajaxCallback: (): void => {
|
|
72
|
+
const form: HTMLFormElement = modal.querySelector('form');
|
|
76
73
|
const formatSelect: HTMLSelectElement = modal.querySelector(Selectors.formatSelector);
|
|
77
74
|
const formatOptions: NodeListOf<HTMLDivElement> = modal.querySelectorAll(Selectors.formatOptions);
|
|
78
75
|
|
|
76
|
+
form?.addEventListener('submit', (): void => modal.hideModal());
|
|
77
|
+
|
|
79
78
|
if (formatSelect === null || !formatOptions.length) {
|
|
80
79
|
// Return in case elements do not exist in the ajax loaded modal content
|
|
81
80
|
return;
|
package/backend/recordlist.ts
CHANGED
|
@@ -274,6 +274,11 @@ class Recordlist {
|
|
|
274
274
|
|
|
275
275
|
private readonly registerPaginationEvents = (): void => {
|
|
276
276
|
document.querySelectorAll('.t3js-recordlist-paging').forEach((trigger: HTMLInputElement) => {
|
|
277
|
+
trigger.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
278
|
+
if (e.key === 'Enter') {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
277
282
|
trigger.addEventListener('keyup', (e: KeyboardEvent) => {
|
|
278
283
|
e.preventDefault();
|
|
279
284
|
let value = Number(trigger.value);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<form id="modal-ajax-form"><input name="filter" autofocus></form>
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
import { html } from 'lit';
|
|
15
|
+
import { expect } from '@open-wc/testing';
|
|
16
|
+
import Modal, { type ModalElement } from '@typo3/backend/modal.js';
|
|
17
|
+
import type { } from 'mocha';
|
|
18
|
+
|
|
19
|
+
describe('@typo3/backend/modal', () => {
|
|
20
|
+
let modal: ModalElement;
|
|
21
|
+
const nativeRequestAnimationFrame = window.requestAnimationFrame;
|
|
22
|
+
|
|
23
|
+
const shown = (element: ModalElement): Promise<void> => new Promise(resolve => {
|
|
24
|
+
element.addEventListener('typo3-modal-shown', () => resolve(), { once: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
before(() => {
|
|
28
|
+
// The test page runs in the background, animation frames never fire there
|
|
29
|
+
window.requestAnimationFrame = (callback: FrameRequestCallback): number => window.setTimeout(() => callback(performance.now()), 0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
after(() => {
|
|
33
|
+
window.requestAnimationFrame = nativeRequestAnimationFrame;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// The dialog close event does not fire in the background either
|
|
38
|
+
modal?.hideModal();
|
|
39
|
+
modal?.remove();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('binds a footer button to the form given by its configuration', async () => {
|
|
43
|
+
modal = Modal.advanced({
|
|
44
|
+
content: html`<form id="modal-test-form"><input name="username"><input name="password"></form>`,
|
|
45
|
+
buttons: [{ text: 'ok', btnClass: 'btn-primary', form: 'modal-test-form' }],
|
|
46
|
+
});
|
|
47
|
+
await shown(modal);
|
|
48
|
+
|
|
49
|
+
const button = modal.querySelector<HTMLButtonElement>('.modal-footer button');
|
|
50
|
+
expect(button.type).to.equal('submit');
|
|
51
|
+
expect(button.form?.id).to.equal('modal-test-form');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps the focus on an autofocus field instead of the active button', async () => {
|
|
55
|
+
modal = Modal.advanced({
|
|
56
|
+
content: html`<form><input name="password" autofocus></form>`,
|
|
57
|
+
buttons: [{ text: 'ok', btnClass: 'btn-primary', active: true }],
|
|
58
|
+
});
|
|
59
|
+
await shown(modal);
|
|
60
|
+
|
|
61
|
+
expect(document.activeElement.matches('input[name="password"]')).to.be.true;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('focuses an autofocus field of ajax content loaded after the modal was shown', async () => {
|
|
65
|
+
const nativeFetch = window.fetch;
|
|
66
|
+
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
67
|
+
await shown(modal);
|
|
68
|
+
return nativeFetch(input, init);
|
|
69
|
+
};
|
|
70
|
+
modal = Modal.advanced({
|
|
71
|
+
type: Modal.types.ajax,
|
|
72
|
+
content: '/Build/Sources/TypeScript/backend/tests/fixtures/modal-autofocus.html',
|
|
73
|
+
buttons: [{ text: 'ok', btnClass: 'btn-primary', active: true }],
|
|
74
|
+
});
|
|
75
|
+
const loaded = new Promise<void>(resolve => modal.addEventListener('modal-loaded', () => resolve(), { once: true }));
|
|
76
|
+
await loaded;
|
|
77
|
+
window.fetch = nativeFetch;
|
|
78
|
+
|
|
79
|
+
expect(document.activeElement.matches('input[name="filter"]')).to.be.true;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('focuses the active button when the body has no autofocus field', async () => {
|
|
83
|
+
modal = Modal.advanced({
|
|
84
|
+
content: html`<form><input name="password"></form>`,
|
|
85
|
+
buttons: [{ text: 'ok', btnClass: 'btn-primary', active: true }],
|
|
86
|
+
});
|
|
87
|
+
await shown(modal);
|
|
88
|
+
|
|
89
|
+
expect(document.activeElement.matches('.modal-footer button')).to.be.true;
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -106,6 +106,124 @@ describe('@typo3/backend/notification:', () => {
|
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
describe('auto dismissal', () => {
|
|
110
|
+
async function showNotification(): Promise<{ element: LitElement, isCleared: () => boolean }> {
|
|
111
|
+
Notification.success('Title', 'Message', 1);
|
|
112
|
+
const element = document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement;
|
|
113
|
+
await element.updateComplete;
|
|
114
|
+
let cleared = false;
|
|
115
|
+
element.addEventListener('typo3-notification-clear', (): void => {
|
|
116
|
+
cleared = true;
|
|
117
|
+
});
|
|
118
|
+
// Notifications appear with a delay, the countdown starts afterwards
|
|
119
|
+
await clock.tickAsync(200);
|
|
120
|
+
return { element, isCleared: (): boolean => cleared };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
it('pauses while the notification is pointed at', async () => {
|
|
124
|
+
const { element, isCleared } = await showNotification();
|
|
125
|
+
|
|
126
|
+
element.dispatchEvent(new MouseEvent('mouseenter'));
|
|
127
|
+
await clock.tickAsync(5000);
|
|
128
|
+
expect(isCleared()).to.be.false;
|
|
129
|
+
|
|
130
|
+
element.dispatchEvent(new MouseEvent('mouseleave'));
|
|
131
|
+
await clock.tickAsync(1100);
|
|
132
|
+
expect(isCleared()).to.be.true;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('pauses while the notification has the focus', async () => {
|
|
136
|
+
const { element, isCleared } = await showNotification();
|
|
137
|
+
|
|
138
|
+
element.dispatchEvent(new FocusEvent('focusin'));
|
|
139
|
+
await clock.tickAsync(5000);
|
|
140
|
+
expect(isCleared()).to.be.false;
|
|
141
|
+
|
|
142
|
+
element.dispatchEvent(new FocusEvent('focusout'));
|
|
143
|
+
await clock.tickAsync(1100);
|
|
144
|
+
expect(isCleared()).to.be.true;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('continues with the time that was left', async () => {
|
|
148
|
+
const { element, isCleared } = await showNotification();
|
|
149
|
+
|
|
150
|
+
await clock.tickAsync(600);
|
|
151
|
+
element.dispatchEvent(new MouseEvent('mouseenter'));
|
|
152
|
+
await clock.tickAsync(5000);
|
|
153
|
+
element.dispatchEvent(new MouseEvent('mouseleave'));
|
|
154
|
+
|
|
155
|
+
await clock.tickAsync(300);
|
|
156
|
+
expect(isCleared()).to.be.false;
|
|
157
|
+
await clock.tickAsync(200);
|
|
158
|
+
expect(isCleared()).to.be.true;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('stays paused when the pointer leaves while the focus is still inside', async () => {
|
|
162
|
+
const { element, isCleared } = await showNotification();
|
|
163
|
+
|
|
164
|
+
element.dispatchEvent(new FocusEvent('focusin'));
|
|
165
|
+
element.dispatchEvent(new MouseEvent('mouseenter'));
|
|
166
|
+
element.dispatchEvent(new MouseEvent('mouseleave'));
|
|
167
|
+
await clock.tickAsync(5000);
|
|
168
|
+
expect(isCleared()).to.be.false;
|
|
169
|
+
|
|
170
|
+
element.dispatchEvent(new FocusEvent('focusout'));
|
|
171
|
+
await clock.tickAsync(1100);
|
|
172
|
+
expect(isCleared()).to.be.true;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('stays paused when the pointer rests on the notification before it appeared', async () => {
|
|
176
|
+
Notification.success('Title', 'Message', 1);
|
|
177
|
+
const element = document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement;
|
|
178
|
+
await element.updateComplete;
|
|
179
|
+
let cleared = false;
|
|
180
|
+
element.addEventListener('typo3-notification-clear', (): void => {
|
|
181
|
+
cleared = true;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
element.dispatchEvent(new MouseEvent('mouseenter'));
|
|
185
|
+
await clock.tickAsync(5000);
|
|
186
|
+
expect(cleared).to.be.false;
|
|
187
|
+
|
|
188
|
+
element.dispatchEvent(new MouseEvent('mouseleave'));
|
|
189
|
+
await clock.tickAsync(1100);
|
|
190
|
+
expect(cleared).to.be.true;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('is not cleared by the countdown while an action is executing', async () => {
|
|
194
|
+
let finishAction: () => void;
|
|
195
|
+
Notification.success('Title', 'Message', 1, [
|
|
196
|
+
{
|
|
197
|
+
label: 'Undo',
|
|
198
|
+
action: new DeferredAction((): Promise<void> => new Promise((resolve): void => {
|
|
199
|
+
finishAction = resolve;
|
|
200
|
+
})),
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
const element = document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement;
|
|
204
|
+
await element.updateComplete;
|
|
205
|
+
let cleared = false;
|
|
206
|
+
element.addEventListener('typo3-notification-clear', (): void => {
|
|
207
|
+
cleared = true;
|
|
208
|
+
});
|
|
209
|
+
await clock.tickAsync(200);
|
|
210
|
+
|
|
211
|
+
(element.querySelector('.alert-actions a') as HTMLAnchorElement).click();
|
|
212
|
+
await clock.tickAsync(5000);
|
|
213
|
+
expect(cleared).to.be.false;
|
|
214
|
+
|
|
215
|
+
finishAction();
|
|
216
|
+
await clock.tickAsync(0);
|
|
217
|
+
expect(cleared).to.be.true;
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('announces notifications to assistive technology', async () => {
|
|
222
|
+
Notification.info('Info message', 'Some text', 1);
|
|
223
|
+
await (document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement).updateComplete;
|
|
224
|
+
expect(document.querySelector('#alert-container .alert-list').getAttribute('aria-live')).to.equal('polite');
|
|
225
|
+
});
|
|
226
|
+
|
|
109
227
|
it('can render action buttons', async () => {
|
|
110
228
|
Notification.info(
|
|
111
229
|
'Info message',
|
|
@@ -158,4 +276,62 @@ describe('@typo3/backend/notification:', () => {
|
|
|
158
276
|
await (document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement).updateComplete;
|
|
159
277
|
expect(called).to.be.true;
|
|
160
278
|
});
|
|
279
|
+
|
|
280
|
+
it('deferred action is called with its link', async () => {
|
|
281
|
+
let called = false;
|
|
282
|
+
Notification.info('Info message', 'Some text', 0, [
|
|
283
|
+
{
|
|
284
|
+
label: 'My deferred action',
|
|
285
|
+
action: new DeferredAction(async (): Promise<void> => {
|
|
286
|
+
called = true;
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
]);
|
|
290
|
+
const element = document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement;
|
|
291
|
+
await element.updateComplete;
|
|
292
|
+
|
|
293
|
+
(element.querySelector('.alert-actions a') as HTMLAnchorElement).click();
|
|
294
|
+
await clock.tickAsync(0);
|
|
295
|
+
|
|
296
|
+
expect(called).to.be.true;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('executes an action only once while it is executing', async () => {
|
|
300
|
+
let calls = 0;
|
|
301
|
+
let finishAction: () => void;
|
|
302
|
+
Notification.info('Info message', 'Some text', 0, [
|
|
303
|
+
{
|
|
304
|
+
label: 'Deferred action',
|
|
305
|
+
action: new DeferredAction((): Promise<void> => {
|
|
306
|
+
calls++;
|
|
307
|
+
return new Promise((resolve): void => {
|
|
308
|
+
finishAction = resolve;
|
|
309
|
+
});
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
label: 'Other action',
|
|
314
|
+
action: new ImmediateAction((): void => {
|
|
315
|
+
calls++;
|
|
316
|
+
}),
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
const element = document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement;
|
|
320
|
+
await element.updateComplete;
|
|
321
|
+
const [first, second] = Array.from(element.querySelectorAll('.alert-actions a')) as HTMLAnchorElement[];
|
|
322
|
+
|
|
323
|
+
first.click();
|
|
324
|
+
await clock.tickAsync(0);
|
|
325
|
+
// Keyboard activation still reaches a link which ignores the pointer
|
|
326
|
+
first.click();
|
|
327
|
+
second.click();
|
|
328
|
+
await clock.tickAsync(0);
|
|
329
|
+
expect(calls).to.equal(1);
|
|
330
|
+
expect(first.getAttribute('aria-disabled')).to.equal('true');
|
|
331
|
+
expect(second.getAttribute('aria-disabled')).to.equal('true');
|
|
332
|
+
|
|
333
|
+
finishAction();
|
|
334
|
+
await clock.tickAsync(0);
|
|
335
|
+
expect(calls).to.equal(1);
|
|
336
|
+
});
|
|
161
337
|
});
|
|
@@ -110,7 +110,9 @@ export class EditablePageTree extends PageTree {
|
|
|
110
110
|
'&data[pages][' + data.node.identifier + '][title]=' + encodeURIComponent(newData.title) +
|
|
111
111
|
'&data[pages][' + data.node.identifier + '][doktype]=' + encodeURIComponent(newData.doktype);
|
|
112
112
|
} else if (data.command === TreeNodeCommandEnum.EDIT) {
|
|
113
|
-
|
|
113
|
+
// The tree may show nav_title instead of title, so write back to the field that is displayed
|
|
114
|
+
const nameSourceField = data.node.nameSourceField || 'title';
|
|
115
|
+
params = '&data[pages][' + data.node.identifier + '][' + nameSourceField + ']=' + encodeURIComponent(data.title);
|
|
114
116
|
} else if (data.command === TreeNodeCommandEnum.DELETE) {
|
|
115
117
|
// @todo currently it's "If uid of deleted record (data.uid) is still selected, randomly select the first node"
|
|
116
118
|
const moduleStateStorage = ModuleStateStorage.current('web');
|
|
@@ -245,6 +247,11 @@ export class EditablePageTree extends PageTree {
|
|
|
245
247
|
command: TreeNodeCommandEnum.MOVE
|
|
246
248
|
};
|
|
247
249
|
|
|
250
|
+
if (!this.settings.displayCopyMoveConfirmation) {
|
|
251
|
+
this.sendChangeCommand(options);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
248
255
|
let modalText = '';
|
|
249
256
|
const languageArguments = [node.name, target.name] as const;
|
|
250
257
|
switch(position) {
|
package/dashboard/dashboard.ts
CHANGED
|
@@ -565,10 +565,10 @@ export class Dashboard extends LitElement {
|
|
|
565
565
|
|
|
566
566
|
private editDashboard(dashboard: DashboardInterface): void {
|
|
567
567
|
const content = html`
|
|
568
|
-
<form>
|
|
568
|
+
<form id="dashboard-edit-form">
|
|
569
569
|
<div class="form-group">
|
|
570
570
|
<label class="form-label" for="dashboard-form-edit-title">${labels.get('dashboard.title')}</label>
|
|
571
|
-
<input class="form-control" id="dashboard-form-edit-title" type="text" name="title" value=${dashboard.title || ''} required="required">
|
|
571
|
+
<input class="form-control" id="dashboard-form-edit-title" type="text" name="title" value=${dashboard.title || ''} required="required" autofocus>
|
|
572
572
|
</div>
|
|
573
573
|
</form>
|
|
574
574
|
`;
|
|
@@ -580,11 +580,6 @@ export class Dashboard extends LitElement {
|
|
|
580
580
|
severity: SeverityEnum.notice,
|
|
581
581
|
content,
|
|
582
582
|
callback: (currentModal: ModalElement): void => {
|
|
583
|
-
|
|
584
|
-
currentModal.addEventListener('typo3-modal-shown', (): void => {
|
|
585
|
-
(currentModal.querySelector('#dashboard-form-edit-title') as HTMLInputElement)?.focus();
|
|
586
|
-
});
|
|
587
|
-
|
|
588
583
|
currentModal.querySelector('form').addEventListener('submit', (e: Event): void => {
|
|
589
584
|
e.preventDefault();
|
|
590
585
|
const form = e.target as HTMLFormElement;
|
|
@@ -608,7 +603,7 @@ export class Dashboard extends LitElement {
|
|
|
608
603
|
text: labels.get('dashboard.configure.button.ok'),
|
|
609
604
|
btnClass: 'btn-primary',
|
|
610
605
|
name: 'save',
|
|
611
|
-
|
|
606
|
+
form: 'dashboard-edit-form',
|
|
612
607
|
},
|
|
613
608
|
]
|
|
614
609
|
});
|
|
@@ -51,10 +51,7 @@ class FileListRenameHandler {
|
|
|
51
51
|
text: labels.get('file_rename.button.rename'),
|
|
52
52
|
btnClass: 'btn-primary',
|
|
53
53
|
name: 'rename',
|
|
54
|
-
|
|
55
|
-
const form: HTMLFormElement = modal.querySelector('form');
|
|
56
|
-
form?.requestSubmit();
|
|
57
|
-
},
|
|
54
|
+
form: 'file-list-rename-form',
|
|
58
55
|
},
|
|
59
56
|
],
|
|
60
57
|
callback: function (modal: ModalElement) {
|
|
@@ -119,11 +116,11 @@ class FileListRenameHandler {
|
|
|
119
116
|
labels.get('folder_rename.label') :
|
|
120
117
|
labels.get('file_rename.label');
|
|
121
118
|
return html`
|
|
122
|
-
<form>
|
|
119
|
+
<form id="file-list-rename-form">
|
|
123
120
|
<label class="form-label" for="rename_target">
|
|
124
121
|
${label}
|
|
125
122
|
</label>
|
|
126
|
-
<input id="rename_target" name="name" class="form-control" value="${resource.name}" required>
|
|
123
|
+
<input id="rename_target" name="name" class="form-control" value="${resource.name}" required autofocus>
|
|
127
124
|
</form>
|
|
128
125
|
`;
|
|
129
126
|
}
|
|
@@ -57,10 +57,7 @@ class FileListReplaceHandler {
|
|
|
57
57
|
text: labels.get('file_replace.button.replace'),
|
|
58
58
|
btnClass: 'btn-primary',
|
|
59
59
|
name: 'rename',
|
|
60
|
-
|
|
61
|
-
const form: HTMLFormElement = modal.querySelector('form');
|
|
62
|
-
form?.requestSubmit();
|
|
63
|
-
},
|
|
60
|
+
form: 'file-list-replace-form',
|
|
64
61
|
},
|
|
65
62
|
],
|
|
66
63
|
callback: function (modal: ModalElement) {
|
|
@@ -126,10 +123,10 @@ class FileListReplaceHandler {
|
|
|
126
123
|
</dl>
|
|
127
124
|
</div>
|
|
128
125
|
</div>
|
|
129
|
-
<form>
|
|
126
|
+
<form id="file-list-replace-form">
|
|
130
127
|
<div class="form-group">
|
|
131
128
|
<label class="form-label" for="file_replace">${labels.get('file_replace.new_file.label')}</label>
|
|
132
|
-
<input id="file_replace" type="file" class="form-control" name="replace_1">
|
|
129
|
+
<input id="file_replace" type="file" class="form-control" name="replace_1" autofocus>
|
|
133
130
|
</div>
|
|
134
131
|
<div class="form-check">
|
|
135
132
|
<input type="checkbox" value="1" id="keepFilename" name="keepFilename" class="form-check-input" checked>
|
package/filelist/file-list.ts
CHANGED
|
@@ -422,10 +422,9 @@ export default class Filelist {
|
|
|
422
422
|
}
|
|
423
423
|
trigger.value = value.toString(10);
|
|
424
424
|
if (e.key === 'Enter' && value !== Number(trigger.dataset.currentpage)) {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
window.location.href = submitUrl.toString();
|
|
425
|
+
const pagingUrl = new URL(trigger.dataset.pagingUrl, window.origin);
|
|
426
|
+
pagingUrl.searchParams.set('currentPage', value.toString());
|
|
427
|
+
window.location.href = pagingUrl.toString();
|
|
429
428
|
}
|
|
430
429
|
});
|
|
431
430
|
});
|