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;
75 // upload ids that belong to an upload of multiple files at once
76 this._multiFileUploadIds
= [];
82 * Creates the upload button.
84 _createButton: function() {
85 this._fileUpload
= elCreate('input');
86 elAttr(this._fileUpload
, 'type', 'file');
87 elAttr(this._fileUpload
, 'name', this._options
.name
);
88 if (this._options
.multiple
) {
89 elAttr(this._fileUpload
, 'multiple', 'true');
91 this._fileUpload
.addEventListener('change', this._upload
.bind(this));
93 this._button
= elCreate('p');
94 this._button
.className
= 'button uploadButton';
96 var span
= elCreate('span');
97 span
.textContent
= Language
.get('wcf.global.button.upload');
98 this._button
.appendChild(span
);
100 DomUtil
.prepend(this._fileUpload
, this._button
);
102 this._insertButton();
104 DomChangeListener
.trigger();
108 * Creates the document element for an uploaded file.
110 * @param {File} file uploaded file
111 * @return {HTMLElement}
113 _createFileElement: function(file
) {
114 var progress
= elCreate('progress');
115 elAttr(progress
, 'max', 100);
117 if (this._target
.nodeName
=== 'OL' || this._target
.nodeName
=== 'UL') {
118 var li
= elCreate('li');
119 li
.innerText
= file
.name
;
120 li
.appendChild(progress
);
122 this._target
.appendChild(li
);
126 else if (this._target
.nodeName
=== 'TBODY') {
127 return this._createFileTableRow(file
);
130 var p
= elCreate('p');
131 p
.appendChild(progress
);
133 this._target
.appendChild(p
);
140 * Creates the document elements for uploaded files.
142 * @param {(FileList|Array.<File>)} files uploaded files
144 _createFileElements: function(files
) {
146 var uploadId
= this._fileElements
.length
;
147 this._fileElements
[uploadId
] = [];
149 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
151 var fileElement
= this._createFileElement(file
);
153 if (!fileElement
.classList
.contains('uploadFailed')) {
154 elData(fileElement
, 'filename', file
.name
);
155 elData(fileElement
, 'internal-file-id', this._internalFileId
++);
156 this._fileElements
[uploadId
][i
] = fileElement
;
160 DomChangeListener
.trigger();
168 _createFileTableRow: function(file
) {
169 throw new Error("Has to be implemented in subclass.");
173 * Handles a failed file upload.
175 * @param {int} uploadId identifier of a file upload
176 * @param {object<string, *>} data response data
177 * @param {string} responseText response
178 * @param {XMLHttpRequest} xhr request object
179 * @param {object<string, *>} requestOptions options used to send AJAX request
180 * @return {boolean} true if the error message should be shown
182 _failure: function(uploadId
, data
, responseText
, xhr
, requestOptions
) {
188 * Return additional parameters for upload requests.
190 * @return {object<string, *>} additional parameters
192 _getParameters: function() {
197 * Inserts the created button to upload files into the button container.
199 _insertButton: function() {
200 DomUtil
.prepend(this._button
, this._buttonContainer
);
204 * Updates the progress of an upload.
206 * @param {int} uploadId internal upload identifier
207 * @param {XMLHttpRequestProgressEvent} event progress event object
209 _progress: function(uploadId
, event
) {
210 var percentComplete
= Math
.round(event
.loaded
/ event
.total
* 100);
212 for (var i
in this._fileElements
[uploadId
]) {
213 var progress
= elByTag('PROGRESS', this._fileElements
[uploadId
][i
]);
214 if (progress
.length
=== 1) {
215 elAttr(progress
[0], 'value', percentComplete
);
221 * Removes the button to upload files.
223 _removeButton: function() {
224 elRemove(this._button
);
226 DomChangeListener
.trigger();
230 * Handles a successful file upload.
232 * @param {int} uploadId identifier of a file upload
233 * @param {object<string, *>} data response data
234 * @param {string} responseText response
235 * @param {XMLHttpRequest} xhr request object
236 * @param {object<string, *>} requestOptions options used to send AJAX request
238 _success: function(uploadId
, data
, responseText
, xhr
, requestOptions
) {
243 * File input change callback to upload files.
245 * @param {Event} event input change event object
246 * @param {File} file uploaded file
247 * @param {Blob} blob file blob
248 * @return {(int|Array.<int>|null)} identifier(s) for the uploaded files
250 _upload: function(event
, file
, blob
) {
251 // remove failed upload elements first
252 var failedUploads
= DomTraverse
.childrenByClass(this._target
, 'uploadFailed');
253 for (var i
= 0, length
= failedUploads
.length
; i
< length
; i
++) {
254 elRemove(failedUploads
[i
]);
264 var fileExtension
= '';
267 fileExtension
= '.jpg';
271 fileExtension
= '.gif';
275 fileExtension
= '.png';
280 name
: 'pasted-from-clipboard' + fileExtension
284 files
= this._fileUpload
.files
;
288 if (this._options
.singleFileRequests
) {
290 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
291 var localUploadId
= this._uploadFiles([ files
[i
] ], blob
);
293 if (files
.length
!== 1) {
294 this._multiFileUploadIds
.push(localUploadId
)
296 uploadId
.push(localUploadId
);
300 uploadId
= this._uploadFiles(files
, blob
);
304 // re-create upload button to effectively reset the 'files'
305 // property of the input element
306 this._removeButton();
307 this._createButton();
313 * Sends the request to upload files.
315 * @param {(FileList|Array.<File>)} files uploaded files
316 * @param {Blob} blob file blob
317 * @return {(int|null)} identifier for the uploaded files
319 _uploadFiles: function(files
, blob
) {
320 var uploadId
= this._createFileElements(files
);
322 // no more files left, abort
323 if (!this._fileElements
[uploadId
].length
) {
327 var formData
= new FormData();
328 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
329 if (this._fileElements
[uploadId
][i
]) {
330 var internalFileId
= elData(this._fileElements
[uploadId
][i
], 'internal-file-id');
333 formData
.append('__files[' + internalFileId
+ ']', blob
, files
[i
].name
);
336 formData
.append('__files[' + internalFileId
+ ']', files
[i
]);
341 formData
.append('actionName', this._options
.action
);
342 formData
.append('className', this._options
.className
);
343 if (this._options
.action
=== 'upload') {
344 formData
.append('interfaceName', 'wcf\\data\\IUploadAction');
347 // recursively append additional parameters to form data
348 var appendFormData = function(parameters
, prefix
) {
349 prefix
= prefix
|| '';
351 for (var name
in parameters
) {
352 if (typeof parameters
[name
] === 'object') {
353 appendFormData(parameters
[name
], prefix
+ '[' + name
+ ']');
356 formData
.append('parameters' + prefix
+ '[' + name
+ ']', parameters
[name
]);
361 appendFormData(this._getParameters());
363 var request
= new AjaxRequest({
366 failure
: this._failure
.bind(this, uploadId
),
368 success
: this._success
.bind(this, uploadId
),
369 uploadProgress
: this._progress
.bind(this, uploadId
),
370 url
: this._options
.url
,
371 withCredentials
: true
373 request
.sendRequest();
379 * Returns true if there are any pending uploads handled by this
385 hasPendingUploads: function() {
386 for (var uploadId
in this._fileElements
) {
387 for (var i
in this._fileElements
[uploadId
]) {
388 var progress
= elByTag('PROGRESS', this._fileElements
[uploadId
][i
]);
389 if (progress
.length
=== 1) {
399 * Uploads the given file blob.
401 * @param {Blob} blob file blob
402 * @return {int} identifier for the uploaded file
404 uploadBlob: function(blob
) {
405 return this._upload(null, null, blob
);
409 * Uploads the given file.
411 * @param {File} file uploaded file
412 * @return {int} identifier(s) for the uploaded file
414 uploadFile: function(file
) {
415 return this._upload(null, file
);