bf7c2cf550e20b4a01e24d32abb942a753d1138d
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Public / JavaScript / DragUploader.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/Backend/DragUploader
16 *
17 */
18 define(['jquery',
19 'moment',
20 'nprogress',
21 'TYPO3/CMS/Backend/Modal',
22 'TYPO3/CMS/Backend/Notification',
23 'TYPO3/CMS/Backend/Severity',
24 'TYPO3/CMS/Lang/Lang'
25 ], function($, moment, NProgress, Modal, Notification, Severity) {
26
27 /**
28 * Array of files which are asked for being overridden
29 *
30 * @type {array}
31 */
32 var askForOverride = [],
33 percentagePerFile = 1;
34
35 /**
36 * File actions
37 */
38 var actions = {
39 OVERRIDE: 'replace',
40 RENAME: 'rename',
41 SKIP: 'cancel',
42 USE_EXISTING: 'useExisting'
43 };
44
45 /*
46 * part 1: a generic jQuery plugin "$.dragUploader"
47 */
48
49 // register the constructor
50 /**
51 *
52 * @param {HTMLElement} element
53 * @constructor
54 * @exports TYPO3/CMS/Backend/DragUploader
55 */
56 var DragUploaderPlugin = function(element) {
57 var me = this;
58 me.$body = $('body');
59 me.$element = $(element);
60 me.$trigger = $(me.$element.data('dropzoneTrigger'));
61 me.$dropzone = $('<div />').addClass('dropzone').hide();
62 me.irreObjectUid = me.$element.data('fileIrreObject');
63 var dropZoneEscapedTarget = me.$element.data('dropzoneTarget');
64 if (me.irreObjectUid && me.$element.nextAll(dropZoneEscapedTarget).length !== 0) {
65 me.dropZoneInsertBefore = true;
66 me.$dropzone.insertBefore(dropZoneEscapedTarget);
67 } else {
68 me.dropZoneInsertBefore = false;
69 me.$dropzone.insertAfter(dropZoneEscapedTarget);
70 }
71 me.$dropzoneMask = $('<div />').addClass('dropzone-mask').appendTo(me.$dropzone);
72 me.$fileInput = $('<input type="file" multiple name="files[]" />').addClass('upload-file-picker').appendTo(me.$body);
73 me.$fileList = $(me.$element.data('progress-container'));
74 me.fileListColumnCount = $('thead tr:first th', me.$fileList).length;
75 me.filesExtensionsAllowed = me.$element.data('file-allowed');
76 me.fileDenyPattern = me.$element.data('file-deny-pattern') ? new RegExp(me.$element.data('file-deny-pattern'), 'i') : false;
77 me.maxFileSize = parseInt(me.$element.data('max-file-size'));
78 me.target = me.$element.data('target-folder');
79
80 me.browserCapabilities = {
81 fileReader: typeof FileReader !== 'undefined',
82 DnD: 'draggable' in document.createElement('span'),
83 FormData: !!window.FormData,
84 Progress: "upload" in new XMLHttpRequest
85 };
86
87 /**
88 *
89 */
90 me.showDropzone = function() {
91 me.$dropzone.show();
92 };
93
94 /**
95 *
96 * @param {Event} event
97 */
98 me.hideDropzone = function(event) {
99 event.stopPropagation();
100 event.preventDefault();
101 me.$dropzone.hide();
102 };
103
104 /**
105 *
106 * @param {Event} event
107 * @returns {Boolean}
108 */
109 me.dragFileIntoDocument = function(event) {
110 event.stopPropagation();
111 event.preventDefault();
112 me.$body.addClass('drop-in-progress');
113 me.showDropzone();
114 return false;
115 };
116
117 /**
118 *
119 * @param {Event} event
120 * @returns {Boolean}
121 */
122 me.dragAborted = function(event) {
123 event.stopPropagation();
124 event.preventDefault();
125 me.$body.removeClass('drop-in-progress');
126 return false;
127 };
128
129 /**
130 *
131 * @param {Event} event
132 * @returns {Boolean}
133 */
134 me.ignoreDrop = function(event) {
135 // stops the browser from redirecting.
136 event.stopPropagation();
137 event.preventDefault();
138 me.dragAborted(event);
139 return false;
140 };
141
142 /**
143 *
144 * @param {Event} event
145 */
146 me.handleDrop = function(event) {
147 me.ignoreDrop(event);
148 me.processFiles(event.originalEvent.dataTransfer.files);
149 me.$dropzone.removeClass('drop-status-ok');
150 };
151
152 /**
153 *
154 * @param {Array} files
155 */
156 me.processFiles = function(files) {
157 me.queueLength = files.length;
158
159 if (!me.$fileList.is(':visible')) {
160 me.$fileList.show();
161 }
162
163 NProgress.start();
164 percentagePerFile = 1 / files.length;
165
166 // Check for each file if is already exist before adding it to the queue
167 var ajaxCalls = [];
168 $.each(files, function(i, file) {
169
170 ajaxCalls[i] = $.ajax({
171 url: TYPO3.settings.ajaxUrls['file_exists'],
172 data: {
173 fileName: file.name,
174 fileTarget: me.target
175 },
176 cache: false,
177 success: function(response) {
178 var fileExists = typeof response.uid !== 'undefined';
179 if (fileExists) {
180 askForOverride.push({
181 original: response,
182 uploaded: file,
183 action: me.irreObjectUid ? actions.USE_EXISTING : actions.SKIP
184 });
185 NProgress.inc(percentagePerFile);
186 } else {
187 new FileQueueItem(me, file, 'cancel');
188 }
189 }
190 });
191 });
192
193 $.when.apply($, ajaxCalls).done(function() {
194 me.drawOverrideModal();
195 NProgress.done();
196 });
197
198 delete ajaxCalls;
199 me.$fileInput.val('');
200 };
201
202 /**
203 *
204 * @param {Event} event
205 */
206 me.fileInDropzone = function(event) {
207 me.$dropzone.addClass('drop-status-ok');
208 };
209
210 /**
211 *
212 * @param {Event} event
213 */
214 me.fileOutOfDropzone = function(event) {
215 me.$dropzone.removeClass('drop-status-ok');
216 };
217
218 /**
219 * bind file picker to default upload button
220 *
221 * @param {Object} button
222 */
223 me.bindUploadButton = function(button) {
224 button.click(function(event) {
225 event.preventDefault();
226 me.$fileInput.click();
227 me.showDropzone();
228 });
229 };
230
231 if (me.browserCapabilities.DnD) {
232 me.$body.on('dragover', me.dragFileIntoDocument);
233 me.$body.on('dragend', me.dragAborted);
234 me.$body.on('drop', me.ignoreDrop);
235
236 me.$dropzone.on('dragenter', me.fileInDropzone);
237 me.$dropzoneMask.on('dragenter', me.fileInDropzone);
238 me.$dropzoneMask.on('dragleave', me.fileOutOfDropzone);
239 me.$dropzoneMask.on('drop', me.handleDrop);
240
241 me.$dropzone.prepend(
242 '<div class="dropzone-hint">' +
243 '<div class="dropzone-hint-media">' +
244 '<div class="dropzone-hint-icon"></div>' +
245 '</div>' +
246 '<div class="dropzone-hint-body">' +
247 '<h3 class="dropzone-hint-title">' +
248 TYPO3.lang['file_upload.dropzonehint.title'] +
249 '</h3>' +
250 '<p class="dropzone-hint-message">' +
251 TYPO3.lang['file_upload.dropzonehint.message'] +
252 '</p>' +
253 '</div>' +
254 '</div>').click(function() {
255 me.$fileInput.click()
256 });
257 $('<span />').addClass('dropzone-close').click(me.hideDropzone).appendTo(me.$dropzone);
258
259 // no filelist then create own progress table
260 if (me.$fileList.length === 0) {
261 me.$fileList = $('<table />')
262 .attr('id', 'typo3-filelist')
263 .addClass('table table-striped table-hover upload-queue')
264 .html('<tbody></tbody>').hide();
265 if (me.dropZoneInsertBefore) {
266 me.$fileList.insertAfter(me.$dropzone);
267 } else {
268 me.$fileList.insertBefore(me.$dropzone);
269 }
270 me.fileListColumnCount = 7;
271 }
272
273 me.$fileInput.on('change', function() {
274 me.processFiles(this.files);
275 });
276
277 me.bindUploadButton(me.$trigger.length ? me.$trigger : me.$element);
278 }
279
280 /**
281 *
282 */
283 me.decrementQueueLength = function() {
284 if (me.queueLength > 0) {
285 me.queueLength--;
286 if (me.queueLength === 0) {
287 $.ajax({
288 url: TYPO3.settings.ajaxUrls['flashmessages_render'],
289 cache: false,
290 success: function(data) {
291 $.each(data, function(index, flashMessage) {
292 Notification.showMessage(flashMessage.title, flashMessage.message, flashMessage.severity);
293 });
294 }
295 });
296 }
297 }
298 };
299
300 /**
301 *
302 */
303 me.drawOverrideModal = function() {
304 var amountOfItems = Object.keys(askForOverride).length;
305 if (amountOfItems === 0) {
306 return;
307 }
308 var $modalContent = $('<div/>').append(
309 $('<p/>').text(TYPO3.lang['file_upload.existingfiles.description']),
310 $('<table/>', {class: 'table'}).append(
311 $('<thead/>').append(
312 $('<tr />').append(
313 $('<th/>'),
314 $('<th/>').text(TYPO3.lang['file_upload.header.originalFile']),
315 $('<th/>').text(TYPO3.lang['file_upload.header.uploadedFile']),
316 $('<th/>').text(TYPO3.lang['file_upload.header.action'])
317 )
318 )
319 )
320 );
321
322 for (var i = 0; i < amountOfItems; ++i) {
323 var $record = $('<tr />').append(
324 $('<td />').append(
325 (askForOverride[i].original.thumbUrl !== ''
326 ? $('<img />', {src: askForOverride[i].original.thumbUrl, height: 40})
327 : $(askForOverride[i].original.icon)
328 )
329 ),
330 $('<td />').html(
331 askForOverride[i].uploaded.name + ' (' + (DragUploader.fileSizeAsString(askForOverride[i].uploaded.size)) + ')' +
332 '<br>' + moment(askForOverride[i].uploaded.lastModified, 'x').format('YYYY-MM-DD HH:mm')
333 ),
334 $('<td />').html(
335 askForOverride[i].uploaded.name + ' (' + (DragUploader.fileSizeAsString(askForOverride[i].original.size)) + ')' +
336 '<br>' + moment(askForOverride[i].original.mtime, 'X').format('YYYY-MM-DD HH:mm')
337 ),
338 $('<td />').append(
339 $('<select />', {class: 'form-control t3js-actions', 'data-override': i}).append(
340 (me.irreObjectUid ? $('<option/>').val(actions.USE_EXISTING).text(TYPO3.lang['file_upload.actions.use_existing']) : ''),
341 $('<option />').val(actions.SKIP).text(TYPO3.lang['file_upload.actions.skip']),
342 $('<option />').val(actions.RENAME).text(TYPO3.lang['file_upload.actions.rename']),
343 $('<option />').val(actions.OVERRIDE).text(TYPO3.lang['file_upload.actions.override'])
344 )
345 )
346 );
347 $modalContent.find('table').append('<tbody />').append($record);
348 }
349
350 var $modal = Modal.confirm(TYPO3.lang['file_upload.existingfiles.title'], $modalContent, Severity.warning, [
351 {
352 text: $(this).data('button-close-text') || TYPO3.lang['file_upload.button.cancel'] || 'Cancel',
353 active: true,
354 btnClass: 'btn-default',
355 name: 'cancel'
356 },
357 {
358 text: $(this).data('button-ok-text') || TYPO3.lang['file_upload.button.continue'] || 'Continue with selected actions',
359 btnClass: 'btn-warning',
360 name: 'continue'
361 }
362 ], ['modal-inner-scroll']);
363 $modal.find('.modal-dialog').addClass('modal-lg');
364
365 $modal.find('.modal-footer').prepend(
366 $('<span/>').addClass('form-inline').append(
367 $('<label/>').text(TYPO3.lang['file_upload.actions.all.label']),
368 $('<select/>', {class: 'form-control t3js-actions-all'}).append(
369 $('<option/>').val('').text(TYPO3.lang['file_upload.actions.all.empty']),
370 (me.irreObjectUid ? $('<option/>').val(actions.USE_EXISTING).text(TYPO3.lang['file_upload.actions.all.use_existing']) : ''),
371 $('<option/>').val(actions.SKIP).text(TYPO3.lang['file_upload.actions.all.skip']),
372 $('<option/>').val(actions.RENAME).text(TYPO3.lang['file_upload.actions.all.rename']),
373 $('<option/>').val(actions.OVERRIDE).text(TYPO3.lang['file_upload.actions.all.override'])
374 )
375 )
376 );
377
378 $modal.on('change', '.t3js-actions-all', function() {
379 var $me = $(this),
380 value = $me.val();
381
382 if (value !== '') {
383 // mass action was selected, apply action to every file
384 $modal.find('.t3js-actions').each(function(i, select) {
385 var $select = $(select),
386 index = parseInt($select.data('override'));
387 $select.val(value).prop('disabled', 'disabled');
388 askForOverride[index].action = $select.val();
389 });
390 } else {
391 $modal.find('.t3js-actions').removeProp('disabled');
392 }
393 }).on('change', '.t3js-actions', function() {
394 var $me = $(this),
395 index = parseInt($me.data('override'));
396 askForOverride[index].action = $me.val();
397 }).on('button.clicked', function(e) {
398 if (e.target.name === 'cancel') {
399 askForOverride = [];
400 Modal.dismiss();
401 } else if (e.target.name === 'continue') {
402 $.each(askForOverride, function(key, fileInfo) {
403 if (fileInfo.action === actions.USE_EXISTING) {
404 DragUploader.addFileToIrre(
405 me.irreObjectUid,
406 fileInfo.original
407 );
408 } else if (fileInfo.action !== actions.SKIP) {
409 new FileQueueItem(me, fileInfo.uploaded, fileInfo.action);
410 }
411 });
412 askForOverride = [];
413 Modal.dismiss();
414 }
415 }).on('hidden.bs.modal', function() {
416 askForOverride = [];
417 });
418 }
419 };
420
421 var FileQueueItem = function(dragUploader, file, override) {
422 var me = this;
423 me.dragUploader = dragUploader;
424 me.file = file;
425 me.override = override;
426
427 me.$row = $('<tr />').addClass('upload-queue-item uploading');
428 me.$iconCol = $('<td />').addClass('col-icon').appendTo(me.$row);
429 me.$fileName = $('<td />').text(file.name).appendTo(me.$row);
430 me.$progress = $('<td />').attr('colspan', me.dragUploader.fileListColumnCount - 2).appendTo(me.$row);
431 me.$progressContainer = $('<div />').addClass('upload-queue-progress').appendTo(me.$progress);
432 me.$progressBar = $('<div />').addClass('upload-queue-progress-bar').appendTo(me.$progressContainer);
433 me.$progressPercentage = $('<span />').addClass('upload-queue-progress-percentage').appendTo(me.$progressContainer);
434 me.$progressMessage = $('<span />').addClass('upload-queue-progress-message').appendTo(me.$progressContainer);
435
436 me.updateMessage = function(message) {
437 me.$progressMessage.text(message);
438 };
439
440 me.removeProgress = function() {
441 if (me.$progress) {
442 me.$progress.remove();
443 }
444 };
445
446 me.uploadStart = function() {
447 me.$progressPercentage.text('(0%)');
448 me.$progressBar.width('1%');
449 me.dragUploader.$trigger.trigger('uploadStart', [me]);
450 };
451
452 me.uploadError = function(response) {
453 me.updateMessage(TYPO3.lang['file_upload.uploadFailed'].replace(/\{0\}/g, me.file.name));
454 var error = $(response.responseText);
455 if (error.is('t3err')) {
456 me.$progressPercentage.text(error.text());
457 } else {
458 me.$progressPercentage.text('(' + response.statusText + ')');
459 }
460 me.$row.addClass('error');
461 me.dragUploader.decrementQueueLength();
462 me.dragUploader.$trigger.trigger('uploadError', [me, response]);
463 };
464
465 me.updateProgress = function(event) {
466 var percentage = Math.round((event.loaded / event.total) * 100) + '%';
467 me.$progressBar.outerWidth(percentage);
468 me.$progressPercentage.text(percentage);
469 me.dragUploader.$trigger.trigger('updateProgress', [me, percentage, event]);
470 };
471
472 me.uploadSuccess = function(data) {
473 if (data.upload) {
474 me.dragUploader.decrementQueueLength();
475 me.$row.removeClass('uploading');
476 me.$fileName.text(data.upload[0].name);
477 me.$progressPercentage.text('');
478 me.$progressMessage.text('100%');
479 me.$progressBar.outerWidth('100%');
480
481 // replace file icon
482 if (data.upload[0].icon) {
483 me.$iconCol.html('<a href="#" class="t3js-contextmenutrigger" data-uid="' + data.upload[0].id + '" data-table="sys_file">' + data.upload[0].icon + '&nbsp;</span></a>');
484 }
485
486 if (me.dragUploader.irreObjectUid) {
487 DragUploader.addFileToIrre(
488 me.dragUploader.irreObjectUid,
489 data.upload[0]
490 );
491 setTimeout(function() {
492 me.$row.remove();
493 if ($('tr', me.dragUploader.$fileList).length === 0) {
494 me.dragUploader.$fileList.hide();
495 me.dragUploader.$trigger.trigger('uploadSuccess', [me, data]);
496 }
497 }, 3000);
498 } else {
499 setTimeout(function() {
500 me.showFileInfo(data.upload[0]);
501 me.dragUploader.$trigger.trigger('uploadSuccess', [me, data]);
502 }, 3000);
503 }
504 }
505 };
506
507 me.showFileInfo = function(fileInfo) {
508 me.removeProgress();
509 // add spacing cells when clibboard and/or extended view is enabled
510 for (i = 7; i < me.dragUploader.fileListColumnCount; i++) {
511 $('<td />').text('').appendTo(me.$row);
512 }
513 $('<td />').text(fileInfo.extension.toUpperCase()).appendTo(me.$row);
514 $('<td />').text(fileInfo.date).appendTo(me.$row);
515 $('<td />').text(DragUploader.fileSizeAsString(fileInfo.size)).appendTo(me.$row);
516 var permissions = '';
517 if (fileInfo.permissions.read) {
518 permissions += '<strong class="text-danger">' + TYPO3.lang['permissions.read'] + '</strong>';
519 }
520 if (fileInfo.permissions.write) {
521 permissions += '<strong class="text-danger">' + TYPO3.lang['permissions.write'] + '</strong>';
522 }
523 $('<td />').html(permissions).appendTo(me.$row);
524 $('<td />').text('-').appendTo(me.$row);
525 };
526
527 me.checkAllowedExtensions = function() {
528 if (!me.dragUploader.filesExtensionsAllowed) {
529 return true;
530 }
531 var extension = me.file.name.split('.').pop();
532 var allowed = me.dragUploader.filesExtensionsAllowed.split(',');
533 if ($.inArray(extension.toLowerCase(), allowed) !== -1) {
534 return true;
535 }
536 return false;
537 };
538
539 // position queue item in filelist
540 if ($('tbody tr.upload-queue-item', me.dragUploader.$fileList).length === 0) {
541 me.$row.prependTo($('tbody', me.dragUploader.$fileList));
542 me.$row.addClass('last');
543 } else {
544 me.$row.insertBefore($('tbody tr.upload-queue-item:first', me.dragUploader.$fileList));
545 }
546
547 // set dummy file icon
548 me.$iconCol.html('<span class="t3-icon t3-icon-mimetypes t3-icon-other-other">&nbsp;</span>')
549
550 // check file size
551 if (me.dragUploader.maxFileSize > 0 && me.file.size > me.dragUploader.maxFileSize) {
552 me.updateMessage(TYPO3.lang['file_upload.maxFileSizeExceeded']
553 .replace(/\{0\}/g, me.file.name)
554 .replace(/\{1\}/g, DragUploader.fileSizeAsString(me.dragUploader.maxFileSize)));
555 me.$row.addClass('error');
556
557 // check filename/extension against deny pattern
558 } else if (me.dragUploader.fileDenyPattern && me.file.name.match(me.dragUploader.fileDenyPattern)) {
559 me.updateMessage(TYPO3.lang['file_upload.fileNotAllowed'].replace(/\{0\}/g, me.file.name));
560 me.$row.addClass('error');
561
562 } else if (!me.checkAllowedExtensions()) {
563 me.updateMessage(TYPO3.lang['file_upload.fileExtensionExpected']
564 .replace(/\{0\}/g, me.dragUploader.filesExtensionsAllowed)
565 );
566 me.$row.addClass('error');
567 } else {
568 me.updateMessage('- ' + DragUploader.fileSizeAsString(me.file.size));
569
570 var formData = new FormData();
571 formData.append('data[upload][1][target]', me.dragUploader.target);
572 formData.append('data[upload][1][data]', '1');
573 formData.append('overwriteExistingFiles', me.override);
574 formData.append('redirect', '');
575 formData.append('upload_1', me.file);
576
577 var s = $.extend(true, {}, $.ajaxSettings, {
578 url: TYPO3.settings.ajaxUrls['file_process'],
579 contentType: false,
580 processData: false,
581 data: formData,
582 cache: false,
583 type: 'POST',
584 success: me.uploadSuccess,
585 error: me.uploadError
586 });
587
588 s.xhr = function() {
589 var xhr = $.ajaxSettings.xhr();
590 xhr.upload.addEventListener('progress', me.updateProgress);
591 return xhr;
592 };
593
594 // start upload
595 me.upload = $.ajax(s);
596 }
597 };
598
599 /**
600 * part 2: The main module of this file
601 * - initialize the DragUploader module and register
602 * the jQuery plugin in the jQuery global object
603 * when initializing the DragUploader module
604 */
605 var DragUploader = {};
606
607 DragUploader.options = {};
608
609 DragUploader.fileSizeAsString = function(size) {
610 var string = '',
611 sizeKB = size / 1024;
612
613 if (parseInt(sizeKB) > 1024) {
614 var sizeMB = sizeKB / 1024;
615 string = sizeMB.toFixed(1) + ' MB';
616 } else {
617 string = sizeKB.toFixed(1) + ' KB';
618 }
619 return string;
620 };
621
622 DragUploader.addFileToIrre = function(irre_object, file) {
623 inline.delayedImportElement(
624 irre_object,
625 'sys_file',
626 file.uid,
627 'file'
628 );
629 };
630
631 DragUploader.initialize = function() {
632 var me = this,
633 opts = me.options;
634
635 // register the jQuery plugin "DragUploaderPlugin"
636 $.fn.dragUploader = function(option) {
637 return this.each(function() {
638 var $this = $(this),
639 data = $this.data('DragUploaderPlugin');
640 if (!data) {
641 $this.data('DragUploaderPlugin', (data = new DragUploaderPlugin(this)));
642 }
643 if (typeof option === 'string') {
644 data[option]();
645 }
646 });
647 };
648
649 $(function() {
650 $('.t3js-drag-uploader').dragUploader();
651 });
652 };
653
654
655 /**
656 * part 3: initialize the RequireJS module, require possible post-initialize hooks,
657 * and return the main object
658 */
659 var initialize = function() {
660
661 DragUploader.initialize();
662
663 // load required modules to hook in the post initialize function
664 if (
665 'undefined' !== typeof TYPO3.settings
666 && 'undefined' !== typeof TYPO3.settings.RequireJS
667 && 'undefined' !== typeof TYPO3.settings.RequireJS.PostInitializationModules
668 && 'undefined' !== typeof TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/DragUploader']
669 ) {
670 $.each(TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/DragUploader'], function(pos, moduleName) {
671 require([moduleName]);
672 });
673 }
674
675 // return the object in the global space
676 return DragUploader;
677 };
678
679 // call the main initialize function and execute the hooks
680 return initialize();
681
682 });