/** * 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: '<div class="qq-uploader">' + ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>' : '') + (!this._options.button ? '<div class="qq-upload-button"><div>{uploadButtonText}</div></div>' : '') + '<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>' + (!this._options.listElement ? '<ul class="qq-upload-list"></ul>' : '') + '</div>', // template for one item in file list fileTemplate: '<li>' + '<div class="qq-progress-bar"></div>' + '<span class="qq-upload-spinner"></span>' + '<span class="qq-upload-finished"></span>' + '<span class="qq-upload-file"></span>' + '<span class="qq-upload-size"></span>' + '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' + '<a class="qq-upload-retry" href="#">{retryButtonText}</a>' + '<a class="qq-upload-delete" href="#">{deleteButtonText}</a>' + '<span class="qq-upload-status-text">{statusText}</span>' + '</li>', 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 <pre> tag if (innerHTML && innerHTML.match(/^<pre/i)) { innerHTML = doc.body.firstChild.firstChild.nodeValue; } response = qq.parseJson(innerHTML); } catch(error){ log('Error when attempting to parse form upload response (' + error + ")", 'error'); response = {success: false}; } return response; } /** * Creates iframe with unique name */ function createIframe(id){ // We can't use following code as the name attribute // won't be properly registered in IE6, and new window // on form submit will open // var iframe = document.createElement('iframe'); // iframe.setAttribute('name', id); var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />'); 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('<form method="' + protocol + '" enctype="multipart/form-data"></form>'), 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; };