collective-fine-upload
Version:
Upload assets to Collective with Fine Uploader
1,579 lines (1,296 loc) • 389 kB
JavaScript
/*!
* 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