From 58c827b7baff2ef2a36d76613e83f545950ba0a9 Mon Sep 17 00:00:00 2001
From: Andreas Kienast <a.fernandez@scripting-base.de>
Date: Wed, 3 Apr 2024 15:53:39 +0200
Subject: [PATCH] [TASK] Deprecate `@typo3/backend/document-save-actions`
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The JavaScript module `@typo3/backend/document-save-actions` was
introduced in TYPO3 v7 to add some interactivity in FormEngine context.
At first it was only used to disable the submit button and render a
spinner icon instead. Over the course of some years, the module got more
functionality.

After some refactorings within FormEngine, the module became rather a
burden. This became visible with the introduction of the Hotkeys API, as
the `@typo3/backend/document-save-actions` module reacts on explicit
`click` events on the save icon, that is not triggered when FormEngine
invokes a save action via keyboard shortcuts.

Adjusting `document-save-actions`'s behavior is necessary here, but
would become a breaking change, which is unacceptable after the 13.0
release. For this reason, said module has been marked as deprecated and
its usages are replaced by its successor
`@typo3/backend/form/submit-interceptor`.

Resolves: #103528
Releases: main
Change-Id: Ic4ee972eb79da2331213de624adf077a7e5b567c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83643
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Garvin Hicking <gh@faktor-e.de>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Simon Schaufelberger <simonschaufi+typo3@gmail.com>
Tested-by: Garvin Hicking <gh@faktor-e.de>
---
 .../backend/document-save-actions.ts          |  2 +
 .../backend/form-engine-validation.ts         |  5 +-
 .../backend/form/submit-interceptor.ts        | 66 ++++++++++++++++++
 .../Sources/TypeScript/scheduler/scheduler.ts | 37 ++++++++--
 .../JavaScript/document-save-actions.js       |  2 +-
 .../JavaScript/form-engine-validation.js      |  2 +-
 .../JavaScript/form/submit-interceptor.js     | 13 ++++
 ...28-DeprecatedDocumentSaveActionsModule.rst | 69 +++++++++++++++++++
 .../Resources/Public/JavaScript/scheduler.js  |  2 +-
 9 files changed, 189 insertions(+), 9 deletions(-)
 create mode 100644 Build/Sources/TypeScript/backend/form/submit-interceptor.ts
 create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/form/submit-interceptor.js
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103528-DeprecatedDocumentSaveActionsModule.rst

diff --git a/Build/Sources/TypeScript/backend/document-save-actions.ts b/Build/Sources/TypeScript/backend/document-save-actions.ts
index d7575d902d76..e7bc9dc8b6d9 100644
--- a/Build/Sources/TypeScript/backend/document-save-actions.ts
+++ b/Build/Sources/TypeScript/backend/document-save-actions.ts
@@ -22,6 +22,7 @@ type SubmitTriggerHTMLElement = HTMLAnchorElement|HTMLButtonElement;
 
 /**
  * Module: @typo3/backend/document-save-actions
+ * @deprecated: use @typo3/backend/form/submit-interceptor instead
  */
 class DocumentSaveActions {
   private static instance: DocumentSaveActions = null;
@@ -29,6 +30,7 @@ class DocumentSaveActions {
   private readonly preSubmitCallbacks: PreSubmitCallback[] = [];
 
   private constructor() {
+    console.warn('The module `@typo3/backend/document-save-actions.js` has been deprecated and will be removed in TYPO3 v14. Please consider migrating to `@typo3/backend/form/submit-interceptor.js` instead.');
     DocumentService.ready().then((): void => {
       this.initializeSaveHandling();
     });
diff --git a/Build/Sources/TypeScript/backend/form-engine-validation.ts b/Build/Sources/TypeScript/backend/form-engine-validation.ts
index 1499d0ca75e0..f9e75894736b 100644
--- a/Build/Sources/TypeScript/backend/form-engine-validation.ts
+++ b/Build/Sources/TypeScript/backend/form-engine-validation.ts
@@ -19,13 +19,13 @@
 import $ from 'jquery';
 import { DateTime } from 'luxon';
 import Md5 from '@typo3/backend/hashing/md5';
-import DocumentSaveActions from '@typo3/backend/document-save-actions';
 import Modal from '@typo3/backend/modal';
 import Severity from '@typo3/backend/severity';
 import Utility from './utility';
 import RegularEvent from '@typo3/core/event/regular-event';
 import DomHelper from '@typo3/backend/utility/dom-helper';
 import { selector } from '@typo3/core/literals';
+import SubmitInterceptor from '@typo3/backend/form/submit-interceptor';
 
 type FormEngineFieldElement = HTMLInputElement|HTMLTextAreaElement|HTMLSelectElement;
 type CustomEvaluationCallback = (value: string) => string;
@@ -747,7 +747,8 @@ export default (function() {
   };
 
   FormEngineValidation.registerSubmitCallback = function () {
-    DocumentSaveActions.getInstance().addPreSubmitCallback((): boolean => {
+    const submitInterceptor = new SubmitInterceptor(formEngineFormElement);
+    submitInterceptor.addPreSubmitCallback((): boolean => {
       if (document.querySelector('.' + FormEngineValidation.errorClass) === null) {
         return true;
       }
diff --git a/Build/Sources/TypeScript/backend/form/submit-interceptor.ts b/Build/Sources/TypeScript/backend/form/submit-interceptor.ts
new file mode 100644
index 000000000000..5d632bab93e6
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/form/submit-interceptor.ts
@@ -0,0 +1,66 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import Icons from '@typo3/backend/icons';
+
+export type PreSubmitCallback = (e: Event) => boolean;
+
+/**
+ * Module: @typo3/backend/form/submit-interceptor
+ */
+export default class SubmitInterceptor {
+  private isSubmitting: boolean = false;
+  private readonly preSubmitCallbacks: PreSubmitCallback[] = [];
+
+  constructor(form: HTMLFormElement) {
+    form.addEventListener('submit', this.submitHandler.bind(this));
+  }
+
+  public addPreSubmitCallback(callback: PreSubmitCallback): SubmitInterceptor {
+    if (typeof callback !== 'function') {
+      throw 'callback must be a function.';
+    }
+
+    this.preSubmitCallbacks.push(callback);
+
+    return this;
+  }
+
+  private submitHandler(e: SubmitEvent): void {
+    if (this.isSubmitting) {
+      return;
+    }
+
+    for (const callback of this.preSubmitCallbacks) {
+      const callbackResult = callback(e);
+      if (!callbackResult) {
+        e.preventDefault();
+        return;
+      }
+    }
+
+    this.isSubmitting = true;
+
+    if (e.submitter !== null) {
+      if (e.submitter instanceof HTMLInputElement || e.submitter instanceof HTMLButtonElement) {
+        e.submitter.disabled = true;
+      }
+      Icons.getIcon('spinner-circle', Icons.sizes.small).then((markup: string): void => {
+        e.submitter.replaceChild(document.createRange().createContextualFragment(markup), e.submitter.querySelector('.t3js-icon'));
+      }).catch(() => {
+        // Catch error in case the promise was not resolved
+        // e.g. loading a new page
+      });
+    }
+  }
+}
diff --git a/Build/Sources/TypeScript/scheduler/scheduler.ts b/Build/Sources/TypeScript/scheduler/scheduler.ts
index 1b77761ab3aa..697e8fc133ce 100644
--- a/Build/Sources/TypeScript/scheduler/scheduler.ts
+++ b/Build/Sources/TypeScript/scheduler/scheduler.ts
@@ -12,7 +12,6 @@
  */
 
 import SortableTable from '@typo3/backend/sortable-table';
-import DocumentSaveActions from '@typo3/backend/document-save-actions';
 import RegularEvent from '@typo3/core/event/regular-event';
 import Modal from '@typo3/backend/modal';
 import Icons from '@typo3/backend/icons';
@@ -23,6 +22,7 @@ import DateTimePicker from '@typo3/backend/date-time-picker';
 import { MultiRecordSelectionSelectors } from '@typo3/backend/multi-record-selection';
 import Severity from '@typo3/backend/severity';
 import DocumentService from '@typo3/core/document-service';
+import SubmitInterceptor from '@typo3/backend/form/submit-interceptor';
 
 interface TableNumberMapping {
   [s: string]: number;
@@ -35,12 +35,11 @@ interface TableNumberMapping {
 class Scheduler {
   constructor() {
     DocumentService.ready().then((): void => {
+      this.initializeSubmitInterceptor();
       this.initializeEvents();
       this.initializeDefaultStates();
       this.initializeCloseConfirm();
     });
-
-    DocumentSaveActions.registerEvents();
   }
 
   private static updateClearableInputs(): void {
@@ -161,6 +160,15 @@ class Scheduler {
     (document.querySelector('#task_multiple_row') as HTMLElement).hidden = !taskIsRecurring;
   }
 
+  private initializeSubmitInterceptor(): void {
+    const schedulerForm: HTMLFormElement = document.querySelector('form[name=tx_scheduler_form]');
+    if (!schedulerForm) {
+      return;
+    }
+
+    new SubmitInterceptor(schedulerForm);
+  }
+
   /**
    * Registers listeners
    */
@@ -233,6 +241,27 @@ class Scheduler {
     new RegularEvent('multiRecordSelection:action:go_cron', this.executeTasks.bind(this)).bindTo(document);
 
     window.addEventListener('message', this.listenOnElementBrowser.bind(this));
+
+    new RegularEvent('click', (e: Event): void => {
+      e.preventDefault();
+
+      this.saveDocument(e);
+    }).delegateTo(document, 'button[form]');
+  }
+
+  private saveDocument(e: Event): void {
+    const schedulerForm: HTMLFormElement = document.querySelector('form[name=tx_scheduler_form]');
+    if (!schedulerForm) {
+      return;
+    }
+
+    const hidden = document.createElement('input')
+    hidden.type = 'hidden';
+    hidden.value = 'save';
+    hidden.name = 'CMD';
+
+    schedulerForm.append(hidden);
+    schedulerForm.requestSubmit(e.target as HTMLElement);
   }
 
   /**
@@ -366,7 +395,7 @@ class Scheduler {
                 hidden.value = 'saveclose';
                 hidden.name = 'CMD';
 
-                schedulerForm.append(hidden)
+                schedulerForm.append(hidden);
                 schedulerForm.submit();
               },
             }
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/document-save-actions.js b/typo3/sysext/backend/Resources/Public/JavaScript/document-save-actions.js
index 71b411c0b971..67bf51498529 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/document-save-actions.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/document-save-actions.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import DocumentService from"@typo3/core/document-service.js";import Icons from"@typo3/backend/icons.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{selector}from"@typo3/core/literals.js";class DocumentSaveActions{constructor(){this.preventDoubleClick=!1,this.preSubmitCallbacks=[],DocumentService.ready().then((()=>{this.initializeSaveHandling()}))}static getInstance(){return null===DocumentSaveActions.instance&&(DocumentSaveActions.instance=new DocumentSaveActions),DocumentSaveActions.instance}static registerEvents(){DocumentSaveActions.getInstance()}addPreSubmitCallback(e){if("function"!=typeof e)throw"callback must be a function.";this.preSubmitCallbacks.push(e)}initializeSaveHandling(){const e=document.querySelector(".t3js-module-docheader");if(null===e)return;const t=["button[form]",'button[name^="_save"]','a[data-name^="_save"]','button[name="CMD"][value^="save"]','a[data-name="CMD"][data-value^="save"]'].join(",");new RegularEvent("click",((e,t)=>{if(this.preventDoubleClick)return;const n=this.getAttachedForm(t);if(null!==n){for(const t of this.preSubmitCallbacks){if(!t(e))return void e.preventDefault()}this.preventDoubleClick=!0,this.attachSaveFieldToForm(n,t),n.addEventListener("submit",(()=>{const e=t.closest(".t3js-splitbutton");let n;null!==e?(n=e.firstElementChild,e.querySelectorAll("button").forEach((e=>{e.disabled=!0}))):(n=t,n instanceof HTMLAnchorElement?n.classList.add("disabled"):n.disabled=!0),Icons.getIcon("spinner-circle",Icons.sizes.small).then((e=>{n.replaceChild(document.createRange().createContextualFragment(e),t.querySelector(".t3js-icon"))})).catch((()=>{}))}),{once:!0})}})).delegateTo(e,t)}getAttachedForm(e){let t;return t=e instanceof HTMLAnchorElement?document.querySelector(selector`#${e.dataset.form}`):e.form,t||(t=e.closest("form")),t}attachSaveFieldToForm(e,t){const n=e.name+"_save_field";let a=document.getElementById(n);null===a&&(a=document.createElement("input"),a.id=n,a.type="hidden",e.append(a)),a.name=t instanceof HTMLAnchorElement?t.dataset.name:t.name,a.value=t instanceof HTMLAnchorElement?t.dataset.value:t.value}}DocumentSaveActions.instance=null;export default DocumentSaveActions;
\ No newline at end of file
+import DocumentService from"@typo3/core/document-service.js";import Icons from"@typo3/backend/icons.js";import RegularEvent from"@typo3/core/event/regular-event.js";import{selector}from"@typo3/core/literals.js";class DocumentSaveActions{constructor(){this.preventDoubleClick=!1,this.preSubmitCallbacks=[],console.warn("The module `@typo3/backend/document-save-actions.js` has been deprecated and will be removed in TYPO3 v14. Please consider migrating to `@typo3/backend/form/submit-interceptor.js` instead."),DocumentService.ready().then((()=>{this.initializeSaveHandling()}))}static getInstance(){return null===DocumentSaveActions.instance&&(DocumentSaveActions.instance=new DocumentSaveActions),DocumentSaveActions.instance}static registerEvents(){DocumentSaveActions.getInstance()}addPreSubmitCallback(e){if("function"!=typeof e)throw"callback must be a function.";this.preSubmitCallbacks.push(e)}initializeSaveHandling(){const e=document.querySelector(".t3js-module-docheader");if(null===e)return;const t=["button[form]",'button[name^="_save"]','a[data-name^="_save"]','button[name="CMD"][value^="save"]','a[data-name="CMD"][data-value^="save"]'].join(",");new RegularEvent("click",((e,t)=>{if(this.preventDoubleClick)return;const n=this.getAttachedForm(t);if(null!==n){for(const t of this.preSubmitCallbacks){if(!t(e))return void e.preventDefault()}this.preventDoubleClick=!0,this.attachSaveFieldToForm(n,t),n.addEventListener("submit",(()=>{const e=t.closest(".t3js-splitbutton");let n;null!==e?(n=e.firstElementChild,e.querySelectorAll("button").forEach((e=>{e.disabled=!0}))):(n=t,n instanceof HTMLAnchorElement?n.classList.add("disabled"):n.disabled=!0),Icons.getIcon("spinner-circle",Icons.sizes.small).then((e=>{n.replaceChild(document.createRange().createContextualFragment(e),t.querySelector(".t3js-icon"))})).catch((()=>{}))}),{once:!0})}})).delegateTo(e,t)}getAttachedForm(e){let t;return t=e instanceof HTMLAnchorElement?document.querySelector(selector`#${e.dataset.form}`):e.form,t||(t=e.closest("form")),t}attachSaveFieldToForm(e,t){const n=e.name+"_save_field";let a=document.getElementById(n);null===a&&(a=document.createElement("input"),a.id=n,a.type="hidden",e.append(a)),a.name=t instanceof HTMLAnchorElement?t.dataset.name:t.name,a.value=t instanceof HTMLAnchorElement?t.dataset.value:t.value}}DocumentSaveActions.instance=null;export default DocumentSaveActions;
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-validation.js b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-validation.js
index 306f60f95db1..65955c7e7c85 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-validation.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-validation.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import $ from"jquery";import{DateTime}from"luxon";import Md5 from"@typo3/backend/hashing/md5.js";import DocumentSaveActions from"@typo3/backend/document-save-actions.js";import Modal from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import Utility from"@typo3/backend/utility.js";import RegularEvent from"@typo3/core/event/regular-event.js";import DomHelper from"@typo3/backend/utility/dom-helper.js";import{selector}from"@typo3/core/literals.js";export default(function(){const FormEngineValidation={rulesSelector:"[data-formengine-validation-rules]",inputSelector:"[data-formengine-input-params]",markerSelector:".t3js-formengine-validation-marker",groupFieldHiddenElement:".t3js-formengine-field-group input[type=hidden]",relatedFieldSelector:"[data-relatedfieldname]",errorClass:"has-error",lastYear:0,lastDate:0,lastTime:0,passwordDummy:"********"};let formEngineFormElement;const customEvaluations=new Map;return FormEngineValidation.initialize=function(e){formEngineFormElement=e,formEngineFormElement.querySelectorAll("."+FormEngineValidation.errorClass).forEach((e=>e.classList.remove(FormEngineValidation.errorClass))),FormEngineValidation.initializeInputFields(),new RegularEvent("change",((e,n)=>{FormEngineValidation.validateField(n),FormEngineValidation.markFieldAsChanged(n)})).delegateTo(formEngineFormElement,FormEngineValidation.rulesSelector),FormEngineValidation.registerSubmitCallback();const n=new Date;FormEngineValidation.lastYear=FormEngineValidation.getYear(n),FormEngineValidation.lastDate=FormEngineValidation.getDate(n),FormEngineValidation.lastTime=0,FormEngineValidation.validate()},FormEngineValidation.initializeInputFields=function(){formEngineFormElement.querySelectorAll(FormEngineValidation.inputSelector).forEach((e=>{const n=JSON.parse(e.dataset.formengineInputParams).field,t=formEngineFormElement.querySelector(selector`[name="${n}"]`);"formengineInputInitialized"in e.dataset||(t.dataset.config=e.dataset.formengineInputParams,FormEngineValidation.initializeInputField(n))}))},FormEngineValidation.initializeInputField=function(e){const n=formEngineFormElement.querySelector(selector`[name="${e}"]`),t=formEngineFormElement.querySelector(selector`[data-formengine-input-name="${e}"]`);if(void 0!==n.dataset.config){const e=JSON.parse(n.dataset.config),a=FormEngineValidation.formatByEvals(e,n.value);a.length&&(t.value=a)}new RegularEvent("change",(()=>{FormEngineValidation.updateInputField(t.dataset.formengineInputName)})).bindTo(t),t.dataset.formengineInputInitialized="true"},FormEngineValidation.registerCustomEvaluation=function(e,n){customEvaluations.has(e)||customEvaluations.set(e,n)},FormEngineValidation.formatByEvals=function(e,n){if(void 0!==e.evalList){const t=Utility.trimExplode(",",e.evalList);for(const a of t)n=FormEngineValidation.formatValue(a,n,e)}return n},FormEngineValidation.formatValue=function(e,n,t){let a,i,o="";switch(e){case"date":if(n.toString().indexOf("-")>0){o=DateTime.fromISO(n.toString(),{zone:"utc"}).toFormat("dd-MM-yyyy")}else{if(""===n||"0"===n)return"";if(a=parseInt(n.toString(),10),isNaN(a))return"";i=new Date(1e3*a);o=i.getUTCDate().toString(10).padStart(2,"0")+"-"+(i.getUTCMonth()+1).toString(10).padStart(2,"0")+"-"+this.getYear(i)}break;case"datetime":if(""===n||"0"===n)return"";o=(FormEngineValidation.formatValue("time",n,t)+" "+FormEngineValidation.formatValue("date",n,t)).trim();break;case"time":case"timesec":let r;if(n.toString().indexOf("-")>0)r=DateTime.fromISO(n.toString(),{zone:"utc"});else{if(""===n||"0"===n)return"";if(a="number"==typeof n?n:parseInt(n),isNaN(a))return"";r=DateTime.fromSeconds(a,{zone:"utc"})}o="timesec"===e?r.toFormat("HH:mm:ss"):r.toFormat("HH:mm");break;case"password":o=n?FormEngineValidation.passwordDummy:"";break;default:o=n.toString()}return o},FormEngineValidation.updateInputField=function(e){const n=formEngineFormElement.querySelector(selector`[name="${e}"]`),t=formEngineFormElement.querySelector(selector`[data-formengine-input-name="${e}"]`);if(void 0!==n.dataset.config){const e=JSON.parse(n.dataset.config),a=FormEngineValidation.processByEvals(e,t.value),i=FormEngineValidation.formatByEvals(e,a);n.value!==a&&(n.disabled&&n.dataset.enableOnModification&&(n.disabled=!1),n.value=a,n.dispatchEvent(new Event("change")),t.value=i)}},FormEngineValidation.validateField=function(e,n){if(e instanceof $&&(console.warn("Passing a jQuery element to FormEngineValidation.validateField() is deprecated and will be removed in TYPO3 v14."),console.trace(),e=e.get(0)),!(e instanceof HTMLElement))return n;if(n=n||e.value||"",void 0===e.dataset.formengineValidationRules)return n;const t=JSON.parse(e.dataset.formengineValidationRules);let a=!1,i=0;const o=n;let r,l,s;Array.isArray(n)||(n=n.trimStart());for(const o of t){if(a)break;switch(o.type){case"required":""===n&&(a=!0,e.closest(FormEngineValidation.markerSelector).classList.add(FormEngineValidation.errorClass));break;case"range":if(""!==n){if((o.minItems||o.maxItems)&&(r=formEngineFormElement.querySelector(selector`[name="${e.dataset.relatedfieldname}"]`),i=null!==r?Utility.trimExplode(",",r.value).length:parseInt(e.value,10),void 0!==o.minItems&&(l=1*o.minItems,!isNaN(l)&&i<l&&(a=!0)),void 0!==o.maxItems&&(s=1*o.maxItems,!isNaN(s)&&i>s&&(a=!0))),void 0!==o.lower){const e=1*o.lower;!isNaN(e)&&parseInt(n,10)<e&&(a=!0)}if(void 0!==o.upper){const e=1*o.upper;!isNaN(e)&&parseInt(n,10)>e&&(a=!0)}}break;case"select":case"category":(o.minItems||o.maxItems)&&(r=formEngineFormElement.querySelector(selector`[name="${e.dataset.relatedfieldname}"]`),i=null!==r?Utility.trimExplode(",",r.value).length:e instanceof HTMLSelectElement?e.querySelectorAll("option:checked").length:e.querySelectorAll("input[value]:checked").length,void 0!==o.minItems&&(l=1*o.minItems,!isNaN(l)&&i<l&&(a=!0)),void 0!==o.maxItems&&(s=1*o.maxItems,!isNaN(s)&&i>s&&(a=!0)));break;case"group":case"folder":case"inline":(o.minItems||o.maxItems)&&(i=Utility.trimExplode(",",e.value).length,void 0!==o.minItems&&(l=1*o.minItems,!isNaN(l)&&i<l&&(a=!0)),void 0!==o.maxItems&&(s=1*o.maxItems,!isNaN(s)&&i>s&&(a=!0)));break;case"min":(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement)&&e.value.length>0&&e.value.length<e.minLength&&(a=!0)}}const m=!a,d=e.closest(FormEngineValidation.markerSelector);return null!==d&&d.classList.toggle(FormEngineValidation.errorClass,!m),FormEngineValidation.markParentTab(e,m),formEngineFormElement.dispatchEvent(new CustomEvent("t3-formengine-postfieldvalidation",{cancelable:!1,bubbles:!0})),o},FormEngineValidation.processByEvals=function(e,n){if(void 0!==e.evalList){const t=Utility.trimExplode(",",e.evalList);for(const a of t)n=FormEngineValidation.processValue(a,n,e)}return n},FormEngineValidation.processValue=function(e,n,t){let a="",i="",o=0,r=n;switch(e){case"alpha":case"num":case"alphanum":case"alphanum_x":for(a="",o=0;o<n.length;o++){const t=n.substr(o,1);let i="_"===t||"-"===t,r=t>="a"&&t<="z"||t>="A"&&t<="Z",l=t>="0"&&t<="9";switch(e){case"alphanum":i=!1;break;case"alpha":l=!1,i=!1;break;case"num":r=!1,i=!1}(r||l||i)&&(a+=t)}a!==n&&(r=a);break;case"is_in":if(t.is_in){i=""+n,t.is_in=t.is_in.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&");const e=new RegExp("[^"+t.is_in+"]+","g");a=i.replace(e,"")}else a=i;r=a;break;case"nospace":r=(""+n).replace(/ /g,"");break;case"md5":""!==n&&(r=Md5.hash(n));break;case"upper":r=n.toUpperCase();break;case"lower":r=n.toLowerCase();break;case"integer":""!==n&&(r=FormEngineValidation.parseInt(n));break;case"decimal":""!==n&&(r=FormEngineValidation.parseDouble(n));break;case"trim":r=String(n).trim();break;case"datetime":""!==n&&(r=FormEngineValidation.parseDateTime(n));break;case"date":""!==n&&(r=FormEngineValidation.parseDate(n));break;case"time":case"timesec":""!==n&&(r=FormEngineValidation.parseTime(n,e));break;case"year":""!==n&&(r=FormEngineValidation.parseYear(n));break;case"null":case"password":break;default:customEvaluations.has(e)?r=customEvaluations.get(e).call(null,n):"object"==typeof TBE_EDITOR&&void 0!==TBE_EDITOR.customEvalFunctions&&"function"==typeof TBE_EDITOR.customEvalFunctions[e]&&(r=TBE_EDITOR.customEvalFunctions[e](n))}return r},FormEngineValidation.validate=function(e){(void 0===e||e instanceof Document)&&formEngineFormElement.querySelectorAll(FormEngineValidation.markerSelector+", .t3js-tabmenu-item").forEach((e=>{e.classList.remove(FormEngineValidation.errorClass,"has-validation-error")}));const n=e||document;for(const e of n.querySelectorAll(FormEngineValidation.rulesSelector))if(null===e.closest(".t3js-flex-section-deleted, .t3js-inline-record-deleted, .t3js-file-reference-deleted")){let n=!1;const t=e.value,a=FormEngineValidation.validateField(e,t);if(Array.isArray(a)&&Array.isArray(t)){if(a.length!==t.length)n=!0;else for(let e=0;e<a.length;e++)if(a[e]!==t[e]){n=!0;break}}else a.length&&t!==a&&(n=!0);n&&(e.disabled&&e.dataset.enableOnModification&&(e.disabled=!1),e.value=a)}},FormEngineValidation.markFieldAsChanged=function(e){if(e instanceof $&&(console.warn("Passing a jQuery element to FormEngineValidation.markFieldAsChanged() is deprecated and will be removed in TYPO3 v14."),console.trace(),e=e.get(0)),!(e instanceof HTMLElement))return;const n=e.closest(".t3js-formengine-palette-field");null!==n&&n.classList.add("has-change")},FormEngineValidation.parseInt=function(e){if(!e)return 0;const n=parseInt(""+e,10);return isNaN(n)?0:n},FormEngineValidation.parseDouble=function(e,n=2){let t=""+e;t=t.replace(/[^0-9,.-]/g,"");const a=t.startsWith("-");t=t.replace(/-/g,""),t=t.replace(/,/g,"."),-1===t.indexOf(".")&&(t+=".0");const i=t.split("."),o=i.pop();let r=Number(i.join("")+"."+o);return a&&(r*=-1),t=r.toFixed(n),t},FormEngineValidation.parseDateTime=function(e){const n=e.indexOf(" ");if(-1!==n){const t=FormEngineValidation.parseDate(e.substring(n+1));FormEngineValidation.lastTime=t+FormEngineValidation.parseTime(e.substring(0,n),"time")}else FormEngineValidation.lastTime=FormEngineValidation.parseDate(e);return FormEngineValidation.lastTime},FormEngineValidation.parseDate=function(e){return FormEngineValidation.lastDate=DateTime.fromFormat(e,"dd-MM-yyyy",{zone:"utc"}).toUnixInteger(),FormEngineValidation.lastDate},FormEngineValidation.parseTime=function(e,n){const t="timesec"===n?"HH:mm:ss":"HH:mm";return FormEngineValidation.lastTime=DateTime.fromFormat(e,t,{zone:"utc"}).set({year:1970,month:1,day:1}).toUnixInteger(),FormEngineValidation.lastTime<0&&(FormEngineValidation.lastTime+=86400),FormEngineValidation.lastTime},FormEngineValidation.parseYear=function(e){let n=parseInt(e,10);return isNaN(n)&&(n=FormEngineValidation.getYear(new Date)),FormEngineValidation.lastYear=n,FormEngineValidation.lastYear},FormEngineValidation.getYear=function(e){return null===e?null:e.getUTCFullYear()},FormEngineValidation.getDate=function(e){const n=new Date(FormEngineValidation.getYear(e),e.getUTCMonth(),e.getUTCDate());return FormEngineValidation.getTimestamp(n)},FormEngineValidation.pol=function(foreign,value){return eval(("-"==foreign?"-":"")+value)},FormEngineValidation.getTimestamp=function(e){return Date.parse(e instanceof Date?e.toISOString():e)/1e3},FormEngineValidation.getTime=function(e){return 60*e.getUTCHours()*60+60*e.getUTCMinutes()+FormEngineValidation.getSecs(e)},FormEngineValidation.getSecs=function(e){return e.getUTCSeconds()},FormEngineValidation.getTimeSecs=function(e){return 60*e.getHours()*60+60*e.getMinutes()+e.getSeconds()},FormEngineValidation.markParentTab=function(e,n){DomHelper.parents(e,".tab-pane").forEach((e=>{n&&(n=null===e.querySelector(".has-error"));const t=e.id;formEngineFormElement.querySelector('a[href="#'+t+'"]').closest(".t3js-tabmenu-item").classList.toggle("has-validation-error",!n)}))},FormEngineValidation.registerSubmitCallback=function(){DocumentSaveActions.getInstance().addPreSubmitCallback((()=>{if(null===document.querySelector("."+FormEngineValidation.errorClass))return!0;const e=Modal.confirm(TYPO3.lang.alert||"Alert",TYPO3.lang["FormEngine.fieldsMissing"],Severity.error,[{text:TYPO3.lang["button.ok"]||"OK",active:!0,btnClass:"btn-default",name:"ok"}]);return e.addEventListener("button.clicked",(()=>e.hideModal())),!1}))},FormEngineValidation}());
\ No newline at end of file
+import $ from"jquery";import{DateTime}from"luxon";import Md5 from"@typo3/backend/hashing/md5.js";import Modal from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import Utility from"@typo3/backend/utility.js";import RegularEvent from"@typo3/core/event/regular-event.js";import DomHelper from"@typo3/backend/utility/dom-helper.js";import{selector}from"@typo3/core/literals.js";import SubmitInterceptor from"@typo3/backend/form/submit-interceptor.js";export default(function(){const FormEngineValidation={rulesSelector:"[data-formengine-validation-rules]",inputSelector:"[data-formengine-input-params]",markerSelector:".t3js-formengine-validation-marker",groupFieldHiddenElement:".t3js-formengine-field-group input[type=hidden]",relatedFieldSelector:"[data-relatedfieldname]",errorClass:"has-error",lastYear:0,lastDate:0,lastTime:0,passwordDummy:"********"};let formEngineFormElement;const customEvaluations=new Map;return FormEngineValidation.initialize=function(e){formEngineFormElement=e,formEngineFormElement.querySelectorAll("."+FormEngineValidation.errorClass).forEach((e=>e.classList.remove(FormEngineValidation.errorClass))),FormEngineValidation.initializeInputFields(),new RegularEvent("change",((e,n)=>{FormEngineValidation.validateField(n),FormEngineValidation.markFieldAsChanged(n)})).delegateTo(formEngineFormElement,FormEngineValidation.rulesSelector),FormEngineValidation.registerSubmitCallback();const n=new Date;FormEngineValidation.lastYear=FormEngineValidation.getYear(n),FormEngineValidation.lastDate=FormEngineValidation.getDate(n),FormEngineValidation.lastTime=0,FormEngineValidation.validate()},FormEngineValidation.initializeInputFields=function(){formEngineFormElement.querySelectorAll(FormEngineValidation.inputSelector).forEach((e=>{const n=JSON.parse(e.dataset.formengineInputParams).field,t=formEngineFormElement.querySelector(selector`[name="${n}"]`);"formengineInputInitialized"in e.dataset||(t.dataset.config=e.dataset.formengineInputParams,FormEngineValidation.initializeInputField(n))}))},FormEngineValidation.initializeInputField=function(e){const n=formEngineFormElement.querySelector(selector`[name="${e}"]`),t=formEngineFormElement.querySelector(selector`[data-formengine-input-name="${e}"]`);if(void 0!==n.dataset.config){const e=JSON.parse(n.dataset.config),a=FormEngineValidation.formatByEvals(e,n.value);a.length&&(t.value=a)}new RegularEvent("change",(()=>{FormEngineValidation.updateInputField(t.dataset.formengineInputName)})).bindTo(t),t.dataset.formengineInputInitialized="true"},FormEngineValidation.registerCustomEvaluation=function(e,n){customEvaluations.has(e)||customEvaluations.set(e,n)},FormEngineValidation.formatByEvals=function(e,n){if(void 0!==e.evalList){const t=Utility.trimExplode(",",e.evalList);for(const a of t)n=FormEngineValidation.formatValue(a,n,e)}return n},FormEngineValidation.formatValue=function(e,n,t){let a,i,o="";switch(e){case"date":if(n.toString().indexOf("-")>0){o=DateTime.fromISO(n.toString(),{zone:"utc"}).toFormat("dd-MM-yyyy")}else{if(""===n||"0"===n)return"";if(a=parseInt(n.toString(),10),isNaN(a))return"";i=new Date(1e3*a);o=i.getUTCDate().toString(10).padStart(2,"0")+"-"+(i.getUTCMonth()+1).toString(10).padStart(2,"0")+"-"+this.getYear(i)}break;case"datetime":if(""===n||"0"===n)return"";o=(FormEngineValidation.formatValue("time",n,t)+" "+FormEngineValidation.formatValue("date",n,t)).trim();break;case"time":case"timesec":let r;if(n.toString().indexOf("-")>0)r=DateTime.fromISO(n.toString(),{zone:"utc"});else{if(""===n||"0"===n)return"";if(a="number"==typeof n?n:parseInt(n),isNaN(a))return"";r=DateTime.fromSeconds(a,{zone:"utc"})}o="timesec"===e?r.toFormat("HH:mm:ss"):r.toFormat("HH:mm");break;case"password":o=n?FormEngineValidation.passwordDummy:"";break;default:o=n.toString()}return o},FormEngineValidation.updateInputField=function(e){const n=formEngineFormElement.querySelector(selector`[name="${e}"]`),t=formEngineFormElement.querySelector(selector`[data-formengine-input-name="${e}"]`);if(void 0!==n.dataset.config){const e=JSON.parse(n.dataset.config),a=FormEngineValidation.processByEvals(e,t.value),i=FormEngineValidation.formatByEvals(e,a);n.value!==a&&(n.disabled&&n.dataset.enableOnModification&&(n.disabled=!1),n.value=a,n.dispatchEvent(new Event("change")),t.value=i)}},FormEngineValidation.validateField=function(e,n){if(e instanceof $&&(console.warn("Passing a jQuery element to FormEngineValidation.validateField() is deprecated and will be removed in TYPO3 v14."),console.trace(),e=e.get(0)),!(e instanceof HTMLElement))return n;if(n=n||e.value||"",void 0===e.dataset.formengineValidationRules)return n;const t=JSON.parse(e.dataset.formengineValidationRules);let a=!1,i=0;const o=n;let r,l,s;Array.isArray(n)||(n=n.trimStart());for(const o of t){if(a)break;switch(o.type){case"required":""===n&&(a=!0,e.closest(FormEngineValidation.markerSelector).classList.add(FormEngineValidation.errorClass));break;case"range":if(""!==n){if((o.minItems||o.maxItems)&&(r=formEngineFormElement.querySelector(selector`[name="${e.dataset.relatedfieldname}"]`),i=null!==r?Utility.trimExplode(",",r.value).length:parseInt(e.value,10),void 0!==o.minItems&&(l=1*o.minItems,!isNaN(l)&&i<l&&(a=!0)),void 0!==o.maxItems&&(s=1*o.maxItems,!isNaN(s)&&i>s&&(a=!0))),void 0!==o.lower){const e=1*o.lower;!isNaN(e)&&parseInt(n,10)<e&&(a=!0)}if(void 0!==o.upper){const e=1*o.upper;!isNaN(e)&&parseInt(n,10)>e&&(a=!0)}}break;case"select":case"category":(o.minItems||o.maxItems)&&(r=formEngineFormElement.querySelector(selector`[name="${e.dataset.relatedfieldname}"]`),i=null!==r?Utility.trimExplode(",",r.value).length:e instanceof HTMLSelectElement?e.querySelectorAll("option:checked").length:e.querySelectorAll("input[value]:checked").length,void 0!==o.minItems&&(l=1*o.minItems,!isNaN(l)&&i<l&&(a=!0)),void 0!==o.maxItems&&(s=1*o.maxItems,!isNaN(s)&&i>s&&(a=!0)));break;case"group":case"folder":case"inline":(o.minItems||o.maxItems)&&(i=Utility.trimExplode(",",e.value).length,void 0!==o.minItems&&(l=1*o.minItems,!isNaN(l)&&i<l&&(a=!0)),void 0!==o.maxItems&&(s=1*o.maxItems,!isNaN(s)&&i>s&&(a=!0)));break;case"min":(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement)&&e.value.length>0&&e.value.length<e.minLength&&(a=!0)}}const m=!a,d=e.closest(FormEngineValidation.markerSelector);return null!==d&&d.classList.toggle(FormEngineValidation.errorClass,!m),FormEngineValidation.markParentTab(e,m),formEngineFormElement.dispatchEvent(new CustomEvent("t3-formengine-postfieldvalidation",{cancelable:!1,bubbles:!0})),o},FormEngineValidation.processByEvals=function(e,n){if(void 0!==e.evalList){const t=Utility.trimExplode(",",e.evalList);for(const a of t)n=FormEngineValidation.processValue(a,n,e)}return n},FormEngineValidation.processValue=function(e,n,t){let a="",i="",o=0,r=n;switch(e){case"alpha":case"num":case"alphanum":case"alphanum_x":for(a="",o=0;o<n.length;o++){const t=n.substr(o,1);let i="_"===t||"-"===t,r=t>="a"&&t<="z"||t>="A"&&t<="Z",l=t>="0"&&t<="9";switch(e){case"alphanum":i=!1;break;case"alpha":l=!1,i=!1;break;case"num":r=!1,i=!1}(r||l||i)&&(a+=t)}a!==n&&(r=a);break;case"is_in":if(t.is_in){i=""+n,t.is_in=t.is_in.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&");const e=new RegExp("[^"+t.is_in+"]+","g");a=i.replace(e,"")}else a=i;r=a;break;case"nospace":r=(""+n).replace(/ /g,"");break;case"md5":""!==n&&(r=Md5.hash(n));break;case"upper":r=n.toUpperCase();break;case"lower":r=n.toLowerCase();break;case"integer":""!==n&&(r=FormEngineValidation.parseInt(n));break;case"decimal":""!==n&&(r=FormEngineValidation.parseDouble(n));break;case"trim":r=String(n).trim();break;case"datetime":""!==n&&(r=FormEngineValidation.parseDateTime(n));break;case"date":""!==n&&(r=FormEngineValidation.parseDate(n));break;case"time":case"timesec":""!==n&&(r=FormEngineValidation.parseTime(n,e));break;case"year":""!==n&&(r=FormEngineValidation.parseYear(n));break;case"null":case"password":break;default:customEvaluations.has(e)?r=customEvaluations.get(e).call(null,n):"object"==typeof TBE_EDITOR&&void 0!==TBE_EDITOR.customEvalFunctions&&"function"==typeof TBE_EDITOR.customEvalFunctions[e]&&(r=TBE_EDITOR.customEvalFunctions[e](n))}return r},FormEngineValidation.validate=function(e){(void 0===e||e instanceof Document)&&formEngineFormElement.querySelectorAll(FormEngineValidation.markerSelector+", .t3js-tabmenu-item").forEach((e=>{e.classList.remove(FormEngineValidation.errorClass,"has-validation-error")}));const n=e||document;for(const e of n.querySelectorAll(FormEngineValidation.rulesSelector))if(null===e.closest(".t3js-flex-section-deleted, .t3js-inline-record-deleted, .t3js-file-reference-deleted")){let n=!1;const t=e.value,a=FormEngineValidation.validateField(e,t);if(Array.isArray(a)&&Array.isArray(t)){if(a.length!==t.length)n=!0;else for(let e=0;e<a.length;e++)if(a[e]!==t[e]){n=!0;break}}else a.length&&t!==a&&(n=!0);n&&(e.disabled&&e.dataset.enableOnModification&&(e.disabled=!1),e.value=a)}},FormEngineValidation.markFieldAsChanged=function(e){if(e instanceof $&&(console.warn("Passing a jQuery element to FormEngineValidation.markFieldAsChanged() is deprecated and will be removed in TYPO3 v14."),console.trace(),e=e.get(0)),!(e instanceof HTMLElement))return;const n=e.closest(".t3js-formengine-palette-field");null!==n&&n.classList.add("has-change")},FormEngineValidation.parseInt=function(e){if(!e)return 0;const n=parseInt(""+e,10);return isNaN(n)?0:n},FormEngineValidation.parseDouble=function(e,n=2){let t=""+e;t=t.replace(/[^0-9,.-]/g,"");const a=t.startsWith("-");t=t.replace(/-/g,""),t=t.replace(/,/g,"."),-1===t.indexOf(".")&&(t+=".0");const i=t.split("."),o=i.pop();let r=Number(i.join("")+"."+o);return a&&(r*=-1),t=r.toFixed(n),t},FormEngineValidation.parseDateTime=function(e){const n=e.indexOf(" ");if(-1!==n){const t=FormEngineValidation.parseDate(e.substring(n+1));FormEngineValidation.lastTime=t+FormEngineValidation.parseTime(e.substring(0,n),"time")}else FormEngineValidation.lastTime=FormEngineValidation.parseDate(e);return FormEngineValidation.lastTime},FormEngineValidation.parseDate=function(e){return FormEngineValidation.lastDate=DateTime.fromFormat(e,"dd-MM-yyyy",{zone:"utc"}).toUnixInteger(),FormEngineValidation.lastDate},FormEngineValidation.parseTime=function(e,n){const t="timesec"===n?"HH:mm:ss":"HH:mm";return FormEngineValidation.lastTime=DateTime.fromFormat(e,t,{zone:"utc"}).set({year:1970,month:1,day:1}).toUnixInteger(),FormEngineValidation.lastTime<0&&(FormEngineValidation.lastTime+=86400),FormEngineValidation.lastTime},FormEngineValidation.parseYear=function(e){let n=parseInt(e,10);return isNaN(n)&&(n=FormEngineValidation.getYear(new Date)),FormEngineValidation.lastYear=n,FormEngineValidation.lastYear},FormEngineValidation.getYear=function(e){return null===e?null:e.getUTCFullYear()},FormEngineValidation.getDate=function(e){const n=new Date(FormEngineValidation.getYear(e),e.getUTCMonth(),e.getUTCDate());return FormEngineValidation.getTimestamp(n)},FormEngineValidation.pol=function(foreign,value){return eval(("-"==foreign?"-":"")+value)},FormEngineValidation.getTimestamp=function(e){return Date.parse(e instanceof Date?e.toISOString():e)/1e3},FormEngineValidation.getTime=function(e){return 60*e.getUTCHours()*60+60*e.getUTCMinutes()+FormEngineValidation.getSecs(e)},FormEngineValidation.getSecs=function(e){return e.getUTCSeconds()},FormEngineValidation.getTimeSecs=function(e){return 60*e.getHours()*60+60*e.getMinutes()+e.getSeconds()},FormEngineValidation.markParentTab=function(e,n){DomHelper.parents(e,".tab-pane").forEach((e=>{n&&(n=null===e.querySelector(".has-error"));const t=e.id;formEngineFormElement.querySelector('a[href="#'+t+'"]').closest(".t3js-tabmenu-item").classList.toggle("has-validation-error",!n)}))},FormEngineValidation.registerSubmitCallback=function(){new SubmitInterceptor(formEngineFormElement).addPreSubmitCallback((()=>{if(null===document.querySelector("."+FormEngineValidation.errorClass))return!0;const e=Modal.confirm(TYPO3.lang.alert||"Alert",TYPO3.lang["FormEngine.fieldsMissing"],Severity.error,[{text:TYPO3.lang["button.ok"]||"OK",active:!0,btnClass:"btn-default",name:"ok"}]);return e.addEventListener("button.clicked",(()=>e.hideModal())),!1}))},FormEngineValidation}());
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/form/submit-interceptor.js b/typo3/sysext/backend/Resources/Public/JavaScript/form/submit-interceptor.js
new file mode 100644
index 000000000000..e4980e37fc7a
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/form/submit-interceptor.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+import Icons from"@typo3/backend/icons.js";export default class SubmitInterceptor{constructor(t){this.isSubmitting=!1,this.preSubmitCallbacks=[],t.addEventListener("submit",this.submitHandler.bind(this))}addPreSubmitCallback(t){if("function"!=typeof t)throw"callback must be a function.";return this.preSubmitCallbacks.push(t),this}submitHandler(t){if(!this.isSubmitting){for(const e of this.preSubmitCallbacks){if(!e(t))return void t.preventDefault()}this.isSubmitting=!0,null!==t.submitter&&((t.submitter instanceof HTMLInputElement||t.submitter instanceof HTMLButtonElement)&&(t.submitter.disabled=!0),Icons.getIcon("spinner-circle",Icons.sizes.small).then((e=>{t.submitter.replaceChild(document.createRange().createContextualFragment(e),t.submitter.querySelector(".t3js-icon"))})).catch((()=>{})))}}}
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103528-DeprecatedDocumentSaveActionsModule.rst b/typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103528-DeprecatedDocumentSaveActionsModule.rst
new file mode 100644
index 000000000000..495650329b92
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.1/Deprecation-103528-DeprecatedDocumentSaveActionsModule.rst
@@ -0,0 +1,69 @@
+.. include:: /Includes.rst.txt
+
+.. _deprecation-103528-1712153304:
+
+==============================================================
+Deprecation: #103528 - Deprecated `DocumentSaveActions` module
+==============================================================
+
+See :issue:`103528`
+
+Description
+===========
+
+The JavaScript module :js:`@typo3/backend/document-save-actions.js` was
+introduced in TYPO3 v7 to add some interactivity in FormEngine context.
+At first it was only used to disable the submit button and render a
+spinner icon instead. Over the course of some years, the module got more
+functionality, for example to prevent saving when validation fails.
+
+Since some refactorings within FormEngine, the module rather became a
+burden. This became visible with the introduction of the Hotkeys API, as
+the :js:`@typo3/backend/document-save-actions.js` reacts on explicit :js:`click`
+events on the save icon, that is not triggered when FormEngine invokes a
+save action via keyboard shortcuts. Adjusting :js:`document-save-actions.js`'s
+behavior is necessary, but would become a breaking change, which is
+unacceptable after the 13.0 release. For this reason, said module has
+been marked as deprecated and its usages are replaced by its successor
+:js:`@typo3/backend/form/submit-interceptor.js`.
+
+
+Impact
+======
+
+Using the JavaScript module :js:`@typo3/backend/document-save-actions.js` will
+render a deprecation warning in the browser's console.
+
+
+Affected installations
+======================
+
+All installations relying on :js:`@typo3/backend/document-save-actions.js` are
+affected.
+
+
+Migration
+=========
+
+To migrate the interception of submit events, the successor module
+:js:`@typo3/backend/form/submit-interceptor.js` shall be used instead.
+
+The usage is similar to :js:`@typo3/backend/document-save-actions.js`, but
+requires the form HTML element in its constructor.
+
+Example
+-------
+
+..  code-block:: js
+
+    import '@typo3/backend/form/submit-interceptor.js';
+
+    // ...
+
+    const formElement = document.querySelector('form');
+    const submitInterceptor = new SubmitInterceptor(formElement);
+    submitInterceptor.addPreSubmitCallback(function() {
+        // the same handling as in @typo3/backend/document-save-actions.js
+    });
+
+.. index:: Backend, JavaScript, NotScanned, ext:backend
diff --git a/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js b/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js
index 0f521eef236a..dac136233460 100644
--- a/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js
+++ b/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import SortableTable from"@typo3/backend/sortable-table.js";import DocumentSaveActions from"@typo3/backend/document-save-actions.js";import RegularEvent from"@typo3/core/event/regular-event.js";import Modal from"@typo3/backend/modal.js";import Icons from"@typo3/backend/icons.js";import{MessageUtility}from"@typo3/backend/utility/message-utility.js";import PersistentStorage from"@typo3/backend/storage/persistent.js";import DateTimePicker from"@typo3/backend/date-time-picker.js";import{MultiRecordSelectionSelectors}from"@typo3/backend/multi-record-selection.js";import Severity from"@typo3/backend/severity.js";import DocumentService from"@typo3/core/document-service.js";class Scheduler{constructor(){DocumentService.ready().then((()=>{this.initializeEvents(),this.initializeDefaultStates(),this.initializeCloseConfirm()})),DocumentSaveActions.registerEvents()}static updateClearableInputs(){const e=document.querySelectorAll(".t3js-clearable");e.length>0&&import("@typo3/backend/input/clearable.js").then((function(){e.forEach((e=>e.clearable()))}))}static updateElementBrowserTriggers(){document.querySelectorAll(".t3js-element-browser").forEach((e=>{const t=document.getElementById(e.dataset.triggerFor);e.dataset.params=t.name+"|||pages"}))}static resolveDefaultNumberOfDays(){const e=document.getElementById("task_tableGarbageCollection_numberOfDays");return null===e||void 0===e.dataset.defaultNumberOfDays?null:JSON.parse(e.dataset.defaultNumberOfDays)}static storeCollapseState(e,t){let a={};PersistentStorage.isset("moduleData.scheduler_manage")&&(a=PersistentStorage.get("moduleData.scheduler_manage"));const n={};n[e]=t?1:0,a={...a,...n},PersistentStorage.set("moduleData.scheduler_manage",a)}toggleTaskSettingFields(e){let t=e.value;t=t.toLowerCase().replace(/\\/g,"-");for(const e of document.querySelectorAll(".extraFields")){const a=e.classList.contains("extra_fields_"+t);e.querySelectorAll("input, textarea, select").forEach((e=>{e.disabled=!a})),e.hidden=!a}}actOnChangeSchedulerTableGarbageCollectionAllTables(e){const t=document.querySelector("#task_tableGarbageCollection_numberOfDays"),a=document.querySelector("#task_tableGarbageCollection_table");if(e.checked)a.disabled=!0,t.disabled=!0;else{let e=parseInt(t.value,10);if(e<1){const t=a.value,n=Scheduler.resolveDefaultNumberOfDays();null!==n&&(e=n[t])}a.disabled=!1,e>0&&(t.disabled=!1)}}actOnChangeSchedulerTableGarbageCollectionTable(e){const t=document.querySelector("#task_tableGarbageCollection_numberOfDays"),a=Scheduler.resolveDefaultNumberOfDays();null!==a&&a[e.value]>0?(t.disabled=!1,t.value=a[e.value].toString(10)):(t.disabled=!0,t.value="0")}toggleFieldsByTaskType(e){const t=2===(e=parseInt(e+"",10));document.querySelector("#task_end_col").hidden=!t,document.querySelector("#task_frequency_row").hidden=!t,document.querySelector("#task_multiple_row").hidden=!t}initializeEvents(){const e=document.querySelector("#task_class");e&&new RegularEvent("change",(e=>{this.toggleTaskSettingFields(e.target)})).bindTo(e);const t=document.querySelector("#task_type");t&&new RegularEvent("change",(e=>{this.toggleFieldsByTaskType(e.target.value)})).bindTo(t);const a=document.querySelector("#task_tableGarbageCollection_allTables");a&&new RegularEvent("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionAllTables(e.target)})).bindTo(a);const n=document.querySelector("#task_tableGarbageCollection_table");n&&new RegularEvent("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionTable(e.target)})).bindTo(n);const o=document.querySelector("[data-update-task-frequency]");o&&new RegularEvent("change",(e=>{const t=e.target;document.querySelector("#task_frequency").value=t.value,t.value="",t.blur()})).bindTo(o),document.querySelectorAll("[data-scheduler-table]").forEach((e=>{new SortableTable(e)})),document.querySelectorAll("#tx_scheduler_form .t3js-datetimepicker").forEach((e=>DateTimePicker.initialize(e))),new RegularEvent("click",((e,t)=>{e.preventDefault();const a=new URL(t.href,window.origin);a.searchParams.set("mode",t.dataset.mode),a.searchParams.set("bparams",t.dataset.params),Modal.advanced({type:Modal.types.iframe,content:a.toString(),size:Modal.sizes.large})})).delegateTo(document,".t3js-element-browser"),new RegularEvent("show.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("hide.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go",this.executeTasks.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go_cron",this.executeTasks.bind(this)).bindTo(document),window.addEventListener("message",this.listenOnElementBrowser.bind(this))}initializeDefaultStates(){const e=document.querySelector("#task_type");null!==e&&this.toggleFieldsByTaskType(e.value);const t=document.querySelector("#task_class");null!==t&&(this.toggleTaskSettingFields(t),Scheduler.updateClearableInputs(),Scheduler.updateElementBrowserTriggers())}listenOnElementBrowser(e){if(!MessageUtility.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;if("typo3:elementBrowser:elementAdded"===e.data.actionName){if(void 0===e.data.fieldName)throw"fieldName not defined in message";if(void 0===e.data.value)throw"value not defined in message";document.querySelector('input[name="'+e.data.fieldName+'"]').value=e.data.value.split("_").pop()}}toggleCollapseIcon(e){const t="hide.bs.collapse"===e.type,a=document.querySelector('.t3js-toggle-table[data-bs-target="#'+e.target.id+'"] .t3js-icon');null!==a&&Icons.getIcon(t?"actions-view-list-expand":"actions-view-list-collapse",Icons.sizes.small).then((e=>{a.replaceWith(document.createRange().createContextualFragment(e))})),Scheduler.storeCollapseState(e.target.dataset.table,t)}executeTasks(e){const t=document.querySelector('[data-multi-record-selection-form="'+e.detail.identifier+'"]');if(null===t)return;const a=[];if(e.detail.checkboxes.forEach((e=>{const t=e.closest(MultiRecordSelectionSelectors.elementSelector);null!==t&&t.dataset.taskId&&a.push(t.dataset.taskId)})),a.length){if("multiRecordSelection:action:go_cron"===e.type){const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","scheduleCron"),e.setAttribute("value",a.join(",")),t.append(e)}else{const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","execute"),e.setAttribute("value",a.join(",")),t.append(e)}t.submit()}}initializeCloseConfirm(){const e=document.querySelector("form[name=tx_scheduler_form]");if(!e)return;const t=new FormData(e);document.querySelector(".t3js-scheduler-close").addEventListener("click",(a=>{const n=new FormData(e),o=Object.fromEntries(t.entries()),l=Object.fromEntries(n.entries());if(JSON.stringify(o)!==JSON.stringify(l)||e.querySelector('input[value="add"]')){a.preventDefault();const t=a.target.href;Modal.confirm(TYPO3.lang["label.confirm.close_without_save.title"]||"Do you want to close without saving?",TYPO3.lang["label.confirm.close_without_save.content"]||"You currently have unsaved changes. Are you sure you want to discard these changes?",Severity.warning,[{text:TYPO3.lang["buttons.confirm.close_without_save.no"]||"No, I will continue editing",btnClass:"btn-default",name:"no",trigger:()=>Modal.dismiss()},{text:TYPO3.lang["buttons.confirm.close_without_save.yes"]||"Yes, discard my changes",btnClass:"btn-default",name:"yes",trigger:()=>{Modal.dismiss(),window.location.href=t}},{text:TYPO3.lang["buttons.confirm.save_and_close"]||"Save and close",btnClass:"btn-primary",name:"save",active:!0,trigger:()=>{Modal.dismiss();const t=document.createElement("input");t.type="hidden",t.value="saveclose",t.name="CMD",e.append(t),e.submit()}}])}}))}}export default new Scheduler;
\ No newline at end of file
+import SortableTable from"@typo3/backend/sortable-table.js";import RegularEvent from"@typo3/core/event/regular-event.js";import Modal from"@typo3/backend/modal.js";import Icons from"@typo3/backend/icons.js";import{MessageUtility}from"@typo3/backend/utility/message-utility.js";import PersistentStorage from"@typo3/backend/storage/persistent.js";import DateTimePicker from"@typo3/backend/date-time-picker.js";import{MultiRecordSelectionSelectors}from"@typo3/backend/multi-record-selection.js";import Severity from"@typo3/backend/severity.js";import DocumentService from"@typo3/core/document-service.js";import SubmitInterceptor from"@typo3/backend/form/submit-interceptor.js";class Scheduler{constructor(){DocumentService.ready().then((()=>{this.initializeSubmitInterceptor(),this.initializeEvents(),this.initializeDefaultStates(),this.initializeCloseConfirm()}))}static updateClearableInputs(){const e=document.querySelectorAll(".t3js-clearable");e.length>0&&import("@typo3/backend/input/clearable.js").then((function(){e.forEach((e=>e.clearable()))}))}static updateElementBrowserTriggers(){document.querySelectorAll(".t3js-element-browser").forEach((e=>{const t=document.getElementById(e.dataset.triggerFor);e.dataset.params=t.name+"|||pages"}))}static resolveDefaultNumberOfDays(){const e=document.getElementById("task_tableGarbageCollection_numberOfDays");return null===e||void 0===e.dataset.defaultNumberOfDays?null:JSON.parse(e.dataset.defaultNumberOfDays)}static storeCollapseState(e,t){let a={};PersistentStorage.isset("moduleData.scheduler_manage")&&(a=PersistentStorage.get("moduleData.scheduler_manage"));const n={};n[e]=t?1:0,a={...a,...n},PersistentStorage.set("moduleData.scheduler_manage",a)}toggleTaskSettingFields(e){let t=e.value;t=t.toLowerCase().replace(/\\/g,"-");for(const e of document.querySelectorAll(".extraFields")){const a=e.classList.contains("extra_fields_"+t);e.querySelectorAll("input, textarea, select").forEach((e=>{e.disabled=!a})),e.hidden=!a}}actOnChangeSchedulerTableGarbageCollectionAllTables(e){const t=document.querySelector("#task_tableGarbageCollection_numberOfDays"),a=document.querySelector("#task_tableGarbageCollection_table");if(e.checked)a.disabled=!0,t.disabled=!0;else{let e=parseInt(t.value,10);if(e<1){const t=a.value,n=Scheduler.resolveDefaultNumberOfDays();null!==n&&(e=n[t])}a.disabled=!1,e>0&&(t.disabled=!1)}}actOnChangeSchedulerTableGarbageCollectionTable(e){const t=document.querySelector("#task_tableGarbageCollection_numberOfDays"),a=Scheduler.resolveDefaultNumberOfDays();null!==a&&a[e.value]>0?(t.disabled=!1,t.value=a[e.value].toString(10)):(t.disabled=!0,t.value="0")}toggleFieldsByTaskType(e){const t=2===(e=parseInt(e+"",10));document.querySelector("#task_end_col").hidden=!t,document.querySelector("#task_frequency_row").hidden=!t,document.querySelector("#task_multiple_row").hidden=!t}initializeSubmitInterceptor(){const e=document.querySelector("form[name=tx_scheduler_form]");e&&new SubmitInterceptor(e)}initializeEvents(){const e=document.querySelector("#task_class");e&&new RegularEvent("change",(e=>{this.toggleTaskSettingFields(e.target)})).bindTo(e);const t=document.querySelector("#task_type");t&&new RegularEvent("change",(e=>{this.toggleFieldsByTaskType(e.target.value)})).bindTo(t);const a=document.querySelector("#task_tableGarbageCollection_allTables");a&&new RegularEvent("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionAllTables(e.target)})).bindTo(a);const n=document.querySelector("#task_tableGarbageCollection_table");n&&new RegularEvent("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionTable(e.target)})).bindTo(n);const o=document.querySelector("[data-update-task-frequency]");o&&new RegularEvent("change",(e=>{const t=e.target;document.querySelector("#task_frequency").value=t.value,t.value="",t.blur()})).bindTo(o),document.querySelectorAll("[data-scheduler-table]").forEach((e=>{new SortableTable(e)})),document.querySelectorAll("#tx_scheduler_form .t3js-datetimepicker").forEach((e=>DateTimePicker.initialize(e))),new RegularEvent("click",((e,t)=>{e.preventDefault();const a=new URL(t.href,window.origin);a.searchParams.set("mode",t.dataset.mode),a.searchParams.set("bparams",t.dataset.params),Modal.advanced({type:Modal.types.iframe,content:a.toString(),size:Modal.sizes.large})})).delegateTo(document,".t3js-element-browser"),new RegularEvent("show.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("hide.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go",this.executeTasks.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go_cron",this.executeTasks.bind(this)).bindTo(document),window.addEventListener("message",this.listenOnElementBrowser.bind(this)),new RegularEvent("click",(e=>{e.preventDefault(),this.saveDocument(e)})).delegateTo(document,"button[form]")}saveDocument(e){const t=document.querySelector("form[name=tx_scheduler_form]");if(!t)return;const a=document.createElement("input");a.type="hidden",a.value="save",a.name="CMD",t.append(a),t.requestSubmit(e.target)}initializeDefaultStates(){const e=document.querySelector("#task_type");null!==e&&this.toggleFieldsByTaskType(e.value);const t=document.querySelector("#task_class");null!==t&&(this.toggleTaskSettingFields(t),Scheduler.updateClearableInputs(),Scheduler.updateElementBrowserTriggers())}listenOnElementBrowser(e){if(!MessageUtility.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;if("typo3:elementBrowser:elementAdded"===e.data.actionName){if(void 0===e.data.fieldName)throw"fieldName not defined in message";if(void 0===e.data.value)throw"value not defined in message";document.querySelector('input[name="'+e.data.fieldName+'"]').value=e.data.value.split("_").pop()}}toggleCollapseIcon(e){const t="hide.bs.collapse"===e.type,a=document.querySelector('.t3js-toggle-table[data-bs-target="#'+e.target.id+'"] .t3js-icon');null!==a&&Icons.getIcon(t?"actions-view-list-expand":"actions-view-list-collapse",Icons.sizes.small).then((e=>{a.replaceWith(document.createRange().createContextualFragment(e))})),Scheduler.storeCollapseState(e.target.dataset.table,t)}executeTasks(e){const t=document.querySelector('[data-multi-record-selection-form="'+e.detail.identifier+'"]');if(null===t)return;const a=[];if(e.detail.checkboxes.forEach((e=>{const t=e.closest(MultiRecordSelectionSelectors.elementSelector);null!==t&&t.dataset.taskId&&a.push(t.dataset.taskId)})),a.length){if("multiRecordSelection:action:go_cron"===e.type){const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","scheduleCron"),e.setAttribute("value",a.join(",")),t.append(e)}else{const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","execute"),e.setAttribute("value",a.join(",")),t.append(e)}t.submit()}}initializeCloseConfirm(){const e=document.querySelector("form[name=tx_scheduler_form]");if(!e)return;const t=new FormData(e);document.querySelector(".t3js-scheduler-close").addEventListener("click",(a=>{const n=new FormData(e),o=Object.fromEntries(t.entries()),l=Object.fromEntries(n.entries());if(JSON.stringify(o)!==JSON.stringify(l)||e.querySelector('input[value="add"]')){a.preventDefault();const t=a.target.href;Modal.confirm(TYPO3.lang["label.confirm.close_without_save.title"]||"Do you want to close without saving?",TYPO3.lang["label.confirm.close_without_save.content"]||"You currently have unsaved changes. Are you sure you want to discard these changes?",Severity.warning,[{text:TYPO3.lang["buttons.confirm.close_without_save.no"]||"No, I will continue editing",btnClass:"btn-default",name:"no",trigger:()=>Modal.dismiss()},{text:TYPO3.lang["buttons.confirm.close_without_save.yes"]||"Yes, discard my changes",btnClass:"btn-default",name:"yes",trigger:()=>{Modal.dismiss(),window.location.href=t}},{text:TYPO3.lang["buttons.confirm.save_and_close"]||"Save and close",btnClass:"btn-primary",name:"save",active:!0,trigger:()=>{Modal.dismiss();const t=document.createElement("input");t.type="hidden",t.value="saveclose",t.name="CMD",e.append(t),e.submit()}}])}}))}}export default new Scheduler;
\ No newline at end of file
-- 
GitLab