fine_uploader 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1634 @@
1
+ /**
2
+ * http://github.com/Valums-File-Uploader/file-uploader
3
+ *
4
+ * Multiple file upload component with progress-bar, drag-and-drop.
5
+ *
6
+ * Have ideas for improving this JS for the general community?
7
+ * Submit your changes at: https://github.com/Valums-File-Uploader/file-uploader
8
+ * Readme at https://github.com/valums/file-uploader/blob/2.1.1/readme.md
9
+ *
10
+ * VERSION 2.1.1
11
+ * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com )
12
+ * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com )
13
+ *
14
+ * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
15
+ */
16
+
17
+ //
18
+ // Helper functions
19
+ //
20
+
21
+ var qq = qq || {};
22
+
23
+ /**
24
+ * Adds all missing properties from second obj to first obj
25
+ */
26
+ qq.extend = function(first, second){
27
+ for (var prop in second){
28
+ first[prop] = second[prop];
29
+ }
30
+ };
31
+
32
+ /**
33
+ * Searches for a given element in the array, returns -1 if it is not present.
34
+ * @param {Number} [from] The index at which to begin the search
35
+ */
36
+ qq.indexOf = function(arr, elt, from){
37
+ if (arr.indexOf) return arr.indexOf(elt, from);
38
+
39
+ from = from || 0;
40
+ var len = arr.length;
41
+
42
+ if (from < 0) from += len;
43
+
44
+ for (; from < len; from++){
45
+ if (from in arr && arr[from] === elt){
46
+ return from;
47
+ }
48
+ }
49
+ return -1;
50
+ };
51
+
52
+ qq.getUniqueId = (function(){
53
+ var id = 0;
54
+ return function(){ return id++; };
55
+ })();
56
+
57
+ //
58
+ // Browsers and platforms detection
59
+
60
+ qq.ie = function(){ return navigator.userAgent.indexOf('MSIE') != -1; }
61
+ qq.safari = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf("Apple") != -1; }
62
+ qq.chrome = function(){ return navigator.vendor != undefined && navigator.vendor.indexOf('Google') != -1; }
63
+ qq.firefox = function(){ return (navigator.userAgent.indexOf('Mozilla') != -1 && navigator.vendor != undefined && navigator.vendor == ''); }
64
+ qq.windows = function(){ return navigator.platform == "Win32"; }
65
+
66
+ //
67
+ // Events
68
+
69
+ /** Returns the function which detaches attached event */
70
+ qq.attach = function(element, type, fn){
71
+ if (element.addEventListener){
72
+ element.addEventListener(type, fn, false);
73
+ } else if (element.attachEvent){
74
+ element.attachEvent('on' + type, fn);
75
+ }
76
+ return function() {
77
+ qq.detach(element, type, fn)
78
+ }
79
+ };
80
+ qq.detach = function(element, type, fn){
81
+ if (element.removeEventListener){
82
+ element.removeEventListener(type, fn, false);
83
+ } else if (element.attachEvent){
84
+ element.detachEvent('on' + type, fn);
85
+ }
86
+ };
87
+
88
+ qq.preventDefault = function(e){
89
+ if (e.preventDefault){
90
+ e.preventDefault();
91
+ } else{
92
+ e.returnValue = false;
93
+ }
94
+ };
95
+
96
+ //
97
+ // Node manipulations
98
+
99
+ /**
100
+ * Insert node a before node b.
101
+ */
102
+ qq.insertBefore = function(a, b){
103
+ b.parentNode.insertBefore(a, b);
104
+ };
105
+ qq.remove = function(element){
106
+ element.parentNode.removeChild(element);
107
+ };
108
+
109
+ qq.contains = function(parent, descendant){
110
+ // compareposition returns false in this case
111
+ if (parent == descendant) return true;
112
+
113
+ if (parent.contains){
114
+ return parent.contains(descendant);
115
+ } else {
116
+ return !!(descendant.compareDocumentPosition(parent) & 8);
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Creates and returns element from html string
122
+ * Uses innerHTML to create an element
123
+ */
124
+ qq.toElement = (function(){
125
+ var div = document.createElement('div');
126
+ return function(html){
127
+ div.innerHTML = html;
128
+ var element = div.firstChild;
129
+ div.removeChild(element);
130
+ return element;
131
+ };
132
+ })();
133
+
134
+ //
135
+ // Node properties and attributes
136
+
137
+ /**
138
+ * Sets styles for an element.
139
+ * Fixes opacity in IE6-8.
140
+ */
141
+ qq.css = function(element, styles){
142
+ if (styles.opacity != null){
143
+ if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
144
+ styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
145
+ }
146
+ }
147
+ qq.extend(element.style, styles);
148
+ };
149
+ qq.hasClass = function(element, name){
150
+ var re = new RegExp('(^| )' + name + '( |$)');
151
+ return re.test(element.className);
152
+ };
153
+ qq.addClass = function(element, name){
154
+ if (!qq.hasClass(element, name)){
155
+ element.className += ' ' + name;
156
+ }
157
+ };
158
+ qq.removeClass = function(element, name){
159
+ var re = new RegExp('(^| )' + name + '( |$)');
160
+ element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
161
+ };
162
+ qq.setText = function(element, text){
163
+ element.innerText = text;
164
+ element.textContent = text;
165
+ };
166
+
167
+ //
168
+ // Selecting elements
169
+
170
+ qq.children = function(element){
171
+ var children = [],
172
+ child = element.firstChild;
173
+
174
+ while (child){
175
+ if (child.nodeType == 1){
176
+ children.push(child);
177
+ }
178
+ child = child.nextSibling;
179
+ }
180
+
181
+ return children;
182
+ };
183
+
184
+ qq.getByClass = function(element, className){
185
+ if (element.querySelectorAll){
186
+ return element.querySelectorAll('.' + className);
187
+ }
188
+
189
+ var result = [];
190
+ var candidates = element.getElementsByTagName("*");
191
+ var len = candidates.length;
192
+
193
+ for (var i = 0; i < len; i++){
194
+ if (qq.hasClass(candidates[i], className)){
195
+ result.push(candidates[i]);
196
+ }
197
+ }
198
+ return result;
199
+ };
200
+
201
+ /**
202
+ * obj2url() takes a json-object as argument and generates
203
+ * a querystring. pretty much like jQuery.param()
204
+ *
205
+ * how to use:
206
+ *
207
+ * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
208
+ *
209
+ * will result in:
210
+ *
211
+ * `http://any.url/upload?otherParam=value&a=b&c=d`
212
+ *
213
+ * @param Object JSON-Object
214
+ * @param String current querystring-part
215
+ * @return String encoded querystring
216
+ */
217
+ qq.obj2url = function(obj, temp, prefixDone){
218
+ var uristrings = [],
219
+ prefix = '&',
220
+ add = function(nextObj, i){
221
+ var nextTemp = temp
222
+ ? (/\[\]$/.test(temp)) // prevent double-encoding
223
+ ? temp
224
+ : temp+'['+i+']'
225
+ : i;
226
+ if ((nextTemp != 'undefined') && (i != 'undefined')) {
227
+ uristrings.push(
228
+ (typeof nextObj === 'object')
229
+ ? qq.obj2url(nextObj, nextTemp, true)
230
+ : (Object.prototype.toString.call(nextObj) === '[object Function]')
231
+ ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
232
+ : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
233
+ );
234
+ }
235
+ };
236
+
237
+ if (!prefixDone && temp) {
238
+ prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
239
+ uristrings.push(temp);
240
+ uristrings.push(qq.obj2url(obj));
241
+ } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
242
+ // we wont use a for-in-loop on an array (performance)
243
+ for (var i = 0, len = obj.length; i < len; ++i){
244
+ add(obj[i], i);
245
+ }
246
+ } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
247
+ // for anything else but a scalar, we will use for-in-loop
248
+ for (var i in obj){
249
+ add(obj[i], i);
250
+ }
251
+ } else {
252
+ uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
253
+ }
254
+
255
+ return uristrings.join(prefix)
256
+ .replace(/^&/, '')
257
+ .replace(/%20/g, '+');
258
+ };
259
+
260
+ //
261
+ //
262
+ // Uploader Classes
263
+ //
264
+ //
265
+
266
+ var qq = qq || {};
267
+
268
+ /**
269
+ * Creates upload button, validates upload, but doesn't create file list or dd.
270
+ */
271
+ qq.FileUploaderBasic = function(o){
272
+ var that = this;
273
+ this._options = {
274
+ // set to true to see the server response
275
+ debug: false,
276
+ action: '/server/upload',
277
+ params: {},
278
+ customHeaders: {},
279
+ button: null,
280
+ multiple: true,
281
+ maxConnections: 3,
282
+ disableCancelForFormUploads: false,
283
+ autoUpload: true,
284
+ forceMultipart: false,
285
+ // validation
286
+ allowedExtensions: [],
287
+ acceptFiles: null, // comma separated string of mime-types for browser to display in browse dialog
288
+ sizeLimit: 0,
289
+ minSizeLimit: 0,
290
+ stopOnFirstInvalidFile: true,
291
+ // events
292
+ // return false to cancel submit
293
+ onSubmit: function(id, fileName){},
294
+ onComplete: function(id, fileName, responseJSON){},
295
+ onCancel: function(id, fileName){},
296
+ onUpload: function(id, fileName, xhr){},
297
+ onProgress: function(id, fileName, loaded, total){},
298
+ onError: function(id, fileName, reason) {},
299
+ // messages
300
+ messages: {
301
+ typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
302
+ sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
303
+ minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
304
+ emptyError: "{file} is empty, please select files again without it.",
305
+ noFilesError: "No files to upload.",
306
+ onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
307
+ },
308
+ showMessage: function(message){
309
+ alert(message);
310
+ },
311
+ inputName: 'qqfile'
312
+ };
313
+ qq.extend(this._options, o);
314
+ this._wrapCallbacks();
315
+ qq.extend(this, qq.DisposeSupport);
316
+
317
+ // number of files being uploaded
318
+ this._filesInProgress = 0;
319
+
320
+ this._storedFileIds = [];
321
+
322
+ this._handler = this._createUploadHandler();
323
+
324
+ if (this._options.button){
325
+ this._button = this._createUploadButton(this._options.button);
326
+ }
327
+
328
+ this._preventLeaveInProgress();
329
+ };
330
+
331
+ qq.FileUploaderBasic.prototype = {
332
+ log: function(str){
333
+ if (this._options.debug && window.console) console.log('[uploader] ' + str);
334
+ },
335
+ setParams: function(params){
336
+ this._options.params = params;
337
+ },
338
+ getInProgress: function(){
339
+ return this._filesInProgress;
340
+ },
341
+ uploadStoredFiles: function(){
342
+ while(this._storedFileIds.length) {
343
+ this._filesInProgress++;
344
+ this._handler.upload(this._storedFileIds.shift(), this._options.params);
345
+ }
346
+ },
347
+ clearStoredFiles: function(){
348
+ this._storedFileIds = [];
349
+ },
350
+ _createUploadButton: function(element){
351
+ var self = this;
352
+
353
+ var button = new qq.UploadButton({
354
+ element: element,
355
+ multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
356
+ acceptFiles: this._options.acceptFiles,
357
+ onChange: function(input){
358
+ self._onInputChange(input);
359
+ }
360
+ });
361
+
362
+ this.addDisposer(function() { button.dispose(); });
363
+ return button;
364
+ },
365
+ _createUploadHandler: function(){
366
+ var self = this,
367
+ handlerClass;
368
+
369
+ if(qq.UploadHandlerXhr.isSupported()){
370
+ handlerClass = 'UploadHandlerXhr';
371
+ } else {
372
+ handlerClass = 'UploadHandlerForm';
373
+ }
374
+
375
+ var handler = new qq[handlerClass]({
376
+ debug: this._options.debug,
377
+ action: this._options.action,
378
+ forceMultipart: this._options.forceMultipart,
379
+ maxConnections: this._options.maxConnections,
380
+ customHeaders: this._options.customHeaders,
381
+ inputName: this._options.inputName,
382
+ demoMode: this._options.demoMode,
383
+ onProgress: function(id, fileName, loaded, total){
384
+ self._onProgress(id, fileName, loaded, total);
385
+ self._options.onProgress(id, fileName, loaded, total);
386
+ },
387
+ onComplete: function(id, fileName, result){
388
+ self._onComplete(id, fileName, result);
389
+ self._options.onComplete(id, fileName, result);
390
+ },
391
+ onCancel: function(id, fileName){
392
+ self._onCancel(id, fileName);
393
+ self._options.onCancel(id, fileName);
394
+ },
395
+ onError: self._options.onError,
396
+ onUpload: function(id, fileName, xhr){
397
+ self._onUpload(id, fileName, xhr);
398
+ self._options.onUpload(id, fileName, xhr);
399
+ }
400
+ });
401
+
402
+ return handler;
403
+ },
404
+ _preventLeaveInProgress: function(){
405
+ var self = this;
406
+
407
+ this._attach(window, 'beforeunload', function(e){
408
+ if (!self._filesInProgress){return;}
409
+
410
+ var e = e || window.event;
411
+ // for ie, ff
412
+ e.returnValue = self._options.messages.onLeave;
413
+ // for webkit
414
+ return self._options.messages.onLeave;
415
+ });
416
+ },
417
+ _onSubmit: function(id, fileName){
418
+ if (this._options.autoUpload) {
419
+ this._filesInProgress++;
420
+ }
421
+ },
422
+ _onProgress: function(id, fileName, loaded, total){
423
+ },
424
+ _onComplete: function(id, fileName, result){
425
+ this._filesInProgress--;
426
+
427
+ if (!result.success){
428
+ var errorReason = result.error ? result.error : "Upload failure reason unknown";
429
+ this._options.onError(id, fileName, errorReason);
430
+ }
431
+ },
432
+ _onCancel: function(id, fileName){
433
+ var storedFileIndex = qq.indexOf(this._storedFileIds, id);
434
+ if (this._options.autoUpload || storedFileIndex < 0) {
435
+ this._filesInProgress--;
436
+ }
437
+ else if (!this._options.autoUpload) {
438
+ this._storedFileIds.splice(storedFileIndex, 1);
439
+ }
440
+ },
441
+ _onUpload: function(id, fileName, xhr){
442
+ },
443
+ _onInputChange: function(input){
444
+ if (this._handler instanceof qq.UploadHandlerXhr){
445
+ this._uploadFileList(input.files);
446
+ } else {
447
+ if (this._validateFile(input)){
448
+ this._uploadFile(input);
449
+ }
450
+ }
451
+ this._button.reset();
452
+ },
453
+ _uploadFileList: function(files){
454
+ if (files.length > 0) {
455
+ for (var i=0; i<files.length; i++){
456
+ if (this._validateFile(files[i])){
457
+ this._uploadFile(files[i]);
458
+ } else {
459
+ if (this._options.stopOnFirstInvalidFile){
460
+ return;
461
+ }
462
+ }
463
+ }
464
+ }
465
+ else {
466
+ this._error('noFilesError', "");
467
+ }
468
+ },
469
+ _uploadFile: function(fileContainer){
470
+ var id = this._handler.add(fileContainer);
471
+ var fileName = this._handler.getName(id);
472
+
473
+ if (this._options.onSubmit(id, fileName) !== false){
474
+ this._onSubmit(id, fileName);
475
+ if (this._options.autoUpload) {
476
+ this._handler.upload(id, this._options.params);
477
+ }
478
+ else {
479
+ this._storeFileForLater(id);
480
+ }
481
+ }
482
+ },
483
+ _storeFileForLater: function(id) {
484
+ this._storedFileIds.push(id);
485
+ },
486
+ _validateFile: function(file){
487
+ var name, size;
488
+
489
+ if (file.value){
490
+ // it is a file input
491
+ // get input value and remove path to normalize
492
+ name = file.value.replace(/.*(\/|\\)/, "");
493
+ } else {
494
+ // fix missing properties in Safari 4 and firefox 11.0a2
495
+ name = (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
496
+ size = (file.fileSize !== null && file.fileSize !== undefined) ? file.fileSize : file.size;
497
+ }
498
+
499
+ if (! this._isAllowedExtension(name)){
500
+ this._error('typeError', name);
501
+ return false;
502
+
503
+ } else if (size === 0){
504
+ this._error('emptyError', name);
505
+ return false;
506
+
507
+ } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){
508
+ this._error('sizeError', name);
509
+ return false;
510
+
511
+ } else if (size && size < this._options.minSizeLimit){
512
+ this._error('minSizeError', name);
513
+ return false;
514
+ }
515
+
516
+ return true;
517
+ },
518
+ _error: function(code, fileName){
519
+ var message = this._options.messages[code];
520
+ function r(name, replacement){ message = message.replace(name, replacement); }
521
+
522
+ var extensions = this._options.allowedExtensions.join(', ');
523
+
524
+ r('{file}', this._formatFileName(fileName));
525
+ r('{extensions}', extensions);
526
+ r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
527
+ r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
528
+
529
+ this._options.onError(null, fileName, message);
530
+ this._options.showMessage(message);
531
+ },
532
+ _formatFileName: function(name){
533
+ if (name.length > 33){
534
+ name = name.slice(0, 19) + '...' + name.slice(-13);
535
+ }
536
+ return name;
537
+ },
538
+ _isAllowedExtension: function(fileName){
539
+ var ext = (-1 !== fileName.indexOf('.'))
540
+ ? fileName.replace(/.*[.]/, '').toLowerCase()
541
+ : '';
542
+ var allowed = this._options.allowedExtensions;
543
+
544
+ if (!allowed.length){return true;}
545
+
546
+ for (var i=0; i<allowed.length; i++){
547
+ if (allowed[i].toLowerCase() == ext){ return true;}
548
+ }
549
+
550
+ return false;
551
+ },
552
+ _formatSize: function(bytes){
553
+ var i = -1;
554
+ do {
555
+ bytes = bytes / 1024;
556
+ i++;
557
+ } while (bytes > 99);
558
+
559
+ return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
560
+ },
561
+ _wrapCallbacks: function() {
562
+ var self, safeCallback;
563
+
564
+ self = this;
565
+
566
+ safeCallback = function(callback, args) {
567
+ try {
568
+ return callback.apply(self, args);
569
+ }
570
+ catch (exception) {
571
+ self.log("Caught " + exception + " in callback: " + callback);
572
+ }
573
+ }
574
+
575
+ for (var prop in this._options) {
576
+ if (/^on[A-Z]/.test(prop)) {
577
+ (function() {
578
+ var oldCallback = self._options[prop];
579
+ self._options[prop] = function() {
580
+ return safeCallback(oldCallback, arguments);
581
+ }
582
+ }());
583
+ }
584
+ }
585
+ }
586
+ };
587
+
588
+
589
+ /**
590
+ * Class that creates upload widget with drag-and-drop and file list
591
+ * @inherits qq.FileUploaderBasic
592
+ */
593
+ qq.FileUploader = function(o){
594
+ // call parent constructor
595
+ qq.FileUploaderBasic.apply(this, arguments);
596
+
597
+ // additional options
598
+ qq.extend(this._options, {
599
+ element: null,
600
+ // if set, will be used instead of qq-upload-list in template
601
+ listElement: null,
602
+ dragText: 'Drop files here to upload',
603
+ extraDropzones : [],
604
+ hideDropzones : true,
605
+ disableDefaultDropzone: false,
606
+ uploadButtonText: 'Upload a file',
607
+ cancelButtonText: 'Cancel',
608
+ failUploadText: 'Upload failed',
609
+
610
+ template: '<div class="qq-uploader">' +
611
+ (!this._options.disableDefaultDropzone ? '<div class="qq-upload-drop-area"><span>{dragText}</span></div>' : '') +
612
+ (!this._options.button ? '<div class="qq-upload-button">{uploadButtonText}</div>' : '') +
613
+ (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') +
614
+ '</div>',
615
+
616
+ // template for one item in file list
617
+ fileTemplate: '<li>' +
618
+ '<div class="qq-progress-bar"></div>' +
619
+ '<span class="qq-upload-spinner"></span>' +
620
+ '<span class="qq-upload-finished"></span>' +
621
+ '<span class="qq-upload-file"></span>' +
622
+ '<span class="qq-upload-size"></span>' +
623
+ '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
624
+ '<span class="qq-upload-failed-text">{failUploadtext}</span>' +
625
+ '</li>',
626
+
627
+ classes: {
628
+ // used to get elements from templates
629
+ button: 'qq-upload-button',
630
+ drop: 'qq-upload-drop-area',
631
+ dropActive: 'qq-upload-drop-area-active',
632
+ dropDisabled: 'qq-upload-drop-area-disabled',
633
+ list: 'qq-upload-list',
634
+ progressBar: 'qq-progress-bar',
635
+ file: 'qq-upload-file',
636
+ spinner: 'qq-upload-spinner',
637
+ finished: 'qq-upload-finished',
638
+ size: 'qq-upload-size',
639
+ cancel: 'qq-upload-cancel',
640
+ failText: 'qq-upload-failed-text',
641
+
642
+ // added to list item <li> when upload completes
643
+ // used in css to hide progress spinner
644
+ success: 'qq-upload-success',
645
+ fail: 'qq-upload-fail',
646
+
647
+ successIcon: null,
648
+ failIcon: null
649
+ },
650
+ extraMessages: {
651
+ formatProgress: "{percent}% of {total_size}",
652
+ tooManyFilesError: "You may only drop one file"
653
+ },
654
+ failedUploadTextDisplay: {
655
+ mode: 'default', //default, custom, or none
656
+ maxChars: 50,
657
+ responseProperty: 'error',
658
+ enableTooltip: true
659
+ }
660
+ });
661
+ // overwrite options with user supplied
662
+ qq.extend(this._options, o);
663
+ this._wrapCallbacks();
664
+
665
+ qq.extend(this._options.messages, this._options.extraMessages);
666
+
667
+ // overwrite the upload button text if any
668
+ // same for the Cancel button and Fail message text
669
+ this._options.template = this._options.template.replace(/\{dragText\}/g, this._options.dragText);
670
+ this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.uploadButtonText);
671
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.cancelButtonText);
672
+ this._options.fileTemplate = this._options.fileTemplate.replace(/\{failUploadtext\}/g, this._options.failUploadText);
673
+
674
+ this._element = this._options.element;
675
+ this._element.innerHTML = this._options.template;
676
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
677
+
678
+ this._classes = this._options.classes;
679
+
680
+ if (!this._button) {
681
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
682
+ }
683
+
684
+ this._bindCancelEvent();
685
+ this._setupDragDrop();
686
+ };
687
+
688
+ // inherit from Basic Uploader
689
+ qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
690
+
691
+ qq.extend(qq.FileUploader.prototype, {
692
+ clearStoredFiles: function() {
693
+ qq.FileUploaderBasic.prototype.clearStoredFiles.apply(this, arguments);
694
+ this._listElement.innerHTML = "";
695
+ },
696
+ addExtraDropzone: function(element){
697
+ this._setupExtraDropzone(element);
698
+ },
699
+ removeExtraDropzone: function(element){
700
+ var dzs = this._options.extraDropzones;
701
+ for(var i in dzs) if (dzs[i] === element) return this._options.extraDropzones.splice(i,1);
702
+ },
703
+ _leaving_document_out: function(e){
704
+ return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows
705
+ || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox
706
+ },
707
+ _storeFileForLater: function(id) {
708
+ qq.FileUploaderBasic.prototype._storeFileForLater.apply(this, arguments);
709
+ var item = this._getItemByFileId(id);
710
+ this._find(item, 'spinner').style.display = "none";
711
+ },
712
+ /**
713
+ * Gets one of the elements listed in this._options.classes
714
+ **/
715
+ _find: function(parent, type){
716
+ var element = qq.getByClass(parent, this._options.classes[type])[0];
717
+ if (!element){
718
+ throw new Error('element not found ' + type);
719
+ }
720
+
721
+ return element;
722
+ },
723
+ _setupExtraDropzone: function(element){
724
+ this._options.extraDropzones.push(element);
725
+ this._setupDropzone(element);
726
+ },
727
+ _setupDropzone: function(dropArea){
728
+ var self = this;
729
+
730
+ var dz = new qq.UploadDropZone({
731
+ element: dropArea,
732
+ onEnter: function(e){
733
+ qq.addClass(dropArea, self._classes.dropActive);
734
+ e.stopPropagation();
735
+ },
736
+ onLeave: function(e){
737
+ //e.stopPropagation();
738
+ },
739
+ onLeaveNotDescendants: function(e){
740
+ qq.removeClass(dropArea, self._classes.dropActive);
741
+ },
742
+ onDrop: function(e){
743
+ if (self._options.hideDropzones) {
744
+ dropArea.style.display = 'none';
745
+ }
746
+ qq.removeClass(dropArea, self._classes.dropActive);
747
+ if (e.dataTransfer.files.length > 1 && !self._options.multiple) {
748
+ self._error('tooManyFilesError', "");
749
+ }
750
+ else {
751
+ self._uploadFileList(e.dataTransfer.files);
752
+ }
753
+ }
754
+ });
755
+
756
+ this.addDisposer(function() { dz.dispose(); });
757
+
758
+ if (this._options.hideDropzones) {
759
+ dropArea.style.display = 'none';
760
+ }
761
+ },
762
+ _setupDragDrop: function(){
763
+ var self = this;
764
+
765
+ if (!this._options.disableDefaultDropzone) {
766
+ var dropArea = this._find(this._element, 'drop');
767
+ this._options.extraDropzones.push(dropArea);
768
+ }
769
+
770
+ var dropzones = this._options.extraDropzones;
771
+ var i;
772
+ for (i=0; i < dropzones.length; i++){
773
+ this._setupDropzone(dropzones[i]);
774
+ }
775
+
776
+ // IE <= 9 does not support the File API used for drag+drop uploads
777
+ // Any volunteers to enable & test this for IE10?
778
+ if (!this._options.disableDefaultDropzone && !qq.ie()) {
779
+ this._attach(document, 'dragenter', function(e){
780
+ if (qq.hasClass(dropArea, self._classes.dropDisabled)) return;
781
+
782
+ dropArea.style.display = 'block';
783
+ for (i=0; i < dropzones.length; i++){ dropzones[i].style.display = 'block'; }
784
+
785
+ });
786
+ }
787
+ this._attach(document, 'dragleave', function(e){
788
+ // only fire when leaving document out
789
+ if (self._options.hideDropzones && qq.FileUploader.prototype._leaving_document_out(e)) {
790
+ for (i=0; i < dropzones.length; i++) {
791
+ dropzones[i].style.display = 'none';
792
+ }
793
+ }
794
+ });
795
+ qq.attach(document, 'drop', function(e){
796
+ if (self._options.hideDropzones) {
797
+ for (i=0; i < dropzones.length; i++){
798
+ dropzones[i].style.display = 'none';
799
+ }
800
+ }
801
+ e.preventDefault();
802
+ });
803
+ },
804
+ _onSubmit: function(id, fileName){
805
+ qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
806
+ this._addToList(id, fileName);
807
+ },
808
+ // Update the progress bar & percentage as the file is uploaded
809
+ _onProgress: function(id, fileName, loaded, total){
810
+ qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
811
+
812
+ var item = this._getItemByFileId(id);
813
+
814
+ if (loaded === total) {
815
+ var cancelLink = this._find(item, 'cancel');
816
+ cancelLink.style.display = 'none';
817
+ }
818
+
819
+ var size = this._find(item, 'size');
820
+ size.style.display = 'inline';
821
+
822
+ var text;
823
+ var percent = Math.round(loaded / total * 100);
824
+
825
+ if (loaded != total) {
826
+ // If still uploading, display percentage
827
+ text = this._formatProgress(loaded, total);
828
+ } else {
829
+ // If complete, just display final size
830
+ text = this._formatSize(total);
831
+ }
832
+
833
+ // Update progress bar <span> tag
834
+ this._find(item, 'progressBar').style.width = percent + '%';
835
+
836
+ qq.setText(size, text);
837
+ },
838
+ _onComplete: function(id, fileName, result){
839
+ qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
840
+
841
+ var item = this._getItemByFileId(id);
842
+
843
+ qq.remove(this._find(item, 'progressBar'));
844
+
845
+ if (!this._options.disableCancelForFormUploads || qq.UploadHandlerXhr.isSupported()) {
846
+ qq.remove(this._find(item, 'cancel'));
847
+ }
848
+ qq.remove(this._find(item, 'spinner'));
849
+
850
+ if (result.success){
851
+ qq.addClass(item, this._classes.success);
852
+ if (this._classes.successIcon) {
853
+ this._find(item, 'finished').style.display = "inline-block";
854
+ qq.addClass(item, this._classes.successIcon)
855
+ }
856
+ } else {
857
+ qq.addClass(item, this._classes.fail);
858
+ if (this._classes.failIcon) {
859
+ this._find(item, 'finished').style.display = "inline-block";
860
+ qq.addClass(item, this._classes.failIcon)
861
+ }
862
+ this._controlFailureTextDisplay(item, result);
863
+ }
864
+ },
865
+ _onUpload: function(id, fileName, xhr){
866
+ qq.FileUploaderBasic.prototype._onUpload.apply(this, arguments);
867
+
868
+ var item = this._getItemByFileId(id);
869
+
870
+ if (qq.UploadHandlerXhr.isSupported()) {
871
+ this._find(item, 'progressBar').style.display = "block";
872
+ }
873
+
874
+ var spinnerEl = this._find(item, 'spinner');
875
+ if (spinnerEl.style.display == "none") {
876
+ spinnerEl.style.display = "inline-block";
877
+ }
878
+ },
879
+ _addToList: function(id, fileName){
880
+ var item = qq.toElement(this._options.fileTemplate);
881
+ if (this._options.disableCancelForFormUploads && !qq.UploadHandlerXhr.isSupported()) {
882
+ var cancelLink = this._find(item, 'cancel');
883
+ qq.remove(cancelLink);
884
+ }
885
+
886
+ item.qqFileId = id;
887
+
888
+ var fileElement = this._find(item, 'file');
889
+ qq.setText(fileElement, this._formatFileName(fileName));
890
+ this._find(item, 'size').style.display = 'none';
891
+ if (!this._options.multiple) this._clearList();
892
+ this._listElement.appendChild(item);
893
+ },
894
+ _clearList: function(){
895
+ this._listElement.innerHTML = '';
896
+ this.clearStoredFiles();
897
+ },
898
+ _getItemByFileId: function(id){
899
+ var item = this._listElement.firstChild;
900
+
901
+ // there can't be txt nodes in dynamically created list
902
+ // and we can use nextSibling
903
+ while (item){
904
+ if (item.qqFileId == id) return item;
905
+ item = item.nextSibling;
906
+ }
907
+ },
908
+ /**
909
+ * delegate click event for cancel link
910
+ **/
911
+ _bindCancelEvent: function(){
912
+ var self = this,
913
+ list = this._listElement;
914
+
915
+ this._attach(list, 'click', function(e){
916
+ e = e || window.event;
917
+ var target = e.target || e.srcElement;
918
+
919
+ if (qq.hasClass(target, self._classes.cancel)){
920
+ qq.preventDefault(e);
921
+
922
+ var item = target.parentNode;
923
+ self._handler.cancel(item.qqFileId);
924
+ qq.remove(item);
925
+ }
926
+ });
927
+ },
928
+ _formatProgress: function (uploadedSize, totalSize) {
929
+ var message = this._options.messages.formatProgress;
930
+ function r(name, replacement) { message = message.replace(name, replacement); }
931
+
932
+ r('{percent}', Math.round(uploadedSize / totalSize * 100));
933
+ r('{total_size}', this._formatSize(totalSize));
934
+ return message;
935
+ },
936
+ _controlFailureTextDisplay: function(item, response) {
937
+ var mode, maxChars, responseProperty, failureReason, shortFailureReason;
938
+
939
+ mode = this._options.failedUploadTextDisplay.mode;
940
+ maxChars = this._options.failedUploadTextDisplay.maxChars;
941
+ responseProperty = this._options.failedUploadTextDisplay.responseProperty;
942
+
943
+ if (mode === 'custom') {
944
+ var failureReason = response[responseProperty];
945
+ if (failureReason) {
946
+ if (failureReason.length > maxChars) {
947
+ shortFailureReason = failureReason.substring(0, maxChars) + '...';
948
+ }
949
+ qq.setText(this._find(item, 'failText'), shortFailureReason || failureReason);
950
+
951
+ if (this._options.failedUploadTextDisplay.enableTooltip) {
952
+ this._showTooltip(item, failureReason);
953
+ }
954
+ }
955
+ else {
956
+ this.log("'" + responseProperty + "' is not a valid property on the server response.");
957
+ }
958
+ }
959
+ else if (mode === 'none') {
960
+ qq.remove(this._find(item, 'failText'));
961
+ }
962
+ else if (mode !== 'default') {
963
+ this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid");
964
+ }
965
+ },
966
+ //TODO turn this into a real tooltip, with click trigger (so it is usable on mobile devices). See case #355 for details.
967
+ _showTooltip: function(item, text) {
968
+ item.title = text;
969
+ }
970
+ });
971
+
972
+ qq.UploadDropZone = function(o){
973
+ this._options = {
974
+ element: null,
975
+ onEnter: function(e){},
976
+ onLeave: function(e){},
977
+ // is not fired when leaving element by hovering descendants
978
+ onLeaveNotDescendants: function(e){},
979
+ onDrop: function(e){}
980
+ };
981
+ qq.extend(this._options, o);
982
+ qq.extend(this, qq.DisposeSupport);
983
+
984
+ this._element = this._options.element;
985
+
986
+ this._disableDropOutside();
987
+ this._attachEvents();
988
+ };
989
+
990
+ qq.UploadDropZone.prototype = {
991
+ _dragover_should_be_canceled: function(){
992
+ return qq.safari() || (qq.firefox() && qq.windows());
993
+ },
994
+ _disableDropOutside: function(e){
995
+ // run only once for all instances
996
+ if (!qq.UploadDropZone.dropOutsideDisabled ){
997
+
998
+ // for these cases we need to catch onDrop to reset dropArea
999
+ if (this._dragover_should_be_canceled){
1000
+ qq.attach(document, 'dragover', function(e){
1001
+ e.preventDefault();
1002
+ });
1003
+ } else {
1004
+ qq.attach(document, 'dragover', function(e){
1005
+ if (e.dataTransfer){
1006
+ e.dataTransfer.dropEffect = 'none';
1007
+ e.preventDefault();
1008
+ }
1009
+ });
1010
+ }
1011
+
1012
+ qq.UploadDropZone.dropOutsideDisabled = true;
1013
+ }
1014
+ },
1015
+ _attachEvents: function(){
1016
+ var self = this;
1017
+
1018
+ self._attach(self._element, 'dragover', function(e){
1019
+ if (!self._isValidFileDrag(e)) return;
1020
+
1021
+ var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
1022
+ if (effect == 'move' || effect == 'linkMove'){
1023
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
1024
+ } else {
1025
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
1026
+ }
1027
+
1028
+ e.stopPropagation();
1029
+ e.preventDefault();
1030
+ });
1031
+
1032
+ self._attach(self._element, 'dragenter', function(e){
1033
+ if (!self._isValidFileDrag(e)) return;
1034
+
1035
+ self._options.onEnter(e);
1036
+ });
1037
+
1038
+ self._attach(self._element, 'dragleave', function(e){
1039
+ if (!self._isValidFileDrag(e)) return;
1040
+
1041
+ self._options.onLeave(e);
1042
+
1043
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
1044
+ // do not fire when moving a mouse over a descendant
1045
+ if (qq.contains(this, relatedTarget)) return;
1046
+
1047
+ self._options.onLeaveNotDescendants(e);
1048
+ });
1049
+
1050
+ self._attach(self._element, 'drop', function(e){
1051
+ if (!self._isValidFileDrag(e)) return;
1052
+
1053
+ e.preventDefault();
1054
+ self._options.onDrop(e);
1055
+ });
1056
+ },
1057
+ _isValidFileDrag: function(e){
1058
+ // e.dataTransfer currently causing IE errors
1059
+ // IE9 does NOT support file API, so drag-and-drop is not possible
1060
+ // IE10 should work, but currently has not been tested - any volunteers?
1061
+ if (qq.ie()) return false;
1062
+
1063
+ var dt = e.dataTransfer,
1064
+ // do not check dt.types.contains in webkit, because it crashes safari 4
1065
+ isSafari = qq.safari();
1066
+
1067
+ // dt.effectAllowed is none in Safari 5
1068
+ // dt.types.contains check is for firefox
1069
+ return dt && dt.effectAllowed != 'none' &&
1070
+ (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
1071
+
1072
+ }
1073
+ };
1074
+
1075
+ qq.UploadButton = function(o){
1076
+ this._options = {
1077
+ element: null,
1078
+ // if set to true adds multiple attribute to file input
1079
+ multiple: false,
1080
+ acceptFiles: null,
1081
+ // name attribute of file input
1082
+ name: 'file',
1083
+ onChange: function(input){},
1084
+ hoverClass: 'qq-upload-button-hover',
1085
+ focusClass: 'qq-upload-button-focus'
1086
+ };
1087
+
1088
+ qq.extend(this._options, o);
1089
+ qq.extend(this, qq.DisposeSupport);
1090
+
1091
+ this._element = this._options.element;
1092
+
1093
+ // make button suitable container for input
1094
+ qq.css(this._element, {
1095
+ position: 'relative',
1096
+ overflow: 'hidden',
1097
+ // Make sure browse button is in the right side
1098
+ // in Internet Explorer
1099
+ direction: 'ltr'
1100
+ });
1101
+
1102
+ this._input = this._createInput();
1103
+ };
1104
+
1105
+ qq.UploadButton.prototype = {
1106
+ /* returns file input element */
1107
+ getInput: function(){
1108
+ return this._input;
1109
+ },
1110
+ /* cleans/recreates the file input */
1111
+ reset: function(){
1112
+ if (this._input.parentNode){
1113
+ qq.remove(this._input);
1114
+ }
1115
+
1116
+ qq.removeClass(this._element, this._options.focusClass);
1117
+ this._input = this._createInput();
1118
+ },
1119
+ _createInput: function(){
1120
+ var input = document.createElement("input");
1121
+
1122
+ if (this._options.multiple){
1123
+ input.setAttribute("multiple", "multiple");
1124
+ }
1125
+
1126
+ if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles);
1127
+
1128
+ input.setAttribute("type", "file");
1129
+ input.setAttribute("name", this._options.name);
1130
+
1131
+ qq.css(input, {
1132
+ position: 'absolute',
1133
+ // in Opera only 'browse' button
1134
+ // is clickable and it is located at
1135
+ // the right side of the input
1136
+ right: 0,
1137
+ top: 0,
1138
+ fontFamily: 'Arial',
1139
+ // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
1140
+ fontSize: '118px',
1141
+ margin: 0,
1142
+ padding: 0,
1143
+ cursor: 'pointer',
1144
+ opacity: 0
1145
+ });
1146
+
1147
+ this._element.appendChild(input);
1148
+
1149
+ var self = this;
1150
+ this._attach(input, 'change', function(){
1151
+ self._options.onChange(input);
1152
+ });
1153
+
1154
+ this._attach(input, 'mouseover', function(){
1155
+ qq.addClass(self._element, self._options.hoverClass);
1156
+ });
1157
+ this._attach(input, 'mouseout', function(){
1158
+ qq.removeClass(self._element, self._options.hoverClass);
1159
+ });
1160
+ this._attach(input, 'focus', function(){
1161
+ qq.addClass(self._element, self._options.focusClass);
1162
+ });
1163
+ this._attach(input, 'blur', function(){
1164
+ qq.removeClass(self._element, self._options.focusClass);
1165
+ });
1166
+
1167
+ // IE and Opera, unfortunately have 2 tab stops on file input
1168
+ // which is unacceptable in our case, disable keyboard access
1169
+ if (window.attachEvent){
1170
+ // it is IE or Opera
1171
+ input.setAttribute('tabIndex', "-1");
1172
+ }
1173
+
1174
+ return input;
1175
+ }
1176
+ };
1177
+
1178
+ /**
1179
+ * Class for uploading files, uploading itself is handled by child classes
1180
+ */
1181
+ qq.UploadHandlerAbstract = function(o){
1182
+ // Default options, can be overridden by the user
1183
+ this._options = {
1184
+ debug: false,
1185
+ action: '/upload.php',
1186
+ // maximum number of concurrent uploads
1187
+ maxConnections: 999,
1188
+ onProgress: function(id, fileName, loaded, total){},
1189
+ onComplete: function(id, fileName, response){},
1190
+ onCancel: function(id, fileName){},
1191
+ onUpload: function(id, fileName, xhr){}
1192
+ };
1193
+ qq.extend(this._options, o);
1194
+
1195
+ this._queue = [];
1196
+ // params for files in queue
1197
+ this._params = [];
1198
+ };
1199
+ qq.UploadHandlerAbstract.prototype = {
1200
+ log: function(str){
1201
+ if (this._options.debug && window.console) console.log('[uploader] ' + str);
1202
+ },
1203
+ /**
1204
+ * Adds file or file input to the queue
1205
+ * @returns id
1206
+ **/
1207
+ add: function(file){},
1208
+ /**
1209
+ * Sends the file identified by id and additional query params to the server
1210
+ */
1211
+ upload: function(id, params){
1212
+ var len = this._queue.push(id);
1213
+
1214
+ var copy = {};
1215
+ qq.extend(copy, params);
1216
+ this._params[id] = copy;
1217
+
1218
+ // if too many active uploads, wait...
1219
+ if (len <= this._options.maxConnections){
1220
+ this._upload(id, this._params[id]);
1221
+ }
1222
+ },
1223
+ /**
1224
+ * Cancels file upload by id
1225
+ */
1226
+ cancel: function(id){
1227
+ this._cancel(id);
1228
+ this._dequeue(id);
1229
+ },
1230
+ /**
1231
+ * Cancells all uploads
1232
+ */
1233
+ cancelAll: function(){
1234
+ for (var i=0; i<this._queue.length; i++){
1235
+ this._cancel(this._queue[i]);
1236
+ }
1237
+ this._queue = [];
1238
+ },
1239
+ /**
1240
+ * Returns name of the file identified by id
1241
+ */
1242
+ getName: function(id){},
1243
+ /**
1244
+ * Returns size of the file identified by id
1245
+ */
1246
+ getSize: function(id){},
1247
+ /**
1248
+ * Returns id of files being uploaded or
1249
+ * waiting for their turn
1250
+ */
1251
+ getQueue: function(){
1252
+ return this._queue;
1253
+ },
1254
+ /**
1255
+ * Actual upload method
1256
+ */
1257
+ _upload: function(id){},
1258
+ /**
1259
+ * Actual cancel method
1260
+ */
1261
+ _cancel: function(id){},
1262
+ /**
1263
+ * Removes element from queue, starts upload of next
1264
+ */
1265
+ _dequeue: function(id){
1266
+ var i = qq.indexOf(this._queue, id);
1267
+ this._queue.splice(i, 1);
1268
+
1269
+ var max = this._options.maxConnections;
1270
+
1271
+ if (this._queue.length >= max && i < max){
1272
+ var nextId = this._queue[max-1];
1273
+ this._upload(nextId, this._params[nextId]);
1274
+ }
1275
+ }
1276
+ };
1277
+
1278
+ /**
1279
+ * Class for uploading files using form and iframe
1280
+ * @inherits qq.UploadHandlerAbstract
1281
+ */
1282
+ qq.UploadHandlerForm = function(o){
1283
+ qq.UploadHandlerAbstract.apply(this, arguments);
1284
+
1285
+ this._inputs = {};
1286
+ this._detach_load_events = {};
1287
+ };
1288
+ // @inherits qq.UploadHandlerAbstract
1289
+ qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
1290
+
1291
+ qq.extend(qq.UploadHandlerForm.prototype, {
1292
+ add: function(fileInput){
1293
+ fileInput.setAttribute('name', this._options.inputName);
1294
+ var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
1295
+
1296
+ this._inputs[id] = fileInput;
1297
+
1298
+ // remove file input from DOM
1299
+ if (fileInput.parentNode){
1300
+ qq.remove(fileInput);
1301
+ }
1302
+
1303
+ return id;
1304
+ },
1305
+ getName: function(id){
1306
+ // get input value and remove path to normalize
1307
+ return this._inputs[id].value.replace(/.*(\/|\\)/, "");
1308
+ },
1309
+ _cancel: function(id){
1310
+ this._options.onCancel(id, this.getName(id));
1311
+
1312
+ delete this._inputs[id];
1313
+ delete this._detach_load_events[id];
1314
+
1315
+ var iframe = document.getElementById(id);
1316
+ if (iframe){
1317
+ // to cancel request set src to something else
1318
+ // we use src="javascript:false;" because it doesn't
1319
+ // trigger ie6 prompt on https
1320
+ iframe.setAttribute('src', 'javascript:false;');
1321
+
1322
+ qq.remove(iframe);
1323
+ }
1324
+ },
1325
+ _upload: function(id, params){
1326
+ this._options.onUpload(id, this.getName(id), false);
1327
+ var input = this._inputs[id];
1328
+
1329
+ if (!input){
1330
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
1331
+ }
1332
+
1333
+ var fileName = this.getName(id);
1334
+ params[this._options.inputName] = fileName;
1335
+
1336
+ var iframe = this._createIframe(id);
1337
+ var form = this._createForm(iframe, params);
1338
+ form.appendChild(input);
1339
+
1340
+ var self = this;
1341
+ this._attachLoadEvent(iframe, function(){
1342
+ self.log('iframe loaded');
1343
+
1344
+ var response = self._getIframeContentJSON(iframe);
1345
+
1346
+ self._options.onComplete(id, fileName, response);
1347
+ self._dequeue(id);
1348
+
1349
+ delete self._inputs[id];
1350
+ // timeout added to fix busy state in FF3.6
1351
+ setTimeout(function(){
1352
+ self._detach_load_events[id]();
1353
+ delete self._detach_load_events[id];
1354
+ qq.remove(iframe);
1355
+ }, 1);
1356
+ });
1357
+
1358
+ form.submit();
1359
+ qq.remove(form);
1360
+
1361
+ return id;
1362
+ },
1363
+ _attachLoadEvent: function(iframe, callback){
1364
+ this._detach_load_events[iframe.id] = qq.attach(iframe, 'load', function(){
1365
+ // when we remove iframe from dom
1366
+ // the request stops, but in IE load
1367
+ // event fires
1368
+ if (!iframe.parentNode){
1369
+ return;
1370
+ }
1371
+
1372
+ try {
1373
+ // fixing Opera 10.53
1374
+ if (iframe.contentDocument &&
1375
+ iframe.contentDocument.body &&
1376
+ iframe.contentDocument.body.innerHTML == "false"){
1377
+ // In Opera event is fired second time
1378
+ // when body.innerHTML changed from false
1379
+ // to server response approx. after 1 sec
1380
+ // when we upload file with iframe
1381
+ return;
1382
+ }
1383
+ }
1384
+ catch (error) {
1385
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1386
+ }
1387
+
1388
+ callback();
1389
+ });
1390
+ },
1391
+ /**
1392
+ * Returns json object received by iframe from server.
1393
+ */
1394
+ _getIframeContentJSON: function(iframe){
1395
+ //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
1396
+ try {
1397
+ // iframe.contentWindow.document - for IE<7
1398
+ var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1399
+ response;
1400
+
1401
+ var innerHTML = doc.body.innerHTML;
1402
+ this.log("converting iframe's innerHTML to JSON");
1403
+ this.log("innerHTML = " + innerHTML);
1404
+ //plain text response may be wrapped in <pre> tag
1405
+ if (innerHTML.slice(0, 5).toLowerCase() == '<pre>' && innerHTML.slice(-6).toLowerCase() == '</pre>') {
1406
+ innerHTML = doc.body.firstChild.firstChild.nodeValue;
1407
+ }
1408
+ response = eval("(" + innerHTML + ")");
1409
+ } catch(err){
1410
+ response = {success: false};
1411
+ }
1412
+
1413
+ return response;
1414
+ },
1415
+ /**
1416
+ * Creates iframe with unique name
1417
+ */
1418
+ _createIframe: function(id){
1419
+ // We can't use following code as the name attribute
1420
+ // won't be properly registered in IE6, and new window
1421
+ // on form submit will open
1422
+ // var iframe = document.createElement('iframe');
1423
+ // iframe.setAttribute('name', id);
1424
+
1425
+ var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1426
+ // src="javascript:false;" removes ie6 prompt on https
1427
+
1428
+ iframe.setAttribute('id', id);
1429
+
1430
+ iframe.style.display = 'none';
1431
+ document.body.appendChild(iframe);
1432
+
1433
+ return iframe;
1434
+ },
1435
+ /**
1436
+ * Creates form, that will be submitted to iframe
1437
+ */
1438
+ _createForm: function(iframe, params){
1439
+ // We can't use the following code in IE6
1440
+ // var form = document.createElement('form');
1441
+ // form.setAttribute('method', 'post');
1442
+ // form.setAttribute('enctype', 'multipart/form-data');
1443
+ // Because in this case file won't be attached to request
1444
+ var protocol = this._options.demoMode ? "GET" : "POST"
1445
+ var form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>');
1446
+
1447
+ var queryString = qq.obj2url(params, this._options.action);
1448
+
1449
+ form.setAttribute('action', queryString);
1450
+ form.setAttribute('target', iframe.name);
1451
+ form.style.display = 'none';
1452
+ document.body.appendChild(form);
1453
+
1454
+ return form;
1455
+ }
1456
+ });
1457
+
1458
+ /**
1459
+ * Class for uploading files using xhr
1460
+ * @inherits qq.UploadHandlerAbstract
1461
+ */
1462
+ qq.UploadHandlerXhr = function(o){
1463
+ qq.UploadHandlerAbstract.apply(this, arguments);
1464
+
1465
+ this._files = [];
1466
+ this._xhrs = [];
1467
+
1468
+ // current loaded size in bytes for each file
1469
+ this._loaded = [];
1470
+ };
1471
+
1472
+ // static method
1473
+ qq.UploadHandlerXhr.isSupported = function(){
1474
+ var input = document.createElement('input');
1475
+ input.type = 'file';
1476
+
1477
+ return (
1478
+ 'multiple' in input &&
1479
+ typeof File != "undefined" &&
1480
+ typeof FormData != "undefined" &&
1481
+ typeof (new XMLHttpRequest()).upload != "undefined" );
1482
+ };
1483
+
1484
+ // @inherits qq.UploadHandlerAbstract
1485
+ qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1486
+
1487
+ qq.extend(qq.UploadHandlerXhr.prototype, {
1488
+ /**
1489
+ * Adds file to the queue
1490
+ * Returns id to use with upload, cancel
1491
+ **/
1492
+ add: function(file){
1493
+ if (!(file instanceof File)){
1494
+ throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1495
+ }
1496
+
1497
+ return this._files.push(file) - 1;
1498
+ },
1499
+ getName: function(id){
1500
+ var file = this._files[id];
1501
+ // fix missing name in Safari 4
1502
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
1503
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
1504
+ },
1505
+ getSize: function(id){
1506
+ var file = this._files[id];
1507
+ return file.fileSize != null ? file.fileSize : file.size;
1508
+ },
1509
+ /**
1510
+ * Returns uploaded bytes for file identified by id
1511
+ */
1512
+ getLoaded: function(id){
1513
+ return this._loaded[id] || 0;
1514
+ },
1515
+ /**
1516
+ * Sends the file identified by id and additional query params to the server
1517
+ * @param {Object} params name-value string pairs
1518
+ */
1519
+ _upload: function(id, params){
1520
+ this._options.onUpload(id, this.getName(id), true);
1521
+
1522
+ var file = this._files[id],
1523
+ name = this.getName(id),
1524
+ size = this.getSize(id);
1525
+
1526
+ this._loaded[id] = 0;
1527
+
1528
+ var xhr = this._xhrs[id] = new XMLHttpRequest();
1529
+ var self = this;
1530
+
1531
+ xhr.upload.onprogress = function(e){
1532
+ if (e.lengthComputable){
1533
+ self._loaded[id] = e.loaded;
1534
+ self._options.onProgress(id, name, e.loaded, e.total);
1535
+ }
1536
+ };
1537
+
1538
+ xhr.onreadystatechange = function(){
1539
+ if (xhr.readyState == 4){
1540
+ self._onComplete(id, xhr);
1541
+ }
1542
+ };
1543
+
1544
+ // build query string
1545
+ params = params || {};
1546
+ params[this._options.inputName] = name;
1547
+ var queryString = qq.obj2url(params, this._options.action);
1548
+
1549
+ var protocol = this._options.demoMode ? "GET" : "POST";
1550
+ xhr.open(protocol, queryString, true);
1551
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1552
+ xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1553
+ xhr.setRequestHeader("Cache-Control", "no-cache");
1554
+ if (this._options.forceMultipart) {
1555
+ var formData = new FormData();
1556
+ formData.append(this._options.inputName, file);
1557
+ file = formData;
1558
+ } else {
1559
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
1560
+ //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
1561
+ xhr.setRequestHeader("X-Mime-Type",file.type );
1562
+ }
1563
+ for (key in this._options.customHeaders){
1564
+ xhr.setRequestHeader(key, this._options.customHeaders[key]);
1565
+ };
1566
+ xhr.send(file);
1567
+ },
1568
+ _onComplete: function(id, xhr){
1569
+ "use strict";
1570
+ // the request was aborted/cancelled
1571
+ if (!this._files[id]) { return; }
1572
+
1573
+ var name = this.getName(id);
1574
+ var size = this.getSize(id);
1575
+ var response; //the parsed JSON response from the server, or the empty object if parsing failed.
1576
+
1577
+ this._options.onProgress(id, name, size, size);
1578
+
1579
+ this.log("xhr - server response received");
1580
+ this.log("responseText = " + xhr.responseText);
1581
+
1582
+ try {
1583
+ if (typeof JSON.parse === "function") {
1584
+ response = JSON.parse(xhr.responseText);
1585
+ } else {
1586
+ response = eval("(" + xhr.responseText + ")");
1587
+ }
1588
+ } catch(err){
1589
+ response = {};
1590
+ }
1591
+ if (xhr.status !== 200){
1592
+ this._options.onError(id, name, "XHR returned response code " + xhr.status);
1593
+ }
1594
+ this._options.onComplete(id, name, response);
1595
+
1596
+ this._xhrs[id] = null;
1597
+ this._dequeue(id);
1598
+ },
1599
+ _cancel: function(id){
1600
+ this._options.onCancel(id, this.getName(id));
1601
+
1602
+ this._files[id] = null;
1603
+
1604
+ if (this._xhrs[id]){
1605
+ this._xhrs[id].abort();
1606
+ this._xhrs[id] = null;
1607
+ }
1608
+ }
1609
+ });
1610
+
1611
+ /**
1612
+ * A generic module which supports object disposing in dispose() method.
1613
+ * */
1614
+ qq.DisposeSupport = {
1615
+ _disposers: [],
1616
+
1617
+ /** Run all registered disposers */
1618
+ dispose: function() {
1619
+ var disposer;
1620
+ while (disposer = this._disposers.shift()) {
1621
+ disposer();
1622
+ }
1623
+ },
1624
+
1625
+ /** Add disposer to the collection */
1626
+ addDisposer: function(disposeFunction) {
1627
+ this._disposers.push(disposeFunction);
1628
+ },
1629
+
1630
+ /** Attach event handler and register de-attacher as a disposer */
1631
+ _attach: function() {
1632
+ this.addDisposer(qq.attach.apply(this, arguments));
1633
+ }
1634
+ };