|
1 YUI.add('uploader-queue', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 * The class manages a queue of files that should be uploaded to the server. |
|
5 * It initializes the required number of uploads, tracks them as they progress, |
|
6 * and automatically advances to the next upload when a preceding one has completed. |
|
7 * @module uploader-queue |
|
8 */ |
|
9 |
|
10 |
|
11 |
|
12 /** |
|
13 * This class manages a queue of files to be uploaded to the server. |
|
14 * @class Uploader.Queue |
|
15 * @extends Base |
|
16 * @constructor |
|
17 */ |
|
18 var UploaderQueue = function() { |
|
19 this.queuedFiles = []; |
|
20 this.uploadRetries = {}; |
|
21 this.numberOfUploads = 0; |
|
22 this.currentUploadedByteValues = {}; |
|
23 this.currentFiles = {}; |
|
24 this.totalBytesUploaded = 0; |
|
25 this.totalBytes = 0; |
|
26 |
|
27 UploaderQueue.superclass.constructor.apply(this, arguments); |
|
28 }; |
|
29 |
|
30 |
|
31 Y.extend(UploaderQueue, Y.Base, { |
|
32 |
|
33 /** |
|
34 * Stored value of the current queue state |
|
35 * @property _currentState |
|
36 * @type {String} |
|
37 * @protected |
|
38 * @default UploaderQueue.STOPPED |
|
39 */ |
|
40 _currentState: UploaderQueue.STOPPED, |
|
41 |
|
42 /** |
|
43 * Construction logic executed during UploaderQueue instantiation. |
|
44 * |
|
45 * @method initializer |
|
46 * @protected |
|
47 */ |
|
48 initializer : function () {}, |
|
49 |
|
50 /** |
|
51 * Handles and retransmits upload start event. |
|
52 * |
|
53 * @method _uploadStartHandler |
|
54 * @param event The event dispatched during the upload process. |
|
55 * @private |
|
56 */ |
|
57 _uploadStartHandler : function (event) { |
|
58 var updatedEvent = event; |
|
59 updatedEvent.file = event.target; |
|
60 updatedEvent.originEvent = event; |
|
61 |
|
62 this.fire("uploadstart", updatedEvent); |
|
63 }, |
|
64 |
|
65 /** |
|
66 * Handles and retransmits upload error event. |
|
67 * |
|
68 * @method _uploadErrorHandler |
|
69 * @param event The event dispatched during the upload process. |
|
70 * @private |
|
71 */ |
|
72 _uploadErrorHandler : function (event) { |
|
73 var errorAction = this.get("errorAction"), |
|
74 updatedEvent = event, |
|
75 fileid, |
|
76 retries; |
|
77 |
|
78 updatedEvent.file = event.target; |
|
79 updatedEvent.originEvent = event; |
|
80 |
|
81 this.numberOfUploads-=1; |
|
82 delete this.currentFiles[event.target.get("id")]; |
|
83 this._detachFileEvents(event.target); |
|
84 |
|
85 event.target.cancelUpload(); |
|
86 |
|
87 if (errorAction === UploaderQueue.STOP) { |
|
88 this.pauseUpload(); |
|
89 } |
|
90 |
|
91 else if (errorAction === UploaderQueue.RESTART_ASAP) { |
|
92 fileid = event.target.get("id"); |
|
93 retries = this.uploadRetries[fileid] || 0; |
|
94 |
|
95 if (retries < this.get("retryCount")) { |
|
96 this.uploadRetries[fileid] = retries + 1; |
|
97 this.addToQueueTop(event.target); |
|
98 } |
|
99 this._startNextFile(); |
|
100 } |
|
101 else if (errorAction === UploaderQueue.RESTART_AFTER) { |
|
102 fileid = event.target.get("id"); |
|
103 retries = this.uploadRetries[fileid] || 0; |
|
104 |
|
105 if (retries < this.get("retryCount")) { |
|
106 this.uploadRetries[fileid] = retries + 1; |
|
107 this.addToQueueBottom(event.target); |
|
108 } |
|
109 this._startNextFile(); |
|
110 } |
|
111 |
|
112 this.fire("uploaderror", updatedEvent); |
|
113 }, |
|
114 |
|
115 /** |
|
116 * Launches the upload of the next file in the queue. |
|
117 * |
|
118 * @method _startNextFile |
|
119 * @private |
|
120 */ |
|
121 _startNextFile : function () { |
|
122 if (this.queuedFiles.length > 0) { |
|
123 var currentFile = this.queuedFiles.shift(), |
|
124 fileId = currentFile.get("id"), |
|
125 parameters = this.get("perFileParameters"), |
|
126 fileParameters = parameters.hasOwnProperty(fileId) ? parameters[fileId] : parameters; |
|
127 |
|
128 this.currentUploadedByteValues[fileId] = 0; |
|
129 |
|
130 currentFile.on("uploadstart", this._uploadStartHandler, this); |
|
131 currentFile.on("uploadprogress", this._uploadProgressHandler, this); |
|
132 currentFile.on("uploadcomplete", this._uploadCompleteHandler, this); |
|
133 currentFile.on("uploaderror", this._uploadErrorHandler, this); |
|
134 currentFile.on("uploadcancel", this._uploadCancelHandler, this); |
|
135 |
|
136 currentFile.set("xhrHeaders", this.get("uploadHeaders")); |
|
137 currentFile.set("xhrWithCredentials", this.get("withCredentials")); |
|
138 |
|
139 currentFile.startUpload(this.get("uploadURL"), fileParameters, this.get("fileFieldName")); |
|
140 |
|
141 this._registerUpload(currentFile); |
|
142 } |
|
143 }, |
|
144 |
|
145 /** |
|
146 * Register a new upload process. |
|
147 * |
|
148 * @method _registerUpload |
|
149 * @private |
|
150 */ |
|
151 _registerUpload : function (file) { |
|
152 this.numberOfUploads += 1; |
|
153 this.currentFiles[file.get("id")] = file; |
|
154 }, |
|
155 |
|
156 /** |
|
157 * Unregisters a new upload process. |
|
158 * |
|
159 * @method _unregisterUpload |
|
160 * @private |
|
161 */ |
|
162 _unregisterUpload : function (file) { |
|
163 if (this.numberOfUploads > 0) { |
|
164 this.numberOfUploads -= 1; |
|
165 } |
|
166 |
|
167 delete this.currentFiles[file.get("id")]; |
|
168 delete this.uploadRetries[file.get("id")]; |
|
169 |
|
170 this._detachFileEvents(file); |
|
171 }, |
|
172 |
|
173 _detachFileEvents : function (file) { |
|
174 file.detach("uploadstart", this._uploadStartHandler); |
|
175 file.detach("uploadprogress", this._uploadProgressHandler); |
|
176 file.detach("uploadcomplete", this._uploadCompleteHandler); |
|
177 file.detach("uploaderror", this._uploadErrorHandler); |
|
178 file.detach("uploadcancel", this._uploadCancelHandler); |
|
179 }, |
|
180 |
|
181 /** |
|
182 * Handles and retransmits upload complete event. |
|
183 * |
|
184 * @method _uploadCompleteHandler |
|
185 * @param event The event dispatched during the upload process. |
|
186 * @private |
|
187 */ |
|
188 _uploadCompleteHandler : function (event) { |
|
189 |
|
190 this._unregisterUpload(event.target); |
|
191 |
|
192 this.totalBytesUploaded += event.target.get("size"); |
|
193 delete this.currentUploadedByteValues[event.target.get("id")]; |
|
194 |
|
195 |
|
196 if (this.queuedFiles.length > 0 && this._currentState === UploaderQueue.UPLOADING) { |
|
197 this._startNextFile(); |
|
198 } |
|
199 |
|
200 var updatedEvent = event, |
|
201 uploadedTotal = this.totalBytesUploaded, |
|
202 percentLoaded = Math.min(100, Math.round(10000*uploadedTotal/this.totalBytes) / 100); |
|
203 |
|
204 updatedEvent.file = event.target; |
|
205 updatedEvent.originEvent = event; |
|
206 |
|
207 Y.each(this.currentUploadedByteValues, function (value) { |
|
208 uploadedTotal += value; |
|
209 }); |
|
210 |
|
211 this.fire("totaluploadprogress", { |
|
212 bytesLoaded: uploadedTotal, |
|
213 bytesTotal: this.totalBytes, |
|
214 percentLoaded: percentLoaded |
|
215 }); |
|
216 |
|
217 this.fire("uploadcomplete", updatedEvent); |
|
218 |
|
219 if (this.queuedFiles.length === 0 && this.numberOfUploads <= 0) { |
|
220 this.fire("alluploadscomplete"); |
|
221 this._currentState = UploaderQueue.STOPPED; |
|
222 } |
|
223 }, |
|
224 |
|
225 /** |
|
226 * Handles and retransmits upload cancel event. |
|
227 * |
|
228 * @method _uploadCancelHandler |
|
229 * @param event The event dispatched during the upload process. |
|
230 * @private |
|
231 */ |
|
232 _uploadCancelHandler : function (event) { |
|
233 |
|
234 var updatedEvent = event; |
|
235 updatedEvent.originEvent = event; |
|
236 updatedEvent.file = event.target; |
|
237 |
|
238 this.fire("uploadcancel", updatedEvent); |
|
239 }, |
|
240 |
|
241 |
|
242 |
|
243 /** |
|
244 * Handles and retransmits upload progress event. |
|
245 * |
|
246 * @method _uploadProgressHandler |
|
247 * @param event The event dispatched during the upload process. |
|
248 * @private |
|
249 */ |
|
250 _uploadProgressHandler : function (event) { |
|
251 |
|
252 this.currentUploadedByteValues[event.target.get("id")] = event.bytesLoaded; |
|
253 |
|
254 var updatedEvent = event, |
|
255 uploadedTotal = this.totalBytesUploaded, |
|
256 percentLoaded = Math.min(100, Math.round(10000*uploadedTotal/this.totalBytes) / 100); |
|
257 |
|
258 updatedEvent.originEvent = event; |
|
259 updatedEvent.file = event.target; |
|
260 |
|
261 this.fire("uploadprogress", updatedEvent); |
|
262 |
|
263 Y.each(this.currentUploadedByteValues, function (value) { |
|
264 uploadedTotal += value; |
|
265 }); |
|
266 |
|
267 this.fire("totaluploadprogress", { |
|
268 bytesLoaded: uploadedTotal, |
|
269 bytesTotal: this.totalBytes, |
|
270 percentLoaded: percentLoaded |
|
271 }); |
|
272 }, |
|
273 |
|
274 /** |
|
275 * Starts uploading the queued up file list. |
|
276 * |
|
277 * @method startUpload |
|
278 */ |
|
279 startUpload: function() { |
|
280 this.queuedFiles = this.get("fileList").slice(0); |
|
281 this.numberOfUploads = 0; |
|
282 this.currentUploadedByteValues = {}; |
|
283 this.currentFiles = {}; |
|
284 this.totalBytesUploaded = 0; |
|
285 |
|
286 this._currentState = UploaderQueue.UPLOADING; |
|
287 |
|
288 while (this.numberOfUploads < this.get("simUploads") && this.queuedFiles.length > 0) { |
|
289 this._startNextFile(); |
|
290 } |
|
291 }, |
|
292 |
|
293 /** |
|
294 * Pauses the upload process. The ongoing file uploads |
|
295 * will complete after this method is called, but no |
|
296 * new ones will be launched. |
|
297 * |
|
298 * @method pauseUpload |
|
299 */ |
|
300 pauseUpload: function () { |
|
301 this._currentState = UploaderQueue.STOPPED; |
|
302 }, |
|
303 |
|
304 /** |
|
305 * Restarts a paused upload process. |
|
306 * |
|
307 * @method restartUpload |
|
308 */ |
|
309 restartUpload: function () { |
|
310 this._currentState = UploaderQueue.UPLOADING; |
|
311 while (this.numberOfUploads < this.get("simUploads")) { |
|
312 this._startNextFile(); |
|
313 } |
|
314 }, |
|
315 |
|
316 /** |
|
317 * If a particular file is stuck in an ongoing upload without |
|
318 * any progress events, this method allows to force its reupload |
|
319 * by cancelling its upload and immediately relaunching it. |
|
320 * |
|
321 * @method forceReupload |
|
322 * @param file {File} The file to force reupload on. |
|
323 */ |
|
324 forceReupload : function (file) { |
|
325 var id = file.get("id"); |
|
326 if (this.currentFiles.hasOwnProperty(id)) { |
|
327 file.cancelUpload(); |
|
328 this._unregisterUpload(file); |
|
329 this.addToQueueTop(file); |
|
330 this._startNextFile(); |
|
331 } |
|
332 }, |
|
333 |
|
334 /** |
|
335 * Add a new file to the top of the queue (the upload will be |
|
336 * launched as soon as the current number of uploading files |
|
337 * drops below the maximum permissible value). |
|
338 * |
|
339 * @method addToQueueTop |
|
340 * @param file {File} The file to add to the top of the queue. |
|
341 */ |
|
342 addToQueueTop: function (file) { |
|
343 this.queuedFiles.unshift(file); |
|
344 }, |
|
345 |
|
346 /** |
|
347 * Add a new file to the bottom of the queue (the upload will be |
|
348 * launched after all the other queued files are uploaded.) |
|
349 * |
|
350 * @method addToQueueBottom |
|
351 * @param file {File} The file to add to the bottom of the queue. |
|
352 */ |
|
353 addToQueueBottom: function (file) { |
|
354 this.queuedFiles.push(file); |
|
355 }, |
|
356 |
|
357 /** |
|
358 * Cancels a specific file's upload. If no argument is passed, |
|
359 * all ongoing uploads are cancelled and the upload process is |
|
360 * stopped. |
|
361 * |
|
362 * @method cancelUpload |
|
363 * @param file {File} An optional parameter - the file whose upload |
|
364 * should be cancelled. |
|
365 */ |
|
366 cancelUpload: function (file) { |
|
367 var id, |
|
368 i, |
|
369 fid; |
|
370 |
|
371 if (file) { |
|
372 id = file.get("id"); |
|
373 |
|
374 if (this.currentFiles[id]) { |
|
375 this.currentFiles[id].cancelUpload(); |
|
376 this._unregisterUpload(this.currentFiles[id]); |
|
377 if (this._currentState === UploaderQueue.UPLOADING) { |
|
378 this._startNextFile(); |
|
379 } |
|
380 } |
|
381 else { |
|
382 for (i = 0, len = this.queuedFiles.length; i < len; i++) { |
|
383 if (this.queuedFiles[i].get("id") === id) { |
|
384 this.queuedFiles.splice(i, 1); |
|
385 break; |
|
386 } |
|
387 } |
|
388 } |
|
389 } |
|
390 else { |
|
391 for (fid in this.currentFiles) { |
|
392 this.currentFiles[fid].cancelUpload(); |
|
393 this._unregisterUpload(this.currentFiles[fid]); |
|
394 } |
|
395 |
|
396 this.currentUploadedByteValues = {}; |
|
397 this.currentFiles = {}; |
|
398 this.totalBytesUploaded = 0; |
|
399 this.fire("alluploadscancelled"); |
|
400 this._currentState = UploaderQueue.STOPPED; |
|
401 } |
|
402 } |
|
403 }, { |
|
404 /** |
|
405 * Static constant for the value of the `errorAction` attribute: |
|
406 * prescribes the queue to continue uploading files in case of |
|
407 * an error. |
|
408 * @property CONTINUE |
|
409 * @readOnly |
|
410 * @type {String} |
|
411 * @static |
|
412 */ |
|
413 CONTINUE: "continue", |
|
414 |
|
415 /** |
|
416 * Static constant for the value of the `errorAction` attribute: |
|
417 * prescribes the queue to stop uploading files in case of |
|
418 * an error. |
|
419 * @property STOP |
|
420 * @readOnly |
|
421 * @type {String} |
|
422 * @static |
|
423 */ |
|
424 STOP: "stop", |
|
425 |
|
426 /** |
|
427 * Static constant for the value of the `errorAction` attribute: |
|
428 * prescribes the queue to restart a file upload immediately in case of |
|
429 * an error. |
|
430 * @property RESTART_ASAP |
|
431 * @readOnly |
|
432 * @type {String} |
|
433 * @static |
|
434 */ |
|
435 RESTART_ASAP: "restartasap", |
|
436 |
|
437 /** |
|
438 * Static constant for the value of the `errorAction` attribute: |
|
439 * prescribes the queue to restart an errored out file upload after |
|
440 * other files have finished uploading. |
|
441 * @property RESTART_AFTER |
|
442 * @readOnly |
|
443 * @type {String} |
|
444 * @static |
|
445 */ |
|
446 RESTART_AFTER: "restartafter", |
|
447 |
|
448 /** |
|
449 * Static constant for the value of the `_currentState` property: |
|
450 * implies that the queue is currently not uploading files. |
|
451 * @property STOPPED |
|
452 * @readOnly |
|
453 * @type {String} |
|
454 * @static |
|
455 */ |
|
456 STOPPED: "stopped", |
|
457 |
|
458 /** |
|
459 * Static constant for the value of the `_currentState` property: |
|
460 * implies that the queue is currently uploading files. |
|
461 * @property UPLOADING |
|
462 * @readOnly |
|
463 * @type {String} |
|
464 * @static |
|
465 */ |
|
466 UPLOADING: "uploading", |
|
467 |
|
468 /** |
|
469 * The identity of the class. |
|
470 * |
|
471 * @property NAME |
|
472 * @type String |
|
473 * @default 'uploaderqueue' |
|
474 * @readOnly |
|
475 * @protected |
|
476 * @static |
|
477 */ |
|
478 NAME: 'uploaderqueue', |
|
479 |
|
480 /** |
|
481 * Static property used to define the default attribute configuration of |
|
482 * the class. |
|
483 * |
|
484 * @property ATTRS |
|
485 * @type {Object} |
|
486 * @protected |
|
487 * @static |
|
488 */ |
|
489 ATTRS: { |
|
490 |
|
491 /** |
|
492 * Maximum number of simultaneous uploads; must be in the |
|
493 * range between 1 and 5. The value of `2` is default. It |
|
494 * is recommended that this value does not exceed 3. |
|
495 * @attribute simUploads |
|
496 * @type Number |
|
497 * @default 2 |
|
498 */ |
|
499 simUploads: { |
|
500 value: 2, |
|
501 validator: function (val) { |
|
502 return (val >= 1 && val <= 5); |
|
503 } |
|
504 }, |
|
505 |
|
506 /** |
|
507 * The action to take in case of error. The valid values for this attribute are: |
|
508 * `Y.Uploader.Queue.CONTINUE` (the upload process should continue on other files, |
|
509 * ignoring the error), `Y.Uploader.Queue.STOP` (the upload process |
|
510 * should stop completely), `Y.Uploader.Queue.RESTART_ASAP` (the upload |
|
511 * should restart immediately on the errored out file and continue as planned), or |
|
512 * Y.Uploader.Queue.RESTART_AFTER (the upload of the errored out file should restart |
|
513 * after all other files have uploaded) |
|
514 * @attribute errorAction |
|
515 * @type String |
|
516 * @default Y.Uploader.Queue.CONTINUE |
|
517 */ |
|
518 errorAction: { |
|
519 value: "continue", |
|
520 validator: function (val) { |
|
521 return ( |
|
522 val === UploaderQueue.CONTINUE || |
|
523 val === UploaderQueue.STOP || |
|
524 val === UploaderQueue.RESTART_ASAP || |
|
525 val === UploaderQueue.RESTART_AFTER |
|
526 ); |
|
527 } |
|
528 }, |
|
529 |
|
530 /** |
|
531 * The total number of bytes that has been uploaded. |
|
532 * @attribute bytesUploaded |
|
533 * @type Number |
|
534 */ |
|
535 bytesUploaded: { |
|
536 readOnly: true, |
|
537 value: 0 |
|
538 }, |
|
539 |
|
540 /** |
|
541 * The total number of bytes in the queue. |
|
542 * @attribute bytesTotal |
|
543 * @type Number |
|
544 */ |
|
545 bytesTotal: { |
|
546 readOnly: true, |
|
547 value: 0 |
|
548 }, |
|
549 |
|
550 /** |
|
551 * The queue file list. This file list should only be modified |
|
552 * before the upload has been started; modifying it after starting |
|
553 * the upload has no effect, and `addToQueueTop` or `addToQueueBottom` methods |
|
554 * should be used instead. |
|
555 * @attribute fileList |
|
556 * @type Array |
|
557 */ |
|
558 fileList: { |
|
559 value: [], |
|
560 lazyAdd: false, |
|
561 setter: function (val) { |
|
562 var newValue = val; |
|
563 Y.Array.each(newValue, function (value) { |
|
564 this.totalBytes += value.get("size"); |
|
565 }, this); |
|
566 |
|
567 return val; |
|
568 } |
|
569 }, |
|
570 |
|
571 /** |
|
572 * A String specifying what should be the POST field name for the file |
|
573 * content in the upload request. |
|
574 * |
|
575 * @attribute fileFieldName |
|
576 * @type {String} |
|
577 * @default Filedata |
|
578 */ |
|
579 fileFieldName: { |
|
580 value: "Filedata" |
|
581 }, |
|
582 |
|
583 /** |
|
584 * The URL to POST the file upload requests to. |
|
585 * |
|
586 * @attribute uploadURL |
|
587 * @type {String} |
|
588 * @default "" |
|
589 */ |
|
590 uploadURL: { |
|
591 value: "" |
|
592 }, |
|
593 |
|
594 /** |
|
595 * Additional HTTP headers that should be included |
|
596 * in the upload request. Due to Flash Player security |
|
597 * restrictions, this attribute is only honored in the |
|
598 * HTML5 Uploader. |
|
599 * |
|
600 * @attribute uploadHeaders |
|
601 * @type {Object} |
|
602 * @default {} |
|
603 */ |
|
604 uploadHeaders: { |
|
605 value: {} |
|
606 }, |
|
607 |
|
608 /** |
|
609 * A Boolean that specifies whether the file should be |
|
610 * uploaded with the appropriate user credentials for the |
|
611 * domain. Due to Flash Player security restrictions, this |
|
612 * attribute is only honored in the HTML5 Uploader. |
|
613 * |
|
614 * @attribute withCredentials |
|
615 * @type {Boolean} |
|
616 * @default true |
|
617 */ |
|
618 withCredentials: { |
|
619 value: true |
|
620 }, |
|
621 |
|
622 |
|
623 /** |
|
624 * An object, keyed by `fileId`, containing sets of key-value pairs |
|
625 * that should be passed as POST variables along with each corresponding |
|
626 * file. |
|
627 * |
|
628 * @attribute perFileParameters |
|
629 * @type {Object} |
|
630 * @default {} |
|
631 */ |
|
632 perFileParameters: { |
|
633 value: {} |
|
634 }, |
|
635 |
|
636 /** |
|
637 * The number of times to try re-uploading a file that failed to upload before |
|
638 * cancelling its upload. |
|
639 * |
|
640 * @attribute retryCount |
|
641 * @type {Number} |
|
642 * @default 3 |
|
643 */ |
|
644 retryCount: { |
|
645 value: 3 |
|
646 } |
|
647 |
|
648 } |
|
649 }); |
|
650 |
|
651 |
|
652 Y.namespace('Uploader'); |
|
653 Y.Uploader.Queue = UploaderQueue; |
|
654 |
|
655 |
|
656 }, '@VERSION@', {"requires": ["base"]}); |