4 * Copyright 2013, Widen Enterprises, Inc. info@fineuploader.com
8 * Homepage: http://fineuploader.com
10 * Repository: git://github.com/Widen/fine-uploader.git
12 * Licensed under GNU GPL v3, see LICENSE
16 /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob, Storage, ActiveXObject */
17 var qq = function(element) {
22 element.style.display = "none";
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);
34 qq(element).detach(type, fn);
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);
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.
56 // compareposition returns false in this case
57 if (element === descendant) {
61 if (element.contains){
62 return element.contains(descendant);
64 /*jslint bitwise: true*/
65 return !!(descendant.compareDocumentPosition(element) & 8);
70 * Insert this element before elementB.
72 insertBefore: function(elementB) {
73 elementB.parentNode.insertBefore(element, elementB);
78 element.parentNode.removeChild(element);
83 * Sets styles for an element.
84 * Fixes opacity in IE6-8.
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!");
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) + ")";
98 qq.extend(element.style, styles);
103 hasClass: function(name) {
104 var re = new RegExp("(^| )" + name + "( |$)");
105 return re.test(element.className);
108 addClass: function(name) {
109 if (!qq(element).hasClass(name)){
110 element.className += " " + name;
115 removeClass: function(name) {
116 var re = new RegExp("(^| )" + name + "( |$)");
117 element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, "");
121 getByClass: function(className) {
125 if (element.querySelectorAll){
126 return element.querySelectorAll("." + className);
129 candidates = element.getElementsByTagName("*");
131 qq.each(candidates, function(idx, val) {
132 if (qq(val).hasClass(className)){
139 children: function() {
141 child = element.firstChild;
144 if (child.nodeType === 1){
145 children.push(child);
147 child = child.nextSibling;
153 setText: function(text) {
154 element.innerText = text;
155 element.textContent = text;
159 clearText: function() {
160 return qq(element).setText("");
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) {
168 if (element.hasAttribute) {
170 if (!element.hasAttribute(attrName)) {
175 return (/^false$/i).exec(element.getAttribute(attrName)) == null;
178 attrVal = element[attrName];
180 if (attrVal === undefined) {
185 return (/^false$/i).exec(attrVal) == null;
194 qq.log = function(message, level) {
195 if (window.console) {
196 if (!level || level === "info") {
197 window.console.log(message);
201 if (window.console[level]) {
202 window.console[level](message);
205 window.console.log("<" + level + "> " + message);
211 qq.isObject = function(variable) {
212 return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]";
215 qq.isFunction = function(variable) {
216 return typeof(variable) === "function";
220 * Check the type of a value. Is it an "array"?
222 * @param value value to test.
223 * @returns true if the value is an array or associated with an `ArrayBuffer`
225 qq.isArray = function(value) {
226 return Object.prototype.toString.call(value) === "[object Array]" ||
227 (value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer);
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]";
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);
244 qq.isString = function(maybeString) {
245 return Object.prototype.toString.call(maybeString) === "[object String]";
248 qq.trimStr = function(string) {
249 if (String.prototype.trim) {
250 return string.trim();
253 return string.replace(/^\s+|\s+$/g,"");
258 * @param str String to format.
259 * @returns {string} A string, swapping argument values with the associated occurrence of {} in the passed string.
261 qq.format = function(str) {
263 var args = Array.prototype.slice.call(arguments, 1),
265 nextIdxToReplace = newStr.indexOf("{}");
267 qq.each(args, function(idx, val) {
268 var strBefore = newStr.substring(0, nextIdxToReplace),
269 strAfter = newStr.substring(nextIdxToReplace+2);
271 newStr = strBefore + val + strAfter;
272 nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length);
274 // End the loop if we have run out of tokens (when the arguments exceed the # of tokens)
275 if (nextIdxToReplace < 0) {
283 qq.isFile = function(maybeFile) {
284 return window.File && Object.prototype.toString.call(maybeFile) === "[object File]";
287 qq.isFileList = function(maybeFileList) {
288 return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]";
291 qq.isFileOrInput = function(maybeFileOrInput) {
292 return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput);
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") {
303 if (maybeInput.tagName) {
304 if (maybeInput.tagName.toLowerCase() === "input") {
305 if (maybeInput.type && maybeInput.type.toLowerCase() === "file") {
314 qq.isBlob = function(maybeBlob) {
315 return window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]";
318 qq.isXhrUploadSupported = function() {
319 var input = document.createElement("input");
323 input.multiple !== undefined &&
324 typeof File !== "undefined" &&
325 typeof FormData !== "undefined" &&
326 typeof (qq.createXhrInstance()).upload !== "undefined" );
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();
336 return new ActiveXObject("MSXML2.XMLHTTP.3.0");
339 qq.log("Neither XHR or ActiveX are supported!", "error");
344 qq.isFolderDropSupported = function(dataTransfer) {
345 return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
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);
354 qq.sliceBlob = function(fileOrBlob, start, end) {
355 var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice;
357 return slicer.call(fileOrBlob, start, end);
360 qq.arrayBufferToHex = function(buffer) {
362 bytes = new Uint8Array(buffer);
365 qq.each(bytes, function(idx, byte) {
366 var byteAsHexStr = byte.toString(16);
368 if (byteAsHexStr.length < 2) {
369 byteAsHexStr = "0" + byteAsHexStr;
372 bytesAsHex += byteAsHexStr;
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();
383 fileReader.onload = function() {
384 promise.success(qq.arrayBufferToHex(fileReader.result));
387 fileReader.readAsArrayBuffer(initialBlob);
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) {
398 qq.extend(first[prop], val, true);
409 * Allow properties in one object to override properties in another,
410 * keeping track of the original values from the target object.
412 * Note that the pre-overriden properties to be overriden by the source will be passed into the `sourceFn` when it is invoked.
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
418 qq.override = function(target, sourceFn) {
420 source = sourceFn(super_);
422 qq.each(source, function(srcPropName, srcPropVal) {
423 if (target[srcPropName] !== undefined) {
424 super_[srcPropName] = target[srcPropName];
427 target[srcPropName] = srcPropVal;
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
437 qq.indexOf = function(arr, elt, from){
439 return arr.indexOf(elt, from);
443 var len = arr.length;
449 for (; from < len; from+=1){
450 if (arr.hasOwnProperty(from) && arr[from] === elt){
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);
467 // Browsers and platforms detection
470 return navigator.userAgent.indexOf("MSIE") !== -1;
473 return navigator.userAgent.indexOf("MSIE 7") !== -1;
475 qq.ie10 = function(){
476 return navigator.userAgent.indexOf("MSIE 10") !== -1;
478 qq.ie11 = function(){
479 return (navigator.userAgent.indexOf("Trident") !== -1 &&
480 navigator.userAgent.indexOf("rv:11") !== -1);
482 qq.safari = function(){
483 return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
485 qq.chrome = function(){
486 return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1;
488 qq.opera = function(){
489 return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1;
491 qq.firefox = function(){
492 return (!qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === "");
494 qq.windows = function(){
495 return navigator.platform === "Win32";
497 qq.android = function(){
498 return navigator.userAgent.toLowerCase().indexOf("android") !== -1;
500 qq.ios7 = function() {
501 return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1;
503 qq.ios = function() {
505 return navigator.userAgent.indexOf("iPad") !== -1
506 || navigator.userAgent.indexOf("iPod") !== -1
507 || navigator.userAgent.indexOf("iPhone") !== -1;
513 qq.preventDefault = function(e){
514 if (e.preventDefault){
517 e.returnValue = false;
522 * Creates and returns element from html string
523 * Uses innerHTML to create an element
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);
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;
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) {
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) {
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) {
568 for (keyOrIndex in iterableItem) {
569 if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) {
570 retVal = callback(keyOrIndex, iterableItem[keyOrIndex]);
571 if (retVal === false) {
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);
586 var newArgs = qq.extend([], args);
587 if (arguments.length) {
588 newArgs = newArgs.concat(Array.prototype.slice.call(arguments));
590 return oldFunc.apply(context, newArgs);
594 throw new Error("first parameter must be a function!");
598 * obj2url() takes a json-object as argument and generates
599 * a querystring. pretty much like jQuery.param()
603 * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
607 * `http://any.url/upload?otherParam=value&a=b&c=d`
609 * @param Object JSON-Object
610 * @param String current querystring-part
611 * @return String encoded querystring
613 qq.obj2url = function(obj, temp, prefixDone){
614 /*jshint laxbreak: true*/
617 add = function(nextObj, i){
619 ? (/\[\]$/.test(temp)) // prevent double-encoding
623 if ((nextTemp !== "undefined") && (i !== "undefined")) {
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)
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) {
642 } else if ((typeof obj !== "undefined") && (obj !== null) && (typeof obj === "object")){
643 qq.each(obj, function(prop, val) {
647 uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj));
651 return uristrings.join(prefix);
653 return uristrings.join(prefix)
655 .replace(/%20/g, "+");
659 qq.obj2FormData = function(obj, formData, arrayKeyName) {
661 formData = new FormData();
664 qq.each(obj, function(key, val) {
665 key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key;
667 if (qq.isObject(val)) {
668 qq.obj2FormData(val, formData, key);
670 else if (qq.isFunction(val)) {
671 formData.append(key, val());
674 formData.append(key, val);
681 qq.obj2Inputs = function(obj, form) {
685 form = document.createElement("form");
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);
700 qq.setCookie = function(name, value, days) {
701 var date = new Date(),
705 date.setTime(date.getTime()+(days*24*60*60*1000));
706 expires = "; expires="+date.toGMTString();
709 document.cookie = name+"="+value+expires+"; path=/";
712 qq.getCookie = function(name) {
713 var nameEQ = name + "=",
714 ca = document.cookie.split(";"),
717 qq.each(ca, function(idx, part) {
719 var cookiePart = part;
720 while (cookiePart.charAt(0) == " ") {
721 cookiePart = cookiePart.substring(1, cookiePart.length);
724 if (cookiePart.indexOf(nameEQ) === 0) {
725 cookie = cookiePart.substring(nameEQ.length, cookiePart.length);
733 qq.getCookieNames = function(regexp) {
734 var cookies = document.cookie.split(";"),
737 qq.each(cookies, function(idx, cookie) {
738 cookie = qq.trimStr(cookie);
740 var equalsIdx = cookie.indexOf("=");
742 if (cookie.match(regexp)) {
743 cookieNames.push(cookie.substr(0, equalsIdx));
750 qq.deleteCookie = function(name) {
751 qq.setCookie(name, "", -1);
754 qq.areCookiesEnabled = function() {
755 var randNum = Math.random() * 100000,
756 name = "qqCookieTest:" + randNum;
757 qq.setCookie(name, 1);
759 if (qq.getCookie(name)) {
760 qq.deleteCookie(name);
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.
770 qq.parseJson = function(json) {
771 /*jshint evil: true*/
772 if (window.JSON && qq.isFunction(JSON.parse)) {
773 return JSON.parse(json);
775 return eval("(" + json + ")");
780 * Retrieve the extension of a file, if it exists.
783 * @returns {string || undefined}
785 qq.getExtension = function(filename) {
786 var extIdx = filename.lastIndexOf(".") + 1;
789 return filename.substr(extIdx, filename.length - extIdx);
793 qq.getFilename = function(blobOrFileInput) {
794 /*jslint regexp: true*/
796 if (qq.isInput(blobOrFileInput)) {
797 // get input value and remove path to normalize
798 return blobOrFileInput.value.replace(/.*(\/|\\)/, "");
800 else if (qq.isFile(blobOrFileInput)) {
801 if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) {
802 return blobOrFileInput.fileName;
806 return blobOrFileInput.name;
810 * A generic module which supports object disposing in dispose() method.
812 qq.DisposeSupport = function() {
816 /** Run all registered disposers */
817 dispose: function() {
820 disposer = disposers.shift();
828 /** Attach event handler and register de-attacher as a disposer */
830 var args = arguments;
831 /*jslint undef:true*/
832 this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
835 /** Add disposer to the collection */
836 addDisposer: function(disposeFunction) {
837 disposers.push(disposeFunction);
845 * Fine Uploader top-level Error container. Inherits from `Error`.
850 qq.Error = function(message) {
851 this.message = message;
854 qq.Error.prototype = new Error();
861 qq.supportedFeatures = (function () {
864 var supportsUploading,
865 supportsAjaxFileUploading,
869 supportsUploadViaPaste,
871 supportsDeleteFileXdr,
872 supportsDeleteFileCorsXhr,
873 supportsDeleteFileCors,
874 supportsFolderSelection,
875 supportsImagePreviews;
878 function testSupportsFileInputElement() {
879 var supported = true,
883 tempInput = document.createElement("input");
884 tempInput.type = "file";
885 qq(tempInput).hide();
887 if (tempInput.disabled) {
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;
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;
910 //Ensure we can send cross-origin `XMLHttpRequest`s
911 function isCrossOriginXhrSupported() {
912 if (window.XMLHttpRequest) {
913 var xhr = qq.createXhrInstance();
915 //Commonly accepted test for XHR CORS support.
916 return xhr.withCredentials !== undefined;
922 //Test for (terrible) cross-origin ajax transport fallback for IE9 and IE8
923 function isXdrSupported() {
924 return window.XDomainRequest !== undefined;
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()) {
934 return isXdrSupported();
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;
943 supportsUploading = testSupportsFileInputElement();
945 supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported();
947 supportsFolderDrop = supportsAjaxFileUploading && isChrome21OrHigher();
949 supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported();
951 supportsResume = supportsAjaxFileUploading && supportsChunking && qq.areCookiesEnabled();
953 supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher();
955 supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading);
957 supportsDeleteFileCorsXhr = isCrossOriginXhrSupported();
959 supportsDeleteFileXdr = isXdrSupported();
961 supportsDeleteFileCors = isCrossOriginAjaxSupported();
963 supportsFolderSelection = isFolderSelectionSupported();
965 supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined;
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
994 qq.Promise = function() {
997 var successArgs, failureArgs,
998 successCallbacks = [],
999 failureCallbacks = [],
1004 then: function(onSuccess, onFailure) {
1007 successCallbacks.push(onSuccess);
1010 failureCallbacks.push(onFailure);
1013 else if (state === -1) {
1014 onFailure && onFailure.apply(null, failureArgs);
1016 else if (onSuccess) {
1017 onSuccess.apply(null,successArgs);
1023 done: function(callback) {
1025 doneCallbacks.push(callback);
1028 callback.apply(null, failureArgs === undefined ? successArgs : failureArgs);
1034 success: function() {
1036 successArgs = arguments;
1038 if (successCallbacks.length) {
1039 qq.each(successCallbacks, function(idx, callback) {
1040 callback.apply(null, successArgs);
1044 if(doneCallbacks.length) {
1045 qq.each(doneCallbacks, function(idx, callback) {
1046 callback.apply(null, successArgs);
1053 failure: function() {
1055 failureArgs = arguments;
1057 if (failureCallbacks.length) {
1058 qq.each(failureCallbacks, function(idx, callback) {
1059 callback.apply(null, failureArgs);
1063 if(doneCallbacks.length) {
1064 qq.each(doneCallbacks, function(idx, callback) {
1065 callback.apply(null, failureArgs);
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.
1082 * TODO Eliminate the mouseover and mouseout event handlers since the :hover CSS pseudo-class should now be
1083 * available on all supported browsers.
1085 * @param o Options to override the default values
1087 qq.UploadButton = function(o) {
1091 var disposeSupport = new qq.DisposeSupport(),
1094 // "Container" element
1097 // If true adds `multiple` attribute to `<input type="file">`
1100 // Corresponds to the `accept` attribute on the associated `<input type="file">`
1103 // A true value allows folders to be selected, if supported by the UA
1106 // `name` attribute of `<input type="file">`
1109 // Called when the browser invokes the onchange handler on the `<input type="file">`
1110 onChange: function(input) {},
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",
1115 focusClass: "qq-upload-button-focus"
1119 // Overrides any of the default option values with any option values passed in during construction.
1120 qq.extend(options, o);
1122 buttonId = qq.getUniqueId();
1124 // Embed an opaque `<input type="file">` element as a child of `options.element`.
1125 function createInput() {
1126 var input = document.createElement("input");
1128 input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId);
1130 if (options.multiple) {
1131 input.setAttribute("multiple", "");
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", "");
1139 if (options.acceptFiles) {
1140 input.setAttribute("accept", options.acceptFiles);
1143 input.setAttribute("type", "file");
1144 input.setAttribute("name", options.name);
1147 position: "absolute",
1148 // in Opera only 'browse' button
1149 // is clickable and it is located at
1150 // the right side of the input
1153 fontFamily: "Arial",
1154 // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
1162 options.element.appendChild(input);
1164 disposeSupport.attach(input, "change", function(){
1165 options.onChange(input);
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);
1172 disposeSupport.attach(input, "mouseout", function(){
1173 qq(options.element).removeClass(options.hoverClass);
1176 disposeSupport.attach(input, "focus", function(){
1177 qq(options.element).addClass(options.focusClass);
1179 disposeSupport.attach(input, "blur", function(){
1180 qq(options.element).removeClass(options.focusClass);
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");
1193 // Make button suitable container for input
1194 qq(options.element).css({
1195 position: "relative",
1197 // Make sure browse button is in the right side in Internet Explorer
1201 input = createInput();
1206 getInput: function() {
1210 getButtonId: function() {
1214 setMultiple: function(isMultiple) {
1215 if (isMultiple !== options.multiple) {
1217 input.setAttribute("multiple", "");
1220 input.removeAttribute("multiple");
1225 setAcceptFiles: function(acceptFiles) {
1226 if (acceptFiles !== options.acceptFiles) {
1227 input.setAttribute("accept", acceptFiles);
1232 if (input.parentNode){
1236 qq(options.element).removeClass(options.focusClass);
1237 input = createInput();
1242 qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id";
1245 qq.UploadData = function(uploaderProxy) {
1253 function getDataByIds(idOrIds) {
1254 if (qq.isArray(idOrIds)) {
1257 qq.each(idOrIds, function(idx, id) {
1258 entries.push(data[id]);
1264 return data[idOrIds];
1267 function getDataByUuids(uuids) {
1268 if (qq.isArray(uuids)) {
1271 qq.each(uuids, function(idx, uuid) {
1272 entries.push(data[byUuid[uuid]]);
1278 return data[byUuid[uuids]];
1281 function getDataByStatus(status) {
1282 var statusResults = [],
1283 statuses = [].concat(status);
1285 qq.each(statuses, function(index, statusEnum) {
1286 var statusResultIndexes = byStatus[statusEnum];
1288 if (statusResultIndexes !== undefined) {
1289 qq.each(statusResultIndexes, function(i, dataIndex) {
1290 statusResults.push(data[dataIndex]);
1295 return statusResults;
1300 * Adds a new file to the data cache for tracking purposes.
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.
1308 addFile: function(uuid, name, size, status) {
1309 status = status || qq.status.SUBMITTING;
1311 var id = data.push({
1322 if (byStatus[status] === undefined) {
1323 byStatus[status] = [];
1325 byStatus[status].push(id);
1327 uploaderProxy.onStatusChange(id, null, status);
1332 retrieve: function(optionalFilter) {
1333 if (qq.isObject(optionalFilter) && data.length) {
1334 if (optionalFilter.id !== undefined) {
1335 return getDataByIds(optionalFilter.id);
1338 else if (optionalFilter.uuid !== undefined) {
1339 return getDataByUuids(optionalFilter.uuid);
1342 else if (optionalFilter.status) {
1343 return getDataByStatus(optionalFilter.status);
1347 return qq.extend([], data, true);
1357 setStatus: function(id, newStatus) {
1358 var oldStatus = data[id].status,
1359 byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id);
1361 byStatus[oldStatus].splice(byStatusOldStatusIndex, 1);
1363 data[id].status = newStatus;
1365 if (byStatus[newStatus] === undefined) {
1366 byStatus[newStatus] = [];
1368 byStatus[newStatus].push(id);
1370 uploaderProxy.onStatusChange(id, oldStatus, newStatus);
1373 uuidChanged: function(id, newUuid) {
1374 var oldUuid = data[id].uuid;
1376 data[id].uuid = newUuid;
1377 byUuid[newUuid] = id;
1378 delete byUuid[oldUuid];
1381 updateName: function(id, newName) {
1382 data[id].name = newName;
1388 SUBMITTING: "submitting",
1389 SUBMITTED: "submitted",
1390 REJECTED: "rejected",
1392 CANCELED: "canceled",
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",
1405 * Defines the public API for FineUploaderBasic mode.
1410 qq.basePublicApi = {
1411 log: function(str, level) {
1412 if (this._options.debug && (!level || level === "info")) {
1413 qq.log("[FineUploader " + qq.version + "] " + str);
1415 else if (level && level !== "info") {
1416 qq.log("[FineUploader " + qq.version + "] " + str, level);
1421 setParams: function(params, id) {
1422 /*jshint eqeqeq: true, eqnull: true*/
1424 this._options.request.params = params;
1427 this._paramsStore.setParams(params, id);
1431 setDeleteFileParams: function(params, id) {
1432 /*jshint eqeqeq: true, eqnull: true*/
1434 this._options.deleteFile.params = params;
1437 this._deleteFileParamsStore.setParams(params, id);
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*/
1445 this._options.request.endpoint = endpoint;
1448 this._endpointStore.setEndpoint(endpoint, id);
1452 getInProgress: function() {
1453 return this._uploadData.retrieve({
1455 qq.status.UPLOADING,
1456 qq.status.UPLOAD_RETRYING,
1462 getNetUploads: function() {
1463 return this._netUploaded;
1466 uploadStoredFiles: function() {
1469 if (this._storedIds.length === 0) {
1470 this._itemError("noFilesError");
1473 while (this._storedIds.length) {
1474 idToUpload = this._storedIds.shift();
1475 this._handler.upload(idToUpload);
1480 clearStoredFiles: function(){
1481 this._storedIds = [];
1484 retry: function(id) {
1485 return this._manualRetry(id);
1488 cancel: function(id) {
1489 this._handler.cancel(id);
1492 cancelAll: function() {
1493 var storedIdsCopy = [],
1496 qq.extend(storedIdsCopy, this._storedIds);
1497 qq.each(storedIdsCopy, function(idx, storedFileId) {
1498 self.cancel(storedFileId);
1501 this._handler.cancelAll();
1505 this.log("Resetting uploader...");
1507 this._handler.reset();
1508 this._storedIds = [];
1509 this._autoRetries = [];
1510 this._retryTimeouts = [];
1511 this._preventRetries = [];
1512 this._thumbnailUrls = [];
1514 qq.each(this._buttons, function(idx, button) {
1518 this._paramsStore.reset();
1519 this._endpointStore.reset();
1520 this._netUploadedOrQueued = 0;
1521 this._netUploaded = 0;
1522 this._uploadData.reset();
1523 this._buttonIdsForFileIds = [];
1525 this._pasteHandler && this._pasteHandler.reset();
1526 this._options.session.refreshOnReset && this._refreshSessionData();
1529 addFiles: function(filesOrInputs, params, endpoint) {
1530 var verifiedFilesOrInputs = [],
1531 fileOrInputIndex, fileOrInput, fileIndex;
1533 if (filesOrInputs) {
1534 if (!qq.isFileList(filesOrInputs)) {
1535 filesOrInputs = [].concat(filesOrInputs);
1538 for (fileOrInputIndex = 0; fileOrInputIndex < filesOrInputs.length; fileOrInputIndex+=1) {
1539 fileOrInput = filesOrInputs[fileOrInputIndex];
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);
1548 this._handleNewFile(fileOrInput, verifiedFilesOrInputs);
1552 this.log(fileOrInput + " is not a File or INPUT element! Ignoring!", "warn");
1556 this.log("Received " + verifiedFilesOrInputs.length + " files or inputs.");
1557 this._prepareItemsForUpload(verifiedFilesOrInputs, params, endpoint);
1561 addBlobs: function(blobDataOrArray, params, endpoint) {
1562 if (blobDataOrArray) {
1563 var blobDataArray = [].concat(blobDataOrArray),
1564 verifiedBlobDataList = [],
1567 qq.each(blobDataArray, function(idx, blobData) {
1570 if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
1573 name: self._options.blobs.defaultName
1576 else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
1577 blobOrBlobData = blobData;
1580 self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
1583 blobOrBlobData && self._handleNewFile(blobOrBlobData, verifiedBlobDataList);
1586 this._prepareItemsForUpload(verifiedBlobDataList, params, endpoint);
1589 this.log("undefined or non-array parameter passed into addBlobs", "error");
1593 getUuid: function(id) {
1594 return this._uploadData.retrieve({id: id}).uuid;
1597 setUuid: function(id, newUuid) {
1598 return this._uploadData.uuidChanged(id, newUuid);
1601 getResumableFilesData: function() {
1602 return this._handler.getResumableFilesData();
1605 getSize: function(id) {
1606 return this._uploadData.retrieve({id: id}).size;
1609 getName: function(id) {
1610 return this._uploadData.retrieve({id: id}).name;
1613 setName: function(id, newName) {
1614 this._uploadData.updateName(id, newName);
1617 getFile: function(fileOrBlobId) {
1618 return this._handler.getFile(fileOrBlobId);
1621 deleteFile: function(id) {
1622 this._onSubmitDelete(id);
1625 setDeleteFileEndpoint: function(endpoint, id) {
1626 /*jshint eqeqeq: true, eqnull: true*/
1628 this._options.deleteFile.endpoint = endpoint;
1631 this._deleteFileEndpointStore.setEndpoint(endpoint, id);
1635 doesExist: function(fileOrBlobId) {
1636 return this._handler.isValid(fileOrBlobId);
1639 getUploads: function(optionalFilter) {
1640 return this._uploadData.retrieve(optionalFilter);
1643 getButton: function(fileId) {
1644 return this._getButton(this._buttonIdsForFileIds[fileId]);
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],
1656 maxSize: maxSize > 0 ? maxSize : null
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);
1665 /* jshint eqeqeq:false,eqnull:true */
1666 if (fileOrUrl == null) {
1667 return new qq.Promise().failure(imgOrCanvas, "File or URL not found.");
1670 return this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options);
1674 pauseUpload: function(id) {
1675 var uploadData = this._uploadData.retrieve({id: id});
1677 if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) {
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);
1688 qq.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error");
1692 qq.log(qq.format("Ignoring pause for file ID {} ({}). Not in progress.", id, this.getName(id)), "error");
1698 continueUpload: function(id) {
1699 var uploadData = this._uploadData.retrieve({id: id});
1701 if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) {
1705 if (uploadData.status === qq.status.PAUSED) {
1706 qq.log(qq.format("Paused file ID {} ({}) will be continued. Not paused.", id, this.getName(id)));
1708 if (!this._handler.upload(id)) {
1709 this._uploadData.setStatus(id, qq.status.QUEUED);
1714 qq.log(qq.format("Ignoring continue for file ID {} ({}). Not paused.", id, this.getName(id)), "error");
1720 getRemainingAllowedItems: function() {
1721 var allowedItems = this._options.validation.itemLimit;
1723 if (allowedItems > 0) {
1724 return this._options.validation.itemLimit - this._netUploadedOrQueued;
1735 * Defines the private (internal) API for FineUploaderBasic mode.
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() {
1743 options = this._options.session;
1745 /* jshint eqnull:true */
1746 if (qq.Session && this._options.session.endpoint != null) {
1747 if (!this._session) {
1748 qq.extend(options, this._options.cors);
1750 options.log = qq.bind(this.log, this);
1751 options.addFileRecord = qq.bind(this._addCannedFile, this);
1753 this._session = new qq.Session(options);
1756 setTimeout(function() {
1757 self._session.refresh().then(function(response, xhrOrXdr) {
1759 self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr);
1761 }, function(response, xhrOrXdr) {
1763 self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr);
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);
1774 sessionData.deleteFileEndpoint && this.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id);
1775 sessionData.deleteFileParams && this.setDeleteFileParams(sessionData.deleteFileParams, id);
1777 if (sessionData.thumbnailUrl) {
1778 this._thumbnailUrls[id] = sessionData.thumbnailUrl;
1781 this._netUploaded++;
1782 this._netUploadedOrQueued++;
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) {
1790 uuid = qq.getUniqueId(),
1791 name = qq.getFilename(file),
1794 if (file.size >= 0) {
1797 else if (file.blob) {
1798 size = file.blob.size;
1801 id = this._uploadData.addFile(uuid, name, size);
1802 this._handler.add(id, file);
1804 this._netUploadedOrQueued++;
1806 newFileWrapperList.push({id: id, file: file});
1809 // Creates an internal object that tracks various properties of each extra button,
1810 // and then actually creates the extra button.
1811 _generateExtraButtonSpecs: function() {
1814 this._extraButtonSpecs = {};
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);
1821 if (multiple === undefined) {
1822 multiple = self._options.multiple;
1825 if (extraButtonSpec.validation) {
1826 qq.extend(validation, extraButtonOptionEntry.validation, true);
1829 qq.extend(extraButtonSpec, {
1831 validation: validation
1834 self._initExtraButton(extraButtonSpec);
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
1848 this._extraButtonSpecs[button.getButtonId()] = spec;
1852 * Gets the internally used tracking ID for a button.
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
1858 _getButtonId: function(buttonOrFileInputOrFile) {
1859 var inputs, fileInput;
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;
1866 else if (buttonOrFileInputOrFile.tagName.toLowerCase() === "input" &&
1867 buttonOrFileInputOrFile.type.toLowerCase() === "file") {
1869 return buttonOrFileInputOrFile.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME);
1872 inputs = buttonOrFileInputOrFile.getElementsByTagName("input");
1874 qq.each(inputs, function(idx, input) {
1875 if (input.getAttribute("type") === "file") {
1882 return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME);
1887 _annotateWithButtonId: function(file, associatedInput) {
1888 if (qq.isFile(file)) {
1889 file.qqButtonId = this._getButtonId(associatedInput);
1893 _getButton: function(buttonId) {
1894 var extraButtonsSpec = this._extraButtonSpecs[buttonId];
1896 if (extraButtonsSpec) {
1897 return extraButtonsSpec.element;
1899 else if (buttonId === this._defaultButtonId) {
1900 return this._options.button;
1904 _handleCheckedCallback: function(details) {
1906 callbackRetVal = details.callback();
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);
1916 if (details.onFailure) {
1917 self.log(details.name + " promise failure for " + details.identifier);
1918 details.onFailure();
1921 self.log(details.name + " promise failure for " + details.identifier);
1926 if (callbackRetVal !== false) {
1927 details.onSuccess(callbackRetVal);
1930 if (details.onFailure) {
1931 this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback.");
1932 details.onFailure();
1935 this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed.");
1939 return callbackRetVal;
1943 * Generate a tracked upload button.
1945 * @param spec Object containing a required `element` property
1946 * along with optional `multiple`, `accept`, and `folders`.
1947 * @returns {qq.UploadButton}
1950 _createUploadButton: function(spec) {
1952 acceptFiles = spec.accept || this._options.validation.acceptFiles,
1953 allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions;
1955 function allowMultiple() {
1956 if (qq.supportedFeatures.ajaxUploading) {
1957 // Workaround for bug in iOS7 (see #1039)
1958 if (qq.ios7() && self._isAllowedExtension(allowedExtensions, ".mov")) {
1962 if (spec.multiple === undefined) {
1963 return self._options.multiple;
1966 return spec.multiple;
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);
1981 hoverClass: this._options.classes.buttonHover,
1982 focusClass: this._options.classes.buttonFocus
1985 this._disposeSupport.addDisposer(function() {
1989 self._buttons.push(button);
1994 _createUploadHandler: function(additionalOptions, namespace) {
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);
2011 onComplete: function(id, name, result, xhr){
2012 var retVal = self._onComplete(id, name, result, xhr);
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);
2022 self._options.callbacks.onComplete(id, name, result, xhr);
2025 onCancel: function(id, name) {
2026 return self._handleCheckedCallback({
2028 callback: qq.bind(self._options.callbacks.onCancel, self, id, name),
2029 onSuccess: qq.bind(self._onCancel, self, id, name),
2033 onUpload: function(id, name) {
2034 self._onUpload(id, name);
2035 self._options.callbacks.onUpload(id, name);
2037 onUploadChunk: function(id, name, chunkData) {
2038 self._onUploadChunk(id, chunkData);
2039 self._options.callbacks.onUploadChunk(id, name, chunkData);
2041 onUploadChunkSuccess: function(id, chunkData, result, xhr) {
2042 self._options.callbacks.onUploadChunkSuccess.apply(self, arguments);
2044 onResume: function(id, name, chunkData) {
2045 return self._options.callbacks.onResume(id, name, chunkData);
2047 onAutoRetry: function(id, name, responseJSON, xhr) {
2048 return self._onAutoRetry.apply(self, arguments);
2050 onUuidChanged: function(id, newUuid) {
2051 self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'");
2052 self.setUuid(id, newUuid);
2054 getName: qq.bind(self.getName, self),
2055 getUuid: qq.bind(self.getUuid, self),
2056 getSize: qq.bind(self.getSize, self)
2059 qq.each(this._options.request, function(prop, val) {
2060 options[prop] = val;
2063 if (additionalOptions) {
2064 qq.each(additionalOptions, function(key, val) {
2069 return new qq.UploadHandler(options, namespace);
2072 _createDeleteHandler: function() {
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) {
2087 self._options.callbacks.onDelete(id);
2089 onDeleteComplete: function(id, xhrOrXdr, isError) {
2090 self._onDeleteComplete(id, xhrOrXdr, isError);
2091 self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError);
2097 _createPasteHandler: function() {
2100 return new qq.PasteSupport({
2101 targetElement: this._options.paste.targetElement,
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"
2116 _createUploadDataTracker: function() {
2119 return new qq.UploadData({
2120 getName: function(id) {
2121 return self.getName(id);
2123 getUuid: function(id) {
2124 return self.getUuid(id);
2126 getSize: function(id) {
2127 return self.getSize(id);
2129 onStatusChange: function(id, oldStatus, newStatus) {
2130 self._onUploadStatusChange(id, oldStatus, newStatus);
2131 self._options.callbacks.onStatusChange(id, oldStatus, newStatus);
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]);
2143 _handlePasteSuccess: function(blob, extSuppliedName) {
2144 var extension = blob.type.split("/")[1],
2145 name = extSuppliedName;
2147 /*jshint eqeqeq: true, eqnull: true*/
2149 name = this._options.paste.defaultName;
2152 name += "." + extension;
2160 _preventLeaveInProgress: function(){
2163 this._disposeSupport.attach(window, "beforeunload", function(e){
2164 if (self.getInProgress()) {
2165 e = e || window.event;
2167 e.returnValue = self._options.messages.onLeave;
2169 return self._options.messages.onLeave;
2174 _onSubmit: function(id, name) {
2175 //nothing to do yet in core uploader
2178 _onProgress: function(id, name, loaded, total) {
2179 //nothing to do yet in core uploader
2182 _onComplete: function(id, name, result, xhr) {
2183 if (!result.success) {
2184 this._netUploadedOrQueued--;
2185 this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED);
2188 if (result.thumbnailUrl) {
2189 this._thumbnailUrls[id] = result.thumbnailUrl;
2192 this._netUploaded++;
2193 this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL);
2196 this._maybeParseAndSendUploadError(id, name, result, xhr);
2198 return result.success ? true : false;
2201 _onCancel: function(id, name) {
2202 this._netUploadedOrQueued--;
2204 clearTimeout(this._retryTimeouts[id]);
2206 var storedItemIndex = qq.indexOf(this._storedIds, id);
2207 if (!this._options.autoUpload && storedItemIndex >= 0) {
2208 this._storedIds.splice(storedItemIndex, 1);
2211 this._uploadData.setStatus(id, qq.status.CANCELED);
2214 _isDeletePossible: function() {
2215 if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) {
2219 if (this._options.cors.expected) {
2220 if (qq.supportedFeatures.deleteFileCorsXhr) {
2224 if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) {
2234 _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) {
2235 var uuid = this.getUuid(id),
2236 adjustedOnSuccessCallback;
2238 if (onSuccessCallback) {
2239 adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams);
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),
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");
2258 _onDelete: function(id) {
2259 this._uploadData.setStatus(id, qq.status.DELETING);
2262 _onDeleteComplete: function(id, xhrOrXdr, isError) {
2263 var name = this.getName(id);
2266 this._uploadData.setStatus(id, qq.status.DELETE_FAILED);
2267 this.log("Delete request for '" + name + "' has failed.", "error");
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);
2275 this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr);
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.");
2287 _onUpload: function(id, name) {
2288 this._uploadData.setStatus(id, qq.status.UPLOADING);
2291 _onUploadChunk: function(id, chunkData) {
2292 //nothing to do in the base uploader
2295 _onInputChange: function(input) {
2298 if (qq.supportedFeatures.ajaxUploading) {
2299 for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) {
2300 this._annotateWithButtonId(input.files[fileIndex], input);
2303 this.addFiles(input.files);
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);
2310 qq.each(this._buttons, function(idx, button) {
2315 _onBeforeAutoRetry: function(id, name) {
2316 this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
2320 * Attempt to automatically retry a failed upload.
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
2331 _onAutoRetry: function(id, name, responseJSON, xhr, callback) {
2334 self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
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);
2341 self._retryTimeouts[id] = setTimeout(function() {
2342 self.log("Retrying " + name + "...");
2343 self._autoRetries[id]++;
2344 self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
2350 self._handler.retry(id);
2352 }, self._options.retry.autoAttemptDelay * 1000);
2358 _shouldAutoRetry: function(id, name, responseJSON) {
2359 var uploadData = this._uploadData.retrieve({id: id});
2361 /*jshint laxbreak: true */
2362 if (!this._preventRetries[id]
2363 && this._options.retry.enableAuto
2364 && uploadData.status !== qq.status.PAUSED) {
2366 if (this._autoRetries[id] === undefined) {
2367 this._autoRetries[id] = 0;
2370 return this._autoRetries[id] < this._options.retry.maxAutoAttempts;
2376 //return false if we should not attempt the requested retry
2377 _onBeforeManualRetry: function(id) {
2378 var itemLimit = this._options.validation.itemLimit;
2380 if (this._preventRetries[id]) {
2381 this.log("Retries are forbidden for id " + id, "warn");
2384 else if (this._handler.isValid(id)) {
2385 var fileName = this.getName(id);
2387 if (this._options.callbacks.onManualRetry(id, fileName) === false) {
2391 if (itemLimit > 0 && this._netUploadedOrQueued+1 > itemLimit) {
2392 this._itemError("retryFailTooManyItems");
2396 this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
2400 this.log("'" + id + "' is not a valid file ID", "error");
2406 * Conditionally orders a manual retry of a failed upload.
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
2414 _manualRetry: function(id, callback) {
2415 if (this._onBeforeManualRetry(id)) {
2416 this._netUploadedOrQueued++;
2417 this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
2423 this._handler.retry(id);
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);
2438 var errorReason = response.error ? response.error : this._options.text.defaultResponseError;
2439 this._options.callbacks.onError(id, name, errorReason, xhr);
2444 _prepareItemsForUpload: function(items, params, endpoint) {
2445 var validationDescriptors = this._getValidationDescriptors(items),
2446 buttonId = this._getButtonId(items[0].file),
2447 button = this._getButton(buttonId);
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"
2458 _upload: function(id, params, endpoint) {
2459 var name = this.getName(id);
2462 this.setParams(params, id);
2466 this.setEndpoint(endpoint, id);
2469 this._handleCheckedCallback({
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),
2478 _onSubmitCallbackSuccess: function(id, name) {
2481 if (qq.supportedFeatures.ajaxUploading) {
2482 buttonId = this._handler.getFile(id).qqButtonId;
2485 buttonId = this._getButtonId(this._handler.getInput(id));
2489 this._buttonIdsForFileIds[id] = buttonId;
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);
2497 if (this._options.autoUpload) {
2498 if (!this._handler.upload(id)) {
2499 this._uploadData.setStatus(id, qq.status.QUEUED);
2503 this._storeForLater(id);
2507 _onSubmitted: function(id) {
2508 //nothing to do in the base uploader
2511 _storeForLater: function(id) {
2512 this._storedIds.push(id);
2515 _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) {
2517 itemLimit = this._options.validation.itemLimit,
2518 proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued;
2520 if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
2521 if (items.length > 0) {
2522 this._handleCheckedCallback({
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
2531 this._itemError("noFilesError");
2535 this._onValidateBatchCallbackFailure(items);
2536 errorMessage = this._options.messages.tooManyItemsError
2537 .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued)
2538 .replace(/\{itemLimit\}/g, itemLimit);
2539 this._batchError(errorMessage);
2543 _onValidateBatchCallbackFailure: function(fileWrappers) {
2546 qq.each(fileWrappers, function(idx, fileWrapper) {
2547 self._fileOrBlobRejected(fileWrapper.id);
2551 _onValidateCallbackSuccess: function(items, index, params, endpoint) {
2553 nextIndex = index+1,
2554 validationDescriptor = this._getValidationDescriptor(items[index].file);
2556 this._validateFileOrBlobData(items[index], validationDescriptor)
2559 self._upload(items[index].id, params, endpoint);
2560 self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint);
2563 self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
2568 _onValidateCallbackFailure: function(items, index, params, endpoint) {
2569 var nextIndex = index+ 1;
2571 this._fileOrBlobRejected(items[0].id, items[0].file.name);
2573 this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
2576 _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) {
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);
2585 self._handleCheckedCallback({
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
2594 else if (!validItem) {
2595 for (; index < items.length; index++) {
2596 self._fileOrBlobRejected(items[index].id);
2603 * Performs some internal validation checks on an item, defined in the `validation` option.
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
2610 _validateFileOrBlobData: function(fileWrapper, validationDescriptor) {
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();
2619 validityChecker.then(
2622 self._fileOrBlobRejected(fileWrapper.id, name);
2625 if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) {
2626 this._itemError("typeError", name, file);
2627 return validityChecker.failure();
2631 this._itemError("emptyError", name, file);
2632 return validityChecker.failure();
2635 if (size && validationBase.sizeLimit && size > validationBase.sizeLimit) {
2636 this._itemError("sizeError", name, file);
2637 return validityChecker.failure();
2640 if (size && size < validationBase.minSizeLimit) {
2641 this._itemError("minSizeError", name, file);
2642 return validityChecker.failure();
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();
2655 validityChecker.success();
2658 return validityChecker;
2661 _fileOrBlobRejected: function(id) {
2662 this._netUploadedOrQueued--;
2663 this._uploadData.setStatus(id, qq.status.REJECTED);
2667 * Constructs and returns a message that describes an item/file error. Also calls `onError` callback.
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">`
2674 _itemError: function(code, maybeNameOrNames, item) {
2675 var message = this._options.messages[code],
2676 allowedExtensions = [],
2677 names = [].concat(maybeNameOrNames),
2679 buttonId = this._getButtonId(item),
2680 validationBase = this._getValidationBase(buttonId),
2681 extensionsForMessage, placeholderMatch;
2683 function r(name, replacement){ message = message.replace(name, replacement); }
2685 qq.each(validationBase.allowedExtensions, function(idx, allowedExtension) {
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.
2690 if (qq.isString(allowedExtension)) {
2691 allowedExtensions.push(allowedExtension);
2695 extensionsForMessage = allowedExtensions.join(", ").toLowerCase();
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));
2702 placeholderMatch = message.match(/(\{\w+\})/g);
2703 if (placeholderMatch !== null) {
2704 qq.each(placeholderMatch, function(idx, placeholder) {
2705 r(placeholder, names[idx]);
2709 this._options.callbacks.onError(null, name, message, undefined);
2714 _batchError: function(message) {
2715 this._options.callbacks.onError(null, null, message, undefined);
2718 _isAllowedExtension: function(allowed, fileName) {
2721 if (!allowed.length) {
2725 qq.each(allowed, function(idx, allowedExt) {
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.
2730 if (qq.isString(allowedExt)) {
2731 /*jshint eqeqeq: true, eqnull: true*/
2732 var extRegex = new RegExp("\\." + allowedExt + "$", "i");
2734 if (fileName.match(extRegex) != null) {
2744 _formatSize: function(bytes){
2747 bytes = bytes / 1000;
2749 } while (bytes > 999);
2751 return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
2754 _wrapCallbacks: function() {
2755 var self, safeCallback;
2759 safeCallback = function(name, callback, args) {
2763 return callback.apply(self, args);
2766 errorMsg = exception.message || exception.toString();
2767 self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error");
2771 /* jshint forin: false, loopfunc: true */
2772 for (var prop in this._options.callbacks) {
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);
2784 _parseFileOrBlobDataName: function(fileOrBlobData) {
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(/.*(\/|\\)/, "");
2793 // fix missing properties in Safari 4 and firefox 11.0a2
2794 name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
2798 name = fileOrBlobData.name;
2804 _parseFileOrBlobDataSize: function(fileOrBlobData) {
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;
2814 size = fileOrBlobData.blob.size;
2820 _getValidationDescriptor: function(fileOrBlobData) {
2821 var fileDescriptor = {},
2822 name = this._parseFileOrBlobDataName(fileOrBlobData),
2823 size = this._parseFileOrBlobDataSize(fileOrBlobData);
2825 fileDescriptor.name = name;
2826 if (size !== undefined) {
2827 fileDescriptor.size = size;
2830 return fileDescriptor;
2833 _getValidationDescriptors: function(fileWrappers) {
2835 fileDescriptors = [];
2837 qq.each(fileWrappers, function(idx, fileWrapper) {
2838 fileDescriptors.push(self._getValidationDescriptor(fileWrapper.file));
2841 return fileDescriptors;
2844 _createParamsStore: function(type) {
2845 var paramsStore = {},
2849 setParams: function(params, id) {
2850 var paramsCopy = {};
2851 qq.extend(paramsCopy, params);
2852 paramsStore[id] = paramsCopy;
2855 getParams: function(id) {
2856 /*jshint eqeqeq: true, eqnull: true*/
2857 var paramsCopy = {};
2859 if (id != null && paramsStore[id]) {
2860 qq.extend(paramsCopy, paramsStore[id]);
2863 qq.extend(paramsCopy, self._options[type].params);
2869 remove: function(fileId) {
2870 return delete paramsStore[fileId];
2879 _createEndpointStore: function(type) {
2880 var endpointStore = {},
2884 setEndpoint: function(endpoint, id) {
2885 endpointStore[id] = endpoint;
2888 getEndpoint: function(id) {
2889 /*jshint eqeqeq: true, eqnull: true*/
2890 if (id != null && endpointStore[id]) {
2891 return endpointStore[id];
2894 return self._options[type].endpoint;
2897 remove: function(fileId) {
2898 return delete endpointStore[fileId];
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;
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];
2920 // Camera access won't work in iOS if the `multiple` attribute is present on the file input
2921 optionRoot.multiple = false;
2923 // update the options
2924 if (optionRoot.validation.acceptFiles === null) {
2925 optionRoot.validation.acceptFiles = acceptIosCamera;
2928 optionRoot.validation.acceptFiles += "," + acceptIosCamera;
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);
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];
2948 return extraButtonSpec ? extraButtonSpec.validation : this._options.validation;
2958 qq.FineUploaderBasic = function(o) {
2959 // These options define FineUploaderBasic mode.
2965 disableCancelForFormUploads: false,
2969 endpoint: "/server/upload",
2973 forceMultipart: true,
2974 inputName: "qqfile",
2976 totalFileSizeName: "qqtotalfilesize",
2977 filenameParam: "qqfilename"
2981 allowedExtensions: [],
2985 stopOnFirstInvalidFile: true,
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) {}
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."
3036 autoAttemptDelay: 5,
3037 preventRetryResponseProperty: "preventRetry"
3041 buttonHover: "qq-upload-button-hover",
3042 buttonFocus: "qq-upload-button-focus"
3049 partIndex: "qqpartindex",
3050 partByteOffset: "qqpartbyteoffset",
3051 chunkSize: "qqchunksize",
3052 totalFileSize: "qqtotalfilesize",
3053 totalParts: "qqtotalparts"
3060 cookiesExpireIn: 7, //days
3062 resuming: "qqresume"
3066 formatFileName: function(fileOrBlobName) {
3067 if (fileOrBlobName !== undefined && fileOrBlobName.length > 33) {
3068 fileOrBlobName = fileOrBlobName.slice(0, 19) + "..." + fileOrBlobName.slice(-14);
3070 return fileOrBlobName;
3074 defaultResponseError: "Upload failure reason unknown",
3075 sizeSymbols: ["kB", "MB", "GB", "TB", "PB", "EB"]
3081 endpoint: "/server/upload",
3088 sendCredentials: false,
3093 defaultName: "misc_data"
3097 targetElement: null,
3098 defaultName: "pasted_image"
3104 // if ios is true: button is null means target the default button, otherwise target the button specified
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`,
3116 // Depends on the session module. Used to query the server for an initial file list
3117 // during initialization and optionally after a `reset`.
3122 refreshOnReset: true
3126 // Replace any default options with user defined ones
3127 qq.extend(this._options, o, true);
3130 this._extraButtonSpecs = {};
3131 this._buttonIdsForFileIds = [];
3133 this._wrapCallbacks();
3134 this._disposeSupport = new qq.DisposeSupport();
3136 this._storedIds = [];
3137 this._autoRetries = [];
3138 this._retryTimeouts = [];
3139 this._preventRetries = [];
3140 this._thumbnailUrls = [];
3142 this._netUploadedOrQueued = 0;
3143 this._netUploaded = 0;
3144 this._uploadData = this._createUploadDataTracker();
3146 this._paramsStore = this._createParamsStore("request");
3147 this._deleteFileParamsStore = this._createParamsStore("deleteFile");
3149 this._endpointStore = this._createEndpointStore("request");
3150 this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
3152 this._handler = this._createUploadHandler();
3154 this._deleteHandler = qq.DeleteFileAjaxRequester && this._createDeleteHandler();
3156 if (this._options.button) {
3157 this._defaultButtonId = this._createUploadButton({element: this._options.button}).getButtonId();
3160 this._generateExtraButtonSpecs();
3162 this._handleCameraAccess();
3164 if (this._options.paste.targetElement) {
3165 if (qq.PasteSupport) {
3166 this._pasteHandler = this._createPasteHandler();
3169 qq.log("Paste support module not found", "info");
3173 this._preventLeaveInProgress();
3175 this._imageGenerator = qq.ImageGenerator && new qq.ImageGenerator(qq.bind(this.log, this));
3176 this._refreshSessionData();
3179 // Define the private & public API methods.
3180 qq.FineUploaderBasic.prototype = qq.basePublicApi;
3181 qq.extend(qq.FineUploaderBasic.prototype, qq.basePrivateApi);
3184 /*globals qq, XDomainRequest*/
3185 /** Generic class for sending non-upload ajax requests and handling the associated responses **/
3186 qq.AjaxRequester = function (o) {
3189 var log, shouldParamsBeInQueryString,
3193 validMethods: ["POST"],
3195 contentType: "application/x-www-form-urlencoded",
3201 allowXRequestedWithAndCacheControl: true,
3202 successfulResponseCodes: {
3203 "DELETE": [200, 202, 204],
3209 sendCredentials: false
3211 log: function (str, level) {},
3212 onSend: function (id) {},
3213 onComplete: function (id, xhrOrXdr, isError) {}
3216 qq.extend(options, o);
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!");
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;
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;
3236 qq.each(containsNonSimple, function(idx, header) {
3237 if (qq.indexOf(["Accept", "Accept-Language", "Content-Language", "Content-Type"], header) < 0) {
3238 containsNonSimple = true;
3243 return containsNonSimple;
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;
3251 // Returns either a new `XMLHttpRequest` or `XDomainRequest` instance.
3252 function getCorsAjaxTransport() {
3255 if (window.XMLHttpRequest || window.ActiveXObject) {
3256 xhrOrXdr = qq.createXhrInstance();
3258 if (xhrOrXdr.withCredentials === undefined) {
3259 xhrOrXdr = new XDomainRequest();
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;
3270 if (!xhrOrXdr && !dontCreateIfNotExist) {
3271 if (options.cors.expected) {
3272 xhrOrXdr = getCorsAjaxTransport();
3275 xhrOrXdr = qq.createXhrInstance();
3278 requestData[id].xhr = xhrOrXdr;
3284 // Removes element from queue, sends next request
3285 function dequeue(id) {
3286 var i = qq.indexOf(queue, id),
3287 max = options.maxConnections,
3290 delete requestData[id];
3293 if (queue.length >= max && i < max) {
3294 nextId = queue[max - 1];
3295 sendRequest(nextId);
3299 function onComplete(id, xdrError) {
3300 var xhr = getXhrOrXdr(id),
3301 method = options.method,
3302 isError = xdrError === true;
3307 log(method + " request for " + id + " has failed", "error");
3309 else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) {
3311 log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
3314 options.onComplete(id, xhr, isError);
3317 function getParams(id) {
3318 var onDemandParams = requestData[id].additionalParams,
3319 mandatedParams = options.mandatedParams,
3322 if (options.paramsStore.getParams) {
3323 params = options.paramsStore.getParams(id);
3326 if (onDemandParams) {
3327 qq.each(onDemandParams, function (name, val) {
3328 params = params || {};
3333 if (mandatedParams) {
3334 qq.each(mandatedParams, function (name, val) {
3335 params = params || {};
3343 function sendRequest(id) {
3344 var xhr = getXhrOrXdr(id),
3345 method = options.method,
3346 params = getParams(id),
3347 payload = requestData[id].payload,
3352 url = createUrl(id, params);
3354 // XDR and XHR status detection APIs differ a bit.
3356 xhr.onload = getXdrLoadHandler(id);
3357 xhr.onerror = getXdrErrorHandler(id);
3360 xhr.onreadystatechange = getXhrReadyStateChangeHandler(id);
3363 // The last parameter is assumed to be ignored if we are actually using `XDomainRequest`.
3364 xhr.open(method, url, true);
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;
3374 log("Sending " + method + " request for " + id);
3379 else if (shouldParamsBeInQueryString || !params) {
3382 else if (params && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) {
3383 xhr.send(qq.obj2url(params, ""));
3385 else if (params && options.contentType.toLowerCase().indexOf("application/json") >= 0) {
3386 xhr.send(JSON.stringify(params));
3393 function createUrl(id, params) {
3394 var endpoint = options.endpointStore.getEndpoint(id),
3395 addToPath = requestData[id].addToPath;
3397 /*jshint -W116,-W041 */
3398 if (addToPath != undefined) {
3399 endpoint += "/" + addToPath;
3402 if (shouldParamsBeInQueryString && params) {
3403 return qq.obj2url(params, endpoint);
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) {
3420 // This will be called by IE to indicate **success** for an associated
3421 // `XDomainRequest` transported request.
3422 function getXdrLoadHandler(id) {
3423 return function () {
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);
3436 function setHeaders(id) {
3437 var xhr = getXhrOrXdr(id),
3438 customHeaders = options.customHeaders,
3439 onDemandHeaders = requestData[id].additionalHeaders || {},
3440 method = options.method,
3443 // If XDomainRequest is being used, we can't set headers, so just ignore this block.
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");
3457 if (options.contentType && (method === "POST" || method === "PUT")) {
3458 xhr.setRequestHeader("Content-Type", options.contentType);
3461 qq.extend(allHeaders, customHeaders);
3462 qq.extend(allHeaders, onDemandHeaders);
3464 qq.each(allHeaders, function (name, val) {
3465 xhr.setRequestHeader(name, val);
3470 function isResponseSuccessful(responseCode) {
3471 return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0;
3474 function prepareToSend(id, addToPath, additionalParams, additionalHeaders, payload) {
3476 addToPath: addToPath,
3477 additionalParams: additionalParams,
3478 additionalHeaders: additionalHeaders,
3482 var len = queue.push(id);
3484 // if too many active connections, wait...
3485 if (len <= options.maxConnections) {
3491 shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE";
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;
3499 // Optionally specify the end of the endpoint path for the request.
3500 withPath: function(appendToPath) {
3501 path = appendToPath;
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;
3514 // Optionally specify additional headers to send along with the request.
3515 withHeaders: function(additionalHeaders) {
3516 headers = additionalHeaders;
3520 // Optionally specify a payload/body for the request.
3521 withPayload: function(thePayload) {
3522 payload = thePayload;
3526 // Send the constructed request.
3528 prepareToSend(id, path, params, headers, payload);
3537 * Base upload handler module. Delegates to more specific handlers.
3539 * @param o Options. Passed along to the specific handler submodule as well.
3540 * @param namespace [optional] Namespace for the specific handler.
3542 qq.UploadHandler = function(o, namespace) {
3546 options, log, handlerImpl;
3548 // Default options, can be overridden by the user
3551 forceMultipart: true,
3552 paramsInBody: false,
3555 filenameParam: "qqfilename",
3558 sendCredentials: false
3560 maxConnections: 3, // maximum number of concurrent uploads
3562 totalFileSizeName: "qqtotalfilesize",
3565 partSize: 2000000, //bytes
3567 partIndex: "qqpartindex",
3568 partByteOffset: "qqpartbyteoffset",
3569 chunkSize: "qqchunksize",
3570 totalParts: "qqtotalparts",
3571 filename: "qqfilename"
3577 cookiesExpireIn: 7, //days
3579 resuming: "qqresume"
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) {}
3595 qq.extend(options, o);
3600 * Removes element from queue, starts upload of next
3602 function dequeue(id) {
3603 var i = qq.indexOf(queue, id),
3604 max = options.maxConnections,
3610 if (queue.length >= max && i < max){
3611 nextId = queue[max-1];
3612 handlerImpl.upload(nextId);
3617 function cancelSuccess(id) {
3618 log("Cancelling " + id);
3619 options.paramsStore.remove(id);
3623 function determineHandlerImpl() {
3624 var handlerType = namespace ? qq[namespace] : qq,
3625 handlerModuleSubtype = qq.supportedFeatures.ajaxUploading ? "Xhr" : "Form";
3627 handlerImpl = new handlerType["UploadHandler" + handlerModuleSubtype](
3629 {onUploadComplete: dequeue, onUuidChanged: options.onUuidChanged,
3630 getName: options.getName, getUuid: options.getUuid, getSize: options.getSize, log: log}
3637 * Adds file or file input to the queue
3640 add: function(id, file) {
3641 return handlerImpl.add.apply(this, arguments);
3645 * Sends the file identified by id
3647 upload: function(id) {
3648 var len = queue.push(id);
3650 // if too many active uploads, wait...
3651 if (len <= options.maxConnections){
3652 handlerImpl.upload(id);
3659 retry: function(id) {
3660 var i = qq.indexOf(queue, id);
3662 return handlerImpl.upload(id, true);
3665 return this.upload(id);
3670 * Cancels file upload by id
3672 cancel: function(id) {
3673 var cancelRetVal = handlerImpl.cancel(id);
3675 if (cancelRetVal instanceof qq.Promise) {
3676 cancelRetVal.then(function() {
3680 else if (cancelRetVal !== false) {
3686 * Cancels all queued or in-progress uploads
3688 cancelAll: function() {
3692 qq.extend(queueCopy, queue);
3693 qq.each(queueCopy, function(idx, fileId) {
3694 self.cancel(fileId);
3700 getFile: function(id) {
3701 if (handlerImpl.getFile) {
3702 return handlerImpl.getFile(id);
3706 getInput: function(id) {
3707 if (handlerImpl.getInput) {
3708 return handlerImpl.getInput(id);
3713 log("Resetting upload handler");
3716 handlerImpl.reset();
3719 expunge: function(id) {
3720 if (this.isValid(id)) {
3721 return handlerImpl.expunge(id);
3726 * Determine if the file exists.
3728 isValid: function(id) {
3729 return handlerImpl.isValid(id);
3732 getResumableFilesData: function() {
3733 if (handlerImpl.getResumableFilesData) {
3734 return handlerImpl.getResumableFilesData();
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.
3744 * @param id Internal file ID
3745 * @returns {*} Some identifier used by a 3rd-party service involved in the upload process
3747 getThirdPartyFileId: function(id) {
3748 if (handlerImpl.getThirdPartyFileId && this.isValid(id)) {
3749 return handlerImpl.getThirdPartyFileId(id);
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
3758 pause: function(id) {
3759 if (handlerImpl.pause && this.isValid(id) && handlerImpl.pause(id)) {
3766 determineHandlerImpl();
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.
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
3779 qq.UploadHandlerFormApi = function(internalApi, spec, proxy) {
3782 var formHandlerInstanceId = qq.getUniqueId(),
3783 onloadCallbacks = {},
3784 detachLoadEvents = {},
3785 postMessageCallbackTimers = {},
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,
3795 corsMessageReceiver = new qq.WindowReceiveMessage({log: log});
3799 * Remove any trace of the file from the handler.
3801 * @param id ID of the associated file
3803 function expungeFile(id) {
3804 delete detachLoadEvents[id];
3805 delete fileState[id];
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.
3811 clearTimeout(postMessageCallbackTimers[id]);
3812 delete postMessageCallbackTimers[id];
3813 corsMessageReceiver.stopReceivingMessages(id);
3816 var iframe = document.getElementById(internalApi.getIframeName(id));
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
3822 qq(iframe).remove();
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.
3830 * @param iframe Listen for messages on this iframe.
3831 * @param callback Invoke this callback with the message from the iframe.
3833 function registerPostMessageCallback(iframe, callback) {
3834 var iframeName = iframe.id,
3835 fileId = getFileIdForIframeName(iframeName),
3836 uuid = getUuid(fileId);
3838 onloadCallbacks[uuid] = callback;
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 + ")");
3846 postMessageCallbackTimers[iframeName] = setTimeout(function() {
3847 var errorMessage = "No valid message received from loaded iframe for iframe name " + iframeName;
3848 log(errorMessage, "error");
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,
3865 if (uuid && onloadCallbacks[uuid]) {
3866 log("Handling response for iframe name " + iframeName);
3867 clearTimeout(postMessageCallbackTimers[iframeName]);
3868 delete postMessageCallbackTimers[iframeName];
3870 internalApi.detachLoadEvent(iframeName);
3872 onloadCallback = onloadCallbacks[uuid];
3874 delete onloadCallbacks[uuid];
3875 corsMessageReceiver.stopReceivingMessages(iframeName);
3876 onloadCallback(response);
3879 log("'" + message + "' does not contain a UUID - ignoring.");
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.
3888 * @param name Name of the iframe.
3889 * @returns {HTMLIFrameElement} The created iframe
3891 function initIframeForUpload(name) {
3892 var iframe = qq.toElement("<iframe src='javascript:false;' name='" + name + "' />");
3894 iframe.setAttribute("id", name);
3896 iframe.style.display = "none";
3897 document.body.appendChild(iframe);
3903 * @param iframeName `document`-unique Name of the associated iframe
3904 * @returns {*} ID of the associated file
3906 function getFileIdForIframeName(iframeName) {
3907 return iframeName.split("_")[0];
3913 qq.extend(internalApi, {
3915 * @param fileId ID of the associated file
3916 * @returns {string} The `document`-unique name of the iframe
3918 getIframeName: function(fileId) {
3919 return fileId + "_" + formHandlerInstanceId;
3923 * Creates an iframe with a specific document-unique name.
3925 * @param id ID of the associated file
3926 * @returns {HTMLIFrameElement}
3928 createIframe: function(id) {
3929 var iframeName = internalApi.getIframeName(id);
3931 return initIframeForUpload(iframeName);
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
3939 parseJsonResponse: function(id, innerHtmlOrMessage) {
3943 response = qq.parseJson(innerHtmlOrMessage);
3945 if (response.newUuid !== undefined) {
3946 onUuidChanged(id, response.newUuid);
3950 log("Error when attempting to parse iframe upload response (" + error.message + ")", "error");
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.
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
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>"),
3976 qq.obj2Inputs(params, form);
3979 url = qq.obj2url(params, endpoint);
3982 form.setAttribute("action", url);
3983 form.setAttribute("target", targetName);
3984 form.style.display = "none";
3985 document.body.appendChild(form);
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.
3995 * @param iframe Associated iframe
3996 * @param callback Callback to invoke after we have determined if the iframe content is accessible.
3998 attachLoadEvent: function(iframe, callback) {
3999 /*jslint eqeq: true*/
4000 var responseDescriptor;
4003 registerPostMessageCallback(iframe, callback);
4006 detachLoadEvents[iframe.id] = qq(iframe).attach("load", function(){
4007 log("Received response for " + iframe.id);
4009 // when we remove iframe from dom
4010 // the request stops, but in IE load
4012 if (!iframe.parentNode){
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
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};
4034 callback(responseDescriptor);
4040 * Called when we are no longer interested in being notified when an iframe has loaded.
4042 * @param id Associated file ID
4044 detachLoadEvent: function(id) {
4045 if (detachLoadEvents[id] !== undefined) {
4046 detachLoadEvents[id]();
4047 delete detachLoadEvents[id];
4056 add: function(id, fileInput) {
4057 fileState[id] = {input: fileInput};
4059 fileInput.setAttribute("name", inputName);
4061 // remove file input from DOM
4062 if (fileInput.parentNode){
4063 qq(fileInput).remove();
4067 getInput: function(id) {
4068 return fileState[id].input;
4071 isValid: function(id) {
4072 return fileState[id] !== undefined &&
4073 fileState[id].input !== undefined;
4077 fileState.length = 0;
4080 expunge: function(id) {
4081 return expungeFile(id);
4084 cancel: function(id) {
4085 var onCancelRetVal = onCancel(id, getName(id));
4087 if (onCancelRetVal instanceof qq.Promise) {
4088 return onCancelRetVal.then(function() {
4092 else if (onCancelRetVal !== false) {
4100 upload: function(id) {
4101 // implementation-specific
4108 * Common API exposed to creators of XHR handlers. This is reused and possibly overriding in some cases by specific
4109 * XHR upload handlers.
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
4116 qq.UploadHandlerXhrApi = function(internalApi, spec, proxy) {
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,
4130 function getChunk(fileOrBlob, startByte, endByte) {
4131 if (fileOrBlob.slice) {
4132 return fileOrBlob.slice(startByte, endByte);
4134 else if (fileOrBlob.mozSlice) {
4135 return fileOrBlob.mozSlice(startByte, endByte);
4137 else if (fileOrBlob.webkitSlice) {
4138 return fileOrBlob.webkitSlice(startByte, endByte);
4142 qq.extend(internalApi, {
4144 * Creates an XHR instance for this file and stores it in the fileState.
4147 * @returns {XMLHttpRequest}
4149 createXhr: function(id) {
4150 var xhr = qq.createXhrInstance();
4152 fileState[id].xhr = xhr;
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
4161 getTotalChunks: function(id) {
4163 var fileSize = getSize(id),
4164 chunkSize = chunking.partSize;
4166 return Math.ceil(fileSize / chunkSize);
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);
4183 blob: getChunk(fileOrBlob, startBytes, endBytes),
4184 size: endBytes - startBytes
4188 getChunkDataForCallback: function(chunkData) {
4190 partIndex: chunkData.part,
4191 startByte: chunkData.start + 1,
4192 endByte: chunkData.end,
4193 totalParts: chunkData.count
4200 * Adds File or Blob to the queue
4202 add: function(id, fileOrBlobData) {
4203 if (qq.isFile(fileOrBlobData)) {
4204 fileState[id] = {file: fileOrBlobData};
4206 else if (qq.isBlob(fileOrBlobData.blob)) {
4207 fileState[id] = {blobData: fileOrBlobData};
4210 throw new Error("Passed obj is not a File or BlobData (in qq.UploadHandlerXhr)");
4214 getFile: function(id) {
4215 if (fileState[id]) {
4216 return fileState[id].file || fileState[id].blobData.blob;
4220 isValid: function(id) {
4221 return fileState[id] !== undefined;
4225 fileState.length = 0;
4228 expunge: function(id) {
4229 var xhr = fileState[id].xhr;
4232 xhr.onreadystatechange = null;
4236 delete fileState[id];
4240 * Sends the file identified by id to the server
4242 upload: function(id, retry) {
4243 fileState[id] && delete fileState[id].paused;
4244 return onUpload(id, retry);
4247 cancel: function(id) {
4248 var onCancelRetVal = onCancel(id, getName(id));
4250 if (onCancelRetVal instanceof qq.Promise) {
4251 return onCancelRetVal.then(function() {
4255 else if (onCancelRetVal !== false) {
4263 pause: function(id) {
4264 var xhr = fileState[id].xhr;
4267 log(qq.format("Aborting XHR upload for {} '{}' due to pause instruction.", id, getName(id)));
4268 fileState[id].paused = true;
4278 qq.WindowReceiveMessage = function(o) {
4282 log: function(message, level) {}
4284 callbackWrapperDetachers = {};
4286 qq.extend(options, o);
4289 receiveMessage : function(id, callback) {
4290 var onMessageCallbackWrapper = function(event) {
4291 callback(event.data);
4294 if (window.postMessage) {
4295 callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper);
4298 log("iframe message passing not supported in this browser!", "error");
4302 stopReceivingMessages : function(id) {
4303 if (window.postMessage) {
4304 var detacher = callbackWrapperDetachers[id];
4315 * Defines the public API for FineUploader mode.
4321 clearStoredFiles: function() {
4322 this._parent.prototype.clearStoredFiles.apply(this, arguments);
4323 this._templating.clearFiles();
4326 addExtraDropzone: function(element){
4327 this._dnd && this._dnd.setupExtraDropzone(element);
4330 removeExtraDropzone: function(element){
4332 return this._dnd.removeDropzone(element);
4336 getItemByFileId: function(id) {
4337 return this._templating.getFileContainer(id);
4341 this._parent.prototype.reset.apply(this, arguments);
4342 this._templating.reset();
4344 if (!this._options.button && this._templating.getButton()) {
4345 this._defaultButtonId = this._createUploadButton({element: this._templating.getButton()}).getButtonId();
4349 this._dnd.dispose();
4350 this._dnd = this._setupDragAndDrop();
4353 this._totalFilesInBatch = 0;
4354 this._filesInBatchAddedToUi = 0;
4356 this._setupClickAndEditEventHandlers();
4359 pauseUpload: function(id) {
4360 var paused = this._parent.prototype.pauseUpload.apply(this, arguments);
4362 paused && this._templating.uploadPaused(id);
4366 continueUpload: function(id) {
4367 var continued = this._parent.prototype.continueUpload.apply(this, arguments);
4369 continued && this._templating.uploadContinued(id);
4373 getId: function(fileContainerOrChildEl) {
4374 return this._templating.getFileId(fileContainerOrChildEl);
4377 getDropTarget: function(fileId) {
4378 var file = this.getFile(fileId);
4380 return file.qqDropTarget;
4388 * Defines the private (internal) API for FineUploader mode.
4391 _getButton: function(buttonId) {
4392 var button = this._parent.prototype._getButton.apply(this, arguments);
4395 if (buttonId === this._defaultButtonId) {
4396 button = this._templating.getButton();
4403 _removeFileItem: function(fileId) {
4404 this._templating.removeFile(fileId);
4407 _setupClickAndEditEventHandlers: function() {
4408 this._fileButtonsClickHandler = qq.FileButtonsClickHandler && this._bindFileButtonsClickEvent();
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();
4414 if (this._isEditFilenameEnabled())
4416 this._filenameClickHandler = this._bindFilenameClickEvent();
4417 this._filenameInputFocusInHandler = this._bindFilenameInputFocusInEvent();
4418 this._filenameInputFocusHandler = this._bindFilenameInputFocusEvent();
4422 _setupDragAndDrop: function() {
4424 dropZoneElements = this._options.dragAndDrop.extraDropzones,
4425 templating = this._templating,
4426 defaultDropZone = templating.getDropZone();
4428 defaultDropZone && dropZoneElements.push(defaultDropZone);
4430 return new qq.DragAndDrop({
4431 dropZoneElements: dropZoneElements,
4432 allowMultipleItems: this._options.multiple,
4434 dropActive: this._options.classes.dropActive
4437 processingDroppedFiles: function() {
4438 templating.showDropProcessing();
4440 processingDroppedFilesComplete: function(files, targetEl) {
4441 templating.hideDropProcessing();
4443 qq.each(files, function(idx, file) {
4444 file.qqDropTarget = targetEl;
4448 self.addFiles(files, null, null);
4451 dropError: function(code, errorData) {
4452 self._itemError(code, errorData);
4454 dropLog: function(message, level) {
4455 self.log(message, level);
4461 _bindFileButtonsClickEvent: function() {
4464 return new qq.FileButtonsClickHandler({
4465 templating: this._templating,
4467 log: function(message, lvl) {
4468 self.log(message, lvl);
4471 onDeleteFile: function(fileId) {
4472 self.deleteFile(fileId);
4475 onCancel: function(fileId) {
4476 self.cancel(fileId);
4479 onRetry: function(fileId) {
4480 qq(self._templating.getFileContainer(fileId)).removeClass(self._classes.retryable);
4484 onPause: function(fileId) {
4485 self.pauseUpload(fileId);
4488 onContinue: function(fileId) {
4489 self.continueUpload(fileId);
4492 onGetName: function(fileId) {
4493 return self.getName(fileId);
4498 _isEditFilenameEnabled: function() {
4500 return this._templating.isEditFilenamePossible()
4501 && !this._options.autoUpload
4502 && qq.FilenameClickHandler
4503 && qq.FilenameInputFocusHandler
4504 && qq.FilenameInputFocusHandler;
4507 _filenameEditHandler: function() {
4509 templating = this._templating;
4512 templating: templating,
4513 log: function(message, lvl) {
4514 self.log(message, lvl);
4516 onGetUploadStatus: function(fileId) {
4517 return self.getUploads({id: fileId}).status;
4519 onGetName: function(fileId) {
4520 return self.getName(fileId);
4522 onSetName: function(id, newName) {
4523 var formattedFilename = self._options.formatFileName(newName);
4525 templating.updateFilename(id, formattedFilename);
4526 self.setName(id, newName);
4528 onEditingStatusChange: function(id, isEditing) {
4529 var qqInput = qq(templating.getEditInput(id)),
4530 qqFileContainer = qq(templating.getFileContainer(id));
4533 qqInput.addClass("qq-editing");
4534 templating.hideFilename(id);
4535 templating.hideEditIcon(id);
4538 qqInput.removeClass("qq-editing");
4539 templating.showFilename(id);
4540 templating.showEditIcon(id);
4543 // Force IE8 and older to repaint
4544 qqFileContainer.addClass("qq-temp").removeClass("qq-temp");
4549 _onUploadStatusChange: function(id, oldStatus, newStatus) {
4550 this._parent.prototype._onUploadStatusChange.apply(this, arguments);
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);
4561 _bindFilenameInputFocusInEvent: function() {
4562 var spec = qq.extend({}, this._filenameEditHandler());
4564 return new qq.FilenameInputFocusInHandler(spec);
4567 _bindFilenameInputFocusEvent: function() {
4568 var spec = qq.extend({}, this._filenameEditHandler());
4570 return new qq.FilenameInputFocusHandler(spec);
4573 _bindFilenameClickEvent: function() {
4574 var spec = qq.extend({}, this._filenameEditHandler());
4576 return new qq.FilenameClickHandler(spec);
4579 _storeForLater: function(id) {
4580 this._parent.prototype._storeForLater.apply(this, arguments);
4581 this._templating.hideSpinner(id);
4584 _onSubmit: function(id, name) {
4585 this._parent.prototype._onSubmit.apply(this, arguments);
4586 this._addToList(id, name);
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);
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));
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);
4607 this._templating.updateProgress(id, loaded, total);
4609 if (loaded === total) {
4610 this._templating.hideCancel(id);
4611 this._templating.hidePause(id);
4613 this._templating.setStatusText(id, this._options.text.waitingForResponse);
4615 // If last byte was sent, display total file size
4616 this._displayFileSize(id);
4619 // If still uploading, display percentage - total size is actually the total request(s) size
4620 this._displayFileSize(id, loaded, total);
4624 _onComplete: function(id, name, result, xhr) {
4625 var parentRetVal = this._parent.prototype._onComplete.apply(this, arguments),
4626 templating = this._templating,
4629 function completeUpload(result) {
4630 templating.setStatusText(id);
4632 qq(templating.getFileContainer(id)).removeClass(self._classes.retrying);
4633 templating.hideProgress(id);
4635 if (!self._options.disableCancelForFormUploads || qq.supportedFeatures.ajaxUploading) {
4636 templating.hideCancel(id);
4638 templating.hideSpinner(id);
4640 if (result.success) {
4641 self._markFileAsSuccessful(id);
4644 qq(templating.getFileContainer(id)).addClass(self._classes.fail);
4646 if (self._templating.isRetryPossible() && !self._preventRetries[id]) {
4647 qq(templating.getFileContainer(id)).addClass(self._classes.retryable);
4649 self._controlFailureTextDisplay(id, result);
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);
4661 completeUpload(result);
4664 return parentRetVal;
4667 _markFileAsSuccessful: function(id) {
4668 var templating = this._templating;
4670 if (this._isDeletePossible()) {
4671 templating.showDeleteButton(id);
4674 qq(templating.getFileContainer(id)).addClass(this._classes.success);
4676 this._maybeUpdateThumbnail(id);
4679 _onUpload: function(id, name){
4680 var parentRetVal = this._parent.prototype._onUpload.apply(this, arguments);
4682 this._templating.showSpinner(id);
4684 return parentRetVal;
4687 _onUploadChunk: function(id, chunkData) {
4688 this._parent.prototype._onUploadChunk.apply(this, arguments);
4690 // Only display the pause button if we have finished uploading at least one chunk
4691 chunkData.partIndex > 0 && this._templating.allowPause(id);
4694 _onCancel: function(id, name) {
4695 this._parent.prototype._onCancel.apply(this, arguments);
4696 this._removeFileItem(id);
4699 _onBeforeAutoRetry: function(id) {
4700 var retryNumForDisplay, maxAuto, retryNote;
4702 this._parent.prototype._onBeforeAutoRetry.apply(this, arguments);
4704 this._showCancelLink(id);
4706 if (this._options.retry.showAutoRetryNote) {
4707 retryNumForDisplay = this._autoRetries[id] + 1;
4708 maxAuto = this._options.retry.maxAutoAttempts;
4710 retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay);
4711 retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto);
4713 this._templating.setStatusText(id, retryNote);
4714 qq(this._templating.getFileContainer(id)).addClass(this._classes.retrying);
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);
4729 qq(this._templating.getFileContainer(id)).addClass(this._classes.retryable);
4734 _onSubmitDelete: function(id) {
4735 var onSuccessCallback = qq.bind(this._onSubmitDeleteSuccess, this);
4737 this._parent.prototype._onSubmitDelete.call(this, id, onSuccessCallback);
4740 _onSubmitDeleteSuccess: function(id, uuid, additionalMandatedParams) {
4741 if (this._options.deleteFile.forceConfirm) {
4742 this._showDeleteConfirm.apply(this, arguments);
4745 this._sendDeleteRequest.apply(this, arguments);
4749 _onDeleteComplete: function(id, xhr, isError) {
4750 this._parent.prototype._onDeleteComplete.apply(this, arguments);
4752 this._templating.hideSpinner(id);
4755 this._templating.setStatusText(id, this._options.deleteFile.deletingFailedText);
4756 this._templating.showDeleteButton(id);
4759 this._removeFileItem(id);
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);
4770 _showDeleteConfirm: function(id, uuid, mandatedParams) {
4772 var fileName = this.getName(id),
4773 confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName),
4774 uuid = this.getUuid(id),
4775 deleteRequestArgs = arguments,
4779 retVal = this._options.showConfirm(confirmMessage);
4781 if (retVal instanceof qq.Promise) {
4782 retVal.then(function () {
4783 self._sendDeleteRequest.apply(self, deleteRequestArgs);
4786 else if (retVal !== false) {
4787 self._sendDeleteRequest.apply(self, deleteRequestArgs);
4791 _addToList: function(id, name, canned) {
4795 if (this._options.display.prependFiles) {
4796 if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) {
4797 prependIndex = this._filesInBatchAddedToUi - 1;
4806 if (this._options.disableCancelForFormUploads && !qq.supportedFeatures.ajaxUploading) {
4807 this._templating.disableCancel();
4810 if (!this._options.multiple) {
4811 this._handler.cancelAll();
4816 this._templating.addFile(id, this._options.formatFileName(name), prependData);
4819 this._thumbnailUrls[id] && this._templating.updateThumbnail(id, this._thumbnailUrls[id], true);
4822 this._templating.generatePreview(id, this.getFile(id));
4825 this._filesInBatchAddedToUi += 1;
4827 if (this._options.display.fileSizeOnSubmit && qq.supportedFeatures.ajaxUploading) {
4828 this._displayFileSize(id);
4832 _clearList: function(){
4833 this._templating.clearFiles();
4834 this.clearStoredFiles();
4837 _displayFileSize: function(id, loadedSize, totalSize) {
4838 var size = this.getSize(id),
4839 sizeForDisplay = this._formatSize(size);
4842 if (loadedSize !== undefined && totalSize !== undefined) {
4843 sizeForDisplay = this._formatProgress(loadedSize, totalSize);
4846 this._templating.updateSize(id, sizeForDisplay);
4850 _formatProgress: function (uploadedSize, totalSize) {
4851 var message = this._options.text.formatProgress;
4852 function r(name, replacement) { message = message.replace(name, replacement); }
4854 r("{percent}", Math.round(uploadedSize / totalSize * 100));
4855 r("{total_size}", this._formatSize(totalSize));
4859 _controlFailureTextDisplay: function(id, response) {
4860 var mode, maxChars, responseProperty, failureReason, shortFailureReason;
4862 mode = this._options.failedUploadTextDisplay.mode;
4863 maxChars = this._options.failedUploadTextDisplay.maxChars;
4864 responseProperty = this._options.failedUploadTextDisplay.responseProperty;
4866 if (mode === "custom") {
4867 failureReason = response[responseProperty];
4868 if (failureReason) {
4869 if (failureReason.length > maxChars) {
4870 shortFailureReason = failureReason.substring(0, maxChars) + "...";
4874 failureReason = this._options.text.failUpload;
4875 this.log("'" + responseProperty + "' is not a valid property on the server response.", "warn");
4878 this._templating.setStatusText(id, shortFailureReason || failureReason);
4880 if (this._options.failedUploadTextDisplay.enableTooltip) {
4881 this._showTooltip(id, failureReason);
4884 else if (mode === "default") {
4885 this._templating.setStatusText(id, this._options.text.failUpload);
4887 else if (mode !== "none") {
4888 this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", "warn");
4892 _showTooltip: function(id, text) {
4893 this._templating.getFileContainer(id).title = text;
4896 _showCancelLink: function(id) {
4897 if (!this._options.disableCancelForFormUploads || qq.supportedFeatures.ajaxUploading) {
4898 this._templating.showCancel(id);
4902 _itemError: function(code, name, item) {
4903 var message = this._parent.prototype._itemError.apply(this, arguments);
4904 this._options.showMessage(message);
4907 _batchError: function(message) {
4908 this._parent.prototype._batchError.apply(this, arguments);
4909 this._options.showMessage(message);
4912 _setupPastePrompt: function() {
4915 this._options.callbacks.onPasteReceived = function() {
4916 var message = self._options.paste.namePromptMessage,
4917 defaultVal = self._options.paste.defaultName;
4919 return self._options.showPrompt(message, defaultVal);
4923 _fileOrBlobRejected: function(id, name) {
4924 this._totalFilesInBatch -= 1;
4925 this._parent.prototype._fileOrBlobRejected.apply(this, arguments);
4928 _prepareItemsForUpload: function(items, params, endpoint) {
4929 this._totalFilesInBatch = items.length;
4930 this._filesInBatchAddedToUi = 0;
4931 this._parent.prototype._prepareItemsForUpload.apply(this, arguments);
4934 _maybeUpdateThumbnail: function(fileId) {
4935 var thumbnailUrl = this._thumbnailUrls[fileId];
4937 this._templating.updateThumbnail(fileId, thumbnailUrl);
4940 _addCannedFile: function(sessionData) {
4941 var id = this._parent.prototype._addCannedFile.apply(this, arguments);
4943 this._addToList(id, this.getName(id), true);
4944 this._templating.hideSpinner(id);
4945 this._templating.hideCancel(id);
4946 this._markFileAsSuccessful(id);
4955 * This defines FineUploader mode, which is a default UI w/ drag & drop uploading.
4957 qq.FineUploader = function(o, namespace) {
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);
4966 // Options provided by FineUploader mode
4967 qq.extend(this._options, {
4979 formatProgress: "{percent}% of {total_size}",
4980 failUpload: "Upload failed",
4981 waitingForResponse: "Processing...",
4985 template: "qq-template",
4988 retrying: "qq-upload-retrying",
4989 retryable: "qq-upload-retryable",
4990 success: "qq-upload-success",
4991 fail: "qq-upload-fail",
4992 editable: "qq-editable",
4994 dropActive: "qq-upload-drop-area-active"
4997 failedUploadTextDisplay: {
4998 mode: "default", //default, custom, or none
5000 responseProperty: "error",
5005 tooManyFilesError: "You may only drop one file",
5006 unsupportedBrowser: "Unrecoverable error - this browser does not permit file uploading of any kind."
5010 showAutoRetryNote: true,
5011 autoRetryNote: "Retrying {retryNum}/{maxAuto}..."
5015 forceConfirm: false,
5016 confirmMessage: "Are you sure you want to delete {filename}?",
5017 deletingStatusText: "Deleting...",
5018 deletingFailedText: "Delete failed"
5023 fileSizeOnSubmit: false,
5028 promptForName: false,
5029 namePromptMessage: "Please name this image"
5034 waitUntilResponse: false,
5035 notAvailablePath: null,
5040 showMessage: function(message){
5041 setTimeout(function() {
5042 window.alert(message);
5046 showConfirm: function(message) {
5047 return window.confirm(message);
5050 showPrompt: function(message, defaultValue) {
5051 return window.prompt(message, defaultValue);
5055 // Replace any default options with user defined ones
5056 qq.extend(this._options, o, true);
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,
5066 hide: this._options.classes.hide,
5067 editable: this._options.classes.editable
5070 waitUntilUpdate: this._options.thumbnails.placeholders.waitUntilResponse,
5071 thumbnailNotAvailable: this._options.thumbnails.placeholders.notAvailablePath,
5072 waitingForThumbnail: this._options.thumbnails.placeholders.waitingPath
5074 text: this._options.text
5077 if (!qq.supportedFeatures.uploading || (this._options.cors.expected && !qq.supportedFeatures.uploadCors)) {
5078 this._templating.renderFailure(this._options.messages.unsupportedBrowser);
5081 this._wrapCallbacks();
5083 this._templating.render();
5085 this._classes = this._options.classes;
5087 if (!this._options.button && this._templating.getButton()) {
5088 this._defaultButtonId = this._createUploadButton({element: this._templating.getButton()}).getButtonId();
5091 this._setupClickAndEditEventHandlers();
5093 if (qq.DragAndDrop && qq.supportedFeatures.fileDrop) {
5094 this._dnd = this._setupDragAndDrop();
5097 if (this._options.paste.targetElement && this._options.paste.promptForName) {
5098 if (qq.PasteSupport) {
5099 this._setupPastePrompt();
5102 qq.log("Paste support module not found.", "info");
5106 this._totalFilesInBatch = 0;
5107 this._filesInBatchAddedToUi = 0;
5111 // Inherit the base public & private API methods
5112 qq.extend(qq.FineUploader.prototype, qq.basePublicApi);
5113 qq.extend(qq.FineUploader.prototype, qq.basePrivateApi);
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);
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.
5126 * @param spec Specification object used to control various templating behaviors
5129 qq.Templating = function(spec) {
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,
5142 templateIdOrEl: "qq-template",
5144 fileContainerEl: null,
5146 imageGenerator: null,
5149 editable: "qq-editable"
5152 waitUntilUpdate: false,
5153 thumbnailNotAvailable: null,
5154 waitingForThumbnail: null
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"
5181 previewGeneration = {},
5182 cachedThumbnailNotAvailableImg = new qq.Promise(),
5183 cachedWaitingForThumbnailImg = new qq.Promise(),
5185 isEditElementsExist,
5186 isRetryElementExist,
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.
5199 * @returns {{template: *, fileTemplate: *}} HTML for the top-level file items templates
5201 function parseAndGetTemplate() {
5212 log("Parsing template");
5215 if (options.templateIdOrEl == null) {
5216 throw new Error("You MUST specify either a template element or ID!");
5219 // Grab the contents of the script tag holding the template.
5220 if (qq.isString(options.templateIdOrEl)) {
5221 scriptEl = document.getElementById(options.templateIdOrEl);
5223 if (scriptEl === null) {
5224 throw new Error(qq.format("Cannot find template script at ID '{}'!", options.templateIdOrEl));
5227 scriptHtml = scriptEl.innerHTML;
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.");
5235 scriptHtml = options.templateIdOrEl.innerHTML;
5238 scriptHtml = qq.trimStr(scriptHtml);
5239 tempTemplateEl = document.createElement("div");
5240 tempTemplateEl.appendChild(qq.toElement(scriptHtml));
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();
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();
5264 dropArea = qq(tempTemplateEl).getByClass(selectorClasses.drop)[0];
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();
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)) {
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();
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;
5295 serverScale = qq(thumbnail).hasAttribute(THUMBNAIL_SERVER_SCALE_ATTR);
5297 showThumbnails = showThumbnails && thumbnail;
5299 isEditElementsExist = qq(tempTemplateEl).getByClass(selectorClasses.editFilenameInput).length > 0;
5300 isRetryElementExist = qq(tempTemplateEl).getByClass(selectorClasses.retry).length > 0;
5302 fileListNode = qq(tempTemplateEl).getByClass(selectorClasses.list)[0];
5304 if (fileListNode == null) {
5305 throw new Error("Could not find the file list container in the template!");
5308 fileListHtml = fileListNode.innerHTML;
5309 fileListNode.innerHTML = "";
5311 log("Template parsing complete");
5314 template: qq.trimStr(tempTemplateEl.innerHTML),
5315 fileTemplate: qq.trimStr(fileListHtml)
5319 function getFile(id) {
5320 return qq(fileList).getByClass(FILE_CLASS_PREFIX + id)[0];
5323 function getTemplateEl(context, cssClass) {
5324 return qq(context).getByClass(cssClass)[0];
5327 function prependFile(el, index) {
5328 var parentEl = fileList,
5329 beforeEl = parentEl.firstChild;
5332 beforeEl = qq(parentEl).children()[index].nextSibling;
5336 parentEl.insertBefore(el, beforeEl);
5339 function getCancel(id) {
5340 return getTemplateEl(getFile(id), selectorClasses.cancel);
5343 function getPause(id) {
5344 return getTemplateEl(getFile(id), selectorClasses.pause);
5347 function getContinue(id) {
5348 return getTemplateEl(getFile(id), selectorClasses.continueButton);
5351 function getProgress(id) {
5352 return getTemplateEl(getFile(id), selectorClasses.progressBarContainer) ||
5353 getTemplateEl(getFile(id), selectorClasses.progressBar);
5356 function getSpinner(id) {
5357 return getTemplateEl(getFile(id), selectorClasses.spinner);
5360 function getEditIcon(id) {
5361 return getTemplateEl(getFile(id), selectorClasses.editNameIcon);
5364 function getSize(id) {
5365 return getTemplateEl(getFile(id), selectorClasses.size);
5368 function getDelete(id) {
5369 return getTemplateEl(getFile(id), selectorClasses.deleteButton);
5372 function getRetry(id) {
5373 return getTemplateEl(getFile(id), selectorClasses.retry);
5376 function getFilename(id) {
5377 return getTemplateEl(getFile(id), selectorClasses.file);
5380 function getDropProcessing() {
5381 return getTemplateEl(container, selectorClasses.dropProcessing);
5384 function getThumbnail(id) {
5385 return showThumbnails && getTemplateEl(getFile(id), selectorClasses.thumbnail);
5389 el && qq(el).addClass(options.classes.hide);
5393 el && qq(el).removeClass(options.classes.hide);
5396 function setProgressBarWidth(id, percent) {
5397 var bar = getProgress(id);
5399 if (bar && !qq(bar).hasClass(selectorClasses.progressBar)) {
5400 bar = qq(bar).getByClass(selectorClasses.progressBar)[0];
5403 bar && qq(bar).css({width: percent + "%"});
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,
5413 maxSize: thumbnailMaxSize,
5417 if (showThumbnails) {
5418 if (notAvailableUrl) {
5419 options.imageGenerator.generate(notAvailableUrl, new Image(), spec).then(
5420 function(updatedImg) {
5421 cachedThumbnailNotAvailableImg.success(updatedImg);
5424 cachedThumbnailNotAvailableImg.failure();
5425 log("Problem loading 'not available' placeholder image at " + notAvailableUrl, "error");
5430 cachedThumbnailNotAvailableImg.failure();
5434 options.imageGenerator.generate(waitingUrl, new Image(), spec).then(
5435 function(updatedImg) {
5436 cachedWaitingForThumbnailImg.success(updatedImg);
5439 cachedWaitingForThumbnailImg.failure();
5440 log("Problem loading 'waiting for thumbnail' placeholder image at " + waitingUrl, "error");
5445 cachedWaitingForThumbnailImg.failure();
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;
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.
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();
5474 cachedThumbnailNotAvailableImg.then(function(img) {
5477 delete previewGeneration[id];
5480 maybeScalePlaceholderViaCss(img, thumbnail);
5481 thumbnail.src = img.src;
5483 delete previewGeneration[id];
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;
5496 if (maxHeight && maxWidth && !thumbnail.style.maxWidth && !thumbnail.style.maxHeight) {
5499 maxHeight: maxHeight
5505 qq.extend(options, spec);
5508 container = options.containerEl;
5509 showThumbnails = options.imageGenerator !== undefined;
5510 templateHtml = parseAndGetTemplate();
5512 cacheThumbnailPlaceholders();
5515 render: function() {
5516 log("Rendering template in DOM.");
5518 container.innerHTML = templateHtml.template;
5519 hide(getDropProcessing());
5520 fileList = options.fileContainerEl || getTemplateEl(container, selectorClasses.list);
5522 log("Template rendering complete");
5525 renderFailure: function(message) {
5526 var cantRenderEl = qq.toElement(message);
5527 container.innerHTML = "";
5528 container.appendChild(cantRenderEl);
5535 clearFiles: function() {
5536 fileList.innerHTML = "";
5539 disableCancel: function() {
5540 isCancelDisabled = true;
5543 addFile: function(id, name, prependInfo) {
5544 var fileEl = qq.toElement(templateHtml.fileTemplate),
5545 fileNameEl = getTemplateEl(fileEl, selectorClasses.file);
5547 qq(fileEl).addClass(FILE_CLASS_PREFIX + id);
5548 fileNameEl && qq(fileNameEl).setText(name);
5549 fileEl.setAttribute(FILE_ID_ATTR, id);
5552 prependFile(fileEl, prependInfo.index);
5555 fileList.appendChild(fileEl);
5558 hide(getProgress(id));
5560 hide(getDelete(id));
5563 hide(getContinue(id));
5565 if (isCancelDisabled) {
5566 this.hideCancel(id);
5570 removeFile: function(id) {
5571 qq(getFile(id)).remove();
5574 getFileId: function(el) {
5575 var currentNode = el;
5579 while (currentNode.getAttribute(FILE_ID_ATTR) == null) {
5580 currentNode = currentNode.parentNode;
5583 return parseInt(currentNode.getAttribute(FILE_ID_ATTR));
5587 getFileList: function() {
5591 markFilenameEditable: function(id) {
5592 var filename = getFilename(id);
5594 filename && qq(filename).addClass(options.classes.editable);
5597 updateFilename: function(id, name) {
5598 var filename = getFilename(id);
5600 filename && qq(filename).setText(name);
5603 hideFilename: function(id) {
5604 hide(getFilename(id));
5607 showFilename: function(id) {
5608 show(getFilename(id));
5611 isFileName: function(el) {
5612 return qq(el).hasClass(selectorClasses.file);
5615 getButton: function() {
5616 return options.button || getTemplateEl(container, selectorClasses.button);
5619 hideDropProcessing: function() {
5620 hide(getDropProcessing());
5623 showDropProcessing: function() {
5624 show(getDropProcessing());
5627 getDropZone: function() {
5628 return getTemplateEl(container, selectorClasses.drop);
5631 isEditFilenamePossible: function() {
5632 return isEditElementsExist;
5635 isRetryPossible: function() {
5636 return isRetryElementExist;
5639 getFileContainer: function(id) {
5643 showEditIcon: function(id) {
5644 var icon = getEditIcon(id);
5646 icon && qq(icon).addClass(options.classes.editable);
5649 hideEditIcon: function(id) {
5650 var icon = getEditIcon(id);
5652 icon && qq(icon).removeClass(options.classes.editable);
5655 isEditIcon: function(el) {
5656 return qq(el).hasClass(selectorClasses.editNameIcon);
5659 getEditInput: function(id) {
5660 return getTemplateEl(getFile(id), selectorClasses.editFilenameInput);
5663 isEditInput: function(el) {
5664 return qq(el).hasClass(selectorClasses.editFilenameInput);
5667 updateProgress: function(id, loaded, total) {
5668 var bar = getProgress(id),
5672 percent = Math.round(loaded / total * 100);
5674 if (loaded === total) {
5681 setProgressBarWidth(id, percent);
5685 hideProgress: function(id) {
5686 var bar = getProgress(id);
5691 resetProgress: function(id) {
5692 setProgressBarWidth(id, 0);
5695 showCancel: function(id) {
5696 if (!isCancelDisabled) {
5697 var cancel = getCancel(id);
5699 cancel && qq(cancel).removeClass(options.classes.hide);
5703 hideCancel: function(id) {
5704 hide(getCancel(id));
5707 isCancel: function(el) {
5708 return qq(el).hasClass(selectorClasses.cancel);
5711 allowPause: function(id) {
5713 hide(getContinue(id));
5716 uploadPaused: function(id) {
5717 this.setStatusText(id, options.text.paused);
5718 this.allowContinueButton(id);
5719 hide(getSpinner(id));
5722 hidePause: function(id) {
5726 isPause: function(el) {
5727 return qq(el).hasClass(selectorClasses.pause);
5730 isContinueButton: function(el) {
5731 return qq(el).hasClass(selectorClasses.continueButton);
5734 allowContinueButton: function(id) {
5735 show(getContinue(id));
5739 uploadContinued: function(id) {
5740 this.setStatusText(id, "");
5741 this.allowPause(id);
5742 show(getSpinner(id));
5745 showDeleteButton: function(id) {
5746 show(getDelete(id));
5749 hideDeleteButton: function(id) {
5750 hide(getDelete(id));
5753 isDeleteButton: function(el) {
5754 return qq(el).hasClass(selectorClasses.deleteButton);
5757 isRetry: function(el) {
5758 return qq(el).hasClass(selectorClasses.retry);
5761 updateSize: function(id, text) {
5762 var size = getSize(id);
5766 qq(size).setText(text);
5770 setStatusText: function(id, text) {
5771 var textEl = getTemplateEl(getFile(id), selectorClasses.statusText);
5776 qq(textEl).clearText();
5779 qq(textEl).setText(text);
5784 hideSpinner: function(id) {
5785 hide(getSpinner(id));
5788 showSpinner: function(id) {
5789 show(getSpinner(id));
5792 generatePreview: function(id, fileOrBlob) {
5793 var thumbnail = getThumbnail(id),
5795 maxSize: thumbnailMaxSize,
5800 if (qq.supportedFeatures.imagePreviews) {
5801 previewGeneration[id] = new qq.Promise();
5804 displayWaitingImg(thumbnail);
5805 return options.imageGenerator.generate(fileOrBlob, thumbnail, spec).then(
5808 previewGeneration[id].success();
5811 previewGeneration[id].failure();
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);
5821 else if (thumbnail) {
5822 displayWaitingImg(thumbnail);
5826 updateThumbnail: function(id, thumbnailUrl, showWaitingImg) {
5827 var thumbnail = getThumbnail(id),
5829 maxSize: thumbnailMaxSize,
5835 if (showWaitingImg) {
5836 displayWaitingImg(thumbnail);
5839 return options.imageGenerator.generate(thumbnailUrl, thumbnail, spec).then(
5844 maybeSetDisplayNotAvailableImg(id, thumbnail);
5849 maybeSetDisplayNotAvailableImg(id, thumbnail);
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
5862 * @param options Options passed from the base handler
5863 * @param proxy Callbacks & methods used to query for or push out data/changes
5865 qq.UploadHandlerForm = function(options, proxy) {
5869 uploadCompleteCallback = proxy.onUploadComplete,
5870 onUuidChanged = proxy.onUuidChanged,
5871 getName = proxy.getName,
5872 getUuid = proxy.getUuid,
5873 uploadComplete = uploadCompleteCallback,
5879 * Returns json object received by iframe from server.
5881 function getIframeContentJson(id, iframe) {
5882 /*jshint evil: true*/
5886 //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
5888 // iframe.contentWindow.document - for IE<7
5889 var doc = iframe.contentDocument || iframe.contentWindow.document,
5890 innerHtml = doc.body.innerHTML;
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;
5899 response = internalApi.parseJsonResponse(id, innerHtml);
5902 log("Error when attempting to parse form upload response (" + error.message + ")", "error");
5903 response = {success: false};
5910 * Creates form, that will be submitted to iframe
5912 function createForm(id, iframe){
5913 var params = options.paramsStore.getParams(id),
5914 method = options.demoMode ? "GET" : "POST",
5915 endpoint = options.endpointStore.getEndpoint(id),
5918 params[options.uuidName] = getUuid(id);
5919 params[options.filenameParam] = name;
5921 return internalApi.initFormForUpload({
5925 paramsInBody: options.paramsInBody,
5926 targetName: iframe.name
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}));
5935 upload: function(id) {
5936 var input = fileState[id].input,
5937 fileName = getName(id),
5938 iframe = internalApi.createIframe(id),
5942 throw new Error("file with passed id was not added, or already uploaded or canceled");
5945 options.onUpload(id, getName(id));
5947 form = createForm(id, iframe);
5948 form.appendChild(input);
5950 internalApi.attachLoadEvent(iframe, function(responseFromMessage){
5951 log("iframe loaded");
5953 var response = responseFromMessage ? responseFromMessage : getIframeContentJson(id, iframe);
5955 internalApi.detachLoadEvent(id);
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();
5962 if (!response.success) {
5963 if (options.onAutoRetry(id, fileName, response)) {
5967 options.onComplete(id, fileName, response);
5971 log("Sending upload request for " + id);
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.
5983 * @param spec Options passed from the base handler
5984 * @param proxy Callbacks & methods used to query for or push out data/changes
5986 qq.UploadHandlerXhr = function(spec, proxy) {
5989 var uploadComplete = proxy.onUploadComplete,
5990 onUuidChanged = proxy.onUuidChanged,
5991 getName = proxy.getName,
5992 getUuid = proxy.getUuid,
5993 getSize = proxy.getSize,
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,
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)) {
6010 return spec.resume.id;
6014 resumeId = getResumeId();
6016 function addChunkingSpecificParams(id, params, chunkData) {
6017 var size = getSize(id),
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;
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.
6031 params[spec.filenameParam] = name;
6035 function addResumeSpecificParams(params) {
6036 params[spec.resume.paramNames.resuming] = true;
6039 function getChunk(fileOrBlob, startByte, endByte) {
6040 if (fileOrBlob.slice) {
6041 return fileOrBlob.slice(startByte, endByte);
6043 else if (fileOrBlob.mozSlice) {
6044 return fileOrBlob.mozSlice(startByte, endByte);
6046 else if (fileOrBlob.webkitSlice) {
6047 return fileOrBlob.webkitSlice(startByte, endByte);
6051 function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
6052 var formData = new FormData(),
6053 method = spec.demoMode ? "GET" : "POST",
6054 endpoint = spec.endpointStore.getEndpoint(id),
6059 params[spec.uuidName] = getUuid(id);
6060 params[spec.filenameParam] = name;
6064 params[spec.totalFileSizeName] = size;
6067 //build query string
6068 if (!spec.paramsInBody) {
6070 params[spec.inputName] = name;
6072 url = qq.obj2url(params, endpoint);
6075 xhr.open(method, url, true);
6077 if (spec.cors.expected && spec.cors.sendCredentials) {
6078 xhr.withCredentials = true;
6082 if (spec.paramsInBody) {
6083 qq.obj2FormData(params, formData);
6086 formData.append(spec.inputName, fileOrBlob);
6093 function setHeaders(id, xhr) {
6094 var extraHeaders = spec.customHeaders,
6095 fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
6097 xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
6098 xhr.setRequestHeader("Cache-Control", "no-cache");
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);
6106 qq.each(extraHeaders, function(name, val) {
6107 xhr.setRequestHeader(name, val);
6111 function handleCompletedItem(id, response, xhr) {
6112 var name = getName(id),
6115 fileState[id].attemptingResume = false;
6117 spec.onProgress(id, name, size, size);
6118 spec.onComplete(id, name, response, xhr);
6120 if (fileState[id]) {
6121 delete fileState[id].xhr;
6127 function uploadNextChunk(id) {
6128 var chunkIdx = fileState[id].remainingChunkIdxs[0],
6129 chunkData = internalApi.getChunkData(id, chunkIdx),
6130 xhr = internalApi.createXhr(id),
6135 if (fileState[id].loaded === undefined) {
6136 fileState[id].loaded = 0;
6139 if (resumeEnabled && fileState[id].file) {
6140 persistChunkData(id, chunkData);
6143 xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
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);
6150 spec.onProgress(id, name, totalLoaded, estTotalRequestsSize);
6154 spec.onUploadChunk(id, name, internalApi.getChunkDataForCallback(chunkData));
6156 params = spec.paramsStore.getParams(id);
6157 addChunkingSpecificParams(id, params, chunkData);
6159 if (fileState[id].attemptingResume) {
6160 addResumeSpecificParams(params);
6163 toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
6164 setHeaders(id, xhr);
6166 log("Sending chunked upload request for item " + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
6170 function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
6171 var chunkData = internalApi.getChunkData(id, chunkIdx),
6172 blobSize = chunkData.size,
6173 overhead = requestSize - blobSize,
6175 chunkCount = chunkData.count,
6176 initialRequestOverhead = fileState[id].initialRequestOverhead,
6177 overheadDiff = overhead - initialRequestOverhead;
6179 fileState[id].lastRequestOverhead = overhead;
6181 if (chunkIdx === 0) {
6182 fileState[id].lastChunkIdxProgress = 0;
6183 fileState[id].initialRequestOverhead = overhead;
6184 fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
6186 else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
6187 fileState[id].lastChunkIdxProgress = chunkIdx;
6188 fileState[id].estTotalRequestsSize += overheadDiff;
6191 return fileState[id].estTotalRequestsSize;
6194 function getLastRequestOverhead(id) {
6196 return fileState[id].lastRequestOverhead;
6203 function handleSuccessfullyCompletedChunk(id, response, xhr) {
6204 var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
6205 chunkData = internalApi.getChunkData(id, chunkIdx);
6207 fileState[id].attemptingResume = false;
6208 fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
6210 spec.onUploadChunkSuccess(id, internalApi.getChunkDataForCallback(chunkData), response, xhr);
6212 if (fileState[id].remainingChunkIdxs.length > 0) {
6213 uploadNextChunk(id);
6216 if (resumeEnabled) {
6217 deletePersistedChunkData(id);
6220 handleCompletedItem(id, response, xhr);
6224 function isErrorResponse(xhr, response) {
6225 return xhr.status !== 200 || !response.success || response.reset;
6228 function parseResponse(id, xhr) {
6232 log(qq.format("Received response status {} with body: {}", xhr.status, xhr.responseText));
6234 response = qq.parseJson(xhr.responseText);
6236 if (response.newUuid !== undefined) {
6237 onUuidChanged(id, response.newUuid);
6241 log("Error when attempting to parse xhr response text (" + error.message + ")", "error");
6248 function handleResetResponse(id) {
6249 log("Server has ordered chunking effort to be restarted on next attempt for item ID " + id, "error");
6251 if (resumeEnabled) {
6252 deletePersistedChunkData(id);
6253 fileState[id].attemptingResume = false;
6256 fileState[id].remainingChunkIdxs = [];
6257 delete fileState[id].loaded;
6258 delete fileState[id].estTotalRequestsSize;
6259 delete fileState[id].initialRequestOverhead;
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);
6269 function handleNonResetErrorResponse(id, response, xhr) {
6270 var name = getName(id);
6272 if (spec.onAutoRetry(id, name, response, xhr)) {
6276 handleCompletedItem(id, response, xhr);
6280 function onComplete(id, xhr) {
6281 var state = fileState[id],
6282 attemptingResume = state && state.attemptingResume,
6283 paused = state && state.paused,
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) {
6292 log("xhr - server response received for " + id);
6293 log("responseText = " + xhr.responseText);
6294 response = parseResponse(id, xhr);
6296 if (isErrorResponse(xhr, response)) {
6297 if (response.reset) {
6298 handleResetResponse(id);
6301 if (attemptingResume && response.reset) {
6302 handleResetResponseOnResumeAttempt(id);
6305 handleNonResetErrorResponse(id, response, xhr);
6308 else if (chunkFiles) {
6309 handleSuccessfullyCompletedChunk(id, response, xhr);
6312 handleCompletedItem(id, response, xhr);
6316 function getReadyStateChangeHandler(id, xhr) {
6318 if (xhr.readyState === 4) {
6319 onComplete(id, xhr);
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;
6337 qq.setCookie(cookieName, cookieValue, cookieExpDays);
6340 function deletePersistedChunkData(id) {
6341 if (fileState[id].file) {
6342 var cookieName = getChunkDataCookieName(id);
6343 qq.deleteCookie(cookieName);
6347 function getPersistedChunkData(id) {
6348 var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
6349 filename = getName(id),
6350 sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
6352 if (chunkCookieValue) {
6353 sections = chunkCookieValue.split(cookieItemDelimiter);
6355 if (sections.length === 5) {
6357 partIndex = parseInt(sections[1], 10);
6358 lastByteSent = parseInt(sections[2], 10);
6359 initialRequestOverhead = parseInt(sections[3], 10);
6360 estTotalRequestsSize = parseInt(sections[4], 10);
6365 lastByteSent: lastByteSent,
6366 initialRequestOverhead: initialRequestOverhead,
6367 estTotalRequestsSize: estTotalRequestsSize
6371 log("Ignoring previously stored resume/chunk cookie for " + filename + " - old cookie format", "warn");
6376 function getChunkDataCookieName(id) {
6377 var filename = getName(id),
6378 fileSize = getSize(id),
6379 maxChunkSize = spec.chunking.partSize,
6382 cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
6384 if (resumeId !== undefined) {
6385 cookieName += cookieItemDelimiter + resumeId;
6391 function calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex) {
6392 var currentChunkIndex;
6394 for (currentChunkIndex = internalApi.getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
6395 fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
6398 uploadNextChunk(id);
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);
6409 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6412 function handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex) {
6413 var name = getName(id),
6414 firstChunkDataForResume = internalApi.getChunkData(id, persistedChunkInfoForResume.part),
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(
6422 onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume);
6425 log("onResume promise fulfilled - failure indicated. Will not resume.");
6426 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6430 else if (onResumeRetVal !== false) {
6431 onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume);
6434 log("onResume callback returned false. Will not resume.");
6435 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6439 function handleFileChunkingUpload(id, retry) {
6440 var firstChunkIndex = 0,
6441 persistedChunkInfoForResume;
6443 if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
6444 fileState[id].remainingChunkIdxs = [];
6446 if (resumeEnabled && !retry && fileState[id].file) {
6447 persistedChunkInfoForResume = getPersistedChunkData(id);
6448 if (persistedChunkInfoForResume) {
6449 handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex);
6452 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6456 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
6460 uploadNextChunk(id);
6464 function handleStandardFileUpload(id) {
6465 var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
6467 xhr, params, toSend;
6469 fileState[id].loaded = 0;
6471 xhr = internalApi.createXhr(id);
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);
6480 xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
6482 params = spec.paramsStore.getParams(id);
6483 toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
6484 setHeaders(id, xhr);
6486 log("Sending upload request for " + id);
6490 function handleUploadSignal(id, retry) {
6491 var name = getName(id);
6493 if (publicApi.isValid(id)) {
6494 spec.onUpload(id, name);
6497 handleFileChunkingUpload(id, retry);
6500 handleStandardFileUpload(id);
6506 qq.extend(this, new qq.UploadHandlerXhrApi(
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}
6513 // Base XHR API overrides
6514 qq.override(this, function(super_) {
6516 add: function(id, fileOrBlobData) {
6517 var persistedChunkData;
6519 super_.add.apply(this, arguments);
6521 if (resumeEnabled) {
6522 persistedChunkData = getPersistedChunkData(id);
6524 if (persistedChunkData) {
6525 onUuidChanged(id, persistedChunkData.uuid);
6532 getResumableFilesData: function() {
6533 var matchingCookieNames = [],
6534 resumableFilesData = [];
6536 if (chunkFiles && resumeEnabled) {
6537 if (resumeId === undefined) {
6538 matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
6539 cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + spec.chunking.partSize + "="));
6542 matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
6543 cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + spec.chunking.partSize + "\\" +
6544 cookieItemDelimiter + resumeId + "="));
6547 qq.each(matchingCookieNames, function(idx, cookieName) {
6548 var cookiesNameParts = cookieName.split(cookieItemDelimiter);
6549 var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
6551 resumableFilesData.push({
6552 name: decodeURIComponent(cookiesNameParts[1]),
6553 size: cookiesNameParts[2],
6554 uuid: cookieValueParts[0],
6555 partIdx: cookieValueParts[1]
6559 return resumableFilesData;
6564 expunge: function(id) {
6565 if (resumeEnabled) {
6566 deletePersistedChunkData(id);
6576 qq.PasteSupport = function(o) {
6579 var options, detachPasteHandler;
6582 targetElement: null,
6584 log: function(message, level) {},
6585 pasteReceived: function(blob) {}
6589 function isImage(item) {
6591 item.type.indexOf("image/") === 0;
6594 function registerPasteHandler() {
6595 qq(options.targetElement).attach("paste", function(event) {
6596 var clipboardData = event.clipboardData;
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);
6609 function unregisterPasteHandler() {
6610 if (detachPasteHandler) {
6611 detachPasteHandler();
6615 qq.extend(options, o);
6616 registerPasteHandler();
6620 unregisterPasteHandler();
6625 /*globals qq, document, CustomEvent*/
6626 qq.DragAndDrop = function(o) {
6630 HIDE_ZONES_EVENT_NAME = "qq-hidezones",
6631 HIDE_BEFORE_ENTER_ATTR = "qq-hide-dropzone",
6632 uploadDropZones = [],
6634 disposeSupport = new qq.DisposeSupport();
6637 dropZoneElements: [],
6638 allowMultipleItems: true,
6642 callbacks: new qq.DragAndDrop.callbacks()
6645 qq.extend(options, o, true);
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);
6651 options.callbacks.dropLog("Grabbed " + files.length + " dropped files.");
6652 uploadDropZone.dropDisabled(false);
6653 options.callbacks.processingDroppedFilesComplete(filesAsArray, uploadDropZone.getElement());
6656 function traverseFileTree(entry) {
6658 parseEntryPromise = new qq.Promise();
6661 entry.file(function(file) {
6662 droppedFiles.push(file);
6663 parseEntryPromise.success();
6665 function(fileError) {
6666 options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error");
6667 parseEntryPromise.failure();
6670 else if (entry.isDirectory) {
6671 dirReader = entry.createReader();
6672 dirReader.readEntries(function(entries) {
6673 var entriesLeft = entries.length;
6675 qq.each(entries, function(idx, entry) {
6676 traverseFileTree(entry).done(function() {
6679 if (entriesLeft === 0) {
6680 parseEntryPromise.success();
6685 if (!entries.length) {
6686 parseEntryPromise.success();
6688 }, function(fileError) {
6689 options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error");
6690 parseEntryPromise.failure();
6694 return parseEntryPromise;
6697 function handleDataTransfer(dataTransfer, uploadDropZone) {
6698 var pendingFolderPromises = [],
6699 handleDataTransferPromise = new qq.Promise();
6701 options.callbacks.processingDroppedFiles();
6702 uploadDropZone.dropDisabled(true);
6704 if (dataTransfer.files.length > 1 && !options.allowMultipleItems) {
6705 options.callbacks.processingDroppedFilesComplete([]);
6706 options.callbacks.dropError("tooManyFilesError", "");
6707 uploadDropZone.dropDisabled(false);
6708 handleDataTransferPromise.failure();
6713 if (qq.isFolderDropSupported(dataTransfer)) {
6714 qq.each(dataTransfer.items, function(idx, item) {
6715 var entry = item.webkitGetAsEntry();
6718 //due to a bug in Chrome's File System API impl - #149735
6720 droppedFiles.push(item.getAsFile());
6724 pendingFolderPromises.push(traverseFileTree(entry).done(function() {
6725 pendingFolderPromises.pop();
6726 if (pendingFolderPromises.length === 0) {
6727 handleDataTransferPromise.success();
6735 droppedFiles = dataTransfer.files;
6738 if (pendingFolderPromises.length === 0) {
6739 handleDataTransferPromise.success();
6743 return handleDataTransferPromise;
6746 function setupDropzone(dropArea) {
6747 var dropZone = new qq.UploadDropZone({
6748 HIDE_ZONES_EVENT_NAME: HIDE_ZONES_EVENT_NAME,
6750 onEnter: function(e){
6751 qq(dropArea).addClass(options.classes.dropActive);
6752 e.stopPropagation();
6754 onLeaveNotDescendants: function(e){
6755 qq(dropArea).removeClass(options.classes.dropActive);
6757 onDrop: function(e){
6758 handleDataTransfer(e.dataTransfer, dropZone).done(function() {
6759 uploadDroppedFiles(droppedFiles, dropZone);
6764 disposeSupport.addDisposer(function() {
6768 qq(dropArea).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropArea).hide();
6770 uploadDropZones.push(dropZone);
6775 function isFileDrag(dragEvent) {
6778 qq.each(dragEvent.dataTransfer.types, function(key, val) {
6779 if (val === "Files") {
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);
6796 function setupDragDrop() {
6797 var dropZones = options.dropZoneElements;
6799 qq.each(dropZones, function(idx, dropZone) {
6800 var uploadDropZone = setupDropzone(dropZone);
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"});
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();
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();
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);
6843 setupExtraDropzone: function(element) {
6844 options.dropZoneElements.push(element);
6845 setupDropzone(element);
6848 removeDropzone: function(element) {
6850 dzs = options.dropZoneElements;
6853 if (dzs[i] === element) {
6854 return dzs.splice(i, 1);
6859 dispose: function() {
6860 disposeSupport.dispose();
6861 qq.each(uploadDropZones, function(idx, dropZone) {
6868 qq.DragAndDrop.callbacks = function() {
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");
6877 dropLog: function(message, level) {
6878 qq.log(message, level);
6883 qq.UploadDropZone = function(o){
6886 var disposeSupport = new qq.DisposeSupport(),
6887 options, element, preventDrop, dropOutsideDisabled;
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){}
6898 qq.extend(options, o);
6899 element = options.element;
6901 function dragover_should_be_canceled(){
6902 return qq.safari() || (qq.firefox() && qq.windows());
6905 function disableDropOutside(e){
6906 // run only once for all instances
6907 if (!dropOutsideDisabled ){
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){
6915 disposeSupport.attach(document, "dragover", function(e){
6916 if (e.dataTransfer){
6917 e.dataTransfer.dropEffect = "none";
6923 dropOutsideDisabled = true;
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()) {
6934 var effectTest, dt = e.dataTransfer,
6935 // do not check dt.types.contains in webkit, because it crashes safari 4
6936 isSafari = qq.safari();
6938 // dt.effectAllowed is none in Safari 5
6939 // dt.types.contains check is for firefox
6941 // dt.effectAllowed crashes IE11 when files have been dragged from
6943 effectTest = (qq.ie10() || qq.ie11()) ? true : dt.effectAllowed !== "none";
6944 return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains("Files")));
6947 function isOrSetDropDisabled(isDisabled) {
6948 if (isDisabled !== undefined) {
6949 preventDrop = isDisabled;
6954 function triggerHidezonesEvent() {
6957 function triggerUsingOldApi() {
6958 hideZonesEvent = document.createEvent("Event");
6959 hideZonesEvent.initEvent(options.HIDE_ZONES_EVENT_NAME, true, true);
6962 if (window.CustomEvent) {
6964 hideZonesEvent = new CustomEvent(options.HIDE_ZONES_EVENT_NAME);
6967 triggerUsingOldApi();
6971 triggerUsingOldApi();
6974 document.dispatchEvent(hideZonesEvent);
6977 function attachEvents(){
6978 disposeSupport.attach(element, "dragover", function(e){
6979 if (!isValidFileDrag(e)) {
6983 // dt.effectAllowed crashes IE11 when files have been dragged from
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)
6989 e.dataTransfer.dropEffect = "copy"; // for Chrome
6992 e.stopPropagation();
6996 disposeSupport.attach(element, "dragenter", function(e){
6997 if (!isOrSetDropDisabled()) {
6998 if (!isValidFileDrag(e)) {
7005 disposeSupport.attach(element, "dragleave", function(e){
7006 if (!isValidFileDrag(e)) {
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)) {
7018 options.onLeaveNotDescendants(e);
7021 disposeSupport.attach(element, "drop", function(e) {
7022 if (!isOrSetDropDisabled()) {
7023 if (!isValidFileDrag(e)) {
7028 e.stopPropagation();
7031 triggerHidezonesEvent();
7036 disableDropOutside();
7040 dropDisabled: function(isDisabled) {
7041 return isOrSetDropDisabled(isDisabled);
7044 dispose: function() {
7045 disposeSupport.dispose();
7048 getElement: function() {
7054 /*globals qq, XMLHttpRequest*/
7055 qq.DeleteFileAjaxRequester = function(o) {
7061 uuidParamName: "qquuid",
7069 sendCredentials: false
7071 log: function(str, level) {},
7072 onDelete: function(id) {},
7073 onDeleteComplete: function(id, xhrOrXdr, isError) {}
7076 qq.extend(options, o);
7078 function getMandatedParams() {
7079 if (options.method.toUpperCase() === "POST") {
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,
7098 onSend: options.onDelete,
7099 onComplete: options.onDeleteComplete,
7105 sendDelete: function(id, uuid, additionalMandatedParams) {
7106 var additionalOptions = additionalMandatedParams || {};
7108 options.log("Submitting delete file request for " + id);
7110 if (options.method === "DELETE") {
7111 requester.initTransport(id)
7113 .withParams(additionalOptions)
7117 additionalOptions[options.uuidParamName] = uuid;
7118 requester.initTransport(id)
7119 .withParams(additionalOptions)
7126 /*global qq, define */
7127 /*jshint strict:false,bitwise:false,nonew:false,asi:true,-W064,-W116,-W089 */
7129 * Mega pixel image rendering library for iOS6 Safari
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.
7135 * Copyright (c) 2012 Shinichi Tomita <shinichi.tomita@gmail.com>
7136 * Released under the MIT license
7141 * Detect subsampling in loaded image.
7142 * In iOS, larger images than 2M pixels may be subsampled in rendering.
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;
7161 * Detecting vertical squash in loaded image.
7162 * Fixes a bug which squash image vertically while drawing into canvas for some images.
7164 function detectVerticalSquash(img, iw, ih) {
7165 var canvas = document.createElement('canvas');
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.
7176 var alpha = data[(py - 1) * 4 + 3];
7182 py = (ey + sy) >> 1;
7184 var ratio = (py / ih);
7185 return (ratio===0)?1:ratio;
7189 * Rendering image element (with resizing) and get its data URL
7191 function renderImageToDataURL(img, options, doSquash) {
7192 var canvas = document.createElement('canvas'),
7193 mime = options.mime || "image/jpeg";
7195 renderImageToCanvas(img, canvas, options, doSquash);
7196 return canvas.toDataURL(mime, options.quality || 0.8);
7200 * Rendering image element (with resizing) into the canvas element
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');
7207 transformCoordinate(canvas, width, height, options.orientation);
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
7212 var subsampled = detectSubsampling(img);
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);
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);
7240 tmpCanvas = tmpCtx = null;
7243 ctx.drawImage(img, 0, 0, width, height);
7248 * Transform canvas coordination according to specified frame size and orientation
7249 * Orientation value is from EXIF tag
7251 function transformCoordinate(canvas, width, height, orientation) {
7252 switch (orientation) {
7257 canvas.width = height;
7258 canvas.height = width;
7261 canvas.width = width;
7262 canvas.height = height;
7264 var ctx = canvas.getContext('2d');
7265 switch (orientation) {
7268 ctx.translate(width, 0);
7273 ctx.translate(width, height);
7274 ctx.rotate(Math.PI);
7278 ctx.translate(0, height);
7282 // vertical flip + 90 rotate right
7283 ctx.rotate(0.5 * Math.PI);
7288 ctx.rotate(0.5 * Math.PI);
7289 ctx.translate(0, -height);
7292 // horizontal flip + 90 rotate right
7293 ctx.rotate(0.5 * Math.PI);
7294 ctx.translate(width, -height);
7299 ctx.rotate(-0.5 * Math.PI);
7300 ctx.translate(-width, 0);
7309 * MegaPixImage class
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 :
7317 if (!URL) { throw Error("No createObjectURL function found to create blob url"); }
7318 img.src = URL.createObjectURL(srcImage);
7319 this.blob = srcImage;
7322 if (!srcImage.naturalWidth && !srcImage.naturalHeight) {
7324 srcImage.onload = function() {
7325 var listeners = _this.imageLoadListeners;
7327 _this.imageLoadListeners = null;
7328 for (var i=0, len=listeners.length; i<len; i++) {
7333 srcImage.onerror = errorCallback;
7334 this.imageLoadListeners = [];
7336 this.srcImage = srcImage;
7340 * Rendering megapix image into specified target element
7342 MegaPixImage.prototype.render = function(target, options) {
7343 if (this.imageLoadListeners) {
7345 this.imageLoadListeners.push(function() { _this.render(target, options) });
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;
7361 if (maxWidth && width > maxWidth) {
7363 height = (imgHeight * width / imgWidth) << 0;
7365 if (maxHeight && height > maxHeight) {
7367 width = (imgWidth * height / imgHeight) << 0;
7369 var opt = { width : width, height : height };
7370 for (var k in options) opt[k] = options[k];
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);
7378 if (typeof this.onrender === 'function') {
7379 this.onrender(target);
7384 * Export class to global
7386 if (typeof define === 'function' && define.amd) {
7387 define([], function() { return MegaPixImage; }); // for AMD loader
7389 this.MegaPixImage = MegaPixImage;
7394 /*globals qq, MegaPixImage */
7396 * Draws a thumbnail of a Blob/File/URL onto an <img> or <canvas>.
7400 qq.ImageGenerator = function(log) {
7403 function isImg(el) {
7404 return el.tagName.toLowerCase() === "img";
7407 function isCanvas(el) {
7408 return el.tagName.toLowerCase() === "canvas";
7411 function isImgCorsSupported() {
7412 return new Image().crossOrigin !== undefined;
7415 function isCanvasSupported() {
7416 var canvas = document.createElement("canvas");
7418 return canvas.getContext && canvas.getContext("2d");
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) {
7426 var pathSegments = nameWithPath.split("/"),
7427 name = pathSegments[pathSegments.length - 1],
7428 extension = qq.getExtension(name);
7430 extension = extension && extension.toLowerCase();
7435 return "image/jpeg";
7444 return "image/tiff";
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;
7457 targetAnchor.href = url;
7459 targetProtocol = targetAnchor.protocol;
7460 targetPort = targetAnchor.port;
7461 targetHostname = targetAnchor.hostname;
7463 if (targetProtocol.toLowerCase() !== window.location.protocol.toLowerCase()) {
7467 if (targetHostname.toLowerCase() !== window.location.hostname.toLowerCase()) {
7471 // IE doesn't take ports into consideration when determining if two endpoints are same origin.
7472 if (targetPort !== window.location.port && !qq.ie()) {
7479 function registerImgLoadListeners(img, promise) {
7480 img.onload = function() {
7483 promise.success(img);
7486 img.onerror = function() {
7489 log("Problem drawing thumbnail!", "error");
7490 promise.failure(img, "Problem drawing thumbnail!");
7494 function registerCanvasDrawImageListener(canvas, promise) {
7495 var context = canvas.getContext("2d"),
7496 oldDrawImage = context.drawImage;
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;
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);
7514 if (isImg(imgOrCanvas)) {
7515 registerImgLoadListeners(imgOrCanvas, promise);
7517 else if (isCanvas(imgOrCanvas)) {
7518 registerCanvasDrawImageListener(imgOrCanvas, promise);
7521 promise.failure(imgOrCanvas);
7522 log(qq.format("Element container of type {} is not supported!", imgOrCanvas.tagName), "error");
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!");
7541 identifier.isPreviewable().then(
7543 var exif = new qq.Exif(fileOrBlob, log),
7544 mpImg = new MegaPixImage(fileOrBlob, megapixErrorHandler);
7546 if (registerThumbnailRenderedListener(container, drawPreview)) {
7549 var orientation = exif.Orientation;
7551 mpImg.render(container, {
7554 orientation: orientation,
7559 function(failureMsg) {
7560 log(qq.format("EXIF data could not be parsed ({}). Assuming orientation = 1.", failureMsg));
7562 mpImg.render(container, {
7573 log("Not previewable");
7574 drawPreview.failure(container, "Not previewable");
7581 function drawOnCanvasOrImgFromUrl(url, canvasOrImg, draw, maxSize) {
7582 var tempImg = new Image(),
7583 tempImgRender = new qq.Promise();
7585 registerThumbnailRenderedListener(tempImg, tempImgRender);
7587 if (isCrossOrigin(url)) {
7588 tempImg.crossOrigin = "anonymous";
7593 tempImgRender.then(function() {
7594 registerThumbnailRenderedListener(canvasOrImg, draw);
7596 var mpImg = new MegaPixImage(tempImg);
7597 mpImg.render(canvasOrImg, {
7600 mime: determineMimeOfFileName(url)
7605 function drawOnImgFromUrlWithCssScaling(url, img, draw, maxSize) {
7606 registerThumbnailRenderedListener(img, draw);
7608 maxWidth: maxSize + "px",
7609 maxHeight: maxSize + "px"
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;
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);
7640 drawOnCanvasOrImgFromUrl(url, container, draw, maxSize);
7644 drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize);
7647 // container is a canvas, scaling optional
7648 else if (isCanvas(container)) {
7649 drawOnCanvasOrImgFromUrl(url, container, draw, maxSize);
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;
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).
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
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 || {});
7677 log("Attempting to draw client-side image preview.");
7678 return draw(fileBlobOrUrl, container, options || {});
7685 this._testing.isImg = isImg;
7686 this._testing.isCanvas = isCanvas;
7687 this._testing.isCrossOrigin = isCrossOrigin;
7688 this._testing.determineMimeOfFileName = determineMimeOfFileName;
7694 * EXIF image data parser. Currently only parses the Orientation tag value,
7695 * but this may be expanded to other tags in the future.
7697 * @param fileOrBlob Attempt to parse EXIF data in this `Blob`
7700 qq.Exif = function(fileOrBlob, log) {
7703 // Orientation is the only tag parsed here at this time.
7704 var TAG_IDS = [274],
7707 name: "Orientation",
7712 // Convert a little endian (hex string) to big endian (decimal).
7713 function parseLittleEndian(hex) {
7717 while (hex.length > 0) {
7718 result += parseInt(hex.substring(0, 2), 16) * Math.pow(2, pow);
7719 hex = hex.substring(2, hex.length);
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) {
7733 thePromise = new qq.Promise();
7736 qq.readBlobToHex(fileOrBlob, theOffset, 4).then(function(hex) {
7737 var match = /^ffe([0-9])/.exec(hex);
7739 if (match[1] !== "1") {
7740 var segmentLength = parseInt(hex.slice(4, 8), 16);
7741 seekToApp1(theOffset + segmentLength + 2, thePromise);
7744 thePromise.success(theOffset);
7748 thePromise.failure("No EXIF header to be found!");
7755 // Find the byte offset of Application Segment 1 (EXIF) for valid JPEGs only.
7756 function getApp1Offset() {
7757 var promise = new qq.Promise();
7759 qq.readBlobToHex(fileOrBlob, 0, 6).then(function(hex) {
7760 if (hex.indexOf("ffd8") !== 0) {
7761 promise.failure("Not a valid JPEG!");
7764 seekToApp1().then(function(offset) {
7765 promise.success(offset);
7768 promise.failure(error);
7776 // Determine the byte ordering of the EXIF header.
7777 function isLittleEndian(app1Start) {
7778 var promise = new qq.Promise();
7780 qq.readBlobToHex(fileOrBlob, app1Start + 10, 2).then(function(hex) {
7781 promise.success(hex === "4949");
7787 // Determine the number of directory entries in the EXIF header.
7788 function getDirEntryCount(app1Start, littleEndian) {
7789 var promise = new qq.Promise();
7791 qq.readBlobToHex(fileOrBlob, app1Start + 18, 2).then(function(hex) {
7793 return promise.success(parseLittleEndian(hex));
7796 promise.success(parseInt(hex, 16));
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;
7808 return qq.readBlobToHex(fileOrBlob, offset, bytes);
7811 // Obtain an array of all directory entries (as hex strings) in the EXIF header.
7812 function getDirEntries(ifdHex) {
7816 while (offset+24 <= ifdHex.length) {
7817 entries.push(ifdHex.slice(offset, offset + 24));
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),
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;
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);
7842 tagsToFind.splice(tagsToFindIdx, 1);
7845 if (tagsToFind.length === 0) {
7855 * Attempt to parse the EXIF header for the `Blob` associated with this instance.
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.
7861 var parser = new qq.Promise(),
7862 onParseFailure = function(message) {
7863 log(qq.format("EXIF header parse failed: '{}' ", message));
7864 parser.failure(message);
7867 getApp1Offset().then(function(app1Offset) {
7868 log(qq.format("Moving forward with EXIF header parsing for '{}'", fileOrBlob.name === undefined ? "blob" : fileOrBlob.name));
7870 isLittleEndian(app1Offset).then(function(littleEndian) {
7872 log(qq.format("EXIF Byte order is {} endian", littleEndian ? "little" : "big"));
7874 getDirEntryCount(app1Offset, littleEndian).then(function(dirEntryCount) {
7876 log(qq.format("Found {} APP1 directory entries", dirEntryCount));
7878 getIfd(app1Offset, dirEntryCount).then(function(ifdHex) {
7879 var dirEntries = getDirEntries(ifdHex),
7880 tagValues = getTagValues(littleEndian, dirEntries);
7882 log("Successfully parsed some EXIF tags");
7884 parser.success(tagValues);
7896 this._testing.parseLittleEndian = parseLittleEndian;
7901 qq.Identify = function(fileOrBlob, log) {
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"]
7912 function isIdentifiable(magicBytes, questionableBytes) {
7913 var identifiable = false,
7914 magicBytesEntries = [].concat(magicBytes);
7916 qq.each(magicBytesEntries, function(idx, magicBytesArrayEntry) {
7917 if (questionableBytes.indexOf(magicBytesArrayEntry) === 0) {
7918 identifiable = true;
7923 return identifiable;
7927 isPreviewable: function() {
7928 var idenitifer = new qq.Promise(),
7929 previewable = false,
7930 name = fileOrBlob.name === undefined ? "blob" : fileOrBlob.name;
7932 log(qq.format("Attempting to determine if {} can be rendered in this browser", name));
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()) {
7941 idenitifer.success(mime);
7948 log(qq.format("'{}' is {} able to be rendered in this browser", name, previewable ? "" : "NOT"));
7951 idenitifer.failure();
7962 * Attempts to validate an image, wherever possible.
7964 * @param blob File or Blob representing a user-selecting image.
7965 * @param log Uses this to post log messages to the console.
7968 qq.ImageValidation = function(blob, log) {
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
7975 function hasNonZeroLimits(limits) {
7976 var atLeastOne = false;
7978 qq.each(limits, function(limit, value) {
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.
7993 function getWidthHeight() {
7994 var sizeDetermination = new qq.Promise();
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 :
8003 image.onerror = function() {
8004 log("Cannot determine dimensions for image. May be too large.", "error");
8005 sizeDetermination.failure();
8008 image.onload = function() {
8009 sizeDetermination.success({
8015 image.src = url.createObjectURL(blob);
8018 log("No createObjectURL function available to generate image URL!", "error");
8019 sizeDetermination.failure();
8021 }, sizeDetermination.failure);
8023 return sizeDetermination;
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.
8032 function getFailingLimit(limits, dimensions) {
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];
8042 switch(limitMatcher[1]) {
8044 if (actualValue < limitValue) {
8045 failingLimit = limitName;
8050 if (actualValue > limitValue) {
8051 failingLimit = limitName;
8059 return failingLimit;
8063 * Validate the associated blob.
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.
8070 this.validate = function(limits) {
8071 var validationEffort = new qq.Promise();
8073 log("Attempting to validate image.");
8075 if (hasNonZeroLimits(limits)) {
8076 getWidthHeight().then(function(dimensions) {
8077 var failingLimit = getFailingLimit(limits, dimensions);
8080 validationEffort.failure(failingLimit);
8083 validationEffort.success();
8085 }, validationEffort.success);
8088 validationEffort.success();
8091 return validationEffort;
8097 * Module used to control populating the initial list of files.
8101 qq.Session = function(spec) {
8109 addFileRecord: function(sessionData) {},
8110 log: function(message, level) {}
8113 qq.extend(options, spec, true);
8116 function isJsonResponseValid(response) {
8117 if (qq.isArray(response)) {
8121 options.log("Session response is not an array.", "error");
8124 function handleFileItems(fileItems, success, xhrOrXdr, promise) {
8125 var someItemsIgnored = false;
8127 success = success && isJsonResponseValid(fileItems);
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");
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");
8142 options.addFileRecord(fileItem);
8146 someItemsIgnored = true;
8147 options.log(err.message, "error");
8155 promise[success && !someItemsIgnored ? "success" : "failure"](fileItems, xhrOrXdr);
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);
8166 requsterOptions = qq.extend({}, options),
8167 requester = new qq.SessionAjaxRequester(
8168 qq.extend(requsterOptions, {onComplete: refreshCompleteCallback})
8171 requester.queryServer();
8173 return refreshEffort;
8177 /*globals qq, XMLHttpRequest*/
8179 * Thin module used to send GET requests to the server, expecting information about session
8180 * data used to initialize an uploader instance.
8182 * @param spec Various options used to influence the associated request.
8185 qq.SessionAjaxRequester = function(spec) {
8195 sendCredentials: false
8197 onComplete: function(response, success, xhrOrXdr) {},
8198 log: function(str, level) {}
8201 qq.extend(options, spec);
8203 function onComplete(id, xhrOrXdr, isError) {
8204 var response = null;
8206 /* jshint eqnull:true */
8207 if (xhrOrXdr.responseText != null) {
8209 response = qq.parseJson(xhrOrXdr.responseText);
8212 options.log("Problem parsing session response: " + err.message, "error");
8217 options.onComplete(response, !isError, xhrOrXdr);
8220 requester = new qq.AjaxRequester({
8221 validMethods: ["GET"],
8224 getEndpoint: function() {
8225 return options.endpoint;
8228 customHeaders: options.customHeaders,
8230 onComplete: onComplete,
8236 queryServer: function() {
8237 var params = qq.extend({}, options.params);
8239 // cache buster, particularly for IE & iOS
8240 params.qqtimestamp = new Date().getTime();
8242 options.log("Session query request.");
8244 requester.initTransport("sessionRefresh")
8252 // Base handler for UI (FineUploader mode) events.
8253 // Some more specific handlers inherit from this one.
8254 qq.UiEventHandler = function(s, protectedApi) {
8257 var disposer = new qq.DisposeSupport(),
8261 onHandled: function(target, event) {}
8265 // This makes up the "public" API methods that will be accessible
8266 // to instances constructing a base or child handler
8268 addHandler: function(element) {
8269 addHandler(element);
8272 dispose: function() {
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;
8282 // On older browsers, we must check the `srcElement` instead of the `target`.
8283 var target = event.target || event.srcElement;
8285 spec.onHandled(target, event);
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;
8295 getDisposeSupport: function() {
8303 if (spec.attachTo) {
8304 addHandler(spec.attachTo);
8309 qq.FileButtonsClickHandler = function(s) {
8312 var inheritedInternalApi = {},
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) {}
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); }
8331 function examineEvent(target, event) {
8332 qq.each(buttonHandlers, function(buttonType, handler) {
8333 var firstLetterCapButtonType = buttonType.charAt(0).toUpperCase() + buttonType.slice(1),
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));
8348 spec.eventType = "click";
8349 spec.onHandled = examineEvent;
8350 spec.attachTo = spec.templating.getFileList();
8352 qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
8356 // Child of FilenameEditHandler. Used to detect click events on filename display elements.
8357 qq.FilenameClickHandler = function(s) {
8360 var inheritedInternalApi = {},
8363 log: function(message, lvl) {},
8365 file: "qq-upload-file",
8366 editNameIcon: "qq-edit-filename-icon"
8368 onGetUploadStatus: function(fileId) {},
8369 onGetName: function(fileId) {}
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);
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);
8385 inheritedInternalApi.handleFilenameEdit(fileId, target, true);
8390 spec.eventType = "click";
8391 spec.onHandled = examineEvent;
8393 qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
8397 // Child of FilenameEditHandler. Used to detect focusin events on file edit input elements.
8398 qq.FilenameInputFocusInHandler = function(s, inheritedInternalApi) {
8403 onGetUploadStatus: function(fileId) {},
8404 log: function(message, lvl) {}
8407 if (!inheritedInternalApi) {
8408 inheritedInternalApi = {};
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);
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);
8424 spec.eventType = "focusin";
8425 spec.onHandled = handleInputFocus;
8428 qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
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.
8436 * @param spec Overrides for default specifications
8438 qq.FilenameInputFocusHandler = function(spec) {
8441 spec.eventType = "focus";
8442 spec.attachTo = null;
8444 qq.extend(this, new qq.FilenameInputFocusInHandler(spec, {}));
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) {
8455 log: function(message, lvl) {},
8456 onGetUploadStatus: function(fileId) {},
8457 onGetName: function(fileId) {},
8458 onSetName: function(fileId, newName) {},
8459 onEditingStatusChange: function(fileId, isEditing) {}
8463 function getFilenameSansExtension(fileId) {
8464 var filenameSansExt = spec.onGetName(fileId),
8465 extIdx = filenameSansExt.lastIndexOf(".");
8468 filenameSansExt = filenameSansExt.substr(0, extIdx);
8471 return filenameSansExt;
8474 function getOriginalExtension(fileId) {
8475 var origName = spec.onGetName(fileId);
8476 return qq.getExtension(origName);
8479 // Callback iff the name has been changed
8480 function handleNameUpdate(newFilenameInputEl, fileId) {
8481 var newName = newFilenameInputEl.value,
8484 if (newName !== undefined && qq.trimStr(newName).length > 0) {
8485 origExtension = getOriginalExtension(fileId);
8487 if (origExtension !== undefined) {
8488 newName = newName + "." + origExtension;
8491 spec.onSetName(fileId, newName);
8494 spec.onEditingStatusChange(fileId, false);
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);
8504 // The name has been updated if the user presses enter.
8505 function registerInputEnterKeyHandler(inputEl, fileId) {
8506 inheritedInternalApi.getDisposeSupport().attach(inputEl, "keyup", function(event) {
8508 var code = event.keyCode || event.which;
8511 handleNameUpdate(inputEl, fileId);
8518 spec.attachTo = spec.templating.getFileList();
8520 qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
8522 qq.extend(inheritedInternalApi, {
8523 handleFilenameEdit: function(id, target, focusInput) {
8524 var newFilenameInputEl = spec.templating.getEditInput(id);
8526 spec.onEditingStatusChange(id, true);
8528 newFilenameInputEl.value = getFilenameSansExtension(id);
8531 newFilenameInputEl.focus();
8534 registerInputBlurHandler(newFilenameInputEl, id);
8535 registerInputEnterKeyHandler(newFilenameInputEl, id);