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.
@@ -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.body.appendChild(this.messageContainer);
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
- this.messageContainer.hidden = this.totalNotifications === 0;
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
- await new Promise(resolve => window.setTimeout(resolve, this.notificationDuration * 1000));
219
- this.clear();
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(event.currentTarget as HTMLAnchorElement);
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 {
@@ -106,9 +106,7 @@ class OnlineMedia {
106
106
  text: btnSubmit,
107
107
  btnClass: 'btn btn-primary',
108
108
  name: 'ok',
109
- trigger: (): void => {
110
- onlineMediaForm.querySelector('form').requestSubmit();
111
- },
109
+ form: 'online-media-form',
112
110
  }],
113
111
  });
114
112
  }
@@ -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
- trigger: (): void => {
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;
@@ -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
- params = '&data[pages][' + data.node.identifier + '][title]=' + encodeURIComponent(data.title);
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) {
@@ -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
- trigger: (e, modal) => modal.querySelector('form').requestSubmit(),
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
- trigger: (): void => {
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
- trigger: (): void => {
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>
@@ -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 form = trigger.closest('form[name="fileListForm"]') as HTMLFormElement;
426
- const submitUrl = new URL(form.action, window.origin);
427
- submitUrl.searchParams.set('currentPage', value.toString());
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
  });