typo3-typescript-sources 14.2.0 → 14.3.0

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.
@@ -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.0
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.0`
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 => {
@@ -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,17 +56,19 @@ 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');
@@ -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"
@@ -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';
@@ -0,0 +1,126 @@
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 { html, type TemplateResult } from 'lit';
15
+ import { customElement, property } from 'lit/decorators.js';
16
+ import { PseudoButtonLitElement } from '@typo3/backend/element/pseudo-button';
17
+ import PersistentStorage from '@typo3/backend/storage/persistent';
18
+ import layoutLabels from '~labels/backend.layout';
19
+ import '@typo3/backend/element/icon-element';
20
+ import { HiddenContentCountChangedEvent } from './page-layout-event';
21
+
22
+ /**
23
+ * Module: @typo3/backend/layout-module/toggle-hidden-element
24
+ *
25
+ * Toolbar toggle button for showing/hiding hidden content elements
26
+ * in the page module. Updates its counter reactively when content
27
+ * elements are toggled.
28
+ */
29
+ @customElement('typo3-backend-page-layout-toggle-hidden')
30
+ export class PageLayoutToggleHidden extends PseudoButtonLitElement {
31
+ @property({ type: Boolean, reflect: true }) active: boolean = false;
32
+ @property({ type: Number }) count: number = 0;
33
+
34
+ public override connectedCallback(): void {
35
+ super.connectedCallback();
36
+ this.syncDropdownToggleStatus();
37
+ document.addEventListener(HiddenContentCountChangedEvent.eventName, this.onCountChanged);
38
+ }
39
+
40
+ public override disconnectedCallback(): void {
41
+ super.disconnectedCallback();
42
+ document.removeEventListener(HiddenContentCountChangedEvent.eventName, this.onCountChanged);
43
+ }
44
+
45
+ public override updated(changedProperties: Map<string, unknown>): void {
46
+ if (changedProperties.has('active')) {
47
+ this.syncDropdownToggleStatus();
48
+ }
49
+ }
50
+
51
+ protected override createRenderRoot(): HTMLElement | ShadowRoot {
52
+ return this;
53
+ }
54
+
55
+ protected override render(): TemplateResult {
56
+ return html`<span class="dropdown-item-status"></span><typo3-backend-icon identifier="actions-eye" size="small"></typo3-backend-icon>${layoutLabels.get('hiddenCE')} (${this.count})`;
57
+ }
58
+
59
+ protected override buttonActivated(): void {
60
+ if (this.hasAttribute('disabled')) {
61
+ return;
62
+ }
63
+ this.setAttribute('disabled', '');
64
+
65
+ const show = !this.active;
66
+ const hiddenElements = document.querySelectorAll('.t3js-hidden-record') as NodeListOf<HTMLElement>;
67
+
68
+ for (const hiddenElement of hiddenElements) {
69
+ hiddenElement.style.display = 'flow-root';
70
+ const scrollHeight = hiddenElement.scrollHeight;
71
+
72
+ // Always set `overflow: clip` after storing scrollHeight
73
+ // * For hidden state `height: 0px` is already set.
74
+ // * For visible state setting `overflow: clip` is fine anyway.
75
+ hiddenElement.style.overflow = 'clip';
76
+
77
+ if (!show) {
78
+ // * Invisible elements must not be accessible/focusable by keyboard.
79
+ // * Spacing between content elements is kept uniform by collapsed margins,
80
+ // hidden elements have a height of 0 and the margins of the surrounding elements
81
+ // cannot collapse, causing a visual gap.
82
+ // Therefore do not display the element at all by setting `display: none`.
83
+ hiddenElement.addEventListener('transitionend', (): void => {
84
+ hiddenElement.style.display = 'none';
85
+ hiddenElement.style.overflow = '';
86
+ }, { once: true });
87
+
88
+ // We use requestAnimationFrame() as we have to set the container's height at first before resizing to
89
+ // collapsed-element-height. This results in a smooth animation.
90
+ requestAnimationFrame((): void => {
91
+ hiddenElement.style.height = scrollHeight + 'px';
92
+ requestAnimationFrame((): void => {
93
+ hiddenElement.style.height = '0px';
94
+ });
95
+ });
96
+ } else {
97
+ hiddenElement.addEventListener('transitionend', (): void => {
98
+ hiddenElement.style.display = '';
99
+ hiddenElement.style.overflow = '';
100
+ hiddenElement.style.height = '';
101
+ }, { once: true });
102
+
103
+ hiddenElement.style.height = scrollHeight + 'px';
104
+ }
105
+ }
106
+
107
+ this.active = show;
108
+ PersistentStorage.set('moduleData.web_layout.showHidden', show ? '1' : '0').then((): void => {
109
+ this.removeAttribute('disabled');
110
+ });
111
+ }
112
+
113
+ private syncDropdownToggleStatus(): void {
114
+ this.dataset.dropdowntoggleStatus = this.active ? 'active' : 'inactive';
115
+ }
116
+
117
+ private readonly onCountChanged = (event: Event): void => {
118
+ this.count = (event as HiddenContentCountChangedEvent).detail.count;
119
+ };
120
+ }
121
+
122
+ declare global {
123
+ interface HTMLElementTagNameMap {
124
+ 'typo3-backend-page-layout-toggle-hidden': PageLayoutToggleHidden;
125
+ }
126
+ }
@@ -133,16 +133,11 @@ export class CspReports extends LitElement {
133
133
 
134
134
  protected override render(): TemplateResult {
135
135
  return html`
136
- <div class="infolist-container infolist-overlay">
136
+ <div class="infolist-container">
137
137
  <div class="infolist">
138
138
  <div class="infolist-header">
139
139
  ${this.renderNavigation()}
140
140
  </div>
141
- </div>
142
- </div>
143
-
144
- <div class="infolist-container">
145
- <div class="infolist">
146
141
  <div class="infolist-content">
147
142
  <div class="table-fit mb-0">
148
143
  <table class="table table-striped">
@@ -35,8 +35,7 @@ interface SettingDefinition {
35
35
  label: string,
36
36
  description: string|null,
37
37
  readonly: boolean,
38
- // @todo php json_encode encodes ['0' => 'foo'] as ['foo'] instead of {'0' => 'foo'}
39
- enum: Record<string, string>|Array<string>,
38
+ enum: Record<string, string>,
40
39
  categories: string[],
41
40
  tags: string[],
42
41
  options: Record<string, unknown>,
@@ -119,10 +118,6 @@ export class EditableSettingElement extends LitElement {
119
118
 
120
119
  protected updateFieldAttributes(element: HTMLElement): void {
121
120
  const { definition, value } = this.setting;
122
- // Force conversion to an object, as PHP json_encode encodes ['0' => 'foo'] as
123
- // ['foo'] instead of {'0' => 'foo'}
124
- const enumEntries = Object.entries(definition.enum || {});
125
-
126
121
  const attributes = {
127
122
  key: definition.key,
128
123
  formid: `setting-${definition.key}`,
@@ -130,9 +125,9 @@ export class EditableSettingElement extends LitElement {
130
125
  value: Array.isArray(value) ? JSON.stringify(value) : String(value),
131
126
  debug: this.mode === SettingsMode.advanced,
132
127
  readonly: definition.readonly,
133
- enum: enumEntries.length > 0 ? JSON.stringify(Object.fromEntries(enumEntries)) : false,
128
+ enum: Object.keys(definition.enum).length > 0 ? JSON.stringify(definition.enum) : false,
134
129
  default: Array.isArray(definition.default) ? JSON.stringify(definition.default) : String(definition.default),
135
- options: definition.options ? (Array.isArray(definition.options) && definition.options.length === 0 ? '{}' : JSON.stringify(definition.options)) : '{}',
130
+ options: JSON.stringify(definition.options),
136
131
  };
137
132
  for (const [key, value] of Object.entries(attributes)) {
138
133
  if (typeof value === 'boolean') {
@@ -36,7 +36,7 @@ export abstract class BaseElement<
36
36
  @property({ type: String }) key: string;
37
37
  @property({ type: String }) formid: string;
38
38
  @property({ type: Boolean }) readonly: boolean = false;
39
- @property({ type: Object }) enum: Record<string, string>;
39
+ @property({ type: Object }) enum: Record<string, string> | undefined;
40
40
  @property({ type: Boolean }) debug: boolean = false;
41
41
  @property({ type: Object }) options: OptionsType;
42
42
 
@@ -18,8 +18,8 @@ import RegularEvent from '@typo3/core/event/regular-event';
18
18
  import Modal from '@typo3/backend/modal';
19
19
 
20
20
  /**
21
- * Module: @typo3/setup/setup-module
22
- * @exports @typo3/setup/setup-module
21
+ * Module: @typo3/backend/setup-module
22
+ * @exports @typo3/backend/setup-module
23
23
  */
24
24
  class SetupModule {
25
25
  private avatarWindowRef: Window;
@@ -71,10 +71,17 @@ class ClearCacheMenu {
71
71
  (new AjaxRequest(ajaxUrl)).post({}).then(
72
72
  async (response: AjaxResponse): Promise<void> => {
73
73
  const data = await response.resolve();
74
- Notification.success(
75
- data.title,
76
- data.message
77
- );
74
+ if (data?.success === false) {
75
+ Notification.error(
76
+ data.title ?? cacheLabels.get('notification.error.title'),
77
+ data.message ?? cacheLabels.get('notification.error.message'),
78
+ );
79
+ } else {
80
+ Notification.success(
81
+ data?.title ?? cacheLabels.get('notification.success.title'),
82
+ data?.message ?? cacheLabels.get('notification.success.message'),
83
+ );
84
+ }
78
85
  },
79
86
  (): void => {
80
87
  Notification.error(
@@ -108,31 +108,17 @@ export class PagePositionSelect extends LitElement {
108
108
  }
109
109
 
110
110
  protected renderTree(): TemplateResult {
111
- const initialized = () => {
111
+ const initialized = async () => {
112
112
  // set up toolbar now with updated properties
113
113
  const toolbar = this.querySelector('typo3-backend-tree-toolbar') as TreeToolbar;
114
114
  toolbar.tree = this.tree;
115
- this.tree.ensureActiveNodeLoaded(this.activePageId).then(
116
- () => {
117
- const currentNode = this.tree.nodes.find(n => n.identifier === String(this.activePageId));
118
- if (currentNode) {
119
- this.toggleDynamicInsertNodes(currentNode).then(
120
- () => {
121
- const activeNode = this.tree.nodes.find(n => this.isActivePagePositionNode(n));
122
- if (activeNode) {
123
- activeNode.checked = true;
124
- this.updateBreadcrumb(this.tree.nodes);
125
- this.tree.requestUpdate();
126
- this.tree.updateComplete.then(() => {
127
- this.tree.scrollNodeIntoViewIfNeeded(activeNode);
128
- this.tree.focusNode(activeNode);
129
- });
130
- }
131
- }
132
- );
133
- }
134
- }
135
- );
115
+ await this.tree.ensureActiveNodeLoaded(this.activePageId);
116
+ const activeNode = await this.applyActiveInsertPosition();
117
+ if (activeNode) {
118
+ await this.tree.updateComplete;
119
+ this.tree.scrollNodeIntoViewIfNeeded(activeNode);
120
+ this.tree.focusNode(activeNode);
121
+ }
136
122
  };
137
123
 
138
124
  return html`
@@ -145,38 +131,63 @@ export class PagePositionSelect extends LitElement {
145
131
  .setup=${this.configuration}
146
132
  @tree:initialized=${initialized}
147
133
  @typo3:tree:node-selected="${this.handleNodeSelected}"
134
+ @typo3:tree:filter-applied=${() => this.applyActiveInsertPosition()}
135
+ @typo3:tree:filter-reset=${() => this.applyActiveInsertPosition()}
148
136
  >
149
137
  </typo3-backend-component-page-position-select-tree>
150
138
  `;
151
139
  }
152
140
 
153
- private handleNodeSelected(event: CustomEvent): void {
141
+ private async handleNodeSelected(event: CustomEvent): Promise<void> {
154
142
  const selectedNode: TreeNodeInterface = event.detail.node;
155
143
  const [pageUid, insertPosition] = selectedNode.identifier.split('-');
156
144
  this.insertPosition = (insertPosition ?? 'inside') as Position;
157
145
  this.activePageId = Number(pageUid);
158
146
 
159
- const callback = () => {
160
- this.tree.nodes.forEach((node: TreeNodeInterface) => {
161
- node.checked = this.isActivePagePositionNode(node);
162
- if (node.checked) {
163
- this.tree.focusNode(node);
164
- }
165
- });
147
+ const activeNode = /^\d+$/.test(selectedNode.identifier)
148
+ ? await this.applyActiveInsertPosition()
149
+ : this.syncActiveInsertPositionState();
166
150
 
167
- this.updateBreadcrumb(this.tree.nodes);
168
- this.dispatchEvent(new InsertPositionChangeEvent(
169
- this.activePageId,
170
- this.insertPosition
171
- ));
172
- };
151
+ if (activeNode) {
152
+ this.tree.focusNode(activeNode);
153
+ }
173
154
 
174
- if (/^\d+$/.test(selectedNode.identifier)) {
175
- this.toggleDynamicInsertNodes(selectedNode).then(callback);
176
- } else {
177
- callback();
178
- this.tree.requestUpdate();
155
+ this.dispatchEvent(new InsertPositionChangeEvent(
156
+ this.activePageId,
157
+ this.insertPosition
158
+ ));
159
+ }
160
+
161
+ private async applyActiveInsertPosition(): Promise<TreeNodeInterface|null> {
162
+ if (this.activePageId === null || this.activePageId === undefined) {
163
+ return null;
179
164
  }
165
+ let currentNode = this.tree.nodes.find(n => n.identifier === String(this.activePageId));
166
+ if (!currentNode) {
167
+ // Load its rootline and expand the path
168
+ await this.tree.ensureActiveNodeLoaded(this.activePageId);
169
+ currentNode = this.tree.nodes.find(n => n.identifier === String(this.activePageId));
170
+ if (!currentNode) {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ await this.tree.expandNodeParents(currentNode);
176
+ await this.toggleDynamicInsertNodes(currentNode);
177
+ return this.syncActiveInsertPositionState();
178
+ }
179
+
180
+ private syncActiveInsertPositionState(): TreeNodeInterface|null {
181
+ let activeNode: TreeNodeInterface|null = null;
182
+ this.tree.nodes.forEach((node: TreeNodeInterface) => {
183
+ node.checked = this.isActivePagePositionNode(node);
184
+ if (node.checked) {
185
+ activeNode = node;
186
+ }
187
+ });
188
+ this.updateBreadcrumb(this.tree.nodes);
189
+ this.tree.requestUpdate();
190
+ return activeNode;
180
191
  }
181
192
 
182
193
  private async toggleDynamicInsertNodes(currentNode: TreeNodeInterface): Promise<void> {
@@ -87,6 +87,24 @@ export class TreeNodeMap<T extends TreeNodeInterface = TreeNodeInterface> {
87
87
  }
88
88
  }
89
89
 
90
+ export class TreeFilterAppliedEvent extends CustomEvent<{searchTerm: string, resultCount: number}> {
91
+ static readonly eventName = 'typo3:tree:filter-applied';
92
+ constructor(searchTerm: string, resultCount: number) {
93
+ super(TreeFilterAppliedEvent.eventName, {
94
+ detail: { searchTerm, resultCount },
95
+ bubbles: true,
96
+ composed: true,
97
+ });
98
+ }
99
+ }
100
+
101
+ export class TreeFilterResetEvent extends Event {
102
+ static readonly eventName = 'typo3:tree:filter-reset';
103
+ constructor() {
104
+ super(TreeFilterResetEvent.eventName, { bubbles: true, composed: true });
105
+ }
106
+ }
107
+
90
108
  export class Tree extends LitElement {
91
109
  @property({ type: Object }) setup?: {[keys: string]: any} = null;
92
110
  @state() settings: TreeSettings = {
@@ -474,22 +492,25 @@ export class Tree extends LitElement {
474
492
  }).then(() => {
475
493
  this.loading = false;
476
494
  this.currentFilterRequest = null;
495
+ this.dispatchEvent(new TreeFilterAppliedEvent(this.searchTerm, this.searchResults));
477
496
  });
478
497
  } else {
479
498
  // restore original state without filters
480
- this.resetFilter();
481
- this.loading = false;
499
+ this.resetFilter().then(() => {
500
+ this.loading = false;
501
+ this.dispatchEvent(new TreeFilterResetEvent());
502
+ });
482
503
  }
483
504
  }
484
505
 
485
- public resetFilter(): void
506
+ public async resetFilter(): Promise<void>
486
507
  {
487
508
  this.searchTerm = '';
488
509
  this.searchResults = 0;
489
510
  if (this.unfilteredNodes.length > 0) {
490
511
  const currentlySelected = this.getSelectedNodes()[0];
491
512
  if (typeof currentlySelected === 'undefined') {
492
- this.loadData();
513
+ await this.loadData();
493
514
  return;
494
515
  }
495
516
  this.nodeMap = new TreeNodeMap(this.enhanceNodes(JSON.parse(this.unfilteredNodes)));
@@ -499,10 +520,10 @@ export class Tree extends LitElement {
499
520
  if (currentlySelectedNode) {
500
521
  this.selectNode(currentlySelectedNode, false);
501
522
  } else {
502
- this.loadData();
523
+ await this.loadData();
503
524
  }
504
525
  } else {
505
- this.loadData();
526
+ await this.loadData();
506
527
  }
507
528
  }
508
529
 
@@ -1578,7 +1578,4 @@ declare global {
1578
1578
 
1579
1579
  [newRecordWizardEventName]: CustomEvent<NewRecordWizardItemSelectedEventInterface>;
1580
1580
  }
1581
- interface FocusOptions {
1582
- focusVisible: boolean;
1583
- }
1584
1581
  }
@@ -583,7 +583,7 @@ function wireAbstractViewFormElementToolbarEventListeners(toolbar: HTMLElement,
583
583
  }
584
584
 
585
585
  qs('abstractViewToolbarRemoveElement')?.addEventListener('click', function() {
586
- getViewModel().showRemoveFormElementModal();
586
+ getViewModel().showRemoveFormElementModal(formElement);
587
587
  });
588
588
  }
589
589
 
@@ -631,7 +631,7 @@ export function createAndAddAbstractViewFormElementToolbar(
631
631
  });
632
632
 
633
633
  toolbar.addEventListener('toolbar-remove-element', () => {
634
- getViewModel().showRemoveFormElementModal();
634
+ getViewModel().showRemoveFormElementModal(formElement);
635
635
  });
636
636
  }
637
637
 
@@ -908,7 +908,7 @@ export function renderFormElementStageItem(formElement: FormElement, template: H
908
908
  });
909
909
 
910
910
  stageItem.addEventListener('toolbar-remove-element', () => {
911
- getViewModel().showRemoveFormElementModal();
911
+ getViewModel().showRemoveFormElementModal(formElement);
912
912
  });
913
913
 
914
914
  template.replaceChildren(stageItem);
@@ -39,19 +39,21 @@ enum Identifiers {
39
39
  }
40
40
 
41
41
  function newFormSetup(formManagerApp: FormManager): void {
42
- const newFormModalTrigger: HTMLElement = document.querySelector<HTMLElement>(Identifiers.newFormModalTrigger);
43
- newFormModalTrigger.addEventListener('click', async (): Promise<void> => {
44
- await topLevelModuleImport('@typo3/form/backend/form-wizard/form-wizard.js');
45
- const content = html`<typo3-backend-form-wizard .formManager="${formManagerApp}"
46
- ></typo3-backend-form-wizard>`;
47
-
48
- Modal.advanced({
49
- title: formManagerLabels.get('formManager.newFormWizard.step1.title'),
50
- content: content,
51
- severity: SeverityEnum.notice,
52
- size: Modal.sizes.medium,
53
- staticBackdrop: true,
54
- buttons: []
42
+ document.querySelectorAll<HTMLElement>(Identifiers.newFormModalTrigger).forEach((element: HTMLElement): void => {
43
+ element.addEventListener('click', async (e: Event): Promise<void> => {
44
+ e.preventDefault();
45
+ await topLevelModuleImport('@typo3/form/backend/form-wizard/form-wizard.js');
46
+ const content = html`<typo3-backend-form-wizard .formManager="${formManagerApp}"
47
+ ></typo3-backend-form-wizard>`;
48
+
49
+ Modal.advanced({
50
+ title: formManagerLabels.get('formManager.newFormWizard.step1.title'),
51
+ content: content,
52
+ severity: SeverityEnum.notice,
53
+ size: Modal.sizes.medium,
54
+ staticBackdrop: true,
55
+ buttons: []
56
+ });
55
57
  });
56
58
  });
57
59
  }
@@ -13,22 +13,17 @@
13
13
 
14
14
  import DocumentService from '@typo3/core/document-service';
15
15
  import Notification from '@typo3/backend/notification';
16
+ import { ProgressBarElement } from '@typo3/backend/element/progress-bar-element';
16
17
  import RegularEvent from '@typo3/core/event/regular-event';
17
18
  import SortableTable from '@typo3/backend/sortable-table';
18
19
 
19
20
  enum Selectors {
20
- // Check
21
- linktypesSelectorCheck = '.t3js-linkvalidator-settings input[type="checkbox"].options-by-type-check',
22
21
  actionButtonSelectorCheck = '.t3js-linkvalidator-action-button-check',
23
-
24
- // Report
25
- toggleAllLinktypesSelectorReport = '.t3js-linkvalidator-settings input[type="checkbox"].options-by-type-toggle-all-report',
26
- linktypesSelectorReport = '.t3js-linkvalidator-settings input[type="checkbox"].options-by-type-report',
27
- actionButtonSelectorReport = '.t3js-linkvalidator-action-button-report'
22
+ actionButtonSelectorReport = '.t3js-linkvalidator-action-button-report',
23
+ reportTable = '.t3js-linkvalidator-report-table',
28
24
  }
29
25
 
30
26
  enum Identifier {
31
- toggleAllLinktypesIdReport = 'options-by-type-toggle-all-report',
32
27
  brokenLinksTableIdReport = 'typo3-broken-links-table'
33
28
  }
34
29
 
@@ -36,6 +31,8 @@ enum Identifier {
36
31
  * Module: @typo3/linkvalidator/linkvalidator
37
32
  */
38
33
  class Linkvalidator {
34
+ private progressBar: ProgressBarElement | null = null;
35
+
39
36
  constructor() {
40
37
  DocumentService.ready().then((): void => {
41
38
  const linkList = document.getElementById(Identifier.brokenLinksTableIdReport);
@@ -49,51 +46,21 @@ class Linkvalidator {
49
46
  this.initializeEvents();
50
47
  }
51
48
 
52
- private static allCheckBoxesAreChecked(checkBoxes: NodeListOf<HTMLInputElement>): boolean {
53
- const checkboxArray = Array.from(checkBoxes);
54
- return checkBoxes.length === checkboxArray.filter((checkBox: HTMLInputElement) => checkBox.checked).length;
55
- }
56
-
57
- private toggleActionButtonReport(): void {
58
- document.querySelector(Selectors.actionButtonSelectorReport)?.toggleAttribute(
59
- 'disabled',
60
- !document.querySelectorAll('input[type="checkbox"]:checked').length
61
- );
62
- }
63
-
64
- /**
65
- * Enables the "Toggle all" checkbox on document load if all child checkboxes are checked
66
- */
67
- private toggleTriggerCheckBoxReport(): void {
68
- const checkBoxes: NodeListOf<HTMLInputElement> = document.querySelectorAll(Selectors.linktypesSelectorReport);
69
- (document.getElementById(Identifier.toggleAllLinktypesIdReport) as HTMLInputElement).checked = Linkvalidator.allCheckBoxesAreChecked(checkBoxes);
49
+ private getProgress(): ProgressBarElement {
50
+ if (!this.progressBar || !this.progressBar.isConnected) {
51
+ this.progressBar = document.createElement('typo3-backend-progress-bar');
52
+ document.querySelector(Selectors.reportTable).prepend(this.progressBar);
53
+ }
54
+ return this.progressBar;
70
55
  }
71
56
 
72
57
  private initializeEvents(): void {
73
- // toggleAll (checkboxes): on change
74
- new RegularEvent('change', (e: Event, currentTarget: HTMLInputElement): void => {
75
- const checkBoxes: NodeListOf<HTMLInputElement> = document.querySelectorAll(Selectors.linktypesSelectorReport);
76
- const checkIt = !Linkvalidator.allCheckBoxesAreChecked(checkBoxes);
77
-
78
- checkBoxes.forEach((checkBox: HTMLInputElement): void => {
79
- checkBox.checked = checkIt;
80
- });
81
- currentTarget.checked = checkIt;
82
- this.toggleActionButtonReport();
83
- }).delegateTo(document, Selectors.toggleAllLinktypesSelectorReport);
84
-
85
- // toggle (checkbox): on change
86
- new RegularEvent('change', (): void => {
87
- this.toggleTriggerCheckBoxReport();
88
- this.toggleActionButtonReport();
89
- }).delegateTo(document, Selectors.linktypesSelectorReport);
90
-
91
58
  new RegularEvent('click', (e: PointerEvent, actionButton: HTMLInputElement): void => {
92
59
  Notification.success(actionButton.dataset.notificationMessage || 'Event triggered', '', 2);
93
60
  }).delegateTo(document, Selectors.actionButtonSelectorCheck);
94
61
 
95
- new RegularEvent('click', (e: PointerEvent, actionButton: HTMLInputElement): void => {
96
- Notification.success(actionButton.dataset.notificationMessage || 'Event triggered', '', 2);
62
+ new RegularEvent('click', (): void => {
63
+ this.getProgress().start();
97
64
  }).delegateTo(document, Selectors.actionButtonSelectorReport);
98
65
  }
99
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typo3-typescript-sources",
3
- "version": "14.2.0",
3
+ "version": "14.3.0",
4
4
  "description": "TYPO3 Core TypeScript Sources - Extracted from TYPO3/typo3 repository",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "typo3": {
30
30
  "sourceRepository": "https://github.com/TYPO3/typo3",
31
- "sourceTag": "v14.2.0",
31
+ "sourceTag": "v14.3.0",
32
32
  "sourcePath": "Build/Sources/TypeScript"
33
33
  }
34
34
  }
@@ -1,91 +0,0 @@
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 RegularEvent from '@typo3/core/event/regular-event';
16
- import PersistentStorage from './storage/persistent';
17
-
18
- enum IdentifierEnum {
19
- hiddenElements = '.t3js-hidden-record',
20
- }
21
-
22
- /**
23
- * Module: @typo3/backend/page-actions
24
- * JavaScript implementations for page actions
25
- */
26
- class PageActions {
27
- constructor() {
28
- DocumentService.ready().then((): void => {
29
- const pageLayoutToggleShowHidden = document.getElementById('pageLayoutToggleShowHidden') as HTMLButtonElement|null;
30
- if (pageLayoutToggleShowHidden !== null) {
31
- new RegularEvent('click', this.toggleContentElementVisibility).bindTo(pageLayoutToggleShowHidden);
32
- }
33
- });
34
- }
35
-
36
- /**
37
- * Toggles the "Show hidden content elements"
38
- */
39
- private toggleContentElementVisibility(e: Event): void {
40
- const me = e.target as HTMLButtonElement;
41
- const hiddenElements = document.querySelectorAll(IdentifierEnum.hiddenElements) as NodeListOf<HTMLElement>;
42
- const show = me.dataset.dropdowntoggleStatus !== 'active';
43
- me.disabled = true;
44
-
45
- for (const hiddenElement of hiddenElements) {
46
- hiddenElement.style.display = 'flow-root';
47
- const scrollHeight = hiddenElement.scrollHeight;
48
-
49
- // Always set `overflow: clip` after storing scrollHeight
50
- // * For hidden state `height: 0px` is already set.
51
- // * For visible state setting `overflow: clip` is fine anyway.
52
- hiddenElement.style.overflow = 'clip';
53
-
54
- if (!show) {
55
- // * Invisible elements must not be accessible/focusable by keyboard.
56
- // * Spacing between content elements is kept uniform by collapsed margins,
57
- // hidden elements have a height of 0 and the margins of the surrounding elements
58
- // cannot collapse, causing a visual gap.
59
- // Therefore do not display the element at all by setting `display: none`.
60
- hiddenElement.addEventListener('transitionend', (): void => {
61
- hiddenElement.style.display = 'none';
62
- hiddenElement.style.overflow = '';
63
- }, { once: true });
64
-
65
- // We use requestAnimationFrame() as we have to set the container's height at first before resizing to
66
- // collapsed-element-height. This results in a smooth animation.
67
- requestAnimationFrame(function() {
68
- hiddenElement.style.height = scrollHeight + 'px';
69
- requestAnimationFrame(function() {
70
- hiddenElement.style.height = 0 + 'px';
71
- });
72
- });
73
- } else {
74
- hiddenElement.addEventListener('transitionend', (): void => {
75
- hiddenElement.style.display = '';
76
- hiddenElement.style.overflow = '';
77
- hiddenElement.style.height = '';
78
- }, { once: true });
79
-
80
- hiddenElement.style.height = scrollHeight + 'px';
81
- }
82
- }
83
-
84
- me.dataset.dropdowntoggleStatus = show ? 'active' : 'inactive';
85
- PersistentStorage.set('moduleData.web_layout.showHidden', show ? '1' : '0').then((): void => {
86
- me.disabled = false;
87
- });
88
- }
89
- }
90
-
91
- export default new PageActions();