diff --git a/services/web/public/js/libs/fineuploader.js b/services/web/public/js/libs/fineuploader.js new file mode 100644 index 0000000000..b109b8a1c1 --- /dev/null +++ b/services/web/public/js/libs/fineuploader.js @@ -0,0 +1,3761 @@ +/** + * http://github.com/Valums-File-Uploader/file-uploader + * + * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers. + * + * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com ) + * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com ) + * + * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt. + */ +/*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/ +var qq = function(element) { + "use strict"; + + return { + hide: function() { + element.style.display = 'none'; + return this; + }, + + /** Returns the function which detaches attached event */ + attach: function(type, fn) { + if (element.addEventListener){ + element.addEventListener(type, fn, false); + } else if (element.attachEvent){ + element.attachEvent('on' + type, fn); + } + return function() { + qq(element).detach(type, fn); + }; + }, + + detach: function(type, fn) { + if (element.removeEventListener){ + element.removeEventListener(type, fn, false); + } else if (element.attachEvent){ + element.detachEvent('on' + type, fn); + } + return this; + }, + + contains: function(descendant) { + // compareposition returns false in this case + if (element === descendant) { + return true; + } + + if (element.contains){ + return element.contains(descendant); + } else { + /*jslint bitwise: true*/ + return !!(descendant.compareDocumentPosition(element) & 8); + } + }, + + /** + * Insert this element before elementB. + */ + insertBefore: function(elementB) { + elementB.parentNode.insertBefore(element, elementB); + return this; + }, + + remove: function() { + element.parentNode.removeChild(element); + return this; + }, + + /** + * Sets styles for an element. + * Fixes opacity in IE6-8. + */ + css: function(styles) { + if (styles.opacity !== null){ + if (typeof element.style.opacity !== 'string' && typeof(element.filters) !== 'undefined'){ + styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')'; + } + } + qq.extend(element.style, styles); + + return this; + }, + + hasClass: function(name) { + var re = new RegExp('(^| )' + name + '( |$)'); + return re.test(element.className); + }, + + addClass: function(name) { + if (!qq(element).hasClass(name)){ + element.className += ' ' + name; + } + return this; + }, + + removeClass: function(name) { + var re = new RegExp('(^| )' + name + '( |$)'); + element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, ""); + return this; + }, + + getByClass: function(className) { + var candidates, + result = []; + + if (element.querySelectorAll){ + return element.querySelectorAll('.' + className); + } + + candidates = element.getElementsByTagName("*"); + + qq.each(candidates, function(idx, val) { + if (qq(val).hasClass(className)){ + result.push(val); + } + }); + return result; + }, + + children: function() { + var children = [], + child = element.firstChild; + + while (child){ + if (child.nodeType === 1){ + children.push(child); + } + child = child.nextSibling; + } + + return children; + }, + + setText: function(text) { + element.innerText = text; + element.textContent = text; + return this; + }, + + clearText: function() { + return qq(element).setText(""); + } + }; +}; + +qq.log = function(message, level) { + "use strict"; + + if (window.console) { + if (!level || level === 'info') { + window.console.log(message); + } + else + { + if (window.console[level]) { + window.console[level](message); + } + else { + window.console.log('<' + level + '> ' + message); + } + } + } +}; + +qq.isObject = function(variable) { + "use strict"; + return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object; +}; + +qq.isFunction = function(variable) { + "use strict"; + return typeof(variable) === "function"; +}; + +qq.trimStr = function(string) { + if (String.prototype.trim) { + return string.trim(); + } + + return string.replace(/^\s+|\s+$/g,''); +}; + +qq.isFileOrInput = function(maybeFileOrInput) { + "use strict"; + if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) { + return true; + } + else if (window.HTMLInputElement) { + if (maybeFileOrInput instanceof HTMLInputElement) { + if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') { + return true; + } + } + } + else if (maybeFileOrInput.tagName) { + if (maybeFileOrInput.tagName.toLowerCase() === 'input') { + if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') { + return true; + } + } + } + + return false; +}; + +qq.isBlob = function(maybeBlob) { + "use strict"; + return window.Blob && maybeBlob instanceof Blob; +}; + +qq.isXhrUploadSupported = function() { + "use strict"; + var input = document.createElement('input'); + input.type = 'file'; + + return ( + input.multiple !== undefined && + typeof File !== "undefined" && + typeof FormData !== "undefined" && + typeof (new XMLHttpRequest()).upload !== "undefined" ); +}; + +qq.isFolderDropSupported = function(dataTransfer) { + "use strict"; + return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry); +}; + +qq.isFileChunkingSupported = function() { + "use strict"; + return !qq.android() && //android's impl of Blob.slice is broken + qq.isXhrUploadSupported() && + (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice); +}; + +qq.extend = function (first, second, extendNested) { + "use strict"; + qq.each(second, function(prop, val) { + if (extendNested && qq.isObject(val)) { + if (first[prop] === undefined) { + first[prop] = {}; + } + qq.extend(first[prop], val, true); + } + else { + first[prop] = val; + } + }); +}; + +/** + * Searches for a given element in the array, returns -1 if it is not present. + * @param {Number} [from] The index at which to begin the search + */ +qq.indexOf = function(arr, elt, from){ + "use strict"; + + if (arr.indexOf) { + return arr.indexOf(elt, from); + } + + from = from || 0; + var len = arr.length; + + if (from < 0) { + from += len; + } + + for (; from < len; from+=1){ + if (arr.hasOwnProperty(from) && arr[from] === elt){ + return from; + } + } + return -1; +}; + +//this is a version 4 UUID +qq.getUniqueId = function(){ + "use strict"; + + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + /*jslint eqeq: true, bitwise: true*/ + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); +}; + +// +// Browsers and platforms detection + +qq.ie = function(){ + "use strict"; + return navigator.userAgent.indexOf('MSIE') !== -1; +}; +qq.ie10 = function(){ + "use strict"; + return navigator.userAgent.indexOf('MSIE 10') !== -1; +}; +qq.safari = function(){ + "use strict"; + return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; +}; +qq.chrome = function(){ + "use strict"; + return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1; +}; +qq.firefox = function(){ + "use strict"; + return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === ''); +}; +qq.windows = function(){ + "use strict"; + return navigator.platform === "Win32"; +}; +qq.android = function(){ + "use strict"; + return navigator.userAgent.toLowerCase().indexOf('android') !== -1; +}; + +// +// Events + +qq.preventDefault = function(e){ + "use strict"; + if (e.preventDefault){ + e.preventDefault(); + } else{ + e.returnValue = false; + } +}; + +/** + * Creates and returns element from html string + * Uses innerHTML to create an element + */ +qq.toElement = (function(){ + "use strict"; + var div = document.createElement('div'); + return function(html){ + div.innerHTML = html; + var element = div.firstChild; + div.removeChild(element); + return element; + }; +}()); + +//key and value are passed to callback for each item in the object or array +qq.each = function(obj, callback) { + "use strict"; + var key, retVal; + if (obj) { + for (key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + retVal = callback(key, obj[key]); + if (retVal === false) { + break; + } + } + } + } +}; + +/** + * obj2url() takes a json-object as argument and generates + * a querystring. pretty much like jQuery.param() + * + * how to use: + * + * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` + * + * will result in: + * + * `http://any.url/upload?otherParam=value&a=b&c=d` + * + * @param Object JSON-Object + * @param String current querystring-part + * @return String encoded querystring + */ +qq.obj2url = function(obj, temp, prefixDone){ + "use strict"; + /*jshint laxbreak: true*/ + var i, len, + uristrings = [], + prefix = '&', + add = function(nextObj, i){ + var nextTemp = temp + ? (/\[\]$/.test(temp)) // prevent double-encoding + ? temp + : temp+'['+i+']' + : i; + if ((nextTemp !== 'undefined') && (i !== 'undefined')) { + uristrings.push( + (typeof nextObj === 'object') + ? qq.obj2url(nextObj, nextTemp, true) + : (Object.prototype.toString.call(nextObj) === '[object Function]') + ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj()) + : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj) + ); + } + }; + + if (!prefixDone && temp) { + prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?'; + uristrings.push(temp); + uristrings.push(qq.obj2url(obj)); + } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) { + // we wont use a for-in-loop on an array (performance) + for (i = -1, len = obj.length; i < len; i+=1){ + add(obj[i], i); + } + } else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){ + // for anything else but a scalar, we will use for-in-loop + for (i in obj){ + if (obj.hasOwnProperty(i)) { + add(obj[i], i); + } + } + } else { + uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj)); + } + + if (temp) { + return uristrings.join(prefix); + } else { + return uristrings.join(prefix) + .replace(/^&/, '') + .replace(/%20/g, '+'); + } +}; + +qq.obj2FormData = function(obj, formData, arrayKeyName) { + "use strict"; + if (!formData) { + formData = new FormData(); + } + + qq.each(obj, function(key, val) { + key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key; + + if (qq.isObject(val)) { + qq.obj2FormData(val, formData, key); + } + else if (qq.isFunction(val)) { + formData.append(key, val()); + } + else { + formData.append(key, val); + } + }); + + return formData; +}; + +qq.obj2Inputs = function(obj, form) { + "use strict"; + var input; + + if (!form) { + form = document.createElement('form'); + } + + qq.obj2FormData(obj, { + append: function(key, val) { + input = document.createElement('input'); + input.setAttribute('name', key); + input.setAttribute('value', val); + form.appendChild(input); + } + }); + + return form; +}; + +qq.setCookie = function(name, value, days) { + var date = new Date(), + expires = ""; + + if (days) { + date.setTime(date.getTime()+(days*24*60*60*1000)); + expires = "; expires="+date.toGMTString(); + } + + document.cookie = name+"="+value+expires+"; path=/"; +}; + +qq.getCookie = function(name) { + var nameEQ = name + "=", + ca = document.cookie.split(';'), + c; + + for(var i=0;i < ca.length;i++) { + c = ca[i]; + while (c.charAt(0)==' ') { + c = c.substring(1,c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length,c.length); + } + } +}; + +qq.getCookieNames = function(regexp) { + var cookies = document.cookie.split(';'), + cookieNames = []; + + qq.each(cookies, function(idx, cookie) { + cookie = qq.trimStr(cookie); + + var equalsIdx = cookie.indexOf("="); + + if (cookie.match(regexp)) { + cookieNames.push(cookie.substr(0, equalsIdx)); + } + }); + + return cookieNames; +}; + +qq.deleteCookie = function(name) { + qq.setCookie(name, "", -1); +}; + +qq.areCookiesEnabled = function() { + var randNum = Math.random() * 100000, + name = "qqCookieTest:" + randNum; + qq.setCookie(name, 1); + + if (qq.getCookie(name)) { + qq.deleteCookie(name); + return true; + } + return false; +}; + +/** + * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not + * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js. + */ +qq.parseJson = function(json) { + /*jshint evil: true*/ + if (window.JSON && qq.isFunction(JSON.parse)) { + return JSON.parse(json); + } else { + return eval("(" + json + ")"); + } +}; + +/** + * A generic module which supports object disposing in dispose() method. + * */ +qq.DisposeSupport = function() { + "use strict"; + var disposers = []; + + return { + /** Run all registered disposers */ + dispose: function() { + var disposer; + do { + disposer = disposers.shift(); + if (disposer) { + disposer(); + } + } + while (disposer); + }, + + /** Attach event handler and register de-attacher as a disposer */ + attach: function() { + var args = arguments; + /*jslint undef:true*/ + this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); + }, + + /** Add disposer to the collection */ + addDisposer: function(disposeFunction) { + disposers.push(disposeFunction); + } + }; +}; +qq.UploadButton = function(o){ + this._options = { + element: null, + // if set to true adds multiple attribute to file input + multiple: false, + acceptFiles: null, + // name attribute of file input + name: 'file', + onChange: function(input){}, + hoverClass: 'qq-upload-button-hover', + focusClass: 'qq-upload-button-focus' + }; + + qq.extend(this._options, o); + this._disposeSupport = new qq.DisposeSupport(); + + this._element = this._options.element; + + // make button suitable container for input + qq(this._element).css({ + position: 'relative', + overflow: 'hidden', + // Make sure browse button is in the right side + // in Internet Explorer + direction: 'ltr' + }); + + this._input = this._createInput(); +}; + +qq.UploadButton.prototype = { + /* returns file input element */ + getInput: function(){ + return this._input; + }, + /* cleans/recreates the file input */ + reset: function(){ + if (this._input.parentNode){ + qq(this._input).remove(); + } + + qq(this._element).removeClass(this._options.focusClass); + this._input = this._createInput(); + }, + _createInput: function(){ + var input = document.createElement("input"); + + if (this._options.multiple){ + input.setAttribute("multiple", "multiple"); + } + + if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles); + + input.setAttribute("type", "file"); + input.setAttribute("name", this._options.name); + + qq(input).css({ + position: 'absolute', + // in Opera only 'browse' button + // is clickable and it is located at + // the right side of the input + right: 0, + top: 0, + fontFamily: 'Arial', + // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 + fontSize: '118px', + margin: 0, + padding: 0, + cursor: 'pointer', + opacity: 0 + }); + + this._element.appendChild(input); + + var self = this; + this._disposeSupport.attach(input, 'change', function(){ + self._options.onChange(input); + }); + + this._disposeSupport.attach(input, 'mouseover', function(){ + qq(self._element).addClass(self._options.hoverClass); + }); + this._disposeSupport.attach(input, 'mouseout', function(){ + qq(self._element).removeClass(self._options.hoverClass); + }); + this._disposeSupport.attach(input, 'focus', function(){ + qq(self._element).addClass(self._options.focusClass); + }); + this._disposeSupport.attach(input, 'blur', function(){ + qq(self._element).removeClass(self._options.focusClass); + }); + + // IE and Opera, unfortunately have 2 tab stops on file input + // which is unacceptable in our case, disable keyboard access + if (window.attachEvent){ + // it is IE or Opera + input.setAttribute('tabIndex', "-1"); + } + + return input; + } +}; +qq.FineUploaderBasic = function(o){ + var that = this; + this._options = { + debug: false, + button: null, + multiple: true, + maxConnections: 3, + disableCancelForFormUploads: false, + autoUpload: true, + request: { + endpoint: '/server/upload', + params: {}, + paramsInBody: true, + customHeaders: {}, + forceMultipart: true, + inputName: 'qqfile', + uuidName: 'qquuid', + totalFileSizeName: 'qqtotalfilesize' + }, + validation: { + allowedExtensions: [], + sizeLimit: 0, + minSizeLimit: 0, + stopOnFirstInvalidFile: true + }, + callbacks: { + onSubmit: function(id, name){}, + onComplete: function(id, name, responseJSON){}, + onCancel: function(id, name){}, + onUpload: function(id, name){}, + onUploadChunk: function(id, name, chunkData){}, + onResume: function(id, fileName, chunkData){}, + onProgress: function(id, name, loaded, total){}, + onError: function(id, name, reason) {}, + onAutoRetry: function(id, name, attemptNumber) {}, + onManualRetry: function(id, name) {}, + onValidateBatch: function(fileOrBlobData) {}, + onValidate: function(fileOrBlobData) {}, + onSubmitDelete: function(id) {}, + onDelete: function(id){}, + onDeleteComplete: function(id, xhr, isError){} + }, + messages: { + typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", + sizeError: "{file} is too large, maximum file size is {sizeLimit}.", + minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", + emptyError: "{file} is empty, please select files again without it.", + noFilesError: "No files to upload.", + onLeave: "The files are being uploaded, if you leave now the upload will be cancelled." + }, + retry: { + enableAuto: false, + maxAutoAttempts: 3, + autoAttemptDelay: 5, + preventRetryResponseProperty: 'preventRetry' + }, + classes: { + buttonHover: 'qq-upload-button-hover', + buttonFocus: 'qq-upload-button-focus' + }, + chunking: { + enabled: false, + partSize: 2000000, + paramNames: { + partIndex: 'qqpartindex', + partByteOffset: 'qqpartbyteoffset', + chunkSize: 'qqchunksize', + totalFileSize: 'qqtotalfilesize', + totalParts: 'qqtotalparts', + filename: 'qqfilename' + } + }, + resume: { + enabled: false, + id: null, + cookiesExpireIn: 7, //days + paramNames: { + resuming: "qqresume" + } + }, + formatFileName: function(fileOrBlobName) { + if (fileOrBlobName.length > 33) { + fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14); + } + return fileOrBlobName; + }, + text: { + sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'] + }, + deleteFile : { + enabled: false, + endpoint: '/server/upload', + customHeaders: {}, + params: {} + }, + cors: { + expected: false, + sendCredentials: false + }, + blobs: { + defaultName: 'Misc data', + paramNames: { + name: 'qqblobname' + } + } + }; + + qq.extend(this._options, o, true); + this._wrapCallbacks(); + this._disposeSupport = new qq.DisposeSupport(); + + // number of files being uploaded + this._filesInProgress = []; + + this._storedIds = []; + + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + + this._paramsStore = this._createParamsStore("request"); + this._deleteFileParamsStore = this._createParamsStore("deleteFile"); + + this._endpointStore = this._createEndpointStore("request"); + this._deleteFileEndpointStore = this._createEndpointStore("deleteFile"); + + this._handler = this._createUploadHandler(); + this._deleteHandler = this._createDeleteHandler(); + + if (this._options.button){ + this._button = this._createUploadButton(this._options.button); + } + + this._preventLeaveInProgress(); +}; + +qq.FineUploaderBasic.prototype = { + log: function(str, level) { + if (this._options.debug && (!level || level === 'info')) { + qq.log('[FineUploader] ' + str); + } + else if (level && level !== 'info') { + qq.log('[FineUploader] ' + str, level); + + } + }, + setParams: function(params, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.request.params = params; + } + else { + this._paramsStore.setParams(params, id); + } + }, + setDeleteFileParams: function(params, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.deleteFile.params = params; + } + else { + this._deleteFileParamsStore.setParams(params, id); + } + }, + setEndpoint: function(endpoint, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.request.endpoint = endpoint; + } + else { + this._endpointStore.setEndpoint(endpoint, id); + } + }, + getInProgress: function(){ + return this._filesInProgress.length; + }, + uploadStoredFiles: function(){ + "use strict"; + var idToUpload; + + while(this._storedIds.length) { + idToUpload = this._storedIds.shift(); + this._filesInProgress.push(idToUpload); + this._handler.upload(idToUpload); + } + }, + clearStoredFiles: function(){ + this._storedIds = []; + }, + retry: function(id) { + if (this._onBeforeManualRetry(id)) { + this._handler.retry(id); + return true; + } + else { + return false; + } + }, + cancel: function(id) { + this._handler.cancel(id); + }, + cancelAll: function() { + var storedIdsCopy = [], + self = this; + + qq.extend(storedIdsCopy, this._storedIds); + qq.each(storedIdsCopy, function(idx, storedFileId) { + self.cancel(storedFileId); + }); + + this._handler.cancelAll(); + }, + reset: function() { + this.log("Resetting uploader..."); + this._handler.reset(); + this._filesInProgress = []; + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._button.reset(); + this._paramsStore.reset(); + this._endpointStore.reset(); + }, + addFiles: function(filesBlobDataOrInputs) { + var self = this, + verifiedFilesOrInputs = [], + index, fileOrInput; + + if (filesBlobDataOrInputs) { + if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) { + filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs); + } + + for (index = 0; index < filesBlobDataOrInputs.length; index+=1) { + fileOrInput = filesBlobDataOrInputs[index]; + + if (qq.isFileOrInput(fileOrInput)) { + verifiedFilesOrInputs.push(fileOrInput); + } + else { + self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn'); + } + } + + this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...'); + this._uploadFileOrBlobDataList(verifiedFilesOrInputs); + } + }, + addBlobs: function(blobDataOrArray) { + if (blobDataOrArray) { + var blobDataArray = [].concat(blobDataOrArray), + verifiedBlobDataList = [], + self = this; + + qq.each(blobDataArray, function(idx, blobData) { + if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) { + verifiedBlobDataList.push({ + blob: blobData, + name: self._options.blobs.defaultName + }); + } + else if (qq.isObject(blobData) && blobData.blob && blobData.name) { + verifiedBlobDataList.push(blobData); + } + else { + self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error"); + } + }); + + this._uploadFileOrBlobDataList(verifiedBlobDataList); + } + else { + this.log("undefined or non-array parameter passed into addBlobs", "error"); + } + }, + getUuid: function(id) { + return this._handler.getUuid(id); + }, + getResumableFilesData: function() { + return this._handler.getResumableFilesData(); + }, + getSize: function(id) { + return this._handler.getSize(id); + }, + getFile: function(fileOrBlobId) { + return this._handler.getFile(fileOrBlobId); + }, + deleteFile: function(id) { + this._onSubmitDelete(id); + }, + setDeleteFileEndpoint: function(endpoint, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.deleteFile.endpoint = endpoint; + } + else { + this._deleteFileEndpointStore.setEndpoint(endpoint, id); + } + }, + _createUploadButton: function(element){ + var self = this; + + var button = new qq.UploadButton({ + element: element, + multiple: this._options.multiple && qq.isXhrUploadSupported(), + acceptFiles: this._options.validation.acceptFiles, + onChange: function(input){ + self._onInputChange(input); + }, + hoverClass: this._options.classes.buttonHover, + focusClass: this._options.classes.buttonFocus + }); + + this._disposeSupport.addDisposer(function() { button.dispose(); }); + return button; + }, + _createUploadHandler: function(){ + var self = this; + + return new qq.UploadHandler({ + debug: this._options.debug, + forceMultipart: this._options.request.forceMultipart, + maxConnections: this._options.maxConnections, + customHeaders: this._options.request.customHeaders, + inputName: this._options.request.inputName, + uuidParamName: this._options.request.uuidName, + totalFileSizeParamName: this._options.request.totalFileSizeName, + cors: this._options.cors, + demoMode: this._options.demoMode, + paramsInBody: this._options.request.paramsInBody, + paramsStore: this._paramsStore, + endpointStore: this._endpointStore, + chunking: this._options.chunking, + resume: this._options.resume, + blobs: this._options.blobs, + log: function(str, level) { + self.log(str, level); + }, + onProgress: function(id, name, loaded, total){ + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + }, + onComplete: function(id, name, result, xhr){ + self._onComplete(id, name, result, xhr); + self._options.callbacks.onComplete(id, name, result); + }, + onCancel: function(id, name){ + self._onCancel(id, name); + self._options.callbacks.onCancel(id, name); + }, + onUpload: function(id, name){ + self._onUpload(id, name); + self._options.callbacks.onUpload(id, name); + }, + onUploadChunk: function(id, name, chunkData){ + self._options.callbacks.onUploadChunk(id, name, chunkData); + }, + onResume: function(id, name, chunkData) { + return self._options.callbacks.onResume(id, name, chunkData); + }, + onAutoRetry: function(id, name, responseJSON, xhr) { + self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; + + if (self._shouldAutoRetry(id, name, responseJSON)) { + self._maybeParseAndSendUploadError(id, name, responseJSON, xhr); + self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1); + self._onBeforeAutoRetry(id, name); + + self._retryTimeouts[id] = setTimeout(function() { + self._onAutoRetry(id, name, responseJSON) + }, self._options.retry.autoAttemptDelay * 1000); + + return true; + } + else { + return false; + } + } + }); + }, + _createDeleteHandler: function() { + var self = this; + + return new qq.DeleteFileAjaxRequestor({ + maxConnections: this._options.maxConnections, + customHeaders: this._options.deleteFile.customHeaders, + paramsStore: this._deleteFileParamsStore, + endpointStore: this._deleteFileEndpointStore, + demoMode: this._options.demoMode, + cors: this._options.cors, + log: function(str, level) { + self.log(str, level); + }, + onDelete: function(id) { + self._onDelete(id); + self._options.callbacks.onDelete(id); + }, + onDeleteComplete: function(id, xhr, isError) { + self._onDeleteComplete(id, xhr, isError); + self._options.callbacks.onDeleteComplete(id, xhr, isError); + } + + }); + }, + _preventLeaveInProgress: function(){ + var self = this; + + this._disposeSupport.attach(window, 'beforeunload', function(e){ + if (!self._filesInProgress.length){return;} + + var e = e || window.event; + // for ie, ff + e.returnValue = self._options.messages.onLeave; + // for webkit + return self._options.messages.onLeave; + }); + }, + _onSubmit: function(id, name){ + if (this._options.autoUpload) { + this._filesInProgress.push(id); + } + }, + _onProgress: function(id, name, loaded, total){ + }, + _onComplete: function(id, name, result, xhr){ + this._removeFromFilesInProgress(id); + this._maybeParseAndSendUploadError(id, name, result, xhr); + }, + _onCancel: function(id, name){ + this._removeFromFilesInProgress(id); + + clearTimeout(this._retryTimeouts[id]); + + var storedItemIndex = qq.indexOf(this._storedIds, id); + if (!this._options.autoUpload && storedItemIndex >= 0) { + this._storedIds.splice(storedItemIndex, 1); + } + }, + _isDeletePossible: function() { + return (this._options.deleteFile.enabled && + (!this._options.cors.expected || + (this._options.cors.expected && (qq.ie10() || !qq.ie())) + ) + ); + }, + _onSubmitDelete: function(id) { + if (this._isDeletePossible()) { + if (this._options.callbacks.onSubmitDelete(id)) { + this._deleteHandler.sendDelete(id, this.getUuid(id)); + } + } + else { + this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + + "due to CORS on a user agent that does not support pre-flighting.", "warn"); + return false; + } + }, + _onDelete: function(fileId) {}, + _onDeleteComplete: function(id, xhr, isError) { + var name = this._handler.getName(id); + + if (isError) { + this.log("Delete request for '" + name + "' has failed.", "error"); + this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status); + } + else { + this.log("Delete request for '" + name + "' has succeeded."); + } + }, + _removeFromFilesInProgress: function(id) { + var index = qq.indexOf(this._filesInProgress, id); + if (index >= 0) { + this._filesInProgress.splice(index, 1); + } + }, + _onUpload: function(id, name){}, + _onInputChange: function(input){ + if (qq.isXhrUploadSupported()){ + this.addFiles(input.files); + } else { + this.addFiles(input); + } + this._button.reset(); + }, + _onBeforeAutoRetry: function(id, name) { + this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); + }, + _onAutoRetry: function(id, name, responseJSON) { + this.log("Retrying " + name + "..."); + this._autoRetries[id]++; + this._handler.retry(id); + }, + _shouldAutoRetry: function(id, name, responseJSON) { + if (!this._preventRetries[id] && this._options.retry.enableAuto) { + if (this._autoRetries[id] === undefined) { + this._autoRetries[id] = 0; + } + + return this._autoRetries[id] < this._options.retry.maxAutoAttempts + } + + return false; + }, + //return false if we should not attempt the requested retry + _onBeforeManualRetry: function(id) { + if (this._preventRetries[id]) { + this.log("Retries are forbidden for id " + id, 'warn'); + return false; + } + else if (this._handler.isValid(id)) { + var fileName = this._handler.getName(id); + + if (this._options.callbacks.onManualRetry(id, fileName) === false) { + return false; + } + + this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); + this._filesInProgress.push(id); + return true; + } + else { + this.log("'" + id + "' is not a valid file ID", 'error'); + return false; + } + }, + _maybeParseAndSendUploadError: function(id, name, response, xhr) { + //assuming no one will actually set the response code to something other than 200 and still set 'success' to true + if (!response.success){ + if (xhr && xhr.status !== 200 && !response.error) { + this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status); + } + else { + var errorReason = response.error ? response.error : "Upload failure reason unknown"; + this._options.callbacks.onError(id, name, errorReason); + } + } + }, + _uploadFileOrBlobDataList: function(fileOrBlobDataList){ + var validationDescriptors, index, batchInvalid; + + validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList); + batchInvalid = this._options.callbacks.onValidateBatch(validationDescriptors) === false; + + if (!batchInvalid) { + if (fileOrBlobDataList.length > 0) { + for (index = 0; index < fileOrBlobDataList.length; index++){ + if (this._validateFileOrBlobData(fileOrBlobDataList[index])){ + this._upload(fileOrBlobDataList[index]); + } else { + if (this._options.validation.stopOnFirstInvalidFile){ + return; + } + } + } + } + else { + this._error('noFilesError', ""); + } + } + }, + _upload: function(blobOrFileContainer){ + var id = this._handler.add(blobOrFileContainer); + var name = this._handler.getName(id); + + if (this._options.callbacks.onSubmit(id, name) !== false){ + this._onSubmit(id, name); + if (this._options.autoUpload) { + this._handler.upload(id); + } + else { + this._storeForLater(id); + } + } + }, + _storeForLater: function(id) { + this._storedIds.push(id); + }, + _validateFileOrBlobData: function(fileOrBlobData){ + var validationDescriptor, name, size; + + validationDescriptor = this._getValidationDescriptor(fileOrBlobData); + name = validationDescriptor.name; + size = validationDescriptor.size; + + if (this._options.callbacks.onValidate(validationDescriptor) === false) { + return false; + } + + if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){ + this._error('typeError', name); + return false; + + } + else if (size === 0){ + this._error('emptyError', name); + return false; + + } + else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){ + this._error('sizeError', name); + return false; + + } + else if (size && size < this._options.validation.minSizeLimit){ + this._error('minSizeError', name); + return false; + } + + return true; + }, + _error: function(code, name){ + var message = this._options.messages[code]; + function r(name, replacement){ message = message.replace(name, replacement); } + + var extensions = this._options.validation.allowedExtensions.join(', ').toLowerCase(); + + r('{file}', this._options.formatFileName(name)); + r('{extensions}', extensions); + r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit)); + r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit)); + + this._options.callbacks.onError(null, name, message); + + return message; + }, + _isAllowedExtension: function(fileName){ + var allowed = this._options.validation.allowedExtensions, + valid = false; + + if (!allowed.length) { + return true; + } + + qq.each(allowed, function(idx, allowedExt) { + /*jshint eqeqeq: true, eqnull: true*/ + var extRegex = new RegExp('\\.' + allowedExt + "$", 'i'); + + if (fileName.match(extRegex) != null) { + valid = true; + return false; + } + }); + + return valid; + }, + _formatSize: function(bytes){ + var i = -1; + do { + bytes = bytes / 1024; + i++; + } while (bytes > 99); + + return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i]; + }, + _wrapCallbacks: function() { + var self, safeCallback; + + self = this; + + safeCallback = function(name, callback, args) { + try { + return callback.apply(self, args); + } + catch (exception) { + self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error'); + } + } + + for (var prop in this._options.callbacks) { + (function() { + var callbackName, callbackFunc; + callbackName = prop; + callbackFunc = self._options.callbacks[callbackName]; + self._options.callbacks[callbackName] = function() { + return safeCallback(callbackName, callbackFunc, arguments); + } + }()); + } + }, + _parseFileOrBlobDataName: function(fileOrBlobData) { + var name; + + if (qq.isFileOrInput(fileOrBlobData)) { + if (fileOrBlobData.value) { + // it is a file input + // get input value and remove path to normalize + name = fileOrBlobData.value.replace(/.*(\/|\\)/, ""); + } else { + // fix missing properties in Safari 4 and firefox 11.0a2 + name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name; + } + } + else { + name = fileOrBlobData.name; + } + + return name; + }, + _parseFileOrBlobDataSize: function(fileOrBlobData) { + var size; + + if (qq.isFileOrInput(fileOrBlobData)) { + if (!fileOrBlobData.value){ + // fix missing properties in Safari 4 and firefox 11.0a2 + size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size; + } + } + else { + size = fileOrBlobData.blob.size; + } + + return size; + }, + _getValidationDescriptor: function(fileOrBlobData) { + var name, size, fileDescriptor; + + fileDescriptor = {}; + name = this._parseFileOrBlobDataName(fileOrBlobData); + size = this._parseFileOrBlobDataSize(fileOrBlobData); + + fileDescriptor.name = name; + if (size) { + fileDescriptor.size = size; + } + + return fileDescriptor; + }, + _getValidationDescriptors: function(files) { + var self = this, + fileDescriptors = []; + + qq.each(files, function(idx, file) { + fileDescriptors.push(self._getValidationDescriptor(file)); + }); + + return fileDescriptors; + }, + _createParamsStore: function(type) { + var paramsStore = {}, + self = this; + + return { + setParams: function(params, id) { + var paramsCopy = {}; + qq.extend(paramsCopy, params); + paramsStore[id] = paramsCopy; + }, + + getParams: function(id) { + /*jshint eqeqeq: true, eqnull: true*/ + var paramsCopy = {}; + + if (id != null && paramsStore[id]) { + qq.extend(paramsCopy, paramsStore[id]); + } + else { + qq.extend(paramsCopy, self._options[type].params); + } + + return paramsCopy; + }, + + remove: function(fileId) { + return delete paramsStore[fileId]; + }, + + reset: function() { + paramsStore = {}; + } + }; + }, + _createEndpointStore: function(type) { + var endpointStore = {}, + self = this; + + return { + setEndpoint: function(endpoint, id) { + endpointStore[id] = endpoint; + }, + + getEndpoint: function(id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id != null && endpointStore[id]) { + return endpointStore[id]; + } + + return self._options[type].endpoint; + }, + + remove: function(fileId) { + return delete endpointStore[fileId]; + }, + + reset: function() { + endpointStore = {}; + } + }; + } +}; +/*globals qq, document*/ +qq.DragAndDrop = function(o) { + "use strict"; + + var options, dz, dirPending, + droppedFiles = [], + droppedEntriesCount = 0, + droppedEntriesParsedCount = 0, + disposeSupport = new qq.DisposeSupport(); + + options = { + dropArea: null, + extraDropzones: [], + hideDropzones: true, + multiple: true, + classes: { + dropActive: null + }, + callbacks: { + dropProcessing: function(isProcessing, files) {}, + error: function(code, filename) {}, + log: function(message, level) {} + } + }; + + qq.extend(options, o); + + function maybeUploadDroppedFiles() { + if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) { + options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal."); + dz.dropDisabled(false); + options.callbacks.dropProcessing(false, droppedFiles); + } + } + function addDroppedFile(file) { + droppedFiles.push(file); + droppedEntriesParsedCount+=1; + maybeUploadDroppedFiles(); + } + + function traverseFileTree(entry) { + var dirReader, i; + + droppedEntriesCount+=1; + + if (entry.isFile) { + entry.file(function(file) { + addDroppedFile(file); + }); + } + else if (entry.isDirectory) { + dirPending = true; + dirReader = entry.createReader(); + dirReader.readEntries(function(entries) { + droppedEntriesParsedCount+=1; + for (i = 0; i < entries.length; i+=1) { + traverseFileTree(entries[i]); + } + + dirPending = false; + + if (!entries.length) { + maybeUploadDroppedFiles(); + } + }); + } + } + + function handleDataTransfer(dataTransfer) { + var i, items, entry; + + options.callbacks.dropProcessing(true); + dz.dropDisabled(true); + + if (dataTransfer.files.length > 1 && !options.multiple) { + options.callbacks.dropProcessing(false); + options.callbacks.error('tooManyFilesError', ""); + dz.dropDisabled(false); + } + else { + droppedFiles = []; + droppedEntriesCount = 0; + droppedEntriesParsedCount = 0; + + if (qq.isFolderDropSupported(dataTransfer)) { + items = dataTransfer.items; + + for (i = 0; i < items.length; i+=1) { + entry = items[i].webkitGetAsEntry(); + if (entry) { + //due to a bug in Chrome's File System API impl - #149735 + if (entry.isFile) { + droppedFiles.push(items[i].getAsFile()); + if (i === items.length-1) { + maybeUploadDroppedFiles(); + } + } + + else { + traverseFileTree(entry); + } + } + } + } + else { + options.callbacks.dropProcessing(false, dataTransfer.files); + dz.dropDisabled(false); + } + } + } + + function setupDropzone(dropArea){ + dz = new qq.UploadDropZone({ + element: dropArea, + onEnter: function(e){ + qq(dropArea).addClass(options.classes.dropActive); + e.stopPropagation(); + }, + onLeaveNotDescendants: function(e){ + qq(dropArea).removeClass(options.classes.dropActive); + }, + onDrop: function(e){ + if (options.hideDropzones) { + qq(dropArea).hide(); + } + qq(dropArea).removeClass(options.classes.dropActive); + + handleDataTransfer(e.dataTransfer); + } + }); + + disposeSupport.addDisposer(function() { + dz.dispose(); + }); + + if (options.hideDropzones) { + qq(dropArea).hide(); + } + } + + function isFileDrag(dragEvent) { + var fileDrag; + + qq.each(dragEvent.dataTransfer.types, function(key, val) { + if (val === 'Files') { + fileDrag = true; + return false; + } + }); + + return fileDrag; + } + + function setupDragDrop(){ + if (options.dropArea) { + options.extraDropzones.push(options.dropArea); + } + + var i, dropzones = options.extraDropzones; + + for (i=0; i < dropzones.length; i+=1){ + setupDropzone(dropzones[i]); + } + + // IE <= 9 does not support the File API used for drag+drop uploads + if (options.dropArea && (!qq.ie() || qq.ie10())) { + disposeSupport.attach(document, 'dragenter', function(e) { + if (!dz.dropDisabled() && isFileDrag(e)) { + if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) { + return; + } + + options.dropArea.style.display = 'block'; + for (i=0; i < dropzones.length; i+=1) { + dropzones[i].style.display = 'block'; + } + } + }); + } + disposeSupport.attach(document, 'dragleave', function(e){ + if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) { + for (i=0; i < dropzones.length; i+=1) { + qq(dropzones[i]).hide(); + } + } + }); + disposeSupport.attach(document, 'drop', function(e){ + if (options.hideDropzones) { + for (i=0; i < dropzones.length; i+=1) { + qq(dropzones[i]).hide(); + } + } + e.preventDefault(); + }); + } + + return { + setup: function() { + setupDragDrop(); + }, + + setupExtraDropzone: function(element) { + options.extraDropzones.push(element); + setupDropzone(element); + }, + + removeExtraDropzone: function(element) { + var i, dzs = options.extraDropzones; + for(i in dzs) { + if (dzs[i] === element) { + return dzs.splice(i, 1); + } + } + }, + + dispose: function() { + disposeSupport.dispose(); + dz.dispose(); + } + }; +}; + + +qq.UploadDropZone = function(o){ + "use strict"; + + var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport(); + + options = { + element: null, + onEnter: function(e){}, + onLeave: function(e){}, + // is not fired when leaving element by hovering descendants + onLeaveNotDescendants: function(e){}, + onDrop: function(e){} + }; + + qq.extend(options, o); + element = options.element; + + function dragover_should_be_canceled(){ + return qq.safari() || (qq.firefox() && qq.windows()); + } + + function disableDropOutside(e){ + // run only once for all instances + if (!dropOutsideDisabled ){ + + // for these cases we need to catch onDrop to reset dropArea + if (dragover_should_be_canceled){ + disposeSupport.attach(document, 'dragover', function(e){ + e.preventDefault(); + }); + } else { + disposeSupport.attach(document, 'dragover', function(e){ + if (e.dataTransfer){ + e.dataTransfer.dropEffect = 'none'; + e.preventDefault(); + } + }); + } + + dropOutsideDisabled = true; + } + } + + function isValidFileDrag(e){ + // e.dataTransfer currently causing IE errors + // IE9 does NOT support file API, so drag-and-drop is not possible + if (qq.ie() && !qq.ie10()) { + return false; + } + + var effectTest, dt = e.dataTransfer, + // do not check dt.types.contains in webkit, because it crashes safari 4 + isSafari = qq.safari(); + + // dt.effectAllowed is none in Safari 5 + // dt.types.contains check is for firefox + effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none'; + return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files'))); + } + + function isOrSetDropDisabled(isDisabled) { + if (isDisabled !== undefined) { + preventDrop = isDisabled; + } + return preventDrop; + } + + function attachEvents(){ + disposeSupport.attach(element, 'dragover', function(e){ + if (!isValidFileDrag(e)) { + return; + } + + var effect = qq.ie() ? null : e.dataTransfer.effectAllowed; + if (effect === 'move' || effect === 'linkMove'){ + e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed) + } else { + e.dataTransfer.dropEffect = 'copy'; // for Chrome + } + + e.stopPropagation(); + e.preventDefault(); + }); + + disposeSupport.attach(element, 'dragenter', function(e){ + if (!isOrSetDropDisabled()) { + if (!isValidFileDrag(e)) { + return; + } + options.onEnter(e); + } + }); + + disposeSupport.attach(element, 'dragleave', function(e){ + if (!isValidFileDrag(e)) { + return; + } + + options.onLeave(e); + + var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); + // do not fire when moving a mouse over a descendant + if (qq(this).contains(relatedTarget)) { + return; + } + + options.onLeaveNotDescendants(e); + }); + + disposeSupport.attach(element, 'drop', function(e){ + if (!isOrSetDropDisabled()) { + if (!isValidFileDrag(e)) { + return; + } + + e.preventDefault(); + options.onDrop(e); + } + }); + } + + disableDropOutside(); + attachEvents(); + + return { + dropDisabled: function(isDisabled) { + return isOrSetDropDisabled(isDisabled); + }, + + dispose: function() { + disposeSupport.dispose(); + } + }; +}; +/** + * Class that creates upload widget with drag-and-drop and file list + * @inherits qq.FineUploaderBasic + */ +qq.FineUploader = function(o){ + // call parent constructor + qq.FineUploaderBasic.apply(this, arguments); + + // additional options + qq.extend(this._options, { + element: null, + listElement: null, + dragAndDrop: { + extraDropzones: [], + hideDropzones: true, + disableDefaultDropzone: false + }, + text: { + uploadButton: 'Upload a file', + cancelButton: 'Cancel', + retryButton: 'Retry', + deleteButton: 'Delete', + failUpload: 'Upload failed', + dragZone: 'Drop files here to upload', + dropProcessing: 'Processing dropped files...', + formatProgress: "{percent}% of {total_size}", + waitingForResponse: "Processing..." + }, + template: '
' + + ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '
{dragZoneText}
' : '') + + (!this._options.button ? '
{uploadButtonText}
' : '') + + '{dropProcessingText}' + + (!this._options.listElement ? '' : '') + + '
', + + // template for one item in file list + fileTemplate: '
  • ' + + '
    ' + + '' + + '' + + '' + + '' + + '{cancelButtonText}' + + '{retryButtonText}' + + '{deleteButtonText}' + + '{statusText}' + + '
  • ', + classes: { + button: 'qq-upload-button', + drop: 'qq-upload-drop-area', + dropActive: 'qq-upload-drop-area-active', + dropDisabled: 'qq-upload-drop-area-disabled', + list: 'qq-upload-list', + progressBar: 'qq-progress-bar', + file: 'qq-upload-file', + spinner: 'qq-upload-spinner', + finished: 'qq-upload-finished', + retrying: 'qq-upload-retrying', + retryable: 'qq-upload-retryable', + size: 'qq-upload-size', + cancel: 'qq-upload-cancel', + deleteButton: 'qq-upload-delete', + retry: 'qq-upload-retry', + statusText: 'qq-upload-status-text', + + success: 'qq-upload-success', + fail: 'qq-upload-fail', + + successIcon: null, + failIcon: null, + + dropProcessing: 'qq-drop-processing', + dropProcessingSpinner: 'qq-drop-processing-spinner' + }, + failedUploadTextDisplay: { + mode: 'default', //default, custom, or none + maxChars: 50, + responseProperty: 'error', + enableTooltip: true + }, + messages: { + tooManyFilesError: "You may only drop one file" + }, + retry: { + showAutoRetryNote: true, + autoRetryNote: "Retrying {retryNum}/{maxAuto}...", + showButton: false + }, + deleteFile: { + forceConfirm: false, + confirmMessage: "Are you sure you want to delete {filename}?", + deletingStatusText: "Deleting...", + deletingFailedText: "Delete failed" + + }, + display: { + fileSizeOnSubmit: false + }, + showMessage: function(message){ + setTimeout(function() { + alert(message); + }, 0); + }, + showConfirm: function(message, okCallback, cancelCallback) { + setTimeout(function() { + var result = confirm(message); + if (result) { + okCallback(); + } + else if (cancelCallback) { + cancelCallback(); + } + }, 0); + } + }, true); + + // overwrite options with user supplied + qq.extend(this._options, o, true); + this._wrapCallbacks(); + + // overwrite the upload button text if any + // same for the Cancel button and Fail message text + this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone); + this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton); + this._options.template = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{deleteButtonText\}/g, this._options.text.deleteButton); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, ""); + + this._element = this._options.element; + this._element.innerHTML = this._options.template; + this._listElement = this._options.listElement || this._find(this._element, 'list'); + + this._classes = this._options.classes; + + if (!this._button) { + this._button = this._createUploadButton(this._find(this._element, 'button')); + } + + this._bindCancelAndRetryEvents(); + + this._dnd = this._setupDragAndDrop(); +}; + +// inherit from Basic Uploader +qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype); + +qq.extend(qq.FineUploader.prototype, { + clearStoredFiles: function() { + qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments); + this._listElement.innerHTML = ""; + }, + addExtraDropzone: function(element){ + this._dnd.setupExtraDropzone(element); + }, + removeExtraDropzone: function(element){ + return this._dnd.removeExtraDropzone(element); + }, + getItemByFileId: function(id){ + var item = this._listElement.firstChild; + + // there can't be txt nodes in dynamically created list + // and we can use nextSibling + while (item){ + if (item.qqFileId == id) return item; + item = item.nextSibling; + } + }, + reset: function() { + qq.FineUploaderBasic.prototype.reset.apply(this, arguments); + this._element.innerHTML = this._options.template; + this._listElement = this._options.listElement || this._find(this._element, 'list'); + if (!this._options.button) { + this._button = this._createUploadButton(this._find(this._element, 'button')); + } + this._bindCancelAndRetryEvents(); + this._dnd.dispose(); + this._dnd = this._setupDragAndDrop(); + }, + _removeFileItem: function(fileId) { + var item = this.getItemByFileId(fileId); + qq(item).remove(); + }, + _setupDragAndDrop: function() { + var self = this, + dropProcessingEl = this._find(this._element, 'dropProcessing'), + dnd, preventSelectFiles, defaultDropAreaEl; + + preventSelectFiles = function(event) { + event.preventDefault(); + }; + + if (!this._options.dragAndDrop.disableDefaultDropzone) { + defaultDropAreaEl = this._find(this._options.element, 'drop'); + } + + dnd = new qq.DragAndDrop({ + dropArea: defaultDropAreaEl, + extraDropzones: this._options.dragAndDrop.extraDropzones, + hideDropzones: this._options.dragAndDrop.hideDropzones, + multiple: this._options.multiple, + classes: { + dropActive: this._options.classes.dropActive + }, + callbacks: { + dropProcessing: function(isProcessing, files) { + var input = self._button.getInput(); + + if (isProcessing) { + qq(dropProcessingEl).css({display: 'block'}); + qq(input).attach('click', preventSelectFiles); + } + else { + qq(dropProcessingEl).hide(); + qq(input).detach('click', preventSelectFiles); + } + + if (files) { + self.addFiles(files); + } + }, + error: function(code, filename) { + self._error(code, filename); + }, + log: function(message, level) { + self.log(message, level); + } + } + }); + + dnd.setup(); + + return dnd; + }, + _leaving_document_out: function(e){ + return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows + || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox + }, + _storeForLater: function(id) { + qq.FineUploaderBasic.prototype._storeForLater.apply(this, arguments); + var item = this.getItemByFileId(id); + qq(this._find(item, 'spinner')).hide(); + }, + /** + * Gets one of the elements listed in this._options.classes + **/ + _find: function(parent, type){ + var element = qq(parent).getByClass(this._options.classes[type])[0]; + if (!element){ + throw new Error('element not found ' + type); + } + + return element; + }, + _onSubmit: function(id, name){ + qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments); + this._addToList(id, name); + }, + // Update the progress bar & percentage as the file is uploaded + _onProgress: function(id, name, loaded, total){ + qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments); + + var item, progressBar, percent, cancelLink; + + item = this.getItemByFileId(id); + progressBar = this._find(item, 'progressBar'); + percent = Math.round(loaded / total * 100); + + if (loaded === total) { + cancelLink = this._find(item, 'cancel'); + qq(cancelLink).hide(); + + qq(progressBar).hide(); + qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse); + + // If last byte was sent, display total file size + this._displayFileSize(id); + } + else { + // If still uploading, display percentage - total size is actually the total request(s) size + this._displayFileSize(id, loaded, total); + + qq(progressBar).css({display: 'block'}); + } + + // Update progress bar element + qq(progressBar).css({width: percent + '%'}); + }, + _onComplete: function(id, name, result, xhr){ + qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments); + + var item = this.getItemByFileId(id); + + qq(this._find(item, 'statusText')).clearText(); + + qq(item).removeClass(this._classes.retrying); + qq(this._find(item, 'progressBar')).hide(); + + if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) { + qq(this._find(item, 'cancel')).hide(); + } + qq(this._find(item, 'spinner')).hide(); + + if (result.success) { + if (this._isDeletePossible()) { + this._showDeleteLink(id); + } + + qq(item).addClass(this._classes.success); + if (this._classes.successIcon) { + this._find(item, 'finished').style.display = "inline-block"; + qq(item).addClass(this._classes.successIcon); + } + } else { + qq(item).addClass(this._classes.fail); + if (this._classes.failIcon) { + this._find(item, 'finished').style.display = "inline-block"; + qq(item).addClass(this._classes.failIcon); + } + if (this._options.retry.showButton && !this._preventRetries[id]) { + qq(item).addClass(this._classes.retryable); + } + this._controlFailureTextDisplay(item, result); + } + }, + _onUpload: function(id, name){ + qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments); + + this._showSpinner(id); + }, + _onCancel: function(id, name) { + qq.FineUploaderBasic.prototype._onCancel.apply(this, arguments); + this._removeFileItem(id); + }, + _onBeforeAutoRetry: function(id) { + var item, progressBar, failTextEl, retryNumForDisplay, maxAuto, retryNote; + + qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments); + + item = this.getItemByFileId(id); + progressBar = this._find(item, 'progressBar'); + + this._showCancelLink(item); + progressBar.style.width = 0; + qq(progressBar).hide(); + + if (this._options.retry.showAutoRetryNote) { + failTextEl = this._find(item, 'statusText'); + retryNumForDisplay = this._autoRetries[id] + 1; + maxAuto = this._options.retry.maxAutoAttempts; + + retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay); + retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto); + + qq(failTextEl).setText(retryNote); + if (retryNumForDisplay === 1) { + qq(item).addClass(this._classes.retrying); + } + } + }, + //return false if we should not attempt the requested retry + _onBeforeManualRetry: function(id) { + if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) { + var item = this.getItemByFileId(id); + this._find(item, 'progressBar').style.width = 0; + qq(item).removeClass(this._classes.fail); + qq(this._find(item, 'statusText')).clearText(); + this._showSpinner(id); + this._showCancelLink(item); + return true; + } + return false; + }, + _onSubmitDelete: function(id) { + if (this._isDeletePossible()) { + if (this._options.callbacks.onSubmitDelete(id) !== false) { + if (this._options.deleteFile.forceConfirm) { + this._showDeleteConfirm(id); + } + else { + this._sendDeleteRequest(id); + } + } + } + else { + this.log("Delete request ignored for file ID " + id + ", delete feature is disabled.", "warn"); + return false; + } + }, + _onDeleteComplete: function(id, xhr, isError) { + qq.FineUploaderBasic.prototype._onDeleteComplete.apply(this, arguments); + + var item = this.getItemByFileId(id), + spinnerEl = this._find(item, 'spinner'), + statusTextEl = this._find(item, 'statusText'); + + qq(spinnerEl).hide(); + + if (isError) { + qq(statusTextEl).setText(this._options.deleteFile.deletingFailedText); + this._showDeleteLink(id); + } + else { + this._removeFileItem(id); + } + }, + _sendDeleteRequest: function(id) { + var item = this.getItemByFileId(id), + deleteLink = this._find(item, 'deleteButton'), + statusTextEl = this._find(item, 'statusText'); + + qq(deleteLink).hide(); + this._showSpinner(id); + qq(statusTextEl).setText(this._options.deleteFile.deletingStatusText); + this._deleteHandler.sendDelete(id, this.getUuid(id)); + }, + _showDeleteConfirm: function(id) { + var fileName = this._handler.getName(id), + confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName), + uuid = this.getUuid(id), + self = this; + + this._options.showConfirm(confirmMessage, function() { + self._sendDeleteRequest(id); + }); + }, + _addToList: function(id, name){ + var item = qq.toElement(this._options.fileTemplate); + if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) { + var cancelLink = this._find(item, 'cancel'); + qq(cancelLink).remove(); + } + + item.qqFileId = id; + + var fileElement = this._find(item, 'file'); + qq(fileElement).setText(this._options.formatFileName(name)); + qq(this._find(item, 'size')).hide(); + if (!this._options.multiple) { + this._handler.cancelAll(); + this._clearList(); + } + + this._listElement.appendChild(item); + + if (this._options.display.fileSizeOnSubmit && qq.isXhrUploadSupported()) { + this._displayFileSize(id); + } + }, + _clearList: function(){ + this._listElement.innerHTML = ''; + this.clearStoredFiles(); + }, + _displayFileSize: function(id, loadedSize, totalSize) { + var item = this.getItemByFileId(id), + size = this.getSize(id), + sizeForDisplay = this._formatSize(size), + sizeEl = this._find(item, 'size'); + + if (loadedSize !== undefined && totalSize !== undefined) { + sizeForDisplay = this._formatProgress(loadedSize, totalSize); + } + + qq(sizeEl).css({display: 'inline'}); + qq(sizeEl).setText(sizeForDisplay); + }, + /** + * delegate click event for cancel & retry links + **/ + _bindCancelAndRetryEvents: function(){ + var self = this, + list = this._listElement; + + this._disposeSupport.attach(list, 'click', function(e){ + e = e || window.event; + var target = e.target || e.srcElement; + + if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry) || qq(target).hasClass(self._classes.deleteButton)){ + qq.preventDefault(e); + + var item = target.parentNode; + while(item.qqFileId === undefined) { + item = target = target.parentNode; + } + + if (qq(target).hasClass(self._classes.deleteButton)) { + self.deleteFile(item.qqFileId); + } + else if (qq(target).hasClass(self._classes.cancel)) { + self.cancel(item.qqFileId); + } + else { + qq(item).removeClass(self._classes.retryable); + self.retry(item.qqFileId); + } + } + }); + }, + _formatProgress: function (uploadedSize, totalSize) { + var message = this._options.text.formatProgress; + function r(name, replacement) { message = message.replace(name, replacement); } + + r('{percent}', Math.round(uploadedSize / totalSize * 100)); + r('{total_size}', this._formatSize(totalSize)); + return message; + }, + _controlFailureTextDisplay: function(item, response) { + var mode, maxChars, responseProperty, failureReason, shortFailureReason; + + mode = this._options.failedUploadTextDisplay.mode; + maxChars = this._options.failedUploadTextDisplay.maxChars; + responseProperty = this._options.failedUploadTextDisplay.responseProperty; + + if (mode === 'custom') { + failureReason = response[responseProperty]; + if (failureReason) { + if (failureReason.length > maxChars) { + shortFailureReason = failureReason.substring(0, maxChars) + '...'; + } + } + else { + failureReason = this._options.text.failUpload; + this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn'); + } + + qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason); + + if (this._options.failedUploadTextDisplay.enableTooltip) { + this._showTooltip(item, failureReason); + } + } + else if (mode === 'default') { + qq(this._find(item, 'statusText')).setText(this._options.text.failUpload); + } + else if (mode !== 'none') { + this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn'); + } + }, + _showTooltip: function(item, text) { + item.title = text; + }, + _showSpinner: function(id) { + var item = this.getItemByFileId(id), + spinnerEl = this._find(item, 'spinner'); + + spinnerEl.style.display = "inline-block"; + }, + _showCancelLink: function(item) { + if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) { + var cancelLink = this._find(item, 'cancel'); + + qq(cancelLink).css({display: 'inline'}); + } + }, + _showDeleteLink: function(id) { + var item = this.getItemByFileId(id), + deleteLink = this._find(item, 'deleteButton'); + + qq(deleteLink).css({display: 'inline'}); + }, + _error: function(code, name){ + var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments); + this._options.showMessage(message); + } +}); +/** Generic class for sending non-upload ajax requests and handling the associated responses **/ +//TODO Use XDomainRequest if expectCors = true. Not necessary now since only DELETE requests are sent and XDR doesn't support pre-flighting. +/*globals qq, XMLHttpRequest*/ +qq.AjaxRequestor = function(o) { + "use strict"; + + var log, shouldParamsBeInQueryString, + queue = [], + requestState = [], + options = { + method: 'POST', + maxConnections: 3, + customHeaders: {}, + endpointStore: {}, + paramsStore: {}, + successfulResponseCodes: [200], + demoMode: false, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onSend: function(id) {}, + onComplete: function(id, xhr, isError) {}, + onCancel: function(id) {} + }; + + qq.extend(options, o); + log = options.log; + shouldParamsBeInQueryString = getMethod() === 'GET' || getMethod() === 'DELETE'; + + + /** + * Removes element from queue, sends next request + */ + function dequeue(id) { + var i = qq.indexOf(queue, id), + max = options.maxConnections, + nextId; + + delete requestState[id]; + queue.splice(i, 1); + + if (queue.length >= max && i < max){ + nextId = queue[max-1]; + sendRequest(nextId); + } + } + + function onComplete(id) { + var xhr = requestState[id].xhr, + method = getMethod(), + isError = false; + + dequeue(id); + + if (!isResponseSuccessful(xhr.status)) { + isError = true; + log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); + } + + options.onComplete(id, xhr, isError); + } + + function sendRequest(id) { + var xhr = new XMLHttpRequest(), + method = getMethod(), + params = {}, + url; + + options.onSend(id); + + if (options.paramsStore.getParams) { + params = options.paramsStore.getParams(id); + } + + url = createUrl(id, params); + + requestState[id].xhr = xhr; + xhr.onreadystatechange = getReadyStateChangeHandler(id); + xhr.open(method, url, true); + + if (options.cors.expected && options.cors.sendCredentials) { + xhr.withCredentials = true; + } + + setHeaders(id); + + log('Sending ' + method + " request for " + id); + if (!shouldParamsBeInQueryString && params) { + xhr.send(qq.obj2url(params, "")); + } + else { + xhr.send(); + } + } + + function createUrl(id, params) { + var endpoint = options.endpointStore.getEndpoint(id), + addToPath = requestState[id].addToPath; + + if (addToPath !== undefined) { + endpoint += "/" + addToPath; + } + + if (shouldParamsBeInQueryString && params) { + return qq.obj2url(params, endpoint); + } + else { + return endpoint; + } + } + + function getReadyStateChangeHandler(id) { + var xhr = requestState[id].xhr; + + return function() { + if (xhr.readyState === 4) { + onComplete(id, xhr); + } + }; + } + + function setHeaders(id) { + var xhr = requestState[id].xhr, + customHeaders = options.customHeaders; + + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + + qq.each(customHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + + function cancelRequest(id) { + var xhr = requestState[id].xhr, + method = getMethod(); + + if (xhr) { + xhr.onreadystatechange = null; + xhr.abort(); + dequeue(id); + + log('Cancelled ' + method + " for " + id); + options.onCancel(id); + + return true; + } + + return false; + } + + function isResponseSuccessful(responseCode) { + return qq.indexOf(options.successfulResponseCodes, responseCode) >= 0; + } + + function getMethod() { + if (options.demoMode) { + return "GET"; + } + + return options.method; + } + + + return { + send: function(id, addToPath) { + requestState[id] = { + addToPath: addToPath + }; + + var len = queue.push(id); + + // if too many active connections, wait... + if (len <= options.maxConnections){ + sendRequest(id); + } + }, + cancel: function(id) { + return cancelRequest(id); + } + }; +}; +/** Generic class for sending non-upload ajax requests and handling the associated responses **/ +/*globals qq, XMLHttpRequest*/ +qq.DeleteFileAjaxRequestor = function(o) { + "use strict"; + + var requestor, + options = { + endpointStore: {}, + maxConnections: 3, + customHeaders: {}, + paramsStore: {}, + demoMode: false, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onDelete: function(id) {}, + onDeleteComplete: function(id, xhr, isError) {} + }; + + qq.extend(options, o); + + requestor = new qq.AjaxRequestor({ + method: 'DELETE', + endpointStore: options.endpointStore, + paramsStore: options.paramsStore, + maxConnections: options.maxConnections, + customHeaders: options.customHeaders, + successfulResponseCodes: [200, 202, 204], + demoMode: options.demoMode, + log: options.log, + onSend: options.onDelete, + onComplete: options.onDeleteComplete + }); + + + return { + sendDelete: function(id, uuid) { + requestor.send(id, uuid); + options.log("Submitted delete file request for " + id); + } + }; +}; +qq.WindowReceiveMessage = function(o) { + var options = { + log: function(message, level) {} + }, + callbackWrapperDetachers = {}; + + qq.extend(options, o); + + return { + receiveMessage : function(id, callback) { + var onMessageCallbackWrapper = function(event) { + callback(event.data); + }; + + if (window.postMessage) { + callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper); + } + else { + log("iframe message passing not supported in this browser!", "error"); + } + }, + + stopReceivingMessages : function(id) { + if (window.postMessage) { + var detacher = callbackWrapperDetachers[id]; + if (detacher) { + detacher(); + } + } + } + }; +}; +/** + * Class for uploading files, uploading itself is handled by child classes + */ +/*globals qq*/ +qq.UploadHandler = function(o) { + "use strict"; + + var queue = [], + options, log, dequeue, handlerImpl; + + // Default options, can be overridden by the user + options = { + debug: false, + forceMultipart: true, + paramsInBody: false, + paramsStore: {}, + endpointStore: {}, + cors: { + expected: false, + sendCredentials: false + }, + maxConnections: 3, // maximum number of concurrent uploads + uuidParamName: 'qquuid', + totalFileSizeParamName: 'qqtotalfilesize', + chunking: { + enabled: false, + partSize: 2000000, //bytes + paramNames: { + partIndex: 'qqpartindex', + partByteOffset: 'qqpartbyteoffset', + chunkSize: 'qqchunksize', + totalParts: 'qqtotalparts', + filename: 'qqfilename' + } + }, + resume: { + enabled: false, + id: null, + cookiesExpireIn: 7, //days + paramNames: { + resuming: "qqresume" + } + }, + blobs: { + paramNames: { + name: 'qqblobname' + } + }, + log: function(str, level) {}, + onProgress: function(id, fileName, loaded, total){}, + onComplete: function(id, fileName, response, xhr){}, + onCancel: function(id, fileName){}, + onUpload: function(id, fileName){}, + onUploadChunk: function(id, fileName, chunkData){}, + onAutoRetry: function(id, fileName, response, xhr){}, + onResume: function(id, fileName, chunkData){} + + }; + qq.extend(options, o); + + log = options.log; + + /** + * Removes element from queue, starts upload of next + */ + dequeue = function(id) { + var i = qq.indexOf(queue, id), + max = options.maxConnections, + nextId; + + if (i >= 0) { + queue.splice(i, 1); + + if (queue.length >= max && i < max){ + nextId = queue[max-1]; + handlerImpl.upload(nextId); + } + } + }; + + if (qq.isXhrUploadSupported()) { + handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log); + } + else { + handlerImpl = new qq.UploadHandlerForm(options, dequeue, log); + } + + + return { + /** + * Adds file or file input to the queue + * @returns id + **/ + add: function(file){ + return handlerImpl.add(file); + }, + /** + * Sends the file identified by id + */ + upload: function(id){ + var len = queue.push(id); + + // if too many active uploads, wait... + if (len <= options.maxConnections){ + return handlerImpl.upload(id); + } + }, + retry: function(id) { + var i = qq.indexOf(queue, id); + if (i >= 0) { + return handlerImpl.upload(id, true); + } + else { + return this.upload(id); + } + }, + /** + * Cancels file upload by id + */ + cancel: function(id) { + log('Cancelling ' + id); + options.paramsStore.remove(id); + handlerImpl.cancel(id); + dequeue(id); + }, + /** + * Cancels all queued or in-progress uploads + */ + cancelAll: function() { + var self = this, + queueCopy = []; + + qq.extend(queueCopy, queue); + qq.each(queueCopy, function(idx, fileId) { + self.cancel(fileId); + }); + + queue = []; + }, + /** + * Returns name of the file identified by id + */ + getName: function(id){ + return handlerImpl.getName(id); + }, + /** + * Returns size of the file identified by id + */ + getSize: function(id){ + if (handlerImpl.getSize) { + return handlerImpl.getSize(id); + } + }, + getFile: function(id) { + if (handlerImpl.getFile) { + return handlerImpl.getFile(id); + } + }, + /** + * Returns id of files being uploaded or + * waiting for their turn + */ + getQueue: function(){ + return queue; + }, + reset: function() { + log('Resetting upload handler'); + queue = []; + handlerImpl.reset(); + }, + getUuid: function(id) { + return handlerImpl.getUuid(id); + }, + /** + * Determine if the file exists. + */ + isValid: function(id) { + return handlerImpl.isValid(id); + }, + getResumableFilesData: function() { + if (handlerImpl.getResumableFilesData) { + return handlerImpl.getResumableFilesData(); + } + return []; + } + }; +}; +/*globals qq, document, setTimeout*/ +/*globals clearTimeout*/ +qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) { + "use strict"; + + var options = o, + inputs = [], + uuids = [], + detachLoadEvents = {}, + postMessageCallbackTimers = {}, + uploadComplete = uploadCompleteCallback, + log = logCallback, + corsMessageReceiver = new qq.WindowReceiveMessage({log: log}), + onloadCallbacks = {}, + api; + + + function detachLoadEvent(id) { + if (detachLoadEvents[id] !== undefined) { + detachLoadEvents[id](); + delete detachLoadEvents[id]; + } + } + + function registerPostMessageCallback(iframe, callback) { + var id = iframe.id; + + onloadCallbacks[uuids[id]] = callback; + + detachLoadEvents[id] = qq(iframe).attach('load', function() { + if (inputs[id]) { + log("Received iframe load event for CORS upload request (file id " + id + ")"); + + postMessageCallbackTimers[id] = setTimeout(function() { + var errorMessage = "No valid message received from loaded iframe for file id " + id; + log(errorMessage, "error"); + callback({ + error: errorMessage + }); + }, 1000); + } + }); + + corsMessageReceiver.receiveMessage(id, function(message) { + log("Received the following window message: '" + message + "'"); + var response = qq.parseJson(message), + uuid = response.uuid, + onloadCallback; + + if (uuid && onloadCallbacks[uuid]) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + + detachLoadEvent(id); + + onloadCallback = onloadCallbacks[uuid]; + + delete onloadCallbacks[uuid]; + corsMessageReceiver.stopReceivingMessages(id); + onloadCallback(response); + } + else if (!uuid) { + log("'" + message + "' does not contain a UUID - ignoring."); + } + }); + } + + function attachLoadEvent(iframe, callback) { + /*jslint eqeq: true*/ + + if (options.cors.expected) { + registerPostMessageCallback(iframe, callback); + } + else { + detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){ + log('Received response for ' + iframe.id); + + // when we remove iframe from dom + // the request stops, but in IE load + // event fires + if (!iframe.parentNode){ + return; + } + + try { + // fixing Opera 10.53 + if (iframe.contentDocument && + iframe.contentDocument.body && + iframe.contentDocument.body.innerHTML == "false"){ + // In Opera event is fired second time + // when body.innerHTML changed from false + // to server response approx. after 1 sec + // when we upload file with iframe + return; + } + } + catch (error) { + //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases + log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error'); + } + + callback(); + }); + } + } + + /** + * Returns json object received by iframe from server. + */ + function getIframeContentJson(iframe) { + /*jshint evil: true*/ + + var response; + + //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases + try { + // iframe.contentWindow.document - for IE<7 + var doc = iframe.contentDocument || iframe.contentWindow.document, + innerHTML = doc.body.innerHTML; + + log("converting iframe's innerHTML to JSON"); + log("innerHTML = " + innerHTML); + //plain text response may be wrapped in
     tag
    +            if (innerHTML && innerHTML.match(/^
    ');
    +
    +        iframe.setAttribute('id', id);
    +
    +        iframe.style.display = 'none';
    +        document.body.appendChild(iframe);
    +
    +        return iframe;
    +    }
    +
    +    /**
    +     * Creates form, that will be submitted to iframe
    +     */
    +    function createForm(id, iframe){
    +        var params = options.paramsStore.getParams(id),
    +            protocol = options.demoMode ? "GET" : "POST",
    +            form = qq.toElement('
    '), + endpoint = options.endpointStore.getEndpoint(id), + url = endpoint; + + params[options.uuidParamName] = uuids[id]; + + if (!options.paramsInBody) { + url = qq.obj2url(params, endpoint); + } + else { + qq.obj2Inputs(params, form); + } + + form.setAttribute('action', url); + form.setAttribute('target', iframe.name); + form.style.display = 'none'; + document.body.appendChild(form); + + return form; + } + + + api = { + add: function(fileInput) { + fileInput.setAttribute('name', options.inputName); + + var id = inputs.push(fileInput) - 1; + uuids[id] = qq.getUniqueId(); + + // remove file input from DOM + if (fileInput.parentNode){ + qq(fileInput).remove(); + } + + return id; + }, + getName: function(id) { + /*jslint regexp: true*/ + + // get input value and remove path to normalize + return inputs[id].value.replace(/.*(\/|\\)/, ""); + }, + isValid: function(id) { + return inputs[id] !== undefined; + }, + reset: function() { + qq.UploadHandler.prototype.reset.apply(this, arguments); + inputs = []; + uuids = []; + detachLoadEvents = {}; + }, + getUuid: function(id) { + return uuids[id]; + }, + cancel: function(id) { + options.onCancel(id, this.getName(id)); + + delete inputs[id]; + delete uuids[id]; + delete detachLoadEvents[id]; + + if (options.cors.expected) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + corsMessageReceiver.stopReceivingMessages(id); + } + + var iframe = document.getElementById(id); + if (iframe) { + // to cancel request set src to something else + // we use src="javascript:false;" because it doesn't + // trigger ie6 prompt on https + iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off + + qq(iframe).remove(); + } + }, + upload: function(id){ + var input = inputs[id], + fileName = api.getName(id), + iframe = createIframe(id), + form; + + if (!input){ + throw new Error('file with passed id was not added, or already uploaded or cancelled'); + } + + options.onUpload(id, this.getName(id)); + + form = createForm(id, iframe); + form.appendChild(input); + + attachLoadEvent(iframe, function(responseFromMessage){ + log('iframe loaded'); + + var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe); + + detachLoadEvent(id); + + //we can't remove an iframe if the iframe doesn't belong to the same domain + if (!options.cors.expected) { + qq(iframe).remove(); + } + + if (!response.success) { + if (options.onAutoRetry(id, fileName, response)) { + return; + } + } + options.onComplete(id, fileName, response); + uploadComplete(id); + }); + + log('Sending upload request for ' + id); + form.submit(); + qq(form).remove(); + + return id; + } + }; + + return api; +}; +/*globals qq, File, XMLHttpRequest, FormData, Blob*/ +qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) { + "use strict"; + + var options = o, + uploadComplete = uploadCompleteCallback, + log = logCallback, + fileState = [], + cookieItemDelimiter = "|", + chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(), + resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(), + resumeId = getResumeId(), + multipart = options.forceMultipart || options.paramsInBody, + api; + + + function addChunkingSpecificParams(id, params, chunkData) { + var size = api.getSize(id), + name = api.getName(id); + + params[options.chunking.paramNames.partIndex] = chunkData.part; + params[options.chunking.paramNames.partByteOffset] = chunkData.start; + params[options.chunking.paramNames.chunkSize] = chunkData.size; + params[options.chunking.paramNames.totalParts] = chunkData.count; + params[options.totalFileSizeParamName] = size; + + /** + * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob" + * or an empty string. So, we will need to include the actual file name as a param in this case. + */ + if (multipart) { + params[options.chunking.paramNames.filename] = name; + } + } + + function addResumeSpecificParams(params) { + params[options.resume.paramNames.resuming] = true; + } + + function getChunk(fileOrBlob, startByte, endByte) { + if (fileOrBlob.slice) { + return fileOrBlob.slice(startByte, endByte); + } + else if (fileOrBlob.mozSlice) { + return fileOrBlob.mozSlice(startByte, endByte); + } + else if (fileOrBlob.webkitSlice) { + return fileOrBlob.webkitSlice(startByte, endByte); + } + } + + function getChunkData(id, chunkIndex) { + var chunkSize = options.chunking.partSize, + fileSize = api.getSize(id), + fileOrBlob = fileState[id].file || fileState[id].blobData.blob, + startBytes = chunkSize * chunkIndex, + endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize, + totalChunks = getTotalChunks(id); + + return { + part: chunkIndex, + start: startBytes, + end: endBytes, + count: totalChunks, + blob: getChunk(fileOrBlob, startBytes, endBytes), + size: endBytes - startBytes + }; + } + + function getTotalChunks(id) { + var fileSize = api.getSize(id), + chunkSize = options.chunking.partSize; + + return Math.ceil(fileSize / chunkSize); + } + + function createXhr(id) { + var xhr = new XMLHttpRequest(); + + fileState[id].xhr = xhr; + + return xhr; + } + + function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) { + var formData = new FormData(), + method = options.demoMode ? "GET" : "POST", + endpoint = options.endpointStore.getEndpoint(id), + url = endpoint, + name = api.getName(id), + size = api.getSize(id), + blobData = fileState[id].blobData; + + params[options.uuidParamName] = fileState[id].uuid; + + if (multipart) { + params[options.totalFileSizeParamName] = size; + + if (blobData) { + /** + * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob" + * or an empty string. So, we will need to include the actual file name as a param in this case. + */ + params[options.blobs.paramNames.name] = blobData.name; + } + } + + //build query string + if (!options.paramsInBody) { + if (!multipart) { + params[options.inputName] = name; + } + url = qq.obj2url(params, endpoint); + } + + xhr.open(method, url, true); + + if (options.cors.expected && options.cors.sendCredentials) { + xhr.withCredentials = true; + } + + if (multipart) { + if (options.paramsInBody) { + qq.obj2FormData(params, formData); + } + + formData.append(options.inputName, fileOrBlob); + return formData; + } + + return fileOrBlob; + } + + function setHeaders(id, xhr) { + var extraHeaders = options.customHeaders, + fileOrBlob = fileState[id].file || fileState[id].blobData.blob; + + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + + if (!multipart) { + xhr.setRequestHeader("Content-Type", "application/octet-stream"); + //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2 + xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type); + } + + qq.each(extraHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + + function handleCompletedItem(id, response, xhr) { + var name = api.getName(id), + size = api.getSize(id); + + fileState[id].attemptingResume = false; + + options.onProgress(id, name, size, size); + + options.onComplete(id, name, response, xhr); + delete fileState[id].xhr; + uploadComplete(id); + } + + function uploadNextChunk(id) { + var chunkIdx = fileState[id].remainingChunkIdxs[0], + chunkData = getChunkData(id, chunkIdx), + xhr = createXhr(id), + size = api.getSize(id), + name = api.getName(id), + toSend, params; + + if (fileState[id].loaded === undefined) { + fileState[id].loaded = 0; + } + + if (resumeEnabled && fileState[id].file) { + persistChunkData(id, chunkData); + } + + xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr); + + xhr.upload.onprogress = function(e) { + if (e.lengthComputable) { + var totalLoaded = e.loaded + fileState[id].loaded, + estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total); + + options.onProgress(id, name, totalLoaded, estTotalRequestsSize); + } + }; + + options.onUploadChunk(id, name, getChunkDataForCallback(chunkData)); + + params = options.paramsStore.getParams(id); + addChunkingSpecificParams(id, params, chunkData); + + if (fileState[id].attemptingResume) { + addResumeSpecificParams(params); + } + + toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id); + setHeaders(id, xhr); + + log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size); + xhr.send(toSend); + } + + function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) { + var chunkData = getChunkData(id, chunkIdx), + blobSize = chunkData.size, + overhead = requestSize - blobSize, + size = api.getSize(id), + chunkCount = chunkData.count, + initialRequestOverhead = fileState[id].initialRequestOverhead, + overheadDiff = overhead - initialRequestOverhead; + + fileState[id].lastRequestOverhead = overhead; + + if (chunkIdx === 0) { + fileState[id].lastChunkIdxProgress = 0; + fileState[id].initialRequestOverhead = overhead; + fileState[id].estTotalRequestsSize = size + (chunkCount * overhead); + } + else if (fileState[id].lastChunkIdxProgress !== chunkIdx) { + fileState[id].lastChunkIdxProgress = chunkIdx; + fileState[id].estTotalRequestsSize += overheadDiff; + } + + return fileState[id].estTotalRequestsSize; + } + + function getLastRequestOverhead(id) { + if (multipart) { + return fileState[id].lastRequestOverhead; + } + else { + return 0; + } + } + + function handleSuccessfullyCompletedChunk(id, response, xhr) { + var chunkIdx = fileState[id].remainingChunkIdxs.shift(), + chunkData = getChunkData(id, chunkIdx); + + fileState[id].attemptingResume = false; + fileState[id].loaded += chunkData.size + getLastRequestOverhead(id); + + if (fileState[id].remainingChunkIdxs.length > 0) { + uploadNextChunk(id); + } + else { + if (resumeEnabled) { + deletePersistedChunkData(id); + } + + handleCompletedItem(id, response, xhr); + } + } + + function isErrorResponse(xhr, response) { + return xhr.status !== 200 || !response.success || response.reset; + } + + function parseResponse(xhr) { + var response; + + try { + response = qq.parseJson(xhr.responseText); + } + catch(error) { + log('Error when attempting to parse xhr response text (' + error + ')', 'error'); + response = {}; + } + + return response; + } + + function handleResetResponse(id) { + log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error'); + + if (resumeEnabled) { + deletePersistedChunkData(id); + fileState[id].attemptingResume = false; + } + + fileState[id].remainingChunkIdxs = []; + delete fileState[id].loaded; + delete fileState[id].estTotalRequestsSize; + delete fileState[id].initialRequestOverhead; + } + + function handleResetResponseOnResumeAttempt(id) { + fileState[id].attemptingResume = false; + log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error'); + handleResetResponse(id); + api.upload(id, true); + } + + function handleNonResetErrorResponse(id, response, xhr) { + var name = api.getName(id); + + if (options.onAutoRetry(id, name, response, xhr)) { + return; + } + else { + handleCompletedItem(id, response, xhr); + } + } + + function onComplete(id, xhr) { + var response; + + // the request was aborted/cancelled + if (!fileState[id]) { + return; + } + + log("xhr - server response received for " + id); + log("responseText = " + xhr.responseText); + response = parseResponse(xhr); + + if (isErrorResponse(xhr, response)) { + if (response.reset) { + handleResetResponse(id); + } + + if (fileState[id].attemptingResume && response.reset) { + handleResetResponseOnResumeAttempt(id); + } + else { + handleNonResetErrorResponse(id, response, xhr); + } + } + else if (chunkFiles) { + handleSuccessfullyCompletedChunk(id, response, xhr); + } + else { + handleCompletedItem(id, response, xhr); + } + } + + function getChunkDataForCallback(chunkData) { + return { + partIndex: chunkData.part, + startByte: chunkData.start + 1, + endByte: chunkData.end, + totalParts: chunkData.count + }; + } + + function getReadyStateChangeHandler(id, xhr) { + return function() { + if (xhr.readyState === 4) { + onComplete(id, xhr); + } + }; + } + + function persistChunkData(id, chunkData) { + var fileUuid = api.getUuid(id), + lastByteSent = fileState[id].loaded, + initialRequestOverhead = fileState[id].initialRequestOverhead, + estTotalRequestsSize = fileState[id].estTotalRequestsSize, + cookieName = getChunkDataCookieName(id), + cookieValue = fileUuid + + cookieItemDelimiter + chunkData.part + + cookieItemDelimiter + lastByteSent + + cookieItemDelimiter + initialRequestOverhead + + cookieItemDelimiter + estTotalRequestsSize, + cookieExpDays = options.resume.cookiesExpireIn; + + qq.setCookie(cookieName, cookieValue, cookieExpDays); + } + + function deletePersistedChunkData(id) { + if (fileState[id].file) { + var cookieName = getChunkDataCookieName(id); + qq.deleteCookie(cookieName); + } + } + + function getPersistedChunkData(id) { + var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)), + filename = api.getName(id), + sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize; + + if (chunkCookieValue) { + sections = chunkCookieValue.split(cookieItemDelimiter); + + if (sections.length === 5) { + uuid = sections[0]; + partIndex = parseInt(sections[1], 10); + lastByteSent = parseInt(sections[2], 10); + initialRequestOverhead = parseInt(sections[3], 10); + estTotalRequestsSize = parseInt(sections[4], 10); + + return { + uuid: uuid, + part: partIndex, + lastByteSent: lastByteSent, + initialRequestOverhead: initialRequestOverhead, + estTotalRequestsSize: estTotalRequestsSize + }; + } + else { + log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn"); + } + } + } + + function getChunkDataCookieName(id) { + var filename = api.getName(id), + fileSize = api.getSize(id), + maxChunkSize = options.chunking.partSize, + cookieName; + + cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize; + + if (resumeId !== undefined) { + cookieName += cookieItemDelimiter + resumeId; + } + + return cookieName; + } + + function getResumeId() { + if (options.resume.id !== null && + options.resume.id !== undefined && + !qq.isFunction(options.resume.id) && + !qq.isObject(options.resume.id)) { + + return options.resume.id; + } + } + + function handleFileChunkingUpload(id, retry) { + var name = api.getName(id), + firstChunkIndex = 0, + persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex; + + if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) { + fileState[id].remainingChunkIdxs = []; + + if (resumeEnabled && !retry && fileState[id].file) { + persistedChunkInfoForResume = getPersistedChunkData(id); + if (persistedChunkInfoForResume) { + firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part); + if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) { + firstChunkIndex = persistedChunkInfoForResume.part; + fileState[id].uuid = persistedChunkInfoForResume.uuid; + fileState[id].loaded = persistedChunkInfoForResume.lastByteSent; + fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize; + fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead; + fileState[id].attemptingResume = true; + log('Resuming ' + name + " at partition index " + firstChunkIndex); + } + } + } + + for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) { + fileState[id].remainingChunkIdxs.unshift(currentChunkIndex); + } + } + + uploadNextChunk(id); + } + + function handleStandardFileUpload(id) { + var fileOrBlob = fileState[id].file || fileState[id].blobData.blob, + name = api.getName(id), + xhr, params, toSend; + + fileState[id].loaded = 0; + + xhr = createXhr(id); + + xhr.upload.onprogress = function(e){ + if (e.lengthComputable){ + fileState[id].loaded = e.loaded; + options.onProgress(id, name, e.loaded, e.total); + } + }; + + xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr); + + params = options.paramsStore.getParams(id); + toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id); + setHeaders(id, xhr); + + log('Sending upload request for ' + id); + xhr.send(toSend); + } + + + api = { + /** + * Adds File or Blob to the queue + * Returns id to use with upload, cancel + **/ + add: function(fileOrBlobData){ + var id; + + if (fileOrBlobData instanceof File) { + id = fileState.push({file: fileOrBlobData}) - 1; + } + else if (fileOrBlobData.blob instanceof Blob) { + id = fileState.push({blobData: fileOrBlobData}) - 1; + } + else { + throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)'); + } + + fileState[id].uuid = qq.getUniqueId(); + return id; + }, + getName: function(id){ + var file = fileState[id].file, + blobData = fileState[id].blobData; + + if (file) { + // fix missing name in Safari 4 + //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined + return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name; + } + else { + return blobData.name; + } + }, + getSize: function(id){ + /*jshint eqnull: true*/ + var fileOrBlob = fileState[id].file || fileState[id].blobData.blob; + + if (qq.isFileOrInput(fileOrBlob)) { + return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size; + } + else { + return fileOrBlob.size; + } + }, + getFile: function(id) { + if (fileState[id]) { + return fileState[id].file || fileState[id].blobData.blob; + } + }, + /** + * Returns uploaded bytes for file identified by id + */ + getLoaded: function(id){ + return fileState[id].loaded || 0; + }, + isValid: function(id) { + return fileState[id] !== undefined; + }, + reset: function() { + fileState = []; + }, + getUuid: function(id) { + return fileState[id].uuid; + }, + /** + * Sends the file identified by id to the server + */ + upload: function(id, retry){ + var name = this.getName(id); + + options.onUpload(id, name); + + if (chunkFiles) { + handleFileChunkingUpload(id, retry); + } + else { + handleStandardFileUpload(id); + } + }, + cancel: function(id){ + var xhr = fileState[id].xhr; + + options.onCancel(id, this.getName(id)); + + if (xhr) { + xhr.onreadystatechange = null; + xhr.abort(); + } + + if (resumeEnabled) { + deletePersistedChunkData(id); + } + + delete fileState[id]; + }, + getResumableFilesData: function() { + var matchingCookieNames = [], + resumableFilesData = []; + + if (chunkFiles && resumeEnabled) { + if (resumeId === undefined) { + matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" + + cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "=")); + } + else { + matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" + + cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" + + cookieItemDelimiter + resumeId + "=")); + } + + qq.each(matchingCookieNames, function(idx, cookieName) { + var cookiesNameParts = cookieName.split(cookieItemDelimiter); + var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter); + + resumableFilesData.push({ + name: decodeURIComponent(cookiesNameParts[1]), + size: cookiesNameParts[2], + uuid: cookieValueParts[0], + partIdx: cookieValueParts[1] + }); + }); + + return resumableFilesData; + } + return []; + } + }; + + return api; +};