[FEATURE] Leaving edit by clicking in page tree does not show a warning 75/53075/13
authorOliver Hader <oliver@typo3.org>
Sat, 3 Jun 2017 12:54:54 +0000 (14:54 +0200)
committerSusanne Moog <susanne.moog@typo3.org>
Tue, 13 Jun 2017 10:38:45 +0000 (12:38 +0200)
If edit forms have unsaved changes, changing the IFRAME URL is caught by
explicitly sending interaction requests that are handled by individual
client components, such as the FormEngine. This feature does not use the
Window.beforeunload event, but some custom messaging API instead.

Click events on the ExtJS page-tree are caught if those trigger a change
request for the content component, however highlighting the clicked page
node is not caught due to nested ExtJS event hierarchies.

Resolves: #77268
Releases: master
Change-Id: I3e2359cf27d95197b17e8d8489759ace403ce1af
Reviewed-on: https://review.typo3.org/53075
Tested-by: TYPO3com <no-reply@typo3.com>
Tested-by: Jasmina LieƟmann <code@frauliessmann.de>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
23 files changed:
typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/ClientRequest.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/Consumable.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/ConsumerScope.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequest.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestAssignment.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestMap.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/TypeScript/Event/TriggerRequest.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/BackendException.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js
typo3/sysext/backend/Resources/Public/JavaScript/Event/ClientRequest.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/Event/Consumable.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/Event/ConsumerScope.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequest.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestAssignment.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestMap.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/Event/TriggerRequest.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js
typo3/sysext/backend/Resources/Public/JavaScript/ModuleMenu.js
typo3/sysext/backend/Resources/Public/JavaScript/Viewport.js
typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/actions.js
typo3/sysext/core/Documentation/Changelog/master/Feature-77268-IntroduceJavaScriptTriggerRequestAPI.rst [new file with mode: 0644]
typo3/sysext/filelist/Resources/Public/JavaScript/ContextMenuActions.js

diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts b/typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts
new file mode 100644 (file)
index 0000000..384e9d0
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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!
+ */
+
+export class BackendException {
+  public readonly message: string;
+  public readonly code: number;
+
+  constructor(message = '', code = 0) {
+    this.message = message;
+    this.code = code;
+  }
+}
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/ClientRequest.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/ClientRequest.ts
new file mode 100644 (file)
index 0000000..2aa583e
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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 InteractionRequest = require('./InteractionRequest');
+
+class ClientRequest extends InteractionRequest {
+  public readonly clientEvent: any;
+
+  constructor(type: string, clientEvent: Event = null) {
+    super(type);
+    this.clientEvent = clientEvent;
+  }
+}
+
+export = ClientRequest;
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/Consumable.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/Consumable.ts
new file mode 100644 (file)
index 0000000..845a00c
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * 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 InteractionRequest = require('./InteractionRequest');
+
+interface Consumable {
+  consume(interactionRequest: InteractionRequest): any;
+}
+
+export = Consumable;
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/ConsumerScope.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/ConsumerScope.ts
new file mode 100644 (file)
index 0000000..3adfe53
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 $ = require('jquery');
+import Consumable = require('./Consumable');
+import InteractionRequest = require('./InteractionRequest');
+
+class ConsumerScope {
+  private consumers: Consumable[] = [];
+
+  public getConsumers(): Consumable[] {
+    return this.consumers;
+  }
+
+  public hasConsumer(consumer: Consumable): boolean {
+    return this.consumers.indexOf(consumer) !== -1;
+  }
+
+  public attach(consumer: Consumable) {
+    if (!this.hasConsumer(consumer)) {
+      this.consumers.push(consumer);
+    }
+  }
+
+  public detach(consumer: Consumable) {
+    this.consumers = this.consumers.filter(
+      (currentConsumer: Consumable) => currentConsumer !== consumer,
+    );
+  }
+
+  public invoke(request: InteractionRequest): any {
+    const deferreds: any[] = [];
+    this.consumers.forEach(
+      (consumer: Consumable) => {
+        const deferred: any = consumer.consume.call(consumer, request);
+        if (deferred) {
+          deferreds.push(deferred);
+        }
+      },
+    );
+    return ($ as any).when.apply($, deferreds);
+  }
+}
+
+export = new ConsumerScope();
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequest.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequest.ts
new file mode 100644 (file)
index 0000000..e4b9197
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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!
+ */
+
+class InteractionRequest {
+  public readonly type: string;
+  public readonly parentRequest: InteractionRequest;
+  protected processed = false;
+  protected processedData: any = null;
+
+  public get outerMostRequest(): InteractionRequest {
+    let request: InteractionRequest = this;
+    while (request.parentRequest instanceof InteractionRequest) {
+      request = request.parentRequest;
+    }
+    return request;
+  }
+
+  constructor(type: string, parentRequest: InteractionRequest = null) {
+    this.type = type;
+    this.parentRequest = parentRequest;
+  }
+
+  public isProcessed(): boolean {
+    return this.processed;
+  }
+
+  public getProcessedData(): any {
+    return this.processedData;
+  }
+
+  public setProcessedData(processedData: any = null) {
+    this.processed = true;
+    this.processedData = processedData;
+  }
+}
+
+export = InteractionRequest;
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestAssignment.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestAssignment.ts
new file mode 100644 (file)
index 0000000..a2a1ca8
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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 InteractionRequest = require('./InteractionRequest');
+
+interface InteractionRequestAssignment {
+  request: InteractionRequest;
+  // @todo Add type for jQuery.Deferred[]
+  deferreds: any[];
+}
+
+export = InteractionRequestAssignment;
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestMap.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestMap.ts
new file mode 100644 (file)
index 0000000..2334eb1
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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 $ = require('jquery');
+import InteractionRequest = require('./InteractionRequest');
+import InteractionRequestAssignment = require('./InteractionRequestAssignment');
+
+class InteractionRequestMap {
+  private assignments: InteractionRequestAssignment[] = [];
+
+  public attachFor(request: InteractionRequest, deferred: any) {
+    let targetAssignment = this.getFor(request);
+    if (targetAssignment === null) {
+      targetAssignment = {request, deferreds: []} as InteractionRequestAssignment;
+      this.assignments.push(targetAssignment);
+    }
+    targetAssignment.deferreds.push(deferred);
+  }
+
+  public detachFor(request: InteractionRequest) {
+    const targetAssignment = this.getFor(request);
+    this.assignments = this.assignments.filter(
+      (assignment: InteractionRequestAssignment) => assignment === targetAssignment,
+    );
+  }
+
+  public getFor(triggerEvent: InteractionRequest): InteractionRequestAssignment {
+    let targetAssignment: InteractionRequestAssignment = null;
+    this.assignments.some(
+      (assignment: InteractionRequestAssignment) => {
+        if (assignment.request === triggerEvent) {
+          targetAssignment = assignment;
+          return true;
+        }
+        return false;
+      },
+    );
+    return targetAssignment;
+  }
+
+  public resolveFor(triggerEvent: InteractionRequest) {
+    const targetAssignment = this.getFor(triggerEvent);
+    if (targetAssignment === null) {
+      return false;
+    }
+    targetAssignment.deferreds.forEach(
+      (deferred: any) => deferred.resolve(),
+    );
+    this.detachFor(triggerEvent);
+    return true;
+  }
+
+  public rejectFor(triggerEvent: InteractionRequest) {
+    const targetAssignment = this.getFor(triggerEvent);
+    if (targetAssignment === null) {
+      return false;
+    }
+    targetAssignment.deferreds.forEach(
+      (deferred: any) => deferred.reject(),
+    );
+    this.detachFor(triggerEvent);
+    return true;
+  }
+}
+
+export = new InteractionRequestMap();
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/Event/TriggerRequest.ts b/typo3/sysext/backend/Resources/Private/TypeScript/Event/TriggerRequest.ts
new file mode 100644 (file)
index 0000000..433c6fd
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 InteractionRequest = require('./InteractionRequest');
+
+class TriggerRequest extends InteractionRequest {
+  constructor(type: string, parentRequest: InteractionRequest = null) {
+    super(type, parentRequest);
+  }
+
+  public concerns(ancestorRequest: InteractionRequest): boolean {
+    if (this === ancestorRequest) {
+      return true;
+    }
+    let request: InteractionRequest = this;
+    while (request.parentRequest instanceof InteractionRequest) {
+      request = request.parentRequest;
+      if (request === ancestorRequest) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public concernsTypes(types: string[]): boolean {
+    if (types.indexOf(this.type) !== -1) {
+      return true;
+    }
+    let request: InteractionRequest = this;
+    while (request.parentRequest instanceof InteractionRequest) {
+      request = request.parentRequest;
+      if (types.indexOf(request.type) !== -1) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
+
+export = TriggerRequest;
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/BackendException.js b/typo3/sysext/backend/Resources/Public/JavaScript/BackendException.js
new file mode 100644 (file)
index 0000000..3f752b1
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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!
+ */
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    var BackendException = (function () {
+        function BackendException(message, code) {
+            if (message === void 0) { message = ''; }
+            if (code === void 0) { code = 0; }
+            this.message = message;
+            this.code = code;
+        }
+        return BackendException;
+    }());
+    exports.BackendException = BackendException;
+});
index 9420146..999d965 100644 (file)
@@ -145,7 +145,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][' + table + '%7C' + uid + ']=1'+ '&CB[setCopyMode]=1';
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };
 
@@ -153,7 +153,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][' + table + '%7C' + uid + ']=0';
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };
 
@@ -161,7 +161,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][' + table + '%7C' + uid + ']=1'+ '&CB[setCopyMode]=0';
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };
 
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/ClientRequest.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/ClientRequest.js
new file mode 100644 (file)
index 0000000..1052a89
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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!
+ */
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+define(["require", "exports", "./InteractionRequest"], function (require, exports, InteractionRequest) {
+    "use strict";
+    var ClientRequest = (function (_super) {
+        __extends(ClientRequest, _super);
+        function ClientRequest(type, clientEvent) {
+            if (clientEvent === void 0) { clientEvent = null; }
+            var _this = _super.call(this, type) || this;
+            _this.clientEvent = clientEvent;
+            return _this;
+        }
+        return ClientRequest;
+    }(InteractionRequest));
+    return ClientRequest;
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/Consumable.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/Consumable.js
new file mode 100644 (file)
index 0000000..fcabab4
--- /dev/null
@@ -0,0 +1,16 @@
+/*
+ * 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!
+ */
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/ConsumerScope.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/ConsumerScope.js
new file mode 100644 (file)
index 0000000..440cab8
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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!
+ */
+define(["require", "exports", "jquery"], function (require, exports, $) {
+    "use strict";
+    var ConsumerScope = (function () {
+        function ConsumerScope() {
+            this.consumers = [];
+        }
+        ConsumerScope.prototype.getConsumers = function () {
+            return this.consumers;
+        };
+        ConsumerScope.prototype.hasConsumer = function (consumer) {
+            return this.consumers.indexOf(consumer) !== -1;
+        };
+        ConsumerScope.prototype.attach = function (consumer) {
+            if (!this.hasConsumer(consumer)) {
+                this.consumers.push(consumer);
+            }
+        };
+        ConsumerScope.prototype.detach = function (consumer) {
+            this.consumers = this.consumers.filter(function (currentConsumer) { return currentConsumer !== consumer; });
+        };
+        ConsumerScope.prototype.invoke = function (request) {
+            var deferreds = [];
+            this.consumers.forEach(function (consumer) {
+                var deferred = consumer.consume.call(consumer, request);
+                if (deferred) {
+                    deferreds.push(deferred);
+                }
+            });
+            return $.when.apply($, deferreds);
+        };
+        return ConsumerScope;
+    }());
+    return new ConsumerScope();
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequest.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequest.js
new file mode 100644 (file)
index 0000000..1b726f5
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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!
+ */
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    var InteractionRequest = (function () {
+        function InteractionRequest(type, parentRequest) {
+            if (parentRequest === void 0) { parentRequest = null; }
+            this.processed = false;
+            this.processedData = null;
+            this.type = type;
+            this.parentRequest = parentRequest;
+        }
+        Object.defineProperty(InteractionRequest.prototype, "outerMostRequest", {
+            get: function () {
+                var request = this;
+                while (request.parentRequest instanceof InteractionRequest) {
+                    request = request.parentRequest;
+                }
+                return request;
+            },
+            enumerable: true,
+            configurable: true
+        });
+        InteractionRequest.prototype.isProcessed = function () {
+            return this.processed;
+        };
+        InteractionRequest.prototype.getProcessedData = function () {
+            return this.processedData;
+        };
+        InteractionRequest.prototype.setProcessedData = function (processedData) {
+            if (processedData === void 0) { processedData = null; }
+            this.processed = true;
+            this.processedData = processedData;
+        };
+        return InteractionRequest;
+    }());
+    return InteractionRequest;
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestAssignment.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestAssignment.js
new file mode 100644 (file)
index 0000000..fcabab4
--- /dev/null
@@ -0,0 +1,16 @@
+/*
+ * 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!
+ */
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestMap.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestMap.js
new file mode 100644 (file)
index 0000000..9983ef9
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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!
+ */
+define(["require", "exports"], function (require, exports) {
+    "use strict";
+    var InteractionRequestMap = (function () {
+        function InteractionRequestMap() {
+            this.assignments = [];
+        }
+        InteractionRequestMap.prototype.attachFor = function (request, deferred) {
+            var targetAssignment = this.getFor(request);
+            if (targetAssignment === null) {
+                targetAssignment = { request: request, deferreds: [] };
+                this.assignments.push(targetAssignment);
+            }
+            targetAssignment.deferreds.push(deferred);
+        };
+        InteractionRequestMap.prototype.detachFor = function (request) {
+            var targetAssignment = this.getFor(request);
+            this.assignments = this.assignments.filter(function (assignment) { return assignment === targetAssignment; });
+        };
+        InteractionRequestMap.prototype.getFor = function (triggerEvent) {
+            var targetAssignment = null;
+            this.assignments.some(function (assignment) {
+                if (assignment.request === triggerEvent) {
+                    targetAssignment = assignment;
+                    return true;
+                }
+                return false;
+            });
+            return targetAssignment;
+        };
+        InteractionRequestMap.prototype.resolveFor = function (triggerEvent) {
+            var targetAssignment = this.getFor(triggerEvent);
+            if (targetAssignment === null) {
+                return false;
+            }
+            targetAssignment.deferreds.forEach(function (deferred) { return deferred.resolve(); });
+            this.detachFor(triggerEvent);
+            return true;
+        };
+        InteractionRequestMap.prototype.rejectFor = function (triggerEvent) {
+            var targetAssignment = this.getFor(triggerEvent);
+            if (targetAssignment === null) {
+                return false;
+            }
+            targetAssignment.deferreds.forEach(function (deferred) { return deferred.reject(); });
+            this.detachFor(triggerEvent);
+            return true;
+        };
+        return InteractionRequestMap;
+    }());
+    return new InteractionRequestMap();
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Event/TriggerRequest.js b/typo3/sysext/backend/Resources/Public/JavaScript/Event/TriggerRequest.js
new file mode 100644 (file)
index 0000000..28d91df
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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!
+ */
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+define(["require", "exports", "./InteractionRequest"], function (require, exports, InteractionRequest) {
+    "use strict";
+    var TriggerRequest = (function (_super) {
+        __extends(TriggerRequest, _super);
+        function TriggerRequest(type, parentRequest) {
+            if (parentRequest === void 0) { parentRequest = null; }
+            return _super.call(this, type, parentRequest) || this;
+        }
+        TriggerRequest.prototype.concerns = function (ancestorRequest) {
+            if (this === ancestorRequest) {
+                return true;
+            }
+            var request = this;
+            while (request.parentRequest instanceof InteractionRequest) {
+                request = request.parentRequest;
+                if (request === ancestorRequest) {
+                    return true;
+                }
+            }
+            return false;
+        };
+        TriggerRequest.prototype.concernsTypes = function (types) {
+            if (types.indexOf(this.type) !== -1) {
+                return true;
+            }
+            var request = this;
+            while (request.parentRequest instanceof InteractionRequest) {
+                request = request.parentRequest;
+                if (types.indexOf(request.type) !== -1) {
+                    return true;
+                }
+            }
+            return false;
+        };
+        return TriggerRequest;
+    }(InteractionRequest));
+    return TriggerRequest;
+});
index edf73ce..a88a42f 100644 (file)
@@ -33,16 +33,30 @@ var setFormValueOpenBrowser,
 define(['jquery',
                'TYPO3/CMS/Backend/FormEngineValidation',
                'TYPO3/CMS/Backend/Modal',
-               'TYPO3/CMS/Backend/Severity'
-          ], function ($, FormEngineValidation, Modal, Severity) {
+               'TYPO3/CMS/Backend/Severity',
+               'TYPO3/CMS/Backend/BackendException',
+               'TYPO3/CMS/Backend/Event/InteractionRequestMap'
+          ], function ($, FormEngineValidation, Modal, Severity, BackendException, InteractionRequestMap) {
+
+       /**
+        * @param {InteractionRequest} interactionRequest
+        * @param {boolean} response
+        */
+       function handleConsumeResponse(interactionRequest, response) {
+               if (response) {
+                       FormEngine.interactionRequestMap.resolveFor(interactionRequest);
+               } else {
+                       FormEngine.interactionRequestMap.rejectFor(interactionRequest);
+               }
+       }
 
        /**
-        *
-        * @type {{Validation: object, formName: *, openedPopupWindow: window, legacyFieldChangedCb: Function, browserUrl: string}}
         * @exports TYPO3/CMS/Backend/FormEngine
         */
        var FormEngine = {
+               consumeTypes: ['typo3.setUrl', 'typo3.beforeSetUrl', 'typo3.refresh'],
                Validation: FormEngineValidation,
+               interactionRequestMap: InteractionRequestMap,
                formName: TYPO3.settings.FormEngine.formName,
                openedPopupWindow: null,
                legacyFieldChangedCb: function() { !$.isFunction(TYPO3.settings.FormEngine.legacyFieldChangedCb) || TYPO3.settings.FormEngine.legacyFieldChangedCb(); },
@@ -67,7 +81,6 @@ define(['jquery',
                FormEngine.openedPopupWindow.focus();
        };
 
-
        /**
         * properly fills the select field from the popup window (element browser, link browser)
         * or from a multi-select (two selects side-by-side)
@@ -581,6 +594,12 @@ define(['jquery',
         * as it using deferrer methods only
         */
        FormEngine.initializeEvents = function() {
+               if (top.TYPO3 && typeof top.TYPO3.Backend !== 'undefined') {
+                       top.TYPO3.Backend.consumerScope.attach(FormEngine);
+                       $(window).on('unload', function() {
+                               top.TYPO3.Backend.consumerScope.detach(FormEngine);
+                       });
+               }
                $(document).on('click', '.t3js-btn-moveoption-top, .t3js-btn-moveoption-up, .t3js-btn-moveoption-down, .t3js-btn-moveoption-bottom, .t3js-btn-removeoption', function(evt) {
                        evt.preventDefault();
 
@@ -626,7 +645,9 @@ define(['jquery',
                        }
                }).on('click', '.t3js-editform-close', function(e) {
                        e.preventDefault();
-                       FormEngine.preventExitIfNotSaved();
+                       FormEngine.preventExitIfNotSaved(
+                               FormEngine.preventExitIfNotSavedCallback
+                       );
                }).on('click', '.t3js-editform-delete-record', function(e) {
                        e.preventDefault();
                        var title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
@@ -728,6 +749,45 @@ define(['jquery',
        };
 
        /**
+        * @param {InteractionRequest} interactionRequest
+        * @return {jQuery.Deferred}
+        */
+       FormEngine.consume = function(interactionRequest) {
+               if (!interactionRequest) {
+                       throw new BackendException('No interaction request given', 1496589980);
+               }
+               if (interactionRequest.concernsTypes(FormEngine.consumeTypes)) {
+                       var outerMostRequest = interactionRequest.outerMostRequest;
+                       var deferred = $.Deferred();
+
+                       FormEngine.interactionRequestMap.attachFor(
+                               outerMostRequest,
+                               deferred
+                       );
+                       // resolve or reject deferreds with previous user choice
+                       if (outerMostRequest.isProcessed()) {
+                               handleConsumeResponse(
+                                       outerMostRequest,
+                                       outerMostRequest.getProcessedData().response
+                               );
+                       // show confirmation dialog
+                       } else if (FormEngine.hasChange()) {
+                               FormEngine.preventExitIfNotSaved(function(response) {
+                                       outerMostRequest.setProcessedData(
+                                               {response: response}
+                                       );
+                                       handleConsumeResponse(outerMostRequest, response);
+                               });
+                       // resolve directly
+                       } else {
+                               FormEngine.interactionRequestMap.resolveFor(outerMostRequest);
+                       }
+
+                       return deferred;
+               }
+       };
+
+       /**
         * Initializes the remaining character views based on the fields' maxlength attribute
         */
        FormEngine.initializeRemainingCharacterViews = function() {
@@ -1052,10 +1112,30 @@ define(['jquery',
        };
 
        /**
-        * Show modal to confirm closing the document without saving
+        * @return {boolean}
         */
-       FormEngine.preventExitIfNotSaved = function() {
-               if ($('form[name="' + FormEngine.formName + '"] .has-change').length > 0) {
+       FormEngine.hasChange = function() {
+               return $('form[name="' + FormEngine.formName + '"] .has-change').length > 0;
+       };
+
+       /**
+        * @param {boolean} response
+        */
+       FormEngine.preventExitIfNotSavedCallback = function(response) {
+               if (response) {
+                       FormEngine.closeDocument();
+               }
+       };
+
+       /**
+        * Show modal to confirm closing the document without saving.
+        *
+        * @param {Function} callback
+        */
+       FormEngine.preventExitIfNotSaved = function(callback) {
+               callback = callback || FormEngine.preventExitIfNotSavedCallback;
+
+               if (FormEngine.hasChange()) {
                        var title = TYPO3.lang['label.confirm.close_without_save.title'] || 'Do you want to quit without saving?';
                        var content = TYPO3.lang['label.confirm.close_without_save.content'] || 'You have currently unsaved changes. Are you sure that you want to discard all changes?';
                        var $modal = Modal.confirm(title, content, Severity.warning, [
@@ -1074,13 +1154,14 @@ define(['jquery',
                        $modal.on('button.clicked', function(e) {
                                if (e.target.name === 'no') {
                                        Modal.dismiss();
+                                       callback.call(null, false);
                                } else if (e.target.name === 'yes') {
                                        Modal.dismiss();
-                                       FormEngine.closeDocument();
+                                       callback.call(null, true);
                                }
                        });
                } else {
-                       FormEngine.closeDocument();
+                       callback.call(null, true);
                }
        };
 
index 5b22615..480585f 100644 (file)
@@ -20,12 +20,15 @@ require(
                'jquery',
                'TYPO3/CMS/Backend/Storage',
                'TYPO3/CMS/Backend/Icons',
-               'TYPO3/CMS/Backend/Viewport'
+               'TYPO3/CMS/Backend/Viewport',
+               'TYPO3/CMS/Backend/Event/ClientRequest',
+               'TYPO3/CMS/Backend/Event/TriggerRequest'
        ],
-       function ($, Storage, Icons) {
+       function ($, Storage, Icons, Viewport, ClientRequest, TriggerRequest) {
                if (typeof TYPO3.ModuleMenu !== 'undefined') {
                        return TYPO3.ModuleMenu.App;
                }
+
                TYPO3.ModuleMenu = {};
                TYPO3.ModuleMenu.App = {
                        loadedModule: null,
@@ -35,42 +38,52 @@ require(
                        initialize: function () {
                                var me = this;
 
+                               var deferred = $.Deferred();
+                               deferred.resolve();
+
                                // load the start module
                                if (top.startInModule && top.startInModule[0] && $('#' + top.startInModule[0]).length > 0) {
-                                       me.showModule(top.startInModule[0], top.startInModule[1]);
+                                       deferred = me.showModule(
+                                               top.startInModule[0],
+                                               top.startInModule[1]
+                                       );
                                } else {
                                        // fetch first module
                                        if ($('.t3js-mainmodule:first').attr('id')) {
-                                               me.showModule($('.t3js-mainmodule:first').attr('id'));
+                                               deferred = me.showModule(
+                                                       $('.t3js-mainmodule:first').attr('id')
+                                               );
                                        }
                                        // else case: the main module has no entries, this is probably a backend
                                        // user with very little access rights, maybe only the logout button and
                                        // a user settings module in topbar.
                                }
 
-                               // check if module menu should be collapsed or not
-                               var state = Storage.Persistent.get('BackendComponents.States.typo3-module-menu');
-                               if (state && state.collapsed) {
-                                       TYPO3.ModuleMenu.App.toggleMenu(state.collapsed === 'true');
-                               }
-
-                               // check if there are collapsed items in the users' configuration
-                               var collapsedMainMenuItems = me.getCollapsedMainMenuItems();
-                               $.each(collapsedMainMenuItems, function (key, itm) {
-                                       if (itm !== true) {
-                                               return;
-                                       }
-                                       var $group = $('#' + key);
-                                       if ($group.length > 0) {
-                                               var $groupContainer = $group.find('.modulemenu-group-container');
-                                               $group.addClass('collapsed').removeClass('expanded');
-                                               TYPO3.Backend.NavigationContainer.cleanup();
-                                               $groupContainer.hide().promise().done(function () {
-                                                       TYPO3.Backend.doLayout();
-                                               });
+                               deferred.then(function() {
+                                       // check if module menu should be collapsed or not
+                                       var state = Storage.Persistent.get('BackendComponents.States.typo3-module-menu');
+                                       if (state && state.collapsed) {
+                                               TYPO3.ModuleMenu.App.toggleMenu(state.collapsed === 'true');
                                        }
+
+                                       // check if there are collapsed items in the users' configuration
+                                       var collapsedMainMenuItems = me.getCollapsedMainMenuItems();
+                                       $.each(collapsedMainMenuItems, function (key, itm) {
+                                               if (itm !== true) {
+                                                       return;
+                                               }
+                                               var $group = $('#' + key);
+                                               if ($group.length > 0) {
+                                                       var $groupContainer = $group.find('.modulemenu-group-container');
+                                                       $group.addClass('collapsed').removeClass('expanded');
+                                                       TYPO3.Backend.NavigationContainer.cleanup();
+                                                       $groupContainer.hide().promise().done(function () {
+                                                               TYPO3.Backend.doLayout();
+                                                       });
+                                               }
+                                       });
+                                       me.initializeEvents();
                                });
-                               me.initializeEvents();
                        },
 
                        initializeEvents: function () {
@@ -98,8 +111,11 @@ require(
                                // register clicking on sub modules
                                $(document).on('click', '.modulemenu-item,.t3-menuitem-submodule', function (evt) {
                                        evt.preventDefault();
-                                       me.showModule($(this).attr('id'));
-                                       TYPO3.Backend.doLayout();
+                                       me.showModule(
+                                               $(this).attr('id'),
+                                               null,
+                                               evt
+                                       );
                                });
                                $(document).on('click', '.t3js-topbar-button-modulemenu',
                                        function (evt) {
@@ -163,33 +179,71 @@ require(
                                };
                        },
 
-                       showModule: function (mod, params) {
+                       /**
+                        * @param {string} mod
+                        * @param {string} params
+                        * @param {Event} [event]
+                        * @return {jQuery.Deferred}
+                        */
+                       showModule: function (mod, params, event) {
                                params = params || '';
                                params = this.includeId(mod, params);
                                var record = this.getRecordFromName(mod);
-                               this.loadModuleComponents(record, params);
+                               return this.loadModuleComponents(
+                                       record,
+                                       params,
+                                       new ClientRequest('typo3.showModule', event)
+                               );
                        },
 
-                       loadModuleComponents: function (record, params) {
+                       /**
+                        * @param {object} record
+                        * @param {string} params
+                        * @param {InteractionRequest} [interactionRequest]
+                        * @return {jQuery.Deferred}
+                        */
+                       loadModuleComponents: function (record, params, interactionRequest) {
                                var mod = record.name;
-                               if (record.navigationComponentId) {
-                                       this.loadNavigationComponent(record.navigationComponentId);
-                               } else if (record.navigationFrameScript) {
-                                       TYPO3.Backend.NavigationContainer.show('typo3-navigationIframe');
-                                       this.openInNavFrame(record.navigationFrameScript, record.navigationFrameScriptParam);
-                               } else {
-                                       TYPO3.Backend.NavigationContainer.hide();
-                               }
-
-                               this.highlightModuleMenuItem(mod);
-                               this.loadedModule = mod;
-                               this.openInContentFrame(record.link, params);
-
-                               // compatibility
-                               top.currentSubScript = record.link;
-                               top.currentModuleLoaded = mod;
 
-                               TYPO3.Backend.doLayout();
+                               var deferred = TYPO3.Backend.ContentContainer.beforeSetUrl(interactionRequest);
+                               deferred.then(
+                                       $.proxy(function() {
+                                               if (record.navigationComponentId) {
+                                                       this.loadNavigationComponent(record.navigationComponentId);
+                                               } else if (record.navigationFrameScript) {
+                                                       TYPO3.Backend.NavigationContainer.show('typo3-navigationIframe');
+                                                       this.openInNavFrame(
+                                                               record.navigationFrameScript,
+                                                               record.navigationFrameScriptParam,
+                                                               new TriggerRequest(
+                                                                       'typo3.loadModuleComponents',
+                                                                       interactionRequest
+                                                               )
+                                                       );
+                                               } else {
+                                                       TYPO3.Backend.NavigationContainer.hide();
+                                               }
+
+                                               this.highlightModuleMenuItem(mod);
+                                               this.loadedModule = mod;
+                                               this.openInContentFrame(
+                                                       record.link,
+                                                       params,
+                                                       new TriggerRequest(
+                                                               'typo3.loadModuleComponents',
+                                                               interactionRequest
+                                                       )
+                                               );
+
+                                               // compatibility
+                                               top.currentSubScript = record.link;
+                                               top.currentModuleLoaded = mod;
+
+                                               TYPO3.Backend.doLayout();
+                                       }, this
+                               ));
+
+                               return deferred;
                        },
 
                        includeId: function (mod, params) {
@@ -231,23 +285,55 @@ require(
                                this.availableNavigationComponents[componentId] = initCallback;
                        },
 
-                       openInNavFrame: function (url, params) {
+                       /**
+                        * @param {string} url
+                        * @param {string} params
+                        * @param {InteractionRequest} [interactionRequest]
+                        * @return {jQuery.Deferred}
+                        */
+                       openInNavFrame: function (url, params, interactionRequest) {
                                var navUrl = url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : '');
                                var currentUrl = TYPO3.Backend.NavigationContainer.getUrl();
+                               var deferred = TYPO3.Backend.NavigationContainer.setUrl(
+                                       url,
+                                       new TriggerRequest('typo3.openInNavFrame', interactionRequest)
+                               );
                                if (currentUrl !== navUrl) {
-                                       TYPO3.Backend.NavigationContainer.refresh();
+                                       // if deferred is already resolved, execute directly
+                                       if (deferred.state() === 'resolved') {
+                                               TYPO3.Backend.NavigationContainer.refresh();
+                                       // otherwise hand in future callback
+                                       } else {
+                                               deferred.then(TYPO3.Backend.NavigationContainer.refresh);
+                                       }
                                }
-                               TYPO3.Backend.NavigationContainer.setUrl(url);
+                               return deferred;
                        },
 
-                       openInContentFrame: function (url, params) {
+                       /**
+                        * @param {string} url
+                        * @param {string} params
+                        * @param {InteractionRequest} [interactionRequest]
+                        * @return {jQuery.Deferred}
+                        */
+                       openInContentFrame: function (url, params, interactionRequest) {
+                               var deferred;
+
                                if (top.nextLoadModuleUrl) {
-                                       TYPO3.Backend.ContentContainer.setUrl(top.nextLoadModuleUrl);
+                                       deferred = TYPO3.Backend.ContentContainer.setUrl(
+                                               top.nextLoadModuleUrl,
+                                               new TriggerRequest('typo3.openInContentFrame', interactionRequest)
+                                       );
                                        top.nextLoadModuleUrl = '';
                                } else {
                                        var urlToLoad = url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : '');
-                                       TYPO3.Backend.ContentContainer.setUrl(urlToLoad);
+                                       deferred = TYPO3.Backend.ContentContainer.setUrl(
+                                               urlToLoad,
+                                               new TriggerRequest('typo3.openInContentFrame', interactionRequest)
+                                       );
                                }
+
+                               return deferred;
                        },
 
                        highlightModuleMenuItem: function (module, mainModule) {
index afb2f50..7b6aecc 100644 (file)
@@ -21,11 +21,26 @@ define(
        [
                'jquery',
                'TYPO3/CMS/Backend/Icons',
+               'TYPO3/CMS/Backend/Event/ConsumerScope',
+               'TYPO3/CMS/Backend/Event/TriggerRequest'
        ],
-       function ($, Icons) {
+       function ($, Icons, ConsumerScope, TriggerRequest) {
                'use strict';
 
+               function resolveIFrameElement() {
+                       var $iFrame = $('.t3js-scaffold-content-module-iframe:first');
+                       if ($iFrame.length === 0) {
+                               return null;
+                       }
+                       return $iFrame.get(0);
+               }
+
                TYPO3.Backend = {
+                       /**
+                        * @type {ConsumerScope}
+                        */
+                       consumerScope: ConsumerScope,
+
                        initialize: function() {
                                TYPO3.Backend.doLayout();
                                $(window).on('resize', TYPO3.Backend.doLayout);
@@ -88,15 +103,29 @@ define(
                                        $('.t3js-scaffold-content-navigation [data-component]').hide();
                                        $('.t3js-scaffold-content-navigation [data-component=' + component + ']').show();
                                },
-                               setUrl: function(urlToLoad) {
-                                       $('.t3js-scaffold').addClass('scaffold-content-navigation-expanded');
-                                       $('.t3js-scaffold-content-navigation-iframe').attr('src', urlToLoad);
+                               /**
+                                * @param {string} urlToLoad
+                                * @param {InteractionRequest} [interactionRequest]
+                                * @return {jQuery.Deferred}
+                                */
+                               setUrl: function(urlToLoad, interactionRequest) {
+                                       var deferred = TYPO3.Backend.consumerScope.invoke(
+                                               new TriggerRequest('typo3.setUrl', interactionRequest)
+                                       );
+                                       deferred.then(function() {
+                                               $('.t3js-scaffold').addClass('scaffold-content-navigation-expanded');
+                                               $('.t3js-scaffold-content-navigation-iframe').attr('src', urlToLoad);
+                                       });
+                                       return deferred;
                                },
                                getUrl: function() {
                                        return $('.t3js-scaffold-content-navigation-iframe').attr('src');
                                },
-                               refresh: function() {
-                                       $('.t3js-scaffold-content-navigation-iframe')[0].contentWindow.location.reload();
+                               /**
+                                * @param {boolean} forceGet
+                                */
+                               refresh: function(forceGet) {
+                                       $('.t3js-scaffold-content-navigation-iframe')[0].contentWindow.location.reload(forceGet);
                                },
                                calculateScrollbar: function (){
                                        TYPO3.Backend.NavigationContainer.cleanup();
@@ -125,19 +154,66 @@ define(
                                get: function() {
                                        return $('.t3js-scaffold-content-module-iframe')[0].contentWindow;
                                },
-                               setUrl: function (urlToLoad) {
-                                       TYPO3.Backend.Loader.start();
-                                       $('.t3js-scaffold-content-module-iframe')
-                                               .attr('src', urlToLoad)
-                                               .one('load', function() {
-                                                       TYPO3.Backend.Loader.finish();
-                                               });
+                               /**
+                                * @param {InteractionRequest} [interactionRequest]
+                                * @return {jQuery.Deferred}
+                                */
+                               beforeSetUrl: function(interactionRequest) {
+                                       return TYPO3.Backend.consumerScope.invoke(
+                                               new TriggerRequest('typo3.beforeSetUrl', interactionRequest)
+                                       );
+                               },
+                               /**
+                                * @param {String} urlToLoad
+                                * @param {InteractionRequest} [interactionRequest]
+                                * @return {jQuery.Deferred}
+                                */
+                               setUrl: function (urlToLoad, interactionRequest) {
+                                       var deferred;
+                                       var iFrame = resolveIFrameElement();
+                                       // abort, if no IFRAME can be found
+                                       if (iFrame === null) {
+                                               deferred = $.Deferred();
+                                               deferred.reject();
+                                               return deferred;
+                                       }
+                                       deferred = TYPO3.Backend.consumerScope.invoke(
+                                               new TriggerRequest('typo3.setUrl', interactionRequest)
+                                       );
+                                       deferred.then(function() {
+                                               TYPO3.Backend.Loader.start();
+                                               $('.t3js-scaffold-content-module-iframe')
+                                                       .attr('src', urlToLoad)
+                                                       .one('load', function() {
+                                                               TYPO3.Backend.Loader.finish();
+                                                       });
+                                       });
+                                       return deferred;
                                },
                                getUrl: function() {
                                        return $('.t3js-scaffold-content-module-iframe').attr('src');
                                },
-                               refresh: function() {
-                                       $('.t3js-scaffold-content-module-iframe')[0].contentWindow.location.reload();
+                               /**
+                                * @param {boolean} forceGet
+                                * @param {InteractionRequest} interactionRequest
+                                * @return {jQuery.Deferred}
+                                */
+                               refresh: function(forceGet, interactionRequest) {
+                                       var deferred;
+                                       var iFrame = resolveIFrameElement();
+                                       // abort, if no IFRAME can be found
+                                       if (iFrame === null) {
+                                               deferred = $.Deferred();
+                                               deferred.reject();
+                                               return deferred;
+                                       }
+                                       deferred = TYPO3.Backend.consumerScope.invoke(
+                                               new TriggerRequest('typo3.refresh', interactionRequest)
+                                       );
+                                       deferred.then(function() {
+                                               iFrame.contentWindow.location.reload(forceGet);
+                                       });
+                                       return deferred;
                                },
                                getIdFromUrl: function() {
                                        if(this.getUrl) {
index fbba6a3..3e640de 100644 (file)
@@ -313,7 +313,6 @@ TYPO3.Components.PageTree.Actions = {
         * @return {void}
         */
        editPageProperties: function(node) {
-               node.select();
                var returnUrl = TYPO3.Backend.ContentContainer.getUrl();
                if (returnUrl.indexOf('returnUrl') !== -1) {
                        returnUrl = TYPO3.Utility.getParameterFromUrl(returnUrl, 'returnUrl');
@@ -329,6 +328,8 @@ TYPO3.Components.PageTree.Actions = {
 
                TYPO3.Backend.ContentContainer.setUrl(
                        TYPO3.settings.FormEngine.moduleUrl + '&edit[pages][' + node.attributes.nodeData.id + ']=edit&returnUrl=' + returnUrl
+               ).then(
+                       node.select
                );
        },
 
@@ -339,9 +340,10 @@ TYPO3.Components.PageTree.Actions = {
         * @return {void}
         */
        newPageWizard: function(node) {
-               node.select();
                TYPO3.Backend.ContentContainer.setUrl(
                        TYPO3.settings.NewRecord.moduleUrl + '&id=' + node.attributes.nodeData.id + '&pagesOnly=1'
+               ).then(
+                       node.select
                );
        },
 
@@ -362,9 +364,10 @@ TYPO3.Components.PageTree.Actions = {
         * @return {void}
         */
        openHistoryPopUp: function(node) {
-               node.select();
                TYPO3.Backend.ContentContainer.setUrl(
                        TYPO3.settings.RecordHistory.moduleUrl + '&element=pages:' + node.attributes.nodeData.id
+               ).then(
+                       node.select
                );
        },
 
@@ -375,13 +378,14 @@ TYPO3.Components.PageTree.Actions = {
         * @return {void}
         */
        exportT3d: function(node) {
-               node.select();
                TYPO3.Backend.ContentContainer.setUrl(
                        TYPO3.settings.ImportExport.moduleUrl +
                        '&tx_impexp[action]=export&' +
                        'id=0&tx_impexp[pagetree][id]=' + node.attributes.nodeData.id +
                        '&tx_impexp[pagetree][levels]=0' +
                        '&tx_impexp[pagetree][tables][]=_ALL'
+               ).then(
+                       node.select
                );
        },
 
@@ -392,11 +396,12 @@ TYPO3.Components.PageTree.Actions = {
         * @return {void}
         */
        importT3d: function(node) {
-               node.select();
                TYPO3.Backend.ContentContainer.setUrl(
                        TYPO3.settings.ImportExport.moduleUrl +
                        '&id=' + node.attributes.nodeData.id +
                        '&table=pages&tx_impexp[action]=import'
+               ).then(
+                       node.select
                );
        },
 
@@ -709,24 +714,22 @@ TYPO3.Components.PageTree.Actions = {
         * @return {void}
         */
        singleClick: function(node, tree) {
-               tree.currentSelectedNode = node;
-
                var separator = '?';
                if (currentSubScript.indexOf('?') !== -1) {
                        separator = '&';
                }
 
-               node.select();
-               if (tree.stateHash) {
-                       tree.stateHash.lastSelectedNode = node.id;
-               }
-
-               fsMod.recentIds['web'] = node.attributes.nodeData.id;
-               fsMod.recentIds['system'] = node.attributes.nodeData.id;
-
                TYPO3.Backend.ContentContainer.setUrl(
                        currentSubScript + separator + 'id=' + node.attributes.nodeData.id
-               );
+               ).then(function() {
+                       node.select();
+                       tree.currentSelectedNode = node;
+                       if (tree.stateHash) {
+                               tree.stateHash.lastSelectedNode = node.id;
+                       }
+                       fsMod.recentIds['web'] = node.attributes.nodeData.id;
+                       fsMod.recentIds['system'] = node.attributes.nodeData.id;
+               });
        },
 
        /**
@@ -742,13 +745,14 @@ TYPO3.Components.PageTree.Actions = {
                        return;
                }
 
-               node.select();
                var nodeId = node.attributes.nodeData.id,
                        idPattern = '###ID###';
                TYPO3.Backend.ContentContainer.setUrl(
                        contextItem.customAttributes.contentUrl
                                .replace(idPattern, nodeId)
                                .replace(encodeURIComponent(idPattern), nodeId)
+               ).then(
+                       node.select
                );
        },
 
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-77268-IntroduceJavaScriptTriggerRequestAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-77268-IntroduceJavaScriptTriggerRequestAPI.rst
new file mode 100644 (file)
index 0000000..3ba0787
--- /dev/null
@@ -0,0 +1,97 @@
+.. include:: ../../Includes.txt
+
+==========================================================
+Feature: #77268 - Introduce JavaScript trigger request API
+==========================================================
+
+See :issue:`77268`
+
+Description
+===========
+
+JavaScript event handling the backend of the TYPO3 core is based on the optimistic
+assumption, that most executions can be executed sequentially and are processed
+just in time. This concept does not consider the fact that other nested components
+can defer the execution based on additional user input e.g. as used in confirmation
+dialogs.
+
+That's why a trigger request API is introduced to first inform dependent components
+about a planned action which will defer the regular execution based on specific
+application state logic of registered components. In the current implementation,
+FormEngine's edit forms register themselves to be notified, thus accidentally
+closing modified forms by clicking e.g. the module menu any other page in the
+page tree can be handled.
+
+Registering component
+~~~~~~~~~~~~~~~~~~~~~
+
+The following code attaches or detaches a particular component (a **consumer**)
+to be notified.
+
+.. code-block:: javascript
+
+       // FormEngine must implement the Consumable interface,
+        // thus having a function named consume(interactionRequest)
+       top.TYPO3.Backend.consumerScope.attach(FormEngine);
+       top.TYPO3.Backend.consumerScope.detach(FormEngine);
+
+Invoking consumers
+~~~~~~~~~~~~~~~~~~
+
+Registered consumers are invoked with a specific interaction request that has a
+defined action type and optionally additional information about the parent call
+(e.g. some client event issued by users). Invocations return a jQuery.Deferred()
+object that resolves when no consumers are registered or every consumer sends a
+resolve command as well - if only one consumer rejects, the collective invocation
+promise is rejected as well.
+
+.. code-block:: javascript
+
+       var deferred = TYPO3.Backend.consumerScope.invoke(
+               new TriggerRequest('typo3.setUrl', interactionRequest)
+       );
+       deferred
+               .then(function() { console.log('consumers are resolved'); })
+               .fail(function() { console.log('some consumer was rejected'); });
+
+Creating interaction requests
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Currently there are two types of reuqests, `ClientRequest` that is based on some
+client event (e.g. `click` event) and `TriggerRequest` which may be based on some
+parent request of type `InteractionRequest` - this is used to cascade actions.
+
+.. code-block:: javascript
+
+   var clickRequest = new ClientRequest('typo3.showModule', event);
+   var triggerRequestA = new TriggerRequest('typo3.a', clickRequest);
+   var triggerRequestB = new TriggerRequest('typo3.b', triggerRequestA);
+
+In the example `triggerRequestB` has all information from the initial click
+event down to the specific `typo3.b` action type. The first request can be
+resolved from the most specific request by `triggerRequestB.outerMostRequest`
+and will return `clickRequest` in this case.
+
+Working with interaction requests
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++ `triggerRequestB.concerns(clickRequest)` checks whether `clickRequest` is an
+  ancestor request in the cascade of `triggerRequestB` (which is true, based on
+  the previous example)
++ `triggerRequestB.concernsType('typo3.showModule')` checks whether `typo3.showModule`
+  is the type of some ancestor request in the cascade of `triggerRequestB` (which
+  is true, based on the previous example)
++ `triggerRequestB.outerMostRequest.setProcessedData({response: true})` sets the
+  property evaluated by `clickRequest.isProcessed()` to `true` and stores any
+  custom user response (e.g. from some confirmation dialog) at the outer-most
+  interaction request
+
+Impact
+======
+
+Using interaction requests requires some modifications in the JavaScript processing
+logic which changes from sequential processing to possibly deferred asynchronous
+processing. This is required since e.g. user input is required first to be able
+to continue the processing. The created promises are based on `jQuery.Deferred`.
+
+.. index:: Backend, JavaScript
index 1cf2586..799bb66 100644 (file)
@@ -109,7 +109,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][_FILE%7C' + shortMD5 + ']=' + top.rawurlencode(uid) + '&CB[setCopyMode]=1';
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };
 
@@ -118,7 +118,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][_FILE%7C' + shortMD5 + ']=0&CB[setCopyMode]=1';
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };
 
@@ -127,7 +127,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][_FILE%7C' + shortMD5 + ']=' + top.rawurlencode(uid);
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };
 
@@ -136,7 +136,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity'], func
         var url = TYPO3.settings.ajaxUrls['contextmenu_clipboard'];
         url += '&CB[el][_FILE%7C' + shortMD5 + ']=0';
         $.ajax(url).always(function () {
-            top.list_frame.location.reload(true);
+            top.TYPO3.Backend.ContentContainer.refresh(true);
         });
     };