UNPKG

collective-fine-upload

Version:

Upload assets to Collective with Fine Uploader

1,579 lines (1,296 loc) 389 kB
/*! * Fine Uploader * * Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com * * Version: 5.2.1 * * Homepage: http://fineuploader.com * * Repository: git://github.com/FineUploader/fine-uploader.git * * Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). */ /*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob, Storage, ActiveXObject */ /* jshint -W079 */ 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) { // The [W3C spec](http://www.w3.org/TR/domcore/#dom-node-contains) // says a `null` (or ostensibly `undefined`) parameter // passed into `Node.contains` should result in a false return value. // IE7 throws an exception if the parameter is `undefined` though. if (!descendant) { return false; } // 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) { /*jshint eqnull: true*/ if (element.style == null) { throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!"); } /*jshint -W116*/ 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, considerParent) { var re = new RegExp("(^| )" + name + "( |$)"); return re.test(element.className) || !!(considerParent && re.test(element.parentNode.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(""); }, // Returns true if the attribute exists on the element // AND the value of the attribute is NOT "false" (case-insensitive) hasAttribute: function(attrName) { var attrVal; if (element.hasAttribute) { if (!element.hasAttribute(attrName)) { return false; } /*jshint -W116*/ return (/^false$/i).exec(element.getAttribute(attrName)) == null; } else { attrVal = element[attrName]; if (attrVal === undefined) { return false; } /*jshint -W116*/ return (/^false$/i).exec(attrVal) == null; } } }; }; (function() { "use strict"; qq.canvasToBlob = function(canvas, mime, quality) { return qq.dataUriToBlob(canvas.toDataURL(mime, quality)); }; qq.dataUriToBlob = function(dataUri) { var arrayBuffer, byteString, createBlob = function(data, mime) { var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, blobBuilder = BlobBuilder && new BlobBuilder(); if (blobBuilder) { blobBuilder.append(data); return blobBuilder.getBlob(mime); } else { return new Blob([data], {type: mime}); } }, intArray, mimeString; // convert base64 to raw binary data held in a string if (dataUri.split(",")[0].indexOf("base64") >= 0) { byteString = atob(dataUri.split(",")[1]); } else { byteString = decodeURI(dataUri.split(",")[1]); } // extract the MIME mimeString = dataUri.split(",")[0] .split(":")[1] .split(";")[0]; // write the bytes of the binary string to an ArrayBuffer arrayBuffer = new ArrayBuffer(byteString.length); intArray = new Uint8Array(arrayBuffer); qq.each(byteString, function(idx, character) { intArray[idx] = character.charCodeAt(0); }); return createBlob(arrayBuffer, mimeString); }; qq.log = function(message, level) { 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) { return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]"; }; qq.isFunction = function(variable) { return typeof (variable) === "function"; }; /** * Check the type of a value. Is it an "array"? * * @param value value to test. * @returns true if the value is an array or associated with an `ArrayBuffer` */ qq.isArray = function(value) { return Object.prototype.toString.call(value) === "[object Array]" || (value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer); }; // Looks for an object on a `DataTransfer` object that is associated with drop events when utilizing the Filesystem API. qq.isItemList = function(maybeItemList) { return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]"; }; // Looks for an object on a `NodeList` or an `HTMLCollection`|`HTMLFormElement`|`HTMLSelectElement` // object that is associated with collections of Nodes. qq.isNodeList = function(maybeNodeList) { return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" || // If `HTMLCollection` is the actual type of the object, we must determine this // by checking for expected properties/methods on the object (maybeNodeList.item && maybeNodeList.namedItem); }; qq.isString = function(maybeString) { 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, ""); }; /** * @param str String to format. * @returns {string} A string, swapping argument values with the associated occurrence of {} in the passed string. */ qq.format = function(str) { var args = Array.prototype.slice.call(arguments, 1), newStr = str, nextIdxToReplace = newStr.indexOf("{}"); qq.each(args, function(idx, val) { var strBefore = newStr.substring(0, nextIdxToReplace), strAfter = newStr.substring(nextIdxToReplace + 2); newStr = strBefore + val + strAfter; nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length); // End the loop if we have run out of tokens (when the arguments exceed the # of tokens) if (nextIdxToReplace < 0) { return false; } }); return newStr; }; qq.isFile = function(maybeFile) { return window.File && Object.prototype.toString.call(maybeFile) === "[object File]"; }; qq.isFileList = function(maybeFileList) { return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]"; }; qq.isFileOrInput = function(maybeFileOrInput) { return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput); }; qq.isInput = function(maybeInput, notFile) { var evaluateType = function(type) { var normalizedType = type.toLowerCase(); if (notFile) { return normalizedType !== "file"; } return normalizedType === "file"; }; if (window.HTMLInputElement) { if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") { if (maybeInput.type && evaluateType(maybeInput.type)) { return true; } } } if (maybeInput.tagName) { if (maybeInput.tagName.toLowerCase() === "input") { if (maybeInput.type && evaluateType(maybeInput.type)) { return true; } } } return false; }; qq.isBlob = function(maybeBlob) { if (window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]") { return true; } }; qq.isXhrUploadSupported = function() { var input = document.createElement("input"); input.type = "file"; return ( input.multiple !== undefined && typeof File !== "undefined" && typeof FormData !== "undefined" && typeof (qq.createXhrInstance()).upload !== "undefined"); }; // Fall back to ActiveX is native XHR is disabled (possible in any version of IE). qq.createXhrInstance = function() { if (window.XMLHttpRequest) { return new XMLHttpRequest(); } try { return new ActiveXObject("MSXML2.XMLHTTP.3.0"); } catch (error) { qq.log("Neither XHR or ActiveX are supported!", "error"); return null; } }; qq.isFolderDropSupported = function(dataTransfer) { return dataTransfer.items && dataTransfer.items.length > 0 && dataTransfer.items[0].webkitGetAsEntry; }; qq.isFileChunkingSupported = function() { return !qq.androidStock() && //Android's stock browser cannot upload Blobs correctly qq.isXhrUploadSupported() && (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined); }; qq.sliceBlob = function(fileOrBlob, start, end) { var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice; return slicer.call(fileOrBlob, start, end); }; qq.arrayBufferToHex = function(buffer) { var bytesAsHex = "", bytes = new Uint8Array(buffer); qq.each(bytes, function(idx, byt) { var byteAsHexStr = byt.toString(16); if (byteAsHexStr.length < 2) { byteAsHexStr = "0" + byteAsHexStr; } bytesAsHex += byteAsHexStr; }); return bytesAsHex; }; qq.readBlobToHex = function(blob, startOffset, length) { var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length), fileReader = new FileReader(), promise = new qq.Promise(); fileReader.onload = function() { promise.success(qq.arrayBufferToHex(fileReader.result)); }; fileReader.onerror = promise.failure; fileReader.readAsArrayBuffer(initialBlob); return promise; }; qq.extend = function(first, second, extendNested) { 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; }; /** * Allow properties in one object to override properties in another, * keeping track of the original values from the target object. * * Note that the pre-overriden properties to be overriden by the source will be passed into the `sourceFn` when it is invoked. * * @param target Update properties in this object from some source * @param sourceFn A function that, when invoked, will return properties that will replace properties with the same name in the target. * @returns {object} The target object */ qq.override = function(target, sourceFn) { var super_ = {}, source = sourceFn(super_); qq.each(source, function(srcPropName, srcPropVal) { if (target[srcPropName] !== undefined) { super_[srcPropName] = target[srcPropName]; } target[srcPropName] = srcPropVal; }); return target; }; /** * Searches for a given element (elt) in the array, returns -1 if it is not present. */ qq.indexOf = function(arr, elt, from) { 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() { 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() { return navigator.userAgent.indexOf("MSIE") !== -1 || navigator.userAgent.indexOf("Trident") !== -1; }; qq.ie7 = function() { return navigator.userAgent.indexOf("MSIE 7") !== -1; }; qq.ie8 = function() { return navigator.userAgent.indexOf("MSIE 8") !== -1; }; qq.ie10 = function() { return navigator.userAgent.indexOf("MSIE 10") !== -1; }; qq.ie11 = function() { return qq.ie() && navigator.userAgent.indexOf("rv:11") !== -1; }; qq.safari = function() { return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; }; qq.chrome = function() { return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1; }; qq.opera = function() { return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1; }; qq.firefox = function() { return (!qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === ""); }; qq.windows = function() { return navigator.platform === "Win32"; }; qq.android = function() { return navigator.userAgent.toLowerCase().indexOf("android") !== -1; }; // We need to identify the Android stock browser via the UA string to work around various bugs in this browser, // such as the one that prevents a `Blob` from being uploaded. qq.androidStock = function() { return qq.android() && navigator.userAgent.toLowerCase().indexOf("chrome") < 0; }; qq.ios6 = function() { return qq.ios() && navigator.userAgent.indexOf(" OS 6_") !== -1; }; qq.ios7 = function() { return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1; }; qq.ios8 = function() { return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; }; // iOS 8.0.0 qq.ios800 = function() { return qq.ios() && navigator.userAgent.indexOf(" OS 8_0 ") !== -1; }; qq.ios = function() { /*jshint -W014 */ return navigator.userAgent.indexOf("iPad") !== -1 || navigator.userAgent.indexOf("iPod") !== -1 || navigator.userAgent.indexOf("iPhone") !== -1; }; qq.iosChrome = function() { return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1; }; qq.iosSafari = function() { return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1; }; qq.iosSafariWebView = function() { return qq.ios() && !qq.iosChrome() && !qq.iosSafari(); }; // // Events qq.preventDefault = function(e) { 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() { 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 entry in the iterable item qq.each = function(iterableItem, callback) { var keyOrIndex, retVal; if (iterableItem) { // Iterate through [`Storage`](http://www.w3.org/TR/webstorage/#the-storage-interface) items if (window.Storage && iterableItem.constructor === window.Storage) { for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex))); if (retVal === false) { break; } } } // `DataTransferItemList` & `NodeList` objects are array-like and should be treated as arrays // when iterating over items inside the object. else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) { for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); if (retVal === false) { break; } } } else if (qq.isString(iterableItem)) { for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex)); if (retVal === false) { break; } } } else { for (keyOrIndex in iterableItem) { if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) { retVal = callback(keyOrIndex, iterableItem[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() { var newArgs = qq.extend([], args); if (arguments.length) { newArgs = newArgs.concat(Array.prototype.slice.call(arguments)); } return oldFunc.apply(context, newArgs); }; } 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) { /*jshint laxbreak: true*/ var 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")) { qq.each(obj, function(idx, val) { add(val, idx); }); } else if ((typeof obj !== "undefined") && (obj !== null) && (typeof obj === "object")) { qq.each(obj, function(prop, val) { add(val, prop); }); } 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) { 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) { 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; }; /** * 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 + ")"); } }; /** * Retrieve the extension of a file, if it exists. * * @param filename * @returns {string || undefined} */ qq.getExtension = function(filename) { var extIdx = filename.lastIndexOf(".") + 1; if (extIdx > 0) { return filename.substr(extIdx, filename.length - extIdx); } }; qq.getFilename = function(blobOrFileInput) { /*jslint regexp: true*/ if (qq.isInput(blobOrFileInput)) { // get input value and remove path to normalize return blobOrFileInput.value.replace(/.*(\/|\\)/, ""); } else if (qq.isFile(blobOrFileInput)) { if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) { return blobOrFileInput.fileName; } } return blobOrFileInput.name; }; /** * A generic module which supports object disposing in dispose() method. * */ qq.DisposeSupport = function() { 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); } }; }; }()); /* globals qq */ /** * Fine Uploader top-level Error container. Inherits from `Error`. */ (function() { "use strict"; qq.Error = function(message) { this.message = "[Fine Uploader " + qq.version + "] " + message; }; qq.Error.prototype = new Error(); }()); /*global qq */ qq.version = "5.2.1"; /* globals qq */ qq.supportedFeatures = (function() { "use strict"; var supportsUploading, supportsUploadingBlobs, supportsFileDrop, supportsAjaxFileUploading, supportsFolderDrop, supportsChunking, supportsResume, supportsUploadViaPaste, supportsUploadCors, supportsDeleteFileXdr, supportsDeleteFileCorsXhr, supportsDeleteFileCors, supportsFolderSelection, supportsImagePreviews, supportsUploadProgress; 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() || qq.opera()) && 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() || qq.opera()) && navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; } //Ensure we can send cross-origin `XMLHttpRequest`s function isCrossOriginXhrSupported() { if (window.XMLHttpRequest) { var xhr = qq.createXhrInstance(); //Commonly accepted test for XHR CORS support. return xhr.withCredentials !== undefined; } return false; } //Test for (terrible) cross-origin ajax transport fallback for IE9 and IE8 function isXdrSupported() { return window.XDomainRequest !== undefined; } // CORS Ajax requests are supported if it is either possible to send credentialed `XMLHttpRequest`s, // or if `XDomainRequest` is an available alternative. function isCrossOriginAjaxSupported() { if (isCrossOriginXhrSupported()) { return true; } return isXdrSupported(); } function isFolderSelectionSupported() { // We know that folder selection is only supported in Chrome via this proprietary attribute for now return document.createElement("input").webkitdirectory !== undefined; } function isLocalStorageSupported() { try { return !!window.localStorage; } catch (error) { // probably caught a security exception, so no localStorage for you return false; } } function isDragAndDropSupported() { var span = document.createElement("span"); return ("draggable" in span || ("ondragstart" in span && "ondrop" in span)) && !qq.android() && !qq.ios(); } supportsUploading = testSupportsFileInputElement(); supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock(); supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported(); supportsFolderDrop = supportsFileDrop && isChrome21OrHigher(); supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported(); supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); supportsDeleteFileCorsXhr = isCrossOriginXhrSupported(); supportsDeleteFileXdr = isXdrSupported(); supportsDeleteFileCors = isCrossOriginAjaxSupported(); supportsFolderSelection = isFolderSelectionSupported(); supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined; supportsUploadProgress = (function() { if (supportsAjaxFileUploading) { return !qq.androidStock() && !qq.iosChrome(); } return false; }()); return { ajaxUploading: supportsAjaxFileUploading, blobUploading: supportsUploadingBlobs, canDetermineSize: supportsAjaxFileUploading, chunking: supportsChunking, deleteFileCors: supportsDeleteFileCors, deleteFileCorsXdr: supportsDeleteFileXdr, //NOTE: will also return true in IE10, where XDR is also supported deleteFileCorsXhr: supportsDeleteFileCorsXhr, dialogElement: !!window.HTMLDialogElement, fileDrop: supportsFileDrop, folderDrop: supportsFolderDrop, folderSelection: supportsFolderSelection, imagePreviews: supportsImagePreviews, imageValidation: supportsImagePreviews, itemSizeValidation: supportsAjaxFileUploading, pause: supportsChunking, progressBar: supportsUploadProgress, resume: supportsResume, scaling: supportsImagePreviews && supportsUploadingBlobs, tiffPreviews: qq.safari(), // Not the best solution, but simple and probably accurate enough (for now) unlimitedScaledImageSize: !qq.ios(), // false simply indicates that there is some known limit uploading: supportsUploading, uploadCors: supportsUploadCors, uploadCustomHeaders: supportsAjaxFileUploading, uploadNonMultipart: supportsAjaxFileUploading, uploadViaPaste: supportsUploadViaPaste }; }()); /*globals qq*/ // Is the passed object a promise instance? qq.isGenericPromise = function(maybePromise) { "use strict"; return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then)); }; qq.Promise = function() { "use strict"; var successArgs, failureArgs, successCallbacks = [], failureCallbacks = [], doneCallbacks = [], state = 0; qq.extend(this, { then: function(onSuccess, onFailure) { if (state === 0) { if (onSuccess) { successCallbacks.push(onSuccess); } if (onFailure) { failureCallbacks.push(onFailure); } } else if (state === -1) { onFailure && onFailure.apply(null, failureArgs); } else if (onSuccess) { onSuccess.apply(null, successArgs); } return this; }, done: function(callback) { if (state === 0) { doneCallbacks.push(callback); } else { callback.apply(null, failureArgs === undefined ? successArgs : failureArgs); } return this; }, success: function() { state = 1; successArgs = arguments; if (successCallbacks.length) { qq.each(successCallbacks, function(idx, callback) { callback.apply(null, successArgs); }); } if (doneCallbacks.length) { qq.each(doneCallbacks, function(idx, callback) { callback.apply(null, successArgs); }); } return this; }, failure: function() { state = -1; failureArgs = arguments; if (failureCallbacks.length) { qq.each(failureCallbacks, function(idx, callback) { callback.apply(null, failureArgs); }); } if (doneCallbacks.length) { qq.each(doneCallbacks, function(idx, callback) { callback.apply(null, failureArgs); }); } return this; } }); }; /* globals qq */ /** * Placeholder for a Blob that will be generated on-demand. * * @param referenceBlob Parent of the generated blob * @param onCreate Function to invoke when the blob must be created. Must be promissory. * @constructor */ qq.BlobProxy = function(referenceBlob, onCreate) { "use strict"; qq.extend(this, { referenceBlob: referenceBlob, create: function() { return onCreate(referenceBlob); } }); }; /*globals qq*/ /** * This module represents an upload or "Select File(s)" button. It's job is to embed an opaque `<input type="file">` * element as a child of a provided "container" element. This "container" element (`options.element`) is used to provide * a custom style for the `<input type="file">` element. The ability to change the style of the container element is also * provided here by adding CSS classes to the container on hover/focus. * * TODO Eliminate the mouseover and mouseout event handlers since the :hover CSS pseudo-class should now be * available on all supported browsers. * * @param o Options to override the default values */ qq.UploadButton = function(o) { "use strict"; var self = this, disposeSupport = new qq.DisposeSupport(), options = { // "Container" element element: null, // If true adds `multiple` attribute to `<input type="file">` multiple: false, // Corresponds to the `accept` attribute on the associated `<input type="file">` acceptFiles: null, // A true value allows folders to be selected, if supported by the UA folders: false, // `name` attribute of `<input type="file">` name: "qqfile", // Called when the browser invokes the onchange handler on the `<input type="file">` onChange: function(input) {}, ios8BrowserCrashWorkaround: false, // **This option will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers hoverClass: "qq-upload-button-hover", focusClass: "qq-upload-button-focus" }, input, buttonId; // Overrides any of the default option values with any option values passed in during construction. qq.extend(options, o); buttonId = qq.getUniqueId(); // Embed an opaque `<input type="file">` element as a child of `options.element`. function createInput() { var input = document.createElement("input"); input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId); input.setAttribute("title", "file input"); self.setMultiple(options.multiple, input); if (options.folders && qq.supportedFeatures.folderSelection) { // selecting directories is only possible in Chrome now, via a vendor-specific prefixed attribute input.setAttribute("webkitdirectory", ""); } 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", // It's especially important to make this an arbitrarily large value // to ensure the rendered input button in IE takes up the entire // space of the container element. Otherwise, the left side of the // button will require a double-click to invoke the file chooser. // In other browsers, this might cause other issues, so a large font-size // is only used in IE. There is a bug in IE8 where the opacity style is ignored // in some cases when the font-size is large. So, this workaround is not applied // to IE8. fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px", margin: 0, padding: 0, cursor: "pointer", opacity: 0 }); // Setting the file input's height to 100% in IE7 causes // most of the visible button to be unclickable. !qq.ie7() && qq(input).css({height: "100%"}); options.element.appendChild(input); disposeSupport.attach(input, "change", function() { options.onChange(input); }); // **These event handlers will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers 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); }); return input; } // 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" }); // Exposed API qq.extend(this, { getInput: function() { return input; }, getButtonId: function() { return buttonId; }, setMultiple: function(isMultiple, optInput) { var input = optInput || this.getInput(); // Temporary workaround for bug in in iOS8 UIWebView that causes the browser to crash // before the file chooser appears if the file input doesn't contain a multiple attribute. // See #1283. if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { input.setAttribute("multiple", ""); } else { if (isMultiple) { input.setAttribute("multiple", ""); } else { input.removeAttribute("multiple"); } } }, setAcceptFiles: function(acceptFiles) { if (acceptFiles !== options.acceptFiles) { input.setAttribute("accept", acceptFiles); } }, reset: function() { if (input.parentNode) { qq(input).remove(); } qq(options.element).removeClass(options.focusClass); input = null; input = createInput(); } }); input = createInput(); }; qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id"; /*globals qq */ qq.UploadData = function(uploaderProxy) { "use strict"; var data = [], byUuid = {}, byStatus = {}, byProxyGroupId = {}, byBatchId = {}; function getDataByIds(idOrIds) { if (qq.isArray(idOrIds)) { var entries = []; qq.each(idOrIds, function(idx, id) { entries.push(data[id]); }); return entries; } return data[idOrIds]; } 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; } qq.extend(this, { /** * Adds a new file to the data cache for tracking purposes. * * @param spec Data that describes this file. Possible properties are: * * - uuid: Initial UUID for this file. * - name: Initial name of this file. * - size: Size of this file, omit if this cannot be determined * - status: Initial `qq.status` for this file. Omit for `qq.status.SUBMITTING`. * - batchId: ID of the batch this file belongs to * - proxyGroupId: ID of the proxy group associated with this file * * @returns {number} Internal ID for this file. */ addFile: function(spec) { var status = spec.status || qq.status.SUBMITTING, id = data.push({ name: spec.name, originalName: spec.name, uuid: spec.uuid, size: spec.size == null ? -1 : spec.size, status: status }) - 1; if (spec.batchId) { data[id].batchId = spec.batchId; if (byBatchId[spec.batchId] === undefined) { byBatchId[spec.batchId] = []; } byBatchId[spec.batchId].push(id); } if (spec.proxyGroupId) { data[id].proxyGroupId = spec.proxyGroupId; if (byProxyGroupId[spec.proxyGroupId] === undefined) { byProxyGroupId[spec.proxyGroupId] = []; } byProxyGroupId[spec.proxyGroupId].push(id); } data[id].id = id; byUuid[spec.uuid] = id; if (byStatus[status] === undefined) { byStatus[status] = []; } byStatus[status].push(id); uploaderProxy.onStatusChange(id, null, status); return id; }, 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 = []; byUuid = {}; byStatus = {}; byBatchId = {}; }, setStatus: function(id, newStatus) { var oldStatus = data[id].status, byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id); byStatus[oldStatus].splice(byStatusOldStatusIndex, 1); data[id].status = newStatus; if (byStatus[newStatus] === undefined) { byStatus[newStatus] = []; } byStatus[newStatus].push(id); uploaderProxy.onStatusChange(id, oldStatus, newStatus); }, uuidChanged: function(id, newUuid) { var oldUuid = data[id].uuid; data[id].uuid = newUuid; byUuid[newUuid] = id; delete byUuid[oldUuid]; }, updateName: function(id, newName) { data[id].name = newName; }, updateSize: function(id, newSize) { data[id].size = newSize; }, // Only applicable if this file has a parent that we may want to reference later. setParentId: function(targetId, parentId) { data[targetId].parentId = parentId; }, getIdsInProxyGroup: function(id) { var proxyGroupId = data[id].proxyGroupId; if (proxyGroupId) { return byProxyGroupId[proxyGroupId]; } return []; }, getIdsInBatch: function(id) { var batchId = data[id].batchId; return byBatchId[batchId]; } }); }; qq.status = { SUBMITTING: "submitting", SUBMITTED: "submitted", REJECTED: "rejected", QUEUED: "queued", CANCELED: "canceled", PAUSED: "paused", UPLOADING: "uploading", UPLOAD_RETRYING: "retrying upload", UPLOAD_SUCCESSFUL: "upload successful", UPLOAD_FAILED: "upload failed", DELETE_FAILED: "delete failed", DELETING: "deleting", DELETED: "deleted" }; /*globals qq*/ /** * Defines the public API for FineUploaderBasic mode. */ (function() { "use strict"; qq.basePublicApi = { // DEPRECATED - TODO REMOVE IN NEXT MAJOR RELEASE (replaced by addFiles) addBlobs: function(blobDataOrArray, params, endpoint) { this.addFiles(blobDataOrArray, params, endpoint); }, addFiles: function(data, params, endpoint) { this._maybeHandleIos8Sa