typo3-typescript-sources 14.3.7 → 14.3.9

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 (189) hide show
  1. package/.github/workflows/sync-and-publish.yml +240 -191
  2. package/.last-synced-tag +1 -1
  3. package/README.md +13 -5
  4. package/adminpanel/admin-panel.ts +22 -22
  5. package/adminpanel/modules/cache.ts +3 -3
  6. package/backend/action-dispatcher.ts +8 -3
  7. package/backend/bookmark/bookmark-store.ts +5 -5
  8. package/backend/clipboard-panel.ts +2 -2
  9. package/backend/code-editor/element/code-mirror-element.ts +1 -1
  10. package/backend/code-editor/language/typoscript.ts +3 -3
  11. package/backend/code-editor/stream-parser/typoscript.ts +2 -2
  12. package/backend/color-picker.ts +2 -2
  13. package/backend/column-selector-button.ts +11 -8
  14. package/backend/context-help.ts +3 -3
  15. package/backend/context-menu.ts +8 -3
  16. package/backend/drag-tooltip.ts +9 -4
  17. package/backend/drag-uploader.ts +81 -89
  18. package/backend/element/draggable-resizable-element.ts +18 -12
  19. package/backend/element/icon-element.ts +2 -2
  20. package/backend/element/thumbnail-element.ts +2 -2
  21. package/backend/element-browser.ts +1 -1
  22. package/backend/event/consumer-scope.ts +2 -2
  23. package/backend/event/interaction-request-assignment.ts +1 -1
  24. package/backend/event/interaction-request-map.ts +3 -3
  25. package/backend/form-engine/container/flex-form-container-container.ts +2 -2
  26. package/backend/form-engine/container/flex-form-section-container.ts +1 -1
  27. package/backend/form-engine/container/inline-control-container.ts +14 -14
  28. package/backend/form-engine/element/abstract-sortable-select-items.ts +3 -3
  29. package/backend/form-engine/element/color-element.ts +1 -1
  30. package/backend/form-engine/element/datetime-element.ts +1 -1
  31. package/backend/form-engine/element/extra/char-counter.ts +2 -2
  32. package/backend/form-engine/element/extra/select-box-filter.ts +1 -1
  33. package/backend/form-engine/element/mfa-info-element.ts +2 -2
  34. package/backend/form-engine/element/online-media-form-element.ts +8 -4
  35. package/backend/form-engine/element/select-multiple-side-by-side-element.ts +2 -2
  36. package/backend/form-engine/element/select-tree.ts +3 -3
  37. package/backend/form-engine/element/slug-element.ts +7 -7
  38. package/backend/form-engine/element/suggest/result-container.ts +1 -1
  39. package/backend/form-engine/element/table-wizard-element.ts +4 -4
  40. package/backend/form-engine/inline-relation/ajax-dispatcher.ts +3 -3
  41. package/backend/form-engine-validation.ts +11 -5
  42. package/backend/form-engine.ts +19 -23
  43. package/backend/global-event-handler.ts +1 -1
  44. package/backend/hotkeys/hotkey-struct.ts +1 -1
  45. package/backend/hotkeys.ts +3 -3
  46. package/backend/image-manipulation.ts +19 -19
  47. package/backend/layout-module/column-restriction.ts +61 -0
  48. package/backend/layout-module/drag-drop.ts +16 -4
  49. package/backend/layout-module/paste.ts +8 -1
  50. package/backend/live-search/element/result/item/action/action-container.ts +1 -1
  51. package/backend/live-search/element/result/item/item-container.ts +4 -4
  52. package/backend/live-search/element/result/result-container.ts +5 -0
  53. package/backend/live-search/element/result/result-pagination.ts +1 -1
  54. package/backend/live-search/live-search.ts +2 -2
  55. package/backend/localization/label-provider.ts +4 -4
  56. package/backend/localization/localization-wizard.ts +4 -4
  57. package/backend/localization/steps/content-record-selection-step.ts +8 -8
  58. package/backend/localization/steps/handler-selection-step.ts +4 -4
  59. package/backend/localization/steps/mode-step.ts +4 -4
  60. package/backend/localization/steps/source-language-step.ts +4 -4
  61. package/backend/localization/steps/target-language-step.ts +4 -4
  62. package/backend/login-refresh.ts +1 -9
  63. package/backend/login.ts +2 -2
  64. package/backend/modal.ts +38 -5
  65. package/backend/module/iframe.ts +10 -0
  66. package/backend/module/router.ts +10 -0
  67. package/backend/module-menu.ts +3 -3
  68. package/backend/multi-record-selection-action.ts +1 -1
  69. package/backend/multi-record-selection-delete-action.ts +1 -1
  70. package/backend/multi-record-selection.ts +14 -14
  71. package/backend/multi-step-wizard.ts +9 -2
  72. package/backend/new-multiple-pages.ts +2 -2
  73. package/backend/new-record-wizard.ts +15 -15
  74. package/backend/notification.ts +90 -6
  75. package/backend/online-media.ts +1 -3
  76. package/backend/page-wizard/page-wizard.ts +2 -2
  77. package/backend/page-wizard/steps/doktype-step.ts +5 -5
  78. package/backend/pagetsconfig/pagetsconfig-includes.ts +1 -1
  79. package/backend/popover.ts +5 -5
  80. package/backend/record-download-button.ts +5 -6
  81. package/backend/recordlist.ts +11 -6
  82. package/backend/security/element/csp-reports.ts +8 -8
  83. package/backend/setup-module.ts +2 -2
  84. package/backend/sortable-table.ts +10 -4
  85. package/backend/storage/abstract-client-storage.ts +2 -2
  86. package/backend/storage/module-state-storage.ts +2 -2
  87. package/backend/tests/backend/utility/dom-helper-test.ts +2 -2
  88. package/backend/tests/fixtures/modal-autofocus.html +1 -0
  89. package/backend/tests/form-engine/element/extra/select-box-filter-test.ts +3 -3
  90. package/backend/tests/form-engine-validation-test.ts +1 -1
  91. package/backend/tests/modal-test.ts +91 -0
  92. package/backend/tests/notification-test.ts +176 -0
  93. package/backend/tests/tree/tree-test.ts +41 -0
  94. package/backend/tree/file-storage-browser.ts +1 -1
  95. package/backend/tree/file-storage-tree-container.ts +4 -4
  96. package/backend/tree/page-browser.ts +1 -1
  97. package/backend/tree/page-position-select.ts +1 -1
  98. package/backend/tree/page-tree-element.ts +9 -2
  99. package/backend/tree/page-tree.ts +7 -1
  100. package/backend/tree/tree-module-state.ts +5 -5
  101. package/backend/tree/tree.ts +15 -8
  102. package/backend/user-pass-login.ts +2 -2
  103. package/backend/utility/collapse-state-persister.ts +1 -1
  104. package/backend/utility/collapse-state-search.ts +6 -6
  105. package/backend/utility/dom-helper.ts +3 -3
  106. package/backend/utility.ts +6 -6
  107. package/backend/viewport/content-navigation.ts +5 -0
  108. package/backend/viewport/navigation-container.ts +1 -1
  109. package/backend/viewport/toolbar.ts +3 -3
  110. package/backend/window-manager.ts +2 -2
  111. package/backend/wizard/events/auto-advance-event.ts +6 -0
  112. package/backend/wizard/events/before-next-step-event.ts +6 -0
  113. package/backend/wizard/events/step-summary-event.ts +6 -0
  114. package/backend/wizard/move-content-element.ts +1 -1
  115. package/backend/wizard/wizard.ts +8 -10
  116. package/belog/backend-log.ts +2 -2
  117. package/beuser/permissions.ts +2 -2
  118. package/core/ajax/ajax-response.ts +1 -1
  119. package/core/ajax/input-transformer.ts +1 -1
  120. package/core/event/debounce-event.ts +4 -4
  121. package/core/event/event-interface.ts +3 -3
  122. package/core/event/regular-event.ts +7 -7
  123. package/core/event/request-animation-frame-event.ts +4 -4
  124. package/core/event/throttle-event.ts +4 -4
  125. package/core/java-script-item-processor.ts +6 -0
  126. package/core/referrer-refresh.ts +2 -2
  127. package/core/security-utility.ts +1 -1
  128. package/dashboard/dashboard-wizard.ts +2 -2
  129. package/dashboard/dashboard.ts +8 -13
  130. package/dashboard/wizard/steps/preset-step.ts +3 -3
  131. package/extensionmanager/main.ts +7 -7
  132. package/filelist/browse-files.ts +1 -1
  133. package/filelist/browse-folders.ts +2 -2
  134. package/filelist/file-list-dragdrop.ts +5 -5
  135. package/filelist/file-list-rename-handler.ts +3 -6
  136. package/filelist/file-list-replace-handler.ts +3 -6
  137. package/filelist/file-list-transfer-handler.ts +2 -2
  138. package/filelist/file-list.ts +8 -9
  139. package/form/backend/form-editor/component/date-editor.ts +4 -0
  140. package/form/backend/form-editor/component/form-element-selector.ts +4 -0
  141. package/form/backend/form-editor/inspector-component.ts +6 -6
  142. package/form/backend/form-editor/mediator.ts +36 -36
  143. package/form/backend/form-editor/modals-component.ts +115 -6
  144. package/form/backend/form-editor/stage-component.ts +4 -4
  145. package/form/backend/form-editor/view-model.ts +7 -7
  146. package/form/backend/form-manager/view-model.ts +5 -5
  147. package/form/backend/form-wizard/form-wizard.ts +2 -2
  148. package/form/backend/form-wizard/steps/storage-step.ts +1 -1
  149. package/impexp/import-export.ts +1 -1
  150. package/info/translation-status.ts +2 -2
  151. package/install/ajax/ajax-queue.ts +1 -1
  152. package/install/module/abstract-interactable-module.ts +2 -2
  153. package/install/module/environment/image-processing.ts +4 -4
  154. package/install/module/environment/mail-test.ts +2 -2
  155. package/install/module/maintenance/clear-tables.ts +3 -3
  156. package/install/module/maintenance/clear-typo3temp-files.ts +8 -8
  157. package/install/module/maintenance/create-admin.ts +3 -3
  158. package/install/module/maintenance/database-analyzer.ts +9 -7
  159. package/install/module/maintenance/language-packs.ts +4 -4
  160. package/install/module/maintenance/reference-index.ts +1 -1
  161. package/install/module/settings/change-install-tool-password.ts +1 -1
  162. package/install/module/settings/extension-configuration.ts +5 -5
  163. package/install/module/settings/features.ts +1 -1
  164. package/install/module/settings/local-configuration.ts +5 -5
  165. package/install/module/settings/presets.ts +2 -2
  166. package/install/module/settings/system-maintainer.ts +1 -1
  167. package/install/module/upgrade/extension-scanner.ts +5 -5
  168. package/install/module/upgrade/tca-migrations-check.ts +1 -1
  169. package/install/module/upgrade/upgrade-docs.ts +16 -16
  170. package/install/module/upgrade/upgrade-wizards.ts +7 -8
  171. package/install/renderable/language-packs.ts +8 -2
  172. package/lowlevel/query-generator.ts +2 -2
  173. package/package.json +2 -2
  174. package/redirects/event-handler.ts +6 -0
  175. package/rte-ckeditor/ckeditor5.ts +7 -3
  176. package/rte-ckeditor/legacy/ckeditor5-bundle.ts +0 -1
  177. package/rte-ckeditor/plugin/typo3-link.ts +7 -9
  178. package/rte-ckeditor/rte-link-browser.ts +1 -1
  179. package/rte-ckeditor/tests/ckeditor5-test.ts +68 -0
  180. package/scheduler/form-engine/element/timing-options-element.ts +1 -1
  181. package/scheduler/scheduler-add-group.ts +4 -12
  182. package/scheduler/scheduler.ts +2 -2
  183. package/styleguide/checkbox-demo.ts +1 -1
  184. package/tstemplate/template-analyzer.ts +1 -1
  185. package/viewpage/main.ts +1 -1
  186. package/workspaces/backend.ts +5 -5
  187. package/workspaces/preview.ts +41 -2
  188. package/workspaces/renderable/record-table.ts +95 -78
  189. package/workspaces/renderable/send-to-stage-form.ts +1 -1
@@ -31,9 +31,9 @@ describe('@typo3/backend/utility/dom-helper', () => {
31
31
  const wrapperElement = document.createElement('div');
32
32
  wrapperElement.append(document.createRange().createContextualFragment(markup));
33
33
 
34
- const parents = DomHelper.parents(wrapperElement.querySelector('#enter-here'), '.dummy-matching-element');
34
+ const parents = DomHelper.parents<HTMLElement>(wrapperElement.querySelector('#enter-here'), '.dummy-matching-element');
35
35
  expect(parents).to.length(3);
36
- expect(parents.map((el: HTMLElement) => el.tagName)).to.have.ordered.members(['SPAN', 'P', 'DIV']);
36
+ expect(parents.map((el) => el.tagName)).to.have.ordered.members(['SPAN', 'P', 'DIV']);
37
37
  });
38
38
 
39
39
  it('nextAll() returns a proper list of all next elements', () => {
@@ -0,0 +1 @@
1
+ <form id="modal-ajax-form"><input name="filter" autofocus></form>
@@ -39,9 +39,9 @@ describe('@typo3/backend/form-engine/element/extra/select-box-filter', () => {
39
39
  });
40
40
 
41
41
  const visibleOptions = (): string[] => Array
42
- .from(root.querySelectorAll('select[multiple] option'))
43
- .filter((option: HTMLOptionElement): boolean => !option.hidden)
44
- .map((option: HTMLOptionElement): string => option.textContent);
42
+ .from(root.querySelectorAll<HTMLOptionElement>('select[multiple] option'))
43
+ .filter((option): boolean => !option.hidden)
44
+ .map((option): string => option.textContent);
45
45
 
46
46
  const filterBy = (value: string): void => {
47
47
  filterField.value = value;
@@ -35,7 +35,7 @@ type Values = any;
35
35
  * @param {Function|Array|Object} values
36
36
  * @param {Function} func
37
37
  */
38
- function using(values: (() => Values)|Values, func: (...args: unknown[]) => void): void {
38
+ function using(values: (() => Values)|Values, func: (...args: any[]) => void): void {
39
39
  if (values instanceof Function) {
40
40
  values = values();
41
41
  }
@@ -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
  });
@@ -0,0 +1,41 @@
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 { expect } from '@open-wc/testing';
15
+ import { Tree } from '@typo3/backend/tree/tree.js';
16
+ import type { TreeNodeInterface } from '@typo3/backend/tree/tree-node.js';
17
+ import type { } from 'mocha';
18
+
19
+ class TestTree extends Tree {
20
+ public enhance(nodes: TreeNodeInterface[]): TreeNodeInterface[] {
21
+ return this.enhanceNodes(nodes);
22
+ }
23
+ }
24
+
25
+ customElements.define('typo3-test-storage-tree', TestTree);
26
+
27
+ describe('file storage tree search', () => {
28
+ it('expands every matching storage root', () => {
29
+ const tree = document.createElement('typo3-test-storage-tree') as TestTree;
30
+ tree.searchTerm = 'needle';
31
+ const nodes = [
32
+ { identifier: '1:/', depth: 0, loaded: false, hasChildren: true },
33
+ { identifier: '1:/needle', depth: 1, loaded: true, hasChildren: false },
34
+ { identifier: '2:/', depth: 0, loaded: false, hasChildren: true },
35
+ { identifier: '2:/needle', depth: 1, loaded: true, hasChildren: false },
36
+ ] as unknown as TreeNodeInterface[];
37
+
38
+ expect(tree.enhance(nodes).filter(node => node.depth === 0).map(node => node.__expanded))
39
+ .to.deep.equal([true, true]);
40
+ });
41
+ });
@@ -119,7 +119,7 @@ export class FileStorageBrowser extends LitElement {
119
119
  private readonly selectActiveNode = (evt: CustomEvent): void => {
120
120
  // Activate the current node
121
121
  const nodes = evt.detail.nodes as Array<TreeNodeInterface>;
122
- evt.detail.nodes = nodes.map((node: TreeNodeInterface) => {
122
+ evt.detail.nodes = nodes.map((node) => {
123
123
  if (decodeURIComponent(node.identifier) === this.activeFolder) {
124
124
  node.checked = true;
125
125
  }
@@ -62,7 +62,7 @@ export class EditableFileStorageTree extends FileStorageTree {
62
62
  const fileOperationCollection = FileOperationCollection.fromNodePositionOptions(options);
63
63
  const operationConflicts = fileOperationCollection.getConflictingOperationsForTreeNode(options.target);
64
64
  if (operationConflicts.length > 0) {
65
- operationConflicts.forEach((operation: FileOperation) => {
65
+ operationConflicts.forEach((operation) => {
66
66
  Notification.showMessage(
67
67
  layoutLabels.get('drop.conflict'),
68
68
  layoutLabels.get('mess.drop.conflict', [
@@ -161,7 +161,7 @@ export class EditableFileStorageTree extends FileStorageTree {
161
161
  const fileOperationCollection = FileOperationCollection.fromDataTransfer(event.dataTransfer, targetResource);
162
162
  const operationConflicts = fileOperationCollection.getConflictingOperationsForTreeNode(node);
163
163
  if (operationConflicts.length > 0) {
164
- operationConflicts.forEach((operation: FileOperation) => {
164
+ operationConflicts.forEach((operation) => {
165
165
  Notification.showMessage(
166
166
  layoutLabels.get('drop.conflict'),
167
167
  layoutLabels.get('mess.drop.conflict', [
@@ -448,12 +448,12 @@ class FileOperationCollection {
448
448
  }
449
449
 
450
450
  public getConflictingOperationsForTreeNode(node: TreeNodeInterface): FileOperation[] {
451
- return this.operations.filter((operation: FileOperation) => operation.hasConflictWithTreeNode(node));
451
+ return this.operations.filter((operation) => operation.hasConflictWithTreeNode(node));
452
452
  }
453
453
 
454
454
  public getResources(): ResourceInterface[] {
455
455
  const resources: ResourceInterface[] = [];
456
- this.operations.forEach((operation: FileOperation) => {
456
+ this.operations.forEach((operation) => {
457
457
  resources.push(operation.resource);
458
458
  });
459
459
 
@@ -194,7 +194,7 @@ export class PageBrowser extends LitElement {
194
194
  private readonly selectActivePageInTree = (evt: CustomEvent): void => {
195
195
  // Activate the current node
196
196
  const nodes = evt.detail.nodes as Array<TreeNodeInterface>;
197
- evt.detail.nodes = nodes.map((node: TreeNodeInterface) => {
197
+ evt.detail.nodes = nodes.map((node) => {
198
198
  if (parseInt(node.identifier, 10) === this.activePageId) {
199
199
  node.checked = true;
200
200
  }
@@ -204,7 +204,7 @@ export class PagePositionSelect extends LitElement {
204
204
 
205
205
  private syncActiveInsertPositionState(): TreeNodeInterface|null {
206
206
  let activeNode: TreeNodeInterface|null = null;
207
- this.tree.nodes.forEach((node: TreeNodeInterface) => {
207
+ this.tree.nodes.forEach((node) => {
208
208
  node.checked = this.isActivePagePositionNode(node);
209
209
  if (node.checked) {
210
210
  activeNode = node;
@@ -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) {
@@ -282,7 +289,7 @@ export class EditablePageTree extends PageTree {
282
289
  ]
283
290
  );
284
291
 
285
- modal.addEventListener('button.clicked', (e: JQueryEventObject) => {
292
+ modal.addEventListener('button.clicked', (e: Event) => {
286
293
  const target = e.target as HTMLInputElement;
287
294
  if (target.name === 'move') {
288
295
  options.command = TreeNodeCommandEnum.MOVE;
@@ -57,7 +57,7 @@ export class PageTree extends Tree
57
57
  return Promise.resolve();
58
58
  }
59
59
 
60
- if (this.nodes.find((node: TreeNodeInterface) => node.checked)) {
60
+ if (this.nodes.find((node) => node.checked)) {
61
61
  return Promise.resolve();
62
62
  }
63
63
 
@@ -170,3 +170,9 @@ export class PageTree extends Tree
170
170
  return false;
171
171
  }
172
172
  }
173
+
174
+ declare global {
175
+ interface DocumentEventMap {
176
+ 'typo3:pagetree:mountPoint': CustomEvent<{ pageId: number }>;
177
+ }
178
+ }
@@ -26,12 +26,12 @@ export const TreeModuleState = <T extends Constructor<LitElement>>(superClass: T
26
26
 
27
27
  public override connectedCallback(): void {
28
28
  super.connectedCallback();
29
- document.addEventListener('typo3:module-state-storage:update:' + this.moduleStateType, this.moduleStateUpdated);
29
+ document.addEventListener('typo3:module-state-storage:update:' + this.moduleStateType, this.moduleStateUpdated as unknown as EventListener);
30
30
  }
31
31
 
32
32
  public override disconnectedCallback(): void {
33
33
  super.disconnectedCallback();
34
- document.removeEventListener('typo3:module-state-storage:update:' + this.moduleStateType, this.moduleStateUpdated);
34
+ document.removeEventListener('typo3:module-state-storage:update:' + this.moduleStateType, this.moduleStateUpdated as unknown as EventListener);
35
35
  }
36
36
 
37
37
  protected transformModuleStateIdentifierToNodeIdentifier(moduleStateIdentifier: string): string {
@@ -102,7 +102,7 @@ export const TreeModuleState = <T extends Constructor<LitElement>>(superClass: T
102
102
  return;
103
103
  }
104
104
 
105
- if (this.tree.nodes.find((node: TreeNodeInterface) => node.checked)) {
105
+ if (this.tree.nodes.find((node) => node.checked)) {
106
106
  return;
107
107
  }
108
108
 
@@ -115,7 +115,7 @@ export const TreeModuleState = <T extends Constructor<LitElement>>(superClass: T
115
115
  /* @todo store all module state updates in a stack to be consumed by a "slow" tree initialization */
116
116
  return;
117
117
  }
118
- if (identifier && identifier === e.detail.oldState.identifier && this.tree.nodes.find((node: TreeNodeInterface) => node.checked)) {
118
+ if (identifier && identifier === e.detail.oldState.identifier && this.tree.nodes.find((node) => node.checked)) {
119
119
  return;
120
120
  }
121
121
 
@@ -130,7 +130,7 @@ export const TreeModuleState = <T extends Constructor<LitElement>>(superClass: T
130
130
 
131
131
  const nodeIdentifier = this.transformModuleStateIdentifierToNodeIdentifier(identifier);
132
132
  const node = this.tree.nodes.find((node) => node.identifier === nodeIdentifier);
133
- const selectedNode = this.tree.nodes.find((node: TreeNodeInterface) => node.checked);
133
+ const selectedNode = this.tree.nodes.find((node) => node.checked);
134
134
  if (node && node === selectedNode) {
135
135
  // node is already selected, only ensure parents are expanded
136
136
  await this.tree.expandNodeParents(node);
@@ -227,7 +227,7 @@ export class Tree extends LitElement {
227
227
  }
228
228
 
229
229
  if (parentNode !== null) {
230
- nodes = nodes.filter((node: TreeNodeInterface) => {
230
+ nodes = nodes.filter((node) => {
231
231
  // Filter (already processed) parentNode from server response
232
232
  // (if given. note: filetree does not deliver this, pagetree does)
233
233
  return node.identifier !== parentNode.identifier;
@@ -244,7 +244,7 @@ export class Tree extends LitElement {
244
244
  }
245
245
 
246
246
  const all = await Promise.all(
247
- nodes.map(async (node: TreeNodeInterface): Promise<TreeNodeInterface[]> => {
247
+ nodes.map(async (node): Promise<TreeNodeInterface[]> => {
248
248
  const parentNodeTreeIdentifier = node.__parents.join('_');
249
249
  const parentNode = nodes.find(p => p.__treeIdentifier === parentNodeTreeIdentifier) || null;
250
250
  const isVisible = parentNode === null || parentNode.__expanded;
@@ -535,7 +535,7 @@ export class Tree extends LitElement {
535
535
  return;
536
536
  }
537
537
  if (Array.isArray(error)) {
538
- error.forEach((message: any) => { Notification.error(
538
+ error.forEach((message) => { Notification.error(
539
539
  message.title,
540
540
  message.message
541
541
  );});
@@ -549,7 +549,7 @@ export class Tree extends LitElement {
549
549
  }
550
550
 
551
551
  public getSelectedNodes(): TreeNodeInterface[] {
552
- return this.nodes.filter((node: TreeNodeInterface) => node.checked);
552
+ return this.nodes.filter((node) => node.checked);
553
553
  }
554
554
 
555
555
  public getNodeByTreeIdentifier(treeIdentifier: string): TreeNodeInterface|null {
@@ -631,7 +631,7 @@ export class Tree extends LitElement {
631
631
 
632
632
  // State
633
633
  if (this.searchTerm) {
634
- node.__expanded = node.loaded && node.hasChildren;
634
+ node.__expanded = (node.loaded || node.depth === 0) && node.hasChildren;
635
635
  } else if (node.hasChildren) {
636
636
  node.__expanded = (this.settings.expandUpToLevel !== null)
637
637
  ? node.depth < this.settings.expandUpToLevel
@@ -717,7 +717,7 @@ export class Tree extends LitElement {
717
717
  node.__hidden !== true &&
718
718
  !node.__treeParents.some(parentTreeIdentifier => this.getNodeByTreeIdentifier(parentTreeIdentifier).__expanded === false)
719
719
  ));
720
- this.displayNodes.forEach((node: TreeNodeInterface, i: number) => {
720
+ this.displayNodes.forEach((node, i) => {
721
721
  node.__x = node.depth * this.indentWidth;
722
722
  node.__y = i * this.nodeHeight;
723
723
  });
@@ -725,7 +725,7 @@ export class Tree extends LitElement {
725
725
  this.lastRenderScrollPosition = this.currentScrollPosition;
726
726
  const visibleRows = Math.ceil(this.currentVisibleHeight / this.nodeHeight);
727
727
  const position = Math.floor(this.currentScrollPosition / this.nodeHeight);
728
- const visibleNodes = this.displayNodes.filter((node: TreeNodeInterface, index: number) => {
728
+ const visibleNodes = this.displayNodes.filter((node, index) => {
729
729
  // first node is fallback target for tabindex, needs to be available every time
730
730
  if (this.getFirstNode() === node) {
731
731
  return true;
@@ -800,7 +800,7 @@ export class Tree extends LitElement {
800
800
  }
801
801
 
802
802
  protected resetSelectedNodes(): void {
803
- this.getSelectedNodes().forEach((node: TreeNodeInterface) => {
803
+ this.getSelectedNodes().forEach((node) => {
804
804
  if (node.checked === true) {
805
805
  node.checked = false;
806
806
  }
@@ -1671,3 +1671,10 @@ export class Tree extends LitElement {
1671
1671
  }
1672
1672
  }
1673
1673
  }
1674
+
1675
+ declare global {
1676
+ interface HTMLElementEventMap {
1677
+ 'typo3:tree:node-selected': CustomEvent<{ node: TreeNodeInterface, propagate: boolean }>;
1678
+ 'typo3:tree:nodes-prepared': CustomEvent<{ nodes: TreeNodeInterface[] }>;
1679
+ }
1680
+ }
@@ -41,8 +41,8 @@ class UserPassLogin {
41
41
  // register submit handler
42
42
  Login.options.submitHandler = this.resetPassword;
43
43
 
44
- [usernameField, passwordField].forEach((field: HTMLInputElement) => new RegularEvent('keypress', this.showCapslockWarning).bindTo(field));
45
- ['input', 'change'].forEach((eventName: string) => new RegularEvent(eventName, this.showPasswordToggle).bindTo(passwordField));
44
+ [usernameField, passwordField].forEach((field) => new RegularEvent('keypress', this.showCapslockWarning).bindTo(field));
45
+ ['input', 'change'].forEach((eventName) => new RegularEvent(eventName, this.showPasswordToggle).bindTo(passwordField));
46
46
  new RegularEvent('keydown', this.toggleCopyright).bindTo(copyrightLink);
47
47
 
48
48
  // if the login screen is shown in the login_frameset window for re-login,
@@ -102,7 +102,7 @@ export class CollapseStatePersister {
102
102
 
103
103
  private recoverStates(): void {
104
104
  const collapseStateElements: NodeListOf<HTMLElement>|null = document.querySelectorAll('.collapse[data-persist-collapse-state="true"]');
105
- collapseStateElements.forEach((element: HTMLElement) => {
105
+ collapseStateElements.forEach((element) => {
106
106
  const suffix: string = element.dataset.persistCollapseStateSuffix ?? this.localStorageKeyDefaultSuffix;
107
107
  const currentStates = this.fromStorage(suffix);
108
108
  const id: string = element.id;
@@ -107,7 +107,7 @@ class CollapseStateSearch {
107
107
  numberOfSearchMatchesContainer.innerText = labels.get('collapse_state_search.numberOfSearchMatches', { count: matchingNodes.length });
108
108
  numberOfSearchMatchesContainer.classList.remove('hidden');
109
109
 
110
- matchingNodes.forEach((match: Element|null): void => {
110
+ matchingNodes.forEach((match): void => {
111
111
  if (match === null) {
112
112
  return;
113
113
  }
@@ -153,28 +153,28 @@ class CollapseStateSearch {
153
153
  }
154
154
 
155
155
  private findNodesByIdentifier(term: string, treeContainer: HTMLElement): Element[] {
156
- return Array.from(treeContainer.querySelectorAll('.treelist-label')).filter((el: Element): boolean => {
156
+ return Array.from(treeContainer.querySelectorAll('.treelist-label')).filter((el): boolean => {
157
157
  return el.textContent.toLowerCase().includes(term);
158
158
  });
159
159
  }
160
160
 
161
161
  private findNodesByValue(term: string, treeContainer: HTMLElement): Element[] {
162
- const matchingValueNodes = Array.from(treeContainer.querySelectorAll('.treelist-value')).filter((el: Element): boolean => {
162
+ const matchingValueNodes = Array.from(treeContainer.querySelectorAll('.treelist-value')).filter((el): boolean => {
163
163
  return el.textContent.toLowerCase().includes(term);
164
164
  });
165
- return matchingValueNodes.map((node: Element): Element => {
165
+ return matchingValueNodes.map((node): Element => {
166
166
  return node.previousElementSibling;
167
167
  });
168
168
  }
169
169
 
170
170
  private findNodesByComment(term: string, treeContainer: HTMLElement): Element[] {
171
- return Array.from(treeContainer.querySelectorAll('.treelist-comment')).filter((el: Element): boolean => {
171
+ return Array.from(treeContainer.querySelectorAll('.treelist-comment')).filter((el): boolean => {
172
172
  return el.textContent.toLowerCase().includes(term);
173
173
  });
174
174
  }
175
175
 
176
176
  private findNodesByConstantSubstitution(term: string, treeContainer: HTMLElement): Element[] {
177
- return Array.from(treeContainer.querySelectorAll('.treelist-constant-substitution')).filter((el: Element): boolean => {
177
+ return Array.from(treeContainer.querySelectorAll('.treelist-constant-substitution')).filter((el): boolean => {
178
178
  return el.textContent.toLowerCase().includes(term);
179
179
  });
180
180
  }