Commit a5f0b7c4 authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Tobi Kretschmann
Browse files

[TASK] Use AJAX API in FormEngine

This patch replaces the usages of `$.ajax()` and its friends with the
AJAX API provided by TYPO3 Core.

Resolves: #90038
Releases: master
Change-Id: Ief3767dd5a5256dca269b8dfb496f37a9bcdbe69
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62790

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarJörg Bösche <typo3@joergboesche.de>
Tested-by: default avatarSteffen Frese <steffenf14@gmail.com>
Tested-by: Henning Liebe's avatarHenning Liebe <h.liebe@neusta.de>
Tested-by: default avatarTobi Kretschmann <tobi@tobishome.de>
Reviewed-by: default avatarJörg Bösche <typo3@joergboesche.de>
Reviewed-by: default avatarSteffen Frese <steffenf14@gmail.com>
Reviewed-by: Henning Liebe's avatarHenning Liebe <h.liebe@neusta.de>
Reviewed-by: default avatarTobi Kretschmann <tobi@tobishome.de>
parent 525cde7e
......@@ -11,19 +11,20 @@
* The TYPO3 project - inspiring people to share!
*/
import * as $ from 'jquery';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import {MessageUtility} from '../../Utility/MessageUtility';
import {AjaxDispatcher} from './../InlineRelation/AjaxDispatcher';
import {InlineResponseInterface} from './../InlineRelation/InlineResponseInterface';
import {MessageUtility} from '../../Utility/MessageUtility';
import * as $ from 'jquery';
import NProgress = require('nprogress');
import Sortable = require('Sortable');
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import FormEngineValidation = require('TYPO3/CMS/Backend/FormEngineValidation');
import Icons = require('../../Icons');
import InfoWindow = require('../../InfoWindow');
import Modal = require('../../Modal');
import Notification = require('../../Notification');
import NProgress = require('nprogress');
import Severity = require('../../Severity');
import Sortable = require('Sortable');
import Utility = require('../../Utility');
enum Selectors {
......@@ -55,8 +56,8 @@ enum SortDirections {
UP = 'up',
}
interface XhrQueue {
[key: string]: JQueryXHR;
interface RequestQueue {
[key: string]: AjaxRequest;
}
interface ProgressQueue {
......@@ -92,7 +93,7 @@ class InlineControlContainer {
private container: HTMLElement = null;
private ajaxDispatcher: AjaxDispatcher = null;
private appearance: Appearance = null;
private xhrQueue: XhrQueue = {};
private requestQueue: RequestQueue = {};
private progessQueue: ProgressQueue = {};
private noTitleString: string = (TYPO3.lang ? TYPO3.lang['FormEngine.noRecordTitle'] : '[No title]');
......@@ -128,10 +129,7 @@ class InlineControlContainer {
*/
private static registerInfoButton(e: Event): void {
let target: HTMLElement;
if ((target = InlineControlContainer.getDelegatedEventTarget(
e.target,
Selectors.infoWindowButton)
) === null) {
if ((target = InlineControlContainer.getDelegatedEventTarget(e.target, Selectors.infoWindowButton)) === null) {
return;
}
......@@ -313,10 +311,7 @@ class InlineControlContainer {
*/
private registerSort(e: Event): void {
let target: HTMLElement;
if ((target = InlineControlContainer.getDelegatedEventTarget(
e.target,
Selectors.controlSectionSelector + ' [data-action="sort"]')
) === null) {
if ((target = InlineControlContainer.getDelegatedEventTarget(e.target, Selectors.controlSectionSelector + ' [data-action="sort"]')) === null) {
return;
}
......@@ -421,14 +416,11 @@ class InlineControlContainer {
* @param {Array} params
* @param {string} afterUid
*/
private importRecord(params: Array<any>, afterUid?: string): void {
const xhr = this.ajaxDispatcher.send(
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_create'))
.withContext()
.withParams(params),
);
xhr.done((response: { [key: string]: any }): void => {
private async importRecord(params: Array<any>, afterUid?: string): Promise<any> {
this.ajaxDispatcher.send(
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_create')),
params,
).then(async (response: InlineResponseInterface): Promise<any> => {
if (this.isBelowMax()) {
this.createRecord(
response.compilerInput.uid,
......@@ -493,10 +485,7 @@ class InlineControlContainer {
*/
private registerDeleteButton(e: Event): void {
let target: HTMLElement;
if ((target = InlineControlContainer.getDelegatedEventTarget(
e.target,
Selectors.deleteRecordButtonSelector)
) === null) {
if ((target = InlineControlContainer.getDelegatedEventTarget(e.target, Selectors.deleteRecordButtonSelector)) === null) {
return;
}
......@@ -537,13 +526,10 @@ class InlineControlContainer {
return;
}
const xhr = this.ajaxDispatcher.send(
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_synchronizelocalize'))
.withContext()
.withParams([this.container.dataset.objectGroup, target.dataset.type]),
);
xhr.done((response: { [key: string]: any }): void => {
this.ajaxDispatcher.send(
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_synchronizelocalize')),
[this.container.dataset.objectGroup, target.dataset.type],
).then(async (response: InlineResponseInterface): Promise<any> => {
document.querySelector('#' + this.container.getAttribute('id') + '_records').insertAdjacentHTML('beforeend', response.data);
const objectIdPrefix = this.container.dataset.objectGroup + Separators.structureSeparator;
......@@ -602,20 +588,18 @@ class InlineControlContainer {
*/
private loadRecordDetails(objectId: string): void {
const recordFieldsContainer = document.querySelector('#' + objectId + '_fields');
const isLoading = typeof this.xhrQueue[objectId] !== 'undefined';
const isLoading = typeof this.requestQueue[objectId] !== 'undefined';
const isLoaded = recordFieldsContainer !== null && recordFieldsContainer.innerHTML.substr(0, 16) !== '<!--notloaded-->';
if (!isLoaded) {
const progress = this.getProgress(objectId);
if (!isLoading) {
const xhr = this.ajaxDispatcher.send(
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_details'))
.withContext()
.withParams([objectId]),
);
xhr.done((response: InlineResponseInterface): void => {
delete this.xhrQueue[objectId];
const ajaxRequest = this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_details'));
const request = this.ajaxDispatcher.send(ajaxRequest, [objectId]);
request.then(async (response: InlineResponseInterface): Promise<any> => {
delete this.requestQueue[objectId];
delete this.progessQueue[objectId];
recordFieldsContainer.innerHTML = response.data;
......@@ -633,12 +617,12 @@ class InlineControlContainer {
}
});
this.xhrQueue[objectId] = xhr;
this.requestQueue[objectId] = ajaxRequest;
progress.start();
} else {
// Abort loading if collapsed again
this.xhrQueue[objectId].abort();
delete this.xhrQueue[objectId];
this.requestQueue[objectId].getAbort().abort();
delete this.requestQueue[objectId];
delete this.progessQueue[objectId];
progress.done();
}
......@@ -677,9 +661,8 @@ class InlineControlContainer {
}
this.ajaxDispatcher.send(
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_expandcollapse'))
.withContext()
.withParams([objectId, expand.join(','), collapse.join(',')]),
this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_expandcollapse')),
[objectId, expand.join(','), collapse.join(',')]
);
}
......
......@@ -12,6 +12,8 @@
*/
import * as $ from 'jquery';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
interface FieldOptions {
pageId: number;
......@@ -66,7 +68,7 @@ class SlugElement {
private $readOnlyField: JQuery = null;
private $inputField: JQuery = null;
private $hiddenField: JQuery = null;
private xhr: JQueryXHR = null;
private request: AjaxRequest = null;
private readonly fieldsToListenOn: { [key: string]: string } = {};
constructor(selector: string, options: FieldOptions) {
......@@ -158,45 +160,44 @@ class SlugElement {
} else {
input.manual = this.$inputField.val();
}
if (this.xhr !== null && this.xhr.readyState !== 4) {
this.xhr.abort();
if (this.request instanceof AjaxRequest) {
this.request.getAbort().abort();
}
this.xhr = $.post(
TYPO3.settings.ajaxUrls.record_slug_suggest,
{
values: input,
mode: mode,
tableName: this.options.tableName,
pageId: this.options.pageId,
parentPageId: this.options.parentPageId,
recordId: this.options.recordId,
language: this.options.language,
fieldName: this.options.fieldName,
command: this.options.command,
signature: this.options.signature,
},
(response: Response): void => {
if (response.hasConflicts) {
this.$fullElement.find('.t3js-form-proposal-accepted').addClass('hidden');
this.$fullElement.find('.t3js-form-proposal-different').removeClass('hidden').find('span').text(response.proposal);
} else {
this.$fullElement.find('.t3js-form-proposal-accepted').removeClass('hidden').find('span').text(response.proposal);
this.$fullElement.find('.t3js-form-proposal-different').addClass('hidden');
}
const isChanged = this.$hiddenField.val() !== response.proposal;
if (isChanged) {
this.$fullElement.find('input').trigger('change');
}
if (mode === ProposalModes.AUTO || mode === ProposalModes.RECREATE) {
this.$readOnlyField.val(response.proposal);
this.$hiddenField.val(response.proposal);
this.$inputField.val(response.proposal);
} else {
this.$hiddenField.val(response.proposal);
}
},
'json',
);
this.request = (new AjaxRequest(TYPO3.settings.ajaxUrls.record_slug_suggest));
this.request.post({
values: input,
mode: mode,
tableName: this.options.tableName,
pageId: this.options.pageId,
parentPageId: this.options.parentPageId,
recordId: this.options.recordId,
language: this.options.language,
fieldName: this.options.fieldName,
command: this.options.command,
signature: this.options.signature,
}).then(async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.hasConflicts) {
this.$fullElement.find('.t3js-form-proposal-accepted').addClass('hidden');
this.$fullElement.find('.t3js-form-proposal-different').removeClass('hidden').find('span').text(data.proposal);
} else {
this.$fullElement.find('.t3js-form-proposal-accepted').removeClass('hidden').find('span').text(data.proposal);
this.$fullElement.find('.t3js-form-proposal-different').addClass('hidden');
}
const isChanged = this.$hiddenField.val() !== data.proposal;
if (isChanged) {
this.$fullElement.find('input').trigger('change');
}
if (mode === ProposalModes.AUTO || mode === ProposalModes.RECREATE) {
this.$readOnlyField.val(data.proposal);
this.$hiddenField.val(data.proposal);
this.$inputField.val(data.proposal);
} else {
this.$hiddenField.val(data.proposal);
}
}).finally((): void => {
this.request = null;
});
}
/**
......
......@@ -11,10 +11,16 @@
* The TYPO3 project - inspiring people to share!
*/
import {AjaxRequest} from './AjaxRequest';
import * as $ from 'jquery';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import Notification = require('../../Notification');
interface Context {
config: Object;
hmac: string;
}
export class AjaxDispatcher {
private readonly objectGroup: string = null;
......@@ -23,7 +29,7 @@ export class AjaxDispatcher {
}
public newRequest(endpoint: string): AjaxRequest {
return new AjaxRequest(endpoint, this.objectGroup);
return new AjaxRequest(endpoint);
}
/**
......@@ -37,21 +43,41 @@ export class AjaxDispatcher {
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);
public send(request: AjaxRequest, params: Array<string>): Promise<any> {
const sentRequest = request.post(this.createRequestBody(params)).then(async (response: AjaxResponse): Promise<any> => {
return this.processResponse(await response.resolve());
});
sentRequest.catch((reason: Error): void => {
Notification.error('Error ' + reason.message);
});
return xhr;
return sentRequest;
}
private createRequestBody(input: Array<string>): { [key: string]: string } {
const body: { [key: string]: string } = {};
for (let i = 0; i < input.length; i++) {
body['ajax[' + i + ']'] = input[i];
}
body['ajax[context]'] = JSON.stringify(this.getContext());
return body;
}
private processResponse(xhr: JQueryXHR): void {
const json = xhr.responseJSON;
private getContext(): Context {
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;
}
return context;
}
private processResponse(json: { [key: string]: any }): { [key: string]: any } {
if (json.hasErrors) {
$.each(json.messages, (position: number, message: { [key: string]: string }): void => {
Notification.error(message.title, message.message);
......@@ -90,5 +116,7 @@ export class AjaxDispatcher {
eval(value);
});
}
return json;
}
}
/*
* 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,
};
}
}
......@@ -13,11 +13,28 @@
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>;
inlineData?: InlineData;
scriptCall?: Array<string>;
stylesheetFiles?: Array<string>;
compilerInput?: CompilerInput,
}
interface InlineData {
config: { [key: string]: Object };
map: { [key: string]: Array<string> };
nested: { [key: string]: Array<Array<string>> };
}
interface CompilerInput {
uid: string;
childChildUid: string;
parentConfig: { [key: string]: any };
delete?: Array<string>;
localize?: Array<LocalizeItem>;
}
interface LocalizeItem {
uid: string;
selectedValue: string;
remove?: number;
}
......@@ -23,7 +23,8 @@
import * as $ from 'jquery';
import 'jquery-ui/sortable';
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import {FlexFormElementOptions} from './FormEngine/FlexForm/FlexFormElementOptions';
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import Modal = require('TYPO3/CMS/Backend/Modal');
......@@ -89,7 +90,7 @@ class FlexFormElement {
this.initializeEvents();
// generate the preview text if a section is hidden on load
this.$el.find(this.opts.sectionSelector).each(function(this: HTMLElement): void {
this.$el.find(this.opts.sectionSelector).each(function (this: HTMLElement): void {
that.generateSectionPreview($(this));
});
......@@ -133,7 +134,7 @@ class FlexFormElement {
evt.preventDefault();
const $sectionEl = $(evt.currentTarget).closest(this.opts.sectionSelector);
this.toggleSection($sectionEl);
}).on('click', this.opts.sectionToggleButtonSelector + ' .form-irre-header-control', function(evt: Event): void {
}).on('click', this.opts.sectionToggleButtonSelector + ' .form-irre-header-control', function (evt: Event): void {
evt.stopPropagation();
});
}
......@@ -161,7 +162,7 @@ class FlexFormElement {
private setActionStatus(): void {
const that = this;
// Traverse and find how many sections are open or closed, and save the value accordingly
this.$el.find(this.opts.sectionActionInputFieldSelector).each(function(this: HTMLElement, index: number): void {
this.$el.find(this.opts.sectionActionInputFieldSelector).each(function (this: HTMLElement, index: number): void {
const actionValue = ($(this).parents(that.opts.sectionSelector).hasClass(that.opts.sectionDeletedClass) ? 'DELETE' : index);
$(this).val(actionValue);
});
......@@ -200,7 +201,7 @@ class FlexFormElement {
let previewContent = '';
if (!$contentEl.is(':visible')) {
$contentEl.find('input[type=text], textarea').each(function(this: HTMLElement): void {
$contentEl.find('input[type=text], textarea').each(function (this: HTMLElement): void {
let content = $($.parseHTML($(this).val())).text();
if (content.length > 50) {
content = content.substring(0, 50) + '...';
......@@ -220,62 +221,56 @@ class FlexFormElement {
}
// register the flex functions as jQuery Plugin
$.fn.t3FormEngineFlexFormElement = function(options: FlexFormElementOptions): JQuery {
$.fn.t3FormEngineFlexFormElement = function (options: FlexFormElementOptions): JQuery {
// apply all util functions to ourself (for use in templates, etc.)
return this.each(function(this: HTMLElement): void {
return this.each(function (this: HTMLElement): void {
new FlexFormElement(this, options);
});
};
// Initialization Code
$(function(): void {
$(function (): void {
// run the flexform functions on all containers (which contains one or more sections)
$('.t3-flex-container').t3FormEngineFlexFormElement();
// Add handler to fetch container data on click on "add container" buttons
$(document).on('click', '.t3js-flex-container-add', function(this: HTMLElement, e: Event): void {
$(document).on('click', '.t3js-flex-container-add', function (this: HTMLElement, e: Event): void {
const me = $(this);
e.preventDefault();
$.ajax({
url: TYPO3.settings.ajaxUrls.record_flex_container_add,
type: 'POST',
cache: false,
data: {
vanillaUid: me.data('vanillauid'),
databaseRowUid: me.data('databaserowuid'),
command: me.data('command'