typo3-typescript-sources 14.3.3 → 14.3.5

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 CHANGED
@@ -1 +1 @@
1
- v14.3.3
1
+ v14.3.5
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.3`
19
+ - **Current TYPO3 Version:** `v14.3.5`
20
20
 
21
21
  ## 🌿 Branches
22
22
 
@@ -47,6 +47,8 @@ enum Mode {
47
47
  BROWSE = 'browse',
48
48
  }
49
49
 
50
+ const uploadFinishedEventName = 'typo3:drag-uploader:upload-finished';
51
+
50
52
  /**
51
53
  * Properties of a file as returned from the AJAX action; essential, this is a serialized instance of
52
54
  * \TYPO3\CMS\Core\Resource\File plus some extra properties (see FileController::flattenResultDataValue())
@@ -208,6 +210,8 @@ export default class DragUploader {
208
210
  dropzoneCloseButton.addEventListener('click', this.hideDropzone);
209
211
  this.dropzone.append(dropzoneCloseButton);
210
212
 
213
+ document.addEventListener(uploadFinishedEventName, this.closeDropzone);
214
+
211
215
  // no filelist then create own progress table
212
216
  if (this.fileList === null) {
213
217
  this.fileList = document.createElement('table');
@@ -289,6 +293,18 @@ export default class DragUploader {
289
293
  this.dropzone.removeAttribute('hidden');
290
294
  }
291
295
 
296
+ public closeDropzone = (): void => {
297
+ this.dropzone.hidden = true;
298
+ this.dropzone.classList.remove('drop-status-ok');
299
+
300
+ if (this.irreObjectUid !== undefined) {
301
+ // Hide artificial file table in IRRE context
302
+ this.fileList.parentElement.hidden = true;
303
+ }
304
+ // User manually hides the dropzone, so we can reset the flag
305
+ this.manuallyTriggered = false;
306
+ };
307
+
292
308
  /**
293
309
  *
294
310
  * @param {Event} event
@@ -296,10 +312,7 @@ export default class DragUploader {
296
312
  public hideDropzone = (event: Event): void => {
297
313
  event.stopPropagation();
298
314
  event.preventDefault();
299
- this.dropzone.setAttribute('hidden', 'hidden');
300
- this.dropzone.classList.remove('drop-status-ok');
301
- // User manually hides the dropzone, so we can reset the flag
302
- this.manuallyTriggered = false;
315
+ this.closeDropzone();
303
316
  };
304
317
 
305
318
  /**
@@ -442,6 +455,8 @@ export default class DragUploader {
442
455
  if (this.queueLength > 0) {
443
456
  this.queueLength--;
444
457
  if (this.queueLength === 0) {
458
+ document.dispatchEvent(new CustomEvent(uploadFinishedEventName));
459
+
445
460
  const timeout: number = messages && messages.length ? 5000 : 0;
446
461
  if (timeout) {
447
462
  for (const flashMessage of messages) {
@@ -612,6 +627,8 @@ export default class DragUploader {
612
627
 
613
628
  modal.addEventListener('typo3-modal-hidden', () => {
614
629
  this.askForOverride = [];
630
+
631
+ document.dispatchEvent(new CustomEvent(uploadFinishedEventName));
615
632
  });
616
633
  }
617
634
  }
@@ -211,9 +211,11 @@ export class EditablePageTitle extends LitElement {
211
211
  return this.editable && this.pageId > 0;
212
212
  }
213
213
 
214
- private endEditing(): void {
214
+ private async endEditing(): Promise<void> {
215
215
  if (this.isEditable()) {
216
216
  this._isEditing = false;
217
+ await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
218
+ this.shadowRoot.querySelector<HTMLButtonElement>('button[data-action="edit"]')?.focus();
217
219
  }
218
220
  }
219
221
 
@@ -0,0 +1,93 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+
14
+ import { customElement, property } from 'lit/decorators.js';
15
+ import { html, LitElement, nothing, type TemplateResult } from 'lit';
16
+ import { classMap, type ClassInfo } from 'lit/directives/class-map.js';
17
+
18
+ // The first group are the base colours; the second group are semantic states
19
+ // that map to one of those base colours in the stylesheet, so a recurring
20
+ // domain concept looks identical wherever it appears in the backend. Keep the
21
+ // semantic group small and curated rather than introducing new colours.
22
+ export type StatusIndicatorState =
23
+ | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'danger' | 'notice' | 'default'
24
+ | 'active' | 'online' | 'running' | 'disabled';
25
+
26
+ // A semantic state may also imply a motion modifier, so a single state carries
27
+ // both the colour and the activity animation of the concept it names.
28
+ const semanticMotion: Partial<Record<StatusIndicatorState, 'live' | 'loading'>> = {
29
+ online: 'live',
30
+ running: 'loading',
31
+ };
32
+
33
+ /**
34
+ * Module: @typo3/backend/element/status-indicator-element
35
+ *
36
+ * A small dot that flags the state of a record, task or interface element.
37
+ * Add the `live` attribute to signal that an element is active or live, or the
38
+ * `loading` attribute to signal an operation in progress. Provide a `label` to
39
+ * expose an accessible name (rendered as role="img" + aria-label) and a hover
40
+ * tooltip (via title); without it the indicator is purely decorative and hidden
41
+ * from assistive technology.
42
+ *
43
+ * The `state` accepts either a base colour (success, danger, …) or a semantic
44
+ * state (active, online, running, disabled) that resolves to one of those
45
+ * colours and may carry its own motion, so `state="running"` is already
46
+ * animated while `state="active"` is a plain dot.
47
+ *
48
+ * @example
49
+ * <typo3-backend-status-indicator state="success" label="Online"></typo3-backend-status-indicator>
50
+ * <typo3-backend-status-indicator state="success" live label="Live"></typo3-backend-status-indicator>
51
+ * <typo3-backend-status-indicator state="info" loading label="Loading"></typo3-backend-status-indicator>
52
+ * <typo3-backend-status-indicator state="running" label="Running"></typo3-backend-status-indicator>
53
+ */
54
+ @customElement('typo3-backend-status-indicator')
55
+ export class StatusIndicatorElement extends LitElement {
56
+ @property({ type: String }) state: StatusIndicatorState = 'default';
57
+ @property({ type: Boolean }) live: boolean = false;
58
+ @property({ type: Boolean }) loading: boolean = false;
59
+ @property({ type: String }) label: string = null;
60
+
61
+ protected override createRenderRoot(): HTMLElement | ShadowRoot {
62
+ return this;
63
+ }
64
+
65
+ protected override render(): TemplateResult {
66
+ const labelled = this.label !== null && this.label !== '';
67
+ return html`
68
+ <span
69
+ class=${classMap(this.getClasses())}
70
+ title=${labelled ? this.label : nothing}
71
+ role=${labelled ? 'img' : nothing}
72
+ aria-label=${labelled ? this.label : nothing}
73
+ aria-hidden=${labelled ? nothing : 'true'}
74
+ ></span>
75
+ `;
76
+ }
77
+
78
+ private getClasses(): ClassInfo {
79
+ const motion = semanticMotion[this.state];
80
+ return {
81
+ ['status-indicator']: true,
82
+ ['status-indicator-' + this.state]: true,
83
+ ['status-indicator-live']: this.live || motion === 'live',
84
+ ['status-indicator-loading']: this.loading || motion === 'loading',
85
+ };
86
+ }
87
+ }
88
+
89
+ declare global {
90
+ interface HTMLElementTagNameMap {
91
+ 'typo3-backend-status-indicator': StatusIndicatorElement;
92
+ }
93
+ }
@@ -642,6 +642,13 @@ class InlineControlContainer extends HTMLElement {
642
642
  const recordContainer = this.getRecordContainer(objectId);
643
643
  this.removeUsed(recordContainer);
644
644
  }
645
+ }).catch((reason: Error): void => {
646
+ if (reason instanceof DOMException && reason.name === 'AbortError') {
647
+ // Request was aborted because the record was collapsed again before
648
+ // loading finished. Nothing to do, the abort handling already cleaned up.
649
+ return;
650
+ }
651
+ throw reason;
645
652
  });
646
653
 
647
654
  this.requestQueue[objectId] = ajaxRequest;
@@ -0,0 +1,61 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+
14
+ import DocumentService from '@typo3/core/document-service';
15
+ import FormEngine from '@typo3/backend/form-engine';
16
+
17
+ /**
18
+ * Client-side logic to toggle required state of "shortcut" based on "shortcut_mode".
19
+ *
20
+ * @internal
21
+ */
22
+ class ShortcutValidation {
23
+ constructor() {
24
+ DocumentService.ready().then((): void => {
25
+ this.run();
26
+ });
27
+ }
28
+
29
+ private run(): void {
30
+ const shortcut = document.querySelector<HTMLInputElement>('[name$="[shortcut]"]');
31
+ const shortcut_mode = document.querySelector<HTMLInputElement>('[name$="[shortcut_mode]"]');
32
+ const form = shortcut?.closest<HTMLFormElement>('form');
33
+
34
+ if (!shortcut || !shortcut_mode || !form) {
35
+ return;
36
+ }
37
+
38
+ shortcut_mode.addEventListener('change', () => {
39
+ this.apply(shortcut, form, shortcut_mode);
40
+ });
41
+ }
42
+
43
+ private async apply(shortcut: HTMLInputElement, form: HTMLFormElement, shortcut_mode: HTMLInputElement): Promise<void> {
44
+ const isRequired = parseInt(shortcut_mode.value, 10) === 0;
45
+ const rules = JSON.parse(shortcut.dataset.formengineValidationRules || '[]');
46
+ const existingRequiredIndex = rules.findIndex((r: any) => r.type === 'required');
47
+
48
+ if (isRequired && existingRequiredIndex === -1) {
49
+ rules.push({ type: 'required' });
50
+ } else if (!isRequired && existingRequiredIndex !== -1) {
51
+ rules.splice(existingRequiredIndex, 1);
52
+ }
53
+
54
+ shortcut.dataset.formengineValidationRules = JSON.stringify(rules);
55
+ FormEngine.reinitialize();
56
+ FormEngine.Validation.initializeInputFields();
57
+ FormEngine.Validation.validate(form);
58
+ }
59
+ }
60
+
61
+ export default new ShortcutValidation();
@@ -62,6 +62,11 @@ export class AjaxDispatcher {
62
62
  return this.processResponse(await response.resolve());
63
63
  });
64
64
  sentRequest.catch((reason: Error): void => {
65
+ if (reason instanceof DOMException && reason.name === 'AbortError') {
66
+ // The request was deliberately aborted (e.g. a record was collapsed again
67
+ // before loading finished), this is not an error worth notifying about.
68
+ return;
69
+ }
65
70
  Notification.error('Error ' + reason.message);
66
71
  });
67
72
 
@@ -385,7 +385,7 @@ class ImageManipulation {
385
385
  // Initialize current crop variant
386
386
  this.update(this.currentCropVariant);
387
387
  },
388
- crop: this.cropMoveHandler.bind(this),
388
+ crop: this.cropMoveHandler,
389
389
  data: this.currentCropVariant.cropArea,
390
390
  }));
391
391
 
@@ -66,6 +66,13 @@ export class StringlistTypeElement extends BaseElement<string[]> {
66
66
 
67
67
  protected override render(): TemplateResult {
68
68
  const value = this.value || [];
69
+ if (value.length === 0) {
70
+ return html`
71
+ <button id=${this.formid} class="btn btn-default" type="button" ?disabled=${this.readonly} @click=${() => this.addValue(0)}>
72
+ <typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
73
+ </button>
74
+ `;
75
+ }
69
76
  return html`
70
77
  <div class="form-control-wrap">
71
78
  <div class="table-fit">
@@ -0,0 +1,90 @@
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 { StringlistTypeElement } from '@typo3/backend/settings/type/stringlist.js';
15
+ import { expect } from '@open-wc/testing';
16
+ import type { } from 'mocha';
17
+
18
+ describe('@typo3/backend/settings/type/stringlist', () => {
19
+ let root: HTMLElement;
20
+
21
+ beforeEach((): void => {
22
+ root = document.createElement('div');
23
+ document.body.appendChild(root);
24
+ });
25
+
26
+ afterEach((): void => {
27
+ root.remove();
28
+ root = null;
29
+ });
30
+
31
+ it('renders one button when the value list is empty', async () => {
32
+ const element = new StringlistTypeElement;
33
+ element.formid = 'setting-test';
34
+ element.value = [];
35
+ root.appendChild(element);
36
+ await new Promise((resolve) => setTimeout(resolve, 100));
37
+
38
+ const inputs = element.querySelectorAll<HTMLInputElement>('input[type="text"]');
39
+ expect(inputs).to.have.lengthOf(0);
40
+
41
+ const buttons = element.querySelectorAll<HTMLButtonElement>('button');
42
+ expect(buttons).to.have.lengthOf(1);
43
+ });
44
+
45
+ it('renders one empty input when the value list is a single empty string', async () => {
46
+ const element = new StringlistTypeElement;
47
+ element.formid = 'setting-test';
48
+ element.value = [''];
49
+ root.appendChild(element);
50
+ await new Promise((resolve) => setTimeout(resolve, 100));
51
+
52
+ const inputs = element.querySelectorAll<HTMLInputElement>('input[type="text"]');
53
+ expect(inputs).to.have.lengthOf(1);
54
+ expect(inputs[0].id).to.equal('setting-test');
55
+ });
56
+
57
+ it('hides the remove button when only one row is shown', async () => {
58
+ const element = new StringlistTypeElement;
59
+ element.value = [];
60
+ root.appendChild(element);
61
+ await new Promise((resolve) => setTimeout(resolve, 100));
62
+
63
+ expect(element.querySelectorAll('button')).to.have.lengthOf(1);
64
+ });
65
+
66
+ it('adds a row when the add button is clicked on an empty list', async () => {
67
+ const element = new StringlistTypeElement;
68
+ element.value = [];
69
+ root.appendChild(element);
70
+ await new Promise((resolve) => setTimeout(resolve, 100));
71
+
72
+ const addButton = element.querySelectorAll<HTMLButtonElement>('button:not([disabled])')[0];
73
+ addButton.click();
74
+ await new Promise((resolve) => setTimeout(resolve, 100));
75
+
76
+ expect(element.value).to.deep.equal(['']);
77
+ expect(element.querySelectorAll('input[type="text"]')).to.have.lengthOf(1);
78
+ });
79
+
80
+ it('renders per-row controls for each entry when value has multiple entries', async () => {
81
+ const element = new StringlistTypeElement;
82
+ element.value = ['/typo3', '/fileadmin'];
83
+ root.appendChild(element);
84
+ await new Promise((resolve) => setTimeout(resolve, 100));
85
+
86
+ expect(element.querySelectorAll('input[type="text"]')).to.have.lengthOf(2);
87
+ // Two rows, each with + and - buttons.
88
+ expect(element.querySelectorAll('button')).to.have.lengthOf(4);
89
+ });
90
+ });
@@ -17,6 +17,7 @@ import Icons from '../icons';
17
17
  import PersistentStorage from '../storage/persistent';
18
18
  import Viewport from '../viewport';
19
19
  import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
20
+ import '../element/status-indicator-element';
20
21
 
21
22
  /**
22
23
  * Explicit selectors to avoid nesting queries
@@ -237,7 +237,11 @@ export class PagePositionSelect extends LitElement {
237
237
  }
238
238
 
239
239
  await this.tree.addNode(insideNode, currentNode, TreeNodePositionEnum.INSIDE);
240
- await this.tree.addNode(afterNode, currentNode, TreeNodePositionEnum.AFTER);
240
+
241
+ // The tree root has no valid "after" sibling, so the option is only added for real pages.
242
+ if (currentNode.identifier !== '0') {
243
+ await this.tree.addNode(afterNode, currentNode, TreeNodePositionEnum.AFTER);
244
+ }
241
245
 
242
246
  for (const childNode of childNodes) {
243
247
  const afterChildNode: TreeNodeInterface = {
@@ -301,4 +305,3 @@ declare global {
301
305
  'typo3-backend-component-page-position-select-tree': PagePositionSelectTree;
302
306
  }
303
307
  }
304
-
package/install/router.ts CHANGED
@@ -25,6 +25,7 @@ import RegularEvent from '@typo3/core/event/regular-event';
25
25
  import '@typo3/backend/element/progress-bar-element';
26
26
  import { ScaffoldState } from '@typo3/backend/viewport/scaffold-state';
27
27
  import '@typo3/backend/element/sidebar-toggle-element';
28
+ import 'bootstrap'; // required for panels in `handleAjaxError()`
28
29
 
29
30
  class Router {
30
31
  private readonly rootSelector: string = '.t3js-body';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typo3-typescript-sources",
3
- "version": "14.3.3",
3
+ "version": "14.3.5",
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.3.3",
31
+ "sourceTag": "v14.3.5",
32
32
  "sourcePath": "Build/Sources/TypeScript"
33
33
  }
34
34
  }
@@ -89,9 +89,9 @@ class EventHandler {
89
89
  }
90
90
 
91
91
  private revert(correlationIds: string[]): Promise<AjaxResponse> {
92
- const request = new AjaxRequest(TYPO3.settings.ajaxUrls.redirects_revert_correlation).withQueryArguments({
93
- correlation_ids: correlationIds
94
- }).get();
92
+ const request = new AjaxRequest(TYPO3.settings.ajaxUrls.redirects_revert_correlation).post({
93
+ correlation_ids: correlationIds,
94
+ });
95
95
 
96
96
  request.then(async (response: AjaxResponse): Promise<void> => {
97
97
  const json = await response.resolve();
@@ -90,14 +90,14 @@ class Scheduler {
90
90
  // Schedule selected tasks for next cron run
91
91
  const goCron: HTMLInputElement = document.createElement('input');
92
92
  goCron.setAttribute('type', 'hidden');
93
- goCron.setAttribute('name', 'scheduleCron');
93
+ goCron.setAttribute('name', 'action[scheduleCron]');
94
94
  goCron.setAttribute('value', taskIds.join(','));
95
95
  form.append(goCron);
96
96
  } else {
97
97
  // Execute selected tasks directly
98
98
  const executeTasks: HTMLInputElement = document.createElement('input');
99
99
  executeTasks.setAttribute('type', 'hidden');
100
- executeTasks.setAttribute('name', 'execute');
100
+ executeTasks.setAttribute('name', 'action[execute]');
101
101
  executeTasks.setAttribute('value', taskIds.join(','));
102
102
  form.append(executeTasks);
103
103
  }
@@ -278,7 +278,7 @@ export class RecordTableElement extends LitElement {
278
278
  private renderElementActions(data: RecordData): TemplateResult[] {
279
279
  return [
280
280
  this.getAction(
281
- data.allowedAction_view,
281
+ (data.allowedAction_view && data.previewUrl !== ''),
282
282
  'preview',
283
283
  'actions-version-workspace-preview',
284
284
  {
@@ -30,7 +30,7 @@ export interface Workspace {
30
30
  }
31
31
 
32
32
  interface WorkspaceInfoResponse {
33
- current: Workspace;
33
+ current: Workspace | null;
34
34
  workspaces: Workspace[];
35
35
  }
36
36