Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Upload.js
1 /**
2 * Uploads file via AJAX.
3 *
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
8 */
9 define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse'], function(AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse) {
10 "use strict";
11
12 if (!COMPILER_TARGET_DEFAULT) {
13 var Fake = function() {};
14 Fake.prototype = {
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() {}
26 };
27 return Fake;
28 }
29
30 /**
31 * @constructor
32 */
33 function Upload(buttonContainerId, targetId, options) {
34 options = options || {};
35
36 if (options.className === undefined) {
37 throw new Error("Missing class name.");
38 }
39
40 // set default options
41 this._options = Core.extend({
42 // name of the PHP action
43 action: 'upload',
44 // is true if multiple files can be uploaded at once
45 multiple: false,
46 // name if the upload field
47 name: '__files[]',
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
52 }, options);
53
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;
57 }
58
59 this._buttonContainer = elById(buttonContainerId);
60 if (this._buttonContainer === null) {
61 throw new Error("Element id '" + buttonContainerId + "' is unknown.");
62 }
63
64 this._target = elById(targetId);
65 if (targetId === null) {
66 throw new Error("Element id '" + targetId + "' is unknown.");
67 }
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.");
70 }
71
72 this._fileElements = [];
73 this._internalFileId = 0;
74
75 this._createButton();
76 }
77 Upload.prototype = {
78 /**
79 * Creates the upload button.
80 */
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');
87 }
88 this._fileUpload.addEventListener('change', this._upload.bind(this));
89
90 this._button = elCreate('p');
91 this._button.className = 'button uploadButton';
92
93 var span = elCreate('span');
94 span.textContent = Language.get('wcf.global.button.upload');
95 this._button.appendChild(span);
96
97 DomUtil.prepend(this._fileUpload, this._button);
98
99 this._insertButton();
100
101 DomChangeListener.trigger();
102 },
103
104 /**
105 * Creates the document element for an uploaded file.
106 *
107 * @param {File} file uploaded file
108 * @return {HTMLElement}
109 */
110 _createFileElement: function(file) {
111 var progress = elCreate('progress');
112 elAttr(progress, 'max', 100);
113
114 if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
115 var li = elCreate('li');
116 li.innerText = file.name;
117 li.appendChild(progress);
118
119 this._target.appendChild(li);
120
121 return li;
122 }
123 else if (this._target.nodeName === 'TBODY') {
124 return this._createFileTableRow(file);
125 }
126 else {
127 var p = elCreate('p');
128 p.appendChild(progress);
129
130 this._target.appendChild(p);
131
132 return p;
133 }
134 },
135
136 /**
137 * Creates the document elements for uploaded files.
138 *
139 * @param {(FileList|Array.<File>)} files uploaded files
140 */
141 _createFileElements: function(files) {
142 if (files.length) {
143 var uploadId = this._fileElements.length;
144 this._fileElements[uploadId] = [];
145
146 for (var i = 0, length = files.length; i < length; i++) {
147 var file = files[i];
148 var fileElement = this._createFileElement(file);
149
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;
154 }
155 }
156
157 DomChangeListener.trigger();
158
159 return uploadId;
160 }
161
162 return null;
163 },
164
165 _createFileTableRow: function(file) {
166 throw new Error("Has to be implemented in subclass.");
167 },
168
169 /**
170 * Handles a failed file upload.
171 *
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
178 */
179 _failure: function(uploadId, data, responseText, xhr, requestOptions) {
180 // does nothing
181 return true;
182 },
183
184 /**
185 * Return additional parameters for upload requests.
186 *
187 * @return {object<string, *>} additional parameters
188 */
189 _getParameters: function() {
190 return {};
191 },
192
193 /**
194 * Inserts the created button to upload files into the button container.
195 */
196 _insertButton: function() {
197 DomUtil.prepend(this._button, this._buttonContainer);
198 },
199
200 /**
201 * Updates the progress of an upload.
202 *
203 * @param {int} uploadId internal upload identifier
204 * @param {XMLHttpRequestProgressEvent} event progress event object
205 */
206 _progress: function(uploadId, event) {
207 var percentComplete = Math.round(event.loaded / event.total * 100);
208
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);
213 }
214 }
215 },
216
217 /**
218 * Removes the button to upload files.
219 */
220 _removeButton: function() {
221 elRemove(this._button);
222
223 DomChangeListener.trigger();
224 },
225
226 /**
227 * Handles a successful file upload.
228 *
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
234 */
235 _success: function(uploadId, data, responseText, xhr, requestOptions) {
236 // does nothing
237 },
238
239 /**
240 * File input change callback to upload files.
241 *
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
246 */
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]);
252 }
253
254 var uploadId = null;
255
256 var files = [];
257 if (file) {
258 files.push(file);
259 }
260 else if (blob) {
261 var fileExtension = '';
262 switch (blob.type) {
263 case 'image/jpeg':
264 fileExtension = '.jpg';
265 break;
266
267 case 'image/gif':
268 fileExtension = '.gif';
269 break;
270
271 case 'image/png':
272 fileExtension = '.png';
273 break;
274 }
275
276 files.push({
277 name: 'pasted-from-clipboard' + fileExtension
278 });
279 }
280 else {
281 files = this._fileUpload.files;
282 }
283
284 if (files.length) {
285 if (this._options.singleFileRequests) {
286 uploadId = [];
287 for (var i = 0, length = files.length; i < length; i++) {
288 uploadId.push(this._uploadFiles([ files[i] ], blob));
289 }
290 }
291 else {
292 uploadId = this._uploadFiles(files, blob);
293 }
294 }
295
296 // re-create upload button to effectively reset the 'files'
297 // property of the input element
298 this._removeButton();
299 this._createButton();
300
301 return uploadId;
302 },
303
304 /**
305 * Sends the request to upload files.
306 *
307 * @param {(FileList|Array.<File>)} files uploaded files
308 * @param {Blob} blob file blob
309 * @return {(int|null)} identifier for the uploaded files
310 */
311 _uploadFiles: function(files, blob) {
312 var uploadId = this._createFileElements(files);
313
314 // no more files left, abort
315 if (!this._fileElements[uploadId].length) {
316 return null;
317 }
318
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');
323
324 if (blob) {
325 formData.append('__files[' + internalFileId + ']', blob, files[i].name);
326 }
327 else {
328 formData.append('__files[' + internalFileId + ']', files[i]);
329 }
330 }
331 }
332
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');
337 }
338
339 // recursively append additional parameters to form data
340 var appendFormData = function(parameters, prefix) {
341 prefix = prefix || '';
342
343 for (var name in parameters) {
344 if (typeof parameters[name] === 'object') {
345 appendFormData(parameters[name], prefix + '[' + name + ']');
346 }
347 else {
348 formData.append('parameters' + prefix + '[' + name + ']', parameters[name]);
349 }
350 }
351 };
352
353 appendFormData(this._getParameters());
354
355 var request = new AjaxRequest({
356 data: formData,
357 contentType: false,
358 failure: this._failure.bind(this, uploadId),
359 silent: true,
360 success: this._success.bind(this, uploadId),
361 uploadProgress: this._progress.bind(this, uploadId),
362 url: this._options.url,
363 withCredentials: true
364 });
365 request.sendRequest();
366
367 return uploadId;
368 },
369
370 /**
371 * Uploads the given file blob.
372 *
373 * @param {Blob} blob file blob
374 * @return {int} identifier for the uploaded file
375 */
376 uploadBlob: function(blob) {
377 return this._upload(null, null, blob);
378 },
379
380 /**
381 * Uploads the given file.
382 *
383 * @param {File} file uploaded file
384 * @return {int} identifier(s) for the uploaded file
385 */
386 uploadFile: function(file) {
387 return this._upload(null, file);
388 }
389 };
390
391 return Upload;
392 });