typo3-typescript-sources 14.3.5 → 14.3.7
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-TYPO3.md +1 -1
- package/README.md +1 -1
- package/backend/element/contextual-record-edit-trigger.ts +9 -1
- package/backend/form-engine/element/extra/select-box-filter.ts +8 -5
- package/backend/form-engine/element/select-tree.ts +16 -2
- package/backend/form-engine/element/slug-element.ts +13 -5
- package/backend/form-engine/field-control/add-record.ts +27 -1
- package/backend/form-engine.ts +80 -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/localization/localization-wizard.ts +3 -0
- package/backend/localization/steps/handler-selection-step.ts +12 -17
- package/backend/localization/steps/mode-step.ts +9 -14
- package/backend/localization/steps/source-language-step.ts +8 -13
- package/backend/localization/steps/target-language-step.ts +12 -17
- package/backend/modal.ts +124 -1
- package/backend/module-docheader.ts +58 -0
- package/backend/page-wizard/steps/position-step.ts +5 -0
- package/backend/tests/form-engine/element/extra/select-box-filter-test.ts +215 -0
- package/backend/tests/form-engine/element/select-tree-test.ts +121 -0
- package/backend/tree/page-position-select.ts +25 -0
- package/backend/wizard/finisher/event-finisher.ts +62 -0
- package/backend/wizard/steps/finisher-step.ts +2 -2
- package/backend/wizard/wizard.ts +110 -7
- package/core/ajax/ajax-request.ts +18 -0
- package/core/tests/ajax/ajax-request-test.ts +1 -1
- package/core/tests/utility/text-normalizer-test.ts +138 -0
- package/core/utility/text-normalizer.ts +98 -0
- 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-editor/view-model.ts +1 -1
- package/form/backend/form-wizard/form-wizard.ts +1 -0
- package/form/backend/form-wizard/steps/settings-step.ts +6 -13
- package/package.json +2 -2
- package/workspaces/backend.ts +7 -2
- package/workspaces/renderable/comment-view.ts +4 -2
- package/workspaces/renderable/record-table.ts +41 -4
- package/workspaces/workspaces.ts +65 -12
package/.last-synced-tag
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v14.3.
|
|
1
|
+
v14.3.7
|
package/README-TYPO3.md
CHANGED
|
@@ -75,7 +75,7 @@ with each other and with the public. Several TYPO3 teams use Slack as a
|
|
|
75
75
|
way to communicate internally and most channels are a welcome place for
|
|
76
76
|
you to join and get yourself involved.
|
|
77
77
|
|
|
78
|
-
* Register: https://
|
|
78
|
+
* Register: https://typo3.community/meet/slack/
|
|
79
79
|
* Slack: https://typo3.slack.com/
|
|
80
80
|
|
|
81
81
|
### Exchange information, ask questions, get help
|
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.7`
|
|
20
20
|
|
|
21
21
|
## 🌿 Branches
|
|
22
22
|
|
|
@@ -27,11 +27,17 @@ import labels from '~labels/backend.alt_doc';
|
|
|
27
27
|
*
|
|
28
28
|
* `url` attribute points to the contextual edit route
|
|
29
29
|
* `edit-url` points to the full FormEngine route.
|
|
30
|
+
* `context` identifies what's being edited, used to persist the sheet's
|
|
31
|
+
* width independently per context. This might be just the database table
|
|
32
|
+
* (e.g. "pages" and "sys_category" are resized independently), but callers
|
|
33
|
+
* may pass a more specific value later, e.g. "tt_content.textmedia" vs.
|
|
34
|
+
* "tt_content.header", once per-CType widths are needed.
|
|
30
35
|
*
|
|
31
36
|
* @example
|
|
32
37
|
* <typo3-backend-contextual-record-edit-trigger
|
|
33
38
|
* url="/typo3/record/edit/contextual?..."
|
|
34
|
-
* edit-url="/typo3/record/edit?..."
|
|
39
|
+
* edit-url="/typo3/record/edit?..."
|
|
40
|
+
* context="pages">
|
|
35
41
|
* Edit record
|
|
36
42
|
* </typo3-backend-contextual-record-edit-trigger>
|
|
37
43
|
*/
|
|
@@ -39,6 +45,7 @@ import labels from '~labels/backend.alt_doc';
|
|
|
39
45
|
export class ContextualRecordEditTriggerElement extends PseudoButtonLitElement {
|
|
40
46
|
@property({ type: String }) url: string;
|
|
41
47
|
@property({ type: String, attribute: 'edit-url' }) editUrl: string;
|
|
48
|
+
@property({ type: String }) context: string = '';
|
|
42
49
|
|
|
43
50
|
protected override async buttonActivated(): Promise<void> {
|
|
44
51
|
if (Persistent.isset('contextualRecordEdit') && Persistent.get('contextualRecordEdit') == 0) {
|
|
@@ -54,6 +61,7 @@ export class ContextualRecordEditTriggerElement extends PseudoButtonLitElement {
|
|
|
54
61
|
size: Sizes.expand,
|
|
55
62
|
position: Positions.sheet,
|
|
56
63
|
hideHeader: true,
|
|
64
|
+
resizeIdentifier: `contextualEdit${this.context ? `.${this.context}` : ''}`,
|
|
57
65
|
});
|
|
58
66
|
this.setupMessageHandling(modal);
|
|
59
67
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import RegularEvent from '@typo3/core/event/regular-event';
|
|
15
|
+
import TextNormalizer from '@typo3/core/utility/text-normalizer';
|
|
15
16
|
|
|
16
17
|
enum Selectors {
|
|
17
18
|
fieldContainerSelector = '.t3js-formengine-field-group',
|
|
@@ -25,6 +26,7 @@ enum Selectors {
|
|
|
25
26
|
class SelectBoxFilter {
|
|
26
27
|
private readonly selectElement: HTMLSelectElement = null;
|
|
27
28
|
private availableOptions: NodeListOf<HTMLOptionElement> = null;
|
|
29
|
+
private foldedLabels: string[] = null;
|
|
28
30
|
|
|
29
31
|
constructor(selectElement: HTMLSelectElement) {
|
|
30
32
|
this.selectElement = selectElement;
|
|
@@ -73,14 +75,15 @@ class SelectBoxFilter {
|
|
|
73
75
|
private filter(filterText: string): void {
|
|
74
76
|
if (this.availableOptions === null) {
|
|
75
77
|
this.availableOptions = this.selectElement.querySelectorAll('option');
|
|
78
|
+
this.foldedLabels = Array.from(this.availableOptions, (option: HTMLOptionElement): string =>
|
|
79
|
+
TextNormalizer.foldCaseAndDiacritics(TextNormalizer.normalizeInvisibleCharacters(option.textContent)));
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
const
|
|
82
|
+
// A plain substring match, no RegExp involved, therefore nothing to escape either
|
|
83
|
+
const foldedFilterText = TextNormalizer.foldCaseAndDiacritics(TextNormalizer.normalizeInvisibleCharacters(filterText));
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
option.hidden = filterText.length > 0 && option.textContent.match(matchFilter) === null;
|
|
85
|
+
this.availableOptions.forEach((option: HTMLOptionElement, index: number): void => {
|
|
86
|
+
option.hidden = filterText.length > 0 && !this.foldedLabels[index].includes(foldedFilterText);
|
|
84
87
|
SelectBoxFilter.toggleOptGroup(option);
|
|
85
88
|
});
|
|
86
89
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { html, type TemplateResult } from 'lit';
|
|
15
15
|
import { Tree, type TreeSettings } from '@typo3/backend/tree/tree';
|
|
16
16
|
import { customElement, state } from 'lit/decorators.js';
|
|
17
|
+
import TextNormalizer from '@typo3/core/utility/text-normalizer';
|
|
17
18
|
import type { TreeNodeInterface } from '@typo3/backend/tree/tree-node';
|
|
18
19
|
|
|
19
20
|
interface SelectTreeSettings extends TreeSettings {
|
|
@@ -48,6 +49,18 @@ export class SelectTree extends Tree
|
|
|
48
49
|
this.addEventListener('typo3:tree:nodes-prepared', this.prepareLoadedNodes);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Fold both sides of the comparison the filter performs, so that a node is
|
|
54
|
+
* found whether or not the diacritics of its name are typed.
|
|
55
|
+
*
|
|
56
|
+
* The names are folded on every keystroke rather than kept beside the node,
|
|
57
|
+
* because writing to a node triggers a re-render and the nodes are replaced
|
|
58
|
+
* whenever a part of the tree is loaded.
|
|
59
|
+
*/
|
|
60
|
+
private static fold(value: string): string {
|
|
61
|
+
return TextNormalizer.foldCaseAndDiacritics(TextNormalizer.normalizeInvisibleCharacters(value));
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
/**
|
|
52
65
|
* Expand all nodes and refresh view
|
|
53
66
|
*/
|
|
@@ -89,7 +102,8 @@ export class SelectTree extends Tree
|
|
|
89
102
|
this.nodes[0].__expanded = false;
|
|
90
103
|
}
|
|
91
104
|
const firstNode = this.nodes[0];
|
|
92
|
-
|
|
105
|
+
// A plain substring match, no RegExp involved, therefore nothing to escape either
|
|
106
|
+
const foldedSearchTerm = SelectTree.fold(searchTerm ?? '');
|
|
93
107
|
|
|
94
108
|
this.nodes.forEach((node: any) => {
|
|
95
109
|
// skip the root node in searches
|
|
@@ -100,7 +114,7 @@ export class SelectTree extends Tree
|
|
|
100
114
|
node.__expanded = false;
|
|
101
115
|
node.__hidden = true;
|
|
102
116
|
|
|
103
|
-
if (
|
|
117
|
+
if (SelectTree.fold(node.name).includes(foldedSearchTerm)) {
|
|
104
118
|
results.push(node);
|
|
105
119
|
}
|
|
106
120
|
});
|
|
@@ -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
|
}
|
|
@@ -33,7 +33,33 @@ class AddRecord {
|
|
|
33
33
|
private readonly registerClickHandler = (e: Event): void => {
|
|
34
34
|
e.preventDefault();
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const href = this.controlElement.getAttribute('href');
|
|
37
|
+
const url = new URL(href, window.location.origin);
|
|
38
|
+
const ownerUid = url.searchParams.get('P[uid]') || '';
|
|
39
|
+
if (ownerUid.startsWith('NEW')) {
|
|
40
|
+
// The record this field belongs to is itself still unsaved:
|
|
41
|
+
// Wizard/AddController can only link a newly created record back into
|
|
42
|
+
// an already-persisted uid, so a plain "leave without saving?" prompt
|
|
43
|
+
// would both discard this record AND silently fail to link anything.
|
|
44
|
+
// Deliberately not passing along this href's own P[returnUrl] here: for
|
|
45
|
+
// a field belonging to an inline/IRRE child that was itself added via
|
|
46
|
+
// AJAX (record_inline_create), that value is whatever URL rendered the
|
|
47
|
+
// child's markup at ajax-creation time. The ajax endpoint's own URL,
|
|
48
|
+
// not a real, navigable page. EditDocumentController computes the
|
|
49
|
+
// correct return-to-this-document URL itself once the save completes.
|
|
50
|
+
FormEngine.preventFollowAddRecordLinkIfNotSaved({
|
|
51
|
+
originalUid: ownerUid,
|
|
52
|
+
ownerTable: url.searchParams.get('P[table]') || '',
|
|
53
|
+
ownerField: url.searchParams.get('P[field]') || '',
|
|
54
|
+
table: url.searchParams.get('P[params][table]') || '',
|
|
55
|
+
pid: url.searchParams.get('P[params][pid]') || '',
|
|
56
|
+
setValue: url.searchParams.get('P[params][setValue]') || 'append',
|
|
57
|
+
flexFormPath: url.searchParams.get('P[flexFormPath]') || '',
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
FormEngine.preventFollowLinkIfNotSaved(href);
|
|
37
63
|
};
|
|
38
64
|
}
|
|
39
65
|
|
package/backend/form-engine.ts
CHANGED
|
@@ -62,6 +62,7 @@ const enum FormAction {
|
|
|
62
62
|
saveAndView = '_savedokview',
|
|
63
63
|
saveAndNew = '_savedoknew',
|
|
64
64
|
duplicate = '_duplicatedoc',
|
|
65
|
+
saveAndAddRecord = '_savedokaddrecord',
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
type OnChangeFieldHandlerCallback = (data: object, e: Event) => void;
|
|
@@ -134,7 +135,7 @@ export default (function() {
|
|
|
134
135
|
btnClass: 'btn-' + Severity.getCssClass(Severity.warning),
|
|
135
136
|
name: 'ok',
|
|
136
137
|
trigger: () => {
|
|
137
|
-
|
|
138
|
+
modal.hideModal();
|
|
138
139
|
saveDocumentWithoutValidation();
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -900,6 +901,84 @@ export default (function() {
|
|
|
900
901
|
}
|
|
901
902
|
};
|
|
902
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Show modal to confirm following a "+ add related record" (fieldControl.addRecord)
|
|
906
|
+
* link whose OWN record is itself still new/unsaved. Since Wizard/AddController
|
|
907
|
+
* can only link a newly created record back into an already-persisted uid,
|
|
908
|
+
* discarding here means abandoning the whole attempt (closing the document,
|
|
909
|
+
* same as preventExitIfNotSavedCallback) rather than following through to
|
|
910
|
+
* create an orphaned record that could never be linked back anyway. The
|
|
911
|
+
* only way to actually continue to the add-record wizard is "save first".
|
|
912
|
+
*
|
|
913
|
+
* @param {Object.<string, string>} addRecordParams
|
|
914
|
+
* @returns {Boolean}
|
|
915
|
+
*/
|
|
916
|
+
FormEngine.preventFollowAddRecordLinkIfNotSaved = function(addRecordParams: {[key: string]: string}): boolean {
|
|
917
|
+
const title = backendAltDocLabels.get('label.confirm.add_record_unsaved_parent.title');
|
|
918
|
+
const content = backendAltDocLabels.get('label.confirm.add_record_unsaved_parent.content');
|
|
919
|
+
const buttons: Array<{text: string, btnClass: string, name: string, active?: boolean}> = [
|
|
920
|
+
{
|
|
921
|
+
text: backendAltDocLabels.get('buttons.confirm.close_without_save.no'),
|
|
922
|
+
btnClass: 'btn-default',
|
|
923
|
+
name: 'no'
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
text: backendAltDocLabels.get('buttons.confirm.close_without_save.yes'),
|
|
927
|
+
btnClass: 'btn-default',
|
|
928
|
+
name: 'yes'
|
|
929
|
+
}
|
|
930
|
+
];
|
|
931
|
+
if (document.querySelector('.has-error') === null) {
|
|
932
|
+
buttons.push({
|
|
933
|
+
text: backendAltDocLabels.get('buttons.confirm.add_record_unsaved_parent.save'),
|
|
934
|
+
btnClass: 'btn-primary',
|
|
935
|
+
name: 'save',
|
|
936
|
+
active: true
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const modal = Modal.confirm(title, content, Severity.warning, buttons);
|
|
941
|
+
modal.addEventListener('button.clicked', function(e: Event) {
|
|
942
|
+
modal.hideModal();
|
|
943
|
+
const name = (e.target as HTMLButtonElement).name;
|
|
944
|
+
if (name === 'yes') {
|
|
945
|
+
FormEngine.closeDocument();
|
|
946
|
+
} else if (name === 'save') {
|
|
947
|
+
FormEngine.saveAndContinueToAddRecord(addRecordParams);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
return false;
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Saves the current document, then (server-side, once the record this
|
|
955
|
+
* addRecordParams.originalUid placeholder was saved under is known)
|
|
956
|
+
* redirects on to Wizard/AddController for the field identified by
|
|
957
|
+
* addRecordParams, see FormAction::savedokaddrecord() /
|
|
958
|
+
* EditDocumentController::processData().
|
|
959
|
+
*
|
|
960
|
+
* @param {Object.<string, string>} addRecordParams
|
|
961
|
+
*/
|
|
962
|
+
FormEngine.saveAndContinueToAddRecord = function(addRecordParams: {[key: string]: string}): void {
|
|
963
|
+
Object.keys(addRecordParams).forEach((key: string): void => {
|
|
964
|
+
const input = document.createElement('input');
|
|
965
|
+
input.type = 'hidden';
|
|
966
|
+
input.name = 'addRecordAfterSave[' + key + ']';
|
|
967
|
+
input.value = addRecordParams[key];
|
|
968
|
+
FormEngine.formElement.append(input);
|
|
969
|
+
});
|
|
970
|
+
const actionInput = document.createElement('input');
|
|
971
|
+
actionInput.type = 'hidden';
|
|
972
|
+
actionInput.name = FormAction.saveAndAddRecord;
|
|
973
|
+
actionInput.value = '1';
|
|
974
|
+
FormEngine.formElement.append(actionInput);
|
|
975
|
+
// Submitted as a regular save on top: "_savedokaddrecord" is only understood by
|
|
976
|
+
// EditDocumentController, while other hosts of FormEngine forms (site configuration,
|
|
977
|
+
// constant editor, ...) test "_savedok" literally. Without it, such a form would
|
|
978
|
+
// neither save nor create anything and silently discard the submitted data.
|
|
979
|
+
submitFormWithAction(FormAction.save);
|
|
980
|
+
};
|
|
981
|
+
|
|
903
982
|
/**
|
|
904
983
|
* Show modal to confirm closing the document without saving
|
|
905
984
|
*/
|
|
@@ -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
|
}
|
|
@@ -151,6 +151,9 @@ export class LocalizationWizard extends LitElement {
|
|
|
151
151
|
|
|
152
152
|
private handleStepSummary(event: StepSummaryEvent) {
|
|
153
153
|
const recordInfo = this.recordInfoTask.value;
|
|
154
|
+
if (recordInfo === undefined) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
154
157
|
event.detail.summaryData = [
|
|
155
158
|
{
|
|
156
159
|
label: recordInfo.typeName,
|
|
@@ -45,23 +45,18 @@ export class HandlerSelectionStep implements WizardStepInterface, WizardStepValu
|
|
|
45
45
|
return [];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return handlers;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.warn('Failed to fetch handlers:', error);
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
48
|
+
const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_handlers)
|
|
49
|
+
.withQueryArguments({
|
|
50
|
+
recordType: recordType,
|
|
51
|
+
recordUid: recordUid,
|
|
52
|
+
sourceLanguage: sourceLanguage,
|
|
53
|
+
targetLanguage: targetLanguage,
|
|
54
|
+
mode: mode,
|
|
55
|
+
})
|
|
56
|
+
.get();
|
|
57
|
+
const handlers: LocalizationHandler[] = await response.resolve();
|
|
58
|
+
|
|
59
|
+
return handlers;
|
|
65
60
|
},
|
|
66
61
|
args: () => [this.context.recordType, this.context.recordUid, this.context.getStoreData('sourceLanguage'), this.context.getStoreData('targetLanguage'), this.context.getStoreData('localizationMode')],
|
|
67
62
|
autoRun: false
|
|
@@ -46,20 +46,15 @@ export class ModeStep implements WizardStepInterface, WizardStepValueInterface,
|
|
|
46
46
|
return [];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return modes;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.warn('Failed to fetch localization modes:', error);
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
49
|
+
const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_modes).withQueryArguments({
|
|
50
|
+
recordType: recordType,
|
|
51
|
+
recordUid: recordUid,
|
|
52
|
+
targetLanguage: targetLanguage,
|
|
53
|
+
sourceLanguage: sourceLanguage,
|
|
54
|
+
}).get();
|
|
55
|
+
const modes: LocalizationMode[] = await response.resolve();
|
|
56
|
+
|
|
57
|
+
return modes;
|
|
63
58
|
},
|
|
64
59
|
args: () => [this.context.recordType, this.context.recordUid, this.context.getStoreData('targetLanguage'), this.context.getStoreData('sourceLanguage')],
|
|
65
60
|
autoRun: false
|
|
@@ -38,19 +38,14 @@ export class SourceLanguageStep implements WizardStepInterface, WizardStepValueI
|
|
|
38
38
|
return [];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return sourceLanguages;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.warn('Failed to fetch source languages:', error);
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
41
|
+
const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.wizard_localization_get_sources).withQueryArguments({
|
|
42
|
+
recordType: recordType,
|
|
43
|
+
recordUid: recordUid,
|
|
44
|
+
targetLanguage: targetLanguageId,
|
|
45
|
+
}).get();
|
|
46
|
+
const sourceLanguages: LocalizationLanguageRecord[] = await response.resolve();
|
|
47
|
+
|
|
48
|
+
return sourceLanguages;
|
|
54
49
|
},
|
|
55
50
|
args: () => [this.context.recordType, this.context.recordUid, this.context.getStoreData('targetLanguage')],
|
|
56
51
|
autoRun: false
|