Commit 0842cea9 authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Frank Nägler
Browse files

[!!!][TASK] Refactor client-side IRRE

This patch refactors the IRRE handling on client side, the JavaScript
land has been rewritten to have a better structure and do less repeating
on-the-fly calculations. Each IRRE container is represented by a
independent instance of InlineControlContainer.

Most of the internally used `scriptCall` directives have been removed.
Currently, `scriptCall` can't get removed completely, as further
refactorings in different areas are required.

All of the "external" communication via `inline.foobar()` has been
replaced by a event-driven approach. This also affects ElementBrowser
windows, those use a minimalistic API based on postMessage.

Some code that was never evaluated in ElementBrowser is considered dead
and has been removed regarding inserting multiple items.

A new sorting library has been added in order to replace jqueryui piece
by piece.

Executed command:

    yarn add --dev sortablejs

On PHP side, some code has been removed as well since the rewritten client
code is event-based and doesn't depend on external calls anymore.

Resolves: #88182
Releases: master
Change-Id: I4176483d2882cef49fbaddb5e2e1914c1f76c908
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/59324

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
parent 7c8ab513
...@@ -483,7 +483,8 @@ module.exports = function (grunt) { ...@@ -483,7 +483,8 @@ module.exports = function (grunt) {
// For the moment this is ok, because we stuck on version 1.11.4 which is very old // For the moment this is ok, because we stuck on version 1.11.4 which is very old
// the jquery ui stuff should be replaced by modern libs asap // the jquery ui stuff should be replaced by modern libs asap
// 'jquery-ui/sortable.js': 'jquery-ui/ui/sortable.js', // 'jquery-ui/sortable.js': 'jquery-ui/ui/sortable.js',
'jquery-ui/widget.js': 'jquery-ui/ui/widget.js' 'jquery-ui/widget.js': 'jquery-ui/ui/widget.js',
'Sortable.min.js': 'sortablejs/Sortable.min.js'
} }
} }
}, },
......
...@@ -205,6 +205,15 @@ div.t3-form-field-container:first-child .t3-form-field-label-flex { ...@@ -205,6 +205,15 @@ div.t3-form-field-container:first-child .t3-form-field-label-flex {
border-top: 0; border-top: 0;
} }
.form-irre-object {
transition: opacity 0.5s;
opacity: 1;
&--deleted {
opacity: 0 !important;
}
}
// //
// TCEforms Inline-Relational-Record-Editing // TCEforms Inline-Relational-Record-Editing
// //
......
...@@ -20,10 +20,7 @@ import moment = require('moment'); ...@@ -20,10 +20,7 @@ import moment = require('moment');
import NProgress = require('nprogress'); import NProgress = require('nprogress');
import Modal = require('./Modal'); import Modal = require('./Modal');
import Notification = require('./Notification'); import Notification = require('./Notification');
// Do not import TYPO3/CMS/Backend/jsfunc.inline because it is loaded in global scope import {MessageUtility} from './Utility/MessageUtility';
// Import here, will load it twice and produce JS errors.
// @TODO: Import later, after decoupling has been finished
// import 'TYPO3/CMS/Backend/jsfunc.inline';
/** /**
* Possible actions for conflicts w/ existing files * Possible actions for conflicts w/ existing files
...@@ -702,12 +699,12 @@ class DragUploader { ...@@ -702,12 +699,12 @@ class DragUploader {
* @param {UploadedFile} file * @param {UploadedFile} file
*/ */
public static addFileToIrre(irre_object: number, file: UploadedFile): void { public static addFileToIrre(irre_object: number, file: UploadedFile): void {
window.inline.delayedImportElement( const message = {
irre_object, objectGroup: irre_object,
'sys_file', table: 'sys_file',
file.uid, uid: file.uid,
'file', };
); MessageUtility.send(message);
} }
public static init(): void { public static init(): void {
......
/*
* 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 {AjaxRequest} from './AjaxRequest';
import * as $ from 'jquery';
import Notification = require('../../Notification');
export class AjaxDispatcher {
private readonly objectGroup: string = null;
constructor(objectGroup: string) {
this.objectGroup = objectGroup;
}
public newRequest(endpoint: string): AjaxRequest {
return new AjaxRequest(endpoint, this.objectGroup);
}
/**
* @param {String} routeName
*/
public getEndpoint(routeName: string): string {
if (typeof TYPO3.settings.ajaxUrls[routeName] !== 'undefined') {
return TYPO3.settings.ajaxUrls[routeName];
}
throw 'Undefined endpoint for route "' + routeName + '"';
}
public send(request: AjaxRequest): JQueryXHR {
const xhr = $.ajax(request.getEndpoint(), request.getOptions());
xhr.done((): void => {
this.processResponse(xhr);
}).fail((): void => {
Notification.error('Error ' + xhr.status, xhr.statusText);
});
return xhr;
}
private processResponse(xhr: JQueryXHR): void {
const json = xhr.responseJSON;
if (json.hasErrors) {
$.each(json.messages, (position: number, message: { [key: string]: string }): void => {
Notification.error(message.title, message.message);
});
}
// If there are elements they should be added to the <HEAD> tag (e.g. for RTEhtmlarea):
if (json.stylesheetFiles) {
$.each(json.stylesheetFiles, (index: number, stylesheetFile: string): void => {
if (!stylesheetFile) {
return;
}
const element = document.createElement('link');
element.rel = 'stylesheet';
element.type = 'text/css';
element.href = stylesheetFile;
document.querySelector('head').appendChild(element);
delete json.stylesheetFiles[index];
});
}
if (typeof json.inlineData === 'object') {
TYPO3.settings.FormEngineInline = $.extend(true, TYPO3.settings.FormEngineInline, json.inlineData);
}
if (typeof json.requireJsModules === 'object') {
for (let requireJsModule of json.requireJsModules) {
new Function(requireJsModule)();
}
}
// TODO: This is subject to be removed
if (json.scriptCall && json.scriptCall.length > 0) {
$.each(json.scriptCall, (index: number, value: string): void => {
// tslint:disable-next-line:no-eval
eval(value);
});
}
}
}
/*
* 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!
*/
interface Context {
config: Object;
hmac: string;
}
export class AjaxRequest {
private readonly endpoint: string = null;
private readonly objectGroup: string = null;
private params: string = '';
private context: Context = null;
constructor(endpoint: string, objectGroup: string) {
this.endpoint = endpoint;
this.objectGroup = objectGroup;
}
public withContext(): AjaxRequest {
let context: Context;
if (typeof TYPO3.settings.FormEngineInline.config[this.objectGroup] !== 'undefined'
&& typeof TYPO3.settings.FormEngineInline.config[this.objectGroup].context !== 'undefined'
) {
context = TYPO3.settings.FormEngineInline.config[this.objectGroup].context;
}
this.context = context;
return this;
}
public withParams(params: Array<string>): AjaxRequest {
for (let i = 0; i < params.length; i++) {
this.params += '&ajax[' + i + ']=' + encodeURIComponent(params[i]);
}
return this;
}
public getEndpoint(): string {
return this.endpoint;
}
public getOptions(): { [key: string]: string} {
let urlParams = this.params;
if (this.context) {
urlParams += '&ajax[context]=' + encodeURIComponent(JSON.stringify(this.context));
}
return {
type: 'POST',
data: urlParams,
};
}
}
/*
* 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 interface InlineResponseInterface {
data: string;
inlineData: {
config: { [key: string]: Object },
map: { [key: string]: Array<string> }
nested: { [key: string]: Array<Array<string>> },
};
scriptCall: Array<string>;
stylesheetFiles: Array<string>;
}
...@@ -17,6 +17,7 @@ import NProgress = require('nprogress'); ...@@ -17,6 +17,7 @@ import NProgress = require('nprogress');
import Modal = require('./Modal'); import Modal = require('./Modal');
import Severity = require('./Severity'); import Severity = require('./Severity');
import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility'); import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility');
import {MessageUtility} from 'TYPO3/CMS/Backend/Utility/MessageUtility';
interface Response { interface Response {
file?: number; file?: number;
...@@ -62,7 +63,12 @@ class OnlineMedia { ...@@ -62,7 +63,12 @@ class OnlineMedia {
}, },
(data: Response): void => { (data: Response): void => {
if (data.file) { if (data.file) {
window.inline.delayedImportElement(irreObjectUid, 'sys_file', data.file, 'file'); const message = {
objectGroup: irreObjectUid,
table: 'sys_file',
uid: data.file,
};
MessageUtility.send(message);
} else { } else {
const $confirm = Modal.confirm( const $confirm = Modal.confirm(
'ERROR', 'ERROR',
......
...@@ -15,6 +15,29 @@ ...@@ -15,6 +15,29 @@
* Module: TYPO3/CMS/Backend/Utility * Module: TYPO3/CMS/Backend/Utility
*/ */
class Utility { class Utility {
/**
* Splits a string by a given delimiter and trims the values
*
* @param {string} delimiter
* @param {string} string
* @return Array<string>
*/
public static trimExplode(delimiter: string, string: string): Array<string> {
return string.split(delimiter).map((item) => item.trim()).filter((item) => item !== '');
}
/**
* Splits a string by a given delimiter and converts the values to integer
*
* @param {string} delimiter
* @param {string} string
* @param {boolean} excludeZeroValues
* @return Array<number>
*/
public static intExplode(delimiter: string, string: string, excludeZeroValues: boolean = false): Array<number> {
return string.split(delimiter).map((item) => parseInt(item, 10)).filter((item) => !isNaN(item) || excludeZeroValues && item === 0);
}
/** /**
* Checks if a given number is really a number * Checks if a given number is really a number
* *
......
/*
* 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 MessageUtility {
/**
* Generates an URL for usage in postMessage
*
* @return {string}
*/
public static getUrl(): string {
const url = new URL(window.location.href);
return url.origin;
}
/**
* @param {string} receivedOrigin
*/
public static verifyOrigin(receivedOrigin: string): boolean {
const currentDomain = MessageUtility.getUrl();
return currentDomain === receivedOrigin;
}
/**
* @param {*} message
* @param {Window} windowObject
*/
public static send(message: any, windowObject: Window = window): void {
windowObject.postMessage(message, MessageUtility.getUrl());
}
}
...@@ -82,18 +82,6 @@ class File { ...@@ -82,18 +82,6 @@ class File {
} }
return result; return result;
} }
/**
* @param {Array} list
*/
public insertElementMultiple(list: Array<any>): void {
for (let i = 0, n = list.length; i < n; i++) {
if (typeof BrowseFiles.elements[list[i]] !== 'undefined') {
const element: LinkElement = BrowseFiles.elements[list[i]];
ElementBrowser.insertMultiple('sys_file', element.uid);
}
}
}
} }
class Selector { class Selector {
...@@ -128,12 +116,8 @@ class Selector { ...@@ -128,12 +116,8 @@ class Selector {
} }
}); });
if (selectedItems.length > 0) { if (selectedItems.length > 0) {
if (ElementBrowser.hasActionMultipleCode) { for (let i = 0; i < selectedItems.length; i++) {
BrowseFiles.File.insertElementMultiple(selectedItems); BrowseFiles.File.insertElement(selectedItems[i]);
} else {
for (let i = 0; i < selectedItems.length; i++) {
BrowseFiles.File.insertElement(selectedItems[i]);
}
} }
} }
ElementBrowser.focusOpenerAndClose(); ElementBrowser.focusOpenerAndClose();
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
* The TYPO3 project - inspiring people to share! * The TYPO3 project - inspiring people to share!
*/ */
import {MessageUtility} from 'TYPO3/CMS/Backend/Utility/MessageUtility';
import * as $ from 'jquery'; import * as $ from 'jquery';
import Modal = require('TYPO3/CMS/Backend/Modal'); import Modal = require('TYPO3/CMS/Backend/Modal');
...@@ -21,9 +22,6 @@ interface RTESettings { ...@@ -21,9 +22,6 @@ interface RTESettings {
interface InlineSettings { interface InlineSettings {
objectId: number; objectId: number;
checkUniqueAction: string;
addAction: string;
insertAction: string;
} }
declare global { declare global {
...@@ -45,8 +43,7 @@ declare global { ...@@ -45,8 +43,7 @@ declare global {
* ElementBrowser communication with parent windows * ElementBrowser communication with parent windows
*/ */
class ElementBrowser { class ElementBrowser {
public hasActionMultipleCode: boolean = false; private opener: Window = null;
private thisScriptUrl: string = ''; private thisScriptUrl: string = '';
private mode: string = ''; private mode: string = '';
private formFieldName: string = ''; private formFieldName: string = '';
...@@ -60,9 +57,6 @@ class ElementBrowser { ...@@ -60,9 +57,6 @@ class ElementBrowser {
}; };
private irre: InlineSettings = { private irre: InlineSettings = {
objectId: 0, objectId: 0,
checkUniqueAction: '',
addAction: '',
insertAction: '',
}; };
constructor() { constructor() {
...@@ -75,11 +69,7 @@ class ElementBrowser { ...@@ -75,11 +69,7 @@ class ElementBrowser {
this.fieldReferenceSlashed = data.fieldReferenceSlashed; this.fieldReferenceSlashed = data.fieldReferenceSlashed;
this.rte.parameters = data.rteParameters; this.rte.parameters = data.rteParameters;
this.rte.configuration = data.rteConfiguration; this.rte.configuration = data.rteConfiguration;
this.irre.checkUniqueAction = data.irreCheckUniqueAction;
this.irre.addAction = data.irreAddAction;
this.irre.insertAction = data.irreInsertAction;
this.irre.objectId = data.irreObjectId; this.irre.objectId = data.irreObjectId;
this.hasActionMultipleCode = (this.irre.objectId > 0 && this.irre.insertAction !== '');
}); });
/** /**
...@@ -129,30 +119,32 @@ class ElementBrowser { ...@@ -129,30 +119,32 @@ class ElementBrowser {
* Returns the parent document object * Returns the parent document object
*/ */
public getParent(): Window | null { public getParent(): Window | null {
let opener: Window = null; if (this.opener === null) {
if ( if (
typeof window.parent !== 'undefined' && typeof window.parent !== 'undefined' &&
typeof window.parent.document.list_frame !== 'undefined' && typeof window.parent.document.list_frame !== 'undefined' &&
window.parent.document.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null window.parent.document.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null
) { ) {
opener = window.parent.document.list_frame; this.opener = window.parent.document.list_frame;
} else if ( } else if (
typeof window.parent !== 'undefined' && typeof window.parent !== 'undefined' &&
typeof window.parent.frames.list_frame !== 'undefined' && typeof window.parent.frames.list_frame !== 'undefined' &&
window.parent.frames.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null window.parent.frames.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null
) { ) {
opener = window.parent.frames.list_frame; this.opener = window.parent.frames.list_frame;
} else if ( } else if (
typeof window.frames !== 'undefined' && typeof window.frames !== 'undefined' &&
typeof window.frames.frameElement !== 'undefined' && typeof window.frames.frameElement !== 'undefined' &&
window.frames.frameElement !== null && window.frames.frameElement !== null &&
window.frames.frameElement.classList.contains('t3js-modal-iframe') window.frames.frameElement.classList.contains('t3js-modal-iframe')
) { ) {
opener = (<HTMLFrameElement>window.frames.frameElement).contentWindow.parent; this.opener = (<HTMLFrameElement>window.frames.frameElement).contentWindow.parent;
} else if (window.opener) { } else if (window.opener) {
opener = window.opener; this.opener = window.opener;
}
} }
return opener;
return this.opener;
} }
public insertElement( public insertElement(
...@@ -166,86 +158,46 @@ class ElementBrowser { ...@@ -166,86 +158,46 @@ class ElementBrowser {
action: string, action: string,
close: boolean, close: boolean,
): boolean { ): boolean {
let performAction = true;
// Call a check function in the opener window (e.g. for uniqueness handling): // Call a check function in the opener window (e.g. for uniqueness handling):
if (this.irre.objectId && this.irre.checkUniqueAction) {