typo3-typescript-sources 14.3.6 → 14.3.8
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/column-selector-button.ts +5 -2
- package/backend/drag-uploader.ts +78 -86
- 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/online-media-form-element.ts +3 -3
- package/backend/form-engine/element/select-tree.ts +16 -2
- package/backend/form-engine/field-control/add-record.ts +27 -1
- package/backend/form-engine.ts +88 -13
- package/backend/layout-module/column-restriction.ts +61 -0
- package/backend/layout-module/drag-drop.ts +12 -0
- package/backend/layout-module/paste.ts +7 -0
- 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/login-refresh.ts +1 -9
- package/backend/modal.ts +162 -6
- package/backend/notification.ts +90 -6
- package/backend/online-media.ts +1 -3
- package/backend/record-download-button.ts +4 -5
- package/backend/recordlist.ts +5 -0
- package/backend/tests/fixtures/modal-autofocus.html +1 -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/tests/modal-test.ts +91 -0
- package/backend/tests/notification-test.ts +176 -0
- package/backend/tree/page-tree-element.ts +8 -1
- package/backend/wizard/steps/finisher-step.ts +2 -2
- package/backend/wizard/wizard.ts +11 -4
- 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.ts +3 -8
- package/filelist/file-list-rename-handler.ts +3 -6
- package/filelist/file-list-replace-handler.ts +3 -6
- package/filelist/file-list.ts +3 -4
- package/form/backend/form-editor/modals-component.ts +114 -5
- package/form/backend/form-editor/view-model.ts +1 -1
- package/form/backend/form-wizard/steps/settings-step.ts +6 -3
- package/package.json +2 -2
- package/rte-ckeditor/legacy/ckeditor5-bundle.ts +0 -1
- package/rte-ckeditor/plugin/typo3-link.ts +7 -9
- package/rte-ckeditor/rte-link-browser.ts +1 -1
- package/scheduler/scheduler-add-group.ts +4 -12
- package/workspaces/backend.ts +7 -2
- package/workspaces/preview.ts +39 -0
- package/workspaces/renderable/comment-view.ts +4 -2
- package/workspaces/renderable/record-table.ts +134 -80
- package/workspaces/workspaces.ts +65 -12
package/.last-synced-tag
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v14.3.
|
|
1
|
+
v14.3.8
|
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.8`
|
|
20
20
|
|
|
21
21
|
## 🌿 Branches
|
|
22
22
|
|
|
@@ -160,7 +160,7 @@ export class ColumnSelectorButton extends PseudoButtonLitElement {
|
|
|
160
160
|
text: this.buttonOk,
|
|
161
161
|
btnClass: 'btn-primary',
|
|
162
162
|
name: 'update',
|
|
163
|
-
|
|
163
|
+
form: 'columnSelectorForm',
|
|
164
164
|
}
|
|
165
165
|
],
|
|
166
166
|
ajaxCallback: (): void => this.handleModalContentLoaded(modal)
|
|
@@ -198,7 +198,10 @@ export class ColumnSelectorButton extends PseudoButtonLitElement {
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
// Prevent the form from being submitted as the form data will be send via an ajax request
|
|
201
|
-
form.addEventListener('submit', (e: Event): void => {
|
|
201
|
+
form.addEventListener('submit', (e: Event): void => {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
this.processSelection(currentModal);
|
|
204
|
+
});
|
|
202
205
|
|
|
203
206
|
const columns: NodeListOf<HTMLInputElement> = currentModal.querySelectorAll(Selectors.columnsSelector);
|
|
204
207
|
const columnsFilter: HTMLInputElement = currentModal.querySelector(Selectors.columnsFilterSelector);
|
package/backend/drag-uploader.ts
CHANGED
|
@@ -371,7 +371,21 @@ export default class DragUploader {
|
|
|
371
371
|
* @param {FileList} files
|
|
372
372
|
*/
|
|
373
373
|
public processFiles(files: FileList): void {
|
|
374
|
-
|
|
374
|
+
const validFiles: File[] = [];
|
|
375
|
+
for (const file of Array.from(files)) {
|
|
376
|
+
const error = this.validateFile(file);
|
|
377
|
+
if (error === null) {
|
|
378
|
+
validFiles.push(file);
|
|
379
|
+
} else {
|
|
380
|
+
Notification.error(coreLabels.get('file_upload.notUploaded.title'), error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
this.fileInput.value = '';
|
|
384
|
+
if (validFiles.length === 0) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.queueLength = validFiles.length;
|
|
375
389
|
|
|
376
390
|
if (this.fileList.parentElement.hasAttribute('hidden')) {
|
|
377
391
|
// Show the filelist (table)
|
|
@@ -385,11 +399,11 @@ export default class DragUploader {
|
|
|
385
399
|
this.overallProgressBar = document.createElement('typo3-backend-progress-bar');
|
|
386
400
|
document.body.appendChild(this.overallProgressBar);
|
|
387
401
|
this.overallProgressBar.start();
|
|
388
|
-
this.percentagePerFile = 1 /
|
|
402
|
+
this.percentagePerFile = 1 / validFiles.length;
|
|
389
403
|
|
|
390
404
|
// Check for each file if is already exist before adding it to the queue
|
|
391
405
|
const ajaxCalls: Promise<void>[] = [];
|
|
392
|
-
|
|
406
|
+
validFiles.forEach((file: File) => {
|
|
393
407
|
const request = new AjaxRequest(TYPO3.settings.ajaxUrls.file_exists).withQueryArguments({
|
|
394
408
|
fileName: file.name,
|
|
395
409
|
fileTarget: this.target,
|
|
@@ -418,8 +432,6 @@ export default class DragUploader {
|
|
|
418
432
|
this.overallProgressBar.done();
|
|
419
433
|
}
|
|
420
434
|
});
|
|
421
|
-
|
|
422
|
-
this.fileInput.value = '';
|
|
423
435
|
}
|
|
424
436
|
|
|
425
437
|
public fileInDropzone = (): void => {
|
|
@@ -631,6 +643,37 @@ export default class DragUploader {
|
|
|
631
643
|
document.dispatchEvent(new CustomEvent(uploadFinishedEventName));
|
|
632
644
|
});
|
|
633
645
|
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* @returns the reason why the file cannot be uploaded, null if it can be uploaded
|
|
649
|
+
*/
|
|
650
|
+
private validateFile(file: File): string | null {
|
|
651
|
+
const extension = file.name.split('.').pop().toLowerCase();
|
|
652
|
+
if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
|
|
653
|
+
return coreLabels.get('file_upload.maxFileSizeExceeded', {
|
|
654
|
+
'0': file.name,
|
|
655
|
+
'1': FormatUtility.fileSizeAsString(this.maxFileSize),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
if (this.fileDenyPattern && file.name.match(this.fileDenyPattern)) {
|
|
659
|
+
return coreLabels.get('file_upload.fileNotAllowed', {
|
|
660
|
+
'0': file.name,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (this.filesExtensionsAllowed && !this.filesExtensionsAllowed.split(',').includes(extension)) {
|
|
664
|
+
return coreLabels.get('file_upload.notUploaded.extensionExpected', {
|
|
665
|
+
'0': file.name,
|
|
666
|
+
'1': this.filesExtensionsAllowed.split(',').join(', '),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (this.filesExtensionsDisallowed && this.filesExtensionsDisallowed.split(',').includes(extension)) {
|
|
670
|
+
return coreLabels.get('file_upload.notUploaded.extensionDisallowed', {
|
|
671
|
+
'0': file.name,
|
|
672
|
+
'1': extension,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
634
677
|
}
|
|
635
678
|
|
|
636
679
|
class FileQueueItem {
|
|
@@ -697,72 +740,41 @@ class FileQueueItem {
|
|
|
697
740
|
// set dummy file icon
|
|
698
741
|
this.iconCol.innerHTML = '<typo3-backend-icon identifier="mimetypes-other-other" />';
|
|
699
742
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
'0': this.dragUploader.filesExtensionsAllowed,
|
|
720
|
-
}));
|
|
721
|
-
this.progressBar.value = 100;
|
|
722
|
-
this.progressBar.severity = SeverityEnum.error;
|
|
723
|
-
} else if (this.isProhibitedByDisallowedExtensionsList()) {
|
|
724
|
-
this.updateMessage(coreLabels.get('file_upload.fileExtensionDisallowed', {
|
|
725
|
-
'0': this.dragUploader.filesExtensionsDisallowed,
|
|
726
|
-
}));
|
|
727
|
-
this.progressBar.value = 100;
|
|
728
|
-
this.progressBar.severity = SeverityEnum.error;
|
|
729
|
-
} else {
|
|
730
|
-
this.updateMessage('- ' + FormatUtility.fileSizeAsString(this.file.size));
|
|
731
|
-
|
|
732
|
-
const formData = new FormData();
|
|
733
|
-
formData.append('data[upload][1][target]', this.dragUploader.target);
|
|
734
|
-
formData.append('data[upload][1][data]', '1');
|
|
735
|
-
formData.append('overwriteExistingFiles', this.override);
|
|
736
|
-
formData.append('redirect', '');
|
|
737
|
-
formData.append('upload_1', this.file);
|
|
738
|
-
|
|
739
|
-
// We use XMLHttpRequest as we need the `progress` event which isn't supported by fetch()
|
|
740
|
-
const xhr = new XMLHttpRequest();
|
|
741
|
-
xhr.onreadystatechange = (): void => {
|
|
742
|
-
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
743
|
-
if (xhr.status === 200) {
|
|
744
|
-
try {
|
|
745
|
-
const response = JSON.parse(xhr.responseText);
|
|
746
|
-
if (!response.hasErrors) {
|
|
747
|
-
this.uploadSuccess(response);
|
|
748
|
-
} else {
|
|
749
|
-
this.uploadError(xhr);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
catch {
|
|
753
|
-
// In case JSON can not be parsed, the upload failed due to server errors,
|
|
754
|
-
// e.g. "POST Content-Length exceeds limit". Just handle as upload error.
|
|
743
|
+
this.updateMessage('- ' + FormatUtility.fileSizeAsString(this.file.size));
|
|
744
|
+
|
|
745
|
+
const formData = new FormData();
|
|
746
|
+
formData.append('data[upload][1][target]', this.dragUploader.target);
|
|
747
|
+
formData.append('data[upload][1][data]', '1');
|
|
748
|
+
formData.append('overwriteExistingFiles', this.override);
|
|
749
|
+
formData.append('redirect', '');
|
|
750
|
+
formData.append('upload_1', this.file);
|
|
751
|
+
|
|
752
|
+
// We use XMLHttpRequest as we need the `progress` event which isn't supported by fetch()
|
|
753
|
+
const xhr = new XMLHttpRequest();
|
|
754
|
+
xhr.onreadystatechange = (): void => {
|
|
755
|
+
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
756
|
+
if (xhr.status === 200) {
|
|
757
|
+
try {
|
|
758
|
+
const response = JSON.parse(xhr.responseText);
|
|
759
|
+
if (!response.hasErrors) {
|
|
760
|
+
this.uploadSuccess(response);
|
|
761
|
+
} else {
|
|
755
762
|
this.uploadError(xhr);
|
|
756
763
|
}
|
|
757
|
-
}
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
// In case JSON can not be parsed, the upload failed due to server errors,
|
|
767
|
+
// e.g. "POST Content-Length exceeds limit". Just handle as upload error.
|
|
758
768
|
this.uploadError(xhr);
|
|
759
769
|
}
|
|
770
|
+
} else {
|
|
771
|
+
this.uploadError(xhr);
|
|
760
772
|
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
xhr.upload.addEventListener('progress', (e: ProgressEvent) => this.updateProgress(e));
|
|
776
|
+
xhr.open('POST', TYPO3.settings.ajaxUrls.file_process);
|
|
777
|
+
xhr.send(formData);
|
|
766
778
|
}
|
|
767
779
|
|
|
768
780
|
/**
|
|
@@ -925,26 +937,6 @@ class FileQueueItem {
|
|
|
925
937
|
this.row.append(document.createElement('td'));
|
|
926
938
|
}
|
|
927
939
|
}
|
|
928
|
-
|
|
929
|
-
public isProhibitedByAllowedExtensionsList(): boolean {
|
|
930
|
-
if (!this.dragUploader.filesExtensionsAllowed) {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
const extension = this.file.name.split('.').pop();
|
|
934
|
-
const allowed = this.dragUploader.filesExtensionsAllowed.split(',');
|
|
935
|
-
|
|
936
|
-
return !allowed.includes(extension.toLowerCase());
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
public isProhibitedByDisallowedExtensionsList(): boolean {
|
|
940
|
-
if (!this.dragUploader.filesExtensionsDisallowed) {
|
|
941
|
-
return false;
|
|
942
|
-
}
|
|
943
|
-
const extension = this.file.name.split('.').pop();
|
|
944
|
-
const disallowed = this.dragUploader.filesExtensionsDisallowed.split(',');
|
|
945
|
-
|
|
946
|
-
return disallowed.includes(extension.toLowerCase());
|
|
947
|
-
}
|
|
948
940
|
}
|
|
949
941
|
|
|
950
942
|
DragUploader.init();
|
|
@@ -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
|
}
|
|
@@ -30,14 +30,14 @@ export class OnlineMediaFormElement extends LitElement {
|
|
|
30
30
|
|
|
31
31
|
protected override render(): TemplateResult {
|
|
32
32
|
return html`
|
|
33
|
-
<form @submit="${ this.dispatchSubmitEvent }">
|
|
33
|
+
<form id="online-media-form" @submit="${ this.dispatchSubmitEvent }">
|
|
34
34
|
<div class="form-control-wrap">
|
|
35
|
-
<input type="text" class="form-control" name="online-media-url" placeholder="${this.placeholder}" required>
|
|
35
|
+
<input type="text" class="form-control" name="online-media-url" placeholder="${this.placeholder}" required autofocus>
|
|
36
36
|
<div class="form-text">
|
|
37
37
|
${this.allowedExtensionsHelpText}<br>
|
|
38
38
|
<ul class="badge-list">
|
|
39
39
|
${this.allowedExtensions.split(',').map((ext: string) => html`
|
|
40
|
-
<li><span class="badge badge-secondary">${ext.trim().
|
|
40
|
+
<li><span class="badge badge-secondary">${ext.trim().toLowerCase()}</span></li>
|
|
41
41
|
`)}
|
|
42
42
|
</ul>
|
|
43
43
|
</div>
|
|
@@ -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
|
});
|
|
@@ -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
|
*/
|
|
@@ -954,8 +1033,12 @@ export default (function() {
|
|
|
954
1033
|
};
|
|
955
1034
|
|
|
956
1035
|
/**
|
|
957
|
-
*
|
|
958
|
-
*
|
|
1036
|
+
* Releases the doc header button bar, which is rendered "inert" as long as the
|
|
1037
|
+
* ui block is shown, to prevent unintended operations while FormEngine is loading.
|
|
1038
|
+
* Called after FormEngine initialization is complete.
|
|
1039
|
+
*
|
|
1040
|
+
* The block is applied to the button bar as a whole, never to the single buttons,
|
|
1041
|
+
* so that the disabled state of individual buttons remains untouched.
|
|
959
1042
|
*/
|
|
960
1043
|
FormEngine.enableDocHeaderButtons = function(): void {
|
|
961
1044
|
const docHeaderBar = document.querySelector('.t3js-module-docheader-buttons');
|
|
@@ -963,16 +1046,8 @@ export default (function() {
|
|
|
963
1046
|
return;
|
|
964
1047
|
}
|
|
965
1048
|
|
|
966
|
-
|
|
967
|
-
docHeaderBar.
|
|
968
|
-
if (element instanceof HTMLButtonElement || element instanceof HTMLInputElement) {
|
|
969
|
-
element.disabled = false;
|
|
970
|
-
} else if (element instanceof HTMLAnchorElement) {
|
|
971
|
-
// For anchor elements - remove TYPO3 disabled state
|
|
972
|
-
element.classList.remove('disabled');
|
|
973
|
-
element.removeAttribute('aria-disabled');
|
|
974
|
-
}
|
|
975
|
-
});
|
|
1049
|
+
docHeaderBar.removeAttribute('inert');
|
|
1050
|
+
docHeaderBar.removeAttribute('aria-busy');
|
|
976
1051
|
};
|
|
977
1052
|
|
|
978
1053
|
/**
|
|
@@ -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
|
+
/**
|
|
15
|
+
* Module: @typo3/backend/layout-module/column-restriction
|
|
16
|
+
*
|
|
17
|
+
* Client-side mirror of the server-side backend layout colPos restriction
|
|
18
|
+
* (\TYPO3\CMS\Backend\View\BackendLayoutView::isCTypeAllowedInColPosByPage()).
|
|
19
|
+
* Used by drag-drop and paste in the page layout module to avoid offering
|
|
20
|
+
* actions that the DataHandler would reject anyway.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function toList(value: string | undefined): string[] {
|
|
26
|
+
return (value ?? '')
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((item: string): string => item.trim())
|
|
29
|
+
.filter((item: string): boolean => item !== '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Evaluates whether the given CType is allowed in the column described by the
|
|
34
|
+
* "data-allowed-content-types" / "data-disallowed-content-types" attributes.
|
|
35
|
+
* Mirrors the server-side logic: disallowed wins, an empty allow list allows all.
|
|
36
|
+
*/
|
|
37
|
+
export function isContentTypeAllowedInColumn(cType: string, columnElement: HTMLElement | null): boolean {
|
|
38
|
+
if (columnElement === null) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const disallowed = toList(columnElement.dataset.disallowedContentTypes);
|
|
43
|
+
if (disallowed.includes(cType)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const allowed = toList(columnElement.dataset.allowedContentTypes);
|
|
48
|
+
if (allowed.length > 0 && !allowed.includes(cType)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolves the column of the given element (nearest ancestor carrying a colPos)
|
|
57
|
+
* and evaluates whether the CType is allowed there.
|
|
58
|
+
*/
|
|
59
|
+
export function isContentTypeAllowedForTarget(cType: string, target: HTMLElement): boolean {
|
|
60
|
+
return isContentTypeAllowedInColumn(cType, target.closest('[data-colpos]'));
|
|
61
|
+
}
|