Merge branch '2.1' into 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-2017 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 /**
13 * @constructor
14 */
15 function Upload(buttonContainerId, targetId, options) {
16 options = options || {};
17
18 if (options.className === undefined) {
19 throw new Error("Missing class name.");
20 }
21
22 // set default options
23 this._options = Core.extend({
24 // name of the PHP action
25 action: 'upload',
26 // is true if multiple files can be uploaded at once
27 multiple: false,
28 // name if the upload field
29 name: '__files[]',
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
34 }, options);
35
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;
39 }
40
41 this._buttonContainer = elById(buttonContainerId);
42 if (this._buttonContainer === null) {
43 throw new Error("Element id '" + buttonContainerId + "' is unknown.");
44 }
45
46 this._target = elById(targetId);
47 if (targetId === null) {
48 throw new Error("Element id '" + targetId + "' is unknown.");
49 }
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.");
52 }
53
54 this._fileElements = [];
55 this._internalFileId = 0;
56
57 this._createButton();
58 }
59 Upload.prototype = {
60 /**
61 * Creates the upload button.
62 */
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');
69 }
70 this._fileUpload.addEventListener('change', this._upload.bind(this));
71
72 this._button = elCreate('p');
73 this._button.className = 'button uploadButton';
74
75 var span = elCreate('span');
76 span.textContent = Language.get('wcf.global.button.upload');
77 this._button.appendChild(span);
78
79 DomUtil.prepend(this._fileUpload, this._button);
80
81 this._insertButton();
82
83 DomChangeListener.trigger();
84 },
85
86 /**
87 * Creates the document element for an uploaded file.
88 *
89 * @param {File} file uploaded file
90 */
91 _createFileElement: function(file) {
92 var progress = elCreate('progress');
93 elAttr(progress, 'max', 100);
94
95 if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
96 var li = elCreate('li');
97 li.innerText = file.name;
98 li.appendChild(progress);
99
100 this._target.appendChild(li);
101
102 return li;
103 }
104 else {
105 var p = elCreate('p');
106 p.appendChild(progress);
107
108 this._target.appendChild(p);
109
110 return p;
111 }
112 },
113
114 /**
115 * Creates the document elements for uploaded files.
116 *
117 * @param {(FileList|Array.<File>)} files uploaded files
118 */
119 _createFileElements: function(files) {
120 if (files.length) {
121 var uploadId = this._fileElements.length;
122 this._fileElements[uploadId] = [];
123
124 for (var i = 0, length = files.length; i < length; i++) {
125 var file = files[i];
126 var fileElement = this._createFileElement(file);
127
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;
132 }
133 }
134
135 DomChangeListener.trigger();
136
137 return uploadId;
138 }
139
140 return null;
141 },
142
143 /**
144 * Handles a failed file upload.
145 *
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
152 */
153 _failure: function(uploadId, data, responseText, xhr, requestOptions) {
154 // does nothing
155 return true;
156 },
157
158 /**
159 * Return additional parameters for upload requests.
160 *
161 * @return {object<string, *>} additional parameters
162 */
163 _getParameters: function() {
164 return {};
165 },
166
167 /**
168 * Inserts the created button to upload files into the button container.
169 */
170 _insertButton: function() {
171 DomUtil.prepend(this._button, this._buttonContainer);
172 },
173
174 /**
175 * Updates the progress of an upload.
176 *
177 * @param {int} uploadId internal upload identifier
178 * @param {XMLHttpRequestProgressEvent} event progress event object
179 */
180 _progress: function(uploadId, event) {
181 var percentComplete = Math.round(event.loaded / event.total * 100);
182
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);
187 }
188 }
189 },
190
191 /**
192 * Removes the button to upload files.
193 */
194 _removeButton: function() {
195 elRemove(this._button);
196
197 DomChangeListener.trigger();
198 },
199
200 /**
201 * Handles a successful file upload.
202 *
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
208 */
209 _success: function(uploadId, data, responseText, xhr, requestOptions) {
210 // does nothing
211 },
212
213 /**
214 * File input change callback to upload files.
215 *
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
220 */
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]);
226 }
227
228 var uploadId = null;
229
230 var files = [];
231 if (file) {
232 files.push(file);
233 }
234 else if (blob) {
235 var fileExtension = '';
236 switch (blob.type) {
237 case 'image/jpeg':
238 fileExtension = '.jpg';
239 break;
240
241 case 'image/gif':
242 fileExtension = '.gif';
243 break;
244
245 case 'image/png':
246 fileExtension = '.png';
247 break;
248 }
249
250 files.push({
251 name: 'pasted-from-clipboard' + fileExtension
252 });
253 }
254 else {
255 files = this._fileUpload.files;
256 }
257
258 if (files.length) {
259 if (this._options.singleFileRequests) {
260 uploadId = [];
261 for (var i = 0, length = files.length; i < length; i++) {
262 uploadId.push(this._uploadFiles([ files[i] ], blob));
263 }
264 }
265 else {
266 uploadId = this._uploadFiles(files, blob);
267 }
268 }
269
270 // re-create upload button to effectively reset the 'files'
271 // property of the input element
272 this._removeButton();
273 this._createButton();
274
275 return uploadId;
276 },
277
278 /**
279 * Sends the request to upload files.
280 *
281 * @param {(FileList|Array.<File>)} files uploaded files
282 * @param {Blob} blob file blob
283 * @return {(int|null)} identifier for the uploaded files
284 */
285 _uploadFiles: function(files, blob) {
286 var uploadId = this._createFileElements(files);
287
288 // no more files left, abort
289 if (!this._fileElements[uploadId].length) {
290 return null;
291 }
292
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');
297
298 if (blob) {
299 formData.append('__files[' + internalFileId + ']', blob, files[i].name);
300 }
301 else {
302 formData.append('__files[' + internalFileId + ']', files[i]);
303 }
304 }
305 }
306
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');
311 }
312
313 // recursively append additional parameters to form data
314 var appendFormData = function(parameters, prefix) {
315 prefix = prefix || '';
316
317 for (var name in parameters) {
318 if (typeof parameters[name] === 'object') {
319 appendFormData(parameters[name], prefix + '[' + name + ']');
320 }
321 else {
322 formData.append('parameters' + prefix + '[' + name + ']', parameters[name]);
323 }
324 }
325 };
326
327 appendFormData(this._getParameters());
328
329 var request = new AjaxRequest({
330 data: formData,
331 contentType: false,
332 failure: this._failure.bind(this, uploadId),
333 silent: true,
334 success: this._success.bind(this, uploadId),
335 uploadProgress: this._progress.bind(this, uploadId),
336 url: this._options.url,
337 withCredentials: true
338 });
339 request.sendRequest();
340
341 return uploadId;
342 }
343 };
344
345 return Upload;
346 });