concen 0.1 → 0.1.1

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.
Files changed (118) hide show
  1. data/LICENSE +20 -0
  2. data/lib/concen/version.rb +1 -1
  3. data/public/concen/images/button-grid.png +0 -0
  4. data/public/concen/images/button-next-disabled.png +0 -0
  5. data/public/concen/images/button-next.png +0 -0
  6. data/public/concen/images/button-prev-disabled.png +0 -0
  7. data/public/concen/images/button-prev.png +0 -0
  8. data/public/concen/images/fileuploader-loading.gif +0 -0
  9. data/public/concen/images/glyph-small-check-alt.png +0 -0
  10. data/public/concen/images/glyph-small-check.png +0 -0
  11. data/public/concen/images/glyph-small-delete.png +0 -0
  12. data/public/concen/images/glyph-small-handle.png +0 -0
  13. data/public/concen/images/grid.png +0 -0
  14. data/public/concen/images/icon-handle.png +0 -0
  15. data/public/concen/images/magnifying-glass.png +0 -0
  16. data/public/concen/images/original/button-grid.png +0 -0
  17. data/public/concen/images/original/button-next-disabled.png +0 -0
  18. data/public/concen/images/original/button-next.png +0 -0
  19. data/public/concen/images/original/button-prev-disabled.png +0 -0
  20. data/public/concen/images/original/button-prev.png +0 -0
  21. data/public/concen/images/original/glyph-small-check-alt.png +0 -0
  22. data/public/concen/images/original/glyph-small-check.png +0 -0
  23. data/public/concen/images/original/glyph-small-delete.png +0 -0
  24. data/public/concen/images/original/glyph-small-handle.png +0 -0
  25. data/public/concen/images/original/magnifying-glass.png +0 -0
  26. data/public/concen/images/original/record-visit.gif +0 -0
  27. data/public/concen/images/original/search-field.png +0 -0
  28. data/public/concen/images/record-visit.gif +0 -0
  29. data/public/concen/images/search-field.png +0 -0
  30. data/public/concen/javascripts/ace/ace-uncompressed.js +17635 -0
  31. data/public/concen/javascripts/ace/ace.js +1 -0
  32. data/public/concen/javascripts/ace/cockpit-uncompressed.js +2504 -0
  33. data/public/concen/javascripts/ace/cockpit.js +1 -0
  34. data/public/concen/javascripts/ace/keybinding-emacs.js +1 -0
  35. data/public/concen/javascripts/ace/keybinding-vim.js +1 -0
  36. data/public/concen/javascripts/ace/mode-c_cpp.js +1 -0
  37. data/public/concen/javascripts/ace/mode-clojure.js +1 -0
  38. data/public/concen/javascripts/ace/mode-coffee.js +1 -0
  39. data/public/concen/javascripts/ace/mode-csharp.js +1 -0
  40. data/public/concen/javascripts/ace/mode-css.js +1 -0
  41. data/public/concen/javascripts/ace/mode-groovy.js +1 -0
  42. data/public/concen/javascripts/ace/mode-html.js +1 -0
  43. data/public/concen/javascripts/ace/mode-java.js +1 -0
  44. data/public/concen/javascripts/ace/mode-javascript.js +1 -0
  45. data/public/concen/javascripts/ace/mode-json.js +1 -0
  46. data/public/concen/javascripts/ace/mode-ocaml.js +1 -0
  47. data/public/concen/javascripts/ace/mode-perl.js +1 -0
  48. data/public/concen/javascripts/ace/mode-php.js +1 -0
  49. data/public/concen/javascripts/ace/mode-python.js +1 -0
  50. data/public/concen/javascripts/ace/mode-ruby.js +1 -0
  51. data/public/concen/javascripts/ace/mode-scad.js +1 -0
  52. data/public/concen/javascripts/ace/mode-scala.js +1 -0
  53. data/public/concen/javascripts/ace/mode-scss.js +1 -0
  54. data/public/concen/javascripts/ace/mode-svg.js +1 -0
  55. data/public/concen/javascripts/ace/mode-textile.js +1 -0
  56. data/public/concen/javascripts/ace/mode-xml.js +1 -0
  57. data/public/concen/javascripts/ace/theme-clouds.js +1 -0
  58. data/public/concen/javascripts/ace/theme-clouds_midnight.js +1 -0
  59. data/public/concen/javascripts/ace/theme-cobalt.js +1 -0
  60. data/public/concen/javascripts/ace/theme-crimson_editor.js +1 -0
  61. data/public/concen/javascripts/ace/theme-dawn.js +1 -0
  62. data/public/concen/javascripts/ace/theme-eclipse.js +1 -0
  63. data/public/concen/javascripts/ace/theme-idle_fingers.js +1 -0
  64. data/public/concen/javascripts/ace/theme-kr_theme.js +1 -0
  65. data/public/concen/javascripts/ace/theme-merbivore.js +1 -0
  66. data/public/concen/javascripts/ace/theme-merbivore_soft.js +1 -0
  67. data/public/concen/javascripts/ace/theme-mono_industrial.js +1 -0
  68. data/public/concen/javascripts/ace/theme-monokai.js +1 -0
  69. data/public/concen/javascripts/ace/theme-pastel_on_dark.js +1 -0
  70. data/public/concen/javascripts/ace/theme-solarized_dark.js +1 -0
  71. data/public/concen/javascripts/ace/theme-solarized_light.js +1 -0
  72. data/public/concen/javascripts/ace/theme-textmate.js +1 -0
  73. data/public/concen/javascripts/ace/theme-twilight.js +1 -0
  74. data/public/concen/javascripts/ace/theme-vibrant_ink.js +1 -0
  75. data/public/concen/javascripts/ace/worker-coffee.js +1 -0
  76. data/public/concen/javascripts/ace/worker-css.js +1 -0
  77. data/public/concen/javascripts/ace/worker-javascript.js +1 -0
  78. data/public/concen/javascripts/blank.gif +0 -0
  79. data/public/concen/javascripts/controlcenter.js +173 -0
  80. data/public/concen/javascripts/excanvas.js +1427 -0
  81. data/public/concen/javascripts/fileuploader.js +1251 -0
  82. data/public/concen/javascripts/jquery-ui/jquery.ui.autocomplete.js +612 -0
  83. data/public/concen/javascripts/jquery-ui/jquery.ui.core.js +312 -0
  84. data/public/concen/javascripts/jquery-ui/jquery.ui.draggable.js +823 -0
  85. data/public/concen/javascripts/jquery-ui/jquery.ui.droppable.js +296 -0
  86. data/public/concen/javascripts/jquery-ui/jquery.ui.mouse.js +160 -0
  87. data/public/concen/javascripts/jquery-ui/jquery.ui.position.js +252 -0
  88. data/public/concen/javascripts/jquery-ui/jquery.ui.resizable.js +842 -0
  89. data/public/concen/javascripts/jquery-ui/jquery.ui.sortable.js +1077 -0
  90. data/public/concen/javascripts/jquery-ui/jquery.ui.widget.js +262 -0
  91. data/public/concen/javascripts/jquery.browser.min.js +1 -0
  92. data/public/concen/javascripts/jquery.easing.1.3.js +205 -0
  93. data/public/concen/javascripts/jquery.equal-heights.js +30 -0
  94. data/public/concen/javascripts/jquery.flot.js +2599 -0
  95. data/public/concen/javascripts/jquery.flot.resize.js +60 -0
  96. data/public/concen/javascripts/jquery.flot.stack.js +184 -0
  97. data/public/concen/javascripts/jquery.js +8981 -0
  98. data/public/concen/javascripts/jquery.min.js +18 -0
  99. data/public/concen/javascripts/jquery.placeholder.js +101 -0
  100. data/public/concen/javascripts/jquery.ui.nestedSortable.js +356 -0
  101. data/public/concen/javascripts/jquery_ujs.js +316 -0
  102. data/public/concen/javascripts/modernizr.js +1116 -0
  103. data/public/concen/javascripts/page.js +153 -0
  104. data/public/concen/javascripts/performance.js +110 -0
  105. data/public/concen/javascripts/pxToEm.js +56 -0
  106. data/public/concen/javascripts/status.js +15 -0
  107. data/public/concen/javascripts/traffic.js +113 -0
  108. data/public/concen/javascripts/user.js +14 -0
  109. data/public/concen/stylesheets/application.css +1342 -0
  110. data/public/concen/stylesheets/fileuploader.css +31 -0
  111. data/public/concen/stylesheets/ie.css +124 -0
  112. data/public/concen/stylesheets/jquery.ui.base.css +11 -0
  113. data/public/concen/stylesheets/jquery.ui.core.css +41 -0
  114. data/public/concen/stylesheets/jquery.ui.resizable.css +20 -0
  115. data/public/concen/stylesheets/main.css +1225 -0
  116. data/public/concen/stylesheets/non_ios.css +28 -0
  117. data/public/concen/stylesheets/print.css +84 -0
  118. metadata +141 -25
@@ -0,0 +1,1251 @@
1
+ /**
2
+ * http://github.com/valums/file-uploader
3
+ *
4
+ * Multiple file upload component with progress-bar, drag-and-drop.
5
+ * © 2010 Andrew Valums ( andrew(at)valums.com )
6
+ *
7
+ * Licensed under GNU GPL 2 or later and GNU LGPL 2 or later, see license.txt.
8
+ */
9
+
10
+ //
11
+ // Helper functions
12
+ //
13
+
14
+ var qq = qq || {};
15
+
16
+ /**
17
+ * Adds all missing properties from second obj to first obj
18
+ */
19
+ qq.extend = function(first, second){
20
+ for (var prop in second){
21
+ first[prop] = second[prop];
22
+ }
23
+ };
24
+
25
+ /**
26
+ * Searches for a given element in the array, returns -1 if it is not present.
27
+ * @param {Number} [from] The index at which to begin the search
28
+ */
29
+ qq.indexOf = function(arr, elt, from){
30
+ if (arr.indexOf) return arr.indexOf(elt, from);
31
+
32
+ from = from || 0;
33
+ var len = arr.length;
34
+
35
+ if (from < 0) from += len;
36
+
37
+ for (; from < len; from++){
38
+ if (from in arr && arr[from] === elt){
39
+ return from;
40
+ }
41
+ }
42
+ return -1;
43
+ };
44
+
45
+ qq.getUniqueId = (function(){
46
+ var id = 0;
47
+ return function(){ return id++; };
48
+ })();
49
+
50
+ //
51
+ // Events
52
+
53
+ qq.attach = function(element, type, fn){
54
+ if (element.addEventListener){
55
+ element.addEventListener(type, fn, false);
56
+ } else if (element.attachEvent){
57
+ element.attachEvent('on' + type, fn);
58
+ }
59
+ };
60
+ qq.detach = function(element, type, fn){
61
+ if (element.removeEventListener){
62
+ element.removeEventListener(type, fn, false);
63
+ } else if (element.attachEvent){
64
+ element.detachEvent('on' + type, fn);
65
+ }
66
+ };
67
+
68
+ qq.preventDefault = function(e){
69
+ if (e.preventDefault){
70
+ e.preventDefault();
71
+ } else{
72
+ e.returnValue = false;
73
+ }
74
+ };
75
+
76
+ //
77
+ // Node manipulations
78
+
79
+ /**
80
+ * Insert node a before node b.
81
+ */
82
+ qq.insertBefore = function(a, b){
83
+ b.parentNode.insertBefore(a, b);
84
+ };
85
+ qq.remove = function(element){
86
+ element.parentNode.removeChild(element);
87
+ };
88
+
89
+ qq.contains = function(parent, descendant){
90
+ // compareposition returns false in this case
91
+ if (parent == descendant) return true;
92
+
93
+ if (parent.contains){
94
+ return parent.contains(descendant);
95
+ } else {
96
+ return !!(descendant.compareDocumentPosition(parent) & 8);
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Creates and returns element from html string
102
+ * Uses innerHTML to create an element
103
+ */
104
+ qq.toElement = (function(){
105
+ var div = document.createElement('div');
106
+ return function(html){
107
+ div.innerHTML = html;
108
+ var element = div.firstChild;
109
+ div.removeChild(element);
110
+ return element;
111
+ };
112
+ })();
113
+
114
+ //
115
+ // Node properties and attributes
116
+
117
+ /**
118
+ * Sets styles for an element.
119
+ * Fixes opacity in IE6-8.
120
+ */
121
+ qq.css = function(element, styles){
122
+ if (styles.opacity != null){
123
+ if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){
124
+ styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')';
125
+ }
126
+ }
127
+ qq.extend(element.style, styles);
128
+ };
129
+ qq.hasClass = function(element, name){
130
+ var re = new RegExp('(^| )' + name + '( |$)');
131
+ return re.test(element.className);
132
+ };
133
+ qq.addClass = function(element, name){
134
+ if (!qq.hasClass(element, name)){
135
+ element.className += ' ' + name;
136
+ }
137
+ };
138
+ qq.removeClass = function(element, name){
139
+ var re = new RegExp('(^| )' + name + '( |$)');
140
+ element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, "");
141
+ };
142
+ qq.setText = function(element, text){
143
+ element.innerText = text;
144
+ element.textContent = text;
145
+ };
146
+
147
+ //
148
+ // Selecting elements
149
+
150
+ qq.children = function(element){
151
+ var children = [],
152
+ child = element.firstChild;
153
+
154
+ while (child){
155
+ if (child.nodeType == 1){
156
+ children.push(child);
157
+ }
158
+ child = child.nextSibling;
159
+ }
160
+
161
+ return children;
162
+ };
163
+
164
+ qq.getByClass = function(element, className){
165
+ if (element.querySelectorAll){
166
+ return element.querySelectorAll('.' + className);
167
+ }
168
+
169
+ var result = [];
170
+ var candidates = element.getElementsByTagName("*");
171
+ var len = candidates.length;
172
+
173
+ for (var i = 0; i < len; i++){
174
+ if (qq.hasClass(candidates[i], className)){
175
+ result.push(candidates[i]);
176
+ }
177
+ }
178
+ return result;
179
+ };
180
+
181
+ /**
182
+ * obj2url() takes a json-object as argument and generates
183
+ * a querystring. pretty much like jQuery.param()
184
+ *
185
+ * how to use:
186
+ *
187
+ * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
188
+ *
189
+ * will result in:
190
+ *
191
+ * `http://any.url/upload?otherParam=value&a=b&c=d`
192
+ *
193
+ * @param Object JSON-Object
194
+ * @param String current querystring-part
195
+ * @return String encoded querystring
196
+ */
197
+ qq.obj2url = function(obj, temp, prefixDone){
198
+ var uristrings = [],
199
+ prefix = '&',
200
+ add = function(nextObj, i){
201
+ var nextTemp = temp
202
+ ? (/\[\]$/.test(temp)) // prevent double-encoding
203
+ ? temp
204
+ : temp+'['+i+']'
205
+ : i;
206
+ if ((nextTemp != 'undefined') && (i != 'undefined')) {
207
+ uristrings.push(
208
+ (typeof nextObj === 'object')
209
+ ? qq.obj2url(nextObj, nextTemp, true)
210
+ : (Object.prototype.toString.call(nextObj) === '[object Function]')
211
+ ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
212
+ : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
213
+ );
214
+ }
215
+ };
216
+
217
+ if (!prefixDone && temp) {
218
+ prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
219
+ uristrings.push(temp);
220
+ uristrings.push(qq.obj2url(obj));
221
+ } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) {
222
+ // we wont use a for-in-loop on an array (performance)
223
+ for (var i = 0, len = obj.length; i < len; ++i){
224
+ add(obj[i], i);
225
+ }
226
+ } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){
227
+ // for anything else but a scalar, we will use for-in-loop
228
+ for (var i in obj){
229
+ add(obj[i], i);
230
+ }
231
+ } else {
232
+ uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
233
+ }
234
+
235
+ return uristrings.join(prefix)
236
+ .replace(/^&/, '')
237
+ .replace(/%20/g, '+');
238
+ };
239
+
240
+ //
241
+ //
242
+ // Uploader Classes
243
+ //
244
+ //
245
+
246
+ var qq = qq || {};
247
+
248
+ /**
249
+ * Creates upload button, validates upload, but doesn't create file list or dd.
250
+ */
251
+ qq.FileUploaderBasic = function(o){
252
+ this._options = {
253
+ // set to true to see the server response
254
+ debug: false,
255
+ action: '/server/upload',
256
+ params: {},
257
+ button: null,
258
+ multiple: true,
259
+ // Rails csrf
260
+ csrf: false,
261
+ maxConnections: 3,
262
+ // validation
263
+ allowedExtensions: [],
264
+ sizeLimit: 0,
265
+ minSizeLimit: 0,
266
+ // events
267
+ // return false to cancel submit
268
+ onSubmit: function(id, fileName){},
269
+ onProgress: function(id, fileName, loaded, total){},
270
+ onComplete: function(id, fileName, responseJSON){},
271
+ onCancel: function(id, fileName){},
272
+ // messages
273
+ messages: {
274
+ typeError: "{file} has invalid extension. Only {extensions} are allowed.",
275
+ sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
276
+ minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
277
+ emptyError: "{file} is empty, please select files again without it.",
278
+ onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
279
+ },
280
+ showMessage: function(message){
281
+ alert(message);
282
+ }
283
+ };
284
+ qq.extend(this._options, o);
285
+
286
+ // number of files being uploaded
287
+ this._filesInProgress = 0;
288
+ this._handler = this._createUploadHandler();
289
+
290
+ if (this._options.button){
291
+ this._button = this._createUploadButton(this._options.button);
292
+ }
293
+
294
+ this._preventLeaveInProgress();
295
+ };
296
+
297
+ qq.FileUploaderBasic.prototype = {
298
+ setParams: function(params){
299
+ this._options.params = params;
300
+ },
301
+ getInProgress: function(){
302
+ return this._filesInProgress;
303
+ },
304
+ _createUploadButton: function(element){
305
+ var self = this;
306
+
307
+ return new qq.UploadButton({
308
+ element: element,
309
+ multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(),
310
+ onChange: function(input){
311
+ self._onInputChange(input);
312
+ }
313
+ });
314
+ },
315
+ _createUploadHandler: function(){
316
+ var self = this,
317
+ handlerClass;
318
+
319
+ if(qq.UploadHandlerXhr.isSupported()){
320
+ handlerClass = 'UploadHandlerXhr';
321
+ } else {
322
+ handlerClass = 'UploadHandlerForm';
323
+ }
324
+
325
+ var handler = new qq[handlerClass]({
326
+ debug: this._options.debug,
327
+ action: this._options.action,
328
+ maxConnections: this._options.maxConnections,
329
+ onProgress: function(id, fileName, loaded, total){
330
+ self._onProgress(id, fileName, loaded, total);
331
+ self._options.onProgress(id, fileName, loaded, total);
332
+ },
333
+ onComplete: function(id, fileName, result){
334
+ self._onComplete(id, fileName, result);
335
+ self._options.onComplete(id, fileName, result);
336
+ },
337
+ onCancel: function(id, fileName){
338
+ self._onCancel(id, fileName);
339
+ self._options.onCancel(id, fileName);
340
+ },
341
+ csrf: this._options.csrf
342
+ });
343
+
344
+ return handler;
345
+ },
346
+ _preventLeaveInProgress: function(){
347
+ var self = this;
348
+
349
+ qq.attach(window, 'beforeunload', function(e){
350
+ if (!self._filesInProgress){return;}
351
+
352
+ var e = e || window.event;
353
+ // for ie, ff
354
+ e.returnValue = self._options.messages.onLeave;
355
+ // for webkit
356
+ return self._options.messages.onLeave;
357
+ });
358
+ },
359
+ _onSubmit: function(id, fileName){
360
+ this._filesInProgress++;
361
+ },
362
+ _onProgress: function(id, fileName, loaded, total){
363
+ },
364
+ _onComplete: function(id, fileName, result){
365
+ this._filesInProgress--;
366
+ if (result.error){
367
+ this._options.showMessage(result.error);
368
+ }
369
+ },
370
+ _onCancel: function(id, fileName){
371
+ this._filesInProgress--;
372
+ },
373
+ _onInputChange: function(input){
374
+ if (this._handler instanceof qq.UploadHandlerXhr){
375
+ this._uploadFileList(input.files);
376
+ } else {
377
+ if (this._validateFile(input)){
378
+ this._uploadFile(input);
379
+ }
380
+ }
381
+ this._button.reset();
382
+ },
383
+ _uploadFileList: function(files){
384
+ for (var i=0; i<files.length; i++){
385
+ if ( !this._validateFile(files[i])){
386
+ return;
387
+ }
388
+ }
389
+
390
+ for (var i=0; i<files.length; i++){
391
+ this._uploadFile(files[i]);
392
+ }
393
+ },
394
+ _uploadFile: function(fileContainer){
395
+ var id = this._handler.add(fileContainer);
396
+ var fileName = this._handler.getName(id);
397
+
398
+ if (this._options.onSubmit(id, fileName) !== false){
399
+ this._onSubmit(id, fileName);
400
+ this._handler.upload(id, this._options.params);
401
+ }
402
+ },
403
+ _validateFile: function(file){
404
+ var name, size;
405
+
406
+ if (file.value){
407
+ // it is a file input
408
+ // get input value and remove path to normalize
409
+ name = file.value.replace(/.*(\/|\\)/, "");
410
+ } else {
411
+ // fix missing properties in Safari
412
+ name = file.fileName != null ? file.fileName : file.name;
413
+ size = file.fileSize != null ? file.fileSize : file.size;
414
+ }
415
+
416
+ if (! this._isAllowedExtension(name)){
417
+ this._error('typeError', name);
418
+ return false;
419
+
420
+ } else if (size === 0){
421
+ this._error('emptyError', name);
422
+ return false;
423
+
424
+ } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){
425
+ this._error('sizeError', name);
426
+ return false;
427
+
428
+ } else if (size && size < this._options.minSizeLimit){
429
+ this._error('minSizeError', name);
430
+ return false;
431
+ }
432
+
433
+ return true;
434
+ },
435
+ _error: function(code, fileName){
436
+ var message = this._options.messages[code];
437
+ function r(name, replacement){ message = message.replace(name, replacement); }
438
+
439
+ r('{file}', this._formatFileName(fileName));
440
+ r('{extensions}', this._options.allowedExtensions.join(', '));
441
+ r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
442
+ r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));
443
+
444
+ this._options.showMessage(message);
445
+ },
446
+ _formatFileName: function(name){
447
+ if (name.length > 33){
448
+ name = name.slice(0, 19) + '...' + name.slice(-13);
449
+ }
450
+ return name;
451
+ },
452
+ _isAllowedExtension: function(fileName){
453
+ var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
454
+ var allowed = this._options.allowedExtensions;
455
+
456
+ if (!allowed.length){return true;}
457
+
458
+ for (var i=0; i<allowed.length; i++){
459
+ if (allowed[i].toLowerCase() == ext){ return true;}
460
+ }
461
+
462
+ return false;
463
+ },
464
+ _formatSize: function(bytes){
465
+ var i = -1;
466
+ do {
467
+ bytes = bytes / 1024;
468
+ i++;
469
+ } while (bytes > 99);
470
+
471
+ return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
472
+ }
473
+ };
474
+
475
+
476
+ /**
477
+ * Class that creates upload widget with drag-and-drop and file list
478
+ * @inherits qq.FileUploaderBasic
479
+ */
480
+ qq.FileUploader = function(o){
481
+ // call parent constructor
482
+ qq.FileUploaderBasic.apply(this, arguments);
483
+
484
+ // additional options
485
+ qq.extend(this._options, {
486
+ element: null,
487
+ // if set, will be used instead of qq-upload-list in template
488
+ listElement: null,
489
+
490
+ template: '<div class="qq-uploader">' +
491
+ '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' +
492
+ '<div class="qq-upload-button">Upload a file</div>' +
493
+ '<ul class="qq-upload-list"></ul>' +
494
+ '</div>',
495
+
496
+ // template for one item in file list
497
+ fileTemplate: '<li>' +
498
+ '<span class="qq-upload-file"></span>' +
499
+ '<span class="qq-upload-spinner"></span>' +
500
+ '<span class="qq-upload-size"></span>' +
501
+ '<a class="qq-upload-cancel" href="#">Cancel</a>' +
502
+ '<span class="qq-upload-failed-text">Failed</span>' +
503
+ '</li>',
504
+
505
+ classes: {
506
+ // used to get elements from templates
507
+ button: 'qq-upload-button',
508
+ drop: 'qq-upload-drop-area',
509
+ dropActive: 'qq-upload-drop-area-active',
510
+ list: 'qq-upload-list',
511
+
512
+ file: 'qq-upload-file',
513
+ spinner: 'qq-upload-spinner',
514
+ size: 'qq-upload-size',
515
+ cancel: 'qq-upload-cancel',
516
+
517
+ // added to list item when upload completes
518
+ // used in css to hide progress spinner
519
+ success: 'qq-upload-success',
520
+ fail: 'qq-upload-fail'
521
+ }
522
+ });
523
+ // overwrite options with user supplied
524
+ qq.extend(this._options, o);
525
+
526
+
527
+ this._element = this._options.element;
528
+ this._element.innerHTML = this._options.template;
529
+ this._listElement = this._options.listElement || this._find(this._element, 'list');
530
+
531
+ this._classes = this._options.classes;
532
+
533
+ this._button = this._createUploadButton(this._find(this._element, 'button'));
534
+
535
+ this._bindCancelEvent();
536
+ this._setupDragDrop();
537
+ };
538
+
539
+ // inherit from Basic Uploader
540
+ qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype);
541
+
542
+ qq.extend(qq.FileUploader.prototype, {
543
+ /**
544
+ * Gets one of the elements listed in this._options.classes
545
+ **/
546
+ _find: function(parent, type){
547
+ var element = qq.getByClass(parent, this._options.classes[type])[0];
548
+ if (!element){
549
+ throw new Error('element not found ' + type);
550
+ }
551
+
552
+ return element;
553
+ },
554
+ _setupDragDrop: function(){
555
+ var self = this,
556
+ dropArea = this._find(this._element, 'drop');
557
+
558
+ var dz = new qq.UploadDropZone({
559
+ element: dropArea,
560
+ onEnter: function(e){
561
+ qq.addClass(dropArea, self._classes.dropActive);
562
+ e.stopPropagation();
563
+ },
564
+ onLeave: function(e){
565
+ e.stopPropagation();
566
+ },
567
+ onLeaveNotDescendants: function(e){
568
+ qq.removeClass(dropArea, self._classes.dropActive);
569
+ },
570
+ onDrop: function(e){
571
+ dropArea.style.display = 'none';
572
+ qq.removeClass(dropArea, self._classes.dropActive);
573
+ self._uploadFileList(e.dataTransfer.files);
574
+ }
575
+ });
576
+
577
+ dropArea.style.display = 'none';
578
+
579
+ qq.attach(document, 'dragenter', function(e){
580
+ if (!dz._isValidFileDrag(e)) return;
581
+
582
+ dropArea.style.display = 'block';
583
+ });
584
+ qq.attach(document, 'dragleave', function(e){
585
+ if (!dz._isValidFileDrag(e)) return;
586
+
587
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
588
+ // only fire when leaving document out
589
+ if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){
590
+ dropArea.style.display = 'none';
591
+ }
592
+ });
593
+ },
594
+ _onSubmit: function(id, fileName){
595
+ qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
596
+ this._addToList(id, fileName);
597
+ },
598
+ _onProgress: function(id, fileName, loaded, total){
599
+ qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments);
600
+
601
+ var item = this._getItemByFileId(id);
602
+ var size = this._find(item, 'size');
603
+ size.style.display = 'inline';
604
+
605
+ var text;
606
+ if (loaded != total){
607
+ text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
608
+ } else {
609
+ text = this._formatSize(total);
610
+ }
611
+
612
+ qq.setText(size, text);
613
+ },
614
+ _onComplete: function(id, fileName, result){
615
+ qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments);
616
+
617
+ // mark completed
618
+ var item = this._getItemByFileId(id);
619
+ qq.remove(this._find(item, 'cancel'));
620
+ qq.remove(this._find(item, 'spinner'));
621
+
622
+ if (result.success){
623
+ qq.addClass(item, this._classes.success);
624
+ } else {
625
+ qq.addClass(item, this._classes.fail);
626
+ }
627
+ },
628
+ _addToList: function(id, fileName){
629
+ var item = qq.toElement(this._options.fileTemplate);
630
+ item.qqFileId = id;
631
+
632
+ var fileElement = this._find(item, 'file');
633
+ qq.setText(fileElement, this._formatFileName(fileName));
634
+ this._find(item, 'size').style.display = 'none';
635
+
636
+ this._listElement.appendChild(item);
637
+ },
638
+ _getItemByFileId: function(id){
639
+ var item = this._listElement.firstChild;
640
+
641
+ // there can't be txt nodes in dynamically created list
642
+ // and we can use nextSibling
643
+ while (item){
644
+ if (item.qqFileId == id) return item;
645
+ item = item.nextSibling;
646
+ }
647
+ },
648
+ /**
649
+ * delegate click event for cancel link
650
+ **/
651
+ _bindCancelEvent: function(){
652
+ var self = this,
653
+ list = this._listElement;
654
+
655
+ qq.attach(list, 'click', function(e){
656
+ e = e || window.event;
657
+ var target = e.target || e.srcElement;
658
+
659
+ if (qq.hasClass(target, self._classes.cancel)){
660
+ qq.preventDefault(e);
661
+
662
+ var item = target.parentNode;
663
+ self._handler.cancel(item.qqFileId);
664
+ qq.remove(item);
665
+ }
666
+ });
667
+ }
668
+ });
669
+
670
+ qq.UploadDropZone = function(o){
671
+ this._options = {
672
+ element: null,
673
+ onEnter: function(e){},
674
+ onLeave: function(e){},
675
+ // is not fired when leaving element by hovering descendants
676
+ onLeaveNotDescendants: function(e){},
677
+ onDrop: function(e){}
678
+ };
679
+ qq.extend(this._options, o);
680
+
681
+ this._element = this._options.element;
682
+
683
+ this._disableDropOutside();
684
+ this._attachEvents();
685
+ };
686
+
687
+ qq.UploadDropZone.prototype = {
688
+ _disableDropOutside: function(e){
689
+ // run only once for all instances
690
+ if (!qq.UploadDropZone.dropOutsideDisabled ){
691
+
692
+ qq.attach(document, 'dragover', function(e){
693
+ if (e.dataTransfer){
694
+ e.dataTransfer.dropEffect = 'none';
695
+ e.preventDefault();
696
+ }
697
+ });
698
+
699
+ qq.UploadDropZone.dropOutsideDisabled = true;
700
+ }
701
+ },
702
+ _attachEvents: function(){
703
+ var self = this;
704
+
705
+ qq.attach(self._element, 'dragover', function(e){
706
+ if (!self._isValidFileDrag(e)) return;
707
+
708
+ var effect = e.dataTransfer.effectAllowed;
709
+ if (effect == 'move' || effect == 'linkMove'){
710
+ e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
711
+ } else {
712
+ e.dataTransfer.dropEffect = 'copy'; // for Chrome
713
+ }
714
+
715
+ e.stopPropagation();
716
+ e.preventDefault();
717
+ });
718
+
719
+ qq.attach(self._element, 'dragenter', function(e){
720
+ if (!self._isValidFileDrag(e)) return;
721
+
722
+ self._options.onEnter(e);
723
+ });
724
+
725
+ qq.attach(self._element, 'dragleave', function(e){
726
+ if (!self._isValidFileDrag(e)) return;
727
+
728
+ self._options.onLeave(e);
729
+
730
+ var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
731
+ // do not fire when moving a mouse over a descendant
732
+ if (qq.contains(this, relatedTarget)) return;
733
+
734
+ self._options.onLeaveNotDescendants(e);
735
+ });
736
+
737
+ qq.attach(self._element, 'drop', function(e){
738
+ if (!self._isValidFileDrag(e)) return;
739
+
740
+ e.preventDefault();
741
+ self._options.onDrop(e);
742
+ });
743
+ },
744
+ _isValidFileDrag: function(e){
745
+ var dt = e.dataTransfer,
746
+ // do not check dt.types.contains in webkit, because it crashes safari 4
747
+ isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;
748
+
749
+ // dt.effectAllowed is none in Safari 5
750
+ // dt.types.contains check is for firefox
751
+ return dt && dt.effectAllowed != 'none' &&
752
+ (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));
753
+
754
+ }
755
+ };
756
+
757
+ qq.UploadButton = function(o){
758
+ this._options = {
759
+ element: null,
760
+ // if set to true adds multiple attribute to file input
761
+ multiple: false,
762
+ // name attribute of file input
763
+ name: 'file',
764
+ onChange: function(input){},
765
+ hoverClass: 'qq-upload-button-hover',
766
+ focusClass: 'qq-upload-button-focus'
767
+ };
768
+
769
+ qq.extend(this._options, o);
770
+ this._element = this._options.element;
771
+
772
+ // make button suitable container for input
773
+ qq.css(this._element, {
774
+ position: 'relative',
775
+ overflow: 'hidden',
776
+ // Make sure browse button is in the right side
777
+ // in Internet Explorer
778
+ direction: 'ltr'
779
+ });
780
+
781
+ this._input = this._createInput();
782
+ };
783
+
784
+ qq.UploadButton.prototype = {
785
+ /* returns file input element */
786
+ getInput: function(){
787
+ return this._input;
788
+ },
789
+ /* cleans/recreates the file input */
790
+ reset: function(){
791
+ if (this._input.parentNode){
792
+ qq.remove(this._input);
793
+ }
794
+
795
+ qq.removeClass(this._element, this._options.focusClass);
796
+ this._input = this._createInput();
797
+ },
798
+ _createInput: function(){
799
+ var input = document.createElement("input");
800
+
801
+ if (this._options.multiple){
802
+ input.setAttribute("multiple", "multiple");
803
+ }
804
+
805
+ input.setAttribute("type", "file");
806
+ input.setAttribute("name", this._options.name);
807
+
808
+ qq.css(input, {
809
+ position: 'absolute',
810
+ // in Opera only 'browse' button
811
+ // is clickable and it is located at
812
+ // the right side of the input
813
+ right: 0,
814
+ top: 0,
815
+ fontFamily: 'Arial',
816
+ // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
817
+ fontSize: '118px',
818
+ margin: 0,
819
+ padding: 0,
820
+ cursor: 'pointer',
821
+ opacity: 0
822
+ });
823
+
824
+ this._element.appendChild(input);
825
+
826
+ var self = this;
827
+ qq.attach(input, 'change', function(){
828
+ self._options.onChange(input);
829
+ });
830
+
831
+ qq.attach(input, 'mouseover', function(){
832
+ qq.addClass(self._element, self._options.hoverClass);
833
+ });
834
+ qq.attach(input, 'mouseout', function(){
835
+ qq.removeClass(self._element, self._options.hoverClass);
836
+ });
837
+ qq.attach(input, 'focus', function(){
838
+ qq.addClass(self._element, self._options.focusClass);
839
+ });
840
+ qq.attach(input, 'blur', function(){
841
+ qq.removeClass(self._element, self._options.focusClass);
842
+ });
843
+
844
+ // IE and Opera, unfortunately have 2 tab stops on file input
845
+ // which is unacceptable in our case, disable keyboard access
846
+ if (window.attachEvent){
847
+ // it is IE or Opera
848
+ input.setAttribute('tabIndex', "-1");
849
+ }
850
+
851
+ return input;
852
+ }
853
+ };
854
+
855
+ /**
856
+ * Class for uploading files, uploading itself is handled by child classes
857
+ */
858
+ qq.UploadHandlerAbstract = function(o){
859
+ this._options = {
860
+ debug: false,
861
+ action: '/upload.php',
862
+ // maximum number of concurrent uploads
863
+ maxConnections: 999,
864
+ onProgress: function(id, fileName, loaded, total){},
865
+ onComplete: function(id, fileName, response){},
866
+ onCancel: function(id, fileName){}
867
+ };
868
+ qq.extend(this._options, o);
869
+
870
+ this._queue = [];
871
+ // params for files in queue
872
+ this._params = [];
873
+ };
874
+ qq.UploadHandlerAbstract.prototype = {
875
+ log: function(str){
876
+ if (this._options.debug && window.console) console.log('[uploader] ' + str);
877
+ },
878
+ /**
879
+ * Adds file or file input to the queue
880
+ * @returns id
881
+ **/
882
+ add: function(file){},
883
+ /**
884
+ * Sends the file identified by id and additional query params to the server
885
+ */
886
+ upload: function(id, params){
887
+ var len = this._queue.push(id);
888
+
889
+ var copy = {};
890
+ qq.extend(copy, params);
891
+ this._params[id] = copy;
892
+
893
+ // if too many active uploads, wait...
894
+ if (len <= this._options.maxConnections){
895
+ this._upload(id, this._params[id]);
896
+ }
897
+ },
898
+ /**
899
+ * Cancels file upload by id
900
+ */
901
+ cancel: function(id){
902
+ this._cancel(id);
903
+ this._dequeue(id);
904
+ },
905
+ /**
906
+ * Cancells all uploads
907
+ */
908
+ cancelAll: function(){
909
+ for (var i=0; i<this._queue.length; i++){
910
+ this._cancel(this._queue[i]);
911
+ }
912
+ this._queue = [];
913
+ },
914
+ /**
915
+ * Returns name of the file identified by id
916
+ */
917
+ getName: function(id){},
918
+ /**
919
+ * Returns size of the file identified by id
920
+ */
921
+ getSize: function(id){},
922
+ /**
923
+ * Returns id of files being uploaded or
924
+ * waiting for their turn
925
+ */
926
+ getQueue: function(){
927
+ return this._queue;
928
+ },
929
+ /**
930
+ * Actual upload method
931
+ */
932
+ _upload: function(id){},
933
+ /**
934
+ * Actual cancel method
935
+ */
936
+ _cancel: function(id){},
937
+ /**
938
+ * Removes element from queue, starts upload of next
939
+ */
940
+ _dequeue: function(id){
941
+ var i = qq.indexOf(this._queue, id);
942
+ this._queue.splice(i, 1);
943
+
944
+ var max = this._options.maxConnections;
945
+
946
+ if (this._queue.length >= max && i < max){
947
+ var nextId = this._queue[max-1];
948
+ this._upload(nextId, this._params[nextId]);
949
+ }
950
+ }
951
+ };
952
+
953
+ /**
954
+ * Class for uploading files using form and iframe
955
+ * @inherits qq.UploadHandlerAbstract
956
+ */
957
+ qq.UploadHandlerForm = function(o){
958
+ qq.UploadHandlerAbstract.apply(this, arguments);
959
+
960
+ this._inputs = {};
961
+ };
962
+ // @inherits qq.UploadHandlerAbstract
963
+ qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
964
+
965
+ qq.extend(qq.UploadHandlerForm.prototype, {
966
+ add: function(fileInput){
967
+ fileInput.setAttribute('name', 'qqfile');
968
+ var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
969
+
970
+ this._inputs[id] = fileInput;
971
+
972
+ // remove file input from DOM
973
+ if (fileInput.parentNode){
974
+ qq.remove(fileInput);
975
+ }
976
+
977
+ return id;
978
+ },
979
+ getName: function(id){
980
+ // get input value and remove path to normalize
981
+ return this._inputs[id].value.replace(/.*(\/|\\)/, "");
982
+ },
983
+ _cancel: function(id){
984
+ this._options.onCancel(id, this.getName(id));
985
+
986
+ delete this._inputs[id];
987
+
988
+ var iframe = document.getElementById(id);
989
+ if (iframe){
990
+ // to cancel request set src to something else
991
+ // we use src="javascript:false;" because it doesn't
992
+ // trigger ie6 prompt on https
993
+ iframe.setAttribute('src', 'javascript:false;');
994
+
995
+ qq.remove(iframe);
996
+ }
997
+ },
998
+ _upload: function(id, params){
999
+ var input = this._inputs[id];
1000
+
1001
+ if (!input){
1002
+ throw new Error('file with passed id was not added, or already uploaded or cancelled');
1003
+ }
1004
+
1005
+ var fileName = this.getName(id);
1006
+
1007
+ var iframe = this._createIframe(id);
1008
+ var form = this._createForm(iframe, params);
1009
+ form.appendChild(input);
1010
+
1011
+ var self = this;
1012
+ this._attachLoadEvent(iframe, function(){
1013
+ self.log('iframe loaded');
1014
+
1015
+ var response = self._getIframeContentJSON(iframe);
1016
+
1017
+ self._options.onComplete(id, fileName, response);
1018
+ self._dequeue(id);
1019
+
1020
+ delete self._inputs[id];
1021
+ // timeout added to fix busy state in FF3.6
1022
+ setTimeout(function(){
1023
+ qq.remove(iframe);
1024
+ }, 1);
1025
+ });
1026
+
1027
+ form.submit();
1028
+ qq.remove(form);
1029
+
1030
+ return id;
1031
+ },
1032
+ _attachLoadEvent: function(iframe, callback){
1033
+ qq.attach(iframe, 'load', function(){
1034
+ // when we remove iframe from dom
1035
+ // the request stops, but in IE load
1036
+ // event fires
1037
+ if (!iframe.parentNode){
1038
+ return;
1039
+ }
1040
+
1041
+ // fixing Opera 10.53
1042
+ if (iframe.contentDocument &&
1043
+ iframe.contentDocument.body &&
1044
+ iframe.contentDocument.body.innerHTML == "false"){
1045
+ // In Opera event is fired second time
1046
+ // when body.innerHTML changed from false
1047
+ // to server response approx. after 1 sec
1048
+ // when we upload file with iframe
1049
+ return;
1050
+ }
1051
+
1052
+ callback();
1053
+ });
1054
+ },
1055
+ /**
1056
+ * Returns json object received by iframe from server.
1057
+ */
1058
+ _getIframeContentJSON: function(iframe){
1059
+ // iframe.contentWindow.document - for IE<7
1060
+ var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document,
1061
+ response;
1062
+
1063
+ this.log("converting iframe's innerHTML to JSON");
1064
+ this.log("innerHTML = " + doc.body.innerHTML);
1065
+
1066
+ try {
1067
+ response = eval("(" + doc.body.innerHTML + ")");
1068
+ } catch(err){
1069
+ response = {};
1070
+ }
1071
+
1072
+ return response;
1073
+ },
1074
+ /**
1075
+ * Creates iframe with unique name
1076
+ */
1077
+ _createIframe: function(id){
1078
+ // We can't use following code as the name attribute
1079
+ // won't be properly registered in IE6, and new window
1080
+ // on form submit will open
1081
+ // var iframe = document.createElement('iframe');
1082
+ // iframe.setAttribute('name', id);
1083
+
1084
+ var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1085
+ // src="javascript:false;" removes ie6 prompt on https
1086
+
1087
+ iframe.setAttribute('id', id);
1088
+
1089
+ iframe.style.display = 'none';
1090
+ document.body.appendChild(iframe);
1091
+
1092
+ return iframe;
1093
+ },
1094
+ /**
1095
+ * Creates form, that will be submitted to iframe
1096
+ */
1097
+ _createForm: function(iframe, params){
1098
+ // We can't use the following code in IE6
1099
+ // var form = document.createElement('form');
1100
+ // form.setAttribute('method', 'post');
1101
+ // form.setAttribute('enctype', 'multipart/form-data');
1102
+ // Because in this case file won't be attached to request
1103
+ var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');
1104
+
1105
+ var queryString = qq.obj2url(params, this._options.action);
1106
+
1107
+ form.setAttribute('action', queryString);
1108
+ form.setAttribute('target', iframe.name);
1109
+ form.style.display = 'none';
1110
+ document.body.appendChild(form);
1111
+
1112
+ return form;
1113
+ }
1114
+ });
1115
+
1116
+ /**
1117
+ * Class for uploading files using xhr
1118
+ * @inherits qq.UploadHandlerAbstract
1119
+ */
1120
+ qq.UploadHandlerXhr = function(o){
1121
+ qq.UploadHandlerAbstract.apply(this, arguments);
1122
+
1123
+ this._files = [];
1124
+ this._xhrs = [];
1125
+
1126
+ // current loaded size in bytes for each file
1127
+ this._loaded = [];
1128
+ };
1129
+
1130
+ // static method
1131
+ qq.UploadHandlerXhr.isSupported = function(){
1132
+ var input = document.createElement('input');
1133
+ input.type = 'file';
1134
+
1135
+ return (
1136
+ 'multiple' in input &&
1137
+ typeof File != "undefined" &&
1138
+ typeof (new XMLHttpRequest()).upload != "undefined" );
1139
+ };
1140
+
1141
+ // @inherits qq.UploadHandlerAbstract
1142
+ qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1143
+
1144
+ qq.extend(qq.UploadHandlerXhr.prototype, {
1145
+ /**
1146
+ * Adds file to the queue
1147
+ * Returns id to use with upload, cancel
1148
+ **/
1149
+ add: function(file){
1150
+ if (!(file instanceof File)){
1151
+ throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1152
+ }
1153
+
1154
+ return this._files.push(file) - 1;
1155
+ },
1156
+ getName: function(id){
1157
+ var file = this._files[id];
1158
+ // fix missing name in Safari 4
1159
+ return file.fileName != null ? file.fileName : file.name;
1160
+ },
1161
+ getSize: function(id){
1162
+ var file = this._files[id];
1163
+ return file.fileSize != null ? file.fileSize : file.size;
1164
+ },
1165
+ /**
1166
+ * Returns uploaded bytes for file identified by id
1167
+ */
1168
+ getLoaded: function(id){
1169
+ return this._loaded[id] || 0;
1170
+ },
1171
+ /**
1172
+ * Sends the file identified by id and additional query params to the server
1173
+ * @param {Object} params name-value string pairs
1174
+ */
1175
+ _upload: function(id, params){
1176
+ var file = this._files[id],
1177
+ name = this.getName(id),
1178
+ size = this.getSize(id);
1179
+
1180
+ this._loaded[id] = 0;
1181
+
1182
+ var xhr = this._xhrs[id] = new XMLHttpRequest();
1183
+ var self = this;
1184
+
1185
+ xhr.upload.onprogress = function(e){
1186
+ if (e.lengthComputable){
1187
+ self._loaded[id] = e.loaded;
1188
+ self._options.onProgress(id, name, e.loaded, e.total);
1189
+ }
1190
+ };
1191
+
1192
+ xhr.onreadystatechange = function(){
1193
+ if (xhr.readyState == 4){
1194
+ self._onComplete(id, xhr);
1195
+ }
1196
+ };
1197
+
1198
+ // build query string
1199
+ params = params || {};
1200
+ params['qqfile'] = name;
1201
+ var queryString = qq.obj2url(params, this._options.action);
1202
+
1203
+ xhr.open("POST", queryString, true);
1204
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1205
+ xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1206
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
1207
+ this._options.csrf && xhr.setRequestHeader("X-CSRF-Token", $("meta[name='csrf-token']").attr("content"));
1208
+ xhr.send(file);
1209
+ },
1210
+ _onComplete: function(id, xhr){
1211
+ // the request was aborted/cancelled
1212
+ if (!this._files[id]) return;
1213
+
1214
+ var name = this.getName(id);
1215
+ var size = this.getSize(id);
1216
+
1217
+ this._options.onProgress(id, name, size, size);
1218
+
1219
+ if (xhr.status == 200){
1220
+ this.log("xhr - server response received");
1221
+ this.log("responseText = " + xhr.responseText);
1222
+
1223
+ var response;
1224
+
1225
+ try {
1226
+ response = eval("(" + xhr.responseText + ")");
1227
+ } catch(err){
1228
+ response = {};
1229
+ }
1230
+
1231
+ this._options.onComplete(id, name, response);
1232
+
1233
+ } else {
1234
+ this._options.onComplete(id, name, {});
1235
+ }
1236
+
1237
+ this._files[id] = null;
1238
+ this._xhrs[id] = null;
1239
+ this._dequeue(id);
1240
+ },
1241
+ _cancel: function(id){
1242
+ this._options.onCancel(id, this.getName(id));
1243
+
1244
+ this._files[id] = null;
1245
+
1246
+ if (this._xhrs[id]){
1247
+ this._xhrs[id].abort();
1248
+ this._xhrs[id] = null;
1249
+ }
1250
+ }
1251
+ });