typo3-typescript-sources 14.2.0 → 14.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.github/workflows/sync-and-publish.yml +6 -6
  2. package/.last-synced-tag +1 -1
  3. package/README.md +1 -1
  4. package/backend/clear-cache.ts +7 -7
  5. package/backend/clipboard-panel.ts +20 -17
  6. package/backend/color-picker.ts +44 -36
  7. package/backend/context-menu-actions.ts +7 -7
  8. package/backend/date-time-picker.ts +38 -16
  9. package/backend/element/contextual-record-edit-trigger.ts +4 -2
  10. package/backend/element/editable-page-title.ts +17 -14
  11. package/backend/form-engine/container/inline-control-container.ts +2 -1
  12. package/backend/form-engine/element/datetime-element.ts +1 -1
  13. package/{styleguide/filter.ts → backend/layout-module/page-layout-event.ts} +8 -8
  14. package/backend/layout-module/page-layout.ts +102 -0
  15. package/backend/layout-module/toggle-hidden-element.ts +126 -0
  16. package/backend/layout-module/velocity-scroll.ts +123 -0
  17. package/backend/live-search/live-search.ts +2 -2
  18. package/backend/modal.ts +79 -4
  19. package/backend/page-wizard/helper/wizard-helper.ts +5 -2
  20. package/backend/security/element/csp-reports.ts +1 -6
  21. package/backend/settings/editor/editable-setting.ts +3 -8
  22. package/backend/settings/type/base.ts +1 -1
  23. package/{setup → backend}/setup-module.ts +3 -3
  24. package/backend/toolbar/clear-cache-menu.ts +11 -4
  25. package/backend/tree/page-position-select.ts +52 -41
  26. package/backend/tree/tree.ts +27 -6
  27. package/dashboard/dashboard.ts +0 -3
  28. package/form/backend/form-editor/inspector-component.ts +1 -1
  29. package/form/backend/form-editor/stage-component.ts +3 -3
  30. package/form/backend/form-manager/view-model.ts +15 -13
  31. package/linkvalidator/linkvalidator.ts +13 -46
  32. package/package.json +2 -2
  33. package/workspaces/renderable/record-table.ts +1 -1
  34. package/backend/page-actions.ts +0 -91
@@ -90,6 +90,7 @@ jobs:
90
90
  echo "New version found: $NEW_TAG (previous: $LAST_SYNCED)"
91
91
  echo "has_new_version=true" >> $GITHUB_OUTPUT
92
92
  echo "typo3_version=$NEW_TAG" >> $GITHUB_OUTPUT
93
+ echo "release_tag=${NEW_TAG#v}" >> $GITHUB_OUTPUT
93
94
  fi
94
95
 
95
96
  - name: Setup Git
@@ -271,12 +272,11 @@ jobs:
271
272
  Source: https://github.com/TYPO3/typo3/tree/${TYPO3_VERSION}
272
273
  Path: Build/Sources/TypeScript/"
273
274
 
274
- # Create tag (remove v-prefix)
275
- NPM_VERSION="${TYPO3_VERSION#v}"
276
- git tag -a "${NPM_VERSION}" -m "Release ${NPM_VERSION} from TYPO3 ${TYPO3_VERSION}"
275
+ RELEASE_TAG="${{ steps.check.outputs.release_tag }}"
276
+ git tag -a "${RELEASE_TAG}" -m "Release ${RELEASE_TAG} from TYPO3 ${TYPO3_VERSION}"
277
277
 
278
278
  git push origin "${TARGET_BRANCH}"
279
- git push origin "${NPM_VERSION}"
279
+ git push origin "${RELEASE_TAG}"
280
280
 
281
281
  - name: Setup Node.js
282
282
  if: steps.check.outputs.has_new_version == 'true'
@@ -306,8 +306,8 @@ jobs:
306
306
  env:
307
307
  GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
308
308
  with:
309
- tag_name: ${{ steps.check.outputs.typo3_version }}
310
- release_name: TYPO3 ${{ steps.check.outputs.typo3_version }}
309
+ tag_name: ${{ steps.check.outputs.release_tag }}
310
+ release_name: TYPO3 ${{ steps.check.outputs.release_tag }}
311
311
  body: |
312
312
  Synchronized with TYPO3 Core ${{ steps.check.outputs.typo3_version }}
313
313
 
package/.last-synced-tag CHANGED
@@ -1 +1 @@
1
- v14.2.0
1
+ v14.3.1
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.2.0`
19
+ - **Current TYPO3 Version:** `v14.3.1`
20
20
 
21
21
  ## 🌿 Branches
22
22
 
@@ -46,15 +46,15 @@ class ClearCache {
46
46
  const request = new AjaxRequest(TYPO3.settings.ajaxUrls.clearcache_page).post({ id: pageId });
47
47
  request.then(async (response: AjaxResponse): Promise<void> => {
48
48
  const data = await response.resolve();
49
- if (data.success === true) {
50
- Notification.success(
51
- data.title,
52
- data.message
49
+ if (data?.success === false) {
50
+ Notification.error(
51
+ data.title ?? cacheLabels.get('notification.error.title'),
52
+ data.message ?? cacheLabels.get('notification.error.message'),
53
53
  );
54
54
  } else {
55
- Notification.error(
56
- data.title,
57
- data.message
55
+ Notification.success(
56
+ data?.title ?? cacheLabels.get('notification.success.title'),
57
+ data?.message ?? cacheLabels.get('notification.success.message'),
58
58
  );
59
59
  }
60
60
  }, (): void => {
@@ -143,23 +143,26 @@ export class ClipboardPanel extends LitElement {
143
143
  </td>
144
144
  <td class="col-control nowrap">
145
145
  ${clipboardData.current !== tab.identifier ? nothing : html`
146
- <div class="btn-group">
147
- <input type="radio" class="btn-check" id="clipboard-copymode-copy" data-action="setCopyMode" ?checked=${clipboardData.copyMode === CopyMode.copy} @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'setCopyMode': '1' } })}">
148
- <label class="btn btn-default btn-sm" for="clipboard-copymode-copy">
149
- <typo3-backend-icon identifier="actions-edit-copy" size="small"></typo3-backend-icon>
150
- ${clipboardData.labels.copyElements}
151
- </label>
152
- <input type="radio" class="btn-check" id="clipboard-copymode-move" data-action="setCopyMode" ?checked=${clipboardData.copyMode !== CopyMode.copy} @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'setCopyMode': '0' } })}">
153
- <label class="btn btn-default btn-sm" for="clipboard-copymode-move">
154
- <typo3-backend-icon identifier="actions-cut" size="small"></typo3-backend-icon>
155
- ${clipboardData.labels.moveElements}
156
- </label>
146
+ <div class="btn-toolbar btn-toolbar-nowrap">
147
+ <div class="btn-group">
148
+ <input type="radio" class="btn-check" id="clipboard-copymode-copy" data-action="setCopyMode" ?checked=${clipboardData.copyMode === CopyMode.copy} @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'setCopyMode': '1' } })}">
149
+ <label class="btn btn-default btn-sm" for="clipboard-copymode-copy">
150
+ <typo3-backend-icon identifier="actions-edit-copy" size="small"></typo3-backend-icon>
151
+ ${clipboardData.labels.copyElements}
152
+ </label>
153
+ <input type="radio" class="btn-check" id="clipboard-copymode-move" data-action="setCopyMode" ?checked=${clipboardData.copyMode !== CopyMode.copy} @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'setCopyMode': '0' } })}">
154
+ <label class="btn btn-default btn-sm" for="clipboard-copymode-move">
155
+ <typo3-backend-icon identifier="actions-cut" size="small"></typo3-backend-icon>
156
+ ${clipboardData.labels.moveElements}
157
+ </label>
158
+ </div>
159
+ ${!clipboardData.elementCount ? nothing : html`
160
+ <button type="button" class="btn btn-default btn-sm" title="${clipboardData.labels.removeAll}" data-action="removeAll" @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'removeAll': tab.identifier } })}">
161
+ <typo3-backend-icon identifier="actions-minus" size="small"></typo3-backend-icon>
162
+ ${clipboardData.labels.removeAll}
163
+ </button>
164
+ `}
157
165
  </div>
158
- ${!clipboardData.elementCount ? nothing : html`
159
- <button type="button" class="btn btn-default btn-sm" title="${clipboardData.labels.removeAll}" data-action="removeAll" @click="${(event: PointerEvent) => this.updateClipboard(event, { CB: { 'removeAll': tab.identifier } })}">
160
- <typo3-backend-icon identifier="actions-minus" size="small"></typo3-backend-icon>
161
- ${clipboardData.labels.removeAll}
162
- </button>`}
163
166
  `}
164
167
  </td>
165
168
  </tr>
@@ -173,7 +176,7 @@ export class ClipboardPanel extends LitElement {
173
176
  <td class="col-icon nowrap ${classMap({ 'ps-4': !tabItem.identifier })}">
174
177
  ${unsafeHTML(tabItem.icon)}
175
178
  </td>
176
- <td class="nowrap" style="width: 95%">
179
+ <td class="col-title col-responsive nowrap">
177
180
  ${unsafeHTML(tabItem.title)}
178
181
  ${tabIdentifier === 'normal' ? html`<strong>(${clipboardData.copyMode === CopyMode.copy ? html`${clipboardData.labels.copy}` : html`${clipboardData.labels.cut}`})</strong>` : nothing}
179
182
  ${tabItem.thumb ? html`<div class="d-block">${unsafeHTML(tabItem.thumb)}</div>` : nothing}
@@ -67,45 +67,53 @@ export class Typo3BackendColorPicker extends LitElement {
67
67
  await DocumentService.ready();
68
68
 
69
69
  const inputElement = this.getInputElement();
70
- if (inputElement) {
71
- if (!inputElement.value && this.color) {
72
- inputElement.value = this.color;
73
- } else {
74
- this.color = inputElement.value;
75
- }
70
+ if (!inputElement) {
71
+ return;
72
+ }
76
73
 
77
- if (inputElement.disabled || inputElement.readOnly) {
78
- return;
79
- }
74
+ if (!inputElement.value && this.color) {
75
+ inputElement.value = this.color;
76
+ } else {
77
+ this.color = inputElement.value;
78
+ }
80
79
 
81
- const alwan = new Alwan(inputElement, {
82
- position: 'bottom-start',
83
- format: 'hex',
84
- opacity: this.opacity,
85
- swatches: this.swatches,
86
- preset: false,
87
- color: this.color,
88
- // Casting to `unknown` to prevent TypeScript Error
89
- // > TS2589: Type instantiation is excessively deep and possibly infinite.
90
- parent: (this.closest('dialog') ?? '') as unknown,
91
- });
92
-
93
- alwan.on('color', (e): void => {
94
- this.color = e.hex;
95
- inputElement.value = this.color;
96
- inputElement.dispatchEvent(new Event('blur'));
97
- });
98
-
99
- // input: react on user input
100
- // change: react on indirect changes, e.g. a value picker
101
- ['input', 'change'].forEach((eventName: string): void => {
102
- new RegularEvent(eventName, (e: Event): void => {
103
- const input = (e.target as HTMLInputElement);
104
- this.color = input.value;
105
- alwan.setColor(this.color);
106
- }).bindTo(inputElement);
107
- });
80
+ if (inputElement.disabled || inputElement.readOnly) {
81
+ return;
108
82
  }
83
+
84
+ const alwan = new Alwan(inputElement, {
85
+ position: 'bottom-start',
86
+ format: 'hex',
87
+ opacity: this.opacity,
88
+ swatches: this.swatches,
89
+ preset: false,
90
+ color: this.color,
91
+ // Casting to `unknown` to prevent TypeScript Error
92
+ // > TS2589: Type instantiation is excessively deep and possibly infinite.
93
+ parent: (this.closest('dialog') ?? '') as unknown,
94
+ });
95
+
96
+ // When the color picker opens, the input loses focus, making value changes difficult.
97
+ // Restore focus to the input after the picker is opened.
98
+ alwan.on('open', (): void => {
99
+ inputElement.focus();
100
+ });
101
+
102
+ alwan.on('color', (e): void => {
103
+ this.color = e.hex;
104
+ inputElement.value = this.color;
105
+ inputElement.dispatchEvent(new Event('blur'));
106
+ });
107
+
108
+ // input: react on user input
109
+ // change: react on indirect changes, e.g. a value picker
110
+ ['input', 'change'].forEach((eventName: string): void => {
111
+ new RegularEvent(eventName, (e: Event): void => {
112
+ const input = (e.target as HTMLInputElement);
113
+ this.color = input.value;
114
+ alwan.setColor(this.color);
115
+ }).bindTo(inputElement);
116
+ });
109
117
  }
110
118
 
111
119
  protected override render(): TemplateResult {
@@ -304,15 +304,15 @@ class ContextMenuActions {
304
304
  (new AjaxRequest(TYPO3.settings.ajaxUrls.clearcache_page)).post({ id: uid }).then(
305
305
  async (response: AjaxResponse): Promise<void> => {
306
306
  const data = await response.resolve();
307
- if (data.success === true) {
308
- Notification.success(
309
- data.title,
310
- data.message
307
+ if (data?.success === false) {
308
+ Notification.error(
309
+ data.title ?? cacheLabels.get('notification.error.title'),
310
+ data.message ?? cacheLabels.get('notification.error.message'),
311
311
  );
312
312
  } else {
313
- Notification.error(
314
- data.title,
315
- data.message
313
+ Notification.success(
314
+ data?.title ?? cacheLabels.get('notification.success.title'),
315
+ data?.message ?? cacheLabels.get('notification.success.message'),
316
316
  );
317
317
  }
318
318
  },
@@ -15,6 +15,7 @@ import flatpickr from 'flatpickr';
15
15
  import Persistent from '@typo3/backend/storage/persistent';
16
16
  import ShortcutButtonsPlugin from 'shortcut-buttons-flatpickr';
17
17
  import { DateTime } from 'luxon';
18
+ import DomHelper from '@typo3/backend/utility/dom-helper';
18
19
  import ThrottleEvent from '@typo3/core/event/throttle-event';
19
20
  import type { PostValidationEvent } from '@typo3/backend/form-engine-validation';
20
21
  import type { DateConfiguration } from '@typo3/backend/type/date-configuration';
@@ -39,7 +40,7 @@ class DateTimePicker {
39
40
  * note: this function can be called multiple times (e.g. after AJAX requests) because it only
40
41
  * applies to fields which haven't been used yet.
41
42
  */
42
- public initialize(element: HTMLInputElement): void {
43
+ public initialize(element: HTMLInputElement, appendTo: HTMLElement | null = null): void {
43
44
  if (!(element instanceof HTMLInputElement) || typeof element.dataset.datepickerInitialized !== 'undefined') {
44
45
  return;
45
46
  }
@@ -55,22 +56,24 @@ class DateTimePicker {
55
56
 
56
57
  element.dataset.datepickerInitialized = '1';
57
58
  import('flatpickr/dist/l10n').then((): void => {
58
- this.initializeField(element, userLocale as flatpickr.Options.LocaleKey);
59
+ this.initializeField(element, userLocale as flatpickr.Options.LocaleKey, appendTo);
59
60
  });
60
61
  }
61
62
 
62
63
  /**
63
64
  * Initialize a single field
64
65
  */
65
- private initializeField(inputElement: HTMLInputElement, locale: flatpickr.Options.LocaleKey): void {
66
- const scrollEvent = this.getScrollEvent();
66
+ private initializeField(inputElement: HTMLInputElement, locale: flatpickr.Options.LocaleKey, appendTo: HTMLElement | null = null): void {
67
67
  const options = this.getDateOptions(inputElement);
68
68
  options.locale = locale;
69
+ if (appendTo) {
70
+ options.appendTo = appendTo;
71
+ }
69
72
 
70
73
  // Custom "first day of week" user preference
71
74
  const dow = Persistent.get('dateTimeFirstDayOfWeek');
72
75
 
73
- if (dow !== '') {
76
+ if (dow != null && dow !== '') {
74
77
  // stored number is 1-index based, convert to 0-index.
75
78
  const dowNumber = parseInt(dow, 10) - 1;
76
79
 
@@ -80,14 +83,35 @@ class DateTimePicker {
80
83
  flatpickr.l10ns.default.firstDayOfWeek = dowNumber;
81
84
  }
82
85
 
83
- options.onOpen = [
84
- (): void => {
85
- scrollEvent.bindTo(document.querySelector('.t3js-module-body'));
86
- }
87
- ];
88
- options.onClose = (): void => {
89
- scrollEvent.release();
90
- };
86
+ const usePopover = appendTo instanceof HTMLElement && appendTo.localName === 'typo3-formengine-element-datetime';
87
+
88
+ if (usePopover) {
89
+ // Disable flatpickr's own positioning; native popover + CSS anchor positioning handle it.
90
+ options.position = (): void => { /* handled by popover */ };
91
+ options.onOpen = [
92
+ (_dates: Date[], _currentDateString: string, self: flatpickr.Instance): void => {
93
+ if (!self.calendarContainer.hasAttribute('popover')) {
94
+ self.calendarContainer.setAttribute('popover', 'manual');
95
+ }
96
+ self.calendarContainer.showPopover();
97
+ }
98
+ ];
99
+ options.onClose = (_dates: Date[], _currentDateString: string, self: flatpickr.Instance): void => {
100
+ if (self.calendarContainer.matches(':popover-open')) {
101
+ self.calendarContainer.hidePopover();
102
+ }
103
+ };
104
+ } else {
105
+ const scrollEvent = this.getScrollEvent();
106
+ options.onOpen = [
107
+ (): void => {
108
+ scrollEvent.bindTo(DomHelper.scrollEventTarget(inputElement));
109
+ }
110
+ ];
111
+ options.onClose = (): void => {
112
+ scrollEvent.release();
113
+ };
114
+ }
91
115
 
92
116
  // initialize the date time picker on this element
93
117
  const dateTimePicker = flatpickr(inputElement, options);
@@ -116,9 +140,7 @@ class DateTimePicker {
116
140
 
117
141
  /**
118
142
  * Due to some whack CSS the scrollPosition of the document stays 0 which renders a stuck date time picker.
119
- * Because of this the position is recalculated on scrolling `.t3js-module-body`.
120
- *
121
- * @return {ThrottleEvent}
143
+ * Because of this the position is recalculated on scrolling the nearest scrollable ancestor.
122
144
  */
123
145
  private getScrollEvent(): ThrottleEvent {
124
146
  return new ThrottleEvent('scroll', (): void => {
@@ -61,12 +61,14 @@ export class ContextualRecordEditTriggerElement extends PseudoButtonLitElement {
61
61
  private setupMessageHandling(modal: ModalElement): void {
62
62
  const messageTarget = top;
63
63
  let savedRecordTitle = '';
64
+ let hasSaved = false;
64
65
  let closeConfirmed = false;
65
66
  const messageHandler = (event: MessageEvent): void => {
66
67
  if (event.origin !== window.location.origin) {
67
68
  return;
68
69
  }
69
70
  if (event.data?.actionName === 'typo3:editform:saved') {
71
+ hasSaved = true;
70
72
  savedRecordTitle = event.data.recordTitle ?? '';
71
73
  }
72
74
  if (event.data?.actionName === 'typo3:editform:closed') {
@@ -94,14 +96,14 @@ export class ContextualRecordEditTriggerElement extends PseudoButtonLitElement {
94
96
 
95
97
  modal.addEventListener('typo3-modal-hidden', () => {
96
98
  messageTarget.removeEventListener('message', messageHandler);
97
- if (savedRecordTitle !== '') {
99
+ if (hasSaved) {
98
100
  top.document.dispatchEvent(new CustomEvent('typo3:pagetree:refresh'));
99
101
  if (top.TYPO3?.Backend?.ContentContainer) {
100
102
  top.TYPO3.Backend.ContentContainer.refresh();
101
103
  }
102
104
  Notification.success(
103
105
  labels.get('notification.record_updated.title'),
104
- labels.get('notification.record_updated.message', [savedRecordTitle]),
106
+ savedRecordTitle !== '' ? labels.get('notification.record_updated.message', [savedRecordTitle]) : undefined,
105
107
  );
106
108
  } else {
107
109
  this.focus();
@@ -84,13 +84,19 @@ export class EditablePageTitle extends LitElement {
84
84
  .wrapper {
85
85
  position: relative;
86
86
  margin: -1px 0;
87
+ padding-inline-end: 1.5em;
87
88
  }
88
89
 
89
- div.wrapper {
90
- padding-inline-end: 1.5em;
90
+ .wrapper:has(> form) .page-title {
91
+ visibility: hidden;
92
+ pointer-events: none;
91
93
  }
92
94
 
93
- form.wrapper {
95
+ .wrapper > form {
96
+ position: absolute;
97
+ top: 0;
98
+ left: 0;
99
+ right: 0;
94
100
  padding-inline-end: 2.5em;
95
101
  }
96
102
 
@@ -190,18 +196,15 @@ export class EditablePageTitle extends LitElement {
190
196
  return html`<div class="wrapper"><h1>${this.pageTitle}</h1></div>`;
191
197
  }
192
198
 
193
- let content;
194
- if (!this._isEditing) {
195
- content = html`
196
- <div class="wrapper">
199
+ return html`
200
+ <div class="wrapper">
201
+ <div class="page-title">
197
202
  <h1 @dblclick="${(): void => { this.startEditing(); }}">${this.pageTitle}</h1>
198
203
  ${this.composeEditButton()}
199
- </div>`;
200
- } else {
201
- content = this.composeEditForm();
202
- }
203
-
204
- return content;
204
+ </div>
205
+ ${this._isEditing ? this.composeEditForm() : nothing}
206
+ </div>
207
+ `;
205
208
  }
206
209
 
207
210
  private isEditable(): boolean {
@@ -268,7 +271,7 @@ export class EditablePageTitle extends LitElement {
268
271
 
269
272
  private composeEditForm(): TemplateResult {
270
273
  return html`
271
- <form class="wrapper" @submit="${ this.updatePageTitle }">
274
+ <form @submit="${ this.updatePageTitle }">
272
275
  <label class="screen-reader" for="input">${this.labels.input}</label>
273
276
  <input
274
277
  autocomplete="off"
@@ -477,7 +477,8 @@ class InlineControlContainer extends HTMLElement {
477
477
 
478
478
  if (hiddenValueCheckBox !== null && hiddenValueInput !== null) {
479
479
  hiddenValueCheckBox.checked = !hiddenValueCheckBox.checked;
480
- hiddenValueInput.value = hiddenValueCheckBox.checked ? '1' : '0';
480
+ const active = hiddenValueCheckBox.checked !== (hiddenValueCheckBox.dataset.invertStateDisplay === 'true');
481
+ hiddenValueInput.value = active ? '1' : '0';
481
482
  FormEngine.markFieldAsChanged(hiddenValueCheckBox);
482
483
  }
483
484
 
@@ -47,7 +47,7 @@ class DatetimeElement extends HTMLElement {
47
47
 
48
48
  this.registerEventHandler();
49
49
  import('../../date-time-picker').then(({ default: DateTimePicker }): void => {
50
- DateTimePicker.initialize(this.element);
50
+ DateTimePicker.initialize(this.element, this);
51
51
  });
52
52
  }
53
53
 
@@ -11,12 +11,12 @@
11
11
  * The TYPO3 project - inspiring people to share!
12
12
  */
13
13
 
14
- import DocumentService from '@typo3/core/document-service';
15
- import DateTimePicker from '@typo3/backend/date-time-picker';
14
+ export class HiddenContentCountChangedEvent extends CustomEvent<{ count: number }> {
15
+ static readonly eventName = 'typo3:page-layout:hidden-content-count-changed';
16
16
 
17
- DocumentService.ready().then(() => {
18
- const dateTimePickers: NodeListOf<HTMLInputElement>|null = document.querySelectorAll('.t3js-datetimepicker');
19
- dateTimePickers?.forEach((element) => {
20
- DateTimePicker.initialize(element);
21
- });
22
- });
17
+ constructor(count: number) {
18
+ super(HiddenContentCountChangedEvent.eventName, {
19
+ detail: { count },
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,102 @@
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 AjaxRequest from '@typo3/core/ajax/ajax-request';
15
+ import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
16
+ import Notification from '@typo3/backend/notification';
17
+ import RegularEvent from '@typo3/core/event/regular-event';
18
+ import { sudoModeInterceptor } from '@typo3/backend/security/sudo-mode-interceptor';
19
+ import layoutLabels from '~labels/backend.layout';
20
+ import { HiddenContentCountChangedEvent } from './page-layout-event';
21
+ import '@typo3/backend/element/icon-element';
22
+ import type { PageLayoutToggleHidden } from './toggle-hidden-element';
23
+
24
+ /**
25
+ * Module: @typo3/backend/layout-module/page-layout
26
+ *
27
+ * Main entry point for the page layout module.
28
+ * Registers all web components and provides inline visibility toggling.
29
+ */
30
+ new RegularEvent('click', async (event: Event, delegateTarget?: Element): Promise<void> => {
31
+ event.preventDefault();
32
+ event.stopPropagation();
33
+
34
+ const target = delegateTarget as HTMLButtonElement;
35
+ if (target.disabled) {
36
+ return;
37
+ }
38
+ target.disabled = true;
39
+
40
+ const contentElement = target.closest('.t3js-page-ce') as HTMLElement | null;
41
+ if (contentElement === null) {
42
+ target.disabled = false;
43
+ return;
44
+ }
45
+
46
+ const table = contentElement.dataset.table;
47
+ const uid = parseInt(contentElement.dataset.uid ?? '0', 10);
48
+ const isHidden = contentElement.classList.contains('t3js-hidden-record');
49
+ const iconElement = target.querySelector('typo3-backend-icon');
50
+
51
+ try {
52
+ const response: AjaxResponse = await new AjaxRequest(TYPO3.settings.ajaxUrls.record_toggle_visibility)
53
+ .addMiddleware(sudoModeInterceptor)
54
+ .post({
55
+ table: table,
56
+ uid: uid,
57
+ action: isHidden ? 'show' : 'hide',
58
+ });
59
+
60
+ const data = await response.resolve();
61
+
62
+ // Update hidden state classes on the content element wrapper
63
+ contentElement.classList.toggle('t3-page-ce-hidden', !data.isVisible);
64
+ contentElement.classList.toggle('t3js-hidden-record', !data.isVisible);
65
+
66
+ // If the element was just hidden and the "show hidden" toggle is inactive,
67
+ // hide the element from view so it disappears from the layout.
68
+ if (!data.isVisible) {
69
+ const toggleHidden = document.querySelector('typo3-backend-page-layout-toggle-hidden') as PageLayoutToggleHidden | null;
70
+ if (toggleHidden && !toggleHidden.active) {
71
+ contentElement.style.display = 'none';
72
+ }
73
+ } else {
74
+ contentElement.style.display = '';
75
+ }
76
+
77
+ // Update toggle button icon and title
78
+ iconElement?.setAttribute('identifier', data.isVisible ? 'actions-edit-hide' : 'actions-edit-unhide');
79
+ target.title = data.isVisible ? layoutLabels.get('hide') : layoutLabels.get('unHide');
80
+
81
+ // Update record icon in context menu trigger (reflects hidden overlay)
82
+ const recordIconElement = contentElement.querySelector('.t3-page-ce-header-left [data-contextmenu-trigger] .t3js-icon');
83
+ if (recordIconElement && data.icon) {
84
+ recordIconElement.replaceWith(document.createRange().createContextualFragment(data.icon));
85
+ }
86
+
87
+ // Update hidden element counter in the toolbar toggle button
88
+ const count = document.querySelectorAll('.t3js-hidden-record').length;
89
+ document.dispatchEvent(new HiddenContentCountChangedEvent(count));
90
+ } catch (error) {
91
+ if (error && typeof (error as AjaxResponse).resolve === 'function') {
92
+ const data = await (error as AjaxResponse).resolve();
93
+ for (const message of data.messages ?? []) {
94
+ Notification.error(message.title, message.message);
95
+ }
96
+ }
97
+ } finally {
98
+ target.disabled = false;
99
+ }
100
+ }).delegateTo(document, 'button[data-action="content-element-visibility-toggle"]');
101
+
102
+ export { HiddenContentCountChangedEvent } from './page-layout-event';