PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 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 All 155 releases
mainwp / assets / js / fileuploader.js

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

1,277 lines 38.4 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 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> <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 = '<span data-tooltip="Upload completed successfully." data-inverted="" data-position="left center"><i class="green check icon"></i></span>'; //MAINWP custom code
635 //MAINWP custom code
636 } else {
637 qq.addClass(item, this._classes.fail);
638 this._find(item, 'fail_msg').innerHTML = '<span data-tooltip="Upload failed." data-inverted="" data-position="left center"><i class="red times icon"></i></span>'; //MAINWP custom code
639 }
640 },
641 _addToList: function (id, fileName) {
642 var item = qq.toElement(this._options.fileTemplate);
643 item.qqFileId = id;
644
645 var fileElement = this._find(item, 'file');
646 qq.setText(fileElement, this._formatFileName(fileName));
647 this._find(item, 'size').style.display = 'none';
648 this._find(item, 'cancel_install').style.display = 'none';
649 this._listElement.appendChild(item);
650 },
651 _getItemByFileId: function (id) {
652 var item = this._listElement.firstChild;
653
654 // there can't be txt nodes in dynamically created list
655 // and we can use nextSibling
656 while (item) {
657 if (item.qqFileId == id) return item;
658 item = item.nextSibling;
659 }
660 },
661 /**
662 * delegate click event for cancel link
663 **/
664 _bindCancelEvent: function () {
665 var self = this,
666 list = this._listElement;
667
668 qq.attach(list, 'click', function (e) {
669 e = e || window.event;
670 var target = e.target || e.srcElement;
671
672 if (qq.hasClass(target, self._classes.cancel)) {
673 qq.preventDefault(e);
674
675 var item = target.parentNode.parentNode.parentNode.parentNode;
676 self._handler.cancel(item.qqFileId);
677 qq.remove(item);
678 }
679 });
680 },
681 _bindCancelInstallEvent: function () {
682 var self = this,
683 list = this._listElement;
684
685 qq.attach(list, 'click', function (e) {
686 e = e || window.event;
687 var target = e.target || e.srcElement;
688
689 if (qq.hasClass(target, self._classes.cancel_install)) {
690 qq.preventDefault(e);
691 var item = target.parentNode.parentNode.parentNode.parentNode;
692 qq.remove(item);
693 }
694 });
695 },
696 });
697
698 qq.UploadDropZone = function (o) {
699 this._options = {
700 element: null,
701 onEnter: function (e) { },
702 onLeave: function (e) { },
703 // is not fired when leaving element by hovering descendants
704 onLeaveNotDescendants: function (e) { },
705 onDrop: function (e) { }
706 };
707 qq.extend(this._options, o);
708
709 this._element = this._options.element;
710
711 this._disableDropOutside();
712 this._attachEvents();
713 };
714
715 qq.UploadDropZone.prototype = {
716 _disableDropOutside: function (e) {
717 // run only once for all instances
718 if (!qq.UploadDropZone.dropOutsideDisabled) {
719
720 qq.attach(document, 'dragover', function (e) {
721 if (e.dataTransfer) {
722 e.dataTransfer.dropEffect = 'none';
723 e.preventDefault();
724 }
725 });
726
727 qq.UploadDropZone.dropOutsideDisabled = true;
728 }
729 },
730 _attachEvents: function () {
731 var self = this;
732
733 qq.attach(self._element, 'dragover', function (e) {
734 if (!self._isValidFileDrag(e)) return;
735
736 var effect = e.dataTransfer.effectAllowed;
737 if (effect == 'move' || effect == 'linkMove') {
738 e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
739 } else {
740 e.dataTransfer.dropEffect = 'copy'; // for Chrome
741 }
742
743 e.stopPropagation();
744 e.preventDefault();
745 });
746
747 qq.attach(self._element, 'dragenter', function (e) {
748 if (!self._isValidFileDrag(e)) return;
749
750 self._options.onEnter(e);
751 });
752
753 qq.attach(self._element, 'dragleave', function (e) {
754 if (!self._isValidFileDrag(e)) return;
755
756 self._options.onLeave(e);
757
758 var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
759 // do not fire when moving a mouse over a descendant
760 if (qq.contains(this, relatedTarget)) return;
761
762 self._options.onLeaveNotDescendants(e);
763 });
764
765 qq.attach(self._element, 'drop', function (e) {
766 if (!self._isValidFileDrag(e)) return;
767
768 e.preventDefault();
769 self._options.onDrop(e);
770 });
771 },
772 _isValidFileDrag: function (e) {
773 var dt = e.dataTransfer,
774 // do not check dt.types.contains in webkit, because it crashes safari 4
775 isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;
776
777 // dt.effectAllowed is none in Safari 5
778 // dt.types.contains check is for firefox
779 return dt && dt.effectAllowed != 'none' &&
780 (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));
781
782 }
783 };
784
785 qq.UploadButton = function (o) {
786 this._options = {
787 element: null,
788 // if set to true adds multiple attribute to file input
789 multiple: false,
790 // name attribute of file input
791 name: 'file',
792 onChange: function (input) { },
793 hoverClass: 'qq-upload-button-hover',
794 focusClass: 'qq-upload-button-focus'
795 };
796
797 qq.extend(this._options, o);
798
799 this._element = this._options.element;
800
801 // make button suitable container for input
802 qq.css(this._element, {
803 position: 'relative',
804 overflow: 'hidden',
805 // Make sure browse button is in the right side
806 // in Internet Explorer
807 direction: 'ltr'
808 });
809
810 this._input = this._createInput();
811 };
812
813 qq.UploadButton.prototype = {
814 /* returns file input element */
815 getInput: function () {
816 return this._input;
817 },
818 /* cleans/recreates the file input */
819 reset: function () {
820 if (this._input.parentNode) {
821 qq.remove(this._input);
822 }
823
824 qq.removeClass(this._element, this._options.focusClass);
825 this._input = this._createInput();
826 },
827 _createInput: function () {
828 var input = document.createElement("input");
829
830 if (this._options.multiple) {
831 input.setAttribute("multiple", "multiple");
832 }
833
834 input.setAttribute("type", "file");
835 input.setAttribute("name", this._options.name);
836 qq.css(input, {
837 position: 'absolute',
838 // in Opera only 'browse' button
839 // is clickable and it is located at
840 // the right side of the input
841 right: 0,
842 top: 0,
843 fontFamily: 'Arial',
844 // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
845 fontSize: '118px',
846 margin: 0,
847 padding: 0,
848 cursor: 'pointer',
849 opacity: 0
850 });
851
852 this._element.appendChild(input);
853
854 var self = this;
855 qq.attach(input, 'change', function () {
856 self._options.onChange(input);
857 });
858
859 qq.attach(input, 'mouseover', function () {
860 qq.addClass(self._element, self._options.hoverClass);
861 });
862 qq.attach(input, 'mouseout', function () {
863 qq.removeClass(self._element, self._options.hoverClass);
864 });
865 qq.attach(input, 'focus', function () {
866 qq.addClass(self._element, self._options.focusClass);
867 });
868 qq.attach(input, 'blur', function () {
869 qq.removeClass(self._element, self._options.focusClass);
870 });
871
872 // IE and Opera, unfortunately have 2 tab stops on file input
873 // which is unacceptable in our case, disable keyboard access
874 if (window.attachEvent) {
875 // it is IE or Opera
876 input.setAttribute('tabIndex', "-1");
877 }
878
879 return input;
880 }
881 };
882
883 /**
884 * Class for uploading files, uploading itself is handled by child classes
885 */
886 qq.UploadHandlerAbstract = function (o) {
887 this._options = {
888 debug: false,
889 action: '/upload.php',
890 // maximum number of concurrent uploads
891 maxConnections: 999,
892 onProgress: function (id, fileName, loaded, total) { },
893 onComplete: function (id, fileName, response) { },
894 onCancel: function (id, fileName) { }
895 };
896 qq.extend(this._options, o);
897
898 this._queue = [];
899 // params for files in queue
900 this._params = [];
901 };
902 qq.UploadHandlerAbstract.prototype = {
903 log: function (str) {
904 if (this._options.debug && window.console) console.log('[uploader] ' + str);
905 },
906 /**
907 * Adds file or file input to the queue
908 * @returns id
909 **/
910 add: function (file) { },
911 /**
912 * Sends the file identified by id and additional query params to the server
913 */
914 upload: function (id, params) {
915 var len = this._queue.push(id);
916
917 var copy = {};
918 qq.extend(copy, params);
919 this._params[id] = copy;
920
921 // if too many active uploads, wait...
922 if (len <= this._options.maxConnections) {
923 this._upload(id, this._params[id]);
924 }
925 },
926 /**
927 * Cancels file upload by id
928 */
929 cancel: function (id) {
930 this._cancel(id);
931 this._dequeue(id);
932 },
933 /**
934 * Cancells all uploads
935 */
936 cancelAll: function () {
937 for (var i = 0; i < this._queue.length; i++) {
938 this._cancel(this._queue[i]);
939 }
940 this._queue = [];
941 },
942 /**
943 * Returns name of the file identified by id
944 */
945 getName: function (id) { },
946 /**
947 * Returns size of the file identified by id
948 */
949 getSize: function (id) { },
950 /**
951 * Returns id of files being uploaded or
952 * waiting for their turn
953 */
954 getQueue: function () {
955 return this._queue;
956 },
957 /**
958 * Actual upload method
959 */
960 _upload: function (id) { },
961 /**
962 * Actual cancel method
963 */
964 _cancel: function (id) { },
965 /**
966 * Removes element from queue, starts upload of next
967 */
968 _dequeue: function (id) {
969 var i = qq.indexOf(this._queue, id);
970 this._queue.splice(i, 1);
971
972 var max = this._options.maxConnections;
973
974 if (this._queue.length >= max && i < max) {
975 var nextId = this._queue[max - 1];
976 this._upload(nextId, this._params[nextId]);
977 }
978 }
979 };
980
981 /**
982 * Class for uploading files using form and iframe
983 * @inherits qq.UploadHandlerAbstract
984 */
985 qq.UploadHandlerForm = function (o) {
986 qq.UploadHandlerAbstract.apply(this, arguments);
987
988 this._inputs = {};
989 };
990 // @inherits qq.UploadHandlerAbstract
991 qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype);
992
993 qq.extend(qq.UploadHandlerForm.prototype, {
994 add: function (fileInput) {
995 fileInput.setAttribute('name', 'qqfile');
996 var id = 'qq-upload-handler-iframe' + qq.getUniqueId();
997
998 this._inputs[id] = fileInput;
999
1000 // remove file input from DOM
1001 if (fileInput.parentNode) {
1002 qq.remove(fileInput);
1003 }
1004
1005 return id;
1006 },
1007 getName: function (id) {
1008 // get input value and remove path to normalize
1009 return this._inputs[id].value.replace(/.*(\/|\\)/, "");
1010 },
1011 _cancel: function (id) {
1012 this._options.onCancel(id, this.getName(id));
1013
1014 delete this._inputs[id];
1015
1016 var iframe = document.getElementById(id);
1017 if (iframe) {
1018 // to cancel request set src to something else
1019 // we use src="javascript:false;" because it doesn't
1020 // trigger ie6 prompt on https
1021 iframe.setAttribute('src', 'javascript:false;');
1022
1023 qq.remove(iframe);
1024 }
1025 },
1026 _upload: function (id, params) {
1027 var input = this._inputs[id];
1028
1029 if (!input) {
1030 throw new Error('file with passed id was not added, or already uploaded or cancelled');
1031 }
1032
1033 var fileName = this.getName(id);
1034
1035 var iframe = this._createIframe(id);
1036 var form = this._createForm(iframe, params);
1037 form.appendChild(input);
1038
1039 var self = this;
1040 this._attachLoadEvent(iframe, function () {
1041 self.log('iframe loaded');
1042
1043 var response = self._getIframeContentJSON(iframe);
1044
1045 self._options.onComplete(id, fileName, response);
1046 self._dequeue(id);
1047
1048 delete self._inputs[id];
1049 // timeout added to fix busy state in FF3.6
1050 setTimeout(function () {
1051 qq.remove(iframe);
1052 }, 1);
1053 });
1054
1055 form.submit();
1056 qq.remove(form);
1057
1058 return id;
1059 },
1060 _attachLoadEvent: function (iframe, callback) {
1061 qq.attach(iframe, 'load', function () {
1062 // when we remove iframe from dom
1063 // the request stops, but in IE load
1064 // event fires
1065 if (!iframe.parentNode) {
1066 return;
1067 }
1068
1069 // fixing Opera 10.53
1070 if (iframe.contentDocument &&
1071 iframe.contentDocument.body &&
1072 iframe.contentDocument.body.innerHTML == "false") {
1073 // In Opera event is fired second time
1074 // when body.innerHTML changed from false
1075 // to server response approx. after 1 sec
1076 // when we upload file with iframe
1077 return;
1078 }
1079
1080 callback();
1081 });
1082 },
1083 /**
1084 * Returns json object received by iframe from server.
1085 */
1086 _getIframeContentJSON: function (iframe) {
1087 // iframe.contentWindow.document - for IE<7
1088 var doc = iframe.contentDocument ? iframe.contentDocument : iframe.contentWindow.document,
1089 response;
1090
1091 this.log("converting iframe's innerHTML to JSON");
1092 this.log("innerHTML = " + doc.body.innerHTML);
1093
1094 try {
1095 response = eval("(" + doc.body.innerHTML + ")");
1096 } catch {
1097 response = {};
1098 }
1099
1100 return response;
1101 },
1102 /**
1103 * Creates iframe with unique name
1104 */
1105 _createIframe: function (id) {
1106 // We can't use following code as the name attribute
1107 // won't be properly registered in IE6, and new window
1108 // on form submit will open
1109 // var iframe = document.createElement('iframe');
1110 // iframe.setAttribute('name', id);
1111
1112 var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />');
1113 // src="javascript:false;" removes ie6 prompt on https
1114
1115 iframe.setAttribute('id', id);
1116
1117 iframe.style.display = 'none';
1118 document.body.appendChild(iframe);
1119
1120 return iframe;
1121 },
1122 /**
1123 * Creates form, that will be submitted to iframe
1124 */
1125 _createForm: function (iframe, params) {
1126 // We can't use the following code in IE6
1127 // var form = document.createElement('form');
1128 // form.setAttribute('method', 'post');
1129 // form.setAttribute('enctype', 'multipart/form-data');
1130 // Because in this case file won't be attached to request
1131 var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>');
1132
1133 var queryString = qq.obj2url(params, this._options.action);
1134
1135 form.setAttribute('action', queryString);
1136 form.setAttribute('target', iframe.name);
1137 form.style.display = 'none';
1138 document.body.appendChild(form);
1139
1140 return form;
1141 }
1142 });
1143
1144 /**
1145 * Class for uploading files using xhr
1146 * @inherits qq.UploadHandlerAbstract
1147 */
1148 qq.UploadHandlerXhr = function (o) {
1149 qq.UploadHandlerAbstract.apply(this, arguments);
1150
1151 this._files = [];
1152 this._xhrs = [];
1153
1154 // current loaded size in bytes for each file
1155 this._loaded = [];
1156 };
1157
1158 // static method
1159 qq.UploadHandlerXhr.isSupported = function () {
1160 var input = document.createElement('input');
1161 input.type = 'file';
1162
1163 return (
1164 'multiple' in input &&
1165 typeof File != "undefined" &&
1166 typeof (new XMLHttpRequest()).upload != "undefined");
1167 };
1168
1169 // @inherits qq.UploadHandlerAbstract
1170 qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
1171
1172 qq.extend(qq.UploadHandlerXhr.prototype, {
1173 /**
1174 * Adds file to the queue
1175 * Returns id to use with upload, cancel
1176 **/
1177 add: function (file) {
1178 if (!(file instanceof File)) {
1179 throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
1180 }
1181
1182 return this._files.push(file) - 1;
1183 },
1184 getName: function (id) {
1185 var file = this._files[id];
1186 // fix missing name in Safari 4
1187 return file.fileName != null ? file.fileName : file.name;
1188 },
1189 getSize: function (id) {
1190 var file = this._files[id];
1191 return file.fileSize != null ? file.fileSize : file.size;
1192 },
1193 /**
1194 * Returns uploaded bytes for file identified by id
1195 */
1196 getLoaded: function (id) {
1197 return this._loaded[id] || 0;
1198 },
1199 /**
1200 * Sends the file identified by id and additional query params to the server
1201 * @param {Object} params name-value string pairs
1202 */
1203 _upload: function (id, params) {
1204 var file = this._files[id],
1205 name = this.getName(id),
1206 size = this.getSize(id);
1207
1208 this._loaded[id] = 0;
1209
1210 var xhr = this._xhrs[id] = new XMLHttpRequest();
1211 var self = this;
1212
1213 xhr.upload.onprogress = function (e) {
1214 if (e.lengthComputable) {
1215 self._loaded[id] = e.loaded;
1216 self._options.onProgress(id, name, e.loaded, e.total);
1217 }
1218 };
1219
1220 xhr.onreadystatechange = function () {
1221 if (xhr.readyState == 4) {
1222 self._onComplete(id, xhr);
1223 }
1224 };
1225
1226 // build query string
1227 params = params || {};
1228 params['qqfile'] = name;
1229 var queryString = qq.obj2url(params, this._options.action);
1230 xhr.open("POST", queryString, true);
1231 xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1232 xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
1233 xhr.setRequestHeader("Content-Type", "application/octet-stream");
1234 xhr.send(file);
1235 },
1236 _onComplete: function (id, xhr) {
1237 // the request was aborted/cancelled
1238 if (!this._files[id]) return;
1239
1240 var name = this.getName(id);
1241 var size = this.getSize(id);
1242
1243 this._options.onProgress(id, name, size, size);
1244
1245 if (xhr.status == 200) {
1246 this.log("xhr - server response received");
1247 this.log("responseText = " + xhr.responseText);
1248
1249 var response;
1250
1251 try {
1252 response = eval("(" + xhr.responseText + ")");
1253 } catch (err) {
1254 response = {};
1255 }
1256
1257 this._options.onComplete(id, name, response);
1258
1259 } else {
1260 this._options.onComplete(id, name, {});
1261 }
1262
1263 this._files[id] = null;
1264 this._xhrs[id] = null;
1265 this._dequeue(id);
1266 },
1267 _cancel: function (id) {
1268 this._options.onCancel(id, this.getName(id));
1269
1270 this._files[id] = null;
1271
1272 if (this._xhrs[id]) {
1273 this._xhrs[id].abort();
1274 this._xhrs[id] = null;
1275 }
1276 }
1277 });