/** * http://github.com/Widen/fine-uploader * * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers. * * Copyright © 2013, Widen Enterprises info@fineupoader.com * * Version: -unstable- * * Licensed under GNU GPL v3, 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.isArray = function(variable) { "use strict"; return Object.prototype.toString.call(variable) === "[object Array]"; } qq.isString = function(maybeString) { "use strict"; return Object.prototype.toString.call(maybeString) === '[object String]'; }; qq.trimStr = function(string) { if (String.prototype.trim) { return string.trim(); } return string.replace(/^\s+|\s+$/g,''); }; qq.isFileOrInput = function(maybeFileOrInput) { "use strict"; if (window.File && maybeFileOrInput instanceof File) { return true; } return qq.isInput(maybeFileOrInput); }; qq.isInput = function(maybeInput) { if (window.HTMLInputElement) { if (maybeInput instanceof HTMLInputElement) { if (maybeInput.type && maybeInput.type.toLowerCase() === 'file') { return true; } } } else if (maybeInput.tagName) { if (maybeInput.tagName.toLowerCase() === 'input') { if (maybeInput.type && maybeInput.type.toLowerCase() === 'file') { return true; } } } return false; }; qq.isBlob = function(maybeBlob) { "use strict"; return window.Blob && Object.prototype.toString.call(maybeBlob) === '[object 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 !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined); }; 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; } }); return first; }; /** * 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; }; qq.ios = function() { "use strict"; return navigator.userAgent.indexOf("iPad") !== -1 || navigator.userAgent.indexOf("iPod") !== -1 || navigator.userAgent.indexOf("iPhone") !== -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(objOrArray, callback) { "use strict"; var keyOrIndex, retVal; if (objOrArray) { if (qq.isArray(objOrArray)) { for (keyOrIndex = 0; keyOrIndex < objOrArray.length; keyOrIndex++) { retVal = callback(keyOrIndex, objOrArray[keyOrIndex]); if (retVal === false) { break; } } } else { for (keyOrIndex in objOrArray) { if (Object.prototype.hasOwnProperty.call(objOrArray, keyOrIndex)) { retVal = callback(keyOrIndex, objOrArray[keyOrIndex]); if (retVal === false) { break; } } } } } }; //include any args that should be passed to the new function after the context arg qq.bind = function(oldFunc, context) { if (qq.isFunction(oldFunc)) { var args = Array.prototype.slice.call(arguments, 2); return function() { if (arguments.length) { args = args.concat(Array.prototype.slice.call(arguments)) } return oldFunc.apply(context, args); }; } throw new Error("first parameter must be a function!"); }; /** * 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(';'), cookie; qq.each(ca, function(idx, part) { var cookiePart = part; while (cookiePart.charAt(0)==' ') { cookiePart = cookiePart.substring(1, cookiePart.length); } if (cookiePart.indexOf(nameEQ) === 0) { cookie = cookiePart.substring(nameEQ.length, cookiePart.length); return false; } }); return cookie; }; 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.version="-unstable-";qq.supportedFeatures = (function() { var supportsUploading, supportsAjaxFileUploading, supportsFolderDrop, supportsChunking, supportsResume, supportsUploadViaPaste, supportsUploadCors, supportsDeleteFileCors; function testSupportsFileInputElement() { var supported = true, tempInput; try { tempInput = document.createElement('input'); tempInput.type = 'file'; qq(tempInput).hide(); if(tempInput.disabled) { supported = false; } } catch(ex) { supported = false; } return supported; } //only way to test for Filesystem API support since webkit does not expose the DataTransfer interface function isChrome21OrHigher() { return qq.chrome() && navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined; } //only way to test for complete Clipboard API support at this time function isChrome14OrHigher() { return qq.chrome() && navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; } supportsUploading = testSupportsFileInputElement(); supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); supportsFolderDrop = supportsAjaxFileUploading && isChrome21OrHigher(); supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); supportsResume = supportsAjaxFileUploading && supportsChunking && qq.areCookiesEnabled(); supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); supportsDeleteFileCors = supportsAjaxFileUploading; return { uploading: supportsUploading, ajaxUploading: supportsAjaxFileUploading, fileDrop: supportsAjaxFileUploading, //NOTE: will also return true for touch-only devices. It's not currently possible to accurately test for touch-only devices folderDrop: supportsFolderDrop, chunking: supportsChunking, resume: supportsResume, uploadCustomHeaders: supportsAjaxFileUploading, uploadNonMultipart: supportsAjaxFileUploading, itemSizeValidation: supportsAjaxFileUploading, uploadViaPaste: supportsUploadViaPaste, progressBar: supportsAjaxFileUploading, uploadCors: supportsUploadCors, deleteFileCors: supportsDeleteFileCors, canDetermineSize: supportsAjaxFileUploading } }()); /*globals qq*/ qq.Promise = function() { "use strict"; var successValue, failureValue, successCallbacks = [], failureCallbacks = [], doneCallbacks = [], state = 0; return { then: function(onSuccess, onFailure) { if (state === 0) { if (onSuccess) { successCallbacks.push(onSuccess); } if (onFailure) { failureCallbacks.push(onFailure); } } else if (state === -1 && onFailure) { onFailure(failureValue); } else if (onSuccess) { onSuccess(successValue); } return this; }, done: function(callback) { if (state === 0) { doneCallbacks.push(callback); } else { callback(); } return this; }, success: function(val) { state = 1; successValue = val; if (successCallbacks.length) { qq.each(successCallbacks, function(idx, callback) { callback(val); }) } if(doneCallbacks.length) { qq.each(doneCallbacks, function(idx, callback) { callback(); }) } return this; }, failure: function(val) { state = -1; failureValue = val; if (failureCallbacks.length) { qq.each(failureCallbacks, function(idx, callback) { callback(val); }) } if(doneCallbacks.length) { qq.each(doneCallbacks, function(idx, callback) { callback(); }) } return this; } }; }; qq.isPromise = function(maybePromise) { return maybePromise && maybePromise.then && maybePromise.done; };/*globals qq*/ qq.UploadButton = function(o) { "use strict"; var input, disposeSupport = new qq.DisposeSupport(), 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' }; function createInput() { var input = document.createElement("input"); if (options.multiple){ input.setAttribute("multiple", "multiple"); } if (options.acceptFiles) { input.setAttribute("accept", options.acceptFiles); } input.setAttribute("type", "file"); input.setAttribute("name", 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 }); options.element.appendChild(input); disposeSupport.attach(input, 'change', function(){ options.onChange(input); }); disposeSupport.attach(input, 'mouseover', function(){ qq(options.element).addClass(options.hoverClass); }); disposeSupport.attach(input, 'mouseout', function(){ qq(options.element).removeClass(options.hoverClass); }); disposeSupport.attach(input, 'focus', function(){ qq(options.element).addClass(options.focusClass); }); disposeSupport.attach(input, 'blur', function(){ qq(options.element).removeClass(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.extend(options, o); // make button suitable container for input qq(options.element).css({ position: 'relative', overflow: 'hidden', // Make sure browse button is in the right side // in Internet Explorer direction: 'ltr' }); input = createInput(); return { getInput: function(){ return input; }, reset: function(){ if (input.parentNode){ qq(input).remove(); } qq(options.element).removeClass(options.focusClass); input = createInput(); } }; }; /*globals qq*/ qq.PasteSupport = function(o) { "use strict"; var options, detachPasteHandler; options = { targetElement: null, callbacks: { log: function(message, level) {}, pasteReceived: function(blob) {} } }; function isImage(item) { return item.type && item.type.indexOf("image/") === 0; } function registerPasteHandler() { qq(options.targetElement).attach("paste", function(event) { var clipboardData = event.clipboardData; if (clipboardData) { qq.each(clipboardData.items, function(idx, item) { if (isImage(item)) { var blob = item.getAsFile(); options.callbacks.pasteReceived(blob); } }); } }); } function unregisterPasteHandler() { if (detachPasteHandler) { detachPasteHandler(); } } qq.extend(options, o); registerPasteHandler(); return { reset: function() { unregisterPasteHandler(); } }; };qq.UploadData = function(uploaderProxy) { var data = [], byId = {}, byUuid = {}, byStatus = {}, api; function getDataByIds(ids) { if (qq.isArray(ids)) { var entries = []; qq.each(ids, function(idx, id) { entries.push(data[byId[id]]); }); return entries; } return data[byId[ids]]; } function getDataByUuids(uuids) { if (qq.isArray(uuids)) { var entries = []; qq.each(uuids, function(idx, uuid) { entries.push(data[byUuid[uuid]]); }); return entries; } return data[byUuid[uuids]]; } function getDataByStatus(status) { var statusResults = [], statuses = [].concat(status); qq.each(statuses, function(index, statusEnum) { var statusResultIndexes = byStatus[statusEnum]; if (statusResultIndexes !== undefined) { qq.each(statusResultIndexes, function(i, dataIndex) { statusResults.push(data[dataIndex]); }); } }); return statusResults; } api = { added: function(id) { var uuid = uploaderProxy.getUuid(id), name = uploaderProxy.getName(id), size = uploaderProxy.getSize(id), status = qq.status.SUBMITTING; var index = data.push({ id: id, name: name, uuid: uuid, size: size, status: status }) - 1; byId[id] = index; byUuid[uuid] = index; if (byStatus[status] === undefined) { byStatus[status] = []; } byStatus[status].push(index); uploaderProxy.onStatusChange(id, undefined, status); }, retrieve: function(optionalFilter) { if (qq.isObject(optionalFilter) && data.length) { if (optionalFilter.id !== undefined) { return getDataByIds(optionalFilter.id); } else if (optionalFilter.uuid !== undefined) { return getDataByUuids(optionalFilter.uuid); } else if (optionalFilter.status) { return getDataByStatus(optionalFilter.status); } } else { return qq.extend([], data, true); } }, reset: function() { data = []; byId = {}; byUuid = {}; byStatus = {}; }, setStatus: function(id, newStatus) { var dataIndex = byId[id], oldStatus = data[dataIndex].status, byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], dataIndex); byStatus[oldStatus].splice(byStatusOldStatusIndex, 1); data[dataIndex].status = newStatus; if (byStatus[newStatus] === undefined) { byStatus[newStatus] = []; } byStatus[newStatus].push(dataIndex); uploaderProxy.onStatusChange(id, oldStatus, newStatus); }, uuidChanged: function(id, newUuid) { var dataIndex = byId[id], oldUuid = data[dataIndex].uuid; data[dataIndex].uuid = newUuid; byUuid[newUuid] = dataIndex; delete byUuid[oldUuid]; } }; return api; }; qq.status = { SUBMITTING: "submitting", SUBMITTED: "submitted", REJECTED: "rejected", QUEUED: "queued", CANCELED: "canceled", UPLOADING: "uploading", UPLOAD_RETRYING: "retrying upload", UPLOAD_SUCCESSFUL: "upload successful", UPLOAD_FAILED: "upload failed", DELETE_FAILED: "delete failed", DELETING: "deleting", DELETED: "deleted" };qq.FineUploaderBasic = function(o) { 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, itemLimit: 0, stopOnFirstInvalidFile: true, acceptFiles: null }, callbacks: { onSubmit: function(id, name){}, onSubmitted: function(id, name){}, onComplete: function(id, name, responseJSON, maybeXhr){}, 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, maybeXhr) {}, 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){}, onPasteReceived: function(blob) {}, onStatusChange: function(id, oldStatus, newStatus) {} }, 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.", tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.", retryFailTooManyItems: "Retry failed - you have reached your file limit.", 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: { defaultResponseError: "Upload failure reason unknown", 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' } }, paste: { targetElement: null, defaultName: 'pasted_image' }, camera: { ios: false } }; qq.extend(this._options, o, true); this._handleCameraAccess(); this._wrapCallbacks(); this._disposeSupport = new qq.DisposeSupport(); this._filesInProgress = []; this._storedIds = []; this._autoRetries = []; this._retryTimeouts = []; this._preventRetries = []; this._netUploadedOrQueued = 0; this._netUploaded = 0; this._uploadData = this._createUploadDataTracker(); 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); } if (this._options.paste.targetElement) { this._pasteHandler = this._createPasteHandler(); } this._preventLeaveInProgress(); }; qq.FineUploaderBasic.prototype = { log: function(str, level) { if (this._options.debug && (!level || level === 'info')) { qq.log('[FineUploader ' + qq.version + '] ' + str); } else if (level && level !== 'info') { qq.log('[FineUploader ' + qq.version + '] ' + 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; }, getNetUploads: function() { return this._netUploaded; }, 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._netUploadedOrQueued++; this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); 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(); this._netUploadedOrQueued = 0; this._netUploaded = 0; this._uploadData.reset(); if (this._pasteHandler) { this._pasteHandler.reset(); } }, addFiles: function(filesOrInputs, params, endpoint) { var self = this, verifiedFilesOrInputs = [], fileOrInputIndex, fileOrInput, fileIndex; if (filesOrInputs) { if (!window.FileList || !(filesOrInputs instanceof FileList)) { filesOrInputs = [].concat(filesOrInputs); } for (fileOrInputIndex = 0; fileOrInputIndex < filesOrInputs.length; fileOrInputIndex+=1) { fileOrInput = filesOrInputs[fileOrInputIndex]; if (qq.isFileOrInput(fileOrInput)) { if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) { for (fileIndex = 0; fileIndex < fileOrInput.files.length; fileIndex++) { verifiedFilesOrInputs.push(fileOrInput.files[fileIndex]); } } else { verifiedFilesOrInputs.push(fileOrInput); } } else { self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn'); } } this.log('Received ' + verifiedFilesOrInputs.length + ' files or inputs.'); this._prepareItemsForUpload(verifiedFilesOrInputs, params, endpoint); } }, addBlobs: function(blobDataOrArray, params, endpoint) { 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._prepareItemsForUpload(verifiedBlobDataList, params, endpoint); } 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); }, getName: function(id) { return this._handler.getName(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); } }, doesExist: function(fileOrBlobId) { return this._handler.isValid(fileOrBlobId); }, getUploads: function(optionalFilter) { return this._uploadData.retrieve(optionalFilter); }, _handleCheckedCallback: function(details) { var self = this, callbackRetVal = details.callback(); if (qq.isPromise(callbackRetVal)) { this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier); return callbackRetVal.then( function(successParam) { self.log(details.name + " promise success for " + details.identifier); details.onSuccess(successParam); }, function() { if (details.onFailure) { self.log(details.name + " promise failure for " + details.identifier); details.onFailure(); } else { self.log(details.name + " promise failure for " + details.identifier); } }); } if (callbackRetVal !== false) { details.onSuccess(callbackRetVal); } else { if (details.onFailure) { this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback.") details.onFailure(); } else { this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed.") } } return callbackRetVal; }, _createUploadButton: function(element){ var self = this; var button = new qq.UploadButton({ element: element, multiple: this._options.multiple && qq.supportedFeatures.ajaxUploading, 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, xhr); }, onCancel: function(id, name) { return self._handleCheckedCallback({ name: "onCancel", callback: qq.bind(self._options.callbacks.onCancel, self, id, name), onSuccess: qq.bind(self._onCancel, self, id, name), identifier: id }); }, 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; } }, onUuidChanged: function(id, newUuid) { self._uploadData.uuidChanged(id, newUuid); } }); }, _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); } }); }, _createPasteHandler: function() { var self = this; return new qq.PasteSupport({ targetElement: this._options.paste.targetElement, callbacks: { log: function(str, level) { self.log(str, level); }, pasteReceived: function(blob) { self._handleCheckedCallback({ name: "onPasteReceived", callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob), onSuccess: qq.bind(self._handlePasteSuccess, self, blob), identifier: "pasted image" }); } } }); }, _createUploadDataTracker: function() { var self = this; return new qq.UploadData({ getName: function(id) { return self.getName(id); }, getUuid: function(id) { return self.getUuid(id); }, getSize: function(id) { return self.getSize(id); }, onStatusChange: function(id, oldStatus, newStatus) { self._options.callbacks.onStatusChange(id, oldStatus, newStatus); } }); }, _handlePasteSuccess: function(blob, extSuppliedName) { var extension = blob.type.split("/")[1], name = extSuppliedName; /*jshint eqeqeq: true, eqnull: true*/ if (name == null) { name = this._options.paste.defaultName; } name += '.' + extension; this.addBlobs({ name: name, blob: blob }); }, _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) { this._netUploadedOrQueued++; if (this._options.autoUpload) { this._filesInProgress.push(id); } }, _onProgress: function(id, name, loaded, total) { //nothing to do yet in core uploader }, _onComplete: function(id, name, result, xhr) { if (!result.success) { this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED); this._netUploadedOrQueued--; } else { this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL); this._netUploaded++; } this._removeFromFilesInProgress(id); this._maybeParseAndSendUploadError(id, name, result, xhr); }, _onCancel: function(id, name) { this._uploadData.setStatus(id, qq.status.CANCELED); this._netUploadedOrQueued--; 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 || qq.supportedFeatures.deleteFileCors)); }, _onSubmitDelete: function(id, onSuccessCallback) { if (this._isDeletePossible()) { return this._handleCheckedCallback({ name: "onSubmitDelete", callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id), onSuccess: onSuccessCallback || qq.bind(this._deleteHandler.sendDelete, this, id, this.getUuid(id)), identifier: 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(id) { this._uploadData.setStatus(id, qq.status.DELETING); }, _onDeleteComplete: function(id, xhr, isError) { var name = this._handler.getName(id); if (isError) { this._uploadData.setStatus(id, qq.status.DELETE_FAILED); this.log("Delete request for '" + name + "' has failed.", "error"); this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status, xhr); } else { this._uploadData.setStatus(id, qq.status.DELETED); this._netUploadedOrQueued--; this._netUploaded--; this._handler.expunge(id); 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) { this._uploadData.setStatus(id, qq.status.UPLOADING); }, _onInputChange: function(input){ if (qq.supportedFeatures.ajaxUploading) { 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._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); 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) { var itemLimit = this._options.validation.itemLimit; 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; } if (itemLimit > 0 && this._netUploadedOrQueued+1 > itemLimit) { this._itemError("retryFailTooManyItems", ""); 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, xhr); } else { var errorReason = response.error ? response.error : this._options.text.defaultResponseError; this._options.callbacks.onError(id, name, errorReason, xhr); } } }, _prepareItemsForUpload: function(items, params, endpoint) { var validationDescriptors = this._getValidationDescriptors(items); this._handleCheckedCallback({ name: "onValidateBatch", callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors), onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint), identifier: "batch validation" }); }, _upload: function(blobOrFileContainer, params, endpoint) { var id = this._handler.add(blobOrFileContainer), name = this._handler.getName(id); this._uploadData.added(id); if (params) { this.setParams(params, id); } if (endpoint) { this.setEndpoint(endpoint, id); } this._handleCheckedCallback({ name: "onSubmit", callback: qq.bind(this._options.callbacks.onSubmit, this, id, name), onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name), onFailure: qq.bind(this._fileOrBlobRejected, this, id, name), identifier: id }); }, _onSubmitCallbackSuccess: function(id, name) { this._uploadData.setStatus(id, qq.status.SUBMITTED); this._onSubmit(id, name); this._options.callbacks.onSubmitted(id, name); if (this._options.autoUpload) { if (!this._handler.upload(id)) { this._uploadData.setStatus(id, qq.status.QUEUED); } } else { this._storeForLater(id); } }, _storeForLater: function(id) { this._storedIds.push(id); }, _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint) { var errorMessage, itemLimit = this._options.validation.itemLimit, proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued + validationDescriptors.length; if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) { if (items.length > 0) { this._handleCheckedCallback({ name: "onValidate", callback: qq.bind(this._options.callbacks.onValidate, this, items[0]), onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint), onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint), identifier: "Item '" + items[0].name + "', size: " + items[0].size }); } else { this._itemError("noFilesError", ""); } } else { errorMessage = this._options.messages.tooManyItemsError .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued) .replace(/\{itemLimit\}/g, itemLimit); this._batchError(errorMessage); } }, _onValidateCallbackSuccess: function(items, index, params, endpoint) { var nextIndex = index+1, validationDescriptor = this._getValidationDescriptor(items[index]), validItem = false; if (this._validateFileOrBlobData(items[index], validationDescriptor)) { validItem = true; this._upload(items[index], params, endpoint); } this._maybeProcessNextItemAfterOnValidateCallback(validItem, items, nextIndex, params, endpoint); }, _onValidateCallbackFailure: function(items, index, params, endpoint) { var nextIndex = index+ 1; this._fileOrBlobRejected(undefined, items[0].name); this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); }, _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) { var self = this; if (items.length > index) { if (validItem || !this._options.validation.stopOnFirstInvalidFile) { //use setTimeout to prevent a stack overflow with a large number of files in the batch & non-promissory callbacks setTimeout(function() { var validationDescriptor = self._getValidationDescriptor(items[index]); self._handleCheckedCallback({ name: "onValidate", callback: qq.bind(self._options.callbacks.onValidate, self, items[index]), onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint), onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint), identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size }); }, 0); } } }, _validateFileOrBlobData: function(item, validationDescriptor) { var name = validationDescriptor.name, size = validationDescriptor.size, valid = true; if (this._options.callbacks.onValidate(validationDescriptor) === false) { valid = false; } if (qq.isFileOrInput(item) && !this._isAllowedExtension(name)){ this._itemError('typeError', name); valid = false; } else if (size === 0){ this._itemError('emptyError', name); valid = false; } else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){ this._itemError('sizeError', name); valid = false; } else if (size && size < this._options.validation.minSizeLimit){ this._itemError('minSizeError', name); valid = false; } if (!valid) { this._fileOrBlobRejected(undefined, name); } return valid; }, _fileOrBlobRejected: function(id, name) { if (id !== undefined) { this._uploadData.setStatus(id, qq.status.REJECTED); } }, _itemError: function(code, nameOrNames) { var message = this._options.messages[code], allowedExtensions = [], names = [].concat(nameOrNames), name = names[0], extensionsForMessage, placeholderMatch; function r(name, replacement){ message = message.replace(name, replacement); } qq.each(this._options.validation.allowedExtensions, function(idx, allowedExtension) { /** * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details. */ if (qq.isString(allowedExtension)) { allowedExtensions.push(allowedExtension); } }); extensionsForMessage = allowedExtensions.join(', ').toLowerCase(); r('{file}', this._options.formatFileName(name)); r('{extensions}', extensionsForMessage); r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit)); r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit)); placeholderMatch = message.match(/(\{\w+\})/g); if (placeholderMatch !== null) { qq.each(placeholderMatch, function(idx, placeholder) { r(placeholder, names[idx]); }); } this._options.callbacks.onError(null, name, message, undefined); return message; }, _batchError: function(message) { this._options.callbacks.onError(null, null, message, undefined); }, _isAllowedExtension: function(fileName){ var allowed = this._options.validation.allowedExtensions, valid = false; if (!allowed.length) { return true; } qq.each(allowed, function(idx, allowedExt) { /** * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details. */ if (qq.isString(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 / 1000; i++; } while (bytes > 999); 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 !== undefined) { 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 = {}; } }; }, _handleCameraAccess: function() { if (this._options.camera.ios && qq.ios()) { this._options.multiple = false; if (this._options.validation.acceptFiles === null) { this._options.validation.acceptFiles = "image/*;capture=camera"; } else { this._options.validation.acceptFiles += ",image/*;capture=camera"; } } } }; /*globals qq, document*/ qq.DragAndDrop = function(o) { "use strict"; var options, dz, droppedFiles = [], disposeSupport = new qq.DisposeSupport(); options = { dropZoneElements: [], hideDropZonesBeforeEnter: false, allowMultipleItems: true, classes: { dropActive: null }, callbacks: new qq.DragAndDrop.callbacks() }; qq.extend(options, o, true); setupDragDrop(); function uploadDroppedFiles(files) { options.callbacks.dropLog('Grabbed ' + files.length + " dropped files."); dz.dropDisabled(false); options.callbacks.processingDroppedFilesComplete(files); } function traverseFileTree(entry) { var dirReader, i, parseEntryPromise = new qq.Promise(); if (entry.isFile) { entry.file(function(file) { droppedFiles.push(file); parseEntryPromise.success(); }, function(fileError) { options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error"); parseEntryPromise.failure(); }); } else if (entry.isDirectory) { dirReader = entry.createReader(); dirReader.readEntries(function(entries) { var entriesLeft = entries.length; for (i = 0; i < entries.length; i+=1) { traverseFileTree(entries[i]).done(function() { entriesLeft-=1; if (entriesLeft === 0) { parseEntryPromise.success(); } }); } if (!entries.length) { parseEntryPromise.success(); } }, function(fileError) { options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error"); parseEntryPromise.failure(); }); } return parseEntryPromise; } function handleDataTransfer(dataTransfer) { var i, items, entry, pendingFolderPromises = [], handleDataTransferPromise = new qq.Promise(); options.callbacks.processingDroppedFiles(); dz.dropDisabled(true); if (dataTransfer.files.length > 1 && !options.allowMultipleItems) { options.callbacks.processingDroppedFilesComplete([]); options.callbacks.dropError('tooManyFilesError', ""); dz.dropDisabled(false); handleDataTransferPromise.failure(); } else { droppedFiles = []; 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()); } else { pendingFolderPromises.push(traverseFileTree(entry).done(function() { pendingFolderPromises.pop(); if (pendingFolderPromises.length === 0) { handleDataTransferPromise.success(); } })); } } } } else { droppedFiles = dataTransfer.files; } if (pendingFolderPromises.length === 0) { handleDataTransferPromise.success(); } } return handleDataTransferPromise; } 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.hideDropZonesBeforeEnter) { qq(dropArea).hide(); } qq(dropArea).removeClass(options.classes.dropActive); handleDataTransfer(e.dataTransfer).done(function() { uploadDroppedFiles(droppedFiles); }); } }); disposeSupport.addDisposer(function() { dz.dispose(); }); if (options.hideDropZonesBeforeEnter) { 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(){ var dropZones = options.dropZoneElements; qq.each(dropZones, function(idx, dropZone) { setupDropzone(dropZone); }) // IE <= 9 does not support the File API used for drag+drop uploads if (dropZones.length && (!qq.ie() || qq.ie10())) { disposeSupport.attach(document, 'dragenter', function(e) { if (!dz.dropDisabled() && isFileDrag(e)) { qq.each(dropZones, function(idx, dropZone) { qq(dropZone).css({display: 'block'}); }); } }); } disposeSupport.attach(document, 'dragleave', function(e){ if (options.hideDropZonesBeforeEnter && qq.FineUploader.prototype._leaving_document_out(e)) { qq.each(dropZones, function(idx, dropZone) { qq(dropZone).hide(); }); } }); disposeSupport.attach(document, 'drop', function(e){ if (options.hideDropZonesBeforeEnter) { qq.each(dropZones, function(idx, dropZone) { qq(dropZone).hide(); }); } e.preventDefault(); }); } return { setupExtraDropzone: function(element) { options.dropZoneElements.push(element); setupDropzone(element); }, removeDropzone: function(element) { var i, dzs = options.dropZoneElements; for(i in dzs) { if (dzs[i] === element) { return dzs.splice(i, 1); } } }, dispose: function() { disposeSupport.dispose(); dz.dispose(); } }; }; qq.DragAndDrop.callbacks = function() { return { processingDroppedFiles: function() {}, processingDroppedFilesComplete: function(files) {}, dropError: function(code, errorSpecifics) { qq.log("Drag & drop error code '" + code + " with these specifics: '" + errorSpecifics + "'", "error"); }, dropLog: function(message, level) { qq.log(message, level); } } } 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: '
tag if (innerHtml && innerHtml.match(/^'); iframe.setAttribute('id', iframeName); 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; } function expungeFile(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(getIframeName(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(); } } function getFileIdForIframeName(iframeName) { return iframeName.split("_")[0]; } function getIframeName(fileId) { return fileId + "_" + formHandlerInstanceId; } 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*/ if (api.isValid(id)) { // get input value and remove path to normalize return inputs[id].value.replace(/.*(\/|\\)/, ""); } else { log(id + " is not a valid item ID.", "error"); } }, isValid: function(id) { return inputs[id] !== undefined; }, reset: function() { inputs = []; uuids = []; detachLoadEvents = {}; formHandlerInstanceId = qq.getUniqueId(); }, expunge: function(id) { return expungeFile(id); }, getUuid: function(id) { return uuids[id]; }, cancel: function(id) { var onCancelRetVal = options.onCancel(id, api.getName(id)); if (qq.isPromise(onCancelRetVal)) { return onCancelRetVal.then(function() { expungeFile(id); }); } else if (onCancelRetVal !== false) { expungeFile(id); return true; } return false; }, 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, api.getName(id)); form = createForm(id, iframe); form.appendChild(input); attachLoadEvent(iframe, function(responseFromMessage){ log('iframe loaded'); var response = responseFromMessage ? responseFromMessage : getIframeContentJson(id, 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 api; }; /*globals qq, File, XMLHttpRequest, FormData, Blob*/ qq.UploadHandlerXhr = function(o, uploadCompleteCallback, onUuidChange, logCallback) { "use strict"; var options = o, uploadComplete = uploadCompleteCallback, log = logCallback, fileState = [], cookieItemDelimiter = "|", chunkFiles = options.chunking.enabled && qq.supportedFeatures.chunking, resumeEnabled = options.resume.enabled && chunkFiles && qq.supportedFeatures.resume, 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); if (fileState[id]) { 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(id, xhr) { var response; try { response = qq.parseJson(xhr.responseText); if (response.newUuid !== undefined) { log("Server requested UUID change from '" + fileState[id].uuid + "' to '" + response.newUuid + "'"); fileState[id].uuid = response.newUuid; onUuidChanged(id, response.newUuid); } } 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(id, 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 calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex) { var currentChunkIndex; for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) { fileState[id].remainingChunkIdxs.unshift(currentChunkIndex); } uploadNextChunk(id); } function onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume) { firstChunkIndex = persistedChunkInfoForResume.part; 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); calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex); } function handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex) { var name = api.getName(id), firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part), onResumeRetVal; onResumeRetVal = options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)); if (qq.isPromise(onResumeRetVal)) { log("Waiting for onResume promise to be fulfilled for " + id); onResumeRetVal.then( function() { onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume); }, function() { log("onResume promise fulfilled - failure indicated. Will not resume.") calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex); } ); } else if (onResumeRetVal !== false) { onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume); } else { log("onResume callback returned false. Will not resume."); calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex); } } function handleFileChunkingUpload(id, retry) { var firstChunkIndex = 0, persistedChunkInfoForResume; if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) { fileState[id].remainingChunkIdxs = []; if (resumeEnabled && !retry && fileState[id].file) { persistedChunkInfoForResume = getPersistedChunkData(id); if (persistedChunkInfoForResume) { handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex); } else { calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex); } } else { calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex); } } else { 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); } function expungeItem(id) { var xhr = fileState[id].xhr; if (xhr) { xhr.onreadystatechange = null; xhr.abort(); } if (resumeEnabled) { deletePersistedChunkData(id); } delete fileState[id]; } api = { /** * Adds File or Blob to the queue * Returns id to use with upload, cancel **/ add: function(fileOrBlobData){ var id, persistedChunkData, uuid = qq.getUniqueId(); if (fileOrBlobData instanceof File) { id = fileState.push({file: fileOrBlobData}) - 1; } else if (qq.isBlob(fileOrBlobData.blob)) { id = fileState.push({blobData: fileOrBlobData}) - 1; } else { throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)'); } if (resumeEnabled) { persistedChunkData = getPersistedChunkData(id); if (persistedChunkData) { uuid = persistedChunkData.uuid; } } fileState[id].uuid = uuid; return id; }, getName: function(id){ if (api.isValid(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; } } else { log(id + " is not a valid item ID.", "error"); } }, 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; } }, isValid: function(id) { return fileState[id] !== undefined; }, reset: function() { fileState = []; }, expunge: function(id) { return expungeItem(id); }, 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); if (this.isValid(id)) { options.onUpload(id, name); if (chunkFiles) { handleFileChunkingUpload(id, retry); } else { handleStandardFileUpload(id); } } }, cancel: function(id) { var onCancelRetVal = options.onCancel(id, this.getName(id)); if (qq.isPromise(onCancelRetVal)) { return onCancelRetVal.then(function() { expungeItem(id); }); } else if (onCancelRetVal !== false) { expungeItem(id); return true; } return false; }, 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; };