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