typo3-typescript-sources 14.2.0 → 14.3.1

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 (34) hide show
  1. package/.github/workflows/sync-and-publish.yml +6 -6
  2. package/.last-synced-tag +1 -1
  3. package/README.md +1 -1
  4. package/backend/clear-cache.ts +7 -7
  5. package/backend/clipboard-panel.ts +20 -17
  6. package/backend/color-picker.ts +44 -36
  7. package/backend/context-menu-actions.ts +7 -7
  8. package/backend/date-time-picker.ts +38 -16
  9. package/backend/element/contextual-record-edit-trigger.ts +4 -2
  10. package/backend/element/editable-page-title.ts +17 -14
  11. package/backend/form-engine/container/inline-control-container.ts +2 -1
  12. package/backend/form-engine/element/datetime-element.ts +1 -1
  13. package/{styleguide/filter.ts → backend/layout-module/page-layout-event.ts} +8 -8
  14. package/backend/layout-module/page-layout.ts +102 -0
  15. package/backend/layout-module/toggle-hidden-element.ts +126 -0
  16. package/backend/layout-module/velocity-scroll.ts +123 -0
  17. package/backend/live-search/live-search.ts +2 -2
  18. package/backend/modal.ts +79 -4
  19. package/backend/page-wizard/helper/wizard-helper.ts +5 -2
  20. package/backend/security/element/csp-reports.ts +1 -6
  21. package/backend/settings/editor/editable-setting.ts +3 -8
  22. package/backend/settings/type/base.ts +1 -1
  23. package/{setup → backend}/setup-module.ts +3 -3
  24. package/backend/toolbar/clear-cache-menu.ts +11 -4
  25. package/backend/tree/page-position-select.ts +52 -41
  26. package/backend/tree/tree.ts +27 -6
  27. package/dashboard/dashboard.ts +0 -3
  28. package/form/backend/form-editor/inspector-component.ts +1 -1
  29. package/form/backend/form-editor/stage-component.ts +3 -3
  30. package/form/backend/form-manager/view-model.ts +15 -13
  31. package/linkvalidator/linkvalidator.ts +13 -46
  32. package/package.json +2 -2
  33. package/workspaces/renderable/record-table.ts +1 -1
  34. package/backend/page-actions.ts +0 -91
@@ -0,0 +1,126 @@
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, type TemplateResult } from 'lit';
15
+ import { customElement, property } from 'lit/decorators.js';
16
+ import { PseudoButtonLitElement } from '@typo3/backend/element/pseudo-button';
17
+ import PersistentStorage from '@typo3/backend/storage/persistent';
18
+ import layoutLabels from '~labels/backend.layout';
19
+ import '@typo3/backend/element/icon-element';
20
+ import { HiddenContentCountChangedEvent } from './page-layout-event';
21
+
22
+ /**
23
+ * Module: @typo3/backend/layout-module/toggle-hidden-element
24
+ *
25
+ * Toolbar toggle button for showing/hiding hidden content elements
26
+ * in the page module. Updates its counter reactively when content
27
+ * elements are toggled.
28
+ */
29
+ @customElement('typo3-backend-page-layout-toggle-hidden')
30
+ export class PageLayoutToggleHidden extends PseudoButtonLitElement {
31
+ @property({ type: Boolean, reflect: true }) active: boolean = false;
32
+ @property({ type: Number }) count: number = 0;
33
+
34
+ public override connectedCallback(): void {
35
+ super.connectedCallback();
36
+ this.syncDropdownToggleStatus();
37
+ document.addEventListener(HiddenContentCountChangedEvent.eventName, this.onCountChanged);
38
+ }
39
+
40
+ public override disconnectedCallback(): void {
41
+ super.disconnectedCallback();
42
+ document.removeEventListener(HiddenContentCountChangedEvent.eventName, this.onCountChanged);
43
+ }
44
+
45
+ public override updated(changedProperties: Map<string, unknown>): void {
46
+ if (changedProperties.has('active')) {
47
+ this.syncDropdownToggleStatus();
48
+ }
49
+ }
50
+
51
+ protected override createRenderRoot(): HTMLElement | ShadowRoot {
52
+ return this;
53
+ }
54
+
55
+ protected override render(): TemplateResult {
56
+ return html`<span class="dropdown-item-status"></span><typo3-backend-icon identifier="actions-eye" size="small"></typo3-backend-icon>${layoutLabels.get('hiddenCE')} (${this.count})`;
57
+ }
58
+
59
+ protected override buttonActivated(): void {
60
+ if (this.hasAttribute('disabled')) {
61
+ return;
62
+ }
63
+ this.setAttribute('disabled', '');
64
+
65
+ const show = !this.active;
66
+ const hiddenElements = document.querySelectorAll('.t3js-hidden-record') as NodeListOf<HTMLElement>;
67
+
68
+ for (const hiddenElement of hiddenElements) {
69
+ hiddenElement.style.display = 'flow-root';
70
+ const scrollHeight = hiddenElement.scrollHeight;
71
+
72
+ // Always set `overflow: clip` after storing scrollHeight
73
+ // * For hidden state `height: 0px` is already set.
74
+ // * For visible state setting `overflow: clip` is fine anyway.
75
+ hiddenElement.style.overflow = 'clip';
76
+
77
+ if (!show) {
78
+ // * Invisible elements must not be accessible/focusable by keyboard.
79
+ // * Spacing between content elements is kept uniform by collapsed margins,
80
+ // hidden elements have a height of 0 and the margins of the surrounding elements
81
+ // cannot collapse, causing a visual gap.
82
+ // Therefore do not display the element at all by setting `display: none`.
83
+ hiddenElement.addEventListener('transitionend', (): void => {
84
+ hiddenElement.style.display = 'none';
85
+ hiddenElement.style.overflow = '';
86
+ }, { once: true });
87
+
88
+ // We use requestAnimationFrame() as we have to set the container's height at first before resizing to
89
+ // collapsed-element-height. This results in a smooth animation.
90
+ requestAnimationFrame((): void => {
91
+ hiddenElement.style.height = scrollHeight + 'px';
92
+ requestAnimationFrame((): void => {
93
+ hiddenElement.style.height = '0px';
94
+ });
95
+ });
96
+ } else {
97
+ hiddenElement.addEventListener('transitionend', (): void => {
98
+ hiddenElement.style.display = '';
99
+ hiddenElement.style.overflow = '';
100
+ hiddenElement.style.height = '';
101
+ }, { once: true });
102
+
103
+ hiddenElement.style.height = scrollHeight + 'px';
104
+ }
105
+ }
106
+
107
+ this.active = show;
108
+ PersistentStorage.set('moduleData.web_layout.showHidden', show ? '1' : '0').then((): void => {
109
+ this.removeAttribute('disabled');
110
+ });
111
+ }
112
+
113
+ private syncDropdownToggleStatus(): void {
114
+ this.dataset.dropdowntoggleStatus = this.active ? 'active' : 'inactive';
115
+ }
116
+
117
+ private readonly onCountChanged = (event: Event): void => {
118
+ this.count = (event as HiddenContentCountChangedEvent).detail.count;
119
+ };
120
+ }
121
+
122
+ declare global {
123
+ interface HTMLElementTagNameMap {
124
+ 'typo3-backend-page-layout-toggle-hidden': PageLayoutToggleHidden;
125
+ }
126
+ }
@@ -0,0 +1,123 @@
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
+ /**
15
+ * Module: @typo3/backend/layout-module/velocity-scroll
16
+ *
17
+ * Enables auto-scrolling while dragging near the viewport edge.
18
+ *
19
+ * Self-binds a delegated `dragstart` listener for all `[draggable="true"]`
20
+ * elements on the page. May also be invoked directly:
21
+ *
22
+ * ```js
23
+ * // inside a dragstart event handler:
24
+ * initVelocityScroll(event);
25
+ * ```
26
+ */
27
+ import DocumentService from '@typo3/core/document-service';
28
+ import RegularEvent from '@typo3/core/event/regular-event';
29
+
30
+ const EDGE_RATIO = 0.2;
31
+ const MAX_STRENGTH_RATIO = 2.5;
32
+
33
+ DocumentService.ready().then((): void => {
34
+ new RegularEvent('dragstart', (event: Event): void => {
35
+ initVelocityScroll(event as DragEvent);
36
+ }).delegateTo(document, '[draggable="true"]');
37
+ });
38
+
39
+ export function initVelocityScroll(event: DragEvent): void {
40
+ const scrollingElement = document.scrollingElement;
41
+ if (!(scrollingElement instanceof HTMLElement)) {
42
+ console.warn('Scrolling element is not an HTMLElement. Velocity scroll will not work.');
43
+ return;
44
+ }
45
+
46
+ // Horizontal scrolling may live on an inner container (e.g. the page module's
47
+ // .t3-grid-container with overflow-x: auto) rather than the document, so target
48
+ // the nearest horizontally scrollable ancestor of the drag source if any exists.
49
+ const horizontalElement = findHorizontalScrollAncestor(event.target as Element | null) ?? scrollingElement;
50
+
51
+ let scrollSpeedVertical = 0;
52
+ let scrollSpeedHorizontal = 0;
53
+ let running = true;
54
+ let lastFrameTime = performance.now();
55
+
56
+ // Disable smooth scrolling for the duration of the drag and restore on dragend
57
+ const originalVerticalScrollBehavior = scrollingElement.style.scrollBehavior;
58
+ const originalHorizontalScrollBehavior = horizontalElement.style.scrollBehavior;
59
+ scrollingElement.style.scrollBehavior = 'auto';
60
+ horizontalElement.style.scrollBehavior = 'auto';
61
+
62
+ const tick = (now: number): void => {
63
+ const dt = (now - lastFrameTime) / 1000;
64
+ lastFrameTime = now;
65
+
66
+ const maxVertical = scrollingElement.scrollHeight - scrollingElement.clientHeight;
67
+ const maxHorizontal = horizontalElement.scrollWidth - horizontalElement.clientWidth;
68
+ scrollingElement.scrollTop = Math.max(0, Math.min(scrollingElement.scrollTop + scrollSpeedVertical * dt, maxVertical));
69
+ horizontalElement.scrollLeft = Math.max(0, Math.min(horizontalElement.scrollLeft + scrollSpeedHorizontal * dt, maxHorizontal));
70
+
71
+ if (running) {
72
+ requestAnimationFrame(tick);
73
+ }
74
+ };
75
+ requestAnimationFrame(tick);
76
+
77
+ // Progress within the edge zone goes from 0 (just entered) to 1 (at the edge).
78
+ // Squaring it makes scrolling accelerate noticeably near the edge.
79
+ const axisStrength = (distance: number, edge: number, max: number): number =>
80
+ distance < edge ? ((1 - distance / edge) ** 2) * max : 0;
81
+
82
+ const autoScroll = (event: DragEvent): void => {
83
+ const verticalEdge = window.innerHeight * EDGE_RATIO;
84
+ const horizontalEdge = window.innerWidth * EDGE_RATIO;
85
+ const maxVerticalStrength = window.innerHeight * MAX_STRENGTH_RATIO;
86
+ const maxHorizontalStrength = window.innerWidth * MAX_STRENGTH_RATIO;
87
+
88
+ scrollSpeedVertical =
89
+ axisStrength(window.innerHeight - event.clientY, verticalEdge, maxVerticalStrength) -
90
+ axisStrength(event.clientY, verticalEdge, maxVerticalStrength);
91
+ scrollSpeedHorizontal =
92
+ axisStrength(window.innerWidth - event.clientX, horizontalEdge, maxHorizontalStrength) -
93
+ axisStrength(event.clientX, horizontalEdge, maxHorizontalStrength);
94
+ };
95
+ autoScroll(event);
96
+ window.addEventListener('dragover', autoScroll);
97
+
98
+ // Bind teardown on window: the source element may be detached or replaced
99
+ // during the drop (drag-drop.ts re-renders content via AJAX), in which case
100
+ // a `dragend` listener on the source element would never fire.
101
+ window.addEventListener('dragend', (): void => {
102
+ window.removeEventListener('dragover', autoScroll);
103
+ scrollSpeedVertical = 0;
104
+ scrollSpeedHorizontal = 0;
105
+ running = false;
106
+ scrollingElement.style.scrollBehavior = originalVerticalScrollBehavior;
107
+ horizontalElement.style.scrollBehavior = originalHorizontalScrollBehavior;
108
+ }, { once: true });
109
+ }
110
+
111
+ function findHorizontalScrollAncestor(element: Element | null): HTMLElement | null {
112
+ let current = element;
113
+ while (current instanceof HTMLElement) {
114
+ if (current.scrollWidth > current.clientWidth) {
115
+ const overflowX = window.getComputedStyle(current).overflowX;
116
+ if (overflowX === 'auto' || overflowX === 'scroll') {
117
+ return current;
118
+ }
119
+ }
120
+ current = current.parentElement;
121
+ }
122
+ return null;
123
+ }
@@ -11,7 +11,7 @@
11
11
  * The TYPO3 project - inspiring people to share!
12
12
  */
13
13
 
14
- import Modal from '@typo3/backend/modal';
14
+ import Modal, { Sizes } from '@typo3/backend/modal';
15
15
  import '@typo3/backend/element/icon-element';
16
16
  import '@typo3/backend/live-search/element/hint';
17
17
  import '@typo3/backend/live-search/element/result/result-pagination';
@@ -105,7 +105,7 @@ class LiveSearch {
105
105
  content: url.toString(),
106
106
  title: coreLabels.get('labels.search'),
107
107
  severity: SeverityEnum.notice,
108
- size: Modal.sizes.medium,
108
+ size: Sizes.large,
109
109
  ajaxCallback: (): void => {
110
110
  const liveSearchContainer = modal.querySelector('typo3-backend-live-search');
111
111
  const searchForm = liveSearchContainer.querySelector('form');
package/backend/modal.ts CHANGED
@@ -46,6 +46,30 @@ export enum Sizes {
46
46
  expand = 'expand',
47
47
  }
48
48
 
49
+ export enum Size {
50
+ small = 'small',
51
+ default = 'default',
52
+ medium = 'medium',
53
+ large = 'large',
54
+ full = 'full',
55
+ }
56
+
57
+ export type SizeConfig = { width?: Size; height?: Size };
58
+
59
+ const isValidSize = (value: unknown): value is Sizes | SizeConfig => {
60
+ if (typeof value === 'string') {
61
+ return value in Sizes;
62
+ }
63
+ if (typeof value !== 'object' || value === null) {
64
+ return false;
65
+ }
66
+ const config = value as SizeConfig;
67
+ return Object.keys(value).length > 0
68
+ && Object.keys(value).every(key => key === 'width' || key === 'height')
69
+ && (config.width === undefined || config.width in Size)
70
+ && (config.height === undefined || config.height in Size);
71
+ };
72
+
49
73
  export enum Positions {
50
74
  center = 'center',
51
75
  top = 'top',
@@ -89,7 +113,7 @@ export interface Configuration {
89
113
  severity: SeverityEnum;
90
114
  buttons: Array<Button>;
91
115
  style: Styles;
92
- size: Sizes;
116
+ size: Sizes | SizeConfig;
93
117
  position: Positions;
94
118
  additionalCssClasses: Array<string>;
95
119
  callback: ModalCallbackFunction | null;
@@ -110,7 +134,6 @@ export class ModalElement extends LitElement {
110
134
  @property({ type: String, reflect: true }) type: Types = Types.default;
111
135
  @property({ type: String, reflect: true }) severity: SeverityEnum = SeverityEnum.notice;
112
136
  @property({ type: String, reflect: true }) variant: Styles = Styles.default;
113
- @property({ type: String, reflect: true }) size: Sizes = Sizes.default;
114
137
  @property({ type: String, reflect: true }) position: Positions = Positions.center;
115
138
  @property({ type: Boolean }) staticBackdrop: boolean = false;
116
139
  @property({ type: Boolean }) hideCloseButton: boolean = false;
@@ -128,6 +151,7 @@ export class ModalElement extends LitElement {
128
151
  public userData: { [key: string]: any } = {};
129
152
 
130
153
  private readonly uniqueId: number;
154
+ #size: Sizes | SizeConfig = Sizes.default;
131
155
 
132
156
  constructor() {
133
157
  super();
@@ -135,6 +159,43 @@ export class ModalElement extends LitElement {
135
159
  this.uniqueId = ++uniqueIdCounter;
136
160
  }
137
161
 
162
+ @property({
163
+ reflect: true,
164
+ converter: {
165
+ toAttribute: (value: Sizes | SizeConfig): string =>
166
+ typeof value === 'string' ? value : JSON.stringify(value),
167
+ fromAttribute: (value: string | null): unknown => {
168
+ if (value === null) {
169
+ return Sizes.default;
170
+ }
171
+ if (isValidSize(value)) {
172
+ return value;
173
+ }
174
+ try {
175
+ return JSON.parse(value);
176
+ } catch {
177
+ return value;
178
+ }
179
+ },
180
+ },
181
+ })
182
+ get size(): Sizes | SizeConfig {
183
+ return this.#size;
184
+ }
185
+ set size(value: unknown) {
186
+ const valid = isValidSize(value);
187
+ this.#size = valid ? value : Sizes.default;
188
+ if (!valid && this.isConnected) {
189
+ // Input was invalid and got sanitized: force the attribute to match,
190
+ // because Lit suppresses attribute reflection on the attribute → property
191
+ // path, which would otherwise leave the invalid string in the DOM.
192
+ const expected = typeof this.#size === 'string' ? this.#size : JSON.stringify(this.#size);
193
+ if (this.getAttribute('size') !== expected) {
194
+ this.setAttribute('size', expected);
195
+ }
196
+ }
197
+ }
198
+
138
199
  public setContent(content: TemplateResult | JQuery | Element | DocumentFragment): void {
139
200
  this.templateResultContent = content;
140
201
  }
@@ -201,7 +262,7 @@ export class ModalElement extends LitElement {
201
262
  `modal-type-${this.type}`,
202
263
  `modal-style-${this.variant}`,
203
264
  `modal-severity-${Severity.getCssClass(this.severity)}`,
204
- `modal-size-${this.size}`,
265
+ ...this.getSizeClasses(),
205
266
  `modal-position-${this.position}`,
206
267
  ...this.additionalCssClasses,
207
268
  ]);
@@ -236,6 +297,20 @@ export class ModalElement extends LitElement {
236
297
  `;
237
298
  }
238
299
 
300
+ private getSizeClasses(): string[] {
301
+ if (typeof this.size === 'string') {
302
+ return [`modal-size-${this.size}`];
303
+ }
304
+ const classes: string[] = [];
305
+ if (this.size.width !== undefined) {
306
+ classes.push(`modal-width-${this.size.width}`);
307
+ }
308
+ if (this.size.height !== undefined) {
309
+ classes.push(`modal-height-${this.size.height}`);
310
+ }
311
+ return classes;
312
+ }
313
+
239
314
  private handleDialogClose(): void {
240
315
  this.trigger('typo3-modal-hidden');
241
316
  }
@@ -599,7 +674,7 @@ class Modal {
599
674
  ? configuration.severity
600
675
  : this.defaultConfiguration.severity;
601
676
  configuration.buttons = <Array<Button>>configuration.buttons || this.defaultConfiguration.buttons;
602
- configuration.size = typeof configuration.size === 'string' && configuration.size in Sizes
677
+ configuration.size = isValidSize(configuration.size)
603
678
  ? configuration.size
604
679
  : this.defaultConfiguration.size;
605
680
  configuration.style = typeof configuration.style === 'string' && configuration.style in Styles
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { topLevelModuleImport } from '@typo3/backend/utility/top-level-module-import';
15
15
  import { html } from 'lit';
16
- import Modal from '@typo3/backend/modal';
16
+ import Modal, { Size } from '@typo3/backend/modal';
17
17
  import { SeverityEnum } from '@typo3/backend/enum/severity';
18
18
  import backendLayoutLabels from '~labels/backend.layout';
19
19
  import type { PageWizardConfiguration } from '@typo3/backend/page-wizard/page-wizard-configuration';
@@ -25,7 +25,10 @@ export const openPageWizardModal = async(configuration?: PageWizardConfiguration
25
25
  title: backendLayoutLabels.get('newPage'),
26
26
  content: html`<typo3-backend-page-wizard .configuration="${configuration}"></typo3-backend-page-wizard>`,
27
27
  severity: SeverityEnum.notice,
28
- size: Modal.sizes.medium,
28
+ size: {
29
+ width: Size.medium,
30
+ height: Size.large
31
+ },
29
32
  staticBackdrop: true,
30
33
  buttons: []
31
34
  });
@@ -133,16 +133,11 @@ export class CspReports extends LitElement {
133
133
 
134
134
  protected override render(): TemplateResult {
135
135
  return html`
136
- <div class="infolist-container infolist-overlay">
136
+ <div class="infolist-container">
137
137
  <div class="infolist">
138
138
  <div class="infolist-header">
139
139
  ${this.renderNavigation()}
140
140
  </div>
141
- </div>
142
- </div>
143
-
144
- <div class="infolist-container">
145
- <div class="infolist">
146
141
  <div class="infolist-content">
147
142
  <div class="table-fit mb-0">
148
143
  <table class="table table-striped">
@@ -35,8 +35,7 @@ interface SettingDefinition {
35
35
  label: string,
36
36
  description: string|null,
37
37
  readonly: boolean,
38
- // @todo php json_encode encodes ['0' => 'foo'] as ['foo'] instead of {'0' => 'foo'}
39
- enum: Record<string, string>|Array<string>,
38
+ enum: Record<string, string>,
40
39
  categories: string[],
41
40
  tags: string[],
42
41
  options: Record<string, unknown>,
@@ -119,10 +118,6 @@ export class EditableSettingElement extends LitElement {
119
118
 
120
119
  protected updateFieldAttributes(element: HTMLElement): void {
121
120
  const { definition, value } = this.setting;
122
- // Force conversion to an object, as PHP json_encode encodes ['0' => 'foo'] as
123
- // ['foo'] instead of {'0' => 'foo'}
124
- const enumEntries = Object.entries(definition.enum || {});
125
-
126
121
  const attributes = {
127
122
  key: definition.key,
128
123
  formid: `setting-${definition.key}`,
@@ -130,9 +125,9 @@ export class EditableSettingElement extends LitElement {
130
125
  value: Array.isArray(value) ? JSON.stringify(value) : String(value),
131
126
  debug: this.mode === SettingsMode.advanced,
132
127
  readonly: definition.readonly,
133
- enum: enumEntries.length > 0 ? JSON.stringify(Object.fromEntries(enumEntries)) : false,
128
+ enum: Object.keys(definition.enum).length > 0 ? JSON.stringify(definition.enum) : false,
134
129
  default: Array.isArray(definition.default) ? JSON.stringify(definition.default) : String(definition.default),
135
- options: definition.options ? (Array.isArray(definition.options) && definition.options.length === 0 ? '{}' : JSON.stringify(definition.options)) : '{}',
130
+ options: JSON.stringify(definition.options),
136
131
  };
137
132
  for (const [key, value] of Object.entries(attributes)) {
138
133
  if (typeof value === 'boolean') {
@@ -36,7 +36,7 @@ export abstract class BaseElement<
36
36
  @property({ type: String }) key: string;
37
37
  @property({ type: String }) formid: string;
38
38
  @property({ type: Boolean }) readonly: boolean = false;
39
- @property({ type: Object }) enum: Record<string, string>;
39
+ @property({ type: Object }) enum: Record<string, string> | undefined;
40
40
  @property({ type: Boolean }) debug: boolean = false;
41
41
  @property({ type: Object }) options: OptionsType;
42
42
 
@@ -18,8 +18,8 @@ import RegularEvent from '@typo3/core/event/regular-event';
18
18
  import Modal from '@typo3/backend/modal';
19
19
 
20
20
  /**
21
- * Module: @typo3/setup/setup-module
22
- * @exports @typo3/setup/setup-module
21
+ * Module: @typo3/backend/setup-module
22
+ * @exports @typo3/backend/setup-module
23
23
  */
24
24
  class SetupModule {
25
25
  private avatarWindowRef: Window;
@@ -72,7 +72,7 @@ class SetupModule {
72
72
  if (typeof evt.data.objectGroup === 'undefined') {
73
73
  throw new Error('No object group defined for message');
74
74
  }
75
- const avatarMatches = evt.data.objectGroup.match(/avatar-(.+)$/);
75
+ const avatarMatches = evt.data.objectGroup.match(/(?:^|-)avatar-(.+)$/);
76
76
  if (avatarMatches === null) {
77
77
  // Received message isn't provisioned for current InlineControlContainer instance
78
78
  return;
@@ -71,10 +71,17 @@ class ClearCacheMenu {
71
71
  (new AjaxRequest(ajaxUrl)).post({}).then(
72
72
  async (response: AjaxResponse): Promise<void> => {
73
73
  const data = await response.resolve();
74
- Notification.success(
75
- data.title,
76
- data.message
77
- );
74
+ if (data?.success === false) {
75
+ Notification.error(
76
+ data.title ?? cacheLabels.get('notification.error.title'),
77
+ data.message ?? cacheLabels.get('notification.error.message'),
78
+ );
79
+ } else {
80
+ Notification.success(
81
+ data?.title ?? cacheLabels.get('notification.success.title'),
82
+ data?.message ?? cacheLabels.get('notification.success.message'),
83
+ );
84
+ }
78
85
  },
79
86
  (): void => {
80
87
  Notification.error(
@@ -108,31 +108,17 @@ export class PagePositionSelect extends LitElement {
108
108
  }
109
109
 
110
110
  protected renderTree(): TemplateResult {
111
- const initialized = () => {
111
+ const initialized = async () => {
112
112
  // set up toolbar now with updated properties
113
113
  const toolbar = this.querySelector('typo3-backend-tree-toolbar') as TreeToolbar;
114
114
  toolbar.tree = this.tree;
115
- this.tree.ensureActiveNodeLoaded(this.activePageId).then(
116
- () => {
117
- const currentNode = this.tree.nodes.find(n => n.identifier === String(this.activePageId));
118
- if (currentNode) {
119
- this.toggleDynamicInsertNodes(currentNode).then(
120
- () => {
121
- const activeNode = this.tree.nodes.find(n => this.isActivePagePositionNode(n));
122
- if (activeNode) {
123
- activeNode.checked = true;
124
- this.updateBreadcrumb(this.tree.nodes);
125
- this.tree.requestUpdate();
126
- this.tree.updateComplete.then(() => {
127
- this.tree.scrollNodeIntoViewIfNeeded(activeNode);
128
- this.tree.focusNode(activeNode);
129
- });
130
- }
131
- }
132
- );
133
- }
134
- }
135
- );
115
+ await this.tree.ensureActiveNodeLoaded(this.activePageId);
116
+ const activeNode = await this.applyActiveInsertPosition();
117
+ if (activeNode) {
118
+ await this.tree.updateComplete;
119
+ this.tree.scrollNodeIntoViewIfNeeded(activeNode);
120
+ this.tree.focusNode(activeNode);
121
+ }
136
122
  };
137
123
 
138
124
  return html`
@@ -145,38 +131,63 @@ export class PagePositionSelect extends LitElement {
145
131
  .setup=${this.configuration}
146
132
  @tree:initialized=${initialized}
147
133
  @typo3:tree:node-selected="${this.handleNodeSelected}"
134
+ @typo3:tree:filter-applied=${() => this.applyActiveInsertPosition()}
135
+ @typo3:tree:filter-reset=${() => this.applyActiveInsertPosition()}
148
136
  >
149
137
  </typo3-backend-component-page-position-select-tree>
150
138
  `;
151
139
  }
152
140
 
153
- private handleNodeSelected(event: CustomEvent): void {
141
+ private async handleNodeSelected(event: CustomEvent): Promise<void> {
154
142
  const selectedNode: TreeNodeInterface = event.detail.node;
155
143
  const [pageUid, insertPosition] = selectedNode.identifier.split('-');
156
144
  this.insertPosition = (insertPosition ?? 'inside') as Position;
157
145
  this.activePageId = Number(pageUid);
158
146
 
159
- const callback = () => {
160
- this.tree.nodes.forEach((node: TreeNodeInterface) => {
161
- node.checked = this.isActivePagePositionNode(node);
162
- if (node.checked) {
163
- this.tree.focusNode(node);
164
- }
165
- });
147
+ const activeNode = /^\d+$/.test(selectedNode.identifier)
148
+ ? await this.applyActiveInsertPosition()
149
+ : this.syncActiveInsertPositionState();
166
150
 
167
- this.updateBreadcrumb(this.tree.nodes);
168
- this.dispatchEvent(new InsertPositionChangeEvent(
169
- this.activePageId,
170
- this.insertPosition
171
- ));
172
- };
151
+ if (activeNode) {
152
+ this.tree.focusNode(activeNode);
153
+ }
173
154
 
174
- if (/^\d+$/.test(selectedNode.identifier)) {
175
- this.toggleDynamicInsertNodes(selectedNode).then(callback);
176
- } else {
177
- callback();
178
- this.tree.requestUpdate();
155
+ this.dispatchEvent(new InsertPositionChangeEvent(
156
+ this.activePageId,
157
+ this.insertPosition
158
+ ));
159
+ }
160
+
161
+ private async applyActiveInsertPosition(): Promise<TreeNodeInterface|null> {
162
+ if (this.activePageId === null || this.activePageId === undefined) {
163
+ return null;
179
164
  }
165
+ let currentNode = this.tree.nodes.find(n => n.identifier === String(this.activePageId));
166
+ if (!currentNode) {
167
+ // Load its rootline and expand the path
168
+ await this.tree.ensureActiveNodeLoaded(this.activePageId);
169
+ currentNode = this.tree.nodes.find(n => n.identifier === String(this.activePageId));
170
+ if (!currentNode) {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ await this.tree.expandNodeParents(currentNode);
176
+ await this.toggleDynamicInsertNodes(currentNode);
177
+ return this.syncActiveInsertPositionState();
178
+ }
179
+
180
+ private syncActiveInsertPositionState(): TreeNodeInterface|null {
181
+ let activeNode: TreeNodeInterface|null = null;
182
+ this.tree.nodes.forEach((node: TreeNodeInterface) => {
183
+ node.checked = this.isActivePagePositionNode(node);
184
+ if (node.checked) {
185
+ activeNode = node;
186
+ }
187
+ });
188
+ this.updateBreadcrumb(this.tree.nodes);
189
+ this.tree.requestUpdate();
190
+ return activeNode;
180
191
  }
181
192
 
182
193
  private async toggleDynamicInsertNodes(currentNode: TreeNodeInterface): Promise<void> {