2 * Uploads file via AJAX.
4 * @author Matthias Schmidt
5 * @copyright 2001-2018 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Upload
9 define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse'], function(AjaxRequest
, Core
, DomChangeListener
, Language
, DomUtil
, DomTraverse
) {
12 if (!COMPILER_TARGET_DEFAULT
) {
13 var Fake = function() {};
15 _createButton: function() {},
16 _createFileElement: function() {},
17 _createFileElements: function() {},
18 _failure: function() {},
19 _getParameters: function() {},
20 _insertButton: function() {},
21 _progress: function() {},
22 _removeButton: function() {},
23 _success: function() {},
24 _upload: function() {},
25 _uploadFiles: function() {}
33 function Upload(buttonContainerId
, targetId
, options
) {
34 options
= options
|| {};
36 if (options
.className
=== undefined) {
37 throw new Error("Missing class name.");
40 // set default options
41 this._options
= Core
.extend({
42 // name of the PHP action
44 // is true if multiple files can be uploaded at once
46 // name if the upload field
48 // is true if every file from a multi-file selection is uploaded in its own request
49 singleFileRequests
: false,
50 // url for uploading file
51 url
: 'index.php?ajax-upload/&t=' + SECURITY_TOKEN
54 this._options
.url
= Core
.convertLegacyUrl(this._options
.url
);
55 if (this._options
.url
.indexOf('index.php') === 0) {
56 this._options
.url
= WSC_API_URL
+ this._options
.url
;
59 this._buttonContainer
= elById(buttonContainerId
);
60 if (this._buttonContainer
=== null) {
61 throw new Error("Element id '" + buttonContainerId
+ "' is unknown.");
64 this._target
= elById(targetId
);
65 if (targetId
=== null) {
66 throw new Error("Element id '" + targetId
+ "' is unknown.");
68 if (options
.multiple
&& this._target
.nodeName
!== 'UL' && this._target
.nodeName
!== 'OL' && this._target
.nodeName
!== 'TBODY') {
69 throw new Error("Target element has to be list or table body if uploading multiple files is supported.");
72 this._fileElements
= [];
73 this._internalFileId
= 0;
79 * Creates the upload button.
81 _createButton: function() {
82 this._fileUpload
= elCreate('input');
83 elAttr(this._fileUpload
, 'type', 'file');
84 elAttr(this._fileUpload
, 'name', this._options
.name
);
85 if (this._options
.multiple
) {
86 elAttr(this._fileUpload
, 'multiple', 'true');
88 this._fileUpload
.addEventListener('change', this._upload
.bind(this));
90 this._button
= elCreate('p');
91 this._button
.className
= 'button uploadButton';
93 var span
= elCreate('span');
94 span
.textContent
= Language
.get('wcf.global.button.upload');
95 this._button
.appendChild(span
);
97 DomUtil
.prepend(this._fileUpload
, this._button
);
101 DomChangeListener
.trigger();
105 * Creates the document element for an uploaded file.
107 * @param {File} file uploaded file
108 * @return {HTMLElement}
110 _createFileElement: function(file
) {
111 var progress
= elCreate('progress');
112 elAttr(progress
, 'max', 100);
114 if (this._target
.nodeName
=== 'OL' || this._target
.nodeName
=== 'UL') {
115 var li
= elCreate('li');
116 li
.innerText
= file
.name
;
117 li
.appendChild(progress
);
119 this._target
.appendChild(li
);
123 else if (this._target
.nodeName
=== 'TBODY') {
124 return this._createFileTableRow(file
);
127 var p
= elCreate('p');
128 p
.appendChild(progress
);
130 this._target
.appendChild(p
);
137 * Creates the document elements for uploaded files.
139 * @param {(FileList|Array.<File>)} files uploaded files
141 _createFileElements: function(files
) {
143 var uploadId
= this._fileElements
.length
;
144 this._fileElements
[uploadId
] = [];
146 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
148 var fileElement
= this._createFileElement(file
);
150 if (!fileElement
.classList
.contains('uploadFailed')) {
151 elData(fileElement
, 'filename', file
.name
);
152 elData(fileElement
, 'internal-file-id', this._internalFileId
++);
153 this._fileElements
[uploadId
][i
] = fileElement
;
157 DomChangeListener
.trigger();
165 _createFileTableRow: function(file
) {
166 throw new Error("Has to be implemented in subclass.");
170 * Handles a failed file upload.
172 * @param {int} uploadId identifier of a file upload
173 * @param {object<string, *>} data response data
174 * @param {string} responseText response
175 * @param {XMLHttpRequest} xhr request object
176 * @param {object<string, *>} requestOptions options used to send AJAX request
177 * @return {boolean} true if the error message should be shown
179 _failure: function(uploadId
, data
, responseText
, xhr
, requestOptions
) {
185 * Return additional parameters for upload requests.
187 * @return {object<string, *>} additional parameters
189 _getParameters: function() {
194 * Inserts the created button to upload files into the button container.
196 _insertButton: function() {
197 DomUtil
.prepend(this._button
, this._buttonContainer
);
201 * Updates the progress of an upload.
203 * @param {int} uploadId internal upload identifier
204 * @param {XMLHttpRequestProgressEvent} event progress event object
206 _progress: function(uploadId
, event
) {
207 var percentComplete
= Math
.round(event
.loaded
/ event
.total
* 100);
209 for (var i
in this._fileElements
[uploadId
]) {
210 var progress
= elByTag('PROGRESS', this._fileElements
[uploadId
][i
]);
211 if (progress
.length
=== 1) {
212 elAttr(progress
[0], 'value', percentComplete
);
218 * Removes the button to upload files.
220 _removeButton: function() {
221 elRemove(this._button
);
223 DomChangeListener
.trigger();
227 * Handles a successful file upload.
229 * @param {int} uploadId identifier of a file upload
230 * @param {object<string, *>} data response data
231 * @param {string} responseText response
232 * @param {XMLHttpRequest} xhr request object
233 * @param {object<string, *>} requestOptions options used to send AJAX request
235 _success: function(uploadId
, data
, responseText
, xhr
, requestOptions
) {
240 * File input change callback to upload files.
242 * @param {Event} event input change event object
243 * @param {File} file uploaded file
244 * @param {Blob} blob file blob
245 * @return {(int|Array.<int>|null)} identifier(s) for the uploaded files
247 _upload: function(event
, file
, blob
) {
248 // remove failed upload elements first
249 var failedUploads
= DomTraverse
.childrenByClass(this._target
, 'uploadFailed');
250 for (var i
= 0, length
= failedUploads
.length
; i
< length
; i
++) {
251 elRemove(failedUploads
[i
]);
261 var fileExtension
= '';
264 fileExtension
= '.jpg';
268 fileExtension
= '.gif';
272 fileExtension
= '.png';
277 name
: 'pasted-from-clipboard' + fileExtension
281 files
= this._fileUpload
.files
;
285 if (this._options
.singleFileRequests
) {
287 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
288 uploadId
.push(this._uploadFiles([ files
[i
] ], blob
));
292 uploadId
= this._uploadFiles(files
, blob
);
296 // re-create upload button to effectively reset the 'files'
297 // property of the input element
298 this._removeButton();
299 this._createButton();
305 * Sends the request to upload files.
307 * @param {(FileList|Array.<File>)} files uploaded files
308 * @param {Blob} blob file blob
309 * @return {(int|null)} identifier for the uploaded files
311 _uploadFiles: function(files
, blob
) {
312 var uploadId
= this._createFileElements(files
);
314 // no more files left, abort
315 if (!this._fileElements
[uploadId
].length
) {
319 var formData
= new FormData();
320 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
321 if (this._fileElements
[uploadId
][i
]) {
322 var internalFileId
= elData(this._fileElements
[uploadId
][i
], 'internal-file-id');
325 formData
.append('__files[' + internalFileId
+ ']', blob
, files
[i
].name
);
328 formData
.append('__files[' + internalFileId
+ ']', files
[i
]);
333 formData
.append('actionName', this._options
.action
);
334 formData
.append('className', this._options
.className
);
335 if (this._options
.action
=== 'upload') {
336 formData
.append('interfaceName', 'wcf\\data\\IUploadAction');
339 // recursively append additional parameters to form data
340 var appendFormData = function(parameters
, prefix
) {
341 prefix
= prefix
|| '';
343 for (var name
in parameters
) {
344 if (typeof parameters
[name
] === 'object') {
345 appendFormData(parameters
[name
], prefix
+ '[' + name
+ ']');
348 formData
.append('parameters' + prefix
+ '[' + name
+ ']', parameters
[name
]);
353 appendFormData(this._getParameters());
355 var request
= new AjaxRequest({
358 failure
: this._failure
.bind(this, uploadId
),
360 success
: this._success
.bind(this, uploadId
),
361 uploadProgress
: this._progress
.bind(this, uploadId
),
362 url
: this._options
.url
,
363 withCredentials
: true
365 request
.sendRequest();
371 * Uploads the given file blob.
373 * @param {Blob} blob file blob
374 * @return {int} identifier for the uploaded file
376 uploadBlob: function(blob
) {
377 return this._upload(null, null, blob
);
381 * Uploads the given file.
383 * @param {File} file uploaded file
384 * @return {int} identifier(s) for the uploaded file
386 uploadFile: function(file
) {
387 return this._upload(null, file
);