c29901383081d9ae5cbf5cec260f0b815afb5159
[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 = response !== false;
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 />').attr('id', 'typo3-filelist').addClass('table table-striped table-hover upload-queue').html('<tbody></tbody>').hide();
262 if (me.dropZoneInsertBefore) {
263 me.$fileList.insertAfter(me.$dropzone);
264 } else {
265 me.$fileList.insertBefore(me.$dropzone);
266 }
267 me.fileListColumnCount = 7;
268 }
269
270 me.$fileInput.on('change', function() {
271 me.processFiles(this.files);
272 });
273
274 me.bindUploadButton(me.$trigger.length ? me.$trigger : me.$element);
275 }
276
277 /**
278 *
279 */
280 me.decrementQueueLength = function() {
281 if (me.queueLength > 0) {
282 me.queueLength--;
283 if (me.queueLength === 0) {
284 $.ajax({
285 url: TYPO3.settings.ajaxUrls['flashmessages_render'],
286 cache: false,
287 success: function(data) {
288 $.each(data, function(index, flashMessage) {
289 Notification.showMessage(flashMessage.title, flashMessage.message, flashMessage.severity);
290 });
291 }
292 });
293 }
294 }
295 };
296
297 /**
298 *
299 */
300 me.drawOverrideModal = function() {
301 var amountOfItems = Object.keys(askForOverride).length;
302 if (amountOfItems === 0) {
303 return;
304 }
305 var $modalContent = $('<div/>').append(
306 $('<p/>').text(TYPO3.lang['file_upload.existingfiles.description']),
307 $('<table/>', {class: 'table table-striped'}).append(
308 $('<tr />').append(
309 $('<th/>'),
310 $('<th/>').text(TYPO3.lang['file_upload.header.originalFile']),
311 $('<th/>').text(TYPO3.lang['file_upload.header.uploadedFile']),
312 $('<th/>').text(TYPO3.lang['file_upload.header.action'])
313 )
314 )
315 );
316
317 for (var i = 0; i < amountOfItems; ++i) {
318 var $record = $('<tr />').append(
319 $('<td />').append(
320 (askForOverride[i].original.thumbUrl !== ''
321 ? $('<img />', {src: askForOverride[i].original.thumbUrl, height: 40})
322 : $(askForOverride[i].original.icon)
323 )
324 ),
325 $('<td />').html(
326 askForOverride[i].uploaded.name + ' (' + (DragUploader.fileSizeAsString(askForOverride[i].uploaded.size)) + ')' +
327 '<br>' + moment(askForOverride[i].uploaded.lastModified, 'x').format('YYYY-MM-DD HH:mm')
328 ),
329 $('<td />').html(
330 askForOverride[i].uploaded.name + ' (' + (DragUploader.fileSizeAsString(askForOverride[i].original.size)) + ')' +
331 '<br>' + moment(askForOverride[i].original.mtime, 'X').format('YYYY-MM-DD HH:mm')
332 ),
333 $('<td />').append(
334 $('<select />', {class: 'form-control t3js-actions', 'data-override': i}).append(
335 (me.irreObjectUid ? $('<option/>').val(actions.USE_EXISTING).text(TYPO3.lang['file_upload.actions.use_existing']) : ''),
336 $('<option />').val(actions.SKIP).text(TYPO3.lang['file_upload.actions.skip']),
337 $('<option />').val(actions.RENAME).text(TYPO3.lang['file_upload.actions.rename']),
338 $('<option />').val(actions.OVERRIDE).text(TYPO3.lang['file_upload.actions.override'])
339 )
340 )
341 );
342 $modalContent.find('table').append($record);
343 }
344
345 var $modal = Modal.confirm(TYPO3.lang['file_upload.existingfiles.title'], $modalContent, Severity.warning, [
346 {
347 text: $(this).data('button-close-text') || TYPO3.lang['file_upload.button.cancel'] || 'Cancel',
348 active: true,
349 btnClass: 'btn-default',
350 name: 'cancel'
351 },
352 {
353 text: $(this).data('button-ok-text') || TYPO3.lang['file_upload.button.continue'] || 'Continue with selected actions',
354 btnClass: 'btn-warning',
355 name: 'continue'
356 }
357 ], ['modal-inner-scroll']);
358 $modal.find('.modal-dialog').addClass('modal-lg');
359
360 $modal.find('.modal-footer').prepend(
361 $('<span/>').addClass('form-inline pull-left').append(
362 $('<label/>').text(TYPO3.lang['file_upload.actions.all.label']),
363 $('<select/>', {class: 'form-control t3js-actions-all'}).append(
364 $('<option/>').val('').text(TYPO3.lang['file_upload.actions.all.empty']),
365 (me.irreObjectUid ? $('<option/>').val(actions.USE_EXISTING).text(TYPO3.lang['file_upload.actions.all.use_existing']) : ''),
366 $('<option/>').val(actions.SKIP).text(TYPO3.lang['file_upload.actions.all.skip']),
367 $('<option/>').val(actions.RENAME).text(TYPO3.lang['file_upload.actions.all.rename']),
368 $('<option/>').val(actions.OVERRIDE).text(TYPO3.lang['file_upload.actions.all.override'])
369 )
370 )
371 );
372
373 $modal.on('change', '.t3js-actions-all', function() {
374 var $me = $(this),
375 value = $me.val();
376
377 if (value !== '') {
378 // mass action was selected, apply action to every file
379 $modal.find('.t3js-actions').each(function(i, select) {
380 var $select = $(select),
381 index = parseInt($select.data('override'));
382 $select.val(value).prop('disabled', 'disabled');
383 askForOverride[index].action = $select.val();
384 });
385 } else {
386 $modal.find('.t3js-actions').removeProp('disabled');
387 }
388 }).on('change', '.t3js-actions', function() {
389 var $me = $(this),
390 index = parseInt($me.data('override'));
391 askForOverride[index].action = $me.val();
392 }).on('button.clicked', function(e) {
393 if (e.target.name === 'cancel') {
394 askForOverride = [];
395 Modal.dismiss();
396 } else if (e.target.name === 'continue') {
397 $.each(askForOverride, function(key, fileInfo) {
398 if (fileInfo.action === actions.USE_EXISTING) {
399 DragUploader.addFileToIrre(
400 me.irreObjectUid,
401 fileInfo.original
402 );
403 } else if (fileInfo.action !== actions.SKIP) {
404 new FileQueueItem(me, fileInfo.uploaded, fileInfo.action);
405 }
406 });
407 askForOverride = [];
408 Modal.dismiss();
409 }
410 }).on('hidden.bs.modal', function() {
411 askForOverride = [];
412 });
413 }
414 };
415
416 var FileQueueItem = function(dragUploader, file, override) {
417 var me = this;
418 me.dragUploader = dragUploader;
419 me.file = file;
420 me.override = override;
421
422 me.$row = $('<tr />').addClass('upload-queue-item uploading');
423 me.$iconCol = $('<td />').addClass('col-icon').appendTo(me.$row);
424 me.$fileName = $('<td />').text(file.name).appendTo(me.$row);
425 me.$progress = $('<td />').attr('colspan', me.dragUploader.fileListColumnCount - 2).appendTo(me.$row);
426 me.$progressContainer = $('<div />').addClass('upload-queue-progress').appendTo(me.$progress);
427 me.$progressBar = $('<div />').addClass('upload-queue-progress-bar').appendTo(me.$progressContainer);
428 me.$progressPercentage = $('<span />').addClass('upload-queue-progress-percentage').appendTo(me.$progressContainer);
429 me.$progressMessage = $('<span />').addClass('upload-queue-progress-message').appendTo(me.$progressContainer);
430
431 me.updateMessage = function(message) {
432 me.$progressMessage.text(message);
433 };
434
435 me.removeProgress = function() {
436 if (me.$progress) {
437 me.$progress.remove();
438 }
439 };
440
441 me.uploadStart = function() {
442 me.$progressPercentage.text('(0%)');
443 me.$progressBar.width('1%');
444 me.dragUploader.$trigger.trigger('uploadStart', [me]);
445 };
446
447 me.uploadError = function(response) {
448 me.updateMessage(TYPO3.lang['file_upload.uploadFailed'].replace(/\{0\}/g, me.file.name));
449 var error = $(response.responseText);
450 if (error.is('t3err')) {
451 me.$progressPercentage.text(error.text());
452 } else {
453 me.$progressPercentage.text('(' + response.statusText + ')');
454 }
455 me.$row.addClass('error');
456 me.dragUploader.decrementQueueLength();
457 me.dragUploader.$trigger.trigger('uploadError', [me, response]);
458 };
459
460 me.updateProgress = function(event) {
461 var percentage = Math.round((event.loaded / event.total) * 100) + '%';
462 me.$progressBar.outerWidth(percentage);
463 me.$progressPercentage.text(percentage);
464 me.dragUploader.$trigger.trigger('updateProgress', [me, percentage, event]);
465 };
466
467 me.uploadSuccess = function(data) {
468 if (data.upload) {
469 me.dragUploader.decrementQueueLength();
470 me.$row.removeClass('uploading');
471 me.$fileName.text(data.upload[0].name);
472 me.$progressPercentage.text('');
473 me.$progressMessage.text('100%');
474 me.$progressBar.outerWidth('100%');
475
476 // replace file icon
477 if (data.upload[0].icon) {
478 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>');
479 }
480
481 if (me.dragUploader.irreObjectUid) {
482 DragUploader.addFileToIrre(
483 me.dragUploader.irreObjectUid,
484 data.upload[0]
485 );
486 setTimeout(function() {
487 me.$row.remove();
488 if ($('tr', me.dragUploader.$fileList).length === 0) {
489 me.dragUploader.$fileList.hide();
490 me.dragUploader.$trigger.trigger('uploadSuccess', [me, data]);
491 }
492 }, 3000);
493 } else {
494 setTimeout(function() {
495 me.showFileInfo(data.upload[0]);
496 me.dragUploader.$trigger.trigger('uploadSuccess', [me, data]);
497 }, 3000);
498 }
499 }
500 };
501
502 me.showFileInfo = function(fileInfo) {
503 me.removeProgress();
504 // add spacing cells when clibboard and/or extended view is enabled
505 for (i = 7; i < me.dragUploader.fileListColumnCount; i++) {
506 $('<td />').text('').appendTo(me.$row);
507 }
508 $('<td />').text(fileInfo.extension.toUpperCase()).appendTo(me.$row);
509 $('<td />').text(fileInfo.date).appendTo(me.$row);
510 $('<td />').text(DragUploader.fileSizeAsString(fileInfo.size)).appendTo(me.$row);
511 var permissions = '';
512 if (fileInfo.permissions.read) {
513 permissions += '<strong class="text-danger">' + TYPO3.lang['permissions.read'] + '</strong>';
514 }
515 if (fileInfo.permissions.write) {
516 permissions += '<strong class="text-danger">' + TYPO3.lang['permissions.write'] + '</strong>';
517 }
518 $('<td />').html(permissions).appendTo(me.$row);
519 $('<td />').text('-').appendTo(me.$row);
520 };
521
522 me.checkAllowedExtensions = function() {
523 if (!me.dragUploader.filesExtensionsAllowed) {
524 return true;
525 }
526 var extension = me.file.name.split('.').pop();
527 var allowed = me.dragUploader.filesExtensionsAllowed.split(',');
528 if ($.inArray(extension.toLowerCase(), allowed) !== -1) {
529 return true;
530 }
531 return false;
532 };
533
534 // position queue item in filelist
535 if ($('tbody tr.upload-queue-item', me.dragUploader.$fileList).length === 0) {
536 me.$row.prependTo($('tbody', me.dragUploader.$fileList));
537 me.$row.addClass('last');
538 } else {
539 me.$row.insertBefore($('tbody tr.upload-queue-item:first', me.dragUploader.$fileList));
540 }
541
542 // set dummy file icon
543 me.$iconCol.html('<span class="t3-icon t3-icon-mimetypes t3-icon-other-other">&nbsp;</span>')
544
545 // check file size
546 if (me.dragUploader.maxFileSize > 0 && me.file.size > me.dragUploader.maxFileSize) {
547 me.updateMessage(TYPO3.lang['file_upload.maxFileSizeExceeded']
548 .replace(/\{0\}/g, me.file.name)
549 .replace(/\{1\}/g, DragUploader.fileSizeAsString(me.dragUploader.maxFileSize)));
550 me.$row.addClass('error');
551
552 // check filename/extension against deny pattern
553 } else if (me.dragUploader.fileDenyPattern && me.file.name.match(me.dragUploader.fileDenyPattern)) {
554 me.updateMessage(TYPO3.lang['file_upload.fileNotAllowed'].replace(/\{0\}/g, me.file.name));
555 me.$row.addClass('error');
556
557 } else if (!me.checkAllowedExtensions()) {
558 me.updateMessage(TYPO3.lang['file_upload.fileExtensionExpected']
559 .replace(/\{0\}/g, me.dragUploader.filesExtensionsAllowed)
560 );
561 me.$row.addClass('error');
562 } else {
563 me.updateMessage('- ' + DragUploader.fileSizeAsString(me.file.size));
564
565 var formData = new FormData();
566 formData.append('file[upload][1][target]', me.dragUploader.target);
567 formData.append('file[upload][1][data]', '1');
568 formData.append('overwriteExistingFiles', me.override);
569 formData.append('redirect', '');
570 formData.append('upload_1', me.file);
571
572 var s = $.extend(true, {}, $.ajaxSettings, {
573 url: TYPO3.settings.ajaxUrls['file_process'],
574 contentType: false,
575 processData: false,
576 data: formData,
577 cache: false,
578 type: 'POST',
579 success: me.uploadSuccess,
580 error: me.uploadError
581 });
582
583 s.xhr = function() {
584 var xhr = $.ajaxSettings.xhr();
585 xhr.upload.addEventListener('progress', me.updateProgress);
586 return xhr;
587 };
588
589 // start upload
590 me.upload = $.ajax(s);
591 }
592 };
593
594 /**
595 * part 2: The main module of this file
596 * - initialize the DragUploader module and register
597 * the jQuery plugin in the jQuery global object
598 * when initializing the DragUploader module
599 */
600 var DragUploader = {};
601
602 DragUploader.options = {};
603
604 DragUploader.fileSizeAsString = function(size) {
605 var string = '',
606 sizeKB = size / 1024;
607
608 if (parseInt(sizeKB) > 1024) {
609 var sizeMB = sizeKB / 1024;
610 string = sizeMB.toFixed(1) + ' MB';
611 } else {
612 string = sizeKB.toFixed(1) + ' KB';
613 }
614 return string;
615 };
616
617 DragUploader.addFileToIrre = function(irre_object, file) {
618 inline.delayedImportElement(
619 irre_object,
620 'sys_file',
621 file.uid,
622 'file'
623 );
624 };
625
626 DragUploader.initialize = function() {
627 var me = this,
628 opts = me.options;
629
630 // register the jQuery plugin "DragUploaderPlugin"
631 $.fn.dragUploader = function(option) {
632 return this.each(function() {
633 var $this = $(this),
634 data = $this.data('DragUploaderPlugin');
635 if (!data) {
636 $this.data('DragUploaderPlugin', (data = new DragUploaderPlugin(this)));
637 }
638 if (typeof option === 'string') {
639 data[option]();
640 }
641 });
642 };
643
644 $(function() {
645 $('.t3js-drag-uploader').dragUploader();
646 });
647 };
648
649
650 /**
651 * part 3: initialize the RequireJS module, require possible post-initialize hooks,
652 * and return the main object
653 */
654 var initialize = function() {
655
656 DragUploader.initialize();
657
658 // load required modules to hook in the post initialize function
659 if (
660 'undefined' !== typeof TYPO3.settings
661 && 'undefined' !== typeof TYPO3.settings.RequireJS
662 && 'undefined' !== typeof TYPO3.settings.RequireJS.PostInitializationModules
663 && 'undefined' !== typeof TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/DragUploader']
664 ) {
665 $.each(TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/DragUploader'], function(pos, moduleName) {
666 require([moduleName]);
667 });
668 }
669
670 // return the object in the global space
671 return DragUploader;
672 };
673
674 // call the main initialize function and execute the hooks
675 return initialize();
676
677 });