[BUGFIX] Remove tooltip over delete button when it's clicked
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Private / TypeScript / AjaxDataHandler.ts
1 /*
2  * This file is part of the TYPO3 CMS project.
3  *
4  * It is free software; you can redistribute it and/or modify it under
5  * the terms of the GNU General Public License, either version 2
6  * of the License, or any later version.
7  *
8  * For the full copyright and license information, please read the
9  * LICENSE.txt file that was distributed with this source code.
10  *
11  * The TYPO3 project - inspiring people to share!
12  */
13
14 import {SeverityEnum} from './Enum/Severity';
15 import MessageInterface from './AjaxDataHandler/MessageInterface';
16 import ResponseInterface from './AjaxDataHandler/ResponseInterface';
17 import * as $ from 'jquery';
18 import Icons = require('./Icons');
19 import Modal = require('./Modal');
20 import Notification = require('./Notification');
21 import Viewport = require('./Viewport');
22
23 enum Identifiers {
24   hide = '.t3js-record-hide',
25   delete = '.t3js-record-delete',
26   icon = '.t3js-icon'
27 }
28
29 /**
30  * Module: TYPO3/CMS/Backend/AjaxDataHandler
31  * Javascript functions to work with AJAX and interacting with Datahandler
32  * through \TYPO3\CMS\Backend\Controller\SimpleDataHandlerController->processAjaxRequest (record_process route)
33  */
34 class AjaxDataHandler {
35   /**
36    * Refresh the page tree
37    */
38   private static refreshPageTree(): void {
39     if (Viewport.NavigationContainer && Viewport.NavigationContainer.PageTree) {
40       Viewport.NavigationContainer.PageTree.refreshTree();
41     }
42   }
43
44   constructor() {
45     $((): void => {
46       this.initialize();
47     });
48   }
49
50   /**
51    * Generic function to call from the outside the script and validate directly showing errors
52    *
53    * @param {Object} parameters
54    * @returns {JQueryPromise<any>}
55    */
56   public process(parameters: Object): JQueryPromise<any> {
57     return this._call(parameters).done((result: ResponseInterface): void => {
58       if (result.hasErrors) {
59         this.handleErrors(result);
60       }
61     });
62   }
63
64   private initialize(): void {
65     // HIDE/UNHIDE: click events for all action icons to hide/unhide
66     $(document).on('click', Identifiers.hide, (e: JQueryEventObject): void => {
67       e.preventDefault();
68       const $anchorElement = $(e.currentTarget);
69       const $iconElement = $anchorElement.find(Identifiers.icon);
70       const $rowElement = $anchorElement.closest('tr[data-uid]');
71       const params = $anchorElement.data('params');
72
73       // add a spinner
74       this._showSpinnerIcon($iconElement);
75
76       // make the AJAX call to toggle the visibility
77       this._call(params).done((result: ResponseInterface): void => {
78         // print messages on errors
79         if (result.hasErrors) {
80           this.handleErrors(result);
81         } else {
82           // adjust overlay icon
83           this.toggleRow($rowElement);
84         }
85       });
86     });
87
88     // DELETE: click events for all action icons to delete
89     $(document).on('click', Identifiers.delete, (evt: JQueryEventObject): void => {
90       evt.preventDefault();
91       const $anchorElement = $(evt.currentTarget);
92       $anchorElement.tooltip('hide');
93       const $modal = Modal.confirm($anchorElement.data('title'), $anchorElement.data('message'), SeverityEnum.warning, [
94         {
95           text: $anchorElement.data('button-close-text') || TYPO3.lang['button.cancel'] || 'Cancel',
96           active: true,
97           btnClass: 'btn-default',
98           name: 'cancel'
99         },
100         {
101           text: $anchorElement.data('button-ok-text') || TYPO3.lang['button.delete'] || 'Delete',
102           btnClass: 'btn-warning',
103           name: 'delete'
104         }
105       ]);
106       $modal.on('button.clicked', (e: JQueryEventObject): void => {
107         if (e.target.getAttribute('name') === 'cancel') {
108           Modal.dismiss();
109         } else if (e.target.getAttribute('name') === 'delete') {
110           Modal.dismiss();
111           this.deleteRecord($anchorElement);
112         }
113       });
114     });
115   }
116
117   /**
118    * Toggle row visibility after record has been changed
119    *
120    * @param {JQuery} $rowElement
121    */
122   private toggleRow($rowElement: JQuery): void {
123     const $anchorElement = $rowElement.find(Identifiers.hide);
124     const table = $anchorElement.closest('table[data-table]').data('table');
125     const params = $anchorElement.data('params');
126     let nextParams;
127     let nextState;
128     let iconName;
129
130     if ($anchorElement.data('state') === 'hidden') {
131       nextState = 'visible';
132       nextParams = params.replace('=0', '=1');
133       iconName = 'actions-edit-hide';
134     } else {
135       nextState = 'hidden';
136       nextParams = params.replace('=1', '=0');
137       iconName = 'actions-edit-unhide';
138     }
139     $anchorElement.data('state', nextState).data('params', nextParams);
140
141     // Update tooltip title
142     $anchorElement.tooltip('hide').one('hidden.bs.tooltip', (): void => {
143       const nextTitle = $anchorElement.data('toggleTitle');
144       // Bootstrap Tooltip internally uses only .attr('data-original-title')
145       $anchorElement
146         .data('toggleTitle', $anchorElement.attr('data-original-title'))
147         .attr('data-original-title', nextTitle)
148         .tooltip('show');
149     });
150
151     const $iconElement = $anchorElement.find(Identifiers.icon);
152     Icons.getIcon(iconName, Icons.sizes.small).done((icon: string): void => {
153       $iconElement.replaceWith(icon);
154     });
155
156     // Set overlay for the record icon
157     const $recordIcon = $rowElement.find('.col-icon ' + Identifiers.icon);
158     if (nextState === 'hidden') {
159       Icons.getIcon('miscellaneous-placeholder', Icons.sizes.small, 'overlay-hidden').done((icon: string): void => {
160         $recordIcon.append($(icon).find('.icon-overlay'));
161       });
162     } else {
163       $recordIcon.find('.icon-overlay').remove();
164     }
165
166     $rowElement.fadeTo('fast', 0.4, (): void => {
167       $rowElement.fadeTo('fast', 1);
168     });
169     if (table === 'pages') {
170       AjaxDataHandler.refreshPageTree();
171     }
172   }
173
174   /**
175    * Delete record by given element (icon in table)
176    * don't call it directly!
177    *
178    * @param {JQuery} $anchorElement
179    */
180   private deleteRecord($anchorElement: JQuery): void {
181     const params = $anchorElement.data('params');
182     let $iconElement = $anchorElement.find(Identifiers.icon);
183
184     // add a spinner
185     this._showSpinnerIcon($iconElement);
186
187     // make the AJAX call to toggle the visibility
188     this._call(params).done((result: ResponseInterface): void => {
189       // revert to the old class
190       Icons.getIcon('actions-edit-delete', Icons.sizes.small).done((icon: string): void => {
191         $iconElement = $anchorElement.find(Identifiers.icon);
192         $iconElement.replaceWith(icon);
193       });
194       // print messages on errors
195       if (result.hasErrors) {
196         this.handleErrors(result);
197       } else {
198         const $table = $anchorElement.closest('table[data-table]');
199         const $panel = $anchorElement.closest('.panel');
200         const $panelHeading = $panel.find('.panel-heading');
201         const table = $table.data('table');
202         let $rowElements = $anchorElement.closest('tr[data-uid]');
203         const uid = $rowElements.data('uid');
204         const $translatedRowElements = $table.find('[data-l10nparent=' + uid + ']').closest('tr[data-uid]');
205         $rowElements = $rowElements.add($translatedRowElements);
206
207         $rowElements.fadeTo('slow', 0.4, (): void => {
208           $rowElements.slideUp('slow', (): void => {
209             $rowElements.remove();
210             if ($table.find('tbody tr').length === 0) {
211               $panel.slideUp('slow');
212             }
213           });
214         });
215         if ($anchorElement.data('l10parent') === '0' || $anchorElement.data('l10parent') === '') {
216           const count = Number($panelHeading.find('.t3js-table-total-items').html());
217           $panelHeading.find('.t3js-table-total-items').text(count - 1);
218         }
219
220         if (table === 'pages') {
221           AjaxDataHandler.refreshPageTree();
222         }
223       }
224     });
225   }
226
227   /**
228    * Handle the errors from result object
229    *
230    * @param {Object} result
231    */
232   private handleErrors(result: ResponseInterface): void {
233     $.each(result.messages, (position: number, message: MessageInterface): void => {
234       Notification.error(message.title, message.message);
235     });
236   }
237
238   /**
239    * AJAX call to record_process route (SimpleDataHandlerController->processAjaxRequest)
240    * returns a jQuery Promise to work with
241    *
242    * @param {Object} params
243    * @returns {JQueryXHR}
244    */
245   private _call(params: Object): JQueryXHR {
246     return $.getJSON(TYPO3.settings.ajaxUrls.record_process, params);
247   }
248
249   /**
250    * Replace the given icon with a spinner icon
251    *
252    * @param {Object} $iconElement
253    * @private
254    */
255   private _showSpinnerIcon($iconElement: JQuery): void {
256     Icons.getIcon('spinner-circle-dark', Icons.sizes.small).done((icon: string): void => {
257       $iconElement.replaceWith(icon);
258     });
259   }
260 }
261
262 export = new AjaxDataHandler();