0a4a072e620ea47799fdd286dc338c229e030e04
[citadel.git] / webcit / static / fineuploader.js
1 /*!
2 * Fine Uploader
3 *
4 * Copyright 2013, Widen Enterprises, Inc. info@fineuploader.com
5 *
6 * Version: 4.2.1
7 *
8 * Homepage: http://fineuploader.com
9 *
10 * Repository: git://github.com/Widen/fine-uploader.git
11 *
12 * Licensed under GNU GPL v3, see LICENSE
13 */ 
14
15
16 /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob, Storage, ActiveXObject */
17 var qq = function(element) {
18     "use strict";
19
20     return {
21         hide: function() {
22             element.style.display = "none";
23             return this;
24         },
25
26         /** Returns the function which detaches attached event */
27         attach: function(type, fn) {
28             if (element.addEventListener){
29                 element.addEventListener(type, fn, false);
30             } else if (element.attachEvent){
31                 element.attachEvent("on" + type, fn);
32             }
33             return function() {
34                 qq(element).detach(type, fn);
35             };
36         },
37
38         detach: function(type, fn) {
39             if (element.removeEventListener){
40                 element.removeEventListener(type, fn, false);
41             } else if (element.attachEvent){
42                 element.detachEvent("on" + type, fn);
43             }
44             return this;
45         },
46
47         contains: function(descendant) {
48             // The [W3C spec](http://www.w3.org/TR/domcore/#dom-node-contains)
49             // says a `null` (or ostensibly `undefined`) parameter
50             // passed into `Node.contains` should result in a false return value.
51             // IE7 throws an exception if the parameter is `undefined` though.
52             if (!descendant) {
53                 return false;
54             }
55
56             // compareposition returns false in this case
57             if (element === descendant) {
58                 return true;
59             }
60
61             if (element.contains){
62                 return element.contains(descendant);
63             } else {
64                 /*jslint bitwise: true*/
65                 return !!(descendant.compareDocumentPosition(element) & 8);
66             }
67         },
68
69         /**
70          * Insert this element before elementB.
71          */
72         insertBefore: function(elementB) {
73             elementB.parentNode.insertBefore(element, elementB);
74             return this;
75         },
76
77         remove: function() {
78             element.parentNode.removeChild(element);
79             return this;
80         },
81
82         /**
83          * Sets styles for an element.
84          * Fixes opacity in IE6-8.
85          */
86         css: function(styles) {
87             /*jshint eqnull: true*/
88             if (element.style == null) {
89                 throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!");
90             }
91
92             /*jshint -W116*/
93             if (styles.opacity != null){
94                 if (typeof element.style.opacity !== "string" && typeof(element.filters) !== "undefined"){
95                     styles.filter = "alpha(opacity=" + Math.round(100 * styles.opacity) + ")";
96                 }
97             }
98             qq.extend(element.style, styles);
99
100             return this;
101         },
102
103         hasClass: function(name) {
104             var re = new RegExp("(^| )" + name + "( |$)");
105             return re.test(element.className);
106         },
107
108         addClass: function(name) {
109             if (!qq(element).hasClass(name)){
110                 element.className += " " + name;
111             }
112             return this;
113         },
114
115         removeClass: function(name) {
116             var re = new RegExp("(^| )" + name + "( |$)");
117             element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, "");
118             return this;
119         },
120
121         getByClass: function(className) {
122             var candidates,
123                 result = [];
124
125             if (element.querySelectorAll){
126                 return element.querySelectorAll("." + className);
127             }
128
129             candidates = element.getElementsByTagName("*");
130
131             qq.each(candidates, function(idx, val) {
132                 if (qq(val).hasClass(className)){
133                     result.push(val);
134                 }
135             });
136             return result;
137         },
138
139         children: function() {
140             var children = [],
141                 child = element.firstChild;
142
143             while (child){
144                 if (child.nodeType === 1){
145                     children.push(child);
146                 }
147                 child = child.nextSibling;
148             }
149
150             return children;
151         },
152
153         setText: function(text) {
154             element.innerText = text;
155             element.textContent = text;
156             return this;
157         },
158
159         clearText: function() {
160             return qq(element).setText("");
161         },
162
163         // Returns true if the attribute exists on the element
164         // AND the value of the attribute is NOT "false" (case-insensitive)
165         hasAttribute: function(attrName) {
166             var attrVal;
167
168             if (element.hasAttribute) {
169
170                 if (!element.hasAttribute(attrName)) {
171                     return false;
172                 }
173
174                 /*jshint -W116*/
175                 return (/^false$/i).exec(element.getAttribute(attrName)) == null;
176             }
177             else {
178                 attrVal = element[attrName];
179
180                 if (attrVal === undefined) {
181                     return false;
182                 }
183
184                 /*jshint -W116*/
185                 return (/^false$/i).exec(attrVal) == null;
186             }
187         }
188     };
189 };
190
191 (function(){
192     "use strict";
193
194     qq.log = function(message, level) {
195         if (window.console) {
196             if (!level || level === "info") {
197                 window.console.log(message);
198             }
199             else
200             {
201                 if (window.console[level]) {
202                     window.console[level](message);
203                 }
204                 else {
205                     window.console.log("<" + level + "> " + message);
206                 }
207             }
208         }
209     };
210
211     qq.isObject = function(variable) {
212         return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]";
213     };
214
215     qq.isFunction = function(variable) {
216         return typeof(variable) === "function";
217     };
218
219     /**
220      * Check the type of a value.  Is it an "array"?
221      *
222      * @param value value to test.
223      * @returns true if the value is an array or associated with an `ArrayBuffer`
224      */
225     qq.isArray = function(value) {
226         return Object.prototype.toString.call(value) === "[object Array]" ||
227             (value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer);
228     };
229
230     // Looks for an object on a `DataTransfer` object that is associated with drop events when utilizing the Filesystem API.
231     qq.isItemList = function(maybeItemList) {
232         return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]";
233     };
234
235     // Looks for an object on a `NodeList` or an `HTMLCollection`|`HTMLFormElement`|`HTMLSelectElement`
236     // object that is associated with collections of Nodes.
237     qq.isNodeList = function(maybeNodeList) {
238         return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" ||
239             // If `HTMLCollection` is the actual type of the object, we must determine this
240             // by checking for expected properties/methods on the object
241             (maybeNodeList.item && maybeNodeList.namedItem);
242     };
243
244     qq.isString = function(maybeString) {
245         return Object.prototype.toString.call(maybeString) === "[object String]";
246     };
247
248     qq.trimStr = function(string) {
249         if (String.prototype.trim) {
250             return string.trim();
251         }
252
253         return string.replace(/^\s+|\s+$/g,"");
254     };
255
256
257     /**
258      * @param str String to format.
259      * @returns {string} A string, swapping argument values with the associated occurrence of {} in the passed string.
260      */
261     qq.format = function(str) {
262
263         var args =  Array.prototype.slice.call(arguments, 1),
264             newStr = str,
265             nextIdxToReplace = newStr.indexOf("{}");
266
267         qq.each(args, function(idx, val) {
268             var strBefore = newStr.substring(0, nextIdxToReplace),
269                 strAfter = newStr.substring(nextIdxToReplace+2);
270
271             newStr = strBefore + val + strAfter;
272             nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length);
273
274             // End the loop if we have run out of tokens (when the arguments exceed the # of tokens)
275             if (nextIdxToReplace < 0) {
276                 return false;
277             }
278         });
279
280         return newStr;
281     };
282
283     qq.isFile = function(maybeFile) {
284         return window.File && Object.prototype.toString.call(maybeFile) === "[object File]";
285     };
286
287     qq.isFileList = function(maybeFileList) {
288         return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]";
289     };
290
291     qq.isFileOrInput = function(maybeFileOrInput) {
292         return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput);
293     };
294
295     qq.isInput = function(maybeInput) {
296         if (window.HTMLInputElement) {
297             if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") {
298                 if (maybeInput.type && maybeInput.type.toLowerCase() === "file") {
299                     return true;
300                 }
301             }
302         }
303         if (maybeInput.tagName) {
304             if (maybeInput.tagName.toLowerCase() === "input") {
305                 if (maybeInput.type && maybeInput.type.toLowerCase() === "file") {
306                     return true;
307                 }
308             }
309         }
310
311         return false;
312     };
313
314     qq.isBlob = function(maybeBlob) {
315         return window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]";
316     };
317
318     qq.isXhrUploadSupported = function() {
319         var input = document.createElement("input");
320         input.type = "file";
321
322         return (
323             input.multiple !== undefined &&
324                 typeof File !== "undefined" &&
325                 typeof FormData !== "undefined" &&
326                 typeof (qq.createXhrInstance()).upload !== "undefined" );
327     };
328
329     // Fall back to ActiveX is native XHR is disabled (possible in any version of IE).
330     qq.createXhrInstance = function() {
331         if (window.XMLHttpRequest) {
332             return new XMLHttpRequest();
333         }
334
335         try {
336             return new ActiveXObject("MSXML2.XMLHTTP.3.0");
337         }
338         catch(error) {
339             qq.log("Neither XHR or ActiveX are supported!", "error");
340             return null;
341         }
342     };
343
344     qq.isFolderDropSupported = function(dataTransfer) {
345         return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
346     };
347
348     qq.isFileChunkingSupported = function() {
349         return !qq.android() && //android's impl of Blob.slice is broken
350             qq.isXhrUploadSupported() &&
351             (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined);
352     };
353
354     qq.sliceBlob = function(fileOrBlob, start, end) {
355         var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice;
356
357         return slicer.call(fileOrBlob, start, end);
358     };
359
360     qq.arrayBufferToHex = function(buffer) {
361         var bytesAsHex = "",
362             bytes = new Uint8Array(buffer);
363
364
365         qq.each(bytes, function(idx, byte) {
366             var byteAsHexStr = byte.toString(16);
367
368             if (byteAsHexStr.length < 2) {
369                 byteAsHexStr = "0" + byteAsHexStr;
370             }
371
372             bytesAsHex += byteAsHexStr;
373         });
374
375         return bytesAsHex;
376     };
377
378     qq.readBlobToHex = function(blob, startOffset, length) {
379         var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length),
380             fileReader = new FileReader(),
381             promise = new qq.Promise();
382
383         fileReader.onload = function() {
384             promise.success(qq.arrayBufferToHex(fileReader.result));
385         };
386
387         fileReader.readAsArrayBuffer(initialBlob);
388
389         return promise;
390     };
391
392     qq.extend = function(first, second, extendNested) {
393         qq.each(second, function(prop, val) {
394             if (extendNested && qq.isObject(val)) {
395                 if (first[prop] === undefined) {
396                     first[prop] = {};
397                 }
398                 qq.extend(first[prop], val, true);
399             }
400             else {
401                 first[prop] = val;
402             }
403         });
404
405         return first;
406     };
407
408     /**
409      * Allow properties in one object to override properties in another,
410      * keeping track of the original values from the target object.
411      *
412      * Note that the pre-overriden properties to be overriden by the source will be passed into the `sourceFn` when it is invoked.
413      *
414      * @param target Update properties in this object from some source
415      * @param sourceFn A function that, when invoked, will return properties that will replace properties with the same name in the target.
416      * @returns {object} The target object
417      */
418     qq.override = function(target, sourceFn) {
419         var super_ = {},
420             source = sourceFn(super_);
421
422         qq.each(source, function(srcPropName, srcPropVal) {
423             if (target[srcPropName] !== undefined) {
424                 super_[srcPropName] = target[srcPropName];
425             }
426
427             target[srcPropName] = srcPropVal;
428         });
429
430         return target;
431     };
432
433     /**
434      * Searches for a given element in the array, returns -1 if it is not present.
435      * @param {Number} [from] The index at which to begin the search
436      */
437     qq.indexOf = function(arr, elt, from){
438         if (arr.indexOf) {
439             return arr.indexOf(elt, from);
440         }
441
442         from = from || 0;
443         var len = arr.length;
444
445         if (from < 0) {
446             from += len;
447         }
448
449         for (; from < len; from+=1){
450             if (arr.hasOwnProperty(from) && arr[from] === elt){
451                 return from;
452             }
453         }
454         return -1;
455     };
456
457     //this is a version 4 UUID
458     qq.getUniqueId = function(){
459         return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
460             /*jslint eqeq: true, bitwise: true*/
461             var r = Math.random()*16|0, v = c == "x" ? r : (r&0x3|0x8);
462             return v.toString(16);
463         });
464     };
465
466     //
467     // Browsers and platforms detection
468
469     qq.ie       = function(){
470         return navigator.userAgent.indexOf("MSIE") !== -1;
471     };
472     qq.ie7      = function(){
473         return navigator.userAgent.indexOf("MSIE 7") !== -1;
474     };
475     qq.ie10     = function(){
476         return navigator.userAgent.indexOf("MSIE 10") !== -1;
477     };
478     qq.ie11     = function(){
479         return (navigator.userAgent.indexOf("Trident") !== -1 &&
480             navigator.userAgent.indexOf("rv:11") !== -1);
481     };
482     qq.safari   = function(){
483         return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
484     };
485     qq.chrome   = function(){
486         return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1;
487     };
488     qq.opera   = function(){
489         return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1;
490     };
491     qq.firefox  = function(){
492         return (!qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === "");
493     };
494     qq.windows  = function(){
495         return navigator.platform === "Win32";
496     };
497     qq.android = function(){
498         return navigator.userAgent.toLowerCase().indexOf("android") !== -1;
499     };
500     qq.ios7 = function() {
501         return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1;
502     };
503     qq.ios = function() {
504         /*jshint -W014 */
505         return navigator.userAgent.indexOf("iPad") !== -1
506             || navigator.userAgent.indexOf("iPod") !== -1
507             || navigator.userAgent.indexOf("iPhone") !== -1;
508     };
509
510     //
511     // Events
512
513     qq.preventDefault = function(e){
514         if (e.preventDefault){
515             e.preventDefault();
516         } else{
517             e.returnValue = false;
518         }
519     };
520
521     /**
522      * Creates and returns element from html string
523      * Uses innerHTML to create an element
524      */
525     qq.toElement = (function(){
526         var div = document.createElement("div");
527         return function(html){
528             div.innerHTML = html;
529             var element = div.firstChild;
530             div.removeChild(element);
531             return element;
532         };
533     }());
534
535     //key and value are passed to callback for each entry in the iterable item
536     qq.each = function(iterableItem, callback) {
537         var keyOrIndex, retVal;
538
539         if (iterableItem) {
540             // Iterate through [`Storage`](http://www.w3.org/TR/webstorage/#the-storage-interface) items
541             if (window.Storage && iterableItem.constructor === window.Storage) {
542                 for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) {
543                     retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex)));
544                     if (retVal === false) {
545                         break;
546                     }
547                 }
548             }
549             // `DataTransferItemList` & `NodeList` objects are array-like and should be treated as arrays
550             // when iterating over items inside the object.
551             else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) {
552                 for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) {
553                     retVal = callback(keyOrIndex, iterableItem[keyOrIndex]);
554                     if (retVal === false) {
555                         break;
556                     }
557                 }
558             }
559             else if (qq.isString(iterableItem)) {
560                 for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) {
561                     retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex));
562                     if (retVal === false) {
563                         break;
564                     }
565                 }
566             }
567             else {
568                 for (keyOrIndex in iterableItem) {
569                     if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) {
570                         retVal = callback(keyOrIndex, iterableItem[keyOrIndex]);
571                         if (retVal === false) {
572                             break;
573                         }
574                     }
575                 }
576             }
577         }
578     };
579
580     //include any args that should be passed to the new function after the context arg
581     qq.bind = function(oldFunc, context) {
582         if (qq.isFunction(oldFunc)) {
583             var args =  Array.prototype.slice.call(arguments, 2);
584
585             return function() {
586                 var newArgs = qq.extend([], args);
587                 if (arguments.length) {
588                     newArgs = newArgs.concat(Array.prototype.slice.call(arguments));
589                 }
590                 return oldFunc.apply(context, newArgs);
591             };
592         }
593
594         throw new Error("first parameter must be a function!");
595     };
596
597     /**
598      * obj2url() takes a json-object as argument and generates
599      * a querystring. pretty much like jQuery.param()
600      *
601      * how to use:
602      *
603      *    `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
604      *
605      * will result in:
606      *
607      *    `http://any.url/upload?otherParam=value&a=b&c=d`
608      *
609      * @param  Object JSON-Object
610      * @param  String current querystring-part
611      * @return String encoded querystring
612      */
613     qq.obj2url = function(obj, temp, prefixDone){
614         /*jshint laxbreak: true*/
615         var uristrings = [],
616             prefix = "&",
617             add = function(nextObj, i){
618                 var nextTemp = temp
619                     ? (/\[\]$/.test(temp)) // prevent double-encoding
620                     ? temp
621                     : temp+"["+i+"]"
622                     : i;
623                 if ((nextTemp !== "undefined") && (i !== "undefined")) {
624                     uristrings.push(
625                         (typeof nextObj === "object")
626                             ? qq.obj2url(nextObj, nextTemp, true)
627                             : (Object.prototype.toString.call(nextObj) === "[object Function]")
628                             ? encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj())
629                             : encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj)
630                     );
631                 }
632             };
633
634         if (!prefixDone && temp) {
635             prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? "" : "&" : "?";
636             uristrings.push(temp);
637             uristrings.push(qq.obj2url(obj));
638         } else if ((Object.prototype.toString.call(obj) === "[object Array]") && (typeof obj !== "undefined") ) {
639             qq.each(obj, function(idx, val) {
640                 add(val, idx);
641             });
642         } else if ((typeof obj !== "undefined") && (obj !== null) && (typeof obj === "object")){
643             qq.each(obj, function(prop, val) {
644                 add(val, prop);
645             });
646         } else {
647             uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj));
648         }
649
650         if (temp) {
651             return uristrings.join(prefix);
652         } else {
653             return uristrings.join(prefix)
654                 .replace(/^&/, "")
655                 .replace(/%20/g, "+");
656         }
657     };
658
659     qq.obj2FormData = function(obj, formData, arrayKeyName) {
660         if (!formData) {
661             formData = new FormData();
662         }
663
664         qq.each(obj, function(key, val) {
665             key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key;
666
667             if (qq.isObject(val)) {
668                 qq.obj2FormData(val, formData, key);
669             }
670             else if (qq.isFunction(val)) {
671                 formData.append(key, val());
672             }
673             else {
674                 formData.append(key, val);
675             }
676         });
677
678         return formData;
679     };
680
681     qq.obj2Inputs = function(obj, form) {
682         var input;
683
684         if (!form) {
685             form = document.createElement("form");
686         }
687
688         qq.obj2FormData(obj, {
689             append: function(key, val) {
690                 input = document.createElement("input");
691                 input.setAttribute("name", key);
692                 input.setAttribute("value", val);
693                 form.appendChild(input);
694             }
695         });
696
697         return form;
698     };
699
700     qq.setCookie = function(name, value, days) {
701         var date = new Date(),
702             expires = "";
703
704         if (days) {
705             date.setTime(date.getTime()+(days*24*60*60*1000));
706             expires = "; expires="+date.toGMTString();
707         }
708
709         document.cookie = name+"="+value+expires+"; path=/";
710     };
711
712     qq.getCookie = function(name) {
713         var nameEQ = name + "=",
714             ca = document.cookie.split(";"),
715             cookie;
716
717         qq.each(ca, function(idx, part) {
718             /*jshint -W116 */
719             var cookiePart = part;
720             while (cookiePart.charAt(0) == " ") {
721                 cookiePart = cookiePart.substring(1, cookiePart.length);
722             }
723
724             if (cookiePart.indexOf(nameEQ) === 0) {
725                 cookie = cookiePart.substring(nameEQ.length, cookiePart.length);
726                 return false;
727             }
728         });
729
730         return cookie;
731     };
732
733     qq.getCookieNames = function(regexp) {
734         var cookies = document.cookie.split(";"),
735             cookieNames = [];
736
737         qq.each(cookies, function(idx, cookie) {
738             cookie = qq.trimStr(cookie);
739
740             var equalsIdx = cookie.indexOf("=");
741
742             if (cookie.match(regexp)) {
743                 cookieNames.push(cookie.substr(0, equalsIdx));
744             }
745         });
746
747         return cookieNames;
748     };
749
750     qq.deleteCookie = function(name) {
751         qq.setCookie(name, "", -1);
752     };
753
754     qq.areCookiesEnabled = function() {
755         var randNum = Math.random() * 100000,
756             name = "qqCookieTest:" + randNum;
757         qq.setCookie(name, 1);
758
759         if (qq.getCookie(name)) {
760             qq.deleteCookie(name);
761             return true;
762         }
763         return false;
764     };
765
766     /**
767      * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
768      * implemented.  For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
769      */
770     qq.parseJson = function(json) {
771         /*jshint evil: true*/
772         if (window.JSON && qq.isFunction(JSON.parse)) {
773             return JSON.parse(json);
774         } else {
775             return eval("(" + json + ")");
776         }
777     };
778
779     /**
780      * Retrieve the extension of a file, if it exists.
781      *
782      * @param filename
783      * @returns {string || undefined}
784      */
785     qq.getExtension = function(filename) {
786         var extIdx = filename.lastIndexOf(".") + 1;
787
788         if (extIdx > 0) {
789             return filename.substr(extIdx, filename.length - extIdx);
790         }
791     };
792
793     qq.getFilename = function(blobOrFileInput) {
794         /*jslint regexp: true*/
795
796         if (qq.isInput(blobOrFileInput)) {
797             // get input value and remove path to normalize
798             return blobOrFileInput.value.replace(/.*(\/|\\)/, "");
799         }
800         else if (qq.isFile(blobOrFileInput)) {
801             if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) {
802                 return blobOrFileInput.fileName;
803             }
804         }
805
806         return blobOrFileInput.name;
807     };
808
809     /**
810      * A generic module which supports object disposing in dispose() method.
811      * */
812     qq.DisposeSupport = function() {
813         var disposers = [];
814
815         return {
816             /** Run all registered disposers */
817             dispose: function() {
818                 var disposer;
819                 do {
820                     disposer = disposers.shift();
821                     if (disposer) {
822                         disposer();
823                     }
824                 }
825                 while (disposer);
826             },
827
828             /** Attach event handler and register de-attacher as a disposer */
829             attach: function() {
830                 var args = arguments;
831                 /*jslint undef:true*/
832                 this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
833             },
834
835             /** Add disposer to the collection */
836             addDisposer: function(disposeFunction) {
837                 disposers.push(disposeFunction);
838             }
839         };
840     };
841 }());
842
843 /* globals qq */
844 /**
845  * Fine Uploader top-level Error container.  Inherits from `Error`.
846  */
847 (function() {
848     "use strict";
849
850     qq.Error = function(message) {
851         this.message = message;
852     };
853
854     qq.Error.prototype = new Error();
855 }());
856
857 /*global qq */
858 qq.version="4.2.1";
859
860 /* globals qq */
861 qq.supportedFeatures = (function () {
862     "use strict";
863
864     var supportsUploading,
865         supportsAjaxFileUploading,
866         supportsFolderDrop,
867         supportsChunking,
868         supportsResume,
869         supportsUploadViaPaste,
870         supportsUploadCors,
871         supportsDeleteFileXdr,
872         supportsDeleteFileCorsXhr,
873         supportsDeleteFileCors,
874         supportsFolderSelection,
875         supportsImagePreviews;
876
877
878     function testSupportsFileInputElement() {
879         var supported = true,
880             tempInput;
881
882         try {
883             tempInput = document.createElement("input");
884             tempInput.type = "file";
885             qq(tempInput).hide();
886
887             if (tempInput.disabled) {
888                 supported = false;
889             }
890         }
891         catch (ex) {
892             supported = false;
893         }
894
895         return supported;
896     }
897
898     //only way to test for Filesystem API support since webkit does not expose the DataTransfer interface
899     function isChrome21OrHigher() {
900         return (qq.chrome() || qq.opera()) &&
901             navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined;
902     }
903
904     //only way to test for complete Clipboard API support at this time
905     function isChrome14OrHigher() {
906         return (qq.chrome() || qq.opera()) &&
907             navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined;
908     }
909
910     //Ensure we can send cross-origin `XMLHttpRequest`s
911     function isCrossOriginXhrSupported() {
912         if (window.XMLHttpRequest) {
913             var xhr = qq.createXhrInstance();
914
915             //Commonly accepted test for XHR CORS support.
916             return xhr.withCredentials !== undefined;
917         }
918
919         return false;
920     }
921
922     //Test for (terrible) cross-origin ajax transport fallback for IE9 and IE8
923     function isXdrSupported() {
924         return window.XDomainRequest !== undefined;
925     }
926
927     // CORS Ajax requests are supported if it is either possible to send credentialed `XMLHttpRequest`s,
928     // or if `XDomainRequest` is an available alternative.
929     function isCrossOriginAjaxSupported() {
930         if (isCrossOriginXhrSupported()) {
931             return true;
932         }
933
934         return isXdrSupported();
935     }
936
937     function isFolderSelectionSupported() {
938         // We know that folder selection is only supported in Chrome via this proprietary attribute for now
939         return document.createElement("input").webkitdirectory !== undefined;
940     }
941
942
943     supportsUploading = testSupportsFileInputElement();
944
945     supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported();
946
947     supportsFolderDrop = supportsAjaxFileUploading && isChrome21OrHigher();
948
949     supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported();
950
951     supportsResume = supportsAjaxFileUploading && supportsChunking && qq.areCookiesEnabled();
952
953     supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher();
954
955     supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading);
956
957     supportsDeleteFileCorsXhr = isCrossOriginXhrSupported();
958
959     supportsDeleteFileXdr = isXdrSupported();
960
961     supportsDeleteFileCors = isCrossOriginAjaxSupported();
962
963     supportsFolderSelection = isFolderSelectionSupported();
964
965     supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined;
966
967
968     return {
969         uploading: supportsUploading,
970         ajaxUploading: supportsAjaxFileUploading,
971         fileDrop: supportsAjaxFileUploading, //NOTE: will also return true for touch-only devices.  It's not currently possible to accurately test for touch-only devices
972         folderDrop: supportsFolderDrop,
973         chunking: supportsChunking,
974         resume: supportsResume,
975         uploadCustomHeaders: supportsAjaxFileUploading,
976         uploadNonMultipart: supportsAjaxFileUploading,
977         itemSizeValidation: supportsAjaxFileUploading,
978         uploadViaPaste: supportsUploadViaPaste,
979         progressBar: supportsAjaxFileUploading,
980         uploadCors: supportsUploadCors,
981         deleteFileCorsXhr: supportsDeleteFileCorsXhr,
982         deleteFileCorsXdr: supportsDeleteFileXdr, //NOTE: will also return true in IE10, where XDR is also supported
983         deleteFileCors: supportsDeleteFileCors,
984         canDetermineSize: supportsAjaxFileUploading,
985         folderSelection: supportsFolderSelection,
986         imagePreviews: supportsImagePreviews,
987         imageValidation: supportsImagePreviews,
988         pause: supportsChunking
989     };
990
991 }());
992
993 /*globals qq*/
994 qq.Promise = function() {
995     "use strict";
996
997     var successArgs, failureArgs,
998         successCallbacks = [],
999         failureCallbacks = [],
1000         doneCallbacks = [],
1001         state = 0;
1002
1003     qq.extend(this, {
1004         then: function(onSuccess, onFailure) {
1005             if (state === 0) {
1006                 if (onSuccess) {
1007                     successCallbacks.push(onSuccess);
1008                 }
1009                 if (onFailure) {
1010                     failureCallbacks.push(onFailure);
1011                 }
1012             }
1013             else if (state === -1) {
1014                 onFailure && onFailure.apply(null, failureArgs);
1015             }
1016             else if (onSuccess) {
1017                 onSuccess.apply(null,successArgs);
1018             }
1019
1020             return this;
1021         },
1022
1023         done: function(callback) {
1024             if (state === 0) {
1025                 doneCallbacks.push(callback);
1026             }
1027             else {
1028                 callback.apply(null, failureArgs === undefined ? successArgs : failureArgs);
1029             }
1030
1031             return this;
1032         },
1033
1034         success: function() {
1035             state = 1;
1036             successArgs = arguments;
1037
1038             if (successCallbacks.length) {
1039                 qq.each(successCallbacks, function(idx, callback) {
1040                     callback.apply(null, successArgs);
1041                 });
1042             }
1043
1044             if(doneCallbacks.length) {
1045                 qq.each(doneCallbacks, function(idx, callback) {
1046                     callback.apply(null, successArgs);
1047                 });
1048             }
1049
1050             return this;
1051         },
1052
1053         failure: function() {
1054             state = -1;
1055             failureArgs = arguments;
1056
1057             if (failureCallbacks.length) {
1058                 qq.each(failureCallbacks, function(idx, callback) {
1059                     callback.apply(null, failureArgs);
1060                 });
1061             }
1062
1063             if(doneCallbacks.length) {
1064                 qq.each(doneCallbacks, function(idx, callback) {
1065                     callback.apply(null, failureArgs);
1066                 });
1067             }
1068
1069             return this;
1070         }
1071     });
1072 };
1073
1074 /*globals qq*/
1075
1076 /**
1077  * This module represents an upload or "Select File(s)" button.  It's job is to embed an opaque `<input type="file">`
1078  * element as a child of a provided "container" element.  This "container" element (`options.element`) is used to provide
1079  * a custom style for the `<input type="file">` element.  The ability to change the style of the container element is also
1080  * provided here by adding CSS classes to the container on hover/focus.
1081  *
1082  * TODO Eliminate the mouseover and mouseout event handlers since the :hover CSS pseudo-class should now be
1083  * available on all supported browsers.
1084  *
1085  * @param o Options to override the default values
1086  */
1087 qq.UploadButton = function(o) {
1088     "use strict";
1089
1090
1091     var disposeSupport = new qq.DisposeSupport(),
1092
1093         options = {
1094             // "Container" element
1095             element: null,
1096
1097             // If true adds `multiple` attribute to `<input type="file">`
1098             multiple: false,
1099
1100             // Corresponds to the `accept` attribute on the associated `<input type="file">`
1101             acceptFiles: null,
1102
1103             // A true value allows folders to be selected, if supported by the UA
1104             folders: false,
1105
1106             // `name` attribute of `<input type="file">`
1107             name: "qqfile",
1108
1109             // Called when the browser invokes the onchange handler on the `<input type="file">`
1110             onChange: function(input) {},
1111
1112             // **This option will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers
1113             hoverClass: "qq-upload-button-hover",
1114
1115             focusClass: "qq-upload-button-focus"
1116         },
1117         input, buttonId;
1118
1119     // Overrides any of the default option values with any option values passed in during construction.
1120     qq.extend(options, o);
1121
1122     buttonId = qq.getUniqueId();
1123
1124     // Embed an opaque `<input type="file">` element as a child of `options.element`.
1125     function createInput() {
1126         var input = document.createElement("input");
1127
1128         input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId);
1129
1130         if (options.multiple) {
1131             input.setAttribute("multiple", "");
1132         }
1133
1134         if (options.folders && qq.supportedFeatures.folderSelection) {
1135             // selecting directories is only possible in Chrome now, via a vendor-specific prefixed attribute
1136             input.setAttribute("webkitdirectory", "");
1137         }
1138
1139         if (options.acceptFiles) {
1140             input.setAttribute("accept", options.acceptFiles);
1141         }
1142
1143         input.setAttribute("type", "file");
1144         input.setAttribute("name", options.name);
1145
1146         qq(input).css({
1147             position: "absolute",
1148             // in Opera only 'browse' button
1149             // is clickable and it is located at
1150             // the right side of the input
1151             right: 0,
1152             top: 0,
1153             fontFamily: "Arial",
1154             // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
1155             fontSize: "118px",
1156             margin: 0,
1157             padding: 0,
1158             cursor: "pointer",
1159             opacity: 0
1160         });
1161
1162         options.element.appendChild(input);
1163
1164         disposeSupport.attach(input, "change", function(){
1165             options.onChange(input);
1166         });
1167
1168         // **These event handlers will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers
1169         disposeSupport.attach(input, "mouseover", function(){
1170             qq(options.element).addClass(options.hoverClass);
1171         });
1172         disposeSupport.attach(input, "mouseout", function(){
1173             qq(options.element).removeClass(options.hoverClass);
1174         });
1175
1176         disposeSupport.attach(input, "focus", function(){
1177             qq(options.element).addClass(options.focusClass);
1178         });
1179         disposeSupport.attach(input, "blur", function(){
1180             qq(options.element).removeClass(options.focusClass);
1181         });
1182
1183         // IE and Opera, unfortunately have 2 tab stops on file input
1184         // which is unacceptable in our case, disable keyboard access
1185         if (window.attachEvent) {
1186             // it is IE or Opera
1187             input.setAttribute("tabIndex", "-1");
1188         }
1189
1190         return input;
1191     }
1192
1193     // Make button suitable container for input
1194     qq(options.element).css({
1195         position: "relative",
1196         overflow: "hidden",
1197         // Make sure browse button is in the right side in Internet Explorer
1198         direction: "ltr"
1199     });
1200
1201     input = createInput();
1202
1203
1204     // Exposed API
1205     qq.extend(this, {
1206         getInput: function() {
1207             return input;
1208         },
1209
1210         getButtonId: function() {
1211             return buttonId;
1212         },
1213
1214         setMultiple: function(isMultiple) {
1215             if (isMultiple !== options.multiple) {
1216                 if (isMultiple) {
1217                     input.setAttribute("multiple", "");
1218                 }
1219                 else {
1220                     input.removeAttribute("multiple");
1221                 }
1222             }
1223         },
1224
1225         setAcceptFiles: function(acceptFiles) {
1226             if (acceptFiles !== options.acceptFiles) {
1227                 input.setAttribute("accept", acceptFiles);
1228             }
1229         },
1230
1231         reset: function(){
1232             if (input.parentNode){
1233                 qq(input).remove();
1234             }
1235
1236             qq(options.element).removeClass(options.focusClass);
1237             input = createInput();
1238         }
1239     });
1240 };
1241
1242 qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id";
1243
1244 /*globals qq */
1245 qq.UploadData = function(uploaderProxy) {
1246     "use strict";
1247
1248     var data = [],
1249         byUuid = {},
1250         byStatus = {};
1251
1252
1253     function getDataByIds(idOrIds) {
1254         if (qq.isArray(idOrIds)) {
1255             var entries = [];
1256
1257             qq.each(idOrIds, function(idx, id) {
1258                 entries.push(data[id]);
1259             });
1260
1261             return entries;
1262         }
1263
1264         return data[idOrIds];
1265     }
1266
1267     function getDataByUuids(uuids) {
1268         if (qq.isArray(uuids)) {
1269             var entries = [];
1270
1271             qq.each(uuids, function(idx, uuid) {
1272                 entries.push(data[byUuid[uuid]]);
1273             });
1274
1275             return entries;
1276         }
1277
1278         return data[byUuid[uuids]];
1279     }
1280
1281     function getDataByStatus(status) {
1282         var statusResults = [],
1283             statuses = [].concat(status);
1284
1285         qq.each(statuses, function(index, statusEnum) {
1286             var statusResultIndexes = byStatus[statusEnum];
1287
1288             if (statusResultIndexes !== undefined) {
1289                 qq.each(statusResultIndexes, function(i, dataIndex) {
1290                     statusResults.push(data[dataIndex]);
1291                 });
1292             }
1293         });
1294
1295         return statusResults;
1296     }
1297
1298     qq.extend(this, {
1299         /**
1300          * Adds a new file to the data cache for tracking purposes.
1301          *
1302          * @param uuid Initial UUID for this file.
1303          * @param name Initial name of this file.
1304          * @param size Size of this file, -1 if this cannot be determined
1305          * @param status Initial `qq.status` for this file.  If null/undefined, `qq.status.SUBMITTING`.
1306          * @returns {number} Internal ID for this file.
1307          */
1308         addFile: function(uuid, name, size, status) {
1309             status = status || qq.status.SUBMITTING;
1310
1311             var id = data.push({
1312                 name: name,
1313                 originalName: name,
1314                 uuid: uuid,
1315                 size: size,
1316                 status: status
1317             }) - 1;
1318
1319             data[id].id = id;
1320             byUuid[uuid] = id;
1321
1322             if (byStatus[status] === undefined) {
1323                 byStatus[status] = [];
1324             }
1325             byStatus[status].push(id);
1326
1327             uploaderProxy.onStatusChange(id, null, status);
1328
1329             return id;
1330         },
1331
1332         retrieve: function(optionalFilter) {
1333             if (qq.isObject(optionalFilter) && data.length)  {
1334                 if (optionalFilter.id !== undefined) {
1335                     return getDataByIds(optionalFilter.id);
1336                 }
1337
1338                 else if (optionalFilter.uuid !== undefined) {
1339                     return getDataByUuids(optionalFilter.uuid);
1340                 }
1341
1342                 else if (optionalFilter.status) {
1343                     return getDataByStatus(optionalFilter.status);
1344                 }
1345             }
1346             else {
1347                 return qq.extend([], data, true);
1348             }
1349         },
1350
1351         reset: function() {
1352             data = [];
1353             byUuid = {};
1354             byStatus = {};
1355         },
1356
1357         setStatus: function(id, newStatus) {
1358             var oldStatus = data[id].status,
1359                 byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id);
1360
1361             byStatus[oldStatus].splice(byStatusOldStatusIndex, 1);
1362
1363             data[id].status = newStatus;
1364
1365             if (byStatus[newStatus] === undefined) {
1366                 byStatus[newStatus] = [];
1367             }
1368             byStatus[newStatus].push(id);
1369
1370             uploaderProxy.onStatusChange(id, oldStatus, newStatus);
1371         },
1372
1373         uuidChanged: function(id, newUuid) {
1374             var oldUuid = data[id].uuid;
1375
1376             data[id].uuid = newUuid;
1377             byUuid[newUuid] = id;
1378             delete byUuid[oldUuid];
1379         },
1380
1381         updateName: function(id, newName) {
1382             data[id].name = newName;
1383         }
1384     });
1385 };
1386
1387 qq.status = {
1388     SUBMITTING: "submitting",
1389     SUBMITTED: "submitted",
1390     REJECTED: "rejected",
1391     QUEUED: "queued",
1392     CANCELED: "canceled",
1393     PAUSED: "paused",
1394     UPLOADING: "uploading",
1395     UPLOAD_RETRYING: "retrying upload",
1396     UPLOAD_SUCCESSFUL: "upload successful",
1397     UPLOAD_FAILED: "upload failed",
1398     DELETE_FAILED: "delete failed",
1399     DELETING: "deleting",
1400     DELETED: "deleted"
1401 };
1402
1403 /*globals qq*/
1404 /**
1405  * Defines the public API for FineUploaderBasic mode.
1406  */
1407 (function(){
1408     "use strict";
1409
1410     qq.basePublicApi = {
1411         log: function(str, level) {
1412             if (this._options.debug && (!level || level === "info")) {
1413                 qq.log("[FineUploader " + qq.version + "] " + str);
1414             }
1415             else if (level && level !== "info") {
1416                 qq.log("[FineUploader " + qq.version + "] " + str, level);
1417
1418             }
1419         },
1420
1421         setParams: function(params, id) {
1422             /*jshint eqeqeq: true, eqnull: true*/
1423             if (id == null) {
1424                 this._options.request.params = params;
1425             }
1426             else {
1427                 this._paramsStore.setParams(params, id);
1428             }
1429         },
1430
1431         setDeleteFileParams: function(params, id) {
1432             /*jshint eqeqeq: true, eqnull: true*/
1433             if (id == null) {
1434                 this._options.deleteFile.params = params;
1435             }
1436             else {
1437                 this._deleteFileParamsStore.setParams(params, id);
1438             }
1439         },
1440
1441         // Re-sets the default endpoint, an endpoint for a specific file, or an endpoint for a specific button
1442         setEndpoint: function(endpoint, id) {
1443             /*jshint eqeqeq: true, eqnull: true*/
1444             if (id == null) {
1445                 this._options.request.endpoint = endpoint;
1446             }
1447             else {
1448                 this._endpointStore.setEndpoint(endpoint, id);
1449             }
1450         },
1451
1452         getInProgress: function() {
1453             return this._uploadData.retrieve({
1454                 status: [
1455                     qq.status.UPLOADING,
1456                     qq.status.UPLOAD_RETRYING,
1457                     qq.status.QUEUED
1458                 ]
1459             }).length;
1460         },
1461
1462         getNetUploads: function() {
1463             return this._netUploaded;
1464         },
1465
1466         uploadStoredFiles: function() {
1467             var idToUpload;
1468
1469             if (this._storedIds.length === 0) {
1470                 this._itemError("noFilesError");
1471             }
1472             else {
1473                 while (this._storedIds.length) {
1474                     idToUpload = this._storedIds.shift();
1475                     this._handler.upload(idToUpload);
1476                 }
1477             }
1478         },
1479
1480         clearStoredFiles: function(){
1481             this._storedIds = [];
1482         },
1483
1484         retry: function(id) {
1485             return this._manualRetry(id);
1486         },
1487
1488         cancel: function(id) {
1489             this._handler.cancel(id);
1490         },
1491
1492         cancelAll: function() {
1493             var storedIdsCopy = [],
1494                 self = this;
1495
1496             qq.extend(storedIdsCopy, this._storedIds);
1497             qq.each(storedIdsCopy, function(idx, storedFileId) {
1498                 self.cancel(storedFileId);
1499             });
1500
1501             this._handler.cancelAll();
1502         },
1503
1504         reset: function() {
1505             this.log("Resetting uploader...");
1506
1507             this._handler.reset();
1508             this._storedIds = [];
1509             this._autoRetries = [];
1510             this._retryTimeouts = [];
1511             this._preventRetries = [];
1512             this._thumbnailUrls = [];
1513
1514             qq.each(this._buttons, function(idx, button) {
1515                 button.reset();
1516             });
1517
1518             this._paramsStore.reset();
1519             this._endpointStore.reset();
1520             this._netUploadedOrQueued = 0;
1521             this._netUploaded = 0;
1522             this._uploadData.reset();
1523             this._buttonIdsForFileIds = [];
1524
1525             this._pasteHandler && this._pasteHandler.reset();
1526             this._options.session.refreshOnReset && this._refreshSessionData();
1527         },
1528
1529         addFiles: function(filesOrInputs, params, endpoint) {
1530             var verifiedFilesOrInputs = [],
1531                 fileOrInputIndex, fileOrInput, fileIndex;
1532
1533             if (filesOrInputs) {
1534                 if (!qq.isFileList(filesOrInputs)) {
1535                     filesOrInputs = [].concat(filesOrInputs);
1536                 }
1537
1538                 for (fileOrInputIndex = 0; fileOrInputIndex < filesOrInputs.length; fileOrInputIndex+=1) {
1539                     fileOrInput = filesOrInputs[fileOrInputIndex];
1540
1541                     if (qq.isFileOrInput(fileOrInput)) {
1542                         if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) {
1543                             for (fileIndex = 0; fileIndex < fileOrInput.files.length; fileIndex++) {
1544                                 this._handleNewFile(fileOrInput.files[fileIndex], verifiedFilesOrInputs);
1545                             }
1546                         }
1547                         else {
1548                             this._handleNewFile(fileOrInput, verifiedFilesOrInputs);
1549                         }
1550                     }
1551                     else {
1552                         this.log(fileOrInput + " is not a File or INPUT element!  Ignoring!", "warn");
1553                     }
1554                 }
1555
1556                 this.log("Received " + verifiedFilesOrInputs.length + " files or inputs.");
1557                 this._prepareItemsForUpload(verifiedFilesOrInputs, params, endpoint);
1558             }
1559         },
1560
1561         addBlobs: function(blobDataOrArray, params, endpoint) {
1562             if (blobDataOrArray) {
1563                 var blobDataArray = [].concat(blobDataOrArray),
1564                     verifiedBlobDataList = [],
1565                     self = this;
1566
1567                 qq.each(blobDataArray, function(idx, blobData) {
1568                     var blobOrBlobData;
1569
1570                     if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
1571                         blobOrBlobData = {
1572                             blob: blobData,
1573                             name: self._options.blobs.defaultName
1574                         };
1575                     }
1576                     else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
1577                         blobOrBlobData = blobData;
1578                     }
1579                     else {
1580                         self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
1581                     }
1582
1583                     blobOrBlobData && self._handleNewFile(blobOrBlobData, verifiedBlobDataList);
1584                 });
1585
1586                 this._prepareItemsForUpload(verifiedBlobDataList, params, endpoint);
1587             }
1588             else {
1589                 this.log("undefined or non-array parameter passed into addBlobs", "error");
1590             }
1591         },
1592
1593         getUuid: function(id) {
1594             return this._uploadData.retrieve({id: id}).uuid;
1595         },
1596
1597         setUuid: function(id, newUuid) {
1598             return this._uploadData.uuidChanged(id, newUuid);
1599         },
1600
1601         getResumableFilesData: function() {
1602             return this._handler.getResumableFilesData();
1603         },
1604
1605         getSize: function(id) {
1606             return this._uploadData.retrieve({id: id}).size;
1607         },
1608
1609         getName: function(id) {
1610             return this._uploadData.retrieve({id: id}).name;
1611         },
1612
1613         setName: function(id, newName) {
1614             this._uploadData.updateName(id, newName);
1615         },
1616
1617         getFile: function(fileOrBlobId) {
1618             return this._handler.getFile(fileOrBlobId);
1619         },
1620
1621         deleteFile: function(id) {
1622             this._onSubmitDelete(id);
1623         },
1624
1625         setDeleteFileEndpoint: function(endpoint, id) {
1626             /*jshint eqeqeq: true, eqnull: true*/
1627             if (id == null) {
1628                 this._options.deleteFile.endpoint = endpoint;
1629             }
1630             else {
1631                 this._deleteFileEndpointStore.setEndpoint(endpoint, id);
1632             }
1633         },
1634
1635         doesExist: function(fileOrBlobId) {
1636             return this._handler.isValid(fileOrBlobId);
1637         },
1638
1639         getUploads: function(optionalFilter) {
1640             return this._uploadData.retrieve(optionalFilter);
1641         },
1642
1643         getButton: function(fileId) {
1644             return this._getButton(this._buttonIdsForFileIds[fileId]);
1645         },
1646
1647         // Generate a variable size thumbnail on an img or canvas,
1648         // returning a promise that is fulfilled when the attempt completes.
1649         // Thumbnail can either be based off of a URL for an image returned
1650         // by the server in the upload response, or the associated `Blob`.
1651         drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer) {
1652             if (this._imageGenerator) {
1653                 var fileOrUrl = this._thumbnailUrls[fileId],
1654                     options = {
1655                         scale: maxSize > 0,
1656                         maxSize: maxSize > 0 ? maxSize : null
1657                     };
1658
1659                 // If client-side preview generation is possible
1660                 // and we are not specifically looking for the image URl returned by the server...
1661                 if (!fromServer && qq.supportedFeatures.imagePreviews) {
1662                     fileOrUrl = this.getFile(fileId);
1663                 }
1664
1665                 /* jshint eqeqeq:false,eqnull:true */
1666                 if (fileOrUrl == null) {
1667                     return new qq.Promise().failure(imgOrCanvas, "File or URL not found.");
1668                 }
1669
1670                 return this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options);
1671             }
1672         },
1673
1674         pauseUpload: function(id) {
1675             var uploadData = this._uploadData.retrieve({id: id});
1676
1677             if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) {
1678                 return false;
1679             }
1680
1681             // Pause only really makes sense if the file is uploading or retrying
1682             if (qq.indexOf([qq.status.UPLOADING, qq.status.UPLOAD_RETRYING], uploadData.status) >= 0) {
1683                 if (this._handler.pause(id)) {
1684                     this._uploadData.setStatus(id, qq.status.PAUSED);
1685                     return true;
1686                 }
1687                 else {
1688                     qq.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error");
1689                 }
1690             }
1691             else {
1692                 qq.log(qq.format("Ignoring pause for file ID {} ({}).  Not in progress.", id, this.getName(id)), "error");
1693             }
1694
1695             return false;
1696         },
1697
1698         continueUpload: function(id) {
1699             var uploadData = this._uploadData.retrieve({id: id});
1700
1701             if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) {
1702                 return false;
1703             }
1704
1705             if (uploadData.status === qq.status.PAUSED) {
1706                 qq.log(qq.format("Paused file ID {} ({}) will be continued.  Not paused.", id, this.getName(id)));
1707
1708                 if (!this._handler.upload(id)) {
1709                     this._uploadData.setStatus(id, qq.status.QUEUED);
1710                 }
1711                 return true;
1712             }
1713             else {
1714                 qq.log(qq.format("Ignoring continue for file ID {} ({}).  Not paused.", id, this.getName(id)), "error");
1715             }
1716
1717             return false;
1718         },
1719
1720         getRemainingAllowedItems: function() {
1721             var allowedItems = this._options.validation.itemLimit;
1722
1723             if (allowedItems > 0) {
1724                 return this._options.validation.itemLimit - this._netUploadedOrQueued;
1725             }
1726
1727             return null;
1728         }
1729     };
1730
1731
1732
1733
1734     /**
1735      * Defines the private (internal) API for FineUploaderBasic mode.
1736      */
1737     qq.basePrivateApi = {
1738         // Attempts to refresh session data only if the `qq.Session` module exists
1739         // and a session endpoint has been specified.  The `onSessionRequestComplete`
1740         // callback will be invoked once the refresh is complete.
1741         _refreshSessionData: function() {
1742             var self = this,
1743                 options = this._options.session;
1744
1745             /* jshint eqnull:true */
1746             if (qq.Session && this._options.session.endpoint != null) {
1747                 if (!this._session) {
1748                     qq.extend(options, this._options.cors);
1749
1750                     options.log = qq.bind(this.log, this);
1751                     options.addFileRecord = qq.bind(this._addCannedFile, this);
1752
1753                     this._session = new qq.Session(options);
1754                 }
1755
1756                 setTimeout(function() {
1757                     self._session.refresh().then(function(response, xhrOrXdr) {
1758
1759                         self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr);
1760
1761                     }, function(response, xhrOrXdr) {
1762
1763                         self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr);
1764                     });
1765                 }, 0);
1766             }
1767         },
1768
1769         // Updates internal state with a file record (not backed by a live file).  Returns the assigned ID.
1770         _addCannedFile: function(sessionData) {
1771             var id = this._uploadData.addFile(sessionData.uuid, sessionData.name, sessionData.size,
1772                 qq.status.UPLOAD_SUCCESSFUL);
1773
1774             sessionData.deleteFileEndpoint && this.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id);
1775             sessionData.deleteFileParams && this.setDeleteFileParams(sessionData.deleteFileParams, id);
1776
1777             if (sessionData.thumbnailUrl) {
1778                 this._thumbnailUrls[id] = sessionData.thumbnailUrl;
1779             }
1780
1781             this._netUploaded++;
1782             this._netUploadedOrQueued++;
1783
1784             return id;
1785         },
1786
1787         // Updates internal state when a new file has been received, and adds it along with its ID to a passed array.
1788         _handleNewFile: function(file, newFileWrapperList) {
1789             var size = -1,
1790                 uuid = qq.getUniqueId(),
1791                 name = qq.getFilename(file),
1792                 id;
1793
1794             if (file.size >= 0) {
1795                 size = file.size;
1796             }
1797             else if (file.blob) {
1798                 size = file.blob.size;
1799             }
1800
1801             id = this._uploadData.addFile(uuid, name, size);
1802             this._handler.add(id, file);
1803
1804             this._netUploadedOrQueued++;
1805
1806             newFileWrapperList.push({id: id, file: file});
1807         },
1808
1809         // Creates an internal object that tracks various properties of each extra button,
1810         // and then actually creates the extra button.
1811         _generateExtraButtonSpecs: function() {
1812             var self = this;
1813
1814             this._extraButtonSpecs = {};
1815
1816             qq.each(this._options.extraButtons, function(idx, extraButtonOptionEntry) {
1817                 var multiple = extraButtonOptionEntry.multiple,
1818                     validation = qq.extend({}, self._options.validation, true),
1819                     extraButtonSpec = qq.extend({}, extraButtonOptionEntry);
1820
1821                 if (multiple === undefined) {
1822                     multiple = self._options.multiple;
1823                 }
1824
1825                 if (extraButtonSpec.validation) {
1826                     qq.extend(validation, extraButtonOptionEntry.validation, true);
1827                 }
1828
1829                 qq.extend(extraButtonSpec, {
1830                     multiple: multiple,
1831                     validation: validation
1832                 }, true);
1833
1834                 self._initExtraButton(extraButtonSpec);
1835             });
1836         },
1837
1838         // Creates an extra button element
1839         _initExtraButton: function(spec) {
1840             var button = this._createUploadButton({
1841                 element: spec.element,
1842                 multiple: spec.multiple,
1843                 accept: spec.validation.acceptFiles,
1844                 folders: spec.folders,
1845                 allowedExtensions: spec.validation.allowedExtensions
1846             });
1847
1848             this._extraButtonSpecs[button.getButtonId()] = spec;
1849         },
1850
1851         /**
1852          * Gets the internally used tracking ID for a button.
1853          *
1854          * @param buttonOrFileInputOrFile `File`, `<input type="file">`, or a button container element
1855          * @returns {*} The button's ID, or undefined if no ID is recoverable
1856          * @private
1857          */
1858         _getButtonId: function(buttonOrFileInputOrFile) {
1859             var inputs, fileInput;
1860
1861             // If the item is a `Blob` it will never be associated with a button or drop zone.
1862             if (buttonOrFileInputOrFile && !buttonOrFileInputOrFile.blob && !qq.isBlob(buttonOrFileInputOrFile)) {
1863                 if (qq.isFile(buttonOrFileInputOrFile)) {
1864                     return buttonOrFileInputOrFile.qqButtonId;
1865                 }
1866                 else if (buttonOrFileInputOrFile.tagName.toLowerCase() === "input" &&
1867                     buttonOrFileInputOrFile.type.toLowerCase() === "file") {
1868
1869                     return buttonOrFileInputOrFile.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME);
1870                 }
1871
1872                 inputs = buttonOrFileInputOrFile.getElementsByTagName("input");
1873
1874                 qq.each(inputs, function(idx, input) {
1875                     if (input.getAttribute("type") === "file") {
1876                         fileInput = input;
1877                         return false;
1878                     }
1879                 });
1880
1881                 if (fileInput) {
1882                     return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME);
1883                 }
1884             }
1885         },
1886
1887         _annotateWithButtonId: function(file, associatedInput) {
1888             if (qq.isFile(file)) {
1889                 file.qqButtonId = this._getButtonId(associatedInput);
1890             }
1891         },
1892
1893         _getButton: function(buttonId) {
1894             var extraButtonsSpec = this._extraButtonSpecs[buttonId];
1895
1896             if (extraButtonsSpec) {
1897                 return extraButtonsSpec.element;
1898             }
1899             else if (buttonId === this._defaultButtonId) {
1900                 return this._options.button;
1901             }
1902         },
1903
1904         _handleCheckedCallback: function(details) {
1905             var self = this,
1906                 callbackRetVal = details.callback();
1907
1908             if (callbackRetVal instanceof qq.Promise) {
1909                 this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier);
1910                 return callbackRetVal.then(
1911                     function(successParam) {
1912                         self.log(details.name + " promise success for " + details.identifier);
1913                         details.onSuccess(successParam);
1914                     },
1915                     function() {
1916                         if (details.onFailure) {
1917                             self.log(details.name + " promise failure for " + details.identifier);
1918                             details.onFailure();
1919                         }
1920                         else {
1921                             self.log(details.name + " promise failure for " + details.identifier);
1922                         }
1923                     });
1924             }
1925
1926             if (callbackRetVal !== false) {
1927                 details.onSuccess(callbackRetVal);
1928             }
1929             else {
1930                 if (details.onFailure) {
1931                     this.log(details.name + " - return value was 'false' for " + details.identifier + ".  Invoking failure callback.");
1932                     details.onFailure();
1933                 }
1934                 else {
1935                     this.log(details.name + " - return value was 'false' for " + details.identifier + ".  Will not proceed.");
1936                 }
1937             }
1938
1939             return callbackRetVal;
1940         },
1941
1942         /**
1943          * Generate a tracked upload button.
1944          *
1945          * @param spec Object containing a required `element` property
1946          * along with optional `multiple`, `accept`, and `folders`.
1947          * @returns {qq.UploadButton}
1948          * @private
1949          */
1950         _createUploadButton: function(spec) {
1951             var self = this,
1952                 acceptFiles = spec.accept || this._options.validation.acceptFiles,
1953                 allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions;
1954
1955             function allowMultiple() {
1956                 if (qq.supportedFeatures.ajaxUploading) {
1957                     // Workaround for bug in iOS7 (see #1039)
1958                     if (qq.ios7() && self._isAllowedExtension(allowedExtensions, ".mov")) {
1959                         return false;
1960                     }
1961
1962                     if (spec.multiple === undefined) {
1963                         return self._options.multiple;
1964                     }
1965
1966                     return spec.multiple;
1967                 }
1968
1969                 return false;
1970             }
1971
1972             var button = new qq.UploadButton({
1973                 element: spec.element,
1974                 folders: spec.folders,
1975                 name: this._options.request.inputName,
1976                 multiple: allowMultiple(),
1977                 acceptFiles: acceptFiles,
1978                 onChange: function(input) {
1979                     self._onInputChange(input);
1980                 },
1981                 hoverClass: this._options.classes.buttonHover,
1982                 focusClass: this._options.classes.buttonFocus
1983             });
1984
1985             this._disposeSupport.addDisposer(function() {
1986                 button.dispose();
1987             });
1988
1989             self._buttons.push(button);
1990
1991             return button;
1992         },
1993
1994         _createUploadHandler: function(additionalOptions, namespace) {
1995             var self = this,
1996                 options = {
1997                     debug: this._options.debug,
1998                     maxConnections: this._options.maxConnections,
1999                     cors: this._options.cors,
2000                     demoMode: this._options.demoMode,
2001                     paramsStore: this._paramsStore,
2002                     endpointStore: this._endpointStore,
2003                     chunking: this._options.chunking,
2004                     resume: this._options.resume,
2005                     blobs: this._options.blobs,
2006                     log: qq.bind(self.log, self),
2007                     onProgress: function(id, name, loaded, total){
2008                         self._onProgress(id, name, loaded, total);
2009                         self._options.callbacks.onProgress(id, name, loaded, total);
2010                     },
2011                     onComplete: function(id, name, result, xhr){
2012                         var retVal = self._onComplete(id, name, result, xhr);
2013
2014                         // If the internal `_onComplete` handler returns a promise, don't invoke the `onComplete` callback
2015                         // until the promise has been fulfilled.
2016                         if (retVal instanceof  qq.Promise) {
2017                             retVal.done(function() {
2018                                 self._options.callbacks.onComplete(id, name, result, xhr);
2019                             });
2020                         }
2021                         else {
2022                             self._options.callbacks.onComplete(id, name, result, xhr);
2023                         }
2024                     },
2025                     onCancel: function(id, name) {
2026                         return self._handleCheckedCallback({
2027                             name: "onCancel",
2028                             callback: qq.bind(self._options.callbacks.onCancel, self, id, name),
2029                             onSuccess: qq.bind(self._onCancel, self, id, name),
2030                             identifier: id
2031                         });
2032                     },
2033                     onUpload: function(id, name) {
2034                         self._onUpload(id, name);
2035                         self._options.callbacks.onUpload(id, name);
2036                     },
2037                     onUploadChunk: function(id, name, chunkData) {
2038                         self._onUploadChunk(id, chunkData);
2039                         self._options.callbacks.onUploadChunk(id, name, chunkData);
2040                     },
2041                     onUploadChunkSuccess: function(id, chunkData, result, xhr) {
2042                         self._options.callbacks.onUploadChunkSuccess.apply(self, arguments);
2043                     },
2044                     onResume: function(id, name, chunkData) {
2045                         return self._options.callbacks.onResume(id, name, chunkData);
2046                     },
2047                     onAutoRetry: function(id, name, responseJSON, xhr) {
2048                         return self._onAutoRetry.apply(self, arguments);
2049                     },
2050                     onUuidChanged: function(id, newUuid) {
2051                         self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'");
2052                         self.setUuid(id, newUuid);
2053                     },
2054                     getName: qq.bind(self.getName, self),
2055                     getUuid: qq.bind(self.getUuid, self),
2056                     getSize: qq.bind(self.getSize, self)
2057                 };
2058
2059             qq.each(this._options.request, function(prop, val) {
2060                 options[prop] = val;
2061             });
2062
2063             if (additionalOptions) {
2064                 qq.each(additionalOptions, function(key, val) {
2065                     options[key] = val;
2066                 });
2067             }
2068
2069             return new qq.UploadHandler(options, namespace);
2070         },
2071
2072         _createDeleteHandler: function() {
2073             var self = this;
2074
2075             return new qq.DeleteFileAjaxRequester({
2076                 method: this._options.deleteFile.method.toUpperCase(),
2077                 maxConnections: this._options.maxConnections,
2078                 uuidParamName: this._options.request.uuidName,
2079                 customHeaders: this._options.deleteFile.customHeaders,
2080                 paramsStore: this._deleteFileParamsStore,
2081                 endpointStore: this._deleteFileEndpointStore,
2082                 demoMode: this._options.demoMode,
2083                 cors: this._options.cors,
2084                 log: qq.bind(self.log, self),
2085                 onDelete: function(id) {
2086                     self._onDelete(id);
2087                     self._options.callbacks.onDelete(id);
2088                 },
2089                 onDeleteComplete: function(id, xhrOrXdr, isError) {
2090                     self._onDeleteComplete(id, xhrOrXdr, isError);
2091                     self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError);
2092                 }
2093
2094             });
2095         },
2096
2097         _createPasteHandler: function() {
2098             var self = this;
2099
2100             return new qq.PasteSupport({
2101                 targetElement: this._options.paste.targetElement,
2102                 callbacks: {
2103                     log: qq.bind(self.log, self),
2104                     pasteReceived: function(blob) {
2105                         self._handleCheckedCallback({
2106                             name: "onPasteReceived",
2107                             callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob),
2108                             onSuccess: qq.bind(self._handlePasteSuccess, self, blob),
2109                             identifier: "pasted image"
2110                         });
2111                     }
2112                 }
2113             });
2114         },
2115
2116         _createUploadDataTracker: function() {
2117             var self = this;
2118
2119             return new qq.UploadData({
2120                 getName: function(id) {
2121                     return self.getName(id);
2122                 },
2123                 getUuid: function(id) {
2124                     return self.getUuid(id);
2125                 },
2126                 getSize: function(id) {
2127                     return self.getSize(id);
2128                 },
2129                 onStatusChange: function(id, oldStatus, newStatus) {
2130                     self._onUploadStatusChange(id, oldStatus, newStatus);
2131                     self._options.callbacks.onStatusChange(id, oldStatus, newStatus);
2132                 }
2133             });
2134         },
2135
2136         _onUploadStatusChange: function(id, oldStatus, newStatus) {
2137             // Make sure a "queued" retry attempt is canceled if the upload has been paused
2138             if (newStatus === qq.status.PAUSED) {
2139                 clearTimeout(this._retryTimeouts[id]);
2140             }
2141         },
2142
2143         _handlePasteSuccess: function(blob, extSuppliedName) {
2144             var extension = blob.type.split("/")[1],
2145                 name = extSuppliedName;
2146
2147             /*jshint eqeqeq: true, eqnull: true*/
2148             if (name == null) {
2149                 name = this._options.paste.defaultName;
2150             }
2151
2152             name += "." + extension;
2153
2154             this.addBlobs({
2155                 name: name,
2156                 blob: blob
2157             });
2158         },
2159
2160         _preventLeaveInProgress: function(){
2161             var self = this;
2162
2163             this._disposeSupport.attach(window, "beforeunload", function(e){
2164                 if (self.getInProgress()) {
2165                     e = e || window.event;
2166                     // for ie, ff
2167                     e.returnValue = self._options.messages.onLeave;
2168                     // for webkit
2169                     return self._options.messages.onLeave;
2170                 }
2171             });
2172         },
2173
2174         _onSubmit: function(id, name) {
2175             //nothing to do yet in core uploader
2176         },
2177
2178         _onProgress: function(id, name, loaded, total) {
2179             //nothing to do yet in core uploader
2180         },
2181
2182         _onComplete: function(id, name, result, xhr) {
2183             if (!result.success) {
2184                 this._netUploadedOrQueued--;
2185                 this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED);
2186             }
2187             else {
2188                 if (result.thumbnailUrl) {
2189                     this._thumbnailUrls[id] = result.thumbnailUrl;
2190                 }
2191
2192                 this._netUploaded++;
2193                 this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL);
2194             }
2195
2196             this._maybeParseAndSendUploadError(id, name, result, xhr);
2197
2198             return result.success ? true : false;
2199         },
2200
2201         _onCancel: function(id, name) {
2202             this._netUploadedOrQueued--;
2203
2204             clearTimeout(this._retryTimeouts[id]);
2205
2206             var storedItemIndex = qq.indexOf(this._storedIds, id);
2207             if (!this._options.autoUpload && storedItemIndex >= 0) {
2208                 this._storedIds.splice(storedItemIndex, 1);
2209             }
2210
2211             this._uploadData.setStatus(id, qq.status.CANCELED);
2212         },
2213
2214         _isDeletePossible: function() {
2215             if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) {
2216                 return false;
2217             }
2218
2219             if (this._options.cors.expected) {
2220                 if (qq.supportedFeatures.deleteFileCorsXhr) {
2221                     return true;
2222                 }
2223
2224                 if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) {
2225                     return true;
2226                 }
2227
2228                 return false;
2229             }
2230
2231             return true;
2232         },
2233
2234         _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) {
2235             var uuid = this.getUuid(id),
2236                 adjustedOnSuccessCallback;
2237
2238             if (onSuccessCallback) {
2239                 adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams);
2240             }
2241
2242             if (this._isDeletePossible()) {
2243                 return this._handleCheckedCallback({
2244                     name: "onSubmitDelete",
2245                     callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id),
2246                     onSuccess: adjustedOnSuccessCallback ||
2247                         qq.bind(this._deleteHandler.sendDelete, this, id, uuid, additionalMandatedParams),
2248                     identifier: id
2249                 });
2250             }
2251             else {
2252                 this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
2253                     "due to CORS on a user agent that does not support pre-flighting.", "warn");
2254                 return false;
2255             }
2256         },
2257
2258         _onDelete: function(id) {
2259             this._uploadData.setStatus(id, qq.status.DELETING);
2260         },
2261
2262         _onDeleteComplete: function(id, xhrOrXdr, isError) {
2263             var name = this.getName(id);
2264
2265             if (isError) {
2266                 this._uploadData.setStatus(id, qq.status.DELETE_FAILED);
2267                 this.log("Delete request for '" + name + "' has failed.", "error");
2268
2269                 // For error reporing, we only have accesss to the response status if this is not
2270                 // an `XDomainRequest`.
2271                 if (xhrOrXdr.withCredentials === undefined) {
2272                     this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr);
2273                 }
2274                 else {
2275                     this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr);
2276                 }
2277             }
2278             else {
2279                 this._netUploadedOrQueued--;
2280                 this._netUploaded--;
2281                 this._handler.expunge(id);
2282                 this._uploadData.setStatus(id, qq.status.DELETED);
2283                 this.log("Delete request for '" + name + "' has succeeded.");
2284             }
2285         },
2286
2287         _onUpload: function(id, name) {
2288             this._uploadData.setStatus(id, qq.status.UPLOADING);
2289         },
2290
2291         _onUploadChunk: function(id, chunkData) {
2292             //nothing to do in the base uploader
2293         },
2294
2295         _onInputChange: function(input) {
2296             var fileIndex;
2297
2298             if (qq.supportedFeatures.ajaxUploading) {
2299                 for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) {
2300                     this._annotateWithButtonId(input.files[fileIndex], input);
2301                 }
2302
2303                 this.addFiles(input.files);
2304             }
2305             // Android 2.3.x will fire `onchange` even if no file has been selected
2306             else if (input.value.length > 0) {
2307                 this.addFiles(input);
2308             }
2309
2310             qq.each(this._buttons, function(idx, button) {
2311                 button.reset();
2312             });
2313         },
2314
2315         _onBeforeAutoRetry: function(id, name) {
2316             this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
2317         },
2318
2319         /**
2320          * Attempt to automatically retry a failed upload.
2321          *
2322          * @param id The file ID of the failed upload
2323          * @param name The name of the file associated with the failed upload
2324          * @param responseJSON Response from the server, parsed into a javascript object
2325          * @param xhr Ajax transport used to send the failed request
2326          * @param callback Optional callback to be invoked if a retry is prudent.
2327          * Invoked in lieu of asking the upload handler to retry.
2328          * @returns {boolean} true if an auto-retry will occur
2329          * @private
2330          */
2331         _onAutoRetry: function(id, name, responseJSON, xhr, callback) {
2332             var self = this;
2333
2334             self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
2335
2336             if (self._shouldAutoRetry(id, name, responseJSON)) {
2337                 self._maybeParseAndSendUploadError.apply(self, arguments);
2338                 self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
2339                 self._onBeforeAutoRetry(id, name);
2340
2341                 self._retryTimeouts[id] = setTimeout(function() {
2342                     self.log("Retrying " + name + "...");
2343                     self._autoRetries[id]++;
2344                     self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
2345
2346                     if (callback) {
2347                         callback(id);
2348                     }
2349                     else {
2350                         self._handler.retry(id);
2351                     }
2352                 }, self._options.retry.autoAttemptDelay * 1000);
2353
2354                 return true;
2355             }
2356         },
2357
2358         _shouldAutoRetry: function(id, name, responseJSON) {
2359             var uploadData = this._uploadData.retrieve({id: id});
2360
2361             /*jshint laxbreak: true */
2362             if (!this._preventRetries[id]
2363                 && this._options.retry.enableAuto
2364                 && uploadData.status !== qq.status.PAUSED) {
2365
2366                 if (this._autoRetries[id] === undefined) {
2367                     this._autoRetries[id] = 0;
2368                 }
2369
2370                 return this._autoRetries[id] < this._options.retry.maxAutoAttempts;
2371             }
2372
2373             return false;
2374         },
2375
2376         //return false if we should not attempt the requested retry
2377         _onBeforeManualRetry: function(id) {
2378             var itemLimit = this._options.validation.itemLimit;
2379
2380             if (this._preventRetries[id]) {
2381                 this.log("Retries are forbidden for id " + id, "warn");
2382                 return false;
2383             }
2384             else if (this._handler.isValid(id)) {
2385                 var fileName = this.getName(id);
2386
2387                 if (this._options.callbacks.onManualRetry(id, fileName) === false) {
2388                     return false;
2389                 }
2390
2391                 if (itemLimit > 0 && this._netUploadedOrQueued+1 > itemLimit) {
2392                     this._itemError("retryFailTooManyItems");
2393                     return false;
2394                 }
2395
2396                 this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
2397                 return true;
2398             }
2399             else {
2400                 this.log("'" + id + "' is not a valid file ID", "error");
2401                 return false;
2402             }
2403         },
2404
2405         /**
2406          * Conditionally orders a manual retry of a failed upload.
2407          *
2408          * @param id File ID of the failed upload
2409          * @param callback Optional callback to invoke if a retry is prudent.
2410          * In lieu of asking the upload handler to retry.
2411          * @returns {boolean} true if a manual retry will occur
2412          * @private
2413          */
2414         _manualRetry: function(id, callback) {
2415             if (this._onBeforeManualRetry(id)) {
2416                 this._netUploadedOrQueued++;
2417                 this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
2418
2419                 if (callback) {
2420                     callback(id);
2421                 }
2422                 else {
2423                     this._handler.retry(id);
2424                 }
2425
2426                 return true;
2427             }
2428         },
2429
2430         _maybeParseAndSendUploadError: function(id, name, response, xhr) {
2431             // Assuming no one will actually set the response code to something other than 200
2432             // and still set 'success' to true...
2433             if (!response.success){
2434                 if (xhr && xhr.status !== 200 && !response.error) {
2435                     this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr);
2436                 }
2437                 else {
2438                     var errorReason = response.error ? response.error : this._options.text.defaultResponseError;
2439                     this._options.callbacks.onError(id, name, errorReason, xhr);
2440                 }
2441             }
2442         },
2443
2444         _prepareItemsForUpload: function(items, params, endpoint) {
2445             var validationDescriptors = this._getValidationDescriptors(items),
2446                 buttonId = this._getButtonId(items[0].file),
2447                 button = this._getButton(buttonId);
2448
2449             this._handleCheckedCallback({
2450                 name: "onValidateBatch",
2451                 callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors, button),
2452                 onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint, button),
2453                 onFailure: qq.bind(this._onValidateBatchCallbackFailure, this, items),
2454                 identifier: "batch validation"
2455             });
2456         },
2457
2458         _upload: function(id, params, endpoint) {
2459             var name = this.getName(id);
2460
2461             if (params) {
2462                 this.setParams(params, id);
2463             }
2464
2465             if (endpoint) {
2466                 this.setEndpoint(endpoint, id);
2467             }
2468
2469             this._handleCheckedCallback({
2470                 name: "onSubmit",
2471                 callback: qq.bind(this._options.callbacks.onSubmit, this, id, name),
2472                 onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name),
2473                 onFailure: qq.bind(this._fileOrBlobRejected, this, id, name),
2474                 identifier: id
2475             });
2476         },
2477
2478         _onSubmitCallbackSuccess: function(id, name) {
2479             var buttonId;
2480
2481             if (qq.supportedFeatures.ajaxUploading) {
2482                 buttonId = this._handler.getFile(id).qqButtonId;
2483             }
2484             else {
2485                 buttonId = this._getButtonId(this._handler.getInput(id));
2486             }
2487
2488             if (buttonId) {
2489                 this._buttonIdsForFileIds[id] = buttonId;
2490             }
2491
2492             this._onSubmit.apply(this, arguments);
2493             this._uploadData.setStatus(id, qq.status.SUBMITTED);
2494             this._onSubmitted.apply(this, arguments);
2495             this._options.callbacks.onSubmitted.apply(this, arguments);
2496
2497             if (this._options.autoUpload) {
2498                 if (!this._handler.upload(id)) {
2499                     this._uploadData.setStatus(id, qq.status.QUEUED);
2500                 }
2501             }
2502             else {
2503                 this._storeForLater(id);
2504             }
2505         },
2506
2507         _onSubmitted: function(id) {
2508             //nothing to do in the base uploader
2509         },
2510
2511         _storeForLater: function(id) {
2512             this._storedIds.push(id);
2513         },
2514
2515         _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) {
2516             var errorMessage,
2517                 itemLimit = this._options.validation.itemLimit,
2518                 proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued;
2519
2520             if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
2521                 if (items.length > 0) {
2522                     this._handleCheckedCallback({
2523                         name: "onValidate",
2524                         callback: qq.bind(this._options.callbacks.onValidate, this, validationDescriptors[0], button),
2525                         onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint),
2526                         onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint),
2527                         identifier: "Item '" + items[0].file.name + "', size: " + items[0].file.size
2528                     });
2529                 }
2530                 else {
2531                     this._itemError("noFilesError");
2532                 }
2533             }
2534             else {
2535                 this._onValidateBatchCallbackFailure(items);
2536                 errorMessage = this._options.messages.tooManyItemsError
2537                     .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued)
2538                     .replace(/\{itemLimit\}/g, itemLimit);
2539                 this._batchError(errorMessage);
2540             }
2541         },
2542
2543         _onValidateBatchCallbackFailure: function(fileWrappers) {
2544             var self = this;
2545
2546             qq.each(fileWrappers, function(idx, fileWrapper) {
2547                 self._fileOrBlobRejected(fileWrapper.id);
2548             });
2549         },
2550
2551         _onValidateCallbackSuccess: function(items, index, params, endpoint) {
2552             var self = this,
2553                 nextIndex = index+1,
2554                 validationDescriptor = this._getValidationDescriptor(items[index].file);
2555
2556             this._validateFileOrBlobData(items[index], validationDescriptor)
2557                 .then(
2558                     function() {
2559                         self._upload(items[index].id, params, endpoint);
2560                         self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint);
2561                     },
2562                     function() {
2563                         self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
2564                     }
2565                 );
2566         },
2567
2568         _onValidateCallbackFailure: function(items, index, params, endpoint) {
2569             var nextIndex = index+ 1;
2570
2571             this._fileOrBlobRejected(items[0].id, items[0].file.name);
2572
2573             this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
2574         },
2575
2576         _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) {
2577             var self = this;
2578
2579             if (items.length > index) {
2580                 if (validItem || !this._options.validation.stopOnFirstInvalidFile) {
2581                     //use setTimeout to prevent a stack overflow with a large number of files in the batch & non-promissory callbacks
2582                     setTimeout(function() {
2583                         var validationDescriptor = self._getValidationDescriptor(items[index].file);
2584
2585                         self._handleCheckedCallback({
2586                             name: "onValidate",
2587                             callback: qq.bind(self._options.callbacks.onValidate, self, items[index].file),
2588                             onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint),
2589                             onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint),
2590                             identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size
2591                         });
2592                     }, 0);
2593                 }
2594                 else if (!validItem) {
2595                     for (; index < items.length; index++) {
2596                         self._fileOrBlobRejected(items[index].id);
2597                     }
2598                 }
2599             }
2600         },
2601
2602         /**
2603          * Performs some internal validation checks on an item, defined in the `validation` option.
2604          *
2605          * @param fileWrapper Wrapper containing a `file` along with an `id`
2606          * @param validationDescriptor Normalized information about the item (`size`, `name`).
2607          * @returns qq.Promise with appropriate callbacks invoked depending on the validity of the file
2608          * @private
2609          */
2610         _validateFileOrBlobData: function(fileWrapper, validationDescriptor) {
2611             var self = this,
2612                 file = fileWrapper.file,
2613                 name = validationDescriptor.name,
2614                 size = validationDescriptor.size,
2615                 buttonId = this._getButtonId(file),
2616                 validationBase = this._getValidationBase(buttonId),
2617                 validityChecker = new qq.Promise();
2618
2619             validityChecker.then(
2620                 function() {},
2621                 function() {
2622                     self._fileOrBlobRejected(fileWrapper.id, name);
2623                 });
2624
2625             if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) {
2626                 this._itemError("typeError", name, file);
2627                 return validityChecker.failure();
2628             }
2629
2630             if (size === 0) {
2631                 this._itemError("emptyError", name, file);
2632                 return validityChecker.failure();
2633             }
2634
2635             if (size && validationBase.sizeLimit && size > validationBase.sizeLimit) {
2636                 this._itemError("sizeError", name, file);
2637                 return validityChecker.failure();
2638             }
2639
2640             if (size && size < validationBase.minSizeLimit) {
2641                 this._itemError("minSizeError", name, file);
2642                 return validityChecker.failure();
2643             }
2644
2645             if (qq.ImageValidation && qq.supportedFeatures.imagePreviews && qq.isFile(file)) {
2646                 new qq.ImageValidation(file, qq.bind(self.log, self)).validate(validationBase.image).then(
2647                     validityChecker.success,
2648                     function(errorCode) {
2649                         self._itemError(errorCode + "ImageError", name, file);
2650                         validityChecker.failure();
2651                     }
2652                 );
2653             }
2654             else {
2655                 validityChecker.success();
2656             }
2657
2658             return validityChecker;
2659         },
2660
2661         _fileOrBlobRejected: function(id) {
2662             this._netUploadedOrQueued--;
2663             this._uploadData.setStatus(id, qq.status.REJECTED);
2664         },
2665
2666         /**
2667          * Constructs and returns a message that describes an item/file error.  Also calls `onError` callback.
2668          *
2669          * @param code REQUIRED - a code that corresponds to a stock message describing this type of error
2670          * @param maybeNameOrNames names of the items that have failed, if applicable
2671          * @param item `File`, `Blob`, or `<input type="file">`
2672          * @private
2673          */
2674         _itemError: function(code, maybeNameOrNames, item) {
2675             var message = this._options.messages[code],
2676                 allowedExtensions = [],
2677                 names = [].concat(maybeNameOrNames),
2678                 name = names[0],
2679                 buttonId = this._getButtonId(item),
2680                 validationBase = this._getValidationBase(buttonId),
2681                 extensionsForMessage, placeholderMatch;
2682
2683             function r(name, replacement){ message = message.replace(name, replacement); }
2684
2685             qq.each(validationBase.allowedExtensions, function(idx, allowedExtension) {
2686                     /**
2687                      * If an argument is not a string, ignore it.  Added when a possible issue with MooTools hijacking the
2688                      * `allowedExtensions` array was discovered.  See case #735 in the issue tracker for more details.
2689                      */
2690                 if (qq.isString(allowedExtension)) {
2691                     allowedExtensions.push(allowedExtension);
2692                 }
2693             });
2694
2695             extensionsForMessage = allowedExtensions.join(", ").toLowerCase();
2696
2697             r("{file}", this._options.formatFileName(name));
2698             r("{extensions}", extensionsForMessage);
2699             r("{sizeLimit}", this._formatSize(validationBase.sizeLimit));
2700             r("{minSizeLimit}", this._formatSize(validationBase.minSizeLimit));
2701
2702             placeholderMatch = message.match(/(\{\w+\})/g);
2703             if (placeholderMatch !== null) {
2704                 qq.each(placeholderMatch, function(idx, placeholder) {
2705                     r(placeholder, names[idx]);
2706                 });
2707             }
2708
2709             this._options.callbacks.onError(null, name, message, undefined);
2710
2711             return message;
2712         },
2713
2714         _batchError: function(message) {
2715             this._options.callbacks.onError(null, null, message, undefined);
2716         },
2717
2718         _isAllowedExtension: function(allowed, fileName) {
2719             var valid = false;
2720
2721             if (!allowed.length) {
2722                 return true;
2723             }
2724
2725             qq.each(allowed, function(idx, allowedExt) {
2726                 /**
2727                  * If an argument is not a string, ignore it.  Added when a possible issue with MooTools hijacking the
2728                  * `allowedExtensions` array was discovered.  See case #735 in the issue tracker for more details.
2729                  */
2730                 if (qq.isString(allowedExt)) {
2731                     /*jshint eqeqeq: true, eqnull: true*/
2732                     var extRegex = new RegExp("\\." + allowedExt + "$", "i");
2733
2734                     if (fileName.match(extRegex) != null) {
2735                         valid = true;
2736                         return false;
2737                     }
2738                 }
2739             });
2740
2741             return valid;
2742         },
2743
2744         _formatSize: function(bytes){
2745             var i = -1;
2746             do {
2747                 bytes = bytes / 1000;
2748                 i++;
2749             } while (bytes > 999);
2750
2751             return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
2752         },
2753
2754         _wrapCallbacks: function() {
2755             var self, safeCallback;
2756
2757             self = this;
2758
2759             safeCallback = function(name, callback, args) {
2760                 var errorMsg;
2761
2762                 try {
2763                     return callback.apply(self, args);
2764                 }
2765                 catch (exception) {
2766                     errorMsg = exception.message || exception.toString();
2767                     self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error");
2768                 }
2769             };
2770
2771             /* jshint forin: false, loopfunc: true */
2772             for (var prop in this._options.callbacks) {
2773                 (function() {
2774                     var callbackName, callbackFunc;
2775                     callbackName = prop;
2776                     callbackFunc = self._options.callbacks[callbackName];
2777                     self._options.callbacks[callbackName] = function() {
2778                         return safeCallback(callbackName, callbackFunc, arguments);
2779                     };
2780                 }());
2781             }
2782         },
2783
2784         _parseFileOrBlobDataName: function(fileOrBlobData) {
2785             var name;
2786
2787             if (qq.isFileOrInput(fileOrBlobData)) {
2788                 if (fileOrBlobData.value) {
2789                     // it is a file input
2790                     // get input value and remove path to normalize
2791                     name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
2792                 } else {
2793                     // fix missing properties in Safari 4 and firefox 11.0a2
2794                     name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
2795                 }
2796             }
2797             else {
2798                 name = fileOrBlobData.name;
2799             }
2800
2801             return name;
2802         },
2803
2804         _parseFileOrBlobDataSize: function(fileOrBlobData) {
2805             var size;
2806
2807             if (qq.isFileOrInput(fileOrBlobData)) {
2808                 if (fileOrBlobData.value === undefined) {
2809                     // fix missing properties in Safari 4 and firefox 11.0a2
2810                     size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
2811                 }
2812             }
2813             else {
2814                 size = fileOrBlobData.blob.size;
2815             }
2816
2817             return size;
2818         },
2819
2820         _getValidationDescriptor: function(fileOrBlobData) {
2821             var fileDescriptor = {},
2822                 name = this._parseFileOrBlobDataName(fileOrBlobData),
2823                 size = this._parseFileOrBlobDataSize(fileOrBlobData);
2824
2825             fileDescriptor.name = name;
2826             if (size !== undefined) {
2827                 fileDescriptor.size = size;
2828             }
2829
2830             return fileDescriptor;
2831         },
2832
2833         _getValidationDescriptors: function(fileWrappers) {
2834             var self = this,
2835                 fileDescriptors = [];
2836
2837             qq.each(fileWrappers, function(idx, fileWrapper) {
2838                 fileDescriptors.push(self._getValidationDescriptor(fileWrapper.file));
2839             });
2840
2841             return fileDescriptors;
2842         },
2843
2844         _createParamsStore: function(type) {
2845             var paramsStore = {},
2846                 self = this;
2847
2848             return {
2849                 setParams: function(params, id) {
2850                     var paramsCopy = {};
2851                     qq.extend(paramsCopy, params);
2852                     paramsStore[id] = paramsCopy;
2853                 },
2854
2855                 getParams: function(id) {
2856                     /*jshint eqeqeq: true, eqnull: true*/
2857                     var paramsCopy = {};
2858
2859                     if (id != null && paramsStore[id]) {
2860                         qq.extend(paramsCopy, paramsStore[id]);
2861                     }
2862                     else {
2863                         qq.extend(paramsCopy, self._options[type].params);
2864                     }
2865
2866                     return paramsCopy;
2867                 },
2868
2869                 remove: function(fileId) {
2870                     return delete paramsStore[fileId];
2871                 },
2872
2873                 reset: function() {
2874                     paramsStore = {};
2875                 }
2876             };
2877         },
2878
2879         _createEndpointStore: function(type) {
2880             var endpointStore = {},
2881             self = this;
2882
2883             return {
2884                 setEndpoint: function(endpoint, id) {
2885                     endpointStore[id] = endpoint;
2886                 },
2887
2888                 getEndpoint: function(id) {
2889                     /*jshint eqeqeq: true, eqnull: true*/
2890                     if (id != null && endpointStore[id]) {
2891                         return endpointStore[id];
2892                     }
2893
2894                     return self._options[type].endpoint;
2895                 },
2896
2897                 remove: function(fileId) {
2898                     return delete endpointStore[fileId];
2899                 },
2900
2901                 reset: function() {
2902                     endpointStore = {};
2903                 }
2904             };
2905         },
2906
2907         // Allows camera access on either the default or an extra button for iOS devices.
2908         _handleCameraAccess: function() {
2909             if (this._options.camera.ios && qq.ios()) {
2910                 var acceptIosCamera = "image/*;capture=camera",
2911                     button = this._options.camera.button,
2912                     buttonId = button ? this._getButtonId(button) : this._defaultButtonId,
2913                     optionRoot = this._options;
2914
2915                 // If we are not targeting the default button, it is an "extra" button
2916                 if (buttonId && buttonId !== this._defaultButtonId) {
2917                     optionRoot = this._extraButtonSpecs[buttonId];
2918                 }
2919
2920                 // Camera access won't work in iOS if the `multiple` attribute is present on the file input
2921                 optionRoot.multiple = false;
2922
2923                 // update the options
2924                 if (optionRoot.validation.acceptFiles === null) {
2925                     optionRoot.validation.acceptFiles = acceptIosCamera;
2926                 }
2927                 else {
2928                     optionRoot.validation.acceptFiles += "," + acceptIosCamera;
2929                 }
2930
2931                 // update the already-created button
2932                 qq.each(this._buttons, function(idx, button) {
2933                     if (button.getButtonId() === buttonId) {
2934                         button.setMultiple(optionRoot.multiple);
2935                         button.setAcceptFiles(optionRoot.acceptFiles);
2936
2937                         return false;
2938                     }
2939                 });
2940             }
2941         },
2942
2943         // Get the validation options for this button.  Could be the default validation option
2944         // or a specific one assigned to this particular button.
2945         _getValidationBase: function(buttonId) {
2946             var extraButtonSpec = this._extraButtonSpecs[buttonId];
2947
2948             return extraButtonSpec ? extraButtonSpec.validation : this._options.validation;
2949
2950         }
2951     };
2952 }());
2953
2954 /*globals qq*/
2955 (function(){
2956     "use strict";
2957
2958     qq.FineUploaderBasic = function(o) {
2959         // These options define FineUploaderBasic mode.
2960         this._options = {
2961             debug: false,
2962             button: null,
2963             multiple: true,
2964             maxConnections: 3,
2965             disableCancelForFormUploads: false,
2966             autoUpload: true,
2967
2968             request: {
2969                 endpoint: "/server/upload",
2970                 params: {},
2971                 paramsInBody: true,
2972                 customHeaders: {},
2973                 forceMultipart: true,
2974                 inputName: "qqfile",
2975                 uuidName: "qquuid",
2976                 totalFileSizeName: "qqtotalfilesize",
2977                 filenameParam: "qqfilename"
2978             },
2979
2980             validation: {
2981                 allowedExtensions: [],
2982                 sizeLimit: 0,
2983                 minSizeLimit: 0,
2984                 itemLimit: 0,
2985                 stopOnFirstInvalidFile: true,
2986                 acceptFiles: null,
2987                 image: {
2988                     maxHeight: 0,
2989                     maxWidth: 0,
2990                     minHeight: 0,
2991                     minWidth: 0
2992                 }
2993             },
2994
2995             callbacks: {
2996                 onSubmit: function(id, name){},
2997                 onSubmitted: function(id, name){},
2998                 onComplete: function(id, name, responseJSON, maybeXhr){},
2999                 onCancel: function(id, name){},
3000                 onUpload: function(id, name){},
3001                 onUploadChunk: function(id, name, chunkData){},
3002                 onUploadChunkSuccess: function(id, chunkData, responseJSON, xhr){},
3003                 onResume: function(id, fileName, chunkData){},
3004                 onProgress: function(id, name, loaded, total){},
3005                 onError: function(id, name, reason, maybeXhrOrXdr) {},
3006                 onAutoRetry: function(id, name, attemptNumber) {},
3007                 onManualRetry: function(id, name) {},
3008                 onValidateBatch: function(fileOrBlobData) {},
3009                 onValidate: function(fileOrBlobData) {},
3010                 onSubmitDelete: function(id) {},
3011                 onDelete: function(id){},
3012                 onDeleteComplete: function(id, xhrOrXdr, isError){},
3013                 onPasteReceived: function(blob) {},
3014                 onStatusChange: function(id, oldStatus, newStatus) {},
3015                 onSessionRequestComplete: function(response, success, xhrOrXdr) {}
3016             },
3017
3018             messages: {
3019                 typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
3020                 sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
3021                 minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
3022                 emptyError: "{file} is empty, please select files again without it.",
3023                 noFilesError: "No files to upload.",
3024                 tooManyItemsError: "Too many items ({netItems}) would be uploaded.  Item limit is {itemLimit}.",
3025                 maxHeightImageError: "Image is too tall.",
3026                 maxWidthImageError: "Image is too wide.",
3027                 minHeightImageError: "Image is not tall enough.",
3028                 minWidthImageError: "Image is not wide enough.",
3029                 retryFailTooManyItems: "Retry failed - you have reached your file limit.",
3030                 onLeave: "The files are being uploaded, if you leave now the upload will be canceled."
3031             },
3032
3033             retry: {
3034                 enableAuto: false,
3035                 maxAutoAttempts: 3,
3036                 autoAttemptDelay: 5,
3037                 preventRetryResponseProperty: "preventRetry"
3038             },
3039
3040             classes: {
3041                 buttonHover: "qq-upload-button-hover",
3042                 buttonFocus: "qq-upload-button-focus"
3043             },
3044
3045             chunking: {
3046                 enabled: false,
3047                 partSize: 2000000,
3048                 paramNames: {
3049                     partIndex: "qqpartindex",
3050                     partByteOffset: "qqpartbyteoffset",
3051                     chunkSize: "qqchunksize",
3052                     totalFileSize: "qqtotalfilesize",
3053                     totalParts: "qqtotalparts"
3054                 }
3055             },
3056
3057             resume: {
3058                 enabled: false,
3059                 id: null,
3060                 cookiesExpireIn: 7, //days
3061                 paramNames: {
3062                     resuming: "qqresume"
3063                 }
3064             },
3065
3066             formatFileName: function(fileOrBlobName) {
3067                 if (fileOrBlobName !== undefined && fileOrBlobName.length > 33) {
3068                     fileOrBlobName = fileOrBlobName.slice(0, 19) + "..." + fileOrBlobName.slice(-14);
3069                 }
3070                 return fileOrBlobName;
3071             },
3072
3073             text: {
3074                 defaultResponseError: "Upload failure reason unknown",
3075                 sizeSymbols: ["kB", "MB", "GB", "TB", "PB", "EB"]
3076             },
3077
3078             deleteFile : {
3079                 enabled: false,
3080                 method: "DELETE",
3081                 endpoint: "/server/upload",
3082                 customHeaders: {},
3083                 params: {}
3084             },
3085
3086             cors: {
3087                 expected: false,
3088                 sendCredentials: false,
3089                 allowXdr: false
3090             },
3091
3092             blobs: {
3093                 defaultName: "misc_data"
3094             },
3095
3096             paste: {
3097                 targetElement: null,
3098                 defaultName: "pasted_image"
3099             },
3100
3101             camera: {
3102                 ios: false,
3103
3104                 // if ios is true: button is null means target the default button, otherwise target the button specified
3105                 button: null
3106             },
3107
3108             // This refers to additional upload buttons to be handled by Fine Uploader.
3109             // Each element is an object, containing `element` as the only required
3110             // property.  The `element` must be a container that will ultimately
3111             // contain an invisible `<input type="file">` created by Fine Uploader.
3112             // Optional properties of each object include `multiple`, `validation`,
3113             // and `folders`.
3114             extraButtons: [],
3115
3116             // Depends on the session module.  Used to query the server for an initial file list
3117             // during initialization and optionally after a `reset`.
3118             session: {
3119                 endpoint: null,
3120                 params: {},
3121                 customHeaders: {},
3122                 refreshOnReset: true
3123             }
3124         };
3125
3126         // Replace any default options with user defined ones
3127         qq.extend(this._options, o, true);
3128
3129         this._buttons = [];
3130         this._extraButtonSpecs = {};
3131         this._buttonIdsForFileIds = [];
3132
3133         this._wrapCallbacks();
3134         this._disposeSupport =  new qq.DisposeSupport();
3135
3136         this._storedIds = [];
3137         this._autoRetries = [];
3138         this._retryTimeouts = [];
3139         this._preventRetries = [];
3140         this._thumbnailUrls = [];
3141
3142         this._netUploadedOrQueued = 0;
3143         this._netUploaded = 0;
3144         this._uploadData = this._createUploadDataTracker();
3145
3146         this._paramsStore = this._createParamsStore("request");
3147         this._deleteFileParamsStore = this._createParamsStore("deleteFile");
3148
3149         this._endpointStore = this._createEndpointStore("request");
3150         this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
3151
3152         this._handler = this._createUploadHandler();
3153
3154         this._deleteHandler = qq.DeleteFileAjaxRequester && this._createDeleteHandler();
3155
3156         if (this._options.button) {
3157             this._defaultButtonId = this._createUploadButton({element: this._options.button}).getButtonId();
3158         }
3159
3160         this._generateExtraButtonSpecs();
3161
3162         this._handleCameraAccess();
3163
3164         if (this._options.paste.targetElement) {
3165             if (qq.PasteSupport) {
3166                 this._pasteHandler = this._createPasteHandler();
3167             }
3168             else {
3169                 qq.log("Paste support module not found", "info");
3170             }
3171         }
3172
3173         this._preventLeaveInProgress();
3174
3175         this._imageGenerator = qq.ImageGenerator && new qq.ImageGenerator(qq.bind(this.log, this));
3176         this._refreshSessionData();
3177     };
3178
3179     // Define the private & public API methods.
3180     qq.FineUploaderBasic.prototype = qq.basePublicApi;
3181     qq.extend(qq.FineUploaderBasic.prototype, qq.basePrivateApi);
3182 }());
3183
3184 /*globals qq, XDomainRequest*/
3185 /** Generic class for sending non-upload ajax requests and handling the associated responses **/
3186 qq.AjaxRequester = function (o) {
3187     "use strict";
3188
3189     var log, shouldParamsBeInQueryString,
3190         queue = [],
3191         requestData = [],
3192         options = {
3193             validMethods: ["POST"],
3194             method: "POST",
3195             contentType: "application/x-www-form-urlencoded",
3196             maxConnections: 3,
3197             customHeaders: {},
3198             endpointStore: {},
3199             paramsStore: {},
3200             mandatedParams: {},
3201             allowXRequestedWithAndCacheControl: true,
3202             successfulResponseCodes: {
3203                 "DELETE": [200, 202, 204],
3204                 "POST": [200, 204],
3205                 "GET": [200]
3206             },
3207             cors: {
3208                 expected: false,
3209                 sendCredentials: false
3210             },
3211             log: function (str, level) {},
3212             onSend: function (id) {},
3213             onComplete: function (id, xhrOrXdr, isError) {}
3214         };
3215
3216     qq.extend(options, o);
3217     log = options.log;
3218
3219     if (qq.indexOf(options.validMethods, options.method) < 0) {
3220         throw new Error("'" + options.method + "' is not a supported method for this type of request!");
3221     }
3222
3223     // [Simple methods](http://www.w3.org/TR/cors/#simple-method)
3224     // are defined by the W3C in the CORS spec as a list of methods that, in part,
3225     // make a CORS request eligible to be exempt from preflighting.
3226     function isSimpleMethod() {
3227         return qq.indexOf(["GET", "POST", "HEAD"], options.method) >= 0;
3228     }
3229
3230     // [Simple headers](http://www.w3.org/TR/cors/#simple-header)
3231     // are defined by the W3C in the CORS spec as a list of headers that, in part,
3232     // make a CORS request eligible to be exempt from preflighting.
3233     function containsNonSimpleHeaders(headers) {
3234         var containsNonSimple = false;
3235
3236         qq.each(containsNonSimple, function(idx, header) {
3237             if (qq.indexOf(["Accept", "Accept-Language", "Content-Language", "Content-Type"], header) < 0) {
3238                 containsNonSimple = true;
3239                 return false;
3240             }
3241         });
3242
3243         return containsNonSimple;
3244     }
3245
3246     function isXdr(xhr) {
3247         //The `withCredentials` test is a commonly accepted way to determine if XHR supports CORS.
3248         return options.cors.expected && xhr.withCredentials === undefined;
3249     }
3250
3251     // Returns either a new `XMLHttpRequest` or `XDomainRequest` instance.
3252     function getCorsAjaxTransport() {
3253         var xhrOrXdr;
3254
3255         if (window.XMLHttpRequest || window.ActiveXObject) {
3256             xhrOrXdr = qq.createXhrInstance();
3257
3258             if (xhrOrXdr.withCredentials === undefined) {
3259                 xhrOrXdr = new XDomainRequest();
3260             }
3261         }
3262
3263         return xhrOrXdr;
3264     }
3265
3266     // Returns either a new XHR/XDR instance, or an existing one for the associated `File` or `Blob`.
3267     function getXhrOrXdr(id, dontCreateIfNotExist) {
3268         var xhrOrXdr = requestData[id].xhr;
3269
3270         if (!xhrOrXdr && !dontCreateIfNotExist) {
3271             if (options.cors.expected) {
3272                 xhrOrXdr = getCorsAjaxTransport();
3273             }
3274             else {
3275                 xhrOrXdr = qq.createXhrInstance();
3276             }
3277
3278             requestData[id].xhr = xhrOrXdr;
3279         }
3280
3281         return xhrOrXdr;
3282     }
3283
3284     // Removes element from queue, sends next request
3285     function dequeue(id) {
3286         var i = qq.indexOf(queue, id),
3287             max = options.maxConnections,
3288             nextId;
3289
3290         delete requestData[id];
3291         queue.splice(i, 1);
3292
3293         if (queue.length >= max && i < max) {
3294             nextId = queue[max - 1];
3295             sendRequest(nextId);
3296         }
3297     }
3298
3299     function onComplete(id, xdrError) {
3300         var xhr = getXhrOrXdr(id),
3301             method = options.method,
3302             isError = xdrError === true;
3303
3304         dequeue(id);
3305
3306         if (isError) {
3307             log(method + " request for " + id + " has failed", "error");
3308         }
3309         else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) {
3310             isError = true;
3311             log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
3312         }
3313
3314         options.onComplete(id, xhr, isError);
3315     }
3316
3317     function getParams(id) {
3318         var onDemandParams = requestData[id].additionalParams,
3319             mandatedParams = options.mandatedParams,
3320             params;
3321
3322         if (options.paramsStore.getParams) {
3323             params = options.paramsStore.getParams(id);
3324         }
3325
3326         if (onDemandParams) {
3327             qq.each(onDemandParams, function (name, val) {
3328                 params = params || {};
3329                 params[name] = val;
3330             });
3331         }
3332
3333         if (mandatedParams) {
3334             qq.each(mandatedParams, function (name, val) {
3335                 params = params || {};
3336                 params[name] = val;
3337             });
3338         }
3339
3340         return params;
3341     }
3342
3343     function sendRequest(id) {
3344         var xhr = getXhrOrXdr(id),
3345             method = options.method,
3346             params = getParams(id),
3347             payload = requestData[id].payload,
3348             url;
3349
3350         options.onSend(id);
3351
3352         url = createUrl(id, params);
3353
3354         // XDR and XHR status detection APIs differ a bit.
3355         if (isXdr(xhr)) {
3356             xhr.onload = getXdrLoadHandler(id);
3357             xhr.onerror = getXdrErrorHandler(id);
3358         }
3359         else {
3360             xhr.onreadystatechange = getXhrReadyStateChangeHandler(id);
3361         }
3362
3363         // The last parameter is assumed to be ignored if we are actually using `XDomainRequest`.
3364         xhr.open(method, url, true);
3365
3366         // Instruct the transport to send cookies along with the CORS request,
3367         // unless we are using `XDomainRequest`, which is not capable of this.
3368         if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) {
3369             xhr.withCredentials = true;
3370         }
3371
3372         setHeaders(id);
3373
3374         log("Sending " + method + " request for " + id);
3375
3376         if (payload) {
3377             xhr.send(payload);
3378         }
3379         else if (shouldParamsBeInQueryString || !params) {
3380             xhr.send();
3381         }
3382         else if (params && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) {
3383             xhr.send(qq.obj2url(params, ""));
3384         }
3385         else if (params && options.contentType.toLowerCase().indexOf("application/json") >= 0) {
3386             xhr.send(JSON.stringify(params));
3387         }
3388         else {
3389             xhr.send(params);
3390         }
3391     }
3392
3393     function createUrl(id, params) {
3394         var endpoint = options.endpointStore.getEndpoint(id),
3395             addToPath = requestData[id].addToPath;
3396
3397         /*jshint -W116,-W041 */
3398         if (addToPath != undefined) {
3399             endpoint += "/" + addToPath;
3400         }
3401
3402         if (shouldParamsBeInQueryString && params) {
3403             return qq.obj2url(params, endpoint);
3404         }
3405         else {
3406             return endpoint;
3407         }
3408     }
3409
3410     // Invoked by the UA to indicate a number of possible states that describe
3411     // a live `XMLHttpRequest` transport.
3412     function getXhrReadyStateChangeHandler(id) {
3413         return function () {
3414             if (getXhrOrXdr(id).readyState === 4) {
3415                 onComplete(id);
3416             }
3417         };
3418     }
3419
3420     // This will be called by IE to indicate **success** for an associated
3421     // `XDomainRequest` transported request.
3422     function getXdrLoadHandler(id) {
3423         return function () {
3424             onComplete(id);
3425         };
3426     }
3427
3428     // This will be called by IE to indicate **failure** for an associated
3429     // `XDomainRequest` transported request.
3430     function getXdrErrorHandler(id) {
3431         return function () {
3432             onComplete(id, true);
3433         };
3434     }
3435
3436     function setHeaders(id) {
3437         var xhr = getXhrOrXdr(id),
3438             customHeaders = options.customHeaders,
3439             onDemandHeaders = requestData[id].additionalHeaders || {},
3440             method = options.method,
3441             allHeaders = {};
3442
3443         // If XDomainRequest is being used, we can't set headers, so just ignore this block.
3444         if (!isXdr(xhr)) {
3445             // Only attempt to add X-Requested-With & Cache-Control if permitted
3446             if (options.allowXRequestedWithAndCacheControl) {
3447                 // Do not add X-Requested-With & Cache-Control if this is a cross-origin request
3448                 // OR the cross-origin request contains a non-simple method or header.
3449                 // This is done to ensure a preflight is not triggered exclusively based on the
3450                 // addition of these 2 non-simple headers.
3451                 if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) {
3452                     xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
3453                     xhr.setRequestHeader("Cache-Control", "no-cache");
3454                 }
3455             }
3456
3457             if (options.contentType && (method === "POST" || method === "PUT")) {
3458                 xhr.setRequestHeader("Content-Type", options.contentType);
3459             }
3460
3461             qq.extend(allHeaders, customHeaders);
3462             qq.extend(allHeaders, onDemandHeaders);
3463
3464             qq.each(allHeaders, function (name, val) {
3465                 xhr.setRequestHeader(name, val);
3466             });
3467         }
3468     }
3469
3470     function isResponseSuccessful(responseCode) {
3471         return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0;
3472     }
3473
3474     function prepareToSend(id, addToPath, additionalParams, additionalHeaders, payload) {
3475         requestData[id] = {
3476             addToPath: addToPath,
3477             additionalParams: additionalParams,
3478             additionalHeaders: additionalHeaders,
3479             payload: payload
3480         };
3481
3482         var len = queue.push(id);
3483
3484         // if too many active connections, wait...
3485         if (len <= options.maxConnections) {
3486             sendRequest(id);
3487         }
3488     }
3489
3490
3491     shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE";
3492
3493     qq.extend(this, {
3494         // Start the process of sending the request.  The ID refers to the file associated with the request.
3495         initTransport: function(id) {
3496             var path, params, headers, payload;
3497
3498             return {
3499                 // Optionally specify the end of the endpoint path for the request.
3500                 withPath: function(appendToPath) {
3501                     path = appendToPath;
3502                     return this;
3503                 },
3504
3505                 // Optionally specify additional parameters to send along with the request.
3506                 // These will be added to the query string for GET/DELETE requests or the payload
3507                 // for POST/PUT requests.  The Content-Type of the request will be used to determine
3508                 // how these parameters should be formatted as well.
3509                 withParams: function(additionalParams) {
3510                     params = additionalParams;
3511                     return this;
3512                 },
3513
3514                 // Optionally specify additional headers to send along with the request.
3515                 withHeaders: function(additionalHeaders) {
3516                     headers = additionalHeaders;
3517                     return this;
3518                 },
3519
3520                 // Optionally specify a payload/body for the request.
3521                 withPayload: function(thePayload) {
3522                     payload = thePayload;
3523                     return this;
3524                 },
3525
3526                 // Send the constructed request.
3527                 send: function() {
3528                     prepareToSend(id, path, params, headers, payload);
3529                 }
3530             };
3531         }
3532     });
3533 };
3534
3535 /*globals qq*/
3536 /**
3537  * Base upload handler module.  Delegates to more specific handlers.
3538  *
3539  * @param o Options.  Passed along to the specific handler submodule as well.
3540  * @param namespace [optional] Namespace for the specific handler.
3541  */
3542 qq.UploadHandler = function(o, namespace) {
3543     "use strict";
3544
3545     var queue = [],
3546         options, log, handlerImpl;
3547
3548     // Default options, can be overridden by the user
3549     options = {
3550         debug: false,
3551         forceMultipart: true,
3552         paramsInBody: false,
3553         paramsStore: {},
3554         endpointStore: {},
3555         filenameParam: "qqfilename",
3556         cors: {
3557             expected: false,
3558             sendCredentials: false
3559         },
3560         maxConnections: 3, // maximum number of concurrent uploads
3561         uuidName: "qquuid",
3562         totalFileSizeName: "qqtotalfilesize",
3563         chunking: {
3564             enabled: false,
3565             partSize: 2000000, //bytes
3566             paramNames: {
3567                 partIndex: "qqpartindex",
3568                 partByteOffset: "qqpartbyteoffset",
3569                 chunkSize: "qqchunksize",
3570                 totalParts: "qqtotalparts",
3571                 filename: "qqfilename"
3572             }
3573         },
3574         resume: {
3575             enabled: false,
3576             id: null,
3577             cookiesExpireIn: 7, //days
3578             paramNames: {
3579                 resuming: "qqresume"
3580             }
3581         },
3582         log: function(str, level) {},
3583         onProgress: function(id, fileName, loaded, total){},
3584         onComplete: function(id, fileName, response, xhr){},
3585         onCancel: function(id, fileName){},
3586         onUpload: function(id, fileName){},
3587         onUploadChunk: function(id, fileName, chunkData){},
3588         onUploadChunkSuccess: function(id, chunkData, response, xhr){},
3589         onAutoRetry: function(id, fileName, response, xhr){},
3590         onResume: function(id, fileName, chunkData){},
3591         onUuidChanged: function(id, newUuid){},
3592         getName: function(id) {}
3593
3594     };
3595     qq.extend(options, o);
3596
3597     log = options.log;
3598
3599     /**
3600      * Removes element from queue, starts upload of next
3601      */
3602     function dequeue(id) {
3603         var i = qq.indexOf(queue, id),
3604             max = options.maxConnections,
3605             nextId;
3606
3607         if (i >= 0) {
3608             queue.splice(i, 1);
3609
3610             if (queue.length >= max && i < max){
3611                 nextId = queue[max-1];
3612                 handlerImpl.upload(nextId);
3613             }
3614         }
3615     }
3616
3617     function cancelSuccess(id) {
3618         log("Cancelling " + id);
3619         options.paramsStore.remove(id);
3620         dequeue(id);
3621     }
3622
3623     function determineHandlerImpl() {
3624         var handlerType = namespace ? qq[namespace] : qq,
3625             handlerModuleSubtype = qq.supportedFeatures.ajaxUploading ? "Xhr" : "Form";
3626
3627         handlerImpl = new handlerType["UploadHandler" + handlerModuleSubtype](
3628             options,
3629             {onUploadComplete: dequeue, onUuidChanged: options.onUuidChanged,
3630                 getName: options.getName, getUuid: options.getUuid, getSize: options.getSize, log: log}
3631         );
3632     }
3633
3634
3635     qq.extend(this, {
3636         /**
3637          * Adds file or file input to the queue
3638          * @returns id
3639          **/
3640         add: function(id, file) {
3641             return handlerImpl.add.apply(this, arguments);
3642         },
3643
3644         /**
3645          * Sends the file identified by id
3646          */
3647         upload: function(id) {
3648             var len = queue.push(id);
3649
3650             // if too many active uploads, wait...
3651             if (len <= options.maxConnections){
3652                 handlerImpl.upload(id);
3653                 return true;
3654             }
3655
3656             return false;
3657         },
3658
3659         retry: function(id) {
3660             var i = qq.indexOf(queue, id);
3661             if (i >= 0) {
3662                 return handlerImpl.upload(id, true);
3663             }
3664             else {
3665                 return this.upload(id);
3666             }
3667         },
3668
3669         /**
3670          * Cancels file upload by id
3671          */
3672         cancel: function(id) {
3673             var cancelRetVal = handlerImpl.cancel(id);
3674
3675             if (cancelRetVal instanceof qq.Promise) {
3676                 cancelRetVal.then(function() {
3677                     cancelSuccess(id);
3678                 });
3679             }
3680             else if (cancelRetVal !== false) {
3681                 cancelSuccess(id);
3682             }
3683         },
3684
3685         /**
3686          * Cancels all queued or in-progress uploads
3687          */
3688         cancelAll: function() {
3689             var self = this,
3690                 queueCopy = [];
3691
3692             qq.extend(queueCopy, queue);
3693             qq.each(queueCopy, function(idx, fileId) {
3694                 self.cancel(fileId);
3695             });
3696
3697             queue = [];
3698         },
3699
3700         getFile: function(id) {
3701             if (handlerImpl.getFile) {
3702                 return handlerImpl.getFile(id);
3703             }
3704         },
3705
3706         getInput: function(id) {
3707             if (handlerImpl.getInput) {
3708                 return handlerImpl.getInput(id);
3709             }
3710         },
3711
3712         reset: function() {
3713             log("Resetting upload handler");
3714             this.cancelAll();
3715             queue = [];
3716             handlerImpl.reset();
3717         },
3718
3719         expunge: function(id) {
3720             if (this.isValid(id)) {
3721                 return handlerImpl.expunge(id);
3722             }
3723         },
3724
3725         /**
3726          * Determine if the file exists.
3727          */
3728         isValid: function(id) {
3729             return handlerImpl.isValid(id);
3730         },
3731
3732         getResumableFilesData: function() {
3733             if (handlerImpl.getResumableFilesData) {
3734                 return handlerImpl.getResumableFilesData();
3735             }
3736             return [];
3737         },
3738
3739         /**
3740          * This may or may not be implemented, depending on the handler.  For handlers where a third-party ID is
3741          * available (such as the "key" for Amazon S3), this will return that value.  Otherwise, the return value
3742          * will be undefined.
3743          *
3744          * @param id Internal file ID
3745          * @returns {*} Some identifier used by a 3rd-party service involved in the upload process
3746          */
3747         getThirdPartyFileId: function(id) {
3748             if (handlerImpl.getThirdPartyFileId && this.isValid(id)) {
3749                 return handlerImpl.getThirdPartyFileId(id);
3750             }
3751         },
3752
3753         /**
3754          * Attempts to pause the associated upload if the specific handler supports this and the file is "valid".
3755          * @param id ID of the upload/file to pause
3756          * @returns {boolean} true if the upload was paused
3757          */
3758         pause: function(id) {
3759             if (handlerImpl.pause && this.isValid(id) && handlerImpl.pause(id)) {
3760                 dequeue(id);
3761                 return true;
3762             }
3763         }
3764     });
3765
3766     determineHandlerImpl();
3767 };
3768
3769 /* globals qq */
3770 /**
3771  * Common APIs exposed to creators of upload via form/iframe handlers.  This is reused and possibly overridden
3772  * in some cases by specific form upload handlers.
3773  *
3774  * @param internalApi Object that will be filled with internal API methods
3775  * @param spec Options/static values used to configure this handler
3776  * @param proxy Callbacks & methods used to query for or push out data/changes
3777  * @constructor
3778  */
3779 qq.UploadHandlerFormApi = function(internalApi, spec, proxy) {
3780     "use strict";
3781
3782     var formHandlerInstanceId = qq.getUniqueId(),
3783         onloadCallbacks = {},
3784         detachLoadEvents = {},
3785         postMessageCallbackTimers = {},
3786         publicApi = this,
3787         isCors = spec.isCors,
3788         fileState = spec.fileState,
3789         inputName = spec.inputName,
3790         onCancel = proxy.onCancel,
3791         onUuidChanged = proxy.onUuidChanged,
3792         getName = proxy.getName,
3793         getUuid = proxy.getUuid,
3794         log = proxy.log,
3795         corsMessageReceiver = new qq.WindowReceiveMessage({log: log});
3796
3797
3798     /**
3799      * Remove any trace of the file from the handler.
3800      *
3801      * @param id ID of the associated file
3802      */
3803     function expungeFile(id) {
3804         delete detachLoadEvents[id];
3805         delete fileState[id];
3806
3807         // If we are dealing with CORS, we might still be waiting for a response from a loaded iframe.
3808         // In that case, terminate the timer waiting for a message from the loaded iframe
3809         // and stop listening for any more messages coming from this iframe.
3810         if (isCors) {
3811             clearTimeout(postMessageCallbackTimers[id]);
3812             delete postMessageCallbackTimers[id];
3813             corsMessageReceiver.stopReceivingMessages(id);
3814         }
3815
3816         var iframe = document.getElementById(internalApi.getIframeName(id));
3817         if (iframe) {
3818             // To cancel request set src to something else.  We use src="javascript:false;"
3819             // because it doesn't trigger ie6 prompt on https
3820             iframe.setAttribute("src", "java" + String.fromCharCode(115) + "cript:false;"); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
3821
3822             qq(iframe).remove();
3823         }
3824     }
3825
3826     /**
3827      * If we are in CORS mode, we must listen for messages (containing the server response) from the associated
3828      * iframe, since we cannot directly parse the content of the iframe due to cross-origin restrictions.
3829      *
3830      * @param iframe Listen for messages on this iframe.
3831      * @param callback Invoke this callback with the message from the iframe.
3832      */
3833     function registerPostMessageCallback(iframe, callback) {
3834         var iframeName = iframe.id,
3835             fileId = getFileIdForIframeName(iframeName),
3836             uuid = getUuid(fileId);
3837
3838         onloadCallbacks[uuid] = callback;
3839
3840         // When the iframe has loaded (after the server responds to an upload request)
3841         // declare the attempt a failure if we don't receive a valid message shortly after the response comes in.
3842         detachLoadEvents[fileId] = qq(iframe).attach("load", function() {
3843             if (fileState[fileId].input) {
3844                 log("Received iframe load event for CORS upload request (iframe name " + iframeName + ")");
3845
3846                 postMessageCallbackTimers[iframeName] = setTimeout(function() {
3847                     var errorMessage = "No valid message received from loaded iframe for iframe name " + iframeName;
3848                     log(errorMessage, "error");
3849                     callback({
3850                         error: errorMessage
3851                     });
3852                 }, 1000);
3853             }
3854         });
3855
3856         // Listen for messages coming from this iframe.  When a message has been received, cancel the timer
3857         // that declares the upload a failure if a message is not received within a reasonable amount of time.
3858         corsMessageReceiver.receiveMessage(iframeName, function(message) {
3859             log("Received the following window message: '" + message + "'");
3860             var fileId = getFileIdForIframeName(iframeName),
3861                 response = internalApi.parseJsonResponse(fileId, message),
3862                 uuid = response.uuid,
3863                 onloadCallback;
3864
3865             if (uuid && onloadCallbacks[uuid]) {
3866                 log("Handling response for iframe name " + iframeName);
3867                 clearTimeout(postMessageCallbackTimers[iframeName]);
3868                 delete postMessageCallbackTimers[iframeName];
3869
3870                 internalApi.detachLoadEvent(iframeName);
3871
3872                 onloadCallback = onloadCallbacks[uuid];
3873
3874                 delete onloadCallbacks[uuid];
3875                 corsMessageReceiver.stopReceivingMessages(iframeName);
3876                 onloadCallback(response);
3877             }
3878             else if (!uuid) {
3879                 log("'" + message + "' does not contain a UUID - ignoring.");
3880             }
3881         });
3882     }
3883
3884     /**
3885      * Generates an iframe to be used as a target for upload-related form submits.  This also adds the iframe
3886      * to the current `document`.  Note that the iframe is hidden from view.
3887      *
3888      * @param name Name of the iframe.
3889      * @returns {HTMLIFrameElement} The created iframe
3890      */
3891     function initIframeForUpload(name) {
3892         var iframe = qq.toElement("<iframe src='javascript:false;' name='" + name + "' />");
3893
3894         iframe.setAttribute("id", name);
3895
3896         iframe.style.display = "none";
3897         document.body.appendChild(iframe);
3898
3899         return iframe;
3900     }
3901
3902     /**
3903      * @param iframeName `document`-unique Name of the associated iframe
3904      * @returns {*} ID of the associated file
3905      */
3906     function getFileIdForIframeName(iframeName) {
3907         return iframeName.split("_")[0];
3908     }
3909
3910
3911 // INTERNAL API
3912
3913     qq.extend(internalApi, {
3914         /**
3915          * @param fileId ID of the associated file
3916          * @returns {string} The `document`-unique name of the iframe
3917          */
3918         getIframeName: function(fileId) {
3919             return fileId + "_" + formHandlerInstanceId;
3920         },
3921
3922         /**
3923          * Creates an iframe with a specific document-unique name.
3924          *
3925          * @param id ID of the associated file
3926          * @returns {HTMLIFrameElement}
3927          */
3928         createIframe: function(id) {
3929             var iframeName = internalApi.getIframeName(id);
3930
3931             return initIframeForUpload(iframeName);
3932         },
3933
3934         /**
3935          * @param id ID of the associated file
3936          * @param innerHtmlOrMessage JSON message
3937          * @returns {*} The parsed response, or an empty object if the response could not be parsed
3938          */
3939         parseJsonResponse: function(id, innerHtmlOrMessage) {
3940             var response;
3941
3942             try {
3943                 response = qq.parseJson(innerHtmlOrMessage);
3944
3945                 if (response.newUuid !== undefined) {
3946                     onUuidChanged(id, response.newUuid);
3947                 }
3948             }
3949             catch(error) {
3950                 log("Error when attempting to parse iframe upload response (" + error.message + ")", "error");
3951                 response = {};
3952             }
3953
3954             return response;
3955         },
3956
3957         /**
3958          * Generates a form element and appends it to the `document`.  When the form is submitted, a specific iframe is targeted.
3959          * The name of the iframe is passed in as a property of the spec parameter, and must be unique in the `document`.  Note
3960          * that the form is hidden from view.
3961          *
3962          * @param spec An object containing various properties to be used when constructing the form.  Required properties are
3963          * currently: `method`, `endpoint`, `params`, `paramsInBody`, and `targetName`.
3964          * @returns {HTMLFormElement} The created form
3965          */
3966         initFormForUpload: function(spec) {
3967             var method = spec.method,
3968                 endpoint = spec.endpoint,
3969                 params = spec.params,
3970                 paramsInBody = spec.paramsInBody,
3971                 targetName = spec.targetName,
3972                 form = qq.toElement("<form method='" + method + "' enctype='multipart/form-data'></form>"),
3973                 url = endpoint;
3974
3975             if (paramsInBody) {
3976                 qq.obj2Inputs(params, form);
3977             }
3978             else {
3979                 url = qq.obj2url(params, endpoint);
3980             }
3981
3982             form.setAttribute("action", url);
3983             form.setAttribute("target", targetName);
3984             form.style.display = "none";
3985             document.body.appendChild(form);
3986
3987             return form;
3988         },
3989
3990         /**
3991          * This function either delegates to a more specific message handler if CORS is involved,
3992          * or simply registers a callback when the iframe has been loaded that invokes the passed callback
3993          * after determining if the content of the iframe is accessible.
3994          *
3995          * @param iframe Associated iframe
3996          * @param callback Callback to invoke after we have determined if the iframe content is accessible.
3997          */
3998         attachLoadEvent: function(iframe, callback) {
3999             /*jslint eqeq: true*/
4000             var responseDescriptor;
4001
4002             if (isCors) {
4003                 registerPostMessageCallback(iframe, callback);
4004             }
4005             else {
4006                 detachLoadEvents[iframe.id] = qq(iframe).attach("load", function(){
4007                     log("Received response for " + iframe.id);
4008
4009                     // when we remove iframe from dom
4010                     // the request stops, but in IE load
4011                     // event fires
4012                     if (!iframe.parentNode){
4013                         return;
4014                     }
4015
4016                     try {
4017                         // fixing Opera 10.53
4018                         if (iframe.contentDocument &&
4019                             iframe.contentDocument.body &&
4020                             iframe.contentDocument.body.innerHTML == "false"){
4021                             // In Opera event is fired second time
4022                             // when body.innerHTML changed from false
4023                             // to server response approx. after 1 sec
4024                             // when we upload file with iframe
4025                             return;
4026                         }
4027                     }
4028                     catch (error) {
4029                         //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
4030                         log("Error when attempting to access iframe during handling of upload response (" + error.message + ")", "error");
4031                         responseDescriptor = {success: false};
4032                     }
4033
4034                     callback(responseDescriptor);
4035                 });
4036             }
4037         },
4038
4039         /**
4040          * Called when we are no longer interested in being notified when an iframe has loaded.
4041          *
4042          * @param id Associated file ID
4043          */
4044         detachLoadEvent: function(id) {
4045             if (detachLoadEvents[id] !== undefined) {
4046                 detachLoadEvents[id]();
4047                 delete detachLoadEvents[id];
4048             }
4049         }
4050     });
4051
4052
4053 // PUBLIC API
4054
4055     qq.extend(this, {
4056         add: function(id, fileInput) {
4057             fileState[id] = {input: fileInput};
4058
4059             fileInput.setAttribute("name", inputName);
4060
4061             // remove file input from DOM
4062             if (fileInput.parentNode){
4063                 qq(fileInput).remove();
4064             }
4065         },
4066
4067         getInput: function(id) {
4068             return fileState[id].input;
4069         },
4070
4071         isValid: function(id) {
4072             return fileState[id] !== undefined &&
4073                 fileState[id].input !== undefined;
4074         },
4075
4076         reset: function() {
4077             fileState.length = 0;
4078         },
4079
4080         expunge: function(id) {
4081             return expungeFile(id);
4082         },
4083
4084         cancel: function(id) {
4085             var onCancelRetVal = onCancel(id, getName(id));
4086
4087             if (onCancelRetVal instanceof qq.Promise) {
4088                 return onCancelRetVal.then(function() {
4089                     this.expunge(id);
4090                 });
4091             }
4092             else if (onCancelRetVal !== false) {
4093                 this.expunge(id);
4094                 return true;
4095             }
4096
4097             return false;
4098         },
4099
4100         upload: function(id) {
4101             // implementation-specific
4102         }
4103     });
4104 };
4105
4106 /* globals qq */
4107 /**
4108  * Common API exposed to creators of XHR handlers.  This is reused and possibly overriding in some cases by specific
4109  * XHR upload handlers.
4110  *
4111  * @param internalApi Object that will be filled with internal API methods
4112  * @param spec Options/static values used to configure this handler
4113  * @param proxy Callbacks & methods used to query for or push out data/changes
4114  * @constructor
4115  */
4116 qq.UploadHandlerXhrApi = function(internalApi, spec, proxy) {
4117     "use strict";
4118
4119     var publicApi = this,
4120         fileState = spec.fileState,
4121         chunking = spec.chunking,
4122         onUpload = proxy.onUpload,
4123         onCancel = proxy.onCancel,
4124         onUuidChanged = proxy.onUuidChanged,
4125         getName = proxy.getName,
4126         getSize = proxy.getSize,
4127         log = proxy.log;
4128
4129
4130     function getChunk(fileOrBlob, startByte, endByte) {
4131         if (fileOrBlob.slice) {
4132             return fileOrBlob.slice(startByte, endByte);
4133         }
4134         else if (fileOrBlob.mozSlice) {
4135             return fileOrBlob.mozSlice(startByte, endByte);
4136         }
4137         else if (fileOrBlob.webkitSlice) {
4138             return fileOrBlob.webkitSlice(startByte, endByte);
4139         }
4140     }
4141
4142     qq.extend(internalApi, {
4143         /**
4144          * Creates an XHR instance for this file and stores it in the fileState.
4145          *
4146          * @param id File ID
4147          * @returns {XMLHttpRequest}
4148          */
4149         createXhr: function(id) {
4150             var xhr = qq.createXhrInstance();
4151
4152             fileState[id].xhr = xhr;
4153
4154             return xhr;
4155         },
4156
4157         /**
4158          * @param id ID of the associated file
4159          * @returns {number} Number of parts this file can be divided into, or undefined if chunking is not supported in this UA
4160          */
4161         getTotalChunks: function(id) {
4162             if (chunking) {
4163                 var fileSize = getSize(id),
4164                     chunkSize = chunking.partSize;
4165
4166                 return Math.ceil(fileSize / chunkSize);
4167             }
4168         },
4169
4170         getChunkData: function(id, chunkIndex) {
4171             var chunkSize = chunking.partSize,
4172                 fileSize = getSize(id),
4173                 fileOrBlob = publicApi.getFile(id),
4174                 startBytes = chunkSize * chunkIndex,
4175                 endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
4176                 totalChunks = internalApi.getTotalChunks(id);
4177
4178             return {
4179                 part: chunkIndex,
4180                 start: startBytes,
4181                 end: endBytes,
4182                 count: totalChunks,
4183                 blob: getChunk(fileOrBlob, startBytes, endBytes),
4184                 size: endBytes - startBytes
4185             };
4186         },
4187
4188         getChunkDataForCallback: function(chunkData) {
4189             return {
4190                 partIndex: chunkData.part,
4191                 startByte: chunkData.start + 1,
4192                 endByte: chunkData.end,
4193                 totalParts: chunkData.count
4194             };
4195         }
4196     });
4197
4198     qq.extend(this, {
4199         /**
4200          * Adds File or Blob to the queue
4201          **/
4202         add: function(id, fileOrBlobData) {
4203             if (qq.isFile(fileOrBlobData)) {
4204                 fileState[id] = {file: fileOrBlobData};
4205             }
4206             else if (qq.isBlob(fileOrBlobData.blob)) {
4207                 fileState[id] =  {blobData: fileOrBlobData};
4208             }
4209             else {
4210                 throw new Error("Passed obj is not a File or BlobData (in qq.UploadHandlerXhr)");
4211             }
4212         },
4213
4214         getFile: function(id) {
4215             if (fileState[id]) {
4216                 return fileState[id].file || fileState[id].blobData.blob;
4217             }
4218         },
4219
4220         isValid: function(id) {
4221             return fileState[id] !== undefined;
4222         },
4223
4224         reset: function() {
4225             fileState.length = 0;
4226         },
4227
4228         expunge: function(id) {
4229             var xhr = fileState[id].xhr;
4230
4231             if (xhr) {
4232                 xhr.onreadystatechange = null;
4233                 xhr.abort();
4234             }
4235
4236             delete fileState[id];
4237         },
4238
4239         /**
4240          * Sends the file identified by id to the server
4241          */
4242         upload: function(id, retry) {
4243             fileState[id] && delete fileState[id].paused;
4244             return onUpload(id, retry);
4245         },
4246
4247         cancel: function(id) {
4248             var onCancelRetVal = onCancel(id, getName(id));
4249
4250             if (onCancelRetVal instanceof qq.Promise) {
4251                 return onCancelRetVal.then(function() {
4252                     this.expunge(id);
4253                 });
4254             }
4255             else if (onCancelRetVal !== false) {
4256                 this.expunge(id);
4257                 return true;
4258             }
4259
4260             return false;
4261         },
4262
4263         pause: function(id) {
4264             var xhr = fileState[id].xhr;
4265
4266             if(xhr) {
4267                 log(qq.format("Aborting XHR upload for {} '{}' due to pause instruction.", id, getName(id)));
4268                 fileState[id].paused = true;
4269                 xhr.abort();
4270                 return true;
4271             }
4272         }
4273     });
4274 };
4275
4276 /*globals qq */
4277 /*jshint -W117 */
4278 qq.WindowReceiveMessage = function(o) {
4279     "use strict";
4280
4281     var options = {
4282             log: function(message, level) {}
4283         },
4284         callbackWrapperDetachers = {};
4285
4286     qq.extend(options, o);
4287
4288     qq.extend(this, {
4289         receiveMessage : function(id, callback) {
4290             var onMessageCallbackWrapper = function(event) {
4291                     callback(event.data);
4292                 };
4293
4294             if (window.postMessage) {
4295                 callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper);
4296             }
4297             else {
4298                 log("iframe message passing not supported in this browser!", "error");
4299             }
4300         },
4301
4302         stopReceivingMessages : function(id) {
4303             if (window.postMessage) {
4304                 var detacher = callbackWrapperDetachers[id];
4305                 if (detacher) {
4306                     detacher();
4307                 }
4308             }
4309         }
4310     });
4311 };
4312
4313 /*globals qq */
4314 /**
4315  * Defines the public API for FineUploader mode.
4316  */
4317 (function(){
4318     "use strict";
4319
4320     qq.uiPublicApi = {
4321         clearStoredFiles: function() {
4322             this._parent.prototype.clearStoredFiles.apply(this, arguments);
4323             this._templating.clearFiles();
4324         },
4325
4326         addExtraDropzone: function(element){
4327             this._dnd && this._dnd.setupExtraDropzone(element);
4328         },
4329
4330         removeExtraDropzone: function(element){
4331             if (this._dnd) {
4332                 return this._dnd.removeDropzone(element);
4333             }
4334         },
4335
4336         getItemByFileId: function(id) {
4337             return this._templating.getFileContainer(id);
4338         },
4339
4340         reset: function() {
4341             this._parent.prototype.reset.apply(this, arguments);
4342             this._templating.reset();
4343
4344             if (!this._options.button && this._templating.getButton()) {
4345                 this._defaultButtonId = this._createUploadButton({element: this._templating.getButton()}).getButtonId();
4346             }
4347
4348             if (this._dnd) {
4349                 this._dnd.dispose();
4350                 this._dnd = this._setupDragAndDrop();
4351             }
4352
4353             this._totalFilesInBatch = 0;
4354             this._filesInBatchAddedToUi = 0;
4355
4356             this._setupClickAndEditEventHandlers();
4357         },
4358
4359         pauseUpload: function(id) {
4360             var paused = this._parent.prototype.pauseUpload.apply(this, arguments);
4361
4362             paused && this._templating.uploadPaused(id);
4363             return paused;
4364         },
4365
4366         continueUpload: function(id) {
4367             var continued = this._parent.prototype.continueUpload.apply(this, arguments);
4368
4369             continued && this._templating.uploadContinued(id);
4370             return continued;
4371         },
4372
4373         getId: function(fileContainerOrChildEl) {
4374             return this._templating.getFileId(fileContainerOrChildEl);
4375         },
4376
4377         getDropTarget: function(fileId) {
4378             var file = this.getFile(fileId);
4379
4380             return file.qqDropTarget;
4381         }
4382     };
4383
4384
4385
4386
4387     /**
4388      * Defines the private (internal) API for FineUploader mode.
4389      */
4390     qq.uiPrivateApi = {
4391         _getButton: function(buttonId) {
4392             var button = this._parent.prototype._getButton.apply(this, arguments);
4393
4394             if (!button) {
4395                 if (buttonId === this._defaultButtonId) {
4396                     button = this._templating.getButton();
4397                 }
4398             }
4399
4400             return button;
4401         },
4402
4403         _removeFileItem: function(fileId) {
4404             this._templating.removeFile(fileId);
4405         },
4406
4407         _setupClickAndEditEventHandlers: function() {
4408             this._fileButtonsClickHandler = qq.FileButtonsClickHandler && this._bindFileButtonsClickEvent();
4409
4410             // A better approach would be to check specifically for focusin event support by querying the DOM API,
4411             // but the DOMFocusIn event is not exposed as a property, so we have to resort to UA string sniffing.
4412             this._focusinEventSupported = !qq.firefox();
4413
4414             if (this._isEditFilenameEnabled())
4415             {
4416                 this._filenameClickHandler = this._bindFilenameClickEvent();
4417                 this._filenameInputFocusInHandler = this._bindFilenameInputFocusInEvent();
4418                 this._filenameInputFocusHandler = this._bindFilenameInputFocusEvent();
4419             }
4420         },
4421
4422         _setupDragAndDrop: function() {
4423             var self = this,
4424                 dropZoneElements = this._options.dragAndDrop.extraDropzones,
4425                 templating = this._templating,
4426                 defaultDropZone = templating.getDropZone();
4427
4428             defaultDropZone && dropZoneElements.push(defaultDropZone);
4429
4430             return new qq.DragAndDrop({
4431                 dropZoneElements: dropZoneElements,
4432                 allowMultipleItems: this._options.multiple,
4433                 classes: {
4434                     dropActive: this._options.classes.dropActive
4435                 },
4436                 callbacks: {
4437                     processingDroppedFiles: function() {
4438                         templating.showDropProcessing();
4439                     },
4440                     processingDroppedFilesComplete: function(files, targetEl) {
4441                         templating.hideDropProcessing();
4442
4443                         qq.each(files, function(idx, file) {
4444                             file.qqDropTarget = targetEl;
4445                         });
4446
4447                         if (files) {
4448                             self.addFiles(files, null, null);
4449                         }
4450                     },
4451                     dropError: function(code, errorData) {
4452                         self._itemError(code, errorData);
4453                     },
4454                     dropLog: function(message, level) {
4455                         self.log(message, level);
4456                     }
4457                 }
4458             });
4459         },
4460
4461         _bindFileButtonsClickEvent: function() {
4462             var self = this;
4463
4464             return new qq.FileButtonsClickHandler({
4465                 templating: this._templating,
4466
4467                 log: function(message, lvl) {
4468                     self.log(message, lvl);
4469                 },
4470
4471                 onDeleteFile: function(fileId) {
4472                     self.deleteFile(fileId);
4473                 },
4474
4475                 onCancel: function(fileId) {
4476                     self.cancel(fileId);
4477                 },
4478
4479                 onRetry: function(fileId) {
4480                     qq(self._templating.getFileContainer(fileId)).removeClass(self._classes.retryable);
4481                     self.retry(fileId);
4482                 },
4483
4484                 onPause: function(fileId) {
4485                     self.pauseUpload(fileId);
4486                 },
4487
4488                 onContinue: function(fileId) {
4489                     self.continueUpload(fileId);
4490                 },
4491
4492                 onGetName: function(fileId) {
4493                     return self.getName(fileId);
4494                 }
4495             });
4496         },
4497
4498         _isEditFilenameEnabled: function() {
4499             /*jshint -W014 */
4500             return this._templating.isEditFilenamePossible()
4501                 && !this._options.autoUpload
4502                 && qq.FilenameClickHandler
4503                 && qq.FilenameInputFocusHandler
4504                 && qq.FilenameInputFocusHandler;
4505         },
4506
4507         _filenameEditHandler: function() {
4508             var self = this,
4509                 templating = this._templating;
4510
4511             return {
4512                 templating: templating,
4513                 log: function(message, lvl) {
4514                     self.log(message, lvl);
4515                 },
4516                 onGetUploadStatus: function(fileId) {
4517                     return self.getUploads({id: fileId}).status;
4518                 },
4519                 onGetName: function(fileId) {
4520                     return self.getName(fileId);
4521                 },
4522                 onSetName: function(id, newName) {
4523                     var formattedFilename = self._options.formatFileName(newName);
4524
4525                     templating.updateFilename(id, formattedFilename);
4526                     self.setName(id, newName);
4527                 },
4528                 onEditingStatusChange: function(id, isEditing) {
4529                     var qqInput = qq(templating.getEditInput(id)),
4530                         qqFileContainer = qq(templating.getFileContainer(id));
4531
4532                     if (isEditing) {
4533                         qqInput.addClass("qq-editing");
4534                         templating.hideFilename(id);
4535                         templating.hideEditIcon(id);
4536                     }
4537                     else {
4538                         qqInput.removeClass("qq-editing");
4539                         templating.showFilename(id);
4540                         templating.showEditIcon(id);
4541                     }
4542
4543                     // Force IE8 and older to repaint
4544                     qqFileContainer.addClass("qq-temp").removeClass("qq-temp");
4545                 }
4546             };
4547         },
4548
4549         _onUploadStatusChange: function(id, oldStatus, newStatus) {
4550             this._parent.prototype._onUploadStatusChange.apply(this, arguments);
4551
4552             if (this._isEditFilenameEnabled()) {
4553                 // Status for a file exists before it has been added to the DOM, so we must be careful here.
4554                 if (this._templating.getFileContainer(id) && newStatus !== qq.status.SUBMITTED) {
4555                     this._templating.markFilenameEditable(id);
4556                     this._templating.hideEditIcon(id);
4557                 }
4558             }
4559         },
4560
4561         _bindFilenameInputFocusInEvent: function() {
4562             var spec = qq.extend({}, this._filenameEditHandler());
4563
4564             return new qq.FilenameInputFocusInHandler(spec);
4565         },
4566
4567         _bindFilenameInputFocusEvent: function() {
4568             var spec = qq.extend({}, this._filenameEditHandler());
4569
4570             return new qq.FilenameInputFocusHandler(spec);
4571         },
4572
4573         _bindFilenameClickEvent: function() {
4574             var spec = qq.extend({}, this._filenameEditHandler());
4575
4576             return new qq.FilenameClickHandler(spec);
4577         },
4578
4579         _storeForLater: function(id) {
4580             this._parent.prototype._storeForLater.apply(this, arguments);
4581             this._templating.hideSpinner(id);
4582         },
4583
4584         _onSubmit: function(id, name) {
4585             this._parent.prototype._onSubmit.apply(this, arguments);
4586             this._addToList(id, name);
4587         },
4588
4589         // The file item has been added to the DOM.
4590         _onSubmitted: function(id) {
4591             // If the edit filename feature is enabled, mark the filename element as "editable" and the associated edit icon
4592             if (this._isEditFilenameEnabled()) {
4593                 this._templating.markFilenameEditable(id);
4594                 this._templating.showEditIcon(id);
4595
4596                 // If the focusin event is not supported, we must add a focus handler to the newly create edit filename text input
4597                 if (!this._focusinEventSupported) {
4598                     this._filenameInputFocusHandler.addHandler(this._templating.getEditInput(id));
4599                 }
4600             }
4601         },
4602
4603         // Update the progress bar & percentage as the file is uploaded
4604         _onProgress: function(id, name, loaded, total){
4605             this._parent.prototype._onProgress.apply(this, arguments);
4606
4607             this._templating.updateProgress(id, loaded, total);
4608
4609             if (loaded === total) {
4610                 this._templating.hideCancel(id);
4611                 this._templating.hidePause(id);
4612
4613                 this._templating.setStatusText(id, this._options.text.waitingForResponse);
4614
4615                 // If last byte was sent, display total file size
4616                 this._displayFileSize(id);
4617             }
4618             else {
4619                 // If still uploading, display percentage - total size is actually the total request(s) size
4620                 this._displayFileSize(id, loaded, total);
4621             }
4622         },
4623
4624         _onComplete: function(id, name, result, xhr) {
4625             var parentRetVal = this._parent.prototype._onComplete.apply(this, arguments),
4626                 templating = this._templating,
4627                 self = this;
4628
4629             function completeUpload(result) {
4630                 templating.setStatusText(id);
4631
4632                 qq(templating.getFileContainer(id)).removeClass(self._classes.retrying);
4633                 templating.hideProgress(id);
4634
4635                 if (!self._options.disableCancelForFormUploads || qq.supportedFeatures.ajaxUploading) {
4636                     templating.hideCancel(id);
4637                 }
4638                 templating.hideSpinner(id);
4639
4640                 if (result.success) {
4641                     self._markFileAsSuccessful(id);
4642                 }
4643                 else {
4644                     qq(templating.getFileContainer(id)).addClass(self._classes.fail);
4645
4646                     if (self._templating.isRetryPossible() && !self._preventRetries[id]) {
4647                         qq(templating.getFileContainer(id)).addClass(self._classes.retryable);
4648                     }
4649                     self._controlFailureTextDisplay(id, result);
4650                 }
4651             }
4652
4653             // The parent may need to perform some async operation before we can accurately determine the status of the upload.
4654             if (parentRetVal instanceof qq.Promise) {
4655                 parentRetVal.done(function(newResult) {
4656                     completeUpload(newResult);
4657                 });
4658
4659             }
4660             else {
4661                 completeUpload(result);
4662             }
4663
4664             return parentRetVal;
4665         },
4666
4667         _markFileAsSuccessful: function(id) {
4668             var templating = this._templating;
4669
4670             if (this._isDeletePossible()) {
4671                 templating.showDeleteButton(id);
4672             }
4673
4674             qq(templating.getFileContainer(id)).addClass(this._classes.success);
4675
4676             this._maybeUpdateThumbnail(id);
4677         },
4678
4679         _onUpload: function(id, name){
4680             var parentRetVal = this._parent.prototype._onUpload.apply(this, arguments);
4681
4682             this._templating.showSpinner(id);
4683
4684             return parentRetVal;
4685         },
4686
4687         _onUploadChunk: function(id, chunkData) {
4688             this._parent.prototype._onUploadChunk.apply(this, arguments);
4689
4690             // Only display the pause button if we have finished uploading at least one chunk
4691             chunkData.partIndex > 0 && this._templating.allowPause(id);
4692         },
4693
4694         _onCancel: function(id, name) {
4695             this._parent.prototype._onCancel.apply(this, arguments);
4696             this._removeFileItem(id);
4697         },
4698
4699         _onBeforeAutoRetry: function(id) {
4700             var retryNumForDisplay, maxAuto, retryNote;
4701
4702             this._parent.prototype._onBeforeAutoRetry.apply(this, arguments);
4703
4704             this._showCancelLink(id);
4705
4706             if (this._options.retry.showAutoRetryNote) {
4707                 retryNumForDisplay = this._autoRetries[id] + 1;
4708                 maxAuto = this._options.retry.maxAutoAttempts;
4709
4710                 retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
4711                 retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
4712
4713                 this._templating.setStatusText(id, retryNote);
4714                 qq(this._templating.getFileContainer(id)).addClass(this._classes.retrying);
4715             }
4716         },
4717
4718         //return false if we should not attempt the requested retry
4719         _onBeforeManualRetry: function(id) {
4720             if (this._parent.prototype._onBeforeManualRetry.apply(this, arguments)) {
4721                 this._templating.resetProgress(id);
4722                 qq(this._templating.getFileContainer(id)).removeClass(this._classes.fail);
4723                 this._templating.setStatusText(id);
4724                 this._templating.showSpinner(id);
4725                 this._showCancelLink(id);
4726                 return true;
4727             }
4728             else {
4729                 qq(this._templating.getFileContainer(id)).addClass(this._classes.retryable);
4730                 return false;
4731             }
4732         },
4733
4734         _onSubmitDelete: function(id) {
4735             var onSuccessCallback = qq.bind(this._onSubmitDeleteSuccess, this);
4736
4737             this._parent.prototype._onSubmitDelete.call(this, id, onSuccessCallback);
4738         },
4739
4740         _onSubmitDeleteSuccess: function(id, uuid, additionalMandatedParams) {
4741             if (this._options.deleteFile.forceConfirm) {
4742                 this._showDeleteConfirm.apply(this, arguments);
4743             }
4744             else {
4745                 this._sendDeleteRequest.apply(this, arguments);
4746             }
4747         },
4748
4749         _onDeleteComplete: function(id, xhr, isError) {
4750             this._parent.prototype._onDeleteComplete.apply(this, arguments);
4751
4752             this._templating.hideSpinner(id);
4753
4754             if (isError) {
4755                 this._templating.setStatusText(id, this._options.deleteFile.deletingFailedText);
4756                 this._templating.showDeleteButton(id);
4757             }
4758             else {
4759                 this._removeFileItem(id);
4760             }
4761         },
4762
4763         _sendDeleteRequest: function(id, uuid, additionalMandatedParams) {
4764             this._templating.hideDeleteButton(id);
4765             this._templating.showSpinner(id);
4766             this._templating.setStatusText(id, this._options.deleteFile.deletingStatusText);
4767             this._deleteHandler.sendDelete.apply(this, arguments);
4768         },
4769
4770         _showDeleteConfirm: function(id, uuid, mandatedParams) {
4771             /*jshint -W004 */
4772             var fileName = this.getName(id),
4773                 confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName),
4774                 uuid = this.getUuid(id),
4775                 deleteRequestArgs = arguments,
4776                 self = this,
4777                 retVal;
4778
4779             retVal = this._options.showConfirm(confirmMessage);
4780
4781             if (retVal instanceof qq.Promise) {
4782                 retVal.then(function () {
4783                     self._sendDeleteRequest.apply(self, deleteRequestArgs);
4784                 });
4785             }
4786             else if (retVal !== false) {
4787                 self._sendDeleteRequest.apply(self, deleteRequestArgs);
4788             }
4789         },
4790
4791         _addToList: function(id, name, canned) {
4792             var prependData,
4793                 prependIndex = 0;
4794
4795             if (this._options.display.prependFiles) {
4796                 if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) {
4797                     prependIndex = this._filesInBatchAddedToUi - 1;
4798                 }
4799
4800                 prependData = {
4801                     index: prependIndex
4802                 };
4803             }
4804
4805             if (!canned) {
4806                 if (this._options.disableCancelForFormUploads && !qq.supportedFeatures.ajaxUploading) {
4807                     this._templating.disableCancel();
4808                 }
4809
4810                 if (!this._options.multiple) {
4811                     this._handler.cancelAll();
4812                     this._clearList();
4813                 }
4814             }
4815
4816             this._templating.addFile(id, this._options.formatFileName(name), prependData);
4817
4818             if (canned) {
4819                 this._thumbnailUrls[id] && this._templating.updateThumbnail(id, this._thumbnailUrls[id], true);
4820             }
4821             else {
4822                 this._templating.generatePreview(id, this.getFile(id));
4823             }
4824
4825             this._filesInBatchAddedToUi += 1;
4826
4827             if (this._options.display.fileSizeOnSubmit && qq.supportedFeatures.ajaxUploading) {
4828                 this._displayFileSize(id);
4829             }
4830         },
4831
4832         _clearList: function(){
4833             this._templating.clearFiles();
4834             this.clearStoredFiles();
4835         },
4836
4837         _displayFileSize: function(id, loadedSize, totalSize) {
4838             var size = this.getSize(id),
4839                 sizeForDisplay = this._formatSize(size);
4840
4841             if (size >= 0) {
4842                 if (loadedSize !== undefined && totalSize !== undefined) {
4843                     sizeForDisplay = this._formatProgress(loadedSize, totalSize);
4844                 }
4845
4846                 this._templating.updateSize(id, sizeForDisplay);
4847             }
4848         },
4849
4850         _formatProgress: function (uploadedSize, totalSize) {
4851             var message = this._options.text.formatProgress;
4852             function r(name, replacement) { message = message.replace(name, replacement); }
4853
4854             r("{percent}", Math.round(uploadedSize / totalSize * 100));
4855             r("{total_size}", this._formatSize(totalSize));
4856             return message;
4857         },
4858
4859         _controlFailureTextDisplay: function(id, response) {
4860             var mode, maxChars, responseProperty, failureReason, shortFailureReason;
4861
4862             mode = this._options.failedUploadTextDisplay.mode;
4863             maxChars = this._options.failedUploadTextDisplay.maxChars;
4864             responseProperty = this._options.failedUploadTextDisplay.responseProperty;
4865
4866             if (mode === "custom") {
4867                 failureReason = response[responseProperty];
4868                 if (failureReason) {
4869                     if (failureReason.length > maxChars) {
4870                         shortFailureReason = failureReason.substring(0, maxChars) + "...";
4871                     }
4872                 }
4873                 else {
4874                     failureReason = this._options.text.failUpload;
4875                     this.log("'" + responseProperty + "' is not a valid property on the server response.", "warn");
4876                 }
4877
4878                 this._templating.setStatusText(id, shortFailureReason || failureReason);
4879
4880                 if (this._options.failedUploadTextDisplay.enableTooltip) {
4881                     this._showTooltip(id, failureReason);
4882                 }
4883             }
4884             else if (mode === "default") {
4885                 this._templating.setStatusText(id, this._options.text.failUpload);
4886             }
4887             else if (mode !== "none") {
4888                 this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", "warn");
4889             }
4890         },
4891
4892         _showTooltip: function(id, text) {
4893             this._templating.getFileContainer(id).title = text;
4894         },
4895
4896         _showCancelLink: function(id) {
4897             if (!this._options.disableCancelForFormUploads || qq.supportedFeatures.ajaxUploading) {
4898                 this._templating.showCancel(id);
4899             }
4900         },
4901
4902         _itemError: function(code, name, item) {
4903             var message = this._parent.prototype._itemError.apply(this, arguments);
4904             this._options.showMessage(message);
4905         },
4906
4907         _batchError: function(message) {
4908             this._parent.prototype._batchError.apply(this, arguments);
4909             this._options.showMessage(message);
4910         },
4911
4912         _setupPastePrompt: function() {
4913             var self = this;
4914
4915             this._options.callbacks.onPasteReceived = function() {
4916                 var message = self._options.paste.namePromptMessage,
4917                     defaultVal = self._options.paste.defaultName;
4918
4919                 return self._options.showPrompt(message, defaultVal);
4920             };
4921         },
4922
4923         _fileOrBlobRejected: function(id, name) {
4924             this._totalFilesInBatch -= 1;
4925             this._parent.prototype._fileOrBlobRejected.apply(this, arguments);
4926         },
4927
4928         _prepareItemsForUpload: function(items, params, endpoint) {
4929             this._totalFilesInBatch = items.length;
4930             this._filesInBatchAddedToUi = 0;
4931             this._parent.prototype._prepareItemsForUpload.apply(this, arguments);
4932         },
4933
4934         _maybeUpdateThumbnail: function(fileId) {
4935             var thumbnailUrl = this._thumbnailUrls[fileId];
4936
4937             this._templating.updateThumbnail(fileId, thumbnailUrl);
4938         },
4939
4940         _addCannedFile: function(sessionData) {
4941             var id = this._parent.prototype._addCannedFile.apply(this, arguments);
4942
4943             this._addToList(id, this.getName(id), true);
4944             this._templating.hideSpinner(id);
4945             this._templating.hideCancel(id);
4946             this._markFileAsSuccessful(id);
4947
4948             return id;
4949         }
4950     };
4951 }());
4952
4953 /*globals qq */
4954 /**
4955  * This defines FineUploader mode, which is a default UI w/ drag & drop uploading.
4956  */
4957 qq.FineUploader = function(o, namespace) {
4958     "use strict";
4959
4960     // By default this should inherit instance data from FineUploaderBasic, but this can be overridden
4961     // if the (internal) caller defines a different parent.  The parent is also used by
4962     // the private and public API functions that need to delegate to a parent function.
4963     this._parent = namespace ? qq[namespace].FineUploaderBasic : qq.FineUploaderBasic;
4964     this._parent.apply(this, arguments);
4965
4966     // Options provided by FineUploader mode
4967     qq.extend(this._options, {
4968         element: null,
4969
4970         button: null,
4971
4972         listElement: null,
4973
4974         dragAndDrop: {
4975             extraDropzones: []
4976         },
4977
4978         text: {
4979             formatProgress: "{percent}% of {total_size}",
4980             failUpload: "Upload failed",
4981             waitingForResponse: "Processing...",
4982             paused: "Paused"
4983         },
4984
4985         template: "qq-template",
4986
4987         classes: {
4988             retrying: "qq-upload-retrying",
4989             retryable: "qq-upload-retryable",
4990             success: "qq-upload-success",
4991             fail: "qq-upload-fail",
4992             editable: "qq-editable",
4993             hide: "qq-hide",
4994             dropActive: "qq-upload-drop-area-active"
4995         },
4996
4997         failedUploadTextDisplay: {
4998             mode: "default", //default, custom, or none
4999             maxChars: 50,
5000             responseProperty: "error",
5001             enableTooltip: true
5002         },
5003
5004         messages: {
5005             tooManyFilesError: "You may only drop one file",
5006             unsupportedBrowser: "Unrecoverable error - this browser does not permit file uploading of any kind."
5007         },
5008
5009         retry: {
5010             showAutoRetryNote: true,
5011             autoRetryNote: "Retrying {retryNum}/{maxAuto}..."
5012         },
5013
5014         deleteFile: {
5015             forceConfirm: false,
5016             confirmMessage: "Are you sure you want to delete {filename}?",
5017             deletingStatusText: "Deleting...",
5018             deletingFailedText: "Delete failed"
5019
5020         },
5021
5022         display: {
5023             fileSizeOnSubmit: false,
5024             prependFiles: false
5025         },
5026
5027         paste: {
5028             promptForName: false,
5029             namePromptMessage: "Please name this image"
5030         },
5031
5032         thumbnails: {
5033             placeholders: {
5034                 waitUntilResponse: false,
5035                 notAvailablePath: null,
5036                 waitingPath: null
5037             }
5038         },
5039
5040         showMessage: function(message){
5041             setTimeout(function() {
5042                 window.alert(message);
5043             }, 0);
5044         },
5045
5046         showConfirm: function(message) {
5047             return window.confirm(message);
5048         },
5049
5050         showPrompt: function(message, defaultValue) {
5051             return window.prompt(message, defaultValue);
5052         }
5053     }, true);
5054
5055     // Replace any default options with user defined ones
5056     qq.extend(this._options, o, true);
5057
5058     this._templating = new qq.Templating({
5059         log: qq.bind(this.log, this),
5060         templateIdOrEl: this._options.template,
5061         containerEl: this._options.element,
5062         fileContainerEl: this._options.listElement,
5063         button: this._options.button,
5064         imageGenerator: this._imageGenerator,
5065         classes: {
5066             hide: this._options.classes.hide,
5067             editable: this._options.classes.editable
5068         },
5069         placeholders: {
5070             waitUntilUpdate: this._options.thumbnails.placeholders.waitUntilResponse,
5071             thumbnailNotAvailable: this._options.thumbnails.placeholders.notAvailablePath,
5072             waitingForThumbnail: this._options.thumbnails.placeholders.waitingPath
5073         },
5074         text: this._options.text
5075     });
5076
5077     if (!qq.supportedFeatures.uploading || (this._options.cors.expected && !qq.supportedFeatures.uploadCors)) {
5078         this._templating.renderFailure(this._options.messages.unsupportedBrowser);
5079     }
5080     else {
5081         this._wrapCallbacks();
5082
5083         this._templating.render();
5084
5085         this._classes = this._options.classes;
5086
5087         if (!this._options.button && this._templating.getButton()) {
5088             this._defaultButtonId = this._createUploadButton({element: this._templating.getButton()}).getButtonId();
5089         }
5090
5091         this._setupClickAndEditEventHandlers();
5092
5093         if (qq.DragAndDrop && qq.supportedFeatures.fileDrop) {
5094             this._dnd = this._setupDragAndDrop();
5095         }
5096
5097         if (this._options.paste.targetElement && this._options.paste.promptForName) {
5098             if (qq.PasteSupport) {
5099                 this._setupPastePrompt();
5100             }
5101             else {
5102                 qq.log("Paste support module not found.", "info");
5103             }
5104         }
5105
5106         this._totalFilesInBatch = 0;
5107         this._filesInBatchAddedToUi = 0;
5108     }
5109 };
5110
5111 // Inherit the base public & private API methods
5112 qq.extend(qq.FineUploader.prototype, qq.basePublicApi);
5113 qq.extend(qq.FineUploader.prototype, qq.basePrivateApi);
5114
5115 // Add the FineUploader/default UI public & private UI methods, which may override some base methods.
5116 qq.extend(qq.FineUploader.prototype, qq.uiPublicApi);
5117 qq.extend(qq.FineUploader.prototype, qq.uiPrivateApi);
5118
5119 /* globals qq */
5120 /* jshint -W065 */
5121 /**
5122  * Module responsible for rendering all Fine Uploader UI templates.  This module also asserts at least
5123  * a limited amount of control over the template elements after they are added to the DOM.
5124  * Wherever possible, this module asserts total control over template elements present in the DOM.
5125  *
5126  * @param spec Specification object used to control various templating behaviors
5127  * @constructor
5128  */
5129 qq.Templating = function(spec) {
5130     "use strict";
5131
5132     var FILE_ID_ATTR = "qq-file-id",
5133         FILE_CLASS_PREFIX = "qq-file-id-",
5134         THUMBNAIL_MAX_SIZE_ATTR = "qq-max-size",
5135         THUMBNAIL_SERVER_SCALE_ATTR = "qq-server-scale",
5136         // This variable is duplicated in the DnD module since it can function as a standalone as well
5137         HIDE_DROPZONE_ATTR = "qq-hide-dropzone",
5138         isCancelDisabled = false,
5139         thumbnailMaxSize = -1,
5140         options = {
5141             log: null,
5142             templateIdOrEl: "qq-template",
5143             containerEl: null,
5144             fileContainerEl: null,
5145             button: null,
5146             imageGenerator: null,
5147             classes: {
5148                 hide: "qq-hide",
5149                 editable: "qq-editable"
5150             },
5151             placeholders: {
5152                 waitUntilUpdate: false,
5153                 thumbnailNotAvailable: null,
5154                 waitingForThumbnail: null
5155             },
5156             text: {
5157                 paused: "Paused"
5158             }
5159         },
5160         selectorClasses = {
5161             button: "qq-upload-button-selector",
5162             drop: "qq-upload-drop-area-selector",
5163             list: "qq-upload-list-selector",
5164             progressBarContainer: "qq-progress-bar-container-selector",
5165             progressBar: "qq-progress-bar-selector",
5166             file: "qq-upload-file-selector",
5167             spinner: "qq-upload-spinner-selector",
5168             size: "qq-upload-size-selector",
5169             cancel: "qq-upload-cancel-selector",
5170             pause: "qq-upload-pause-selector",
5171             continueButton: "qq-upload-continue-selector",
5172             deleteButton: "qq-upload-delete-selector",
5173             retry: "qq-upload-retry-selector",
5174             statusText: "qq-upload-status-text-selector",
5175             editFilenameInput: "qq-edit-filename-selector",
5176             editNameIcon: "qq-edit-filename-icon-selector",
5177             dropProcessing: "qq-drop-processing-selector",
5178             dropProcessingSpinner: "qq-drop-processing-spinner-selector",
5179             thumbnail: "qq-thumbnail-selector"
5180         },
5181         previewGeneration = {},
5182         cachedThumbnailNotAvailableImg = new qq.Promise(),
5183         cachedWaitingForThumbnailImg = new qq.Promise(),
5184         log,
5185         isEditElementsExist,
5186         isRetryElementExist,
5187         templateHtml,
5188         container,
5189         fileList,
5190         showThumbnails,
5191         serverScale;
5192
5193     /**
5194      * Grabs the HTML from the script tag holding the template markup.  This function will also adjust
5195      * some internally-tracked state variables based on the contents of the template.
5196      * The template is filtered so that irrelevant elements (such as the drop zone if DnD is not supported)
5197      * are omitted from the DOM.  Useful errors will be thrown if the template cannot be parsed.
5198      *
5199      * @returns {{template: *, fileTemplate: *}} HTML for the top-level file items templates
5200      */
5201     function parseAndGetTemplate() {
5202         var scriptEl,
5203             scriptHtml,
5204             fileListNode,
5205             tempTemplateEl,
5206             fileListHtml,
5207             defaultButton,
5208             dropArea,
5209             thumbnail,
5210             dropProcessing;
5211
5212         log("Parsing template");
5213
5214         /*jshint -W116*/
5215         if (options.templateIdOrEl == null) {
5216             throw new Error("You MUST specify either a template element or ID!");
5217         }
5218
5219         // Grab the contents of the script tag holding the template.
5220         if (qq.isString(options.templateIdOrEl)) {
5221             scriptEl = document.getElementById(options.templateIdOrEl);
5222
5223             if (scriptEl === null) {
5224                 throw new Error(qq.format("Cannot find template script at ID '{}'!", options.templateIdOrEl));
5225             }
5226
5227             scriptHtml = scriptEl.innerHTML;
5228         }
5229         else {
5230             if (options.templateIdOrEl.innerHTML === undefined) {
5231                 throw new Error("You have specified an invalid value for the template option!  " +
5232                     "It must be an ID or an Element.");
5233             }
5234
5235             scriptHtml = options.templateIdOrEl.innerHTML;
5236         }
5237
5238         scriptHtml = qq.trimStr(scriptHtml);
5239         tempTemplateEl = document.createElement("div");
5240         tempTemplateEl.appendChild(qq.toElement(scriptHtml));
5241
5242         // Don't include the default template button in the DOM
5243         // if an alternate button container has been specified.
5244         if (options.button) {
5245             defaultButton = qq(tempTemplateEl).getByClass(selectorClasses.button)[0];
5246             if (defaultButton) {
5247                 qq(defaultButton).remove();
5248             }
5249         }
5250
5251         // Omit the drop processing element from the DOM if DnD is not supported by the UA,
5252         // or the drag and drop module is not found.
5253         // NOTE: We are consciously not removing the drop zone if the UA doesn't support DnD
5254         // to support layouts where the drop zone is also a container for visible elements,
5255         // such as the file list.
5256         if (!qq.DragAndDrop || !qq.supportedFeatures.fileDrop) {
5257             dropProcessing = qq(tempTemplateEl).getByClass(selectorClasses.dropProcessing)[0];
5258             if (dropProcessing) {
5259                 qq(dropProcessing).remove();
5260             }
5261
5262         }
5263
5264         dropArea = qq(tempTemplateEl).getByClass(selectorClasses.drop)[0];
5265
5266         // If DnD is not available then remove
5267         // it from the DOM as well.
5268         if (dropArea && !qq.DragAndDrop) {
5269             qq.log("DnD module unavailable.", "info");
5270             qq(dropArea).remove();
5271         }
5272
5273         // If there is a drop area defined in the template, and the current UA doesn't support DnD,
5274         // and the drop area is marked as "hide before enter", ensure it is hidden as the DnD module
5275         // will not do this (since we will not be loading the DnD module)
5276         if (dropArea && !qq.supportedFeatures.fileDrop &&
5277             qq(dropArea).hasAttribute(HIDE_DROPZONE_ATTR)) {
5278
5279             qq(dropArea).css({
5280                 display: "none"
5281             });
5282         }
5283
5284         // Ensure the `showThumbnails` flag is only set if the thumbnail element
5285         // is present in the template AND the current UA is capable of generating client-side previews.
5286         thumbnail = qq(tempTemplateEl).getByClass(selectorClasses.thumbnail)[0];
5287         if (!showThumbnails) {
5288             thumbnail && qq(thumbnail).remove();
5289         }
5290         else if (thumbnail) {
5291             thumbnailMaxSize = parseInt(thumbnail.getAttribute(THUMBNAIL_MAX_SIZE_ATTR));
5292             // Only enforce max size if the attr value is non-zero
5293             thumbnailMaxSize = thumbnailMaxSize > 0 ? thumbnailMaxSize : null;
5294
5295             serverScale = qq(thumbnail).hasAttribute(THUMBNAIL_SERVER_SCALE_ATTR);
5296         }
5297         showThumbnails = showThumbnails && thumbnail;
5298
5299         isEditElementsExist = qq(tempTemplateEl).getByClass(selectorClasses.editFilenameInput).length > 0;
5300         isRetryElementExist = qq(tempTemplateEl).getByClass(selectorClasses.retry).length > 0;
5301
5302         fileListNode = qq(tempTemplateEl).getByClass(selectorClasses.list)[0];
5303         /*jshint -W116*/
5304         if (fileListNode == null) {
5305             throw new Error("Could not find the file list container in the template!");
5306         }
5307
5308         fileListHtml = fileListNode.innerHTML;
5309         fileListNode.innerHTML = "";
5310
5311         log("Template parsing complete");
5312
5313         return {
5314             template: qq.trimStr(tempTemplateEl.innerHTML),
5315             fileTemplate: qq.trimStr(fileListHtml)
5316         };
5317     }
5318
5319     function getFile(id) {
5320         return qq(fileList).getByClass(FILE_CLASS_PREFIX + id)[0];
5321     }
5322
5323     function getTemplateEl(context, cssClass) {
5324         return qq(context).getByClass(cssClass)[0];
5325     }
5326
5327     function prependFile(el, index) {
5328         var parentEl = fileList,
5329             beforeEl = parentEl.firstChild;
5330
5331         if (index > 0) {
5332             beforeEl = qq(parentEl).children()[index].nextSibling;
5333
5334         }
5335
5336         parentEl.insertBefore(el, beforeEl);
5337     }
5338
5339     function getCancel(id) {
5340         return getTemplateEl(getFile(id), selectorClasses.cancel);
5341     }
5342
5343     function getPause(id) {
5344         return getTemplateEl(getFile(id), selectorClasses.pause);
5345     }
5346
5347     function getContinue(id) {
5348         return getTemplateEl(getFile(id), selectorClasses.continueButton);
5349     }
5350
5351     function getProgress(id) {
5352         return getTemplateEl(getFile(id), selectorClasses.progressBarContainer) ||
5353             getTemplateEl(getFile(id), selectorClasses.progressBar);
5354     }
5355
5356     function getSpinner(id) {
5357         return getTemplateEl(getFile(id), selectorClasses.spinner);
5358     }
5359
5360     function getEditIcon(id) {
5361         return getTemplateEl(getFile(id), selectorClasses.editNameIcon);
5362     }
5363
5364     function getSize(id) {
5365         return getTemplateEl(getFile(id), selectorClasses.size);
5366     }
5367
5368     function getDelete(id) {
5369         return getTemplateEl(getFile(id), selectorClasses.deleteButton);
5370     }
5371
5372     function getRetry(id) {
5373         return getTemplateEl(getFile(id), selectorClasses.retry);
5374     }
5375
5376     function getFilename(id) {
5377         return getTemplateEl(getFile(id), selectorClasses.file);
5378     }
5379
5380     function getDropProcessing() {
5381         return getTemplateEl(container, selectorClasses.dropProcessing);
5382     }
5383
5384     function getThumbnail(id) {
5385         return showThumbnails && getTemplateEl(getFile(id), selectorClasses.thumbnail);
5386     }
5387
5388     function hide(el) {
5389         el && qq(el).addClass(options.classes.hide);
5390     }
5391
5392     function show(el) {
5393         el && qq(el).removeClass(options.classes.hide);
5394     }
5395
5396     function setProgressBarWidth(id, percent) {
5397         var bar = getProgress(id);
5398
5399         if (bar && !qq(bar).hasClass(selectorClasses.progressBar)) {
5400             bar = qq(bar).getByClass(selectorClasses.progressBar)[0];
5401         }
5402
5403         bar && qq(bar).css({width: percent + "%"});
5404     }
5405
5406     // During initialization of the templating module we should cache any
5407     // placeholder images so we can quickly swap them into the file list on demand.
5408     // Any placeholder images that cannot be loaded/found are simply ignored.
5409     function cacheThumbnailPlaceholders() {
5410         var notAvailableUrl =  options.placeholders.thumbnailNotAvailable,
5411             waitingUrl = options.placeholders.waitingForThumbnail,
5412             spec = {
5413                 maxSize: thumbnailMaxSize,
5414                 scale: serverScale
5415             };
5416
5417         if (showThumbnails) {
5418             if (notAvailableUrl) {
5419                 options.imageGenerator.generate(notAvailableUrl, new Image(), spec).then(
5420                     function(updatedImg) {
5421                         cachedThumbnailNotAvailableImg.success(updatedImg);
5422                     },
5423                     function() {
5424                         cachedThumbnailNotAvailableImg.failure();
5425                         log("Problem loading 'not available' placeholder image at " + notAvailableUrl, "error");
5426                     }
5427                 );
5428             }
5429             else {
5430                 cachedThumbnailNotAvailableImg.failure();
5431             }
5432
5433             if (waitingUrl) {
5434                 options.imageGenerator.generate(waitingUrl, new Image(), spec).then(
5435                     function(updatedImg) {
5436                         cachedWaitingForThumbnailImg.success(updatedImg);
5437                     },
5438                     function() {
5439                         cachedWaitingForThumbnailImg.failure();
5440                         log("Problem loading 'waiting for thumbnail' placeholder image at " + waitingUrl, "error");
5441                     }
5442                 );
5443             }
5444             else {
5445                 cachedWaitingForThumbnailImg.failure();
5446             }
5447         }
5448     }
5449
5450     // Displays a "waiting for thumbnail" type placeholder image
5451     // iff we were able to load it during initialization of the templating module.
5452     function displayWaitingImg(thumbnail) {
5453         cachedWaitingForThumbnailImg.then(function(img) {
5454             maybeScalePlaceholderViaCss(img, thumbnail);
5455             /* jshint eqnull:true */
5456             if (thumbnail.getAttribute("src") == null) {
5457                 thumbnail.src = img.src;
5458                 show(thumbnail);
5459             }
5460         }, function() {
5461             // In some browsers (such as IE9 and older) an img w/out a src attribute
5462             // are displayed as "broken" images, so we sohuld just hide the img tag
5463             // if we aren't going to display the "waiting" placeholder.
5464             hide(thumbnail);
5465         });
5466     }
5467
5468     // Displays a "thumbnail not available" type placeholder image
5469     // iff we were able to load this placeholder during initialization
5470     // of the templating module or after preview generation has failed.
5471     function maybeSetDisplayNotAvailableImg(id, thumbnail) {
5472         var previewing = previewGeneration[id] || new qq.Promise().failure();
5473
5474         cachedThumbnailNotAvailableImg.then(function(img) {
5475             previewing.then(
5476                 function() {
5477                     delete previewGeneration[id];
5478                 },
5479                 function() {
5480                     maybeScalePlaceholderViaCss(img, thumbnail);
5481                     thumbnail.src = img.src;
5482                     show(thumbnail);
5483                     delete previewGeneration[id];
5484                 }
5485             );
5486         });
5487     }
5488
5489     // Ensures a placeholder image does not exceed any max size specified
5490     // via `style` attribute properties iff <canvas> was not used to scale
5491     // the placeholder AND the target <img> doesn't already have these `style` attribute properties set.
5492     function maybeScalePlaceholderViaCss(placeholder, thumbnail) {
5493         var maxWidth = placeholder.style.maxWidth,
5494             maxHeight = placeholder.style.maxHeight;
5495
5496         if (maxHeight && maxWidth && !thumbnail.style.maxWidth && !thumbnail.style.maxHeight) {
5497             qq(thumbnail).css({
5498                 maxWidth: maxWidth,
5499                 maxHeight: maxHeight
5500             });
5501         }
5502     }
5503
5504
5505     qq.extend(options, spec);
5506     log = options.log;
5507
5508     container = options.containerEl;
5509     showThumbnails = options.imageGenerator !== undefined;
5510     templateHtml = parseAndGetTemplate();
5511
5512     cacheThumbnailPlaceholders();
5513
5514     qq.extend(this, {
5515         render: function() {
5516             log("Rendering template in DOM.");
5517
5518             container.innerHTML = templateHtml.template;
5519             hide(getDropProcessing());
5520             fileList = options.fileContainerEl || getTemplateEl(container, selectorClasses.list);
5521
5522             log("Template rendering complete");
5523         },
5524
5525         renderFailure: function(message) {
5526             var cantRenderEl = qq.toElement(message);
5527             container.innerHTML = "";
5528             container.appendChild(cantRenderEl);
5529         },
5530
5531         reset: function() {
5532             this.render();
5533         },
5534
5535         clearFiles: function() {
5536             fileList.innerHTML = "";
5537         },
5538
5539         disableCancel: function() {
5540             isCancelDisabled = true;
5541         },
5542
5543         addFile: function(id, name, prependInfo) {
5544             var fileEl = qq.toElement(templateHtml.fileTemplate),
5545                 fileNameEl = getTemplateEl(fileEl, selectorClasses.file);
5546
5547             qq(fileEl).addClass(FILE_CLASS_PREFIX + id);
5548             fileNameEl && qq(fileNameEl).setText(name);
5549             fileEl.setAttribute(FILE_ID_ATTR, id);
5550
5551             if (prependInfo) {
5552                 prependFile(fileEl, prependInfo.index);
5553             }
5554             else {
5555                 fileList.appendChild(fileEl);
5556             }
5557
5558             hide(getProgress(id));
5559             hide(getSize(id));
5560             hide(getDelete(id));
5561             hide(getRetry(id));
5562             hide(getPause(id));
5563             hide(getContinue(id));
5564
5565             if (isCancelDisabled) {
5566                 this.hideCancel(id);
5567             }
5568         },
5569
5570         removeFile: function(id) {
5571             qq(getFile(id)).remove();
5572         },
5573
5574         getFileId: function(el) {
5575             var currentNode = el;
5576
5577             if (currentNode) {
5578                 /*jshint -W116*/
5579                 while (currentNode.getAttribute(FILE_ID_ATTR) == null) {
5580                     currentNode = currentNode.parentNode;
5581                 }
5582
5583                 return parseInt(currentNode.getAttribute(FILE_ID_ATTR));
5584             }
5585         },
5586
5587         getFileList: function() {
5588             return fileList;
5589         },
5590
5591         markFilenameEditable: function(id) {
5592             var filename = getFilename(id);
5593
5594             filename && qq(filename).addClass(options.classes.editable);
5595         },
5596
5597         updateFilename: function(id, name) {
5598             var filename = getFilename(id);
5599
5600             filename && qq(filename).setText(name);
5601         },
5602
5603         hideFilename: function(id) {
5604             hide(getFilename(id));
5605         },
5606
5607         showFilename: function(id) {
5608             show(getFilename(id));
5609         },
5610
5611         isFileName: function(el) {
5612             return qq(el).hasClass(selectorClasses.file);
5613         },
5614
5615         getButton: function() {
5616             return options.button || getTemplateEl(container, selectorClasses.button);
5617         },
5618
5619         hideDropProcessing: function() {
5620             hide(getDropProcessing());
5621         },
5622
5623         showDropProcessing: function() {
5624             show(getDropProcessing());
5625         },
5626
5627         getDropZone: function() {
5628             return getTemplateEl(container, selectorClasses.drop);
5629         },
5630
5631         isEditFilenamePossible: function() {
5632             return isEditElementsExist;
5633         },
5634
5635         isRetryPossible: function() {
5636             return isRetryElementExist;
5637         },
5638
5639         getFileContainer: function(id) {
5640             return getFile(id);
5641         },
5642
5643         showEditIcon: function(id) {
5644             var icon = getEditIcon(id);
5645
5646             icon && qq(icon).addClass(options.classes.editable);
5647         },
5648
5649         hideEditIcon: function(id) {
5650             var icon = getEditIcon(id);
5651
5652             icon && qq(icon).removeClass(options.classes.editable);
5653         },
5654
5655         isEditIcon: function(el) {
5656             return qq(el).hasClass(selectorClasses.editNameIcon);
5657         },
5658
5659         getEditInput: function(id) {
5660             return getTemplateEl(getFile(id), selectorClasses.editFilenameInput);
5661         },
5662
5663         isEditInput: function(el) {
5664             return qq(el).hasClass(selectorClasses.editFilenameInput);
5665         },
5666
5667         updateProgress: function(id, loaded, total) {
5668             var bar = getProgress(id),
5669                 percent;
5670
5671             if (bar) {
5672                 percent = Math.round(loaded / total * 100);
5673
5674                 if (loaded === total) {
5675                     hide(bar);
5676                 }
5677                 else {
5678                     show(bar);
5679                 }
5680
5681                 setProgressBarWidth(id, percent);
5682             }
5683         },
5684
5685         hideProgress: function(id) {
5686             var bar = getProgress(id);
5687
5688             bar && hide(bar);
5689         },
5690
5691         resetProgress: function(id) {
5692             setProgressBarWidth(id, 0);
5693         },
5694
5695         showCancel: function(id) {
5696             if (!isCancelDisabled) {
5697                 var cancel = getCancel(id);
5698
5699                 cancel && qq(cancel).removeClass(options.classes.hide);
5700             }
5701         },
5702
5703         hideCancel: function(id) {
5704             hide(getCancel(id));
5705         },
5706
5707         isCancel: function(el)  {
5708             return qq(el).hasClass(selectorClasses.cancel);
5709         },
5710
5711         allowPause: function(id) {
5712             show(getPause(id));
5713             hide(getContinue(id));
5714         },
5715
5716         uploadPaused: function(id) {
5717             this.setStatusText(id, options.text.paused);
5718             this.allowContinueButton(id);
5719             hide(getSpinner(id));
5720         },
5721
5722         hidePause: function(id) {
5723             hide(getPause(id));
5724         },
5725
5726         isPause: function(el) {
5727             return qq(el).hasClass(selectorClasses.pause);
5728         },
5729
5730         isContinueButton: function(el) {
5731             return qq(el).hasClass(selectorClasses.continueButton);
5732         },
5733
5734         allowContinueButton: function(id) {
5735             show(getContinue(id));
5736             hide(getPause(id));
5737         },
5738
5739         uploadContinued: function(id) {
5740             this.setStatusText(id, "");
5741             this.allowPause(id);
5742             show(getSpinner(id));
5743         },
5744
5745         showDeleteButton: function(id) {
5746             show(getDelete(id));
5747         },
5748
5749         hideDeleteButton: function(id) {
5750             hide(getDelete(id));
5751         },
5752
5753         isDeleteButton: function(el) {
5754             return qq(el).hasClass(selectorClasses.deleteButton);
5755         },
5756
5757         isRetry: function(el) {
5758             return qq(el).hasClass(selectorClasses.retry);
5759         },
5760
5761         updateSize: function(id, text) {
5762             var size = getSize(id);
5763
5764             if (size) {
5765                 show(size);
5766                 qq(size).setText(text);
5767             }
5768         },
5769
5770         setStatusText: function(id, text) {
5771             var textEl = getTemplateEl(getFile(id), selectorClasses.statusText);
5772
5773             if (textEl) {
5774                 /*jshint -W116*/
5775                 if (text == null) {
5776                     qq(textEl).clearText();
5777                 }
5778                 else {
5779                     qq(textEl).setText(text);
5780                 }
5781             }
5782         },
5783
5784         hideSpinner: function(id) {
5785             hide(getSpinner(id));
5786         },
5787
5788         showSpinner: function(id) {
5789             show(getSpinner(id));
5790         },
5791
5792         generatePreview: function(id, fileOrBlob) {
5793             var thumbnail = getThumbnail(id),
5794                 spec = {
5795                     maxSize: thumbnailMaxSize,
5796                     scale: true,
5797                     orient: true
5798                 };
5799
5800             if (qq.supportedFeatures.imagePreviews) {
5801                 previewGeneration[id] = new qq.Promise();
5802
5803                 if (thumbnail) {
5804                     displayWaitingImg(thumbnail);
5805                     return options.imageGenerator.generate(fileOrBlob, thumbnail, spec).then(
5806                         function() {
5807                             show(thumbnail);
5808                             previewGeneration[id].success();
5809                         },
5810                         function() {
5811                             previewGeneration[id].failure();
5812
5813                             // Display the "not available" placeholder img only if we are
5814                             // not expecting a thumbnail at a later point, such as in a server response.
5815                             if (!options.placeholders.waitUntilUpdate) {
5816                                 maybeSetDisplayNotAvailableImg(id, thumbnail);
5817                             }
5818                         });
5819                 }
5820             }
5821             else if (thumbnail) {
5822                 displayWaitingImg(thumbnail);
5823             }
5824         },
5825
5826         updateThumbnail: function(id, thumbnailUrl, showWaitingImg) {
5827             var thumbnail = getThumbnail(id),
5828                 spec = {
5829                     maxSize: thumbnailMaxSize,
5830                     scale: serverScale
5831                 };
5832
5833             if (thumbnail) {
5834                 if (thumbnailUrl) {
5835                     if (showWaitingImg) {
5836                         displayWaitingImg(thumbnail);
5837                     }
5838
5839                     return options.imageGenerator.generate(thumbnailUrl, thumbnail, spec).then(
5840                         function() {
5841                             show(thumbnail);
5842                         },
5843                         function() {
5844                             maybeSetDisplayNotAvailableImg(id, thumbnail);
5845                         }
5846                     );
5847                 }
5848                 else {
5849                     maybeSetDisplayNotAvailableImg(id, thumbnail);
5850                 }
5851             }
5852         }
5853     });
5854 };
5855
5856 /*globals qq*/
5857 /**
5858  * Upload handler used that assumes the current user agent does not have any support for the
5859  * File API, and, therefore, makes use of iframes and forms to submit the files directly to
5860  * a generic server.
5861  *
5862  * @param options Options passed from the base handler
5863  * @param proxy Callbacks & methods used to query for or push out data/changes
5864  */
5865 qq.UploadHandlerForm = function(options, proxy) {
5866     "use strict";
5867
5868     var fileState = [],
5869         uploadCompleteCallback = proxy.onUploadComplete,
5870         onUuidChanged = proxy.onUuidChanged,
5871         getName = proxy.getName,
5872         getUuid = proxy.getUuid,
5873         uploadComplete = uploadCompleteCallback,
5874         log = proxy.log,
5875         internalApi = {};
5876
5877
5878     /**
5879      * Returns json object received by iframe from server.
5880      */
5881     function getIframeContentJson(id, iframe) {
5882         /*jshint evil: true*/
5883
5884         var response;
5885
5886         //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
5887         try {
5888             // iframe.contentWindow.document - for IE<7
5889             var doc = iframe.contentDocument || iframe.contentWindow.document,
5890                 innerHtml = doc.body.innerHTML;
5891
5892             log("converting iframe's innerHTML to JSON");
5893             log("innerHTML = " + innerHtml);
5894             //plain text response may be wrapped in <pre> tag
5895             if (innerHtml && innerHtml.match(/^<pre/i)) {
5896                 innerHtml = doc.body.firstChild.firstChild.nodeValue;
5897             }
5898
5899             response = internalApi.parseJsonResponse(id, innerHtml);
5900         }
5901         catch(error) {
5902             log("Error when attempting to parse form upload response (" + error.message + ")", "error");
5903             response = {success: false};
5904         }
5905
5906         return response;
5907     }
5908
5909     /**
5910      * Creates form, that will be submitted to iframe
5911      */
5912     function createForm(id, iframe){
5913         var params = options.paramsStore.getParams(id),
5914             method = options.demoMode ? "GET" : "POST",
5915             endpoint = options.endpointStore.getEndpoint(id),
5916             name = getName(id);
5917
5918         params[options.uuidName] = getUuid(id);
5919         params[options.filenameParam] = name;
5920
5921         return internalApi.initFormForUpload({
5922             method: method,
5923             endpoint: endpoint,
5924             params: params,
5925             paramsInBody: options.paramsInBody,
5926             targetName: iframe.name
5927         });
5928     }
5929
5930     qq.extend(this, new qq.UploadHandlerFormApi(internalApi,
5931         {fileState: fileState, isCors: options.cors.expected, inputName: options.inputName},
5932         {onCancel: options.onCancel, onUuidChanged: onUuidChanged, getName: getName, getUuid: getUuid, log: log}));
5933
5934     qq.extend(this, {
5935         upload: function(id) {
5936             var input = fileState[id].input,
5937                 fileName = getName(id),
5938                 iframe = internalApi.createIframe(id),
5939                 form;
5940
5941             if (!input){
5942                 throw new Error("file with passed id was not added, or already uploaded or canceled");
5943             }
5944
5945             options.onUpload(id, getName(id));
5946
5947             form = createForm(id, iframe);
5948             form.appendChild(input);
5949
5950             internalApi.attachLoadEvent(iframe, function(responseFromMessage){
5951                 log("iframe loaded");
5952
5953                 var response = responseFromMessage ? responseFromMessage : getIframeContentJson(id, iframe);
5954
5955                 internalApi.detachLoadEvent(id);
5956
5957                 //we can't remove an iframe if the iframe doesn't belong to the same domain
5958                 if (!options.cors.expected) {
5959                     qq(iframe).remove();
5960                 }
5961
5962                 if (!response.success) {
5963                     if (options.onAutoRetry(id, fileName, response)) {
5964                         return;
5965                     }
5966                 }
5967                 options.onComplete(id, fileName, response);
5968                 uploadComplete(id);
5969             });
5970
5971             log("Sending upload request for " + id);
5972             form.submit();
5973             qq(form).remove();
5974         }
5975     });
5976 };
5977
5978 /*globals qq*/
5979 /**
5980  * Upload handler used to upload to traditional endpoints.  It depends on File API support, and, therefore,
5981  * makes use of `XMLHttpRequest` level 2 to upload `File`s and `Blob`s to a generic server.
5982  *
5983  * @param spec Options passed from the base handler
5984  * @param proxy Callbacks & methods used to query for or push out data/changes
5985  */
5986 qq.UploadHandlerXhr = function(spec, proxy) {
5987     "use strict";
5988
5989     var uploadComplete = proxy.onUploadComplete,
5990         onUuidChanged = proxy.onUuidChanged,
5991         getName = proxy.getName,
5992         getUuid = proxy.getUuid,
5993         getSize = proxy.getSize,
5994         log = proxy.log,
5995         fileState = [],
5996         cookieItemDelimiter = "|",
5997         chunkFiles = spec.chunking.enabled && qq.supportedFeatures.chunking,
5998         resumeEnabled = spec.resume.enabled && chunkFiles && qq.supportedFeatures.resume,
5999         multipart = spec.forceMultipart || spec.paramsInBody,
6000         internalApi = {},
6001         publicApi = this,
6002         resumeId;
6003
6004     function getResumeId() {
6005         if (spec.resume.id !== null &&
6006             spec.resume.id !== undefined &&
6007             !qq.isFunction(spec.resume.id) &&
6008             !qq.isObject(spec.resume.id)) {
6009
6010             return spec.resume.id;
6011         }
6012     }
6013
6014     resumeId = getResumeId();
6015
6016     function addChunkingSpecificParams(id, params, chunkData) {
6017         var size = getSize(id),
6018             name = getName(id);
6019
6020         params[spec.chunking.paramNames.partIndex] = chunkData.part;
6021         params[spec.chunking.paramNames.partByteOffset] = chunkData.start;
6022         params[spec.chunking.paramNames.chunkSize] = chunkData.size;
6023         params[spec.chunking.paramNames.totalParts] = chunkData.count;
6024         params[spec.totalFileSizeName] = size;
6025
6026         /**
6027          * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
6028          * or an empty string.  So, we will need to include the actual file name as a param in this case.
6029          */
6030         if (multipart) {
6031             params[spec.filenameParam] = name;
6032         }
6033     }
6034
6035     function addResumeSpecificParams(params) {
6036         params[spec.resume.paramNames.resuming] = true;
6037     }
6038
6039     function getChunk(fileOrBlob, startByte, endByte) {
6040         if (fileOrBlob.slice) {
6041             return fileOrBlob.slice(startByte, endByte);
6042         }
6043         else if (fileOrBlob.mozSlice) {
6044             return fileOrBlob.mozSlice(startByte, endByte);
6045         }
6046         else if (fileOrBlob.webkitSlice) {
6047             return fileOrBlob.webkitSlice(startByte, endByte);
6048         }
6049     }
6050
6051     function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
6052         var formData = new FormData(),
6053             method = spec.demoMode ? "GET" : "POST",
6054             endpoint = spec.endpointStore.getEndpoint(id),
6055             url = endpoint,
6056             name = getName(id),
6057             size = getSize(id);
6058
6059         params[spec.uuidName] = getUuid(id);
6060         params[spec.filenameParam] = name;
6061
6062
6063         if (multipart) {
6064             params[spec.totalFileSizeName] = size;
6065         }
6066
6067         //build query string
6068         if (!spec.paramsInBody) {
6069             if (!multipart) {
6070                 params[spec.inputName] = name;
6071             }
6072             url = qq.obj2url(params, endpoint);
6073         }
6074
6075         xhr.open(method, url, true);
6076
6077         if (spec.cors.expected && spec.cors.sendCredentials) {
6078             xhr.withCredentials = true;
6079         }
6080
6081         if (multipart) {
6082             if (spec.paramsInBody) {
6083                 qq.obj2FormData(params, formData);
6084             }
6085
6086             formData.append(spec.inputName, fileOrBlob);
6087             return formData;
6088         }
6089
6090         return fileOrBlob;
6091     }
6092
6093     function setHeaders(id, xhr) {
6094         var extraHeaders = spec.customHeaders,
6095             fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
6096
6097         xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
6098         xhr.setRequestHeader("Cache-Control", "no-cache");
6099
6100         if (!multipart) {
6101             xhr.setRequestHeader("Content-Type", "application/octet-stream");
6102             //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
6103             xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
6104         }
6105
6106         qq.each(extraHeaders, function(name, val) {
6107             xhr.setRequestHeader(name, val);
6108         });
6109     }
6110
6111     function handleCompletedItem(id, response, xhr) {
6112         var name = getName(id),
6113             size = getSize(id);
6114
6115         fileState[id].attemptingResume = false;
6116
6117         spec.onProgress(id, name, size, size);
6118         spec.onComplete(id, name, response, xhr);
6119
6120         if (fileState[id]) {
6121             delete fileState[id].xhr;
6122         }
6123
6124         uploadComplete(id);
6125     }
6126
6127     function uploadNextChunk(id) {
6128         var chunkIdx = fileState[id].remainingChunkIdxs[0],
6129             chunkData = internalApi.getChunkData(id, chunkIdx),
6130             xhr = internalApi.createXhr(id),
6131             size = getSize(id),
6132             name = getName(id),
6133             toSend, params;
6134
6135         if (fileState[id].loaded === undefined) {
6136             fileState[id].loaded = 0;
6137         }
6138
6139         if (resumeEnabled && fileState[id].file) {
6140             persistChunkData(id, chunkData);
6141         }
6142
6143         xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
6144
6145         xhr.upload.onprogress = function(e) {
6146             if (e.lengthComputable) {
6147                 var totalLoaded = e.loaded + fileState[id].loaded,
6148                     estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
6149
6150                 spec.onProgress(id, name, totalLoaded, estTotalRequestsSize);
6151             }
6152         };
6153
6154         spec.onUploadChunk(id, name, internalApi.getChunkDataForCallback(chunkData));
6155
6156         params = spec.paramsStore.getParams(id);
6157         addChunkingSpecificParams(id, params, chunkData);
6158
6159         if (fileState[id].attemptingResume) {
6160             addResumeSpecificParams(params);
6161         }
6162
6163         toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
6164         setHeaders(id, xhr);
6165
6166         log("Sending chunked upload request for item " + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
6167         xhr.send(toSend);
6168     }
6169
6170     function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
6171         var chunkData = internalApi.getChunkData(id, chunkIdx),
6172             blobSize = chunkData.size,
6173             overhead = requestSize - blobSize,
6174             size = getSize(id),
6175             chunkCount = chunkData.count,
6176             initialRequestOverhead = fileState[id].initialRequestOverhead,
6177             overheadDiff = overhead - initialRequestOverhead;
6178
6179         fileState[id].lastRequestOverhead = overhead;
6180
6181         if (chunkIdx === 0) {
6182             fileState[id].lastChunkIdxProgress = 0;
6183             fileState[id].initialRequestOverhead = overhead;
6184             fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
6185         }
6186         else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
6187             fileState[id].lastChunkIdxProgress = chunkIdx;
6188             fileState[id].estTotalRequestsSize += overheadDiff;
6189         }
6190
6191         return fileState[id].estTotalRequestsSize;
6192     }
6193
6194     function getLastRequestOverhead(id) {
6195         if (multipart) {
6196             return fileState[id].lastRequestOverhead;
6197         }
6198         else {
6199             return 0;
6200         }
6201     }
6202
6203     function handleSuccessfullyCompletedChunk(id, response, xhr) {
6204         var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
6205             chunkData = internalApi.getChunkData(id, chunkIdx);
6206
6207         fileState[id].attemptingResume = false;
6208         fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
6209
6210         spec.onUploadChunkSuccess(id, internalApi.getChunkDataForCallback(chunkData), response, xhr);
6211
6212         if (fileState[id].remainingChunkIdxs.length > 0) {
6213             uploadNextChunk(id);
6214         }
6215         else {
6216             if (resumeEnabled) {
6217                 deletePersistedChunkData(id);
6218             }
6219
6220             handleCompletedItem(id, response, xhr);
6221         }
6222     }
6223
6224     function isErrorResponse(xhr, response) {
6225         return xhr.status !== 200 || !response.success || response.reset;
6226     }
6227
6228     function parseResponse(id, xhr) {
6229         var response;
6230
6231         try {
6232             log(qq.format("Received response status {} with body: {}", xhr.status, xhr.responseText));
6233
6234             response = qq.parseJson(xhr.responseText);
6235
6236             if (response.newUuid !== undefined) {
6237                 onUuidChanged(id, response.newUuid);
6238             }
6239         }
6240         catch(error) {
6241             log("Error when attempting to parse xhr response text (" + error.message + ")", "error");
6242             response = {};
6243         }
6244
6245         return response;
6246     }
6247
6248     function handleResetResponse(id) {
6249         log("Server has ordered chunking effort to be restarted on next attempt for item ID " + id, "error");
6250
6251         if (resumeEnabled) {
6252             deletePersistedChunkData(id);
6253             fileState[id].attemptingResume = false;
6254         }
6255
6256         fileState[id].remainingChunkIdxs = [];
6257         delete fileState[id].loaded;
6258         delete fileState[id].estTotalRequestsSize;
6259         delete fileState[id].initialRequestOverhead;
6260     }
6261
6262     function handleResetResponseOnResumeAttempt(id) {
6263         fileState[id].attemptingResume = false;
6264         log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", "error");
6265         handleResetResponse(id);
6266         publicApi.upload(id, true);
6267     }
6268
6269     function handleNonResetErrorResponse(id, response, xhr) {
6270         var name = getName(id);
6271
6272         if (spec.onAutoRetry(id, name, response, xhr)) {
6273             return;
6274         }
6275         else {
6276             handleCompletedItem(id, response, xhr);
6277         }
6278     }
6279
6280     function onComplete(id, xhr) {
6281         var state = fileState[id],
6282             attemptingResume = state && state.attemptingResume,
6283             paused = state && state.paused,
6284             response;
6285
6286         // The logic in this function targets uploads that have not been paused or canceled,
6287         // so return at once if this is not the case.
6288         if (!state || paused) {
6289             return;
6290         }
6291
6292         log("xhr - server response received for " + id);
6293         log("responseText = " + xhr.responseText);
6294         response = parseResponse(id, xhr);
6295
6296         if (isErrorResponse(xhr, response)) {
6297             if (response.reset) {
6298                 handleResetResponse(id);
6299             }
6300
6301             if (attemptingResume && response.reset) {
6302                 handleResetResponseOnResumeAttempt(id);
6303             }
6304             else {
6305                 handleNonResetErrorResponse(id, response, xhr);
6306             }
6307         }
6308         else if (chunkFiles) {
6309             handleSuccessfullyCompletedChunk(id, response, xhr);
6310         }
6311         else {
6312             handleCompletedItem(id, response, xhr);
6313         }
6314     }
6315
6316     function getReadyStateChangeHandler(id, xhr) {
6317         return function() {
6318             if (xhr.readyState === 4) {
6319                 onComplete(id, xhr);
6320             }
6321         };
6322     }
6323
6324     function persistChunkData(id, chunkData) {
6325         var fileUuid = getUuid(id),
6326             lastByteSent = fileState[id].loaded,
6327             initialRequestOverhead = fileState[id].initialRequestOverhead,
6328             estTotalRequestsSize = fileState[id].estTotalRequestsSize,
6329             cookieName = getChunkDataCookieName(id),
6330             cookieValue = fileUuid +
6331                 cookieItemDelimiter + chunkData.part +
6332                 cookieItemDelimiter + lastByteSent +
6333                 cookieItemDelimiter + initialRequestOverhead +
6334                 cookieItemDelimiter + estTotalRequestsSize,
6335             cookieExpDays = spec.resume.cookiesExpireIn;
6336
6337         qq.setCookie(cookieName, cookieValue, cookieExpDays);
6338     }
6339
6340     function deletePersistedChunkData(id) {
6341         if (fileState[id].file) {
6342             var cookieName = getChunkDataCookieName(id);
6343             qq.deleteCookie(cookieName);
6344         }
6345     }
6346
6347     function getPersistedChunkData(id) {
6348         var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
6349             filename = getName(id),
6350             sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
6351
6352         if (chunkCookieValue) {
6353             sections = chunkCookieValue.split(cookieItemDelimiter);
6354
6355             if (sections.length === 5) {
6356                 uuid = sections[0];
6357                 partIndex = parseInt(sections[1], 10);
6358                 lastByteSent = parseInt(sections[2], 10);
6359                 initialRequestOverhead = parseInt(sections[3], 10);
6360                 estTotalRequestsSize = parseInt(sections[4], 10);
6361
6362                 return {
6363                     uuid: uuid,
6364                     part: partIndex,
6365                     lastByteSent: lastByteSent,
6366                     initialRequestOverhead: initialRequestOverhead,
6367                     estTotalRequestsSize: estTotalRequestsSize
6368                 };
6369             }
6370             else {
6371                 log("Ignoring previously stored resume/chunk cookie for " + filename + " - old cookie format", "warn");
6372             }
6373         }
6374     }
6375
6376     function getChunkDataCookieName(id) {
6377         var filename = getName(id),
6378             fileSize = getSize(id),
6379             maxChunkSize = spec.chunking.partSize,
6380             cookieName;
6381
6382         cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
6383
6384         if (resumeId !== undefined) {
6385             cookieName += cookieItemDelimiter + resumeId;
6386         }
6387
6388         return cookieName;
6389     }
6390
6391     function calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex) {
6392         var currentChunkIndex;
6393
6394         for (currentChunkIndex = internalApi.getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
6395             fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
6396         }
6397
6398         uploadNextChunk(id);
6399     }
6400
6401     function onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume) {
6402         firstChunkIndex = persistedChunkInfoForResume.part;
6403         fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
6404         fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
6405         fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
6406         fileState[id].attemptingResume = true;
6407         log("Resuming " + name + " at partition index " + firstChunkIndex);
6408
6409         calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6410     }
6411
6412     function handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex) {
6413         var name = getName(id),
6414             firstChunkDataForResume = internalApi.getChunkData(id, persistedChunkInfoForResume.part),
6415             onResumeRetVal;
6416
6417         onResumeRetVal = spec.onResume(id, name, internalApi.getChunkDataForCallback(firstChunkDataForResume));
6418         if (onResumeRetVal instanceof qq.Promise) {
6419             log("Waiting for onResume promise to be fulfilled for " + id);
6420             onResumeRetVal.then(
6421                 function() {
6422                     onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume);
6423                 },
6424                 function() {
6425                     log("onResume promise fulfilled - failure indicated.  Will not resume.");
6426                     calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6427                 }
6428             );
6429         }
6430         else if (onResumeRetVal !== false) {
6431             onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume);
6432         }
6433         else {
6434             log("onResume callback returned false.  Will not resume.");
6435             calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6436         }
6437     }
6438
6439     function handleFileChunkingUpload(id, retry) {
6440         var firstChunkIndex = 0,
6441             persistedChunkInfoForResume;
6442
6443         if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
6444             fileState[id].remainingChunkIdxs = [];
6445
6446             if (resumeEnabled && !retry && fileState[id].file) {
6447                 persistedChunkInfoForResume = getPersistedChunkData(id);
6448                 if (persistedChunkInfoForResume) {
6449                     handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex);
6450                 }
6451                 else {
6452                     calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6453                 }
6454             }
6455             else {
6456                 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6457             }
6458         }
6459         else {
6460             uploadNextChunk(id);
6461         }
6462     }
6463
6464     function handleStandardFileUpload(id) {
6465         var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
6466             name = getName(id),
6467             xhr, params, toSend;
6468
6469         fileState[id].loaded = 0;
6470
6471         xhr = internalApi.createXhr(id);
6472
6473         xhr.upload.onprogress = function(e){
6474             if (e.lengthComputable){
6475                 fileState[id].loaded = e.loaded;
6476                 spec.onProgress(id, name, e.loaded, e.total);
6477             }
6478         };
6479
6480         xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
6481
6482         params = spec.paramsStore.getParams(id);
6483         toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
6484         setHeaders(id, xhr);
6485
6486         log("Sending upload request for " + id);
6487         xhr.send(toSend);
6488     }
6489
6490     function handleUploadSignal(id, retry) {
6491         var name = getName(id);
6492
6493         if (publicApi.isValid(id)) {
6494             spec.onUpload(id, name);
6495
6496             if (chunkFiles) {
6497                 handleFileChunkingUpload(id, retry);
6498             }
6499             else {
6500                 handleStandardFileUpload(id);
6501             }
6502         }
6503     }
6504
6505
6506     qq.extend(this, new qq.UploadHandlerXhrApi(
6507         internalApi,
6508         {fileState: fileState, chunking: chunkFiles ? spec.chunking : null},
6509         {onUpload: handleUploadSignal, onCancel: spec.onCancel, onUuidChanged: onUuidChanged, getName: getName,
6510             getSize: getSize, getUuid: getUuid, log: log}
6511     ));
6512
6513     // Base XHR API overrides
6514     qq.override(this, function(super_) {
6515         return {
6516             add: function(id, fileOrBlobData) {
6517                 var persistedChunkData;
6518
6519                 super_.add.apply(this, arguments);
6520
6521                 if (resumeEnabled) {
6522                     persistedChunkData = getPersistedChunkData(id);
6523
6524                     if (persistedChunkData) {
6525                         onUuidChanged(id, persistedChunkData.uuid);
6526                     }
6527                 }
6528
6529                 return id;
6530             },
6531
6532             getResumableFilesData: function() {
6533                 var matchingCookieNames = [],
6534                     resumableFilesData = [];
6535
6536                 if (chunkFiles && resumeEnabled) {
6537                     if (resumeId === undefined) {
6538                         matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
6539                             cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + spec.chunking.partSize + "="));
6540                     }
6541                     else {
6542                         matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
6543                             cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + spec.chunking.partSize + "\\" +
6544                             cookieItemDelimiter + resumeId + "="));
6545                     }
6546
6547                     qq.each(matchingCookieNames, function(idx, cookieName) {
6548                         var cookiesNameParts = cookieName.split(cookieItemDelimiter);
6549                         var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
6550
6551                         resumableFilesData.push({
6552                             name: decodeURIComponent(cookiesNameParts[1]),
6553                             size: cookiesNameParts[2],
6554                             uuid: cookieValueParts[0],
6555                             partIdx: cookieValueParts[1]
6556                         });
6557                     });
6558
6559                     return resumableFilesData;
6560                 }
6561                 return [];
6562             },
6563
6564             expunge: function(id) {
6565                 if (resumeEnabled) {
6566                     deletePersistedChunkData(id);
6567                 }
6568
6569                 super_.expunge(id);
6570             }
6571         };
6572     });
6573 };
6574
6575 /*globals qq*/
6576 qq.PasteSupport = function(o) {
6577     "use strict";
6578
6579     var options, detachPasteHandler;
6580
6581     options = {
6582         targetElement: null,
6583         callbacks: {
6584             log: function(message, level) {},
6585             pasteReceived: function(blob) {}
6586         }
6587     };
6588
6589     function isImage(item) {
6590         return item.type &&
6591             item.type.indexOf("image/") === 0;
6592     }
6593
6594     function registerPasteHandler() {
6595         qq(options.targetElement).attach("paste", function(event) {
6596             var clipboardData = event.clipboardData;
6597
6598             if (clipboardData) {
6599                 qq.each(clipboardData.items, function(idx, item) {
6600                     if (isImage(item)) {
6601                         var blob = item.getAsFile();
6602                         options.callbacks.pasteReceived(blob);
6603                     }
6604                 });
6605             }
6606         });
6607     }
6608
6609     function unregisterPasteHandler() {
6610         if (detachPasteHandler) {
6611             detachPasteHandler();
6612         }
6613     }
6614
6615     qq.extend(options, o);
6616     registerPasteHandler();
6617
6618     qq.extend(this, {
6619         reset: function() {
6620             unregisterPasteHandler();
6621         }
6622     });
6623 };
6624
6625 /*globals qq, document, CustomEvent*/
6626 qq.DragAndDrop = function(o) {
6627     "use strict";
6628
6629     var options,
6630         HIDE_ZONES_EVENT_NAME = "qq-hidezones",
6631         HIDE_BEFORE_ENTER_ATTR = "qq-hide-dropzone",
6632         uploadDropZones = [],
6633         droppedFiles = [],
6634         disposeSupport = new qq.DisposeSupport();
6635
6636     options = {
6637         dropZoneElements: [],
6638         allowMultipleItems: true,
6639         classes: {
6640             dropActive: null
6641         },
6642         callbacks: new qq.DragAndDrop.callbacks()
6643     };
6644
6645     qq.extend(options, o, true);
6646
6647     function uploadDroppedFiles(files, uploadDropZone) {
6648         // We need to convert the `FileList` to an actual `Array` to avoid iteration issues
6649         var filesAsArray = Array.prototype.slice.call(files);
6650
6651         options.callbacks.dropLog("Grabbed " + files.length + " dropped files.");
6652         uploadDropZone.dropDisabled(false);
6653         options.callbacks.processingDroppedFilesComplete(filesAsArray, uploadDropZone.getElement());
6654     }
6655
6656     function traverseFileTree(entry) {
6657         var dirReader,
6658             parseEntryPromise = new qq.Promise();
6659
6660         if (entry.isFile) {
6661             entry.file(function(file) {
6662                 droppedFiles.push(file);
6663                 parseEntryPromise.success();
6664             },
6665             function(fileError) {
6666                 options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'.  FileError code " + fileError.code + ".", "error");
6667                 parseEntryPromise.failure();
6668             });
6669         }
6670         else if (entry.isDirectory) {
6671             dirReader = entry.createReader();
6672             dirReader.readEntries(function(entries) {
6673                 var entriesLeft = entries.length;
6674
6675                 qq.each(entries, function(idx, entry) {
6676                     traverseFileTree(entry).done(function() {
6677                         entriesLeft-=1;
6678
6679                         if (entriesLeft === 0) {
6680                             parseEntryPromise.success();
6681                         }
6682                     });
6683                 });
6684
6685                 if (!entries.length) {
6686                     parseEntryPromise.success();
6687                 }
6688             }, function(fileError) {
6689                 options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'.  FileError code " + fileError.code + ".", "error");
6690                 parseEntryPromise.failure();
6691             });
6692         }
6693
6694         return parseEntryPromise;
6695     }
6696
6697     function handleDataTransfer(dataTransfer, uploadDropZone) {
6698         var pendingFolderPromises = [],
6699             handleDataTransferPromise = new qq.Promise();
6700
6701         options.callbacks.processingDroppedFiles();
6702         uploadDropZone.dropDisabled(true);
6703
6704         if (dataTransfer.files.length > 1 && !options.allowMultipleItems) {
6705             options.callbacks.processingDroppedFilesComplete([]);
6706             options.callbacks.dropError("tooManyFilesError", "");
6707             uploadDropZone.dropDisabled(false);
6708             handleDataTransferPromise.failure();
6709         }
6710         else {
6711             droppedFiles = [];
6712
6713             if (qq.isFolderDropSupported(dataTransfer)) {
6714                 qq.each(dataTransfer.items, function(idx, item) {
6715                     var entry = item.webkitGetAsEntry();
6716
6717                     if (entry) {
6718                         //due to a bug in Chrome's File System API impl - #149735
6719                         if (entry.isFile) {
6720                             droppedFiles.push(item.getAsFile());
6721                         }
6722
6723                         else {
6724                             pendingFolderPromises.push(traverseFileTree(entry).done(function() {
6725                                 pendingFolderPromises.pop();
6726                                 if (pendingFolderPromises.length === 0) {
6727                                     handleDataTransferPromise.success();
6728                                 }
6729                             }));
6730                         }
6731                     }
6732                 });
6733             }
6734             else {
6735                 droppedFiles = dataTransfer.files;
6736             }
6737
6738             if (pendingFolderPromises.length === 0) {
6739                 handleDataTransferPromise.success();
6740             }
6741         }
6742
6743         return handleDataTransferPromise;
6744     }
6745
6746     function setupDropzone(dropArea) {
6747         var dropZone = new qq.UploadDropZone({
6748             HIDE_ZONES_EVENT_NAME: HIDE_ZONES_EVENT_NAME,
6749             element: dropArea,
6750             onEnter: function(e){
6751                 qq(dropArea).addClass(options.classes.dropActive);
6752                 e.stopPropagation();
6753             },
6754             onLeaveNotDescendants: function(e){
6755                 qq(dropArea).removeClass(options.classes.dropActive);
6756             },
6757             onDrop: function(e){
6758                 handleDataTransfer(e.dataTransfer, dropZone).done(function() {
6759                     uploadDroppedFiles(droppedFiles, dropZone);
6760                 });
6761             }
6762         });
6763
6764         disposeSupport.addDisposer(function() {
6765             dropZone.dispose();
6766         });
6767
6768         qq(dropArea).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropArea).hide();
6769
6770         uploadDropZones.push(dropZone);
6771
6772         return dropZone;
6773     }
6774
6775     function isFileDrag(dragEvent) {
6776         var fileDrag;
6777
6778         qq.each(dragEvent.dataTransfer.types, function(key, val) {
6779             if (val === "Files") {
6780                 fileDrag = true;
6781                 return false;
6782             }
6783         });
6784
6785         return fileDrag;
6786     }
6787
6788     function leavingDocumentOut(e) {
6789         /* jshint -W041, eqeqeq:false */
6790             // null coords for Chrome and Safari Windows
6791         return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) ||
6792             // null e.relatedTarget for Firefox
6793             (qq.firefox() && !e.relatedTarget);
6794     }
6795
6796     function setupDragDrop() {
6797         var dropZones = options.dropZoneElements;
6798
6799         qq.each(dropZones, function(idx, dropZone) {
6800             var uploadDropZone = setupDropzone(dropZone);
6801
6802             // IE <= 9 does not support the File API used for drag+drop uploads
6803             if (dropZones.length && (!qq.ie() || qq.ie10())) {
6804                 disposeSupport.attach(document, "dragenter", function(e) {
6805                     if (!uploadDropZone.dropDisabled() && isFileDrag(e)) {
6806                         qq.each(dropZones, function(idx, dropZone) {
6807                             // We can't apply styles to non-HTMLElements, since they lack the `style` property
6808                             if (dropZone instanceof HTMLElement) {
6809                                 qq(dropZone).css({display: "block"});
6810                             }
6811                         });
6812                     }
6813                 });
6814             }
6815         });
6816
6817         disposeSupport.attach(document, "dragleave", function(e) {
6818             if (leavingDocumentOut(e)) {
6819                 qq.each(dropZones, function(idx, dropZone) {
6820                     qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropZone).hide();
6821                 });
6822             }
6823         });
6824
6825         disposeSupport.attach(document, "drop", function(e){
6826             qq.each(dropZones, function(idx, dropZone) {
6827                 qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropZone).hide();
6828             });
6829             e.preventDefault();
6830         });
6831
6832         disposeSupport.attach(document, HIDE_ZONES_EVENT_NAME, function(e) {
6833             qq.each(options.dropZoneElements, function(idx, zone) {
6834                 qq(zone).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(zone).hide();
6835                 qq(zone).removeClass(options.classes.dropActive);
6836             });
6837         });
6838     }
6839
6840     setupDragDrop();
6841
6842     qq.extend(this, {
6843         setupExtraDropzone: function(element) {
6844             options.dropZoneElements.push(element);
6845             setupDropzone(element);
6846         },
6847
6848         removeDropzone: function(element) {
6849             var i,
6850                 dzs = options.dropZoneElements;
6851
6852             for(i in dzs) {
6853                 if (dzs[i] === element) {
6854                     return dzs.splice(i, 1);
6855                 }
6856             }
6857         },
6858
6859         dispose: function() {
6860             disposeSupport.dispose();
6861             qq.each(uploadDropZones, function(idx, dropZone) {
6862                 dropZone.dispose();
6863             });
6864         }
6865     });
6866 };
6867
6868 qq.DragAndDrop.callbacks = function() {
6869     "use strict";
6870
6871     return {
6872         processingDroppedFiles: function() {},
6873         processingDroppedFilesComplete: function(files, targetEl) {},
6874         dropError: function(code, errorSpecifics) {
6875             qq.log("Drag & drop error code '" + code + " with these specifics: '" + errorSpecifics + "'", "error");
6876         },
6877         dropLog: function(message, level) {
6878             qq.log(message, level);
6879         }
6880     };
6881 };
6882
6883 qq.UploadDropZone = function(o){
6884     "use strict";
6885
6886     var disposeSupport = new qq.DisposeSupport(),
6887         options, element, preventDrop, dropOutsideDisabled;
6888
6889     options = {
6890         element: null,
6891         onEnter: function(e){},
6892         onLeave: function(e){},
6893         // is not fired when leaving element by hovering descendants
6894         onLeaveNotDescendants: function(e){},
6895         onDrop: function(e){}
6896     };
6897
6898     qq.extend(options, o);
6899     element = options.element;
6900
6901     function dragover_should_be_canceled(){
6902         return qq.safari() || (qq.firefox() && qq.windows());
6903     }
6904
6905     function disableDropOutside(e){
6906         // run only once for all instances
6907         if (!dropOutsideDisabled ){
6908
6909             // for these cases we need to catch onDrop to reset dropArea
6910             if (dragover_should_be_canceled){
6911                 disposeSupport.attach(document, "dragover", function(e){
6912                     e.preventDefault();
6913                 });
6914             } else {
6915                 disposeSupport.attach(document, "dragover", function(e){
6916                     if (e.dataTransfer){
6917                         e.dataTransfer.dropEffect = "none";
6918                         e.preventDefault();
6919                     }
6920                 });
6921             }
6922
6923             dropOutsideDisabled = true;
6924         }
6925     }
6926
6927     function isValidFileDrag(e){
6928         // e.dataTransfer currently causing IE errors
6929         // IE9 does NOT support file API, so drag-and-drop is not possible
6930         if (qq.ie() && !qq.ie10()) {
6931             return false;
6932         }
6933
6934         var effectTest, dt = e.dataTransfer,
6935         // do not check dt.types.contains in webkit, because it crashes safari 4
6936         isSafari = qq.safari();
6937
6938         // dt.effectAllowed is none in Safari 5
6939         // dt.types.contains check is for firefox
6940
6941         // dt.effectAllowed crashes IE11 when files have been dragged from
6942         // the filesystem
6943         effectTest = (qq.ie10() || qq.ie11()) ? true : dt.effectAllowed !== "none";
6944         return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains("Files")));
6945     }
6946
6947     function isOrSetDropDisabled(isDisabled) {
6948         if (isDisabled !== undefined) {
6949             preventDrop = isDisabled;
6950         }
6951         return preventDrop;
6952     }
6953
6954     function triggerHidezonesEvent() {
6955         var hideZonesEvent;
6956
6957         function triggerUsingOldApi() {
6958             hideZonesEvent = document.createEvent("Event");
6959             hideZonesEvent.initEvent(options.HIDE_ZONES_EVENT_NAME, true, true);
6960         }
6961
6962         if (window.CustomEvent) {
6963             try {
6964                 hideZonesEvent = new CustomEvent(options.HIDE_ZONES_EVENT_NAME);
6965             }
6966             catch (err) {
6967                 triggerUsingOldApi();
6968             }
6969         }
6970         else {
6971             triggerUsingOldApi();
6972         }
6973
6974         document.dispatchEvent(hideZonesEvent);
6975     }
6976
6977     function attachEvents(){
6978         disposeSupport.attach(element, "dragover", function(e){
6979             if (!isValidFileDrag(e)) {
6980                 return;
6981             }
6982
6983             // dt.effectAllowed crashes IE11 when files have been dragged from
6984             // the filesystem
6985             var effect = (qq.ie() || qq.ie11()) ? null : e.dataTransfer.effectAllowed;
6986             if (effect === "move" || effect === "linkMove"){
6987                 e.dataTransfer.dropEffect = "move"; // for FF (only move allowed)
6988             } else {
6989                 e.dataTransfer.dropEffect = "copy"; // for Chrome
6990             }
6991
6992             e.stopPropagation();
6993             e.preventDefault();
6994         });
6995
6996         disposeSupport.attach(element, "dragenter", function(e){
6997             if (!isOrSetDropDisabled()) {
6998                 if (!isValidFileDrag(e)) {
6999                     return;
7000                 }
7001                 options.onEnter(e);
7002             }
7003         });
7004
7005         disposeSupport.attach(element, "dragleave", function(e){
7006             if (!isValidFileDrag(e)) {
7007                 return;
7008             }
7009
7010             options.onLeave(e);
7011
7012             var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
7013             // do not fire when moving a mouse over a descendant
7014             if (qq(this).contains(relatedTarget)) {
7015                 return;
7016             }
7017
7018             options.onLeaveNotDescendants(e);
7019         });
7020
7021         disposeSupport.attach(element, "drop", function(e) {
7022             if (!isOrSetDropDisabled()) {
7023                 if (!isValidFileDrag(e)) {
7024                     return;
7025                 }
7026
7027                 e.preventDefault();
7028                 e.stopPropagation();
7029                 options.onDrop(e);
7030
7031                 triggerHidezonesEvent();
7032             }
7033         });
7034     }
7035
7036     disableDropOutside();
7037     attachEvents();
7038
7039     qq.extend(this, {
7040         dropDisabled: function(isDisabled) {
7041             return isOrSetDropDisabled(isDisabled);
7042         },
7043
7044         dispose: function() {
7045             disposeSupport.dispose();
7046         },
7047
7048         getElement: function() {
7049             return element;
7050         }
7051     });
7052 };
7053
7054 /*globals qq, XMLHttpRequest*/
7055 qq.DeleteFileAjaxRequester = function(o) {
7056     "use strict";
7057
7058     var requester,
7059         options = {
7060             method: "DELETE",
7061             uuidParamName: "qquuid",
7062             endpointStore: {},
7063             maxConnections: 3,
7064             customHeaders: {},
7065             paramsStore: {},
7066             demoMode: false,
7067             cors: {
7068                 expected: false,
7069                 sendCredentials: false
7070             },
7071             log: function(str, level) {},
7072             onDelete: function(id) {},
7073             onDeleteComplete: function(id, xhrOrXdr, isError) {}
7074         };
7075
7076     qq.extend(options, o);
7077
7078     function getMandatedParams() {
7079         if (options.method.toUpperCase() === "POST") {
7080             return {
7081                 "_method": "DELETE"
7082             };
7083         }
7084
7085         return {};
7086     }
7087
7088     requester = new qq.AjaxRequester({
7089         validMethods: ["POST", "DELETE"],
7090         method: options.method,
7091         endpointStore: options.endpointStore,
7092         paramsStore: options.paramsStore,
7093         mandatedParams: getMandatedParams(),
7094         maxConnections: options.maxConnections,
7095         customHeaders: options.customHeaders,
7096         demoMode: options.demoMode,
7097         log: options.log,
7098         onSend: options.onDelete,
7099         onComplete: options.onDeleteComplete,
7100         cors: options.cors
7101     });
7102
7103
7104     qq.extend(this, {
7105         sendDelete: function(id, uuid, additionalMandatedParams) {
7106             var additionalOptions = additionalMandatedParams || {};
7107
7108             options.log("Submitting delete file request for " + id);
7109
7110             if (options.method === "DELETE") {
7111                 requester.initTransport(id)
7112                     .withPath(uuid)
7113                     .withParams(additionalOptions)
7114                     .send();
7115             }
7116             else {
7117                 additionalOptions[options.uuidParamName] = uuid;
7118                 requester.initTransport(id)
7119                     .withParams(additionalOptions)
7120                     .send();
7121             }
7122         }
7123     });
7124 };
7125
7126 /*global qq, define */
7127 /*jshint strict:false,bitwise:false,nonew:false,asi:true,-W064,-W116,-W089 */
7128 /**
7129  * Mega pixel image rendering library for iOS6 Safari
7130  *
7131  * Fixes iOS6 Safari's image file rendering issue for large size image (over mega-pixel),
7132  * which causes unexpected subsampling when drawing it in canvas.
7133  * By using this library, you can safely render the image with proper stretching.
7134  *
7135  * Copyright (c) 2012 Shinichi Tomita <shinichi.tomita@gmail.com>
7136  * Released under the MIT license
7137  */
7138 (function() {
7139
7140   /**
7141    * Detect subsampling in loaded image.
7142    * In iOS, larger images than 2M pixels may be subsampled in rendering.
7143    */
7144   function detectSubsampling(img) {
7145     var iw = img.naturalWidth, ih = img.naturalHeight;
7146     if (iw * ih > 1024 * 1024) { // subsampling may happen over megapixel image
7147       var canvas = document.createElement('canvas');
7148       canvas.width = canvas.height = 1;
7149       var ctx = canvas.getContext('2d');
7150       ctx.drawImage(img, -iw + 1, 0);
7151       // subsampled image becomes half smaller in rendering size.
7152       // check alpha channel value to confirm image is covering edge pixel or not.
7153       // if alpha value is 0 image is not covering, hence subsampled.
7154       return ctx.getImageData(0, 0, 1, 1).data[3] === 0;
7155     } else {
7156       return false;
7157     }
7158   }
7159
7160   /**
7161    * Detecting vertical squash in loaded image.
7162    * Fixes a bug which squash image vertically while drawing into canvas for some images.
7163    */
7164   function detectVerticalSquash(img, iw, ih) {
7165     var canvas = document.createElement('canvas');
7166     canvas.width = 1;
7167     canvas.height = ih;
7168     var ctx = canvas.getContext('2d');
7169     ctx.drawImage(img, 0, 0);
7170     var data = ctx.getImageData(0, 0, 1, ih).data;
7171     // search image edge pixel position in case it is squashed vertically.
7172     var sy = 0;
7173     var ey = ih;
7174     var py = ih;
7175     while (py > sy) {
7176       var alpha = data[(py - 1) * 4 + 3];
7177       if (alpha === 0) {
7178         ey = py;
7179       } else {
7180         sy = py;
7181       }
7182       py = (ey + sy) >> 1;
7183     }
7184     var ratio = (py / ih);
7185     return (ratio===0)?1:ratio;
7186   }
7187
7188   /**
7189    * Rendering image element (with resizing) and get its data URL
7190    */
7191   function renderImageToDataURL(img, options, doSquash) {
7192     var canvas = document.createElement('canvas'),
7193         mime = options.mime || "image/jpeg";
7194
7195     renderImageToCanvas(img, canvas, options, doSquash);
7196     return canvas.toDataURL(mime, options.quality || 0.8);
7197   }
7198
7199   /**
7200    * Rendering image element (with resizing) into the canvas element
7201    */
7202   function renderImageToCanvas(img, canvas, options, doSquash) {
7203     var iw = img.naturalWidth, ih = img.naturalHeight;
7204     var width = options.width, height = options.height;
7205     var ctx = canvas.getContext('2d');
7206     ctx.save();
7207     transformCoordinate(canvas, width, height, options.orientation);
7208
7209     // Fine Uploader specific: Save some CPU cycles if not using iOS
7210     // Assumption: This logic is only needed to overcome iOS image sampling issues
7211     if (qq.ios()) {
7212         var subsampled = detectSubsampling(img);
7213         if (subsampled) {
7214           iw /= 2;
7215           ih /= 2;
7216         }
7217         var d = 1024; // size of tiling canvas
7218         var tmpCanvas = document.createElement('canvas');
7219         tmpCanvas.width = tmpCanvas.height = d;
7220         var tmpCtx = tmpCanvas.getContext('2d');
7221         var vertSquashRatio = doSquash ? detectVerticalSquash(img, iw, ih) : 1;
7222         var dw = Math.ceil(d * width / iw);
7223         var dh = Math.ceil(d * height / ih / vertSquashRatio);
7224         var sy = 0;
7225         var dy = 0;
7226         while (sy < ih) {
7227           var sx = 0;
7228           var dx = 0;
7229           while (sx < iw) {
7230             tmpCtx.clearRect(0, 0, d, d);
7231             tmpCtx.drawImage(img, -sx, -sy);
7232             ctx.drawImage(tmpCanvas, 0, 0, d, d, dx, dy, dw, dh);
7233             sx += d;
7234             dx += dw;
7235           }
7236           sy += d;
7237           dy += dh;
7238         }
7239         ctx.restore();
7240         tmpCanvas = tmpCtx = null;
7241     }
7242     else {
7243         ctx.drawImage(img, 0, 0, width, height);
7244     }
7245   }
7246
7247   /**
7248    * Transform canvas coordination according to specified frame size and orientation
7249    * Orientation value is from EXIF tag
7250    */
7251   function transformCoordinate(canvas, width, height, orientation) {
7252     switch (orientation) {
7253       case 5:
7254       case 6:
7255       case 7:
7256       case 8:
7257         canvas.width = height;
7258         canvas.height = width;
7259         break;
7260       default:
7261         canvas.width = width;
7262         canvas.height = height;
7263     }
7264     var ctx = canvas.getContext('2d');
7265     switch (orientation) {
7266       case 2:
7267         // horizontal flip
7268         ctx.translate(width, 0);
7269         ctx.scale(-1, 1);
7270         break;
7271       case 3:
7272         // 180 rotate left
7273         ctx.translate(width, height);
7274         ctx.rotate(Math.PI);
7275         break;
7276       case 4:
7277         // vertical flip
7278         ctx.translate(0, height);
7279         ctx.scale(1, -1);
7280         break;
7281       case 5:
7282         // vertical flip + 90 rotate right
7283         ctx.rotate(0.5 * Math.PI);
7284         ctx.scale(1, -1);
7285         break;
7286       case 6:
7287         // 90 rotate right
7288         ctx.rotate(0.5 * Math.PI);
7289         ctx.translate(0, -height);
7290         break;
7291       case 7:
7292         // horizontal flip + 90 rotate right
7293         ctx.rotate(0.5 * Math.PI);
7294         ctx.translate(width, -height);
7295         ctx.scale(-1, 1);
7296         break;
7297       case 8:
7298         // 90 rotate left
7299         ctx.rotate(-0.5 * Math.PI);
7300         ctx.translate(-width, 0);
7301         break;
7302       default:
7303         break;
7304     }
7305   }
7306
7307
7308   /**
7309    * MegaPixImage class
7310    */
7311   function MegaPixImage(srcImage, errorCallback) {
7312     if (window.Blob && srcImage instanceof Blob) {
7313       var img = new Image();
7314       var URL = window.URL && window.URL.createObjectURL ? window.URL :
7315                 window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL :
7316                 null;
7317       if (!URL) { throw Error("No createObjectURL function found to create blob url"); }
7318       img.src = URL.createObjectURL(srcImage);
7319       this.blob = srcImage;
7320       srcImage = img;
7321     }
7322     if (!srcImage.naturalWidth && !srcImage.naturalHeight) {
7323       var _this = this;
7324       srcImage.onload = function() {
7325         var listeners = _this.imageLoadListeners;
7326         if (listeners) {
7327           _this.imageLoadListeners = null;
7328           for (var i=0, len=listeners.length; i<len; i++) {
7329             listeners[i]();
7330           }
7331         }
7332       };
7333       srcImage.onerror = errorCallback;
7334       this.imageLoadListeners = [];
7335     }
7336     this.srcImage = srcImage;
7337   }
7338
7339   /**
7340    * Rendering megapix image into specified target element
7341    */
7342   MegaPixImage.prototype.render = function(target, options) {
7343     if (this.imageLoadListeners) {
7344       var _this = this;
7345       this.imageLoadListeners.push(function() { _this.render(target, options) });
7346       return;
7347     }
7348     options = options || {};
7349     var imgWidth = this.srcImage.naturalWidth, imgHeight = this.srcImage.naturalHeight,
7350         width = options.width, height = options.height,
7351         maxWidth = options.maxWidth, maxHeight = options.maxHeight,
7352         doSquash = !this.blob || this.blob.type === 'image/jpeg';
7353     if (width && !height) {
7354       height = (imgHeight * width / imgWidth) << 0;
7355     } else if (height && !width) {
7356       width = (imgWidth * height / imgHeight) << 0;
7357     } else {
7358       width = imgWidth;
7359       height = imgHeight;
7360     }
7361     if (maxWidth && width > maxWidth) {
7362       width = maxWidth;
7363       height = (imgHeight * width / imgWidth) << 0;
7364     }
7365     if (maxHeight && height > maxHeight) {
7366       height = maxHeight;
7367       width = (imgWidth * height / imgHeight) << 0;
7368     }
7369     var opt = { width : width, height : height };
7370     for (var k in options) opt[k] = options[k];
7371
7372     var tagName = target.tagName.toLowerCase();
7373     if (tagName === 'img') {
7374       target.src = renderImageToDataURL(this.srcImage, opt, doSquash);
7375     } else if (tagName === 'canvas') {
7376       renderImageToCanvas(this.srcImage, target, opt, doSquash);
7377     }
7378     if (typeof this.onrender === 'function') {
7379       this.onrender(target);
7380     }
7381   };
7382
7383   /**
7384    * Export class to global
7385    */
7386   if (typeof define === 'function' && define.amd) {
7387     define([], function() { return MegaPixImage; }); // for AMD loader
7388   } else {
7389     this.MegaPixImage = MegaPixImage;
7390   }
7391
7392 })();
7393
7394 /*globals qq, MegaPixImage */
7395 /**
7396  * Draws a thumbnail of a Blob/File/URL onto an <img> or <canvas>.
7397  *
7398  * @constructor
7399  */
7400 qq.ImageGenerator = function(log) {
7401     "use strict";
7402
7403     function isImg(el) {
7404         return el.tagName.toLowerCase() === "img";
7405     }
7406
7407     function isCanvas(el) {
7408         return el.tagName.toLowerCase() === "canvas";
7409     }
7410
7411     function isImgCorsSupported() {
7412         return new Image().crossOrigin !== undefined;
7413     }
7414
7415     function isCanvasSupported() {
7416         var canvas = document.createElement("canvas");
7417
7418         return canvas.getContext && canvas.getContext("2d");
7419     }
7420
7421     // This is only meant to determine the MIME type of a renderable image file.
7422     // It is used to ensure images drawn from a URL that have transparent backgrounds
7423     // are rendered correctly, among other things.
7424     function determineMimeOfFileName(nameWithPath) {
7425         /*jshint -W015 */
7426         var pathSegments = nameWithPath.split("/"),
7427             name = pathSegments[pathSegments.length - 1],
7428             extension = qq.getExtension(name);
7429
7430         extension = extension && extension.toLowerCase();
7431
7432         switch(extension) {
7433             case "jpeg":
7434             case "jpg":
7435                 return "image/jpeg";
7436             case "png":
7437                 return "image/png";
7438             case "bmp":
7439                 return "image/bmp";
7440             case "gif":
7441                 return "image/gif";
7442             case "tiff":
7443             case "tif":
7444                 return "image/tiff";
7445         }
7446     }
7447
7448     // This will likely not work correctly in IE8 and older.
7449     // It's only used as part of a formula to determine
7450     // if a canvas can be used to scale a server-hosted thumbnail.
7451     // If canvas isn't supported by the UA (IE8 and older)
7452     // this method should not even be called.
7453     function isCrossOrigin(url) {
7454         var targetAnchor = document.createElement("a"),
7455             targetProtocol, targetHostname, targetPort;
7456
7457         targetAnchor.href = url;
7458
7459         targetProtocol = targetAnchor.protocol;
7460         targetPort = targetAnchor.port;
7461         targetHostname = targetAnchor.hostname;
7462
7463         if (targetProtocol.toLowerCase() !== window.location.protocol.toLowerCase()) {
7464             return true;
7465         }
7466
7467         if (targetHostname.toLowerCase() !== window.location.hostname.toLowerCase()) {
7468             return true;
7469         }
7470
7471         // IE doesn't take ports into consideration when determining if two endpoints are same origin.
7472         if (targetPort !== window.location.port && !qq.ie()) {
7473             return true;
7474         }
7475
7476         return false;
7477     }
7478
7479     function registerImgLoadListeners(img, promise) {
7480         img.onload = function() {
7481             img.onload = null;
7482             img.onerror = null;
7483             promise.success(img);
7484         };
7485
7486         img.onerror = function() {
7487             img.onload = null;
7488             img.onerror = null;
7489             log("Problem drawing thumbnail!", "error");
7490             promise.failure(img, "Problem drawing thumbnail!");
7491         };
7492     }
7493
7494     function registerCanvasDrawImageListener(canvas, promise) {
7495         var context = canvas.getContext("2d"),
7496             oldDrawImage = context.drawImage;
7497
7498         // The image is drawn on the canvas by a third-party library,
7499         // and we want to know when this happens so we can fulfill the associated promise.
7500         context.drawImage = function() {
7501             oldDrawImage.apply(this, arguments);
7502             promise.success(canvas);
7503             context.drawImage = oldDrawImage;
7504         };
7505     }
7506
7507     // Fulfills a `qq.Promise` when an image has been drawn onto the target,
7508     // whether that is a <canvas> or an <img>.  The attempt is considered a
7509     // failure if the target is not an <img> or a <canvas>, or if the drawing
7510     // attempt was not successful.
7511     function registerThumbnailRenderedListener(imgOrCanvas, promise) {
7512         var registered = isImg(imgOrCanvas) || isCanvas(imgOrCanvas);
7513
7514         if (isImg(imgOrCanvas)) {
7515             registerImgLoadListeners(imgOrCanvas, promise);
7516         }
7517         else if (isCanvas(imgOrCanvas)) {
7518             registerCanvasDrawImageListener(imgOrCanvas, promise);
7519         }
7520         else {
7521             promise.failure(imgOrCanvas);
7522             log(qq.format("Element container of type {} is not supported!", imgOrCanvas.tagName), "error");
7523         }
7524
7525         return registered;
7526     }
7527
7528     // Draw a preview iff the current UA can natively display it.
7529     // Also rotate the image if necessary.
7530     function draw(fileOrBlob, container, options) {
7531         var drawPreview = new qq.Promise(),
7532             identifier = new qq.Identify(fileOrBlob, log),
7533             maxSize = options.maxSize,
7534             megapixErrorHandler = function() {
7535                 container.onerror = null;
7536                 container.onload = null;
7537                 log("Could not render preview, file may be too large!", "error");
7538                 drawPreview.failure(container, "Browser cannot render image!");
7539             };
7540
7541         identifier.isPreviewable().then(
7542             function(mime) {
7543                 var exif = new qq.Exif(fileOrBlob, log),
7544                     mpImg = new MegaPixImage(fileOrBlob, megapixErrorHandler);
7545
7546                 if (registerThumbnailRenderedListener(container, drawPreview)) {
7547                     exif.parse().then(
7548                         function(exif) {
7549                             var orientation = exif.Orientation;
7550
7551                             mpImg.render(container, {
7552                                 maxWidth: maxSize,
7553                                 maxHeight: maxSize,
7554                                 orientation: orientation,
7555                                 mime: mime
7556                             });
7557                         },
7558
7559                         function(failureMsg) {
7560                             log(qq.format("EXIF data could not be parsed ({}).  Assuming orientation = 1.", failureMsg));
7561
7562                             mpImg.render(container, {
7563                                 maxWidth: maxSize,
7564                                 maxHeight: maxSize,
7565                                 mime: mime
7566                             });
7567                         }
7568                     );
7569                 }
7570             },
7571
7572             function() {
7573                 log("Not previewable");
7574                 drawPreview.failure(container, "Not previewable");
7575             }
7576         );
7577
7578         return drawPreview;
7579     }
7580
7581     function drawOnCanvasOrImgFromUrl(url, canvasOrImg, draw, maxSize) {
7582         var tempImg = new Image(),
7583             tempImgRender = new qq.Promise();
7584
7585         registerThumbnailRenderedListener(tempImg, tempImgRender);
7586
7587         if (isCrossOrigin(url)) {
7588             tempImg.crossOrigin = "anonymous";
7589         }
7590
7591         tempImg.src = url;
7592
7593         tempImgRender.then(function() {
7594             registerThumbnailRenderedListener(canvasOrImg, draw);
7595
7596             var mpImg = new MegaPixImage(tempImg);
7597             mpImg.render(canvasOrImg, {
7598                 maxWidth: maxSize,
7599                 maxHeight: maxSize,
7600                 mime: determineMimeOfFileName(url)
7601             });
7602         });
7603     }
7604
7605     function drawOnImgFromUrlWithCssScaling(url, img, draw, maxSize) {
7606         registerThumbnailRenderedListener(img, draw);
7607         qq(img).css({
7608             maxWidth: maxSize + "px",
7609             maxHeight: maxSize + "px"
7610         });
7611
7612         img.src = url;
7613     }
7614
7615     // Draw a (server-hosted) thumbnail given a URL.
7616     // This will optionally scale the thumbnail as well.
7617     // It attempts to use <canvas> to scale, but will fall back
7618     // to max-width and max-height style properties if the UA
7619     // doesn't support canvas or if the images is cross-domain and
7620     // the UA doesn't support the crossorigin attribute on img tags,
7621     // which is required to scale a cross-origin image using <canvas> &
7622     // then export it back to an <img>.
7623     function drawFromUrl(url, container, options) {
7624         var draw = new qq.Promise(),
7625             scale = options.scale,
7626             maxSize = scale ? options.maxSize : null;
7627
7628         // container is an img, scaling needed
7629         if (scale && isImg(container)) {
7630             // Iff canvas is available in this UA, try to use it for scaling.
7631             // Otherwise, fall back to CSS scaling
7632             if (isCanvasSupported()) {
7633                 // Attempt to use <canvas> for image scaling,
7634                 // but we must fall back to scaling via CSS/styles
7635                 // if this is a cross-origin image and the UA doesn't support <img> CORS.
7636                 if (isCrossOrigin(url) && !isImgCorsSupported()) {
7637                     drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize);
7638                 }
7639                 else {
7640                     drawOnCanvasOrImgFromUrl(url, container, draw, maxSize);
7641                 }
7642             }
7643             else {
7644                 drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize);
7645             }
7646         }
7647         // container is a canvas, scaling optional
7648         else if (isCanvas(container)) {
7649             drawOnCanvasOrImgFromUrl(url, container, draw, maxSize);
7650         }
7651         // container is an img & no scaling: just set the src attr to the passed url
7652         else if (registerThumbnailRenderedListener(container, draw)) {
7653             container.src = url;
7654         }
7655
7656         return draw;
7657     }
7658
7659
7660     qq.extend(this, {
7661         /**
7662          * Generate a thumbnail.  Depending on the arguments, this may either result in
7663          * a client-side rendering of an image (if a `Blob` is supplied) or a server-generated
7664          * image that may optionally be scaled client-side using <canvas> or CSS/styles (as a fallback).
7665          *
7666          * @param fileBlobOrUrl a `File`, `Blob`, or a URL pointing to the image
7667          * @param container <img> or <canvas> to contain the preview
7668          * @param options possible properties include `maxSize` (int), `orient` (bool), and `resize` (bool)
7669          * @returns qq.Promise fulfilled when the preview has been drawn, or the attempt has failed
7670          */
7671         generate: function(fileBlobOrUrl, container, options) {
7672             if (qq.isString(fileBlobOrUrl)) {
7673                 log("Attempting to update thumbnail based on server response.");
7674                 return drawFromUrl(fileBlobOrUrl, container, options || {});
7675             }
7676             else {
7677                 log("Attempting to draw client-side image preview.");
7678                 return draw(fileBlobOrUrl, container, options || {});
7679             }
7680         }
7681     });
7682
7683     /*<testing>*/
7684     this._testing = {};
7685     this._testing.isImg = isImg;
7686     this._testing.isCanvas = isCanvas;
7687     this._testing.isCrossOrigin = isCrossOrigin;
7688     this._testing.determineMimeOfFileName = determineMimeOfFileName;
7689     /*</testing>*/
7690 };
7691
7692 /*globals qq */
7693 /**
7694  * EXIF image data parser.  Currently only parses the Orientation tag value,
7695  * but this may be expanded to other tags in the future.
7696  *
7697  * @param fileOrBlob Attempt to parse EXIF data in this `Blob`
7698  * @constructor
7699  */
7700 qq.Exif = function(fileOrBlob, log) {
7701     "use strict";
7702
7703     // Orientation is the only tag parsed here at this time.
7704     var TAG_IDS = [274],
7705         TAG_INFO = {
7706             274: {
7707                 name: "Orientation",
7708                 bytes: 2
7709             }
7710         };
7711
7712     // Convert a little endian (hex string) to big endian (decimal).
7713     function parseLittleEndian(hex) {
7714         var result = 0,
7715             pow = 0;
7716
7717         while (hex.length > 0) {
7718             result += parseInt(hex.substring(0, 2), 16) * Math.pow(2, pow);
7719             hex = hex.substring(2, hex.length);
7720             pow += 8;
7721         }
7722
7723         return result;
7724     }
7725
7726     // Find the byte offset, of Application Segment 1 (EXIF).
7727     // External callers need not supply any arguments.
7728     function seekToApp1(offset, promise) {
7729         var theOffset = offset,
7730             thePromise = promise;
7731         if (theOffset === undefined) {
7732             theOffset = 2;
7733             thePromise = new qq.Promise();
7734         }
7735
7736         qq.readBlobToHex(fileOrBlob, theOffset, 4).then(function(hex) {
7737             var match = /^ffe([0-9])/.exec(hex);
7738             if (match) {
7739                 if (match[1] !== "1") {
7740                     var segmentLength = parseInt(hex.slice(4, 8), 16);
7741                     seekToApp1(theOffset + segmentLength + 2, thePromise);
7742                 }
7743                 else {
7744                     thePromise.success(theOffset);
7745                 }
7746             }
7747             else {
7748                 thePromise.failure("No EXIF header to be found!");
7749             }
7750         });
7751
7752         return thePromise;
7753     }
7754
7755     // Find the byte offset of Application Segment 1 (EXIF) for valid JPEGs only.
7756     function getApp1Offset() {
7757         var promise = new qq.Promise();
7758
7759         qq.readBlobToHex(fileOrBlob, 0, 6).then(function(hex) {
7760             if (hex.indexOf("ffd8") !== 0) {
7761                 promise.failure("Not a valid JPEG!");
7762             }
7763             else {
7764                 seekToApp1().then(function(offset) {
7765                     promise.success(offset);
7766                 },
7767                 function(error) {
7768                     promise.failure(error);
7769                 });
7770             }
7771         });
7772
7773         return promise;
7774     }
7775
7776     // Determine the byte ordering of the EXIF header.
7777     function isLittleEndian(app1Start) {
7778         var promise = new qq.Promise();
7779
7780         qq.readBlobToHex(fileOrBlob, app1Start + 10, 2).then(function(hex) {
7781             promise.success(hex === "4949");
7782         });
7783
7784         return promise;
7785     }
7786
7787     // Determine the number of directory entries in the EXIF header.
7788     function getDirEntryCount(app1Start, littleEndian) {
7789         var promise = new qq.Promise();
7790
7791         qq.readBlobToHex(fileOrBlob, app1Start + 18, 2).then(function(hex) {
7792             if (littleEndian) {
7793                 return promise.success(parseLittleEndian(hex));
7794             }
7795             else {
7796                 promise.success(parseInt(hex, 16));
7797             }
7798         });
7799
7800         return promise;
7801     }
7802
7803     // Get the IFD portion of the EXIF header as a hex string.
7804     function getIfd(app1Start, dirEntries) {
7805         var offset = app1Start + 20,
7806             bytes = dirEntries * 12;
7807
7808         return qq.readBlobToHex(fileOrBlob, offset, bytes);
7809     }
7810
7811     // Obtain an array of all directory entries (as hex strings) in the EXIF header.
7812     function getDirEntries(ifdHex) {
7813         var entries = [],
7814             offset = 0;
7815
7816         while (offset+24 <= ifdHex.length) {
7817             entries.push(ifdHex.slice(offset, offset + 24));
7818             offset += 24;
7819         }
7820
7821         return entries;
7822     }
7823
7824     // Obtain values for all relevant tags and return them.
7825     function getTagValues(littleEndian, dirEntries) {
7826         var TAG_VAL_OFFSET = 16,
7827             tagsToFind = qq.extend([], TAG_IDS),
7828             vals = {};
7829
7830         qq.each(dirEntries, function(idx, entry) {
7831             var idHex = entry.slice(0, 4),
7832                 id = littleEndian ? parseLittleEndian(idHex) : parseInt(idHex, 16),
7833                 tagsToFindIdx = tagsToFind.indexOf(id),
7834                 tagValHex, tagName, tagValLength;
7835
7836             if (tagsToFindIdx >= 0) {
7837                 tagName = TAG_INFO[id].name;
7838                 tagValLength = TAG_INFO[id].bytes;
7839                 tagValHex = entry.slice(TAG_VAL_OFFSET, TAG_VAL_OFFSET + (tagValLength*2));
7840                 vals[tagName] = littleEndian ? parseLittleEndian(tagValHex) : parseInt(tagValHex, 16);
7841
7842                 tagsToFind.splice(tagsToFindIdx, 1);
7843             }
7844
7845             if (tagsToFind.length === 0) {
7846                 return false;
7847             }
7848         });
7849
7850         return vals;
7851     }
7852
7853     qq.extend(this, {
7854         /**
7855          * Attempt to parse the EXIF header for the `Blob` associated with this instance.
7856          *
7857          * @returns {qq.Promise} To be fulfilled when the parsing is complete.
7858          * If successful, the parsed EXIF header as an object will be included.
7859          */
7860         parse: function() {
7861             var parser = new qq.Promise(),
7862                 onParseFailure = function(message) {
7863                     log(qq.format("EXIF header parse failed: '{}' ", message));
7864                     parser.failure(message);
7865                 };
7866
7867             getApp1Offset().then(function(app1Offset) {
7868                 log(qq.format("Moving forward with EXIF header parsing for '{}'", fileOrBlob.name === undefined ? "blob" : fileOrBlob.name));
7869
7870                 isLittleEndian(app1Offset).then(function(littleEndian) {
7871
7872                     log(qq.format("EXIF Byte order is {} endian", littleEndian ? "little" : "big"));
7873
7874                     getDirEntryCount(app1Offset, littleEndian).then(function(dirEntryCount) {
7875
7876                         log(qq.format("Found {} APP1 directory entries", dirEntryCount));
7877
7878                         getIfd(app1Offset, dirEntryCount).then(function(ifdHex) {
7879                             var dirEntries = getDirEntries(ifdHex),
7880                                 tagValues = getTagValues(littleEndian, dirEntries);
7881
7882                             log("Successfully parsed some EXIF tags");
7883
7884                             parser.success(tagValues);
7885                         }, onParseFailure);
7886                     }, onParseFailure);
7887                 }, onParseFailure);
7888             }, onParseFailure);
7889
7890             return parser;
7891         }
7892     });
7893
7894     /*<testing>*/
7895     this._testing = {};
7896     this._testing.parseLittleEndian = parseLittleEndian;
7897     /*</testing>*/
7898 };
7899
7900 /*globals qq */
7901 qq.Identify = function(fileOrBlob, log) {
7902     "use strict";
7903
7904     var PREVIEWABLE_MAGIC_BYTES = {
7905             "image/jpeg": "ffd8ff",
7906             "image/gif": "474946",
7907             "image/png": "89504e",
7908             "image/bmp": "424d",
7909             "image/tiff": ["49492a00", "4d4d002a"]
7910         };
7911
7912     function isIdentifiable(magicBytes, questionableBytes) {
7913         var identifiable = false,
7914             magicBytesEntries = [].concat(magicBytes);
7915
7916         qq.each(magicBytesEntries, function(idx, magicBytesArrayEntry) {
7917             if (questionableBytes.indexOf(magicBytesArrayEntry) === 0) {
7918                 identifiable = true;
7919                 return false;
7920             }
7921         });
7922
7923         return identifiable;
7924     }
7925
7926     qq.extend(this, {
7927         isPreviewable: function() {
7928             var idenitifer = new qq.Promise(),
7929                 previewable = false,
7930                 name = fileOrBlob.name === undefined ? "blob" : fileOrBlob.name;
7931
7932             log(qq.format("Attempting to determine if {} can be rendered in this browser", name));
7933
7934             qq.readBlobToHex(fileOrBlob, 0, 4).then(function(hex) {
7935                 qq.each(PREVIEWABLE_MAGIC_BYTES, function(mime, bytes) {
7936                     if (isIdentifiable(bytes, hex)) {
7937                         // Safari is the only supported browser that can deal with TIFFs natively,
7938                         // so, if this is a TIFF and the UA isn't Safari, declare this file "non-previewable".
7939                         if (mime !== "image/tiff" || qq.safari()) {
7940                             previewable = true;
7941                             idenitifer.success(mime);
7942                         }
7943
7944                         return false;
7945                     }
7946                 });
7947
7948                 log(qq.format("'{}' is {} able to be rendered in this browser", name, previewable ? "" : "NOT"));
7949
7950                 if (!previewable) {
7951                     idenitifer.failure();
7952                 }
7953             });
7954
7955             return idenitifer;
7956         }
7957     });
7958 };
7959
7960 /*globals qq*/
7961 /**
7962  * Attempts to validate an image, wherever possible.
7963  *
7964  * @param blob File or Blob representing a user-selecting image.
7965  * @param log Uses this to post log messages to the console.
7966  * @constructor
7967  */
7968 qq.ImageValidation = function(blob, log) {
7969     "use strict";
7970
7971     /**
7972      * @param limits Object with possible image-related limits to enforce.
7973      * @returns {boolean} true if at least one of the limits has a non-zero value
7974      */
7975     function hasNonZeroLimits(limits) {
7976         var atLeastOne = false;
7977
7978         qq.each(limits, function(limit, value) {
7979             if (value > 0) {
7980                 atLeastOne = true;
7981                 return false;
7982             }
7983         });
7984
7985         return atLeastOne;
7986     }
7987
7988     /**
7989      * @returns {qq.Promise} The promise is a failure if we can't obtain the width & height.
7990      * Otherwise, `success` is called on the returned promise with an object containing
7991      * `width` and `height` properties.
7992      */
7993     function getWidthHeight() {
7994         var sizeDetermination = new qq.Promise();
7995
7996         new qq.Identify(blob, log).isPreviewable().then(function() {
7997             var image = new Image(),
7998                 url = window.URL && window.URL.createObjectURL ? window.URL :
7999                       window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL :
8000                       null;
8001
8002             if (url) {
8003                 image.onerror = function() {
8004                     log("Cannot determine dimensions for image.  May be too large.", "error");
8005                     sizeDetermination.failure();
8006                 };
8007
8008                 image.onload = function() {
8009                     sizeDetermination.success({
8010                         width: this.width,
8011                         height: this.height
8012                     });
8013                 };
8014
8015                 image.src = url.createObjectURL(blob);
8016             }
8017             else {
8018                 log("No createObjectURL function available to generate image URL!", "error");
8019                 sizeDetermination.failure();
8020             }
8021         }, sizeDetermination.failure);
8022
8023         return sizeDetermination;
8024     }
8025
8026     /**
8027      *
8028      * @param limits Object with possible image-related limits to enforce.
8029      * @param dimensions Object containing `width` & `height` properties for the image to test.
8030      * @returns {String || undefined} The name of the failing limit.  Undefined if no failing limits.
8031      */
8032     function getFailingLimit(limits, dimensions) {
8033         var failingLimit;
8034
8035         qq.each(limits, function(limitName, limitValue) {
8036             if (limitValue > 0) {
8037                 var limitMatcher = /(max|min)(Width|Height)/.exec(limitName),
8038                     dimensionPropName = limitMatcher[2].charAt(0).toLowerCase() + limitMatcher[2].slice(1),
8039                     actualValue = dimensions[dimensionPropName];
8040
8041                 /*jshint -W015*/
8042                 switch(limitMatcher[1]) {
8043                     case "min":
8044                         if (actualValue < limitValue) {
8045                             failingLimit = limitName;
8046                             return false;
8047                         }
8048                         break;
8049                     case "max":
8050                         if (actualValue > limitValue) {
8051                             failingLimit = limitName;
8052                             return false;
8053                         }
8054                         break;
8055                 }
8056             }
8057         });
8058
8059         return failingLimit;
8060     }
8061
8062     /**
8063      * Validate the associated blob.
8064      *
8065      * @param limits
8066      * @returns {qq.Promise} `success` is called on the promise is the image is valid or
8067      * if the blob is not an image, or if the image is not verifiable.
8068      * Otherwise, `failure` with the name of the failing limit.
8069      */
8070     this.validate = function(limits) {
8071         var validationEffort = new qq.Promise();
8072
8073         log("Attempting to validate image.");
8074
8075         if (hasNonZeroLimits(limits)) {
8076             getWidthHeight().then(function(dimensions) {
8077                 var failingLimit = getFailingLimit(limits, dimensions);
8078
8079                 if (failingLimit) {
8080                     validationEffort.failure(failingLimit);
8081                 }
8082                 else {
8083                     validationEffort.success();
8084                 }
8085             }, validationEffort.success);
8086         }
8087         else {
8088             validationEffort.success();
8089         }
8090
8091         return validationEffort;
8092     };
8093 };
8094
8095 /* globals qq */
8096 /**
8097  * Module used to control populating the initial list of files.
8098  *
8099  * @constructor
8100  */
8101 qq.Session = function(spec) {
8102     "use strict";
8103
8104     var options = {
8105         endpoint: null,
8106         params: {},
8107         customHeaders: {},
8108         cors: {},
8109         addFileRecord: function(sessionData) {},
8110         log: function(message, level) {}
8111     };
8112
8113     qq.extend(options, spec, true);
8114
8115
8116     function isJsonResponseValid(response) {
8117         if (qq.isArray(response)) {
8118             return true;
8119         }
8120
8121         options.log("Session response is not an array.", "error");
8122     }
8123
8124     function handleFileItems(fileItems, success, xhrOrXdr, promise) {
8125         var someItemsIgnored = false;
8126
8127         success = success && isJsonResponseValid(fileItems);
8128
8129         if (success) {
8130             qq.each(fileItems, function(idx, fileItem) {
8131                 /* jshint eqnull:true */
8132                 if (fileItem.uuid == null) {
8133                     someItemsIgnored = true;
8134                     options.log(qq.format("Session response item {} did not include a valid UUID - ignoring.", idx), "error");
8135                 }
8136                 else if (fileItem.name == null) {
8137                     someItemsIgnored = true;
8138                     options.log(qq.format("Session response item {} did not include a valid name - ignoring.", idx), "error");
8139                 }
8140                 else {
8141                     try {
8142                         options.addFileRecord(fileItem);
8143                         return true;
8144                     }
8145                     catch(err) {
8146                         someItemsIgnored = true;
8147                         options.log(err.message, "error");
8148                     }
8149                 }
8150
8151                 return false;
8152             });
8153         }
8154
8155         promise[success && !someItemsIgnored ? "success" : "failure"](fileItems, xhrOrXdr);
8156     }
8157
8158     // Initiate a call to the server that will be used to populate the initial file list.
8159     // Returns a `qq.Promise`.
8160     this.refresh = function() {
8161         /*jshint indent:false */
8162         var refreshEffort = new qq.Promise(),
8163             refreshCompleteCallback = function(response, success, xhrOrXdr) {
8164                 handleFileItems(response, success, xhrOrXdr, refreshEffort);
8165             },
8166             requsterOptions = qq.extend({}, options),
8167             requester = new qq.SessionAjaxRequester(
8168                 qq.extend(requsterOptions, {onComplete: refreshCompleteCallback})
8169             );
8170
8171         requester.queryServer();
8172
8173         return refreshEffort;
8174     };
8175 };
8176
8177 /*globals qq, XMLHttpRequest*/
8178 /**
8179  * Thin module used to send GET requests to the server, expecting information about session
8180  * data used to initialize an uploader instance.
8181  *
8182  * @param spec Various options used to influence the associated request.
8183  * @constructor
8184  */
8185 qq.SessionAjaxRequester = function(spec) {
8186     "use strict";
8187
8188     var requester,
8189         options = {
8190             endpoint: null,
8191             customHeaders: {},
8192             params: {},
8193             cors: {
8194                 expected: false,
8195                 sendCredentials: false
8196             },
8197             onComplete: function(response, success, xhrOrXdr) {},
8198             log: function(str, level) {}
8199         };
8200
8201     qq.extend(options, spec);
8202
8203     function onComplete(id, xhrOrXdr, isError) {
8204         var response = null;
8205
8206         /* jshint eqnull:true */
8207         if (xhrOrXdr.responseText != null) {
8208             try {
8209                 response = qq.parseJson(xhrOrXdr.responseText);
8210             }
8211             catch(err) {
8212                 options.log("Problem parsing session response: " + err.message, "error");
8213                 isError = true;
8214             }
8215         }
8216
8217         options.onComplete(response, !isError, xhrOrXdr);
8218     }
8219
8220     requester = new qq.AjaxRequester({
8221         validMethods: ["GET"],
8222         method: "GET",
8223         endpointStore: {
8224             getEndpoint: function() {
8225                 return options.endpoint;
8226             }
8227         },
8228         customHeaders: options.customHeaders,
8229         log: options.log,
8230         onComplete: onComplete,
8231         cors: options.cors
8232     });
8233
8234
8235     qq.extend(this, {
8236         queryServer: function() {
8237             var params = qq.extend({}, options.params);
8238
8239             // cache buster, particularly for IE & iOS
8240             params.qqtimestamp = new Date().getTime();
8241
8242             options.log("Session query request.");
8243
8244             requester.initTransport("sessionRefresh")
8245                 .withParams(params)
8246                 .send();
8247         }
8248     });
8249 };
8250
8251 /*globals qq */
8252 // Base handler for UI (FineUploader mode) events.
8253 // Some more specific handlers inherit from this one.
8254 qq.UiEventHandler = function(s, protectedApi) {
8255     "use strict";
8256
8257     var disposer = new qq.DisposeSupport(),
8258         spec = {
8259             eventType: "click",
8260             attachTo: null,
8261             onHandled: function(target, event) {}
8262         };
8263
8264
8265     // This makes up the "public" API methods that will be accessible
8266     // to instances constructing a base or child handler
8267     qq.extend(this, {
8268         addHandler: function(element) {
8269             addHandler(element);
8270         },
8271
8272         dispose: function() {
8273             disposer.dispose();
8274         }
8275     });
8276
8277     function addHandler(element) {
8278         disposer.attach(element, spec.eventType, function(event) {
8279             // Only in IE: the `event` is a property of the `window`.
8280             event = event || window.event;
8281
8282             // On older browsers, we must check the `srcElement` instead of the `target`.
8283             var target = event.target || event.srcElement;
8284
8285             spec.onHandled(target, event);
8286         });
8287     }
8288
8289     // These make up the "protected" API methods that children of this base handler will utilize.
8290     qq.extend(protectedApi, {
8291         getFileIdFromItem: function(item) {
8292             return item.qqFileId;
8293         },
8294
8295         getDisposeSupport: function() {
8296             return disposer;
8297         }
8298     });
8299
8300
8301     qq.extend(spec, s);
8302
8303     if (spec.attachTo) {
8304         addHandler(spec.attachTo);
8305     }
8306 };
8307
8308 /* global qq */
8309 qq.FileButtonsClickHandler = function(s) {
8310     "use strict";
8311
8312     var inheritedInternalApi = {},
8313         spec = {
8314             templating: null,
8315             log: function(message, lvl) {},
8316             onDeleteFile: function(fileId) {},
8317             onCancel: function(fileId) {},
8318             onRetry: function(fileId) {},
8319             onPause: function(fileId) {},
8320             onContinue: function(fileId) {},
8321             onGetName: function(fileId) {}
8322         },
8323         buttonHandlers = {
8324             cancel: function(id) { spec.onCancel(id); },
8325             retry:  function(id) { spec.onRetry(id); },
8326             deleteButton: function(id) { spec.onDeleteFile(id); },
8327             pause: function(id) { spec.onPause(id); },
8328             continueButton: function(id) { spec.onContinue(id); }
8329         };
8330
8331     function examineEvent(target, event) {
8332         qq.each(buttonHandlers, function(buttonType, handler) {
8333             var firstLetterCapButtonType = buttonType.charAt(0).toUpperCase() + buttonType.slice(1),
8334                 fileId;
8335
8336             if (spec.templating["is" + firstLetterCapButtonType](target)) {
8337                 fileId = spec.templating.getFileId(target);
8338                 qq.preventDefault(event);
8339                 spec.log(qq.format("Detected valid file button click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
8340                 handler(fileId);
8341                 return false;
8342             }
8343         });
8344     }
8345
8346     qq.extend(spec, s);
8347
8348     spec.eventType = "click";
8349     spec.onHandled = examineEvent;
8350     spec.attachTo = spec.templating.getFileList();
8351
8352     qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
8353 };
8354
8355 /*globals qq */
8356 // Child of FilenameEditHandler.  Used to detect click events on filename display elements.
8357 qq.FilenameClickHandler = function(s) {
8358     "use strict";
8359
8360     var inheritedInternalApi = {},
8361         spec = {
8362             templating: null,
8363             log: function(message, lvl) {},
8364             classes: {
8365                 file: "qq-upload-file",
8366                 editNameIcon: "qq-edit-filename-icon"
8367             },
8368             onGetUploadStatus: function(fileId) {},
8369             onGetName: function(fileId) {}
8370         };
8371
8372     qq.extend(spec, s);
8373
8374     // This will be called by the parent handler when a `click` event is received on the list element.
8375     function examineEvent(target, event) {
8376         if (spec.templating.isFileName(target) || spec.templating.isEditIcon(target)) {
8377             var fileId = spec.templating.getFileId(target),
8378                 status = spec.onGetUploadStatus(fileId);
8379
8380             // We only allow users to change filenames of files that have been submitted but not yet uploaded.
8381             if (status === qq.status.SUBMITTED) {
8382                 spec.log(qq.format("Detected valid filename click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
8383                 qq.preventDefault(event);
8384
8385                 inheritedInternalApi.handleFilenameEdit(fileId, target, true);
8386             }
8387         }
8388     }
8389
8390     spec.eventType = "click";
8391     spec.onHandled = examineEvent;
8392
8393     qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
8394 };
8395
8396 /*globals qq */
8397 // Child of FilenameEditHandler.  Used to detect focusin events on file edit input elements.
8398 qq.FilenameInputFocusInHandler = function(s, inheritedInternalApi) {
8399     "use strict";
8400
8401     var spec = {
8402             templating: null,
8403             onGetUploadStatus: function(fileId) {},
8404             log: function(message, lvl) {}
8405         };
8406
8407     if (!inheritedInternalApi) {
8408         inheritedInternalApi = {};
8409     }
8410
8411     // This will be called by the parent handler when a `focusin` event is received on the list element.
8412     function handleInputFocus(target, event) {
8413         if (spec.templating.isEditInput(target)) {
8414             var fileId = spec.templating.getFileId(target),
8415                 status = spec.onGetUploadStatus(fileId);
8416
8417             if (status === qq.status.SUBMITTED) {
8418                 spec.log(qq.format("Detected valid filename input focus event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
8419                 inheritedInternalApi.handleFilenameEdit(fileId, target);
8420             }
8421         }
8422     }
8423
8424     spec.eventType = "focusin";
8425     spec.onHandled = handleInputFocus;
8426
8427     qq.extend(spec, s);
8428     qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
8429 };
8430
8431 /*globals qq */
8432 /**
8433  * Child of FilenameInputFocusInHandler.  Used to detect focus events on file edit input elements.  This child module is only
8434  * needed for UAs that do not support the focusin event.  Currently, only Firefox lacks this event.
8435  *
8436  * @param spec Overrides for default specifications
8437  */
8438 qq.FilenameInputFocusHandler = function(spec) {
8439     "use strict";
8440
8441     spec.eventType = "focus";
8442     spec.attachTo = null;
8443
8444     qq.extend(this, new qq.FilenameInputFocusInHandler(spec, {}));
8445 };
8446
8447 /*globals qq */
8448 // Handles edit-related events on a file item (FineUploader mode).  This is meant to be a parent handler.
8449 // Children will delegate to this handler when specific edit-related actions are detected.
8450 qq.FilenameEditHandler = function(s, inheritedInternalApi) {
8451     "use strict";
8452
8453     var spec = {
8454             templating: null,
8455             log: function(message, lvl) {},
8456             onGetUploadStatus: function(fileId) {},
8457             onGetName: function(fileId) {},
8458             onSetName: function(fileId, newName) {},
8459             onEditingStatusChange: function(fileId, isEditing) {}
8460         };
8461
8462
8463     function getFilenameSansExtension(fileId) {
8464         var filenameSansExt = spec.onGetName(fileId),
8465             extIdx = filenameSansExt.lastIndexOf(".");
8466
8467         if (extIdx > 0) {
8468             filenameSansExt = filenameSansExt.substr(0, extIdx);
8469         }
8470
8471         return filenameSansExt;
8472     }
8473
8474     function getOriginalExtension(fileId) {
8475         var origName = spec.onGetName(fileId);
8476         return qq.getExtension(origName);
8477     }
8478
8479     // Callback iff the name has been changed
8480     function handleNameUpdate(newFilenameInputEl, fileId) {
8481         var newName = newFilenameInputEl.value,
8482             origExtension;
8483
8484         if (newName !== undefined && qq.trimStr(newName).length > 0) {
8485             origExtension = getOriginalExtension(fileId);
8486
8487             if (origExtension !== undefined) {
8488                 newName = newName + "." + origExtension;
8489             }
8490
8491             spec.onSetName(fileId, newName);
8492         }
8493
8494         spec.onEditingStatusChange(fileId, false);
8495     }
8496
8497     // The name has been updated if the filename edit input loses focus.
8498     function registerInputBlurHandler(inputEl, fileId) {
8499         inheritedInternalApi.getDisposeSupport().attach(inputEl, "blur", function() {
8500             handleNameUpdate(inputEl, fileId);
8501         });
8502     }
8503
8504     // The name has been updated if the user presses enter.
8505     function registerInputEnterKeyHandler(inputEl, fileId) {
8506         inheritedInternalApi.getDisposeSupport().attach(inputEl, "keyup", function(event) {
8507
8508             var code = event.keyCode || event.which;
8509
8510             if (code === 13) {
8511                 handleNameUpdate(inputEl, fileId);
8512             }
8513         });
8514     }
8515
8516     qq.extend(spec, s);
8517
8518     spec.attachTo = spec.templating.getFileList();
8519
8520     qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
8521
8522     qq.extend(inheritedInternalApi, {
8523         handleFilenameEdit: function(id, target, focusInput) {
8524             var newFilenameInputEl = spec.templating.getEditInput(id);
8525
8526             spec.onEditingStatusChange(id, true);
8527
8528             newFilenameInputEl.value = getFilenameSansExtension(id);
8529
8530             if (focusInput) {
8531                 newFilenameInputEl.focus();
8532             }
8533
8534             registerInputBlurHandler(newFilenameInputEl, id);
8535             registerInputEnterKeyHandler(newFilenameInputEl, id);
8536         }
8537     });
8538 };
8539
8540 /*! 2014-01-19 */