typo3-typescript-sources 14.3.4 → 14.3.6

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 (37) hide show
  1. package/.last-synced-tag +1 -1
  2. package/README.md +1 -1
  3. package/backend/drag-uploader.ts +21 -4
  4. package/backend/element/editable-page-title.ts +3 -1
  5. package/backend/element/status-indicator-element.ts +93 -0
  6. package/backend/form-engine/container/inline-control-container.ts +7 -0
  7. package/backend/form-engine/element/slug-element.ts +13 -5
  8. package/backend/form-engine/field-wizard/shortcut-validation.ts +61 -0
  9. package/backend/form-engine/inline-relation/ajax-dispatcher.ts +5 -0
  10. package/backend/image-manipulation.ts +1 -1
  11. package/backend/layout-module/sticky-language-header.ts +186 -0
  12. package/backend/live-search/element/result/item/item.ts +2 -0
  13. package/backend/live-search/element/result/result-detail-container.ts +14 -1
  14. package/backend/module-docheader.ts +58 -0
  15. package/backend/page-wizard/steps/position-step.ts +5 -0
  16. package/backend/settings/type/stringlist.ts +7 -0
  17. package/backend/tests/settings/type/stringlist-test.ts +90 -0
  18. package/backend/toolbar/system-information-menu.ts +1 -0
  19. package/backend/tree/page-position-select.ts +25 -0
  20. package/backend/wizard/finisher/event-finisher.ts +62 -0
  21. package/backend/wizard/wizard.ts +99 -3
  22. package/dashboard/dashboard-wizard.ts +86 -0
  23. package/dashboard/dashboard.ts +28 -76
  24. package/dashboard/wizard/finisher/dashboard-wizard-submission-service.ts +43 -0
  25. package/dashboard/wizard/steps/preset-step.ts +164 -0
  26. package/dashboard/wizard/steps/title-step.ts +90 -0
  27. package/form/backend/form-editor/core.ts +22 -9
  28. package/form/backend/form-editor/inspector-component.ts +61 -38
  29. package/form/backend/form-editor/stage-component.ts +2 -2
  30. package/form/backend/form-wizard/form-wizard.ts +1 -0
  31. package/form/backend/form-wizard/steps/settings-step.ts +0 -10
  32. package/install/router.ts +1 -0
  33. package/package.json +2 -2
  34. package/redirects/event-handler.ts +3 -3
  35. package/scheduler/scheduler.ts +2 -2
  36. package/workspaces/renderable/record-table.ts +1 -1
  37. package/workspaces/workspace-state.ts +1 -1
package/.last-synced-tag CHANGED
@@ -1 +1 @@
1
- v14.3.4
1
+ v14.3.6
package/README.md CHANGED
@@ -16,7 +16,7 @@ npm install typo3-typescript-sources
16
16
 
17
17
  - **Original Repository:** [TYPO3/typo3](https://github.com/TYPO3/typo3)
18
18
  - **Source Path:** `Build/Sources/TypeScript/`
19
- - **Current TYPO3 Version:** `v14.3.4`
19
+ - **Current TYPO3 Version:** `v14.3.6`
20
20
 
21
21
  ## 🌿 Branches
22
22
 
@@ -47,6 +47,8 @@ enum Mode {
47
47
  BROWSE = 'browse',
48
48
  }
49
49
 
50
+ const uploadFinishedEventName = 'typo3:drag-uploader:upload-finished';
51
+
50
52
  /**
51
53
  * Properties of a file as returned from the AJAX action; essential, this is a serialized instance of
52
54
  * \TYPO3\CMS\Core\Resource\File plus some extra properties (see FileController::flattenResultDataValue())
@@ -208,6 +210,8 @@ export default class DragUploader {
208
210
  dropzoneCloseButton.addEventListener('click', this.hideDropzone);
209
211
  this.dropzone.append(dropzoneCloseButton);
210
212
 
213
+ document.addEventListener(uploadFinishedEventName, this.closeDropzone);
214
+
211
215
  // no filelist then create own progress table
212
216
  if (this.fileList === null) {
213
217
  this.fileList = document.createElement('table');
@@ -289,6 +293,18 @@ export default class DragUploader {
289
293
  this.dropzone.removeAttribute('hidden');
290
294
  }
291
295
 
296
+ public closeDropzone = (): void => {
297
+ this.dropzone.hidden = true;
298
+ this.dropzone.classList.remove('drop-status-ok');
299
+
300
+ if (this.irreObjectUid !== undefined) {
301
+ // Hide artificial file table in IRRE context
302
+ this.fileList.parentElement.hidden = true;
303
+ }
304
+ // User manually hides the dropzone, so we can reset the flag
305
+ this.manuallyTriggered = false;
306
+ };
307
+
292
308
  /**
293
309
  *
294
310
  * @param {Event} event
@@ -296,10 +312,7 @@ export default class DragUploader {
296
312
  public hideDropzone = (event: Event): void => {
297
313
  event.stopPropagation();
298
314
  event.preventDefault();
299
- this.dropzone.setAttribute('hidden', 'hidden');
300
- this.dropzone.classList.remove('drop-status-ok');
301
- // User manually hides the dropzone, so we can reset the flag
302
- this.manuallyTriggered = false;
315
+ this.closeDropzone();
303
316
  };
304
317
 
305
318
  /**
@@ -442,6 +455,8 @@ export default class DragUploader {
442
455
  if (this.queueLength > 0) {
443
456
  this.queueLength--;
444
457
  if (this.queueLength === 0) {
458
+ document.dispatchEvent(new CustomEvent(uploadFinishedEventName));
459
+
445
460
  const timeout: number = messages && messages.length ? 5000 : 0;
446
461
  if (timeout) {
447
462
  for (const flashMessage of messages) {
@@ -612,6 +627,8 @@ export default class DragUploader {
612
627
 
613
628
  modal.addEventListener('typo3-modal-hidden', () => {
614
629
  this.askForOverride = [];
630
+
631
+ document.dispatchEvent(new CustomEvent(uploadFinishedEventName));
615
632
  });
616
633
  }
617
634
  }
@@ -211,9 +211,11 @@ export class EditablePageTitle extends LitElement {
211
211
  return this.editable && this.pageId > 0;
212
212
  }
213
213
 
214
- private endEditing(): void {
214
+ private async endEditing(): Promise<void> {
215
215
  if (this.isEditable()) {
216
216
  this._isEditing = false;
217
+ await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
218
+ this.shadowRoot.querySelector<HTMLButtonElement>('button[data-action="edit"]')?.focus();
217
219
  }
218
220
  }
219
221
 
@@ -0,0 +1,93 @@
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 { customElement, property } from 'lit/decorators.js';
15
+ import { html, LitElement, nothing, type TemplateResult } from 'lit';
16
+ import { classMap, type ClassInfo } from 'lit/directives/class-map.js';
17
+
18
+ // The first group are the base colours; the second group are semantic states
19
+ // that map to one of those base colours in the stylesheet, so a recurring
20
+ // domain concept looks identical wherever it appears in the backend. Keep the
21
+ // semantic group small and curated rather than introducing new colours.
22
+ export type StatusIndicatorState =
23
+ | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'danger' | 'notice' | 'default'
24
+ | 'active' | 'online' | 'running' | 'disabled';
25
+
26
+ // A semantic state may also imply a motion modifier, so a single state carries
27
+ // both the colour and the activity animation of the concept it names.
28
+ const semanticMotion: Partial<Record<StatusIndicatorState, 'live' | 'loading'>> = {
29
+ online: 'live',
30
+ running: 'loading',
31
+ };
32
+
33
+ /**
34
+ * Module: @typo3/backend/element/status-indicator-element
35
+ *
36
+ * A small dot that flags the state of a record, task or interface element.
37
+ * Add the `live` attribute to signal that an element is active or live, or the
38
+ * `loading` attribute to signal an operation in progress. Provide a `label` to
39
+ * expose an accessible name (rendered as role="img" + aria-label) and a hover
40
+ * tooltip (via title); without it the indicator is purely decorative and hidden
41
+ * from assistive technology.
42
+ *
43
+ * The `state` accepts either a base colour (success, danger, …) or a semantic
44
+ * state (active, online, running, disabled) that resolves to one of those
45
+ * colours and may carry its own motion, so `state="running"` is already
46
+ * animated while `state="active"` is a plain dot.
47
+ *
48
+ * @example
49
+ * <typo3-backend-status-indicator state="success" label="Online"></typo3-backend-status-indicator>
50
+ * <typo3-backend-status-indicator state="success" live label="Live"></typo3-backend-status-indicator>
51
+ * <typo3-backend-status-indicator state="info" loading label="Loading"></typo3-backend-status-indicator>
52
+ * <typo3-backend-status-indicator state="running" label="Running"></typo3-backend-status-indicator>
53
+ */
54
+ @customElement('typo3-backend-status-indicator')
55
+ export class StatusIndicatorElement extends LitElement {
56
+ @property({ type: String }) state: StatusIndicatorState = 'default';
57
+ @property({ type: Boolean }) live: boolean = false;
58
+ @property({ type: Boolean }) loading: boolean = false;
59
+ @property({ type: String }) label: string = null;
60
+
61
+ protected override createRenderRoot(): HTMLElement | ShadowRoot {
62
+ return this;
63
+ }
64
+
65
+ protected override render(): TemplateResult {
66
+ const labelled = this.label !== null && this.label !== '';
67
+ return html`
68
+ <span
69
+ class=${classMap(this.getClasses())}
70
+ title=${labelled ? this.label : nothing}
71
+ role=${labelled ? 'img' : nothing}
72
+ aria-label=${labelled ? this.label : nothing}
73
+ aria-hidden=${labelled ? nothing : 'true'}
74
+ ></span>
75
+ `;
76
+ }
77
+
78
+ private getClasses(): ClassInfo {
79
+ const motion = semanticMotion[this.state];
80
+ return {
81
+ ['status-indicator']: true,
82
+ ['status-indicator-' + this.state]: true,
83
+ ['status-indicator-live']: this.live || motion === 'live',
84
+ ['status-indicator-loading']: this.loading || motion === 'loading',
85
+ };
86
+ }
87
+ }
88
+
89
+ declare global {
90
+ interface HTMLElementTagNameMap {
91
+ 'typo3-backend-status-indicator': StatusIndicatorElement;
92
+ }
93
+ }
@@ -642,6 +642,13 @@ class InlineControlContainer extends HTMLElement {
642
642
  const recordContainer = this.getRecordContainer(objectId);
643
643
  this.removeUsed(recordContainer);
644
644
  }
645
+ }).catch((reason: Error): void => {
646
+ if (reason instanceof DOMException && reason.name === 'AbortError') {
647
+ // Request was aborted because the record was collapsed again before
648
+ // loading finished. Nothing to do, the abort handling already cleaned up.
649
+ return;
650
+ }
651
+ throw reason;
645
652
  });
646
653
 
647
654
  this.requestQueue[objectId] = ajaxRequest;
@@ -88,7 +88,7 @@ class SlugElement {
88
88
  }
89
89
 
90
90
  private registerEvents(): void {
91
- const fieldsToListenOnList = Object.values(this.getAvailableFieldsForProposalGeneration()).map((field: HTMLElement) => `[id="${field.id}"]`);
91
+ const fieldsToListenOnList = Object.values(this.getAvailableFieldsForProposalGeneration()).map((field: HTMLElement) => field.dataset.slugSelector);
92
92
  const recreateButton: HTMLButtonElement = this.fullElement.querySelector(Selectors.recreateButton);
93
93
 
94
94
  // Listen on 'listenerFieldNames' for new pages. This is typically the 'title' field
@@ -217,12 +217,20 @@ class SlugElement {
217
217
  private getAvailableFieldsForProposalGeneration(): { [key: string]: HTMLElement } {
218
218
  const availableFields: { [key: string]: HTMLElement } = {};
219
219
  for (const [fieldName, selector] of Object.entries(this.fieldsToListenOn)) {
220
- let field = document.querySelector('[data-formengine-input-name="' + selector + '"]') as HTMLElement;
221
- if (field === null) {
220
+ const selectorCandidates = [
221
+ '[data-formengine-input-name="' + selector + '"]',
222
222
  // Also check for fields, which do not point to a hidden input field (e.g. "input type=hidden", readOnly, or select fields)
223
- field = document.querySelector('[name="' + selector + '"]') as HTMLElement;
224
- }
223
+ '[name="' + selector + '"]'
224
+ ];
225
+ let field: HTMLElement;
226
+ let usedSelector = '';
227
+ selectorCandidates.some((testSelector: string) => {
228
+ field = document.querySelector(testSelector) as HTMLElement;
229
+ usedSelector = testSelector;
230
+ return field !== null;
231
+ });
225
232
  if (field !== null) {
233
+ field.dataset.slugSelector = usedSelector;
226
234
  availableFields[fieldName] = field;
227
235
  }
228
236
  }
@@ -0,0 +1,61 @@
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 DocumentService from '@typo3/core/document-service';
15
+ import FormEngine from '@typo3/backend/form-engine';
16
+
17
+ /**
18
+ * Client-side logic to toggle required state of "shortcut" based on "shortcut_mode".
19
+ *
20
+ * @internal
21
+ */
22
+ class ShortcutValidation {
23
+ constructor() {
24
+ DocumentService.ready().then((): void => {
25
+ this.run();
26
+ });
27
+ }
28
+
29
+ private run(): void {
30
+ const shortcut = document.querySelector<HTMLInputElement>('[name$="[shortcut]"]');
31
+ const shortcut_mode = document.querySelector<HTMLInputElement>('[name$="[shortcut_mode]"]');
32
+ const form = shortcut?.closest<HTMLFormElement>('form');
33
+
34
+ if (!shortcut || !shortcut_mode || !form) {
35
+ return;
36
+ }
37
+
38
+ shortcut_mode.addEventListener('change', () => {
39
+ this.apply(shortcut, form, shortcut_mode);
40
+ });
41
+ }
42
+
43
+ private async apply(shortcut: HTMLInputElement, form: HTMLFormElement, shortcut_mode: HTMLInputElement): Promise<void> {
44
+ const isRequired = parseInt(shortcut_mode.value, 10) === 0;
45
+ const rules = JSON.parse(shortcut.dataset.formengineValidationRules || '[]');
46
+ const existingRequiredIndex = rules.findIndex((r: any) => r.type === 'required');
47
+
48
+ if (isRequired && existingRequiredIndex === -1) {
49
+ rules.push({ type: 'required' });
50
+ } else if (!isRequired && existingRequiredIndex !== -1) {
51
+ rules.splice(existingRequiredIndex, 1);
52
+ }
53
+
54
+ shortcut.dataset.formengineValidationRules = JSON.stringify(rules);
55
+ FormEngine.reinitialize();
56
+ FormEngine.Validation.initializeInputFields();
57
+ FormEngine.Validation.validate(form);
58
+ }
59
+ }
60
+
61
+ export default new ShortcutValidation();
@@ -62,6 +62,11 @@ export class AjaxDispatcher {
62
62
  return this.processResponse(await response.resolve());
63
63
  });
64
64
  sentRequest.catch((reason: Error): void => {
65
+ if (reason instanceof DOMException && reason.name === 'AbortError') {
66
+ // The request was deliberately aborted (e.g. a record was collapsed again
67
+ // before loading finished), this is not an error worth notifying about.
68
+ return;
69
+ }
65
70
  Notification.error('Error ' + reason.message);
66
71
  });
67
72
 
@@ -385,7 +385,7 @@ class ImageManipulation {
385
385
  // Initialize current crop variant
386
386
  this.update(this.currentCropVariant);
387
387
  },
388
- crop: this.cropMoveHandler.bind(this),
388
+ crop: this.cropMoveHandler,
389
389
  data: this.currentCropVariant.cropArea,
390
390
  }));
391
391
 
@@ -0,0 +1,186 @@
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 { css, html, LitElement, type TemplateResult } from 'lit';
15
+ import { customElement, query, state } from 'lit/decorators.js';
16
+ import RegularEvent from '@typo3/core/event/regular-event';
17
+
18
+ /**
19
+ * Module: @typo3/backend/layout-module/sticky-language-header
20
+ *
21
+ * Carries the header information of a language column twice: the default slot
22
+ * stays in the flow, the "sticky" slot holds a copy that is placed below the
23
+ * doc header, aligned with its column, for as long as the header itself is
24
+ * scrolled out of view. Without a "sticky" slot the element is inert.
25
+ */
26
+ @customElement('typo3-backend-page-layout-sticky-language-header')
27
+ export class PageLayoutStickyLanguageHeader extends LitElement {
28
+ static override styles = css`
29
+ /* the global border-box reset does not cross the shadow boundary */
30
+ :host,
31
+ *,
32
+ *::before,
33
+ *::after {
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ :host {
38
+ display: block;
39
+ }
40
+
41
+ .sticky {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: .5rem;
45
+ position: fixed;
46
+ z-index: calc(var(--typo3-zindex-header) - 2);
47
+ padding: .5rem .75rem;
48
+ white-space: nowrap;
49
+ overflow: hidden;
50
+ pointer-events: none;
51
+ color: var(--typo3-component-color);
52
+ background-color: var(--typo3-component-bg);
53
+ border: var(--typo3-component-border-width) solid var(--typo3-component-border-color);
54
+ border-top: 0;
55
+ border-radius: 0 0 var(--pagemodule-grid-cell-border-radius) var(--pagemodule-grid-cell-border-radius);
56
+ box-shadow: var(--typo3-component-box-shadow-flyout);
57
+ opacity: 0;
58
+ /*
59
+ * The doc header is overlapped by a pixel: it ends on a fractional
60
+ * position, and two boxes meeting there round apart when they are
61
+ * rasterized, which lets the page show through the seam. Offset via top,
62
+ * a transform on a fixed element causes repaint artifacts.
63
+ */
64
+ top: calc(var(--sticky-top, 0px) - 1px - .5rem);
65
+ transition: opacity .2s ease-out, top .2s ease-out;
66
+ }
67
+
68
+ .sticky-active {
69
+ opacity: 1;
70
+ top: calc(var(--sticky-top, 0px) - 1px);
71
+ }
72
+
73
+ ::slotted([slot="sticky"]) {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: .5rem;
77
+ min-width: 0;
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce) {
81
+ .sticky {
82
+ transition: none;
83
+ }
84
+ }
85
+ `;
86
+
87
+ // how far the box shadow reaches beyond the border box
88
+ private static readonly shadowBleed: number = 64;
89
+
90
+ @state() protected active: boolean = false;
91
+
92
+ @query('.inline') private readonly inline: HTMLElement;
93
+ @query('.sticky') private readonly sticky: HTMLElement;
94
+
95
+ private scrollEvent: RegularEvent | null = null;
96
+ private resizeEvent: RegularEvent | null = null;
97
+ private ticking: boolean = false;
98
+
99
+ public override connectedCallback(): void {
100
+ super.connectedCallback();
101
+ const sticky = this.querySelector(':scope > [slot="sticky"]');
102
+ if (sticky === null) {
103
+ return;
104
+ }
105
+ // the slot is rendered with [hidden] so it does not flash before the element
106
+ // is upgraded, and the global reset declares that one !important
107
+ sticky.removeAttribute('hidden');
108
+ this.scrollEvent = new RegularEvent('scroll', this.scheduleSync, true);
109
+ this.scrollEvent.bindTo(document);
110
+ this.resizeEvent = new RegularEvent('resize', this.scheduleSync);
111
+ this.resizeEvent.bindTo(window);
112
+ }
113
+
114
+ public override disconnectedCallback(): void {
115
+ this.scrollEvent?.release();
116
+ this.resizeEvent?.release();
117
+ this.scrollEvent = null;
118
+ this.resizeEvent = null;
119
+ super.disconnectedCallback();
120
+ }
121
+
122
+ protected override firstUpdated(): void {
123
+ this.scheduleSync();
124
+ }
125
+
126
+ protected override render(): TemplateResult {
127
+ return html`
128
+ <div class="inline"><slot></slot></div>
129
+ <div class="sticky${this.active ? ' sticky-active' : ''}" aria-hidden="true">
130
+ <slot name="sticky"></slot>
131
+ </div>`;
132
+ }
133
+
134
+ private readonly scheduleSync = (): void => {
135
+ if (this.ticking) {
136
+ return;
137
+ }
138
+ this.ticking = true;
139
+ requestAnimationFrame((): void => {
140
+ this.ticking = false;
141
+ this.sync();
142
+ });
143
+ };
144
+
145
+ private sync(): void {
146
+ const container = this.closest('.t3-grid-container');
147
+ if (container === null || !this.inline || !this.sticky) {
148
+ return;
149
+ }
150
+ // the lower edge of the doc header in viewport coordinates, which is where the
151
+ // element is placed: whichever of its bars still reaches into the viewport wins,
152
+ // so it holds for a missing button bar and for a bar that wrapped into two rows
153
+ const topOffset: number = Math.max(0, ...Array.from(
154
+ this.ownerDocument.querySelectorAll('.module-docheader'),
155
+ (bar: Element): number => bar.getBoundingClientRect().bottom,
156
+ ));
157
+ const containerRect = container.getBoundingClientRect();
158
+ const inlineRect = this.inline.getBoundingClientRect();
159
+ this.active = inlineRect.bottom <= topOffset && containerRect.bottom > topOffset;
160
+ if (!this.active) {
161
+ return;
162
+ }
163
+ this.sticky.style.setProperty('--sticky-top', topOffset + 'px');
164
+ this.sticky.style.left = inlineRect.left + 'px';
165
+ this.sticky.style.width = inlineRect.width + 'px';
166
+ const cutStart = Math.max(0, containerRect.left - inlineRect.left);
167
+ const cutEnd = Math.max(0, inlineRect.right - containerRect.right);
168
+ if (cutStart > 0 || cutEnd > 0) {
169
+ // clip-path takes the border box as its reference, so the region has to reach
170
+ // beyond it on every side that is not cut, or the box shadow is clipped away too
171
+ const bleed = -PageLayoutStickyLanguageHeader.shadowBleed + 'px';
172
+ const start = cutStart > 0 ? cutStart + 'px' : bleed;
173
+ const end = cutEnd > 0 ? cutEnd + 'px' : bleed;
174
+ this.sticky.style.clipPath = `inset(${bleed} ${end} ${bleed} ${start})`;
175
+ } else {
176
+ this.sticky.style.clipPath = '';
177
+ }
178
+ this.sticky.style.visibility = cutStart < inlineRect.width && cutEnd < inlineRect.width ? '' : 'hidden';
179
+ }
180
+ }
181
+
182
+ declare global {
183
+ interface HTMLElementTagNameMap {
184
+ 'typo3-backend-page-layout-sticky-language-header': PageLayoutStickyLanguageHeader;
185
+ }
186
+ }
@@ -27,8 +27,10 @@ export interface ResultItemInterface {
27
27
  defaultAction: ResultItemActionInterface;
28
28
  icon: Record<string, string>;
29
29
  language: ResultItemLanguageInterface | null;
30
+ thumbnailUrl: string | null;
30
31
  itemTitle: string;
31
32
  typeLabel: string;
33
+ properties: Record<string, string>;
32
34
  extraData: { [key: string]: any }
33
35
  }
34
36
 
@@ -32,12 +32,25 @@ export class ResultDetailContainer extends LitElement {
32
32
  return nothing;
33
33
  }
34
34
 
35
+ const propertyEntries = Object.entries(this.resultItem.properties ?? {});
36
+
35
37
  return html`
36
38
  <div class="livesearch-detail-preamble">
37
- <typo3-backend-icon identifier="${this.resultItem.icon.identifier}" overlay="${this.resultItem.icon.overlay}" size="large"></typo3-backend-icon>
39
+ ${this.resultItem.thumbnailUrl
40
+ ? html`<div class="livesearch-detail-preamble-thumbnail"><img src="${this.resultItem.thumbnailUrl}" loading="lazy" alt=""></div>`
41
+ : html`<typo3-backend-icon identifier="${this.resultItem.icon.identifier}" overlay="${this.resultItem.icon.overlay}" size="large"></typo3-backend-icon>`
42
+ }
38
43
  <h3>${this.resultItem.itemTitle}</h3>
39
44
  <p class="livesearch-detail-preamble-type">${this.resultItem.typeLabel}</p>
40
45
  </div>
46
+ ${propertyEntries.length > 0 ? html`
47
+ <dl class="livesearch-detail-properties">
48
+ ${propertyEntries.map(([label, value]: [string, string]) => html`
49
+ <dt>${label}</dt>
50
+ <dd>${value}</dd>
51
+ `)}
52
+ </dl>
53
+ ` : nothing}
41
54
  <typo3-backend-live-search-result-item-action-container .resultItem="${this.resultItem}"></typo3-backend-live-search-result-item-action-container>
42
55
  `;
43
56
  }
@@ -0,0 +1,58 @@
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 DocumentService from '@typo3/core/document-service';
15
+
16
+ enum Selectors {
17
+ wrapper = '.module-docheader-wrapper',
18
+ navigation = '.t3js-module-docheader-navigation',
19
+ }
20
+
21
+ /**
22
+ * Module: @typo3/backend/module-docheader
23
+ *
24
+ * Publishes the measured docheader heights as CSS custom properties, because
25
+ * both bars wrap and the stylesheet cannot know how much of it stays pinned.
26
+ */
27
+ class ModuleDocHeader {
28
+ private readonly resizeObserver: ResizeObserver;
29
+ private wrapper: HTMLElement | null = null;
30
+ private navigation: HTMLElement | null = null;
31
+
32
+ constructor() {
33
+ this.resizeObserver = new ResizeObserver((): void => this.publish());
34
+ DocumentService.ready().then((): void => this.observe());
35
+ }
36
+
37
+ private observe(): void {
38
+ this.wrapper = document.querySelector(Selectors.wrapper);
39
+ if (this.wrapper === null) {
40
+ return;
41
+ }
42
+
43
+ this.navigation = this.wrapper.querySelector(Selectors.navigation);
44
+ // observing delivers an initial record, which is what publishes the first values
45
+ this.resizeObserver.observe(this.wrapper);
46
+ if (this.navigation !== null) {
47
+ this.resizeObserver.observe(this.navigation);
48
+ }
49
+ }
50
+
51
+ private publish(): void {
52
+ const style = document.documentElement.style;
53
+ style.setProperty('--module-docheader-height', `${this.wrapper.getBoundingClientRect().height}px`);
54
+ style.setProperty('--module-docheader-navigation-height', `${this.navigation?.getBoundingClientRect().height ?? 0}px`);
55
+ }
56
+ }
57
+
58
+ export default new ModuleDocHeader();
@@ -80,6 +80,7 @@ export class PositionStep implements WizardStepInterface, WizardStepValueInterfa
80
80
  .activePageId="${currentValue?.pageUid}"
81
81
  .insertPosition="${currentValue?.insertPosition}"
82
82
  @typo3:page-position-select-tree:insert-position-change=${(event: CustomEvent) => this.handleInsertPositionChange(event)}
83
+ @typo3:page-position-select-tree:insert-position-confirm=${() => this.handleInsertPositionConfirm()}
83
84
  >
84
85
  </typo3-backend-component-page-position-select>
85
86
  `;
@@ -141,6 +142,10 @@ export class PositionStep implements WizardStepInterface, WizardStepValueInterfa
141
142
  });
142
143
  }
143
144
 
145
+ private handleInsertPositionConfirm(): void {
146
+ this.context.dispatchAutoAdvance();
147
+ }
148
+
144
149
  private initSummaryTask(): void {
145
150
  this.summaryTask = new Task(this.context.wizard, {
146
151
  task: async ([pageUid]: [number]): Promise<PageDetail> => {
@@ -66,6 +66,13 @@ export class StringlistTypeElement extends BaseElement<string[]> {
66
66
 
67
67
  protected override render(): TemplateResult {
68
68
  const value = this.value || [];
69
+ if (value.length === 0) {
70
+ return html`
71
+ <button id=${this.formid} class="btn btn-default" type="button" ?disabled=${this.readonly} @click=${() => this.addValue(0)}>
72
+ <typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
73
+ </button>
74
+ `;
75
+ }
69
76
  return html`
70
77
  <div class="form-control-wrap">
71
78
  <div class="table-fit">