/* * This file is part of the TYPO3 CMS project. * * It is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, either version 2 * of the License, or any later version. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. * * The TYPO3 project - inspiring people to share! */ /** * Module: TYPO3/CMS/Backend/DragUploader */ import {SeverityEnum} from './Enum/Severity'; import * as $ from 'jquery'; import moment = require('moment'); import NProgress = require('nprogress'); import Modal = require('./Modal'); import Notification = require('./Notification'); import {MessageUtility} from './Utility/MessageUtility'; /** * Possible actions for conflicts w/ existing files */ enum Action { OVERRIDE = 'replace', RENAME = 'rename', SKIP = 'cancel', USE_EXISTING = 'useExisting', } /** * Properties of a file as returned from the AJAX action; essential, this is a serialized instance of * \TYPO3\CMS\Core\Resource\File plus some extra properties (see FileController::flattenResultDataValue()) */ interface UploadedFile { name: string; id: number; uid: number; icon: string; extension: string; permissions: { read: boolean; write: boolean }; size: number; // formatted as ddmmyy date: string; mtime: Date; thumbUrl: string; type: string; } interface InternalFile extends File { lastModified: any; } interface DragUploaderOptions { /** * CSS selector for the element where generated messages are inserted. (required) */ outputSelector: string; /** * Color of the message text. (optional) */ outputColor?: string; } class DragUploaderPlugin { public irreObjectUid: number; public $fileList: JQuery; public fileListColumnCount: number; public filesExtensionsAllowed: string; public fileDenyPattern: RegExp | null; public maxFileSize: number; public $trigger: JQuery; public target: string; /** * Array of files which are asked for being overridden */ private askForOverride: Array<{ original: UploadedFile, uploaded: InternalFile, action: Action }> = []; private percentagePerFile: number = 1; private $body: JQuery; private $element: JQuery; private $dropzone: JQuery; private $dropzoneMask: JQuery; private fileInput: HTMLInputElement; private browserCapabilities: { fileReader: boolean; DnD: boolean; Progress: boolean }; private dropZoneInsertBefore: boolean; private queueLength: number; constructor(element: HTMLElement) { this.$body = $('body'); this.$element = $(element); const hasTrigger = this.$element.data('dropzoneTrigger') !== undefined; this.$trigger = $(this.$element.data('dropzoneTrigger')); this.$dropzone = $('
').addClass('dropzone').hide(); this.irreObjectUid = this.$element.data('fileIrreObject'); const dropZoneEscapedTarget = this.$element.data('dropzoneTarget'); if (this.irreObjectUid && this.$element.nextAll(dropZoneEscapedTarget).length !== 0) { this.dropZoneInsertBefore = true; this.$dropzone.insertBefore(dropZoneEscapedTarget); } else { this.dropZoneInsertBefore = false; this.$dropzone.insertAfter(dropZoneEscapedTarget); } this.$dropzoneMask = $('
').addClass('dropzone-mask').appendTo(this.$dropzone); this.fileInput = document.createElement('input'); this.fileInput.setAttribute('type', 'file'); this.fileInput.setAttribute('multiple', 'multiple'); this.fileInput.setAttribute('name', 'files[]'); this.fileInput.classList.add('upload-file-picker'); this.$body.append(this.fileInput); this.$fileList = $(this.$element.data('progress-container')); this.fileListColumnCount = $('thead tr:first th', this.$fileList).length; this.filesExtensionsAllowed = this.$element.data('file-allowed'); this.fileDenyPattern = this.$element.data('file-deny-pattern') ? new RegExp(this.$element.data('file-deny-pattern'), 'i') : null; this.maxFileSize = parseInt(this.$element.data('max-file-size'), 10); this.target = this.$element.data('target-folder'); this.browserCapabilities = { fileReader: typeof FileReader !== 'undefined', DnD: 'draggable' in document.createElement('span'), Progress: 'upload' in new XMLHttpRequest, }; if (!this.browserCapabilities.DnD) { console.warn('Browser has no Drag and drop capabilities; cannot initialize DragUploader'); return; } this.$body.on('dragover', this.dragFileIntoDocument); this.$body.on('dragend', this.dragAborted); this.$body.on('drop', this.ignoreDrop); this.$dropzone.on('dragenter', this.fileInDropzone); this.$dropzoneMask.on('dragenter', this.fileInDropzone); this.$dropzoneMask.on('dragleave', this.fileOutOfDropzone); this.$dropzoneMask.on('drop', (ev: JQueryEventObject) => this.handleDrop(>ev)); this.$dropzone.prepend( '
' + '
' + '
' + '
' + '
' + '

' + TYPO3.lang['file_upload.dropzonehint.title'] + '

' + '

' + TYPO3.lang['file_upload.dropzonehint.message'] + '

' + '
' + '
', ).click(() => { this.fileInput.click(); }); $('').addClass('dropzone-close').click(this.hideDropzone).appendTo(this.$dropzone); // no filelist then create own progress table if (this.$fileList.length === 0) { this.$fileList = $('') .attr('id', 'typo3-filelist') .addClass('table table-striped table-hover upload-queue') .html('').hide(); if (this.dropZoneInsertBefore) { this.$fileList.insertAfter(this.$dropzone); } else { this.$fileList.insertBefore(this.$dropzone); } this.fileListColumnCount = 7; } this.fileInput.addEventListener('change', () => { this.processFiles(Array.apply(null, this.fileInput.files)); }); this.bindUploadButton(hasTrigger === true ? this.$trigger : this.$element); } public showDropzone(): void { this.$dropzone.show(); } /** * * @param {Event} event */ public hideDropzone(event: Event): void { event.stopPropagation(); event.preventDefault(); this.$dropzone.hide(); } /** * @param {Event} event * @returns {boolean} */ public dragFileIntoDocument = (event: Event): boolean => { event.stopPropagation(); event.preventDefault(); $(event.currentTarget).addClass('drop-in-progress'); this.showDropzone(); return false; } /** * * @param {Event} event * @returns {Boolean} */ public dragAborted = (event: Event): boolean => { event.stopPropagation(); event.preventDefault(); $(event.currentTarget).removeClass('drop-in-progress'); return false; } public ignoreDrop = (event: Event): boolean => { // stops the browser from redirecting. event.stopPropagation(); event.preventDefault(); this.dragAborted(event); return false; } public handleDrop = (event: JQueryTypedEvent): void => { this.ignoreDrop(event); this.processFiles(event.originalEvent.dataTransfer.files); this.$dropzone.removeClass('drop-status-ok'); } /** * @param {FileList} files */ public processFiles(files: FileList): void { this.queueLength = files.length; if (!this.$fileList.is(':visible')) { this.$fileList.show(); } NProgress.start(); this.percentagePerFile = 1 / files.length; // Check for each file if is already exist before adding it to the queue const ajaxCalls: JQueryXHR[] = []; $.each(files, (i: string, file) => { ajaxCalls[parseInt(i, 10)] = $.ajax({ url: TYPO3.settings.ajaxUrls.file_exists, data: { fileName: file.name, fileTarget: this.target, }, cache: false, success: (response: any) => { const fileExists = typeof response.uid !== 'undefined'; if (fileExists) { this.askForOverride.push({ original: response, uploaded: file, action: this.irreObjectUid ? Action.USE_EXISTING : Action.SKIP, }); NProgress.inc(this.percentagePerFile); } else { // Unused var _ is necessary as "no-unused-expression" is active const _ = new FileQueueItem(this, file, Action.SKIP); } }, }); }); $.when.apply($, ajaxCalls).done(() => { this.drawOverrideModal(); NProgress.done(); }); this.fileInput.value = ''; } public fileInDropzone = (): void => { this.$dropzone.addClass('drop-status-ok'); } public fileOutOfDropzone = (): void => { this.$dropzone.removeClass('drop-status-ok'); } /** * Bind file picker to default upload button * * @param {Object} button */ public bindUploadButton(button: JQuery): void { button.click((event: Event) => { event.preventDefault(); this.fileInput.click(); this.showDropzone(); }); } /** * Decrements the queue and renders a flash message if queue is empty */ public decrementQueueLength(): void { if (this.queueLength > 0) { this.queueLength--; if (this.queueLength === 0) { $.ajax({ url: TYPO3.settings.ajaxUrls.flashmessages_render, cache: false, success: (data) => { $.each(data, (index: number, flashMessage: { title: string, message: string, severity: number }) => { Notification.showMessage(flashMessage.title, flashMessage.message, flashMessage.severity); }); }, }); } } } /** * Renders the modal for existing files */ public drawOverrideModal(): void { const amountOfItems = Object.keys(this.askForOverride).length; if (amountOfItems === 0) { return; } const $modalContent = $('
').append( $('

').text(TYPO3.lang['file_upload.existingfiles.description']), $('

', {class: 'table'}).append( $('').append( $('').append( $('').append( $('').append($record); } const $modal = Modal.confirm( TYPO3.lang['file_upload.existingfiles.title'], $modalContent, SeverityEnum.warning, [ { text: $(this).data('button-close-text') || TYPO3.lang['file_upload.button.cancel'] || 'Cancel', active: true, btnClass: 'btn-default', name: 'cancel', }, { text: $(this).data('button-ok-text') || TYPO3.lang['file_upload.button.continue'] || 'Continue with selected actions', btnClass: 'btn-warning', name: 'continue', }, ], ['modal-inner-scroll'], ); $modal.find('.modal-dialog').addClass('modal-lg'); $modal.find('.modal-footer').prepend( $('').addClass('form-inline').append( $('').addClass('upload-queue-item uploading'); this.$iconCol = $('
'), $('').text(TYPO3.lang['file_upload.header.originalFile']), $('').text(TYPO3.lang['file_upload.header.uploadedFile']), $('').text(TYPO3.lang['file_upload.header.action']), ), ), ), ); for (let i = 0; i < amountOfItems; ++i) { const $record = $('
').append( (this.askForOverride[i].original.thumbUrl !== '' ? $('', {src: this.askForOverride[i].original.thumbUrl, height: 40}) : $(this.askForOverride[i].original.icon) ), ), $('').html( this.askForOverride[i].original.name + ' (' + (DragUploader.fileSizeAsString(this.askForOverride[i].original.size)) + ')' + '
' + moment.unix(this.askForOverride[i].original.mtime).format('YYYY-MM-DD HH:mm'), ), $('
').html( this.askForOverride[i].uploaded.name + ' (' + (DragUploader.fileSizeAsString(this.askForOverride[i].uploaded.size)) + ')' + '
' + moment( this.askForOverride[i].uploaded.lastModified ? this.askForOverride[i].uploaded.lastModified : this.askForOverride[i].uploaded.lastModifiedDate, ).format('YYYY-MM-DD HH:mm'), ), $('
').append( $('
').addClass('col-icon').appendTo(this.$row); this.$fileName = $('').text(file.name).appendTo(this.$row); this.$progress = $('').attr('colspan', this.dragUploader.fileListColumnCount - 2).appendTo(this.$row); this.$progressContainer = $('
').addClass('upload-queue-progress').appendTo(this.$progress); this.$progressBar = $('
').addClass('upload-queue-progress-bar').appendTo(this.$progressContainer); this.$progressPercentage = $('').addClass('upload-queue-progress-percentage').appendTo(this.$progressContainer); this.$progressMessage = $('').addClass('upload-queue-progress-message').appendTo(this.$progressContainer); // position queue item in filelist if ($('tbody tr.upload-queue-item', this.dragUploader.$fileList).length === 0) { this.$row.prependTo($('tbody', this.dragUploader.$fileList)); this.$row.addClass('last'); } else { this.$row.insertBefore($('tbody tr.upload-queue-item:first', this.dragUploader.$fileList)); } // set dummy file icon this.$iconCol.html(' '); // check file size if (this.dragUploader.maxFileSize > 0 && this.file.size > this.dragUploader.maxFileSize) { this.updateMessage(TYPO3.lang['file_upload.maxFileSizeExceeded'] .replace(/\{0\}/g, this.file.name) .replace(/\{1\}/g, DragUploader.fileSizeAsString(this.dragUploader.maxFileSize))); this.$row.addClass('error'); // check filename/extension against deny pattern } else if (this.dragUploader.fileDenyPattern && this.file.name.match(this.dragUploader.fileDenyPattern)) { this.updateMessage(TYPO3.lang['file_upload.fileNotAllowed'].replace(/\{0\}/g, this.file.name)); this.$row.addClass('error'); } else if (!this.checkAllowedExtensions()) { this.updateMessage(TYPO3.lang['file_upload.fileExtensionExpected'] .replace(/\{0\}/g, this.dragUploader.filesExtensionsAllowed), ); this.$row.addClass('error'); } else { this.updateMessage('- ' + DragUploader.fileSizeAsString(this.file.size)); const formData = new FormData(); formData.append('data[upload][1][target]', this.dragUploader.target); formData.append('data[upload][1][data]', '1'); formData.append('overwriteExistingFiles', this.override); formData.append('redirect', ''); formData.append('upload_1', this.file); const s = $.extend(true, {}, $.ajaxSettings, { url: TYPO3.settings.ajaxUrls.file_process, contentType: false, processData: false, data: formData, cache: false, type: 'POST', success: (data: { upload?: UploadedFile[] }) => this.uploadSuccess(data), error: (response: XMLHttpRequest) => this.uploadError(response), }); s.xhr = () => { const xhr = $.ajaxSettings.xhr(); xhr.upload.addEventListener('progress', (e: ProgressEvent) => this.updateProgress(e)); return xhr; }; // start upload this.upload = $.ajax(s); } } /** * @param {string} message */ public updateMessage(message: string): void { this.$progressMessage.text(message); } /** * Remove the progress bar */ public removeProgress(): void { if (this.$progress) { this.$progress.remove(); } } public uploadStart(): void { this.$progressPercentage.text('(0%)'); this.$progressBar.width('1%'); this.dragUploader.$trigger.trigger('uploadStart', [this]); } /** * @param {XMLHttpRequest} response */ public uploadError(response: XMLHttpRequest): void { this.updateMessage(TYPO3.lang['file_upload.uploadFailed'].replace(/\{0\}/g, this.file.name)); const error = $(response.responseText); if (error.is('t3err')) { this.$progressPercentage.text(error.text()); } else { this.$progressPercentage.text('(' + response.statusText + ')'); } this.$row.addClass('error'); this.dragUploader.decrementQueueLength(); this.dragUploader.$trigger.trigger('uploadError', [this, response]); } /** * @param {ProgressEvent} event */ public updateProgress(event: ProgressEvent): void { const percentage = Math.round((event.loaded / event.total) * 100) + '%'; this.$progressBar.outerWidth(percentage); this.$progressPercentage.text(percentage); this.dragUploader.$trigger.trigger('updateProgress', [this, percentage, event]); } /** * @param {{upload?: UploadedFile[]}} data */ public uploadSuccess(data: { upload?: UploadedFile[] }): void { if (data.upload) { this.dragUploader.decrementQueueLength(); this.$row.removeClass('uploading'); this.$fileName.text(data.upload[0].name); this.$progressPercentage.text(''); this.$progressMessage.text('100%'); this.$progressBar.outerWidth('100%'); // replace file icon if (data.upload[0].icon) { this.$iconCol .html( '' + data.upload[0].icon + ' ', ); } if (this.dragUploader.irreObjectUid) { DragUploader.addFileToIrre( this.dragUploader.irreObjectUid, data.upload[0], ); setTimeout( () => { this.$row.remove(); if ($('tr', this.dragUploader.$fileList).length === 0) { this.dragUploader.$fileList.hide(); this.dragUploader.$trigger.trigger('uploadSuccess', [this, data]); } }, 3000); } else { setTimeout( () => { this.showFileInfo(data.upload[0]); this.dragUploader.$trigger.trigger('uploadSuccess', [this, data]); }, 3000); } } } /** * @param {UploadedFile} fileInfo */ public showFileInfo(fileInfo: UploadedFile): void { this.removeProgress(); // add spacing cells when clibboard and/or extended view is enabled for (let i = 7; i < this.dragUploader.fileListColumnCount; i++) { $('
').text('').appendTo(this.$row); } $('').text(fileInfo.extension.toUpperCase()).appendTo(this.$row); $('').text(fileInfo.date).appendTo(this.$row); $('').text(DragUploader.fileSizeAsString(fileInfo.size)).appendTo(this.$row); let permissions = ''; if (fileInfo.permissions.read) { permissions += '' + TYPO3.lang['permissions.read'] + ''; } if (fileInfo.permissions.write) { permissions += '' + TYPO3.lang['permissions.write'] + ''; } $('').html(permissions).appendTo(this.$row); $('').text('-').appendTo(this.$row); } public checkAllowedExtensions(): boolean { if (!this.dragUploader.filesExtensionsAllowed) { return true; } const extension = this.file.name.split('.').pop(); const allowed = this.dragUploader.filesExtensionsAllowed.split(','); return $.inArray(extension.toLowerCase(), allowed) !== -1; } } class DragUploader { public fileListColumnCount: number; public filesExtensionsAllowed: string; public fileDenyPattern: string; private static options: DragUploaderOptions; public static fileSizeAsString(size: number): string { const sizeKB: number = size / 1024; let str = ''; if (sizeKB > 1024) { str = (sizeKB / 1024).toFixed(1) + ' MB'; } else { str = sizeKB.toFixed(1) + ' KB'; } return str; } /** * @param {number} irre_object * @param {UploadedFile} file */ public static addFileToIrre(irre_object: number, file: UploadedFile): void { const message = { objectGroup: irre_object, table: 'sys_file', uid: file.uid, }; MessageUtility.send(message); } public static init(): void { const me = this; const opts = me.options; // register the jQuery plugin "DragUploaderPlugin" $.fn.extend({ dragUploader: function (options?: DragUploaderOptions | string): JQuery { return this.each((index: number, elem: HTMLElement): void => { const $this = $(elem); let data = $this.data('DragUploaderPlugin'); if (!data) { $this.data('DragUploaderPlugin', (data = new DragUploaderPlugin(elem))); } if (typeof options === 'string') { data[options](); } }); }, }); $(() => { $('.t3js-drag-uploader').dragUploader(opts); }); } } /** * Function to apply the example plugin to the selected elements of a jQuery result. */ interface DragUploaderFunction { /** * Apply the example plugin to the elements selected in the jQuery result. * * @param options Options to use for this application of the example plugin. * @returns jQuery result. */ (options: DragUploaderOptions): JQuery; } export const initialize = function (): void { DragUploader.init(); // load required modules to hook in the post initialize function if ( 'undefined' !== typeof TYPO3.settings && 'undefined' !== typeof TYPO3.settings.RequireJS && 'undefined' !== typeof TYPO3.settings.RequireJS.PostInitializationModules && 'undefined' !== typeof TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/DragUploader'] ) { $.each( TYPO3.settings.RequireJS.PostInitializationModules['TYPO3/CMS/Backend/DragUploader'], (pos, moduleName) => { require([moduleName]); }, ); } }; initialize();