2 * Uploads file via AJAX.
4 * @author Matthias Schmidt
5 * @copyright 2001-2017 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
) {
15 function Upload(buttonContainerId
, targetId
, options
) {
16 options
= options
|| {};
18 if (options
.className
=== undefined) {
19 throw new Error("Missing class name.");
22 // set default options
23 this._options
= Core
.extend({
24 // name of the PHP action
26 // is true if multiple files can be uploaded at once
28 // name if the upload field
30 // is true if every file from a multi-file selection is uploaded in its own request
31 singleFileRequests
: false,
32 // url for uploading file
33 url
: 'index.php?ajax-upload/&t=' + SECURITY_TOKEN
36 this._options
.url
= Core
.convertLegacyUrl(this._options
.url
);
37 if (this._options
.url
.indexOf('index.php') === 0) {
38 this._options
.url
= WSC_API_URL
+ this._options
.url
;
41 this._buttonContainer
= elById(buttonContainerId
);
42 if (this._buttonContainer
=== null) {
43 throw new Error("Element id '" + buttonContainerId
+ "' is unknown.");
46 this._target
= elById(targetId
);
47 if (targetId
=== null) {
48 throw new Error("Element id '" + targetId
+ "' is unknown.");
50 if (options
.multiple
&& this._target
.nodeName
!== 'UL' && this._target
.nodeName
!== 'OL') {
51 throw new Error("Target element has to be list when allowing upload of multiple files.");
54 this._fileElements
= [];
55 this._internalFileId
= 0;
61 * Creates the upload button.
63 _createButton: function() {
64 this._fileUpload
= elCreate('input');
65 elAttr(this._fileUpload
, 'type', 'file');
66 elAttr(this._fileUpload
, 'name', this._options
.name
);
67 if (this._options
.multiple
) {
68 elAttr(this._fileUpload
, 'multiple', 'true');
70 this._fileUpload
.addEventListener('change', this._upload
.bind(this));
72 this._button
= elCreate('p');
73 this._button
.className
= 'button uploadButton';
75 var span
= elCreate('span');
76 span
.textContent
= Language
.get('wcf.global.button.upload');
77 this._button
.appendChild(span
);
79 DomUtil
.prepend(this._fileUpload
, this._button
);
83 DomChangeListener
.trigger();
87 * Creates the document element for an uploaded file.
89 * @param {File} file uploaded file
91 _createFileElement: function(file
) {
92 var progress
= elCreate('progress');
93 elAttr(progress
, 'max', 100);
95 if (this._target
.nodeName
=== 'OL' || this._target
.nodeName
=== 'UL') {
96 var li
= elCreate('li');
97 li
.innerText
= file
.name
;
98 li
.appendChild(progress
);
100 this._target
.appendChild(li
);
105 var p
= elCreate('p');
106 p
.appendChild(progress
);
108 this._target
.appendChild(p
);
115 * Creates the document elements for uploaded files.
117 * @param {(FileList|Array.<File>)} files uploaded files
119 _createFileElements: function(files
) {
121 var uploadId
= this._fileElements
.length
;
122 this._fileElements
[uploadId
] = [];
124 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
126 var fileElement
= this._createFileElement(file
);
128 if (!fileElement
.classList
.contains('uploadFailed')) {
129 elData(fileElement
, 'filename', file
.name
);
130 elData(fileElement
, 'internal-file-id', this._internalFileId
++);
131 this._fileElements
[uploadId
][i
] = fileElement
;
135 DomChangeListener
.trigger();
144 * Handles a failed file upload.
146 * @param {int} uploadId identifier of a file upload
147 * @param {object<string, *>} data response data
148 * @param {string} responseText response
149 * @param {XMLHttpRequest} xhr request object
150 * @param {object<string, *>} requestOptions options used to send AJAX request
151 * @return {boolean} true if the error message should be shown
153 _failure: function(uploadId
, data
, responseText
, xhr
, requestOptions
) {
159 * Return additional parameters for upload requests.
161 * @return {object<string, *>} additional parameters
163 _getParameters: function() {
168 * Inserts the created button to upload files into the button container.
170 _insertButton: function() {
171 DomUtil
.prepend(this._button
, this._buttonContainer
);
175 * Updates the progress of an upload.
177 * @param {int} uploadId internal upload identifier
178 * @param {XMLHttpRequestProgressEvent} event progress event object
180 _progress: function(uploadId
, event
) {
181 var percentComplete
= Math
.round(event
.loaded
/ event
.total
* 100);
183 for (var i
in this._fileElements
[uploadId
]) {
184 var progress
= elByTag('PROGRESS', this._fileElements
[uploadId
][i
]);
185 if (progress
.length
=== 1) {
186 elAttr(progress
[0], 'value', percentComplete
);
192 * Removes the button to upload files.
194 _removeButton: function() {
195 elRemove(this._button
);
197 DomChangeListener
.trigger();
201 * Handles a successful file upload.
203 * @param {int} uploadId identifier of a file upload
204 * @param {object<string, *>} data response data
205 * @param {string} responseText response
206 * @param {XMLHttpRequest} xhr request object
207 * @param {object<string, *>} requestOptions options used to send AJAX request
209 _success: function(uploadId
, data
, responseText
, xhr
, requestOptions
) {
214 * File input change callback to upload files.
216 * @param {Event} event input change event object
217 * @param {File} file uploaded file
218 * @param {Blob} blob file blob
219 * @return {(int|Array.<int>|null)} identifier(s) for the uploaded files
221 _upload: function(event
, file
, blob
) {
222 // remove failed upload elements first
223 var failedUploads
= DomTraverse
.childrenByClass(this._target
, 'uploadFailed');
224 for (var i
= 0, length
= failedUploads
.length
; i
< length
; i
++) {
225 elRemove(failedUploads
[i
]);
235 var fileExtension
= '';
238 fileExtension
= '.jpg';
242 fileExtension
= '.gif';
246 fileExtension
= '.png';
251 name
: 'pasted-from-clipboard' + fileExtension
255 files
= this._fileUpload
.files
;
259 if (this._options
.singleFileRequests
) {
261 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
262 uploadId
.push(this._uploadFiles([ files
[i
] ], blob
));
266 uploadId
= this._uploadFiles(files
, blob
);
270 // re-create upload button to effectively reset the 'files'
271 // property of the input element
272 this._removeButton();
273 this._createButton();
279 * Sends the request to upload files.
281 * @param {(FileList|Array.<File>)} files uploaded files
282 * @param {Blob} blob file blob
283 * @return {(int|null)} identifier for the uploaded files
285 _uploadFiles: function(files
, blob
) {
286 var uploadId
= this._createFileElements(files
);
288 // no more files left, abort
289 if (!this._fileElements
[uploadId
].length
) {
293 var formData
= new FormData();
294 for (var i
= 0, length
= files
.length
; i
< length
; i
++) {
295 if (this._fileElements
[uploadId
][i
]) {
296 var internalFileId
= elData(this._fileElements
[uploadId
][i
], 'internal-file-id');
299 formData
.append('__files[' + internalFileId
+ ']', blob
, files
[i
].name
);
302 formData
.append('__files[' + internalFileId
+ ']', files
[i
]);
307 formData
.append('actionName', this._options
.action
);
308 formData
.append('className', this._options
.className
);
309 if (this._options
.action
=== 'upload') {
310 formData
.append('interfaceName', 'wcf\\data\\IUploadAction');
313 // recursively append additional parameters to form data
314 var appendFormData = function(parameters
, prefix
) {
315 prefix
= prefix
|| '';
317 for (var name
in parameters
) {
318 if (typeof parameters
[name
] === 'object') {
319 appendFormData(parameters
[name
], prefix
+ '[' + name
+ ']');
322 formData
.append('parameters' + prefix
+ '[' + name
+ ']', parameters
[name
]);
327 appendFormData(this._getParameters());
329 var request
= new AjaxRequest({
332 failure
: this._failure
.bind(this, uploadId
),
334 success
: this._success
.bind(this, uploadId
),
335 uploadProgress
: this._progress
.bind(this, uploadId
),
336 url
: this._options
.url
,
337 withCredentials
: true
339 request
.sendRequest();