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