fileuploader-rails 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };