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