AjaxDataHandler.ts 9.96 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
 * 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!
 */

14
import {BroadcastMessage} from 'TYPO3/CMS/Backend/BroadcastMessage';
15
16
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
17
import {SeverityEnum} from './Enum/Severity';
18
19
import MessageInterface from './AjaxDataHandler/MessageInterface';
import ResponseInterface from './AjaxDataHandler/ResponseInterface';
20
import $ from 'jquery';
21
import BroadcastService = require('TYPO3/CMS/Backend/BroadcastService');
22
23
24
25
26
27
28
29
import Icons = require('./Icons');
import Modal = require('./Modal');
import Notification = require('./Notification');
import Viewport = require('./Viewport');

enum Identifiers {
  hide = '.t3js-record-hide',
  delete = '.t3js-record-delete',
30
  icon = '.t3js-icon',
31
32
}

33
34
35
36
37
38
39
interface AfterProcessEventDict {
  component: string;
  action: string;
  table: string;
  uid: number;
}

40
41
/**
 * Module: TYPO3/CMS/Backend/AjaxDataHandler
42
43
 * Javascript functions to work with AJAX and interacting with Datahandler
 * through \TYPO3\CMS\Backend\Controller\SimpleDataHandlerController->processAjaxRequest (record_process route)
44
45
46
47
48
49
50
51
52
53
54
 */
class AjaxDataHandler {
  /**
   * Refresh the page tree
   */
  private static refreshPageTree(): void {
    if (Viewport.NavigationContainer && Viewport.NavigationContainer.PageTree) {
      Viewport.NavigationContainer.PageTree.refreshTree();
    }
  }

55
56
57
58
59
60
61
62
63
64
65
66
67
  /**
   * AJAX call to record_process route (SimpleDataHandlerController->processAjaxRequest)
   * returns a jQuery Promise to work with
   *
   * @param {string | object} params
   * @returns {Promise<any>}
   */
  private static call(params: string | object): Promise<ResponseInterface> {
    return (new AjaxRequest(TYPO3.settings.ajaxUrls.record_process)).withQueryArguments(params).get().then(async (response: AjaxResponse): Promise<ResponseInterface> => {
      return await response.resolve();
    });
  }

68
69
70
71
72
73
74
75
76
  constructor() {
    $((): void => {
      this.initialize();
    });
  }

  /**
   * Generic function to call from the outside the script and validate directly showing errors
   *
77
78
79
   * @param {string | object} parameters
   * @param {AfterProcessEventDict} eventDict Dictionary used as event detail. This is private API yet.
   * @returns {Promise<any>}
80
   */
81
  public process(parameters: string | object, eventDict?: AfterProcessEventDict): Promise<any> {
82
83
    const promise = AjaxDataHandler.call(parameters);
    return promise.then((result: ResponseInterface): ResponseInterface => {
84
85
86
      if (result.hasErrors) {
        this.handleErrors(result);
      }
87

88
      if (eventDict) {
89
90
91
        const payload = {...eventDict, hasErrors: result.hasErrors};
        const message = new BroadcastMessage(
          'datahandler',
92
          'process',
93
94
95
96
          payload
        );
        BroadcastService.post(message);

97
        const event = new CustomEvent('typo3:datahandler:process',{
98
99
100
          detail: {
            payload: payload
          }
101
102
103
104
        });
        document.dispatchEvent(event);
      }

105
      return result;
106
107
108
    });
  }

109
  // TODO: Many extensions rely on this behavior but it's misplaced in AjaxDataHandler. Move into Recordlist.ts and deprecate in v11.
110
111
112
113
114
115
116
117
118
119
120
121
122
  private initialize(): void {
    // HIDE/UNHIDE: click events for all action icons to hide/unhide
    $(document).on('click', Identifiers.hide, (e: JQueryEventObject): void => {
      e.preventDefault();
      const $anchorElement = $(e.currentTarget);
      const $iconElement = $anchorElement.find(Identifiers.icon);
      const $rowElement = $anchorElement.closest('tr[data-uid]');
      const params = $anchorElement.data('params');

      // add a spinner
      this._showSpinnerIcon($iconElement);

      // make the AJAX call to toggle the visibility
123
      this.process(params).then((result: ResponseInterface): void => {
124
125
126
127
128
129
130
131
132
133
134
135
136
137
        // print messages on errors
        if (result.hasErrors) {
          this.handleErrors(result);
        } else {
          // adjust overlay icon
          this.toggleRow($rowElement);
        }
      });
    });

    // DELETE: click events for all action icons to delete
    $(document).on('click', Identifiers.delete, (evt: JQueryEventObject): void => {
      evt.preventDefault();
      const $anchorElement = $(evt.currentTarget);
138
      $anchorElement.tooltip('hide');
139
140
141
142
143
      const $modal = Modal.confirm($anchorElement.data('title'), $anchorElement.data('message'), SeverityEnum.warning, [
        {
          text: $anchorElement.data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
          active: true,
          btnClass: 'btn-default',
144
          name: 'cancel',
145
146
147
148
        },
        {
          text: $anchorElement.data('button-ok-text') || TYPO3.lang['button.delete'] || 'Delete',
          btnClass: 'btn-warning',
149
150
          name: 'delete',
        },
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
      ]);
      $modal.on('button.clicked', (e: JQueryEventObject): void => {
        if (e.target.getAttribute('name') === 'cancel') {
          Modal.dismiss();
        } else if (e.target.getAttribute('name') === 'delete') {
          Modal.dismiss();
          this.deleteRecord($anchorElement);
        }
      });
    });
  }

  /**
   * Toggle row visibility after record has been changed
   *
   * @param {JQuery} $rowElement
   */
  private toggleRow($rowElement: JQuery): void {
    const $anchorElement = $rowElement.find(Identifiers.hide);
    const table = $anchorElement.closest('table[data-table]').data('table');
    const params = $anchorElement.data('params');
    let nextParams;
    let nextState;
    let iconName;

    if ($anchorElement.data('state') === 'hidden') {
      nextState = 'visible';
      nextParams = params.replace('=0', '=1');
      iconName = 'actions-edit-hide';
    } else {
      nextState = 'hidden';
      nextParams = params.replace('=1', '=0');
      iconName = 'actions-edit-unhide';
    }
    $anchorElement.data('state', nextState).data('params', nextParams);

    // Update tooltip title
    $anchorElement.tooltip('hide').one('hidden.bs.tooltip', (): void => {
      const nextTitle = $anchorElement.data('toggleTitle');
190
      // Bootstrap Tooltip internally uses only .attr('data-bs-original-title')
191
      $anchorElement
192
193
        .data('toggleTitle', $anchorElement.attr('data-bs-original-title'))
        .attr('data-bs-original-title', nextTitle);
194
195
196
    });

    const $iconElement = $anchorElement.find(Identifiers.icon);
197
    Icons.getIcon(iconName, Icons.sizes.small).then((icon: string): void => {
198
199
200
201
202
203
      $iconElement.replaceWith(icon);
    });

    // Set overlay for the record icon
    const $recordIcon = $rowElement.find('.col-icon ' + Identifiers.icon);
    if (nextState === 'hidden') {
204
      Icons.getIcon('miscellaneous-placeholder', Icons.sizes.small, 'overlay-hidden').then((icon: string): void => {
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
        $recordIcon.append($(icon).find('.icon-overlay'));
      });
    } else {
      $recordIcon.find('.icon-overlay').remove();
    }

    $rowElement.fadeTo('fast', 0.4, (): void => {
      $rowElement.fadeTo('fast', 1);
    });
    if (table === 'pages') {
      AjaxDataHandler.refreshPageTree();
    }
  }

  /**
   * Delete record by given element (icon in table)
   * don't call it directly!
   *
   * @param {JQuery} $anchorElement
   */
  private deleteRecord($anchorElement: JQuery): void {
    const params = $anchorElement.data('params');
    let $iconElement = $anchorElement.find(Identifiers.icon);

    // add a spinner
    this._showSpinnerIcon($iconElement);

232
233
234
235
236
    const $table = $anchorElement.closest('table[data-table]');
    const table = $table.data('table');
    let $rowElements = $anchorElement.closest('tr[data-uid]');
    const uid = $rowElements.data('uid');

237
    // make the AJAX call to toggle the visibility
238
    const eventData = {component: 'datahandler', action: 'delete', table, uid};
239
    this.process(params, eventData).then((result: ResponseInterface): void => {
240
      // revert to the old class
241
      Icons.getIcon('actions-edit-delete', Icons.sizes.small).then((icon: string): void => {
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
        $iconElement = $anchorElement.find(Identifiers.icon);
        $iconElement.replaceWith(icon);
      });
      // print messages on errors
      if (result.hasErrors) {
        this.handleErrors(result);
      } else {
        const $panel = $anchorElement.closest('.panel');
        const $panelHeading = $panel.find('.panel-heading');
        const $translatedRowElements = $table.find('[data-l10nparent=' + uid + ']').closest('tr[data-uid]');
        $rowElements = $rowElements.add($translatedRowElements);

        $rowElements.fadeTo('slow', 0.4, (): void => {
          $rowElements.slideUp('slow', (): void => {
            $rowElements.remove();
            if ($table.find('tbody tr').length === 0) {
              $panel.slideUp('slow');
            }
          });
        });
        if ($anchorElement.data('l10parent') === '0' || $anchorElement.data('l10parent') === '') {
          const count = Number($panelHeading.find('.t3js-table-total-items').html());
          $panelHeading.find('.t3js-table-total-items').text(count - 1);
        }

        if (table === 'pages') {
          AjaxDataHandler.refreshPageTree();
        }
      }
    });
  }

  /**
   * Handle the errors from result object
   *
   * @param {Object} result
   */
279
280
  private handleErrors(result: ResponseInterface): void {
    $.each(result.messages, (position: number, message: MessageInterface): void => {
281
282
283
284
285
286
287
288
289
290
291
      Notification.error(message.title, message.message);
    });
  }

  /**
   * Replace the given icon with a spinner icon
   *
   * @param {Object} $iconElement
   * @private
   */
  private _showSpinnerIcon($iconElement: JQuery): void {
292
    Icons.getIcon('spinner-circle-dark', Icons.sizes.small).then((icon: string): void => {
293
294
295
296
297
      $iconElement.replaceWith(icon);
    });
  }
}

298
export = new AjaxDataHandler();