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