PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 5.0.3.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v5.0.3.2
6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 5.2.2 5.3 All 153 releases
mainwp / assets / js / fileuploader.js

fileuploader.js in MainWP Dashboard: Self-hosted WordPress Management for Agencies 5.0.3.2, at assets/js/fileuploader.js

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