fileuploader-rails 2.1.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,575 @@
1
+ /**
2
+ * Class that creates upload widget with drag-and-drop and file list
3
+ * @inherits qq.FineUploaderBasic
4
+ */
5
+ qq.FineUploader = function(o){
6
+ // call parent constructor
7
+ qq.FineUploaderBasic.apply(this, arguments);
8
+
9
+ // additional options
10
+ qq.extend(this._options, {
11
+ element: null,
12
+ listElement: null,
13
+ dragAndDrop: {
14
+ extraDropzones: [],
15
+ hideDropzones: true,
16
+ disableDefaultDropzone: false
17
+ },
18
+ text: {
19
+ uploadButton: 'Upload a file',
20
+ cancelButton: 'Cancel',
21
+ retryButton: 'Retry',
22
+ failUpload: 'Upload failed',
23
+ dragZone: 'Drop files here to upload',
24
+ formatProgress: "{percent}% of {total_size}",
25
+ waitingForResponse: "Processing..."
26
+ },
27
+ template: '<div class="qq-uploader">' +
28
+ ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') +
29
+ (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') +
30
+ (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
31
+ '</div>',
32
+
33
+ // template for one item in file list
34
+ fileTemplate: '<li>' +
35
+ '<div class="qq-progress-bar"></div>' +
36
+ '<span class="qq-upload-spinner"></span>' +
37
+ '<span class="qq-upload-finished"></span>' +
38
+ '<span class="qq-upload-file"></span>' +
39
+ '<span class="qq-upload-size"></span>' +
40
+ '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
41
+ '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' +
42
+ '<span class="qq-upload-status-text">{statusText}</span>' +
43
+ '</li>',
44
+ classes: {
45
+ // used to get elements from templates
46
+ button: 'qq-upload-button',
47
+ drop: 'qq-upload-drop-area',
48
+ dropActive: 'qq-upload-drop-area-active',
49
+ dropDisabled: 'qq-upload-drop-area-disabled',
50
+ list: 'qq-upload-list',
51
+ progressBar: 'qq-progress-bar',
52
+ file: 'qq-upload-file',
53
+ spinner: 'qq-upload-spinner',
54
+ finished: 'qq-upload-finished',
55
+ retrying: 'qq-upload-retrying',
56
+ retryable: 'qq-upload-retryable',
57
+ size: 'qq-upload-size',
58
+ cancel: 'qq-upload-cancel',
59
+ retry: 'qq-upload-retry',
60
+ statusText: 'qq-upload-status-text',
61
+
62
+ // added to list item <li> when upload completes
63
+ // used in css to hide progress spinner
64
+ success: 'qq-upload-success',
65
+ fail: 'qq-upload-fail',
66
+
67
+ successIcon: null,
68
+ failIcon: null
69
+ },
70
+ failedUploadTextDisplay: {
71
+ mode: 'default', //default, custom, or none
72
+ maxChars: 50,
73
+ responseProperty: 'error',
74
+ enableTooltip: true
75
+ },
76
+ messages: {
77
+ tooManyFilesError: "You may only drop one file"
78
+ },
79
+ retry: {
80
+ showAutoRetryNote: true,
81
+ autoRetryNote: "Retrying {retryNum}/{maxAuto}...",
82
+ showButton: false
83
+ },
84
+ showMessage: function(message){
85
+ alert(message);
86
+ }
87
+ }, true);
88
+
89
+ // overwrite options with user supplied
90
+ qq.extend(this._options, o, true);
91
+ this._wrapCallbacks();
92
+
93
+ // overwrite the upload button text if any
94
+ // same for the Cancel button and Fail message text
95
+ this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone);
96
+ this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton);
97
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton);
98
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton);
99
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, "");
100
+
101
+ this._element = this._options.element;
102
+ this._element.innerHTML = this._options.template;
103
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
104
+
105
+ this._classes = this._options.classes;
106
+
107
+ if (!this._button) {
108
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
109
+ }
110
+
111
+ this._bindCancelAndRetryEvents();
112
+ this._setupDragDrop();
113
+ };
114
+
115
+ // inherit from Basic Uploader
116
+ qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype);
117
+
118
+ qq.extend(qq.FineUploader.prototype, {
119
+ clearStoredFiles: function() {
120
+ qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
121
+ this._listElement.innerHTML = "";
122
+ },
123
+ addExtraDropzone: function(element){
124
+ this._setupExtraDropzone(element);
125
+ },
126
+ removeExtraDropzone: function(element){
127
+ var dzs = this._options.dragAndDrop.extraDropzones;
128
+ for(var i in dzs) if (dzs[i] === element) return this._options.dragAndDrop.extraDropzones.splice(i,1);
129
+ },
130
+ getItemByFileId: function(id){
131
+ var item = this._listElement.firstChild;
132
+
133
+ // there can't be txt nodes in dynamically created list
134
+ // and we can use nextSibling
135
+ while (item){
136
+ if (item.qqFileId == id) return item;
137
+ item = item.nextSibling;
138
+ }
139
+ },
140
+ reset: function() {
141
+ qq.FineUploaderBasic.prototype.reset.apply(this, arguments);
142
+ this._element.innerHTML = this._options.template;
143
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
144
+ if (!this._options.button) {
145
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
146
+ }
147
+ this._bindCancelAndRetryEvents();
148
+ this._setupDragDrop();
149
+ },
150
+ _leaving_document_out: function(e){
151
+ return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
152
+ || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
153
+ },
154
+ _storeFileForLater: function(id) {
155
+ qq.FineUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
156
+ var item = this.getItemByFileId(id);
157
+ qq(this._find(item, 'spinner')).hide();
158
+ },
159
+ /**
160
+ * Gets one of the elements listed in this._options.classes
161
+ **/
162
+ _find: function(parent, type){
163
+ var element = qq(parent).getByClass(this._options.classes[type])[0];
164
+ if (!element){
165
+ throw new Error('element not found ' + type);
166
+ }
167
+
168
+ return element;
169
+ },
170
+ _setupExtraDropzone: function(element){
171
+ this._options.dragAndDrop.extraDropzones.push(element);
172
+ this._setupDropzone(element);
173
+ },
174
+ _setupDropzone: function(dropArea){
175
+ var self = this;
176
+
177
+ var dz = new qq.UploadDropZone({
178
+ element: dropArea,
179
+ onEnter: function(e){
180
+ qq(dropArea).addClass(self._classes.dropActive);
181
+ e.stopPropagation();
182
+ },
183
+ onLeave: function(e){
184
+ //e.stopPropagation();
185
+ },
186
+ onLeaveNotDescendants: function(e){
187
+ qq(dropArea).removeClass(self._classes.dropActive);
188
+ },
189
+ onDrop: function(e){
190
+ if (self._options.dragAndDrop.hideDropzones) {
191
+ qq(dropArea).hide();
192
+ }
193
+
194
+ qq(dropArea).removeClass(self._classes.dropActive);
195
+ if (e.dataTransfer.files.length > 1 && !self._options.multiple) {
196
+ self._error('tooManyFilesError', "");
197
+ }
198
+ else {
199
+ self._uploadFileList(e.dataTransfer.files);
200
+ }
201
+ }
202
+ });
203
+
204
+ this.addDisposer(function() { dz.dispose(); });
205
+
206
+ if (this._options.dragAndDrop.hideDropzones) {
207
+ qq(dropArea).hide();
208
+ }
209
+ },
210
+ _setupDragDrop: function(){
211
+ var self, dropArea;
212
+
213
+ self = this;
214
+
215
+ if (!this._options.dragAndDrop.disableDefaultDropzone) {
216
+ dropArea = this._find(this._element, 'drop');
217
+ this._options.dragAndDrop.extraDropzones.push(dropArea);
218
+ }
219
+
220
+ var dropzones = this._options.dragAndDrop.extraDropzones;
221
+ var i;
222
+ for (i=0; i < dropzones.length; i++){
223
+ this._setupDropzone(dropzones[i]);
224
+ }
225
+
226
+ // IE <= 9 does not support the File API used for drag+drop uploads
227
+ if (!this._options.dragAndDrop.disableDefaultDropzone && (!qq.ie() || qq.ie10())) {
228
+ this._attach(document, 'dragenter', function(e){
229
+ if (qq(dropArea).hasClass(self._classes.dropDisabled)) return;
230
+
231
+ dropArea.style.display = 'block';
232
+ for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; }
233
+
234
+ });
235
+ }
236
+ this._attach(document, 'dragleave', function(e){
237
+ if (self._options.dragAndDrop.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) {
238
+ for (i=0; i < dropzones.length; i++) {
239
+ qq(dropzones[i]).hide();
240
+ }
241
+ }
242
+ });
243
+ qq(document).attach('drop', function(e){
244
+ if (self._options.dragAndDrop.hideDropzones) {
245
+ for (i=0; i < dropzones.length; i++) {
246
+ qq(dropzones[i]).hide();
247
+ }
248
+ }
249
+ e.preventDefault();
250
+ });
251
+ },
252
+ _onSubmit: function(id, fileName){
253
+ qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments);
254
+ this._addToList(id, fileName);
255
+ },
256
+ // Update the progress bar & percentage as the file is uploaded
257
+ _onProgress: function(id, fileName, loaded, total){
258
+ qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments);
259
+
260
+ var item, progressBar, text, percent, cancelLink, size;
261
+
262
+ item = this.getItemByFileId(id);
263
+ progressBar = this._find(item, 'progressBar');
264
+ percent = Math.round(loaded / total * 100);
265
+
266
+ if (loaded === total) {
267
+ cancelLink = this._find(item, 'cancel');
268
+ qq(cancelLink).hide();
269
+
270
+ qq(progressBar).hide();
271
+ qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse);
272
+
273
+ // If last byte was sent, just display final size
274
+ text = this._formatSize(total);
275
+ }
276
+ else {
277
+ // If still uploading, display percentage
278
+ text = this._formatProgress(loaded, total);
279
+
280
+ qq(progressBar).css({display: 'block'});
281
+ }
282
+
283
+ // Update progress bar element
284
+ qq(progressBar).css({width: percent + '%'});
285
+
286
+ size = this._find(item, 'size');
287
+ qq(size).css({display: 'inline'});
288
+ qq(size).setText(text);
289
+ },
290
+ _onComplete: function(id, fileName, result, xhr){
291
+ qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments);
292
+
293
+ var item = this.getItemByFileId(id);
294
+
295
+ qq(this._find(item, 'statusText')).clearText();
296
+
297
+ qq(item).removeClass(this._classes.retrying);
298
+ qq(this._find(item, 'progressBar')).hide();
299
+
300
+ if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
301
+ qq(this._find(item, 'cancel')).hide();
302
+ }
303
+ qq(this._find(item, 'spinner')).hide();
304
+
305
+ if (result.success){
306
+ qq(item).addClass(this._classes.success);
307
+ if (this._classes.successIcon) {
308
+ this._find(item, 'finished').style.display = "inline-block";
309
+ qq(item).addClass(this._classes.successIcon);
310
+ }
311
+ } else {
312
+ qq(item).addClass(this._classes.fail);
313
+ if (this._classes.failIcon) {
314
+ this._find(item, 'finished').style.display = "inline-block";
315
+ qq(item).addClass(this._classes.failIcon);
316
+ }
317
+ if (this._options.retry.showButton && !this._preventRetries[id]) {
318
+ qq(item).addClass(this._classes.retryable);
319
+ }
320
+ this._controlFailureTextDisplay(item, result);
321
+ }
322
+ },
323
+ _onUpload: function(id, fileName, xhr){
324
+ qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments);
325
+
326
+ var item = this.getItemByFileId(id);
327
+ this._showSpinner(item);
328
+ },
329
+ _onBeforeAutoRetry: function(id) {
330
+ var item, progressBar, cancelLink, failTextEl, retryNumForDisplay, maxAuto, retryNote;
331
+
332
+ qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments);
333
+
334
+ item = this.getItemByFileId(id);
335
+ progressBar = this._find(item, 'progressBar');
336
+
337
+ this._showCancelLink(item);
338
+ progressBar.style.width = 0;
339
+ qq(progressBar).hide();
340
+
341
+ if (this._options.retry.showAutoRetryNote) {
342
+ failTextEl = this._find(item, 'statusText');
343
+ retryNumForDisplay = this._autoRetries[id] + 1;
344
+ maxAuto = this._options.retry.maxAutoAttempts;
345
+
346
+ retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
347
+ retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
348
+
349
+ qq(failTextEl).setText(retryNote);
350
+ if (retryNumForDisplay === 1) {
351
+ qq(item).addClass(this._classes.retrying);
352
+ }
353
+ }
354
+ },
355
+ //return false if we should not attempt the requested retry
356
+ _onBeforeManualRetry: function(id) {
357
+ if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
358
+ var item = this.getItemByFileId(id);
359
+ this._find(item, 'progressBar').style.width = 0;
360
+ qq(item).removeClass(this._classes.fail);
361
+ this._showSpinner(item);
362
+ this._showCancelLink(item);
363
+ return true;
364
+ }
365
+ return false;
366
+ },
367
+ _addToList: function(id, fileName){
368
+ var item = qq.toElement(this._options.fileTemplate);
369
+ if (this._options.disableCancelForFormUploads && !qq.UploadHandlerXhr.isSupported()) {
370
+ var cancelLink = this._find(item, 'cancel');
371
+ qq(cancelLink).remove();
372
+ }
373
+
374
+ item.qqFileId = id;
375
+
376
+ var fileElement = this._find(item, 'file');
377
+ qq(fileElement).setText(this._formatFileName(fileName));
378
+ qq(this._find(item, 'size')).hide();
379
+ if (!this._options.multiple) this._clearList();
380
+ this._listElement.appendChild(item);
381
+ },
382
+ _clearList: function(){
383
+ this._listElement.innerHTML = '';
384
+ this.clearStoredFiles();
385
+ },
386
+ /**
387
+ * delegate click event for cancel & retry links
388
+ **/
389
+ _bindCancelAndRetryEvents: function(){
390
+ var self = this,
391
+ list = this._listElement;
392
+
393
+ this._attach(list, 'click', function(e){
394
+ e = e || window.event;
395
+ var target = e.target || e.srcElement;
396
+
397
+ if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry)){
398
+ qq.preventDefault(e);
399
+
400
+ var item = target.parentNode;
401
+ while(item.qqFileId == undefined) {
402
+ item = target = target.parentNode;
403
+ }
404
+
405
+ if (qq(target).hasClass(self._classes.cancel)) {
406
+ self.cancel(item.qqFileId);
407
+ qq(item).remove();
408
+ }
409
+ else {
410
+ qq(item).removeClass(self._classes.retryable);
411
+ self.retry(item.qqFileId);
412
+ }
413
+ }
414
+ });
415
+ },
416
+ _formatProgress: function (uploadedSize, totalSize) {
417
+ var message = this._options.text.formatProgress;
418
+ function r(name, replacement) { message = message.replace(name, replacement); }
419
+
420
+ r('{percent}', Math.round(uploadedSize / totalSize * 100));
421
+ r('{total_size}', this._formatSize(totalSize));
422
+ return message;
423
+ },
424
+ _controlFailureTextDisplay: function(item, response) {
425
+ var mode, maxChars, responseProperty, failureReason, shortFailureReason;
426
+
427
+ mode = this._options.failedUploadTextDisplay.mode;
428
+ maxChars = this._options.failedUploadTextDisplay.maxChars;
429
+ responseProperty = this._options.failedUploadTextDisplay.responseProperty;
430
+
431
+ if (mode === 'custom') {
432
+ failureReason = response[responseProperty];
433
+ if (failureReason) {
434
+ if (failureReason.length > maxChars) {
435
+ shortFailureReason = failureReason.substring(0, maxChars) + '...';
436
+ }
437
+ }
438
+ else {
439
+ failureReason = this._options.text.failUpload;
440
+ this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn');
441
+ }
442
+
443
+ qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason);
444
+
445
+ if (this._options.failedUploadTextDisplay.enableTooltip) {
446
+ this._showTooltip(item, failureReason);
447
+ }
448
+ }
449
+ else if (mode === 'default') {
450
+ qq(this._find(item, 'statusText')).setText(this._options.text.failUpload);
451
+ }
452
+ else if (mode !== 'none') {
453
+ this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn');
454
+ }
455
+ },
456
+ //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details.
457
+ _showTooltip: function(item, text) {
458
+ item.title = text;
459
+ },
460
+ _showSpinner: function(item) {
461
+ var spinnerEl = this._find(item, 'spinner');
462
+ spinnerEl.style.display = "inline-block";
463
+ },
464
+ _showCancelLink: function(item) {
465
+ if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
466
+ var cancelLink = this._find(item, 'cancel');
467
+ cancelLink.style.display = 'inline';
468
+ }
469
+ },
470
+ _error: function(code, fileName){
471
+ var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments);
472
+ this._options.showMessage(message);
473
+ }
474
+ });
475
+
476
+ qq.UploadDropZone = function(o){
477
+ this._options = {
478
+ element: null,
479
+ onEnter: function(e){},
480
+ onLeave: function(e){},
481
+ // is not fired when leaving element by hovering descendants
482
+ onLeaveNotDescendants: function(e){},
483
+ onDrop: function(e){}
484
+ };
485
+ qq.extend(this._options, o);
486
+ qq.extend(this, qq.DisposeSupport);
487
+
488
+ this._element = this._options.element;
489
+
490
+ this._disableDropOutside();
491
+ this._attachEvents();
492
+ };
493
+
494
+ qq.UploadDropZone.prototype = {
495
+ _dragover_should_be_canceled: function(){
496
+ return qq.safari() || (qq.firefox() && qq.windows());
497
+ },
498
+ _disableDropOutside: function(e){
499
+ // run only once for all instances
500
+ if (!qq.UploadDropZone.dropOutsideDisabled ){
501
+
502
+ // for these cases we need to catch onDrop to reset dropArea
503
+ if (this._dragover_should_be_canceled){
504
+ qq(document).attach('dragover', function(e){
505
+ e.preventDefault();
506
+ });
507
+ } else {
508
+ qq(document).attach('dragover', function(e){
509
+ if (e.dataTransfer){
510
+ e.dataTransfer.dropEffect = 'none';
511
+ e.preventDefault();
512
+ }
513
+ });
514
+ }
515
+
516
+ qq.UploadDropZone.dropOutsideDisabled = true;
517
+ }
518
+ },
519
+ _attachEvents: function(){
520
+ var self = this;
521
+
522
+ self._attach(self._element, 'dragover', function(e){
523
+ if (!self._isValidFileDrag(e)) return;
524
+
525
+ var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
526
+ if (effect == 'move' || effect == 'linkMove'){
527
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
528
+ } else {
529
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
530
+ }
531
+
532
+ e.stopPropagation();
533
+ e.preventDefault();
534
+ });
535
+
536
+ self._attach(self._element, 'dragenter', function(e){
537
+ if (!self._isValidFileDrag(e)) return;
538
+
539
+ self._options.onEnter(e);
540
+ });
541
+
542
+ self._attach(self._element, 'dragleave', function(e){
543
+ if (!self._isValidFileDrag(e)) return;
544
+
545
+ self._options.onLeave(e);
546
+
547
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
548
+ // do not fire when moving a mouse over a descendant
549
+ if (qq(this).contains(relatedTarget)) return;
550
+
551
+ self._options.onLeaveNotDescendants(e);
552
+ });
553
+
554
+ self._attach(self._element, 'drop', function(e){
555
+ if (!self._isValidFileDrag(e)) return;
556
+
557
+ e.preventDefault();
558
+ self._options.onDrop(e);
559
+ });
560
+ },
561
+ _isValidFileDrag: function(e){
562
+ // e.dataTransfer currently causing IE errors
563
+ // IE9 does NOT support file API, so drag-and-drop is not possible
564
+ if (qq.ie() && !qq.ie10()) return false;
565
+
566
+ var dt = e.dataTransfer,
567
+ // do not check dt.types.contains in webkit, because it crashes safari 4
568
+ isSafari = qq.safari();
569
+
570
+ // dt.effectAllowed is none in Safari 5
571
+ // dt.types.contains check is for firefox
572
+ var effectTest = qq.ie10() ? true : dt.effectAllowed != 'none';
573
+ return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
574
+ }
575
+ };