activeadmin_images 0.0.1 → 0.2.0

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