From 72edfd5eac9609915f4a408e5e957b077aad922a Mon Sep 17 00:00:00 2001 From: Oliver Hader <oliver@typo3.org> Date: Sat, 3 Jun 2017 14:54:54 +0200 Subject: [PATCH] [FEATURE] Leaving edit by clicking in page tree does not show a warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../Private/TypeScript/BackendException.ts | 22 ++ .../Private/TypeScript/Event/ClientRequest.ts | 25 +++ .../Private/TypeScript/Event/Consumable.ts | 20 ++ .../Private/TypeScript/Event/ConsumerScope.ts | 55 +++++ .../TypeScript/Event/InteractionRequest.ts | 47 +++++ .../Event/InteractionRequestAssignment.ts | 22 ++ .../TypeScript/Event/InteractionRequestMap.ts | 76 +++++++ .../TypeScript/Event/TriggerRequest.ts | 50 +++++ .../Public/JavaScript/BackendException.js | 26 +++ .../Public/JavaScript/ContextMenuActions.js | 6 +- .../Public/JavaScript/Event/ClientRequest.js | 36 ++++ .../Public/JavaScript/Event/Consumable.js | 16 ++ .../Public/JavaScript/Event/ConsumerScope.js | 46 +++++ .../JavaScript/Event/InteractionRequest.js | 48 +++++ .../Event/InteractionRequestAssignment.js | 16 ++ .../JavaScript/Event/InteractionRequestMap.js | 63 ++++++ .../Public/JavaScript/Event/TriggerRequest.js | 60 ++++++ .../Resources/Public/JavaScript/FormEngine.js | 103 +++++++++- .../Resources/Public/JavaScript/ModuleMenu.js | 192 +++++++++++++----- .../Resources/Public/JavaScript/Viewport.js | 106 ++++++++-- .../components/pagetree/javascript/actions.js | 38 ++-- ...8-IntroduceJavaScriptTriggerRequestAPI.rst | 97 +++++++++ .../Public/JavaScript/ContextMenuActions.js | 8 +- 23 files changed, 1075 insertions(+), 103 deletions(-) create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/ClientRequest.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/Consumable.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/ConsumerScope.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequest.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestAssignment.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestMap.ts create mode 100644 typo3/sysext/backend/Resources/Private/TypeScript/Event/TriggerRequest.ts create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/BackendException.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/ClientRequest.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/Consumable.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/ConsumerScope.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequest.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestAssignment.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestMap.js create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/Event/TriggerRequest.js create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-77268-IntroduceJavaScriptTriggerRequestAPI.rst diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts b/typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts new file mode 100644 index 000000000000..384e9d0b14aa --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/BackendException.ts @@ -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 index 000000000000..2aa583e544f4 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/ClientRequest.ts @@ -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 index 000000000000..845a00c77596 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/Consumable.ts @@ -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 index 000000000000..3adfe53dd279 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/ConsumerScope.ts @@ -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 index 000000000000..e4b91971cf5e --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequest.ts @@ -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 index 000000000000..a2a1ca8ccb26 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestAssignment.ts @@ -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 index 000000000000..2334eb167c6f --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/InteractionRequestMap.ts @@ -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 index 000000000000..433c6fd12886 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/TypeScript/Event/TriggerRequest.ts @@ -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 index 000000000000..3f752b124560 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/BackendException.js @@ -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; +}); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js b/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js index 94201460216b..999d9653813c 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenuActions.js @@ -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 index 000000000000..1052a8921635 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/ClientRequest.js @@ -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 index 000000000000..fcabab445aeb --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/Consumable.js @@ -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 index 000000000000..440cab88b6b0 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/ConsumerScope.js @@ -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 index 000000000000..1b726f5353ea --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequest.js @@ -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 index 000000000000..fcabab445aeb --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestAssignment.js @@ -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 index 000000000000..9983ef9423a6 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/InteractionRequestMap.js @@ -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 index 000000000000..28d91dfb16c6 --- /dev/null +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Event/TriggerRequest.js @@ -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; +}); diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js index edf73ce6aa0c..a88a42f6b6c5 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js @@ -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?'; @@ -727,6 +748,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 */ @@ -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); } }; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ModuleMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/ModuleMenu.js index 5b226158bfd0..480585f44aa2 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/ModuleMenu.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/ModuleMenu.js @@ -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) { diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Viewport.js b/typo3/sysext/backend/Resources/Public/JavaScript/Viewport.js index afb2f50d634a..7b6aecc00fd2 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/Viewport.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/Viewport.js @@ -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) { diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/actions.js b/typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/actions.js index fbba6a3adcd4..3e640de39205 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/actions.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/extjs/components/pagetree/javascript/actions.js @@ -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 index 000000000000..3ba07876e126 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-77268-IntroduceJavaScriptTriggerRequestAPI.rst @@ -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 diff --git a/typo3/sysext/filelist/Resources/Public/JavaScript/ContextMenuActions.js b/typo3/sysext/filelist/Resources/Public/JavaScript/ContextMenuActions.js index 1cf258657ba8..799bb6615a4f 100644 --- a/typo3/sysext/filelist/Resources/Public/JavaScript/ContextMenuActions.js +++ b/typo3/sysext/filelist/Resources/Public/JavaScript/ContextMenuActions.js @@ -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); }); }; -- GitLab