fileuploader-rails 2.1.2

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