1128c58e5562b85d63973ad7f53b1bf3c5e2618d
[Packages/TYPO3.CMS.git] / typo3 / sysext / recycler / Resources / Public / JavaScript / Recycler.js
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 /**
15 * Module: TYPO3/CMS/Recycler/Recycler
16 * RequireJS module for Recycler
17 */
18 define(['jquery',
19 'nprogress',
20 'TYPO3/CMS/Backend/Modal',
21 'TYPO3/CMS/Backend/Notification',
22 'TYPO3/CMS/Backend/Severity',
23 'TYPO3/CMS/Backend/jquery.clearable'
24 ], function($, NProgress, Modal, Notification, Severity) {
25 'use strict';
26
27 /**
28 *
29 * @type {{identifiers: {searchForm: string, searchText: string, searchSubmitBtn: string, depthSelector: string, tableSelector: string, recyclerTable: string, paginator: string, reloadAction: string, massUndo: string, massDelete: string, toggleAll: string}, elements: {}, paging: {currentPage: number, totalPages: number, totalItems: number, itemsPerPage: number}, markedRecordsForMassAction: Array, allToggled: boolean}}
30 * @exports TYPO3/CMS/Recycler/Recycler
31 */
32 var Recycler = {
33 identifiers: {
34 searchForm: '#recycler-form',
35 searchText: '#recycler-form [name=search-text]',
36 searchSubmitBtn: '#recycler-form button[type=submit]',
37 depthSelector: '#recycler-form [name=depth]',
38 tableSelector: '#recycler-form [name=pages]',
39 recyclerTable: '#itemsInRecycler',
40 paginator: '#recycler-index nav',
41 reloadAction: 'a[data-action=reload]',
42 massUndo: 'button[data-action=massundo]',
43 massDelete: 'button[data-action=massdelete]',
44 toggleAll: '.t3js-toggle-all'
45 },
46 elements: {}, // filled in getElements()
47 paging: {
48 currentPage: 1,
49 totalPages: 1,
50 totalItems: 0,
51 itemsPerPage: TYPO3.settings.Recycler.pagingSize
52 },
53 markedRecordsForMassAction: [],
54 allToggled: false
55 };
56
57 /**
58 * Gets required elements
59 */
60 Recycler.getElements = function() {
61 Recycler.elements = {
62 $searchForm: $(Recycler.identifiers.searchForm),
63 $searchTextField: $(Recycler.identifiers.searchText),
64 $searchSubmitBtn: $(Recycler.identifiers.searchSubmitBtn),
65 $depthSelector: $(Recycler.identifiers.depthSelector),
66 $tableSelector: $(Recycler.identifiers.tableSelector),
67 $recyclerTable: $(Recycler.identifiers.recyclerTable),
68 $tableBody: $(Recycler.identifiers.recyclerTable).find('tbody'),
69 $paginator: $(Recycler.identifiers.paginator),
70 $reloadAction: $(Recycler.identifiers.reloadAction),
71 $massUndo: $(Recycler.identifiers.massUndo),
72 $massDelete: $(Recycler.identifiers.massDelete),
73 $toggleAll: $(Recycler.identifiers.toggleAll)
74 };
75 };
76
77 /**
78 * Register events
79 */
80 Recycler.registerEvents = function() {
81 // submitting the form
82 Recycler.elements.$searchForm.on('submit', function(e) {
83 e.preventDefault();
84 if (Recycler.elements.$searchTextField.val() !== '') {
85 Recycler.loadDeletedElements();
86 }
87 });
88
89 // changing the search field
90 Recycler.elements.$searchTextField.on('keyup', function() {
91 var $me = $(this);
92
93 if ($me.val() !== '') {
94 Recycler.elements.$searchSubmitBtn.removeClass('disabled');
95 } else {
96 Recycler.elements.$searchSubmitBtn.addClass('disabled');
97 Recycler.loadDeletedElements();
98 }
99 }).clearable(
100 {
101 onClear: function() {
102 Recycler.elements.$searchSubmitBtn.addClass('disabled');
103 Recycler.loadDeletedElements();
104 }
105 }
106 );
107
108 // changing "depth"
109 Recycler.elements.$depthSelector.on('change', function() {
110 $.when(Recycler.loadAvailableTables()).done(function() {
111 Recycler.loadDeletedElements();
112 });
113 });
114
115 // changing "table"
116 Recycler.elements.$tableSelector.on('change', function() {
117 Recycler.paging.currentPage = 1;
118 Recycler.loadDeletedElements();
119 });
120
121 // clicking "recover" in single row
122 Recycler.elements.$recyclerTable.on('click', '[data-action=undo]', Recycler.undoRecord);
123
124 // clicking "delete" in single row
125 Recycler.elements.$recyclerTable.on('click', '[data-action=delete]', Recycler.deleteRecord);
126
127 Recycler.elements.$reloadAction.on('click', function(e) {
128 e.preventDefault();
129 $.when(Recycler.loadAvailableTables()).done(function() {
130 Recycler.loadDeletedElements();
131 });
132 });
133
134 // clicking an action in the paginator
135 Recycler.elements.$paginator.on('click', 'a[data-action]', function(e) {
136 e.preventDefault();
137
138 var $el = $(this),
139 reload = false;
140
141 switch ($el.data('action')) {
142 case 'previous':
143 if (Recycler.paging.currentPage > 1) {
144 Recycler.paging.currentPage--;
145 reload = true;
146 }
147 break;
148 case 'next':
149 if (Recycler.paging.currentPage < Recycler.paging.totalPages) {
150 Recycler.paging.currentPage++;
151 reload = true;
152 }
153 break;
154 case 'page':
155 Recycler.paging.currentPage = parseInt($el.find('span').text());
156 reload = true;
157 break;
158 }
159
160 if (reload) {
161 Recycler.loadDeletedElements();
162 }
163 });
164
165 if (!TYPO3.settings.Recycler.deleteDisable) {
166 Recycler.elements.$massDelete.show();
167 } else {
168 Recycler.elements.$massDelete.remove();
169 }
170
171 Recycler.elements.$recyclerTable.on('show.bs.collapse hide.bs.collapse', 'tr.collapse', function(e) {
172 var $trigger = $(e.currentTarget).prev('tr').find('[data-action=expand]'),
173 $iconEl = $trigger.find('.t3-icon'),
174 removeClass,
175 addClass;
176
177 switch (e.type) {
178 case 'show':
179 removeClass = 't3-icon-pagetree-collapse';
180 addClass = 't3-icon-pagetree-expand';
181 break;
182 case 'hide':
183 removeClass = 't3-icon-pagetree-expand';
184 addClass = 't3-icon-pagetree-collapse';
185 break;
186 }
187
188 $iconEl.removeClass(removeClass).addClass(addClass);
189 });
190
191 // checkboxes in the table
192 Recycler.elements.$toggleAll.on('click', function() {
193 Recycler.allToggled = !Recycler.allToggled;
194 $('input[type="checkbox"]').prop('checked', Recycler.allToggled).trigger('change');
195 });
196 Recycler.elements.$recyclerTable.on('change', 'tr input[type=checkbox]', Recycler.handleCheckboxSelects);
197
198 Recycler.elements.$massUndo.on('click', Recycler.undoRecord);
199 Recycler.elements.$massDelete.on('click', Recycler.deleteRecord);
200 };
201
202 /**
203 * Initialize the recycler module
204 */
205 Recycler.initialize = function() {
206 NProgress.configure({parent: '.module-loading-indicator', showSpinner: false});
207
208 Recycler.getElements();
209 Recycler.registerEvents();
210
211 if (TYPO3.settings.Recycler.depthSelection > 0) {
212 Recycler.elements.$depthSelector.val(TYPO3.settings.Recycler.depthSelection).trigger('change');
213 } else {
214 $.when(Recycler.loadAvailableTables()).done(function() {
215 Recycler.loadDeletedElements();
216 });
217 }
218 };
219
220 /**
221 * Handles the clicks on checkboxes in the records table
222 */
223 Recycler.handleCheckboxSelects = function() {
224 var $checkbox = $(this),
225 $tr = $checkbox.parents('tr'),
226 table = $tr.data('table'),
227 uid = $tr.data('uid'),
228 record = table + ':' + uid;
229
230 if ($checkbox.prop('checked')) {
231 Recycler.markedRecordsForMassAction.push(record);
232 $tr.addClass('warning');
233 } else {
234 var index = Recycler.markedRecordsForMassAction.indexOf(record);
235 if (index > -1) {
236 Recycler.markedRecordsForMassAction.splice(index, 1);
237 }
238 $tr.removeClass('warning');
239 }
240
241 if (Recycler.markedRecordsForMassAction.length > 0) {
242 if (Recycler.elements.$massUndo.hasClass('disabled')) {
243 Recycler.elements.$massUndo.removeClass('disabled');
244 }
245 if (Recycler.elements.$massDelete.hasClass('disabled')) {
246 Recycler.elements.$massDelete.removeClass('disabled');
247 }
248
249 var btnTextUndo = Recycler.createMessage(TYPO3.lang['button.undoselected'], [Recycler.markedRecordsForMassAction.length]),
250 btnTextDelete = Recycler.createMessage(TYPO3.lang['button.deleteselected'], [Recycler.markedRecordsForMassAction.length]);
251
252 Recycler.elements.$massUndo.find('span.text').text(btnTextUndo);
253 Recycler.elements.$massDelete.find('span.text').text(btnTextDelete);
254
255 } else {
256 Recycler.resetMassActionButtons();
257 }
258 };
259
260 /**
261 * Resets the mass action state
262 */
263 Recycler.resetMassActionButtons = function() {
264 Recycler.markedRecordsForMassAction = [];
265 Recycler.elements.$massUndo.addClass('disabled');
266 Recycler.elements.$massUndo.find('span.text').text(TYPO3.lang['button.undo']);
267 Recycler.elements.$massDelete.addClass('disabled');
268 Recycler.elements.$massDelete.find('span.text').text(TYPO3.lang['button.delete']);
269 };
270
271 /**
272 * Loads all tables which contain deleted records.
273 *
274 * @returns {Promise}
275 */
276 Recycler.loadAvailableTables = function() {
277 return $.ajax({
278 url: TYPO3.settings.ajaxUrls['recycler'],
279 dataType: 'json',
280 data: {
281 action: 'getTables',
282 startUid: TYPO3.settings.Recycler.startUid,
283 depth: Recycler.elements.$depthSelector.find('option:selected').val()
284 },
285 beforeSend: function() {
286 NProgress.start();
287 Recycler.elements.$tableSelector.val('');
288 Recycler.paging.currentPage = 1;
289 },
290 success: function(data) {
291 var tables = [];
292 Recycler.elements.$tableSelector.children().remove();
293 $.each(data, function(_, value) {
294 var tableName = value[0],
295 deletedRecords = value[1],
296 tableDescription = value[2];
297
298 if (tableDescription === '') {
299 tableDescription = TYPO3.lang['label_allrecordtypes'];
300 }
301 var optionText = tableDescription + ' (' + deletedRecords + ')';
302 tables.push($('<option />').val(tableName).text(optionText))
303 });
304
305 if (tables.length > 0) {
306 Recycler.elements.$tableSelector.append(tables);
307 if (TYPO3.settings.Recycler.tableSelection !== '') {
308 Recycler.elements.$tableSelector.val(TYPO3.settings.Recycler.tableSelection);
309 }
310 }
311 },
312 complete: function() {
313 NProgress.done();
314 }
315 });
316 };
317
318 /**
319 * Loads the deleted elements, based on the filters
320 *
321 * @returns {Promise}
322 */
323 Recycler.loadDeletedElements = function() {
324 return $.ajax({
325 url: TYPO3.settings.ajaxUrls['recycler'],
326 dataType: 'json',
327 data: {
328 action: 'getDeletedRecords',
329 depth: Recycler.elements.$depthSelector.find('option:selected').val(),
330 startUid: TYPO3.settings.Recycler.startUid,
331 table: Recycler.elements.$tableSelector.find('option:selected').val(),
332 filterTxt: Recycler.elements.$searchTextField.val(),
333 start: (Recycler.paging.currentPage - 1) * Recycler.paging.itemsPerPage,
334 limit: Recycler.paging.itemsPerPage
335 },
336 beforeSend: function() {
337 NProgress.start();
338 Recycler.resetMassActionButtons();
339 },
340 success: function(data) {
341 Recycler.elements.$tableBody.html(data.rows);
342 Recycler.buildPaginator(data.totalItems);
343 },
344 complete: function() {
345 NProgress.done();
346 }
347 });
348 };
349
350 /**
351 *
352 */
353 Recycler.deleteRecord = function() {
354 if (TYPO3.settings.Recycler.deleteDisable) {
355 return;
356 }
357
358 var $tr = $(this).parents('tr'),
359 isMassDelete = $tr.parent().prop('tagName') !== 'TBODY'; // deleteRecord() was invoked by the mass delete button
360
361 var records, message;
362 if (isMassDelete) {
363 records = Recycler.markedRecordsForMassAction;
364 message = TYPO3.lang['modal.massdelete.text'];
365 } else {
366 var uid = $tr.data('uid'),
367 table = $tr.data('table'),
368 recordTitle = $tr.data('recordtitle');
369 records = table + ':' + uid;
370 message = table === 'pages' ? TYPO3.lang['modal.deletepage.text'] : TYPO3.lang['modal.deletecontent.text'];
371 message = Recycler.createMessage(message, [recordTitle, '[' + records + ']']);
372 }
373
374 Modal.confirm(TYPO3.lang['modal.delete.header'], message, Severity.error, [
375 {
376 text: TYPO3.lang['button.cancel'],
377 btnClass: 'btn-default',
378 trigger: function() {
379 Modal.dismiss();
380 }
381 }, {
382 text: TYPO3.lang['button.delete'],
383 btnClass: 'btn-danger',
384 trigger: function() {
385 Recycler.callAjaxAction('delete', typeof records === 'object' ? records : [records], isMassDelete);
386 }
387 }
388 ]);
389 };
390
391 /**
392 *
393 */
394 Recycler.undoRecord = function() {
395 var $tr = $(this).parents('tr'),
396 isMassUndo = $tr.parent().prop('tagName') !== 'TBODY'; // undoRecord() was invoked by the mass delete button
397
398 var records, messageText, recoverPages;
399 if (isMassUndo) {
400 records = Recycler.markedRecordsForMassAction;
401 messageText = TYPO3.lang['modal.massundo.text'];
402 recoverPages = true;
403 } else {
404 var uid = $tr.data('uid'),
405 table = $tr.data('table'),
406 recordTitle = $tr.data('recordtitle');
407
408 records = table + ':' + uid;
409 recoverPages = table === 'pages';
410 messageText = recoverPages ? TYPO3.lang['modal.undopage.text'] : TYPO3.lang['modal.undocontent.text'];
411 messageText = Recycler.createMessage(messageText, [recordTitle, '[' + records + ']']);
412
413 if (recoverPages && $tr.data('parentDeleted')) {
414 messageText += TYPO3.lang['modal.undo.parentpages'];
415 }
416 }
417
418 var $message = null;
419 if (recoverPages) {
420 $message = $('<div />').append(
421 $('<p />').text(messageText),
422 $('<div />', {class: 'checkbox'}).append(
423 $('<label />').append(TYPO3.lang['modal.undo.recursive']).prepend($('<input />', {
424 id: 'undo-recursive',
425 type: 'checkbox'
426 }))
427 )
428 );
429 } else {
430 $message = messageText;
431 }
432
433 Modal.confirm(TYPO3.lang['modal.undo.header'], $message, Severity.ok, [
434 {
435 text: TYPO3.lang['button.cancel'],
436 btnClass: 'btn-default',
437 trigger: function() {
438 Modal.dismiss();
439 }
440 }, {
441 text: TYPO3.lang['button.undo'],
442 btnClass: 'btn-success',
443 trigger: function() {
444 Recycler.callAjaxAction('undo', typeof records === 'object' ? records : [records], isMassUndo, $message.find('#undo-recursive').prop('checked') ? 1 : 0);
445 }
446 }
447 ]);
448 };
449
450 /**
451 *
452 * @param {String} action
453 * @param {Object} records
454 * @param {Boolean} isMassAction
455 * @param {Boolean} recursive
456 */
457 Recycler.callAjaxAction = function(action, records, isMassAction, recursive) {
458 var data = {
459 records: records,
460 action: ''
461 },
462 reloadPageTree = false;
463 if (action === 'undo') {
464 data.action = 'undoRecords';
465 data.recursive = recursive ? 1 : 0;
466 reloadPageTree = true;
467 } else if (action === 'delete') {
468 data.action = 'deleteRecords';
469 } else {
470 return;
471 }
472
473 $.ajax({
474 url: TYPO3.settings.ajaxUrls['recycler'],
475 dataType: 'json',
476 data: data,
477 beforeSend: function() {
478 NProgress.start();
479 },
480 success: function(data) {
481 if (data.success) {
482 Notification.success('', data.message);
483 } else {
484 Notification.error('', data.message);
485 }
486
487 // reload recycler data
488 Recycler.paging.currentPage = 1;
489
490 $.when(Recycler.loadAvailableTables()).done(function() {
491 Recycler.loadDeletedElements();
492 if (isMassAction) {
493 Recycler.resetMassActionButtons();
494 }
495
496 if (reloadPageTree) {
497 Recycler.refreshPageTree();
498 }
499
500 // Reset toggle state
501 Recycler.allToggled = false;
502 });
503 },
504 complete: function() {
505 Modal.dismiss();
506 NProgress.done();
507 }
508 });
509 };
510
511 /**
512 * Replaces the placeholders with actual values
513 *
514 * @param {String} message
515 * @param {Array} placeholders
516 * @returns {*}
517 */
518 Recycler.createMessage = function(message, placeholders) {
519 if (typeof message === 'undefined') {
520 return '';
521 }
522
523 return message.replace(
524 /\{([0-9]+)\}/g,
525 function(_, index) {
526 return placeholders[index];
527 }
528 );
529 };
530
531 /**
532 * Reloads the page tree
533 */
534 Recycler.refreshPageTree = function() {
535 if (top.TYPO3 && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer && top.TYPO3.Backend.NavigationContainer.PageTree) {
536 top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree();
537 }
538 };
539
540 /**
541 * Build the paginator
542 *
543 * @param {Number} totalItems
544 */
545 Recycler.buildPaginator = function(totalItems) {
546 if (totalItems === 0) {
547 Recycler.elements.$paginator.contents().remove();
548 return;
549 }
550
551 Recycler.paging.totalItems = totalItems;
552 Recycler.paging.totalPages = Math.ceil(totalItems / Recycler.paging.itemsPerPage);
553
554 if (Recycler.paging.totalPages === 1) {
555 // early abort if only one page is available
556 Recycler.elements.$paginator.contents().remove();
557 return;
558 }
559
560 var $ul = $('<ul />', {class: 'pagination pagination-block'}),
561 liElements = [],
562 $controlFirstPage = $('<li />').append(
563 $('<a />', {'data-action': 'previous'}).append(
564 $('<span />', {class: 't3-icon fa fa-arrow-left'})
565 )
566 ),
567 $controlLastPage = $('<li />').append(
568 $('<a />', {'data-action': 'next'}).append(
569 $('<span />', {class: 't3-icon fa fa-arrow-right'})
570 )
571 );
572
573 if (Recycler.paging.currentPage === 1) {
574 $controlFirstPage.disablePagingAction();
575 }
576
577 if (Recycler.paging.currentPage === Recycler.paging.totalPages) {
578 $controlLastPage.disablePagingAction();
579 }
580
581 for (var i = 1; i <= Recycler.paging.totalPages; i++) {
582 var $li = $('<li />', {class: Recycler.paging.currentPage === i ? 'active' : ''});
583 $li.append(
584 $('<a />', {'data-action': 'page'}).append(
585 $('<span />').text(i)
586 )
587 );
588 liElements.push($li);
589 }
590
591 $ul.append($controlFirstPage, liElements, $controlLastPage);
592 Recycler.elements.$paginator.html($ul);
593 };
594
595 /**
596 * Changes the markup of a pagination action being disabled
597 */
598 $.fn.disablePagingAction = function() {
599 $(this).addClass('disabled').find('.t3-icon').unwrap().wrap($('<span />'));
600 };
601
602 $(Recycler.initialize);
603
604 return Recycler;
605 });