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) {
// 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
// '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 {
border-top: 0;
}
.form-irre-object {
transition: opacity 0.5s;
opacity: 1;
&--deleted {
opacity: 0 !important;
}
}
//
// TCEforms Inline-Relational-Record-Editing
//
......
......@@ -20,10 +20,7 @@ import moment = require('moment');
import NProgress = require('nprogress');
import Modal = require('./Modal');
import Notification = require('./Notification');
// Do not import TYPO3/CMS/Backend/jsfunc.inline because it is loaded in global scope
// Import here, will load it twice and produce JS errors.
// @TODO: Import later, after decoupling has been finished
// import 'TYPO3/CMS/Backend/jsfunc.inline';
import {MessageUtility} from './Utility/MessageUtility';
/**
* Possible actions for conflicts w/ existing files
......@@ -702,12 +699,12 @@ class DragUploader {
* @param {UploadedFile} file
*/
public static addFileToIrre(irre_object: number, file: UploadedFile): void {
window.inline.delayedImportElement(
irre_object,
'sys_file',
file.uid,
'file',
);
const message = {
objectGroup: irre_object,
table: 'sys_file',
uid: file.uid,
};
MessageUtility.send(message);
}
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');
import Modal = require('./Modal');
import Severity = require('./Severity');
import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility');
import {MessageUtility} from 'TYPO3/CMS/Backend/Utility/MessageUtility';
interface Response {
file?: number;
......@@ -62,7 +63,12 @@ class OnlineMedia {
},
(data: Response): void => {
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 {
const $confirm = Modal.confirm(
'ERROR',
......
......@@ -15,6 +15,29 @@
* Module: TYPO3/CMS/Backend/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
*
......
/*
* 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 {
}
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 {
......@@ -128,12 +116,8 @@ class Selector {
}
});
if (selectedItems.length > 0) {
if (ElementBrowser.hasActionMultipleCode) {
BrowseFiles.File.insertElementMultiple(selectedItems);
} else {
for (let i = 0; i < selectedItems.length; i++) {
BrowseFiles.File.insertElement(selectedItems[i]);
}
for (let i = 0; i < selectedItems.length; i++) {
BrowseFiles.File.insertElement(selectedItems[i]);
}
}
ElementBrowser.focusOpenerAndClose();
......
......@@ -11,6 +11,7 @@
* The TYPO3 project - inspiring people to share!
*/
import {MessageUtility} from 'TYPO3/CMS/Backend/Utility/MessageUtility';
import * as $ from 'jquery';
import Modal = require('TYPO3/CMS/Backend/Modal');
......@@ -21,9 +22,6 @@ interface RTESettings {
interface InlineSettings {
objectId: number;
checkUniqueAction: string;
addAction: string;
insertAction: string;
}
declare global {
......@@ -45,8 +43,7 @@ declare global {
* ElementBrowser communication with parent windows
*/
class ElementBrowser {
public hasActionMultipleCode: boolean = false;
private opener: Window = null;
private thisScriptUrl: string = '';
private mode: string = '';
private formFieldName: string = '';
......@@ -60,9 +57,6 @@ class ElementBrowser {
};
private irre: InlineSettings = {
objectId: 0,
checkUniqueAction: '',
addAction: '',
insertAction: '',
};
constructor() {
......@@ -75,11 +69,7 @@ class ElementBrowser {
this.fieldReferenceSlashed = data.fieldReferenceSlashed;
this.rte.parameters = data.rteParameters;
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.hasActionMultipleCode = (this.irre.objectId > 0 && this.irre.insertAction !== '');
});
/**
......@@ -129,30 +119,32 @@ class ElementBrowser {
* Returns the parent document object
*/
public getParent(): Window | null {
let opener: Window = null;
if (
typeof window.parent !== 'undefined' &&
typeof window.parent.document.list_frame !== 'undefined' &&
window.parent.document.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null
) {
opener = window.parent.document.list_frame;
} else if (
typeof window.parent !== 'undefined' &&
typeof window.parent.frames.list_frame !== 'undefined' &&
window.parent.frames.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null
) {
opener = window.parent.frames.list_frame;
} else if (
typeof window.frames !== 'undefined' &&
typeof window.frames.frameElement !== 'undefined' &&
window.frames.frameElement !== null &&
window.frames.frameElement.classList.contains('t3js-modal-iframe')
) {
opener = (<HTMLFrameElement>window.frames.frameElement).contentWindow.parent;
} else if (window.opener) {
opener = window.opener;
if (this.opener === null) {
if (
typeof window.parent !== 'undefined' &&
typeof window.parent.document.list_frame !== 'undefined' &&
window.parent.document.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null
) {
this.opener = window.parent.document.list_frame;
} else if (
typeof window.parent !== 'undefined' &&
typeof window.parent.frames.list_frame !== 'undefined' &&
window.parent.frames.list_frame.parent.document.querySelector('.t3js-modal-iframe') !== null
) {
this.opener = window.parent.frames.list_frame;
} else if (
typeof window.frames !== 'undefined' &&
typeof window.frames.frameElement !== 'undefined' &&
window.frames.frameElement !== null &&
window.frames.frameElement.classList.contains('t3js-modal-iframe')
) {
this.opener = (<HTMLFrameElement>window.frames.frameElement).contentWindow.parent;
} else if (window.opener) {
this.opener = window.opener;
}
}
return opener;
return this.opener;
}
public insertElement(
......@@ -166,86 +158,46 @@ class ElementBrowser {
action: string,
close: boolean,
): boolean {
let performAction = true;
// Call a check function in the opener window (e.g. for uniqueness handling):
if (this.irre.objectId && this.irre.checkUniqueAction) {
if (this.irre.objectId) {
if (this.getParent()) {
const res = this.executeFunctionByName(this.irre.checkUniqueAction, this.getParent(), this.irre.objectId, table, uid, type);
if (!res.passed) {
if (res.message) {
alert(res.message);
}
performAction = false;
}
const message = {
objectGroup: this.irre.objectId,
table: table,
uid: uid,
};
MessageUtility.send(message, this.getParent());
} else {
alert('Error - reference to main window is not set properly!');
this.focusOpenerAndClose();
}
}
// Call performing function and finish this action:
if (performAction) {
// Call helper function to manage data in the opener window:
if (this.irre.objectId && this.irre.addAction) {
if (this.getParent()) {
this.executeFunctionByName(
this.irre.addAction, this.getParent(), this.irre.objectId,
table, uid, type, this.fieldReferenceSlashed,
);
} else {
alert('Error - reference to main window is not set properly!');
this.focusOpenerAndClose();
}
}
if (this.irre.objectId && this.irre.insertAction) {
if (this.getParent()) {
this.executeFunctionByName(this.irre.insertAction, this.getParent(), this.irre.objectId, table, uid, type);
if (close) {
this.focusOpenerAndClose();
}
} else {
alert('Error - reference to main window is not set properly!');
if (close) {
this.focusOpenerAndClose();
}
}
} else if (this.fieldReference && !this.rte.parameters && !this.rte.configuration) {
this.addElement(filename, table + '_' + uid, fp, close);
} else {
if (
this.getParent() && this.getParent().content && this.getParent().content.document.editform
&& this.getParent().content.document.editform[this.formFieldName]
) {
this.getParent().group_change(
'add',
this.fieldReference,
this.rte.parameters,
this.rte.configuration,
this.targetDoc.editform[this.formFieldName],
this.getParent().content.document,
);
} else {
alert('Error - reference to main window is not set properly!');
}
if (close) {
this.focusOpenerAndClose();
}
if (close) {
this.focusOpenerAndClose();
}