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.
- package/.last-synced-tag +1 -1
- package/README.md +1 -1
- package/backend/drag-uploader.ts +21 -4
- package/backend/element/editable-page-title.ts +3 -1
- package/backend/element/status-indicator-element.ts +93 -0
- package/backend/form-engine/container/inline-control-container.ts +7 -0
- package/backend/form-engine/element/slug-element.ts +13 -5
- package/backend/form-engine/field-wizard/shortcut-validation.ts +61 -0
- package/backend/form-engine/inline-relation/ajax-dispatcher.ts +5 -0
- package/backend/image-manipulation.ts +1 -1
- package/backend/layout-module/sticky-language-header.ts +186 -0
- package/backend/live-search/element/result/item/item.ts +2 -0
- package/backend/live-search/element/result/result-detail-container.ts +14 -1
- package/backend/module-docheader.ts +58 -0
- package/backend/page-wizard/steps/position-step.ts +5 -0
- package/backend/settings/type/stringlist.ts +7 -0
- package/backend/tests/settings/type/stringlist-test.ts +90 -0
- package/backend/toolbar/system-information-menu.ts +1 -0
- package/backend/tree/page-position-select.ts +25 -0
- package/backend/wizard/finisher/event-finisher.ts +62 -0
- package/backend/wizard/wizard.ts +99 -3
- package/dashboard/dashboard-wizard.ts +86 -0
- package/dashboard/dashboard.ts +28 -76
- package/dashboard/wizard/finisher/dashboard-wizard-submission-service.ts +43 -0
- package/dashboard/wizard/steps/preset-step.ts +164 -0
- package/dashboard/wizard/steps/title-step.ts +90 -0
- package/form/backend/form-editor/core.ts +22 -9
- package/form/backend/form-editor/inspector-component.ts +61 -38
- package/form/backend/form-editor/stage-component.ts +2 -2
- package/form/backend/form-wizard/form-wizard.ts +1 -0
- package/form/backend/form-wizard/steps/settings-step.ts +0 -10
- package/install/router.ts +1 -0
- package/package.json +2 -2
- package/redirects/event-handler.ts +3 -3
- package/scheduler/scheduler.ts +2 -2
- package/workspaces/renderable/record-table.ts +1 -1
- package/workspaces/workspace-state.ts +1 -1
package/.last-synced-tag
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v14.3.
|
|
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.
|
|
19
|
+
- **Current TYPO3 Version:** `v14.3.6`
|
|
20
20
|
|
|
21
21
|
## 🌿 Branches
|
|
22
22
|
|
package/backend/drag-uploader.ts
CHANGED
|
@@ -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.
|
|
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) =>
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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">
|