/*!
|
* Fine Uploader
|
*
|
* Copyright 2013, Widen Enterprises, Inc. info@fineuploader.com
|
*
|
* Version: 3.7.1
|
*
|
* Homepage: http://fineuploader.com
|
*
|
* Repository: git://github.com/Widen/fine-uploader.git
|
*
|
* Licensed under GNU GPL v3, see LICENSE
|
*/
|
|
|
/*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) {
|
// 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) {
|
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 && !variable.nodeType && Object.prototype.toString.call(variable) === '[object Object]';
|
};
|
|
qq.isFunction = function(variable) {
|
"use strict";
|
return typeof(variable) === "function";
|
};
|
|
qq.isArray = function(variable) {
|
"use strict";
|
return Object.prototype.toString.call(variable) === "[object Array]";
|
}
|
|
qq.isString = function(maybeString) {
|
"use strict";
|
return Object.prototype.toString.call(maybeString) === '[object String]';
|
};
|
|
qq.trimStr = function(string) {
|
if (String.prototype.trim) {
|
return string.trim();
|
}
|
|
return string.replace(/^\s+|\s+$/g,'');
|
};
|
|
|
// Returns a string, swapping argument values with the associated occurrence of {} in the passed string.
|
qq.format = function(str) {
|
"use strict";
|
|
var args = Array.prototype.slice.call(arguments, 1),
|
newStr = str;
|
|
qq.each(args, function(idx, val) {
|
newStr = newStr.replace(/{}/, val);
|
});
|
|
return newStr;
|
};
|
|
qq.isFile = function(maybeFile) {
|
"use strict";
|
|
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) {
|
"use strict";
|
|
return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput);
|
};
|
|
qq.isInput = function(maybeInput) {
|
if (window.HTMLInputElement) {
|
if (Object.prototype.toString.call(maybeInput) === '[object HTMLInputElement]') {
|
if (maybeInput.type && maybeInput.type.toLowerCase() === 'file') {
|
return true;
|
}
|
}
|
}
|
if (maybeInput.tagName) {
|
if (maybeInput.tagName.toLowerCase() === 'input') {
|
if (maybeInput.type && maybeInput.type.toLowerCase() === 'file') {
|
return true;
|
}
|
}
|
}
|
|
return false;
|
};
|
|
qq.isBlob = function(maybeBlob) {
|
"use strict";
|
return window.Blob && Object.prototype.toString.call(maybeBlob) === '[object Blob]';
|
};
|
|
qq.isXhrUploadSupported = function() {
|
"use strict";
|
var input = document.createElement('input');
|
input.type = 'file';
|
|
return (
|
input.multiple !== undefined &&
|
typeof File !== "undefined" &&
|
typeof FormData !== "undefined" &&
|
typeof (new XMLHttpRequest()).upload !== "undefined" );
|
};
|
|
qq.isFolderDropSupported = function(dataTransfer) {
|
"use strict";
|
return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry);
|
};
|
|
qq.isFileChunkingSupported = function() {
|
"use strict";
|
return !qq.android() && //android's impl of Blob.slice is broken
|
qq.isXhrUploadSupported() &&
|
(File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined);
|
};
|
|
qq.extend = function (first, second, extendNested) {
|
"use strict";
|
|
qq.each(second, function(prop, val) {
|
if (extendNested && qq.isObject(val)) {
|
if (first[prop] === undefined) {
|
first[prop] = {};
|
}
|
qq.extend(first[prop], val, true);
|
}
|
else {
|
first[prop] = val;
|
}
|
});
|
|
return first;
|
};
|
|
/**
|
* Searches for a given element in the array, returns -1 if it is not present.
|
* @param {Number} [from] The index at which to begin the search
|
*/
|
qq.indexOf = function(arr, elt, from){
|
"use strict";
|
|
if (arr.indexOf) {
|
return arr.indexOf(elt, from);
|
}
|
|
from = from || 0;
|
var len = arr.length;
|
|
if (from < 0) {
|
from += len;
|
}
|
|
for (; from < len; from+=1){
|
if (arr.hasOwnProperty(from) && arr[from] === elt){
|
return from;
|
}
|
}
|
return -1;
|
};
|
|
//this is a version 4 UUID
|
qq.getUniqueId = function(){
|
"use strict";
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
/*jslint eqeq: true, bitwise: true*/
|
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
|
return v.toString(16);
|
});
|
};
|
|
//
|
// Browsers and platforms detection
|
|
qq.ie = function(){
|
"use strict";
|
return navigator.userAgent.indexOf('MSIE') !== -1;
|
};
|
qq.ie10 = function(){
|
"use strict";
|
return navigator.userAgent.indexOf('MSIE 10') !== -1;
|
};
|
qq.safari = function(){
|
"use strict";
|
return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1;
|
};
|
qq.chrome = function(){
|
"use strict";
|
return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1;
|
};
|
qq.firefox = function(){
|
"use strict";
|
return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === '');
|
};
|
qq.windows = function(){
|
"use strict";
|
return navigator.platform === "Win32";
|
};
|
qq.android = function(){
|
"use strict";
|
return navigator.userAgent.toLowerCase().indexOf('android') !== -1;
|
};
|
qq.ios = function() {
|
"use strict";
|
return navigator.userAgent.indexOf("iPad") !== -1
|
|| navigator.userAgent.indexOf("iPod") !== -1
|
|| navigator.userAgent.indexOf("iPhone") !== -1;
|
};
|
|
//
|
// Events
|
|
qq.preventDefault = function(e){
|
"use strict";
|
if (e.preventDefault){
|
e.preventDefault();
|
} else{
|
e.returnValue = false;
|
}
|
};
|
|
/**
|
* Creates and returns element from html string
|
* Uses innerHTML to create an element
|
*/
|
qq.toElement = (function(){
|
"use strict";
|
var div = document.createElement('div');
|
return function(html){
|
div.innerHTML = html;
|
var element = div.firstChild;
|
div.removeChild(element);
|
return element;
|
};
|
}());
|
|
//key and value are passed to callback for each item in the object or array
|
qq.each = function(objOrArray, callback) {
|
"use strict";
|
var keyOrIndex, retVal;
|
if (objOrArray) {
|
if (qq.isArray(objOrArray)) {
|
for (keyOrIndex = 0; keyOrIndex < objOrArray.length; keyOrIndex++) {
|
retVal = callback(keyOrIndex, objOrArray[keyOrIndex]);
|
if (retVal === false) {
|
break;
|
}
|
}
|
}
|
else {
|
for (keyOrIndex in objOrArray) {
|
if (Object.prototype.hasOwnProperty.call(objOrArray, keyOrIndex)) {
|
retVal = callback(keyOrIndex, objOrArray[keyOrIndex]);
|
if (retVal === false) {
|
break;
|
}
|
}
|
}
|
}
|
}
|
};
|
|
//include any args that should be passed to the new function after the context arg
|
qq.bind = function(oldFunc, context) {
|
if (qq.isFunction(oldFunc)) {
|
var args = Array.prototype.slice.call(arguments, 2);
|
|
return function() {
|
if (arguments.length) {
|
args = args.concat(Array.prototype.slice.call(arguments))
|
}
|
return oldFunc.apply(context, args);
|
};
|
}
|
|
throw new Error("first parameter must be a function!");
|
};
|
|
/**
|
* obj2url() takes a json-object as argument and generates
|
* a querystring. pretty much like jQuery.param()
|
*
|
* how to use:
|
*
|
* `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');`
|
*
|
* will result in:
|
*
|
* `http://any.url/upload?otherParam=value&a=b&c=d`
|
*
|
* @param Object JSON-Object
|
* @param String current querystring-part
|
* @return String encoded querystring
|
*/
|
qq.obj2url = function(obj, temp, prefixDone){
|
"use strict";
|
/*jshint laxbreak: true*/
|
var i, len,
|
uristrings = [],
|
prefix = '&',
|
add = function(nextObj, i){
|
var nextTemp = temp
|
? (/\[\]$/.test(temp)) // prevent double-encoding
|
? temp
|
: temp+'['+i+']'
|
: i;
|
if ((nextTemp !== 'undefined') && (i !== 'undefined')) {
|
uristrings.push(
|
(typeof nextObj === 'object')
|
? qq.obj2url(nextObj, nextTemp, true)
|
: (Object.prototype.toString.call(nextObj) === '[object Function]')
|
? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj())
|
: encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)
|
);
|
}
|
};
|
|
if (!prefixDone && temp) {
|
prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?';
|
uristrings.push(temp);
|
uristrings.push(qq.obj2url(obj));
|
} else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) {
|
// we wont use a for-in-loop on an array (performance)
|
for (i = -1, len = obj.length; i < len; i+=1){
|
add(obj[i], i);
|
}
|
} else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){
|
// for anything else but a scalar, we will use for-in-loop
|
for (i in obj){
|
if (obj.hasOwnProperty(i)) {
|
add(obj[i], i);
|
}
|
}
|
} else {
|
uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj));
|
}
|
|
if (temp) {
|
return uristrings.join(prefix);
|
} else {
|
return uristrings.join(prefix)
|
.replace(/^&/, '')
|
.replace(/%20/g, '+');
|
}
|
};
|
|
qq.obj2FormData = function(obj, formData, arrayKeyName) {
|
"use strict";
|
if (!formData) {
|
formData = new FormData();
|
}
|
|
qq.each(obj, function(key, val) {
|
key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key;
|
|
if (qq.isObject(val)) {
|
qq.obj2FormData(val, formData, key);
|
}
|
else if (qq.isFunction(val)) {
|
formData.append(key, val());
|
}
|
else {
|
formData.append(key, val);
|
}
|
});
|
|
return formData;
|
};
|
|
qq.obj2Inputs = function(obj, form) {
|
"use strict";
|
var input;
|
|
if (!form) {
|
form = document.createElement('form');
|
}
|
|
qq.obj2FormData(obj, {
|
append: function(key, val) {
|
input = document.createElement('input');
|
input.setAttribute('name', key);
|
input.setAttribute('value', val);
|
form.appendChild(input);
|
}
|
});
|
|
return form;
|
};
|
|
qq.setCookie = function(name, value, days) {
|
var date = new Date(),
|
expires = "";
|
|
if (days) {
|
date.setTime(date.getTime()+(days*24*60*60*1000));
|
expires = "; expires="+date.toGMTString();
|
}
|
|
document.cookie = name+"="+value+expires+"; path=/";
|
};
|
|
qq.getCookie = function(name) {
|
var nameEQ = name + "=",
|
ca = document.cookie.split(';'),
|
cookie;
|
|
qq.each(ca, function(idx, part) {
|
var cookiePart = part;
|
while (cookiePart.charAt(0)==' ') {
|
cookiePart = cookiePart.substring(1, cookiePart.length);
|
}
|
|
if (cookiePart.indexOf(nameEQ) === 0) {
|
cookie = cookiePart.substring(nameEQ.length, cookiePart.length);
|
return false;
|
}
|
});
|
|
return cookie;
|
};
|
|
qq.getCookieNames = function(regexp) {
|
var cookies = document.cookie.split(';'),
|
cookieNames = [];
|
|
qq.each(cookies, function(idx, cookie) {
|
cookie = qq.trimStr(cookie);
|
|
var equalsIdx = cookie.indexOf("=");
|
|
if (cookie.match(regexp)) {
|
cookieNames.push(cookie.substr(0, equalsIdx));
|
}
|
});
|
|
return cookieNames;
|
};
|
|
qq.deleteCookie = function(name) {
|
qq.setCookie(name, "", -1);
|
};
|
|
qq.areCookiesEnabled = function() {
|
var randNum = Math.random() * 100000,
|
name = "qqCookieTest:" + randNum;
|
qq.setCookie(name, 1);
|
|
if (qq.getCookie(name)) {
|
qq.deleteCookie(name);
|
return true;
|
}
|
return false;
|
};
|
|
/**
|
* Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not
|
* implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js.
|
*/
|
qq.parseJson = function(json) {
|
/*jshint evil: true*/
|
if (window.JSON && qq.isFunction(JSON.parse)) {
|
return JSON.parse(json);
|
} else {
|
return eval("(" + json + ")");
|
}
|
};
|
|
/**
|
* A generic module which supports object disposing in dispose() method.
|
* */
|
qq.DisposeSupport = function() {
|
"use strict";
|
var disposers = [];
|
|
return {
|
/** Run all registered disposers */
|
dispose: function() {
|
var disposer;
|
do {
|
disposer = disposers.shift();
|
if (disposer) {
|
disposer();
|
}
|
}
|
while (disposer);
|
},
|
|
/** Attach event handler and register de-attacher as a disposer */
|
attach: function() {
|
var args = arguments;
|
/*jslint undef:true*/
|
this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1)));
|
},
|
|
/** Add disposer to the collection */
|
addDisposer: function(disposeFunction) {
|
disposers.push(disposeFunction);
|
}
|
};
|
};
|
;qq.version="3.7.1";;qq.supportedFeatures = (function () {
|
var supportsUploading,
|
supportsAjaxFileUploading,
|
supportsFolderDrop,
|
supportsChunking,
|
supportsResume,
|
supportsUploadViaPaste,
|
supportsUploadCors,
|
supportsDeleteFileXdr,
|
supportsDeleteFileCorsXhr,
|
supportsDeleteFileCors;
|
|
|
function testSupportsFileInputElement() {
|
var supported = true,
|
tempInput;
|
|
try {
|
tempInput = document.createElement('input');
|
tempInput.type = 'file';
|
qq(tempInput).hide();
|
|
if (tempInput.disabled) {
|
supported = false;
|
}
|
}
|
catch (ex) {
|
supported = false;
|
}
|
|
return supported;
|
}
|
|
//only way to test for Filesystem API support since webkit does not expose the DataTransfer interface
|
function isChrome21OrHigher() {
|
return qq.chrome() &&
|
navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined;
|
}
|
|
//only way to test for complete Clipboard API support at this time
|
function isChrome14OrHigher() {
|
return qq.chrome() &&
|
navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined;
|
}
|
|
//Ensure we can send cross-origin `XMLHttpRequest`s
|
function isCrossOriginXhrSupported() {
|
if (window.XMLHttpRequest) {
|
var xhr = new XMLHttpRequest();
|
|
//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();
|
}
|
|
|
supportsUploading = testSupportsFileInputElement();
|
|
supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported();
|
|
supportsFolderDrop = supportsAjaxFileUploading && isChrome21OrHigher();
|
|
supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported();
|
|
supportsResume = supportsAjaxFileUploading && supportsChunking && qq.areCookiesEnabled();
|
|
supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher();
|
|
supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading);
|
|
supportsDeleteFileCorsXhr = isCrossOriginXhrSupported();
|
|
supportsDeleteFileXdr = isXdrSupported();
|
|
supportsDeleteFileCors = isCrossOriginAjaxSupported();
|
|
|
return {
|
uploading: supportsUploading,
|
ajaxUploading: supportsAjaxFileUploading,
|
fileDrop: supportsAjaxFileUploading, //NOTE: will also return true for touch-only devices. It's not currently possible to accurately test for touch-only devices
|
folderDrop: supportsFolderDrop,
|
chunking: supportsChunking,
|
resume: supportsResume,
|
uploadCustomHeaders: supportsAjaxFileUploading,
|
uploadNonMultipart: supportsAjaxFileUploading,
|
itemSizeValidation: supportsAjaxFileUploading,
|
uploadViaPaste: supportsUploadViaPaste,
|
progressBar: supportsAjaxFileUploading,
|
uploadCors: supportsUploadCors,
|
deleteFileCorsXhr: supportsDeleteFileCorsXhr,
|
deleteFileCorsXdr: supportsDeleteFileXdr, //NOTE: will also return true in IE10, where XDR is also supported
|
deleteFileCors: supportsDeleteFileCors,
|
canDetermineSize: supportsAjaxFileUploading
|
}
|
|
}());
|
;/*globals qq*/
|
qq.Promise = function() {
|
"use strict";
|
|
var successValue, failureValue,
|
successCallbacks = [],
|
failureCallbacks = [],
|
doneCallbacks = [],
|
state = 0;
|
|
return {
|
then: function(onSuccess, onFailure) {
|
if (state === 0) {
|
if (onSuccess) {
|
successCallbacks.push(onSuccess);
|
}
|
if (onFailure) {
|
failureCallbacks.push(onFailure);
|
}
|
}
|
else if (state === -1 && onFailure) {
|
onFailure(failureValue);
|
}
|
else if (onSuccess) {
|
onSuccess(successValue);
|
}
|
|
return this;
|
},
|
|
done: function(callback) {
|
if (state === 0) {
|
doneCallbacks.push(callback);
|
}
|
else {
|
callback();
|
}
|
|
return this;
|
},
|
|
success: function(val) {
|
state = 1;
|
successValue = val;
|
|
if (successCallbacks.length) {
|
qq.each(successCallbacks, function(idx, callback) {
|
callback(val);
|
})
|
}
|
|
if(doneCallbacks.length) {
|
qq.each(doneCallbacks, function(idx, callback) {
|
callback();
|
})
|
}
|
|
return this;
|
},
|
|
failure: function(val) {
|
state = -1;
|
failureValue = val;
|
|
if (failureCallbacks.length) {
|
qq.each(failureCallbacks, function(idx, callback) {
|
callback(val);
|
})
|
}
|
|
if(doneCallbacks.length) {
|
qq.each(doneCallbacks, function(idx, callback) {
|
callback();
|
})
|
}
|
|
return this;
|
}
|
};
|
};
|
|
qq.isPromise = function(maybePromise) {
|
return maybePromise && maybePromise.then && maybePromise.done;
|
};;/*globals qq*/
|
|
/**
|
* 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 input,
|
// Used to detach all event handlers created at once for this instance
|
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,
|
|
// `name` attribute of `<input type="file">`
|
name: 'qqfile',
|
|
// Called when the browser invokes the onchange handler on the `<input type="file">`
|
onChange: function(input) {},
|
|
// **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'
|
};
|
|
// Overrides any of the default option values with any option values passed in during construction.
|
qq.extend(options, o);
|
|
|
// Embed an opaque `<input type="file">` element as a child of `options.element`.
|
function createInput() {
|
var input = document.createElement("input");
|
|
if (options.multiple){
|
input.setAttribute("multiple", "multiple");
|
}
|
|
if (options.acceptFiles) {
|
input.setAttribute("accept", options.acceptFiles);
|
}
|
|
input.setAttribute("type", "file");
|
input.setAttribute("name", options.name);
|
|
qq(input).css({
|
position: 'absolute',
|
// in Opera only 'browse' button
|
// is clickable and it is located at
|
// the right side of the input
|
right: 0,
|
top: 0,
|
fontFamily: 'Arial',
|
// 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
|
fontSize: '118px',
|
margin: 0,
|
padding: 0,
|
cursor: 'pointer',
|
opacity: 0
|
});
|
|
options.element.appendChild(input);
|
|
disposeSupport.attach(input, 'change', function(){
|
options.onChange(input);
|
});
|
|
// **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);
|
});
|
|
// 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;
|
}
|
|
// Make button suitable container for input
|
qq(options.element).css({
|
position: 'relative',
|
overflow: 'hidden',
|
// Make sure browse button is in the right side in Internet Explorer
|
direction: 'ltr'
|
});
|
|
input = createInput();
|
|
|
// Exposed API
|
return {
|
getInput: function(){
|
return input;
|
},
|
|
reset: function(){
|
if (input.parentNode){
|
qq(input).remove();
|
}
|
|
qq(options.element).removeClass(options.focusClass);
|
input = createInput();
|
}
|
};
|
};
|
;/*globals qq*/
|
qq.PasteSupport = function(o) {
|
"use strict";
|
|
var options, detachPasteHandler;
|
|
options = {
|
targetElement: null,
|
callbacks: {
|
log: function(message, level) {},
|
pasteReceived: function(blob) {}
|
}
|
};
|
|
function isImage(item) {
|
return item.type &&
|
item.type.indexOf("image/") === 0;
|
}
|
|
function registerPasteHandler() {
|
qq(options.targetElement).attach("paste", function(event) {
|
var clipboardData = event.clipboardData;
|
|
if (clipboardData) {
|
qq.each(clipboardData.items, function(idx, item) {
|
if (isImage(item)) {
|
var blob = item.getAsFile();
|
options.callbacks.pasteReceived(blob);
|
}
|
});
|
}
|
});
|
}
|
|
function unregisterPasteHandler() {
|
if (detachPasteHandler) {
|
detachPasteHandler();
|
}
|
}
|
|
qq.extend(options, o);
|
registerPasteHandler();
|
|
return {
|
reset: function() {
|
unregisterPasteHandler();
|
}
|
};
|
};;qq.UploadData = function(uploaderProxy) {
|
var data = [],
|
byId = {},
|
byUuid = {},
|
byStatus = {},
|
api;
|
|
function getDataByIds(ids) {
|
if (qq.isArray(ids)) {
|
var entries = [];
|
|
qq.each(ids, function(idx, id) {
|
entries.push(data[byId[id]]);
|
});
|
|
return entries;
|
}
|
|
return data[byId[ids]];
|
}
|
|
function getDataByUuids(uuids) {
|
if (qq.isArray(uuids)) {
|
var entries = [];
|
|
qq.each(uuids, function(idx, uuid) {
|
entries.push(data[byUuid[uuid]]);
|
});
|
|
return entries;
|
}
|
|
return data[byUuid[uuids]];
|
}
|
|
function getDataByStatus(status) {
|
var statusResults = [],
|
statuses = [].concat(status);
|
|
qq.each(statuses, function(index, statusEnum) {
|
var statusResultIndexes = byStatus[statusEnum];
|
|
if (statusResultIndexes !== undefined) {
|
qq.each(statusResultIndexes, function(i, dataIndex) {
|
statusResults.push(data[dataIndex]);
|
});
|
}
|
});
|
|
return statusResults;
|
}
|
|
api = {
|
added: function(id) {
|
var uuid = uploaderProxy.getUuid(id),
|
name = uploaderProxy.getName(id),
|
size = uploaderProxy.getSize(id),
|
status = qq.status.SUBMITTING;
|
|
var index = data.push({
|
id: id,
|
name: name,
|
originalName: name,
|
uuid: uuid,
|
size: size,
|
status: status
|
}) - 1;
|
|
byId[id] = index;
|
|
byUuid[uuid] = index;
|
|
if (byStatus[status] === undefined) {
|
byStatus[status] = [];
|
}
|
byStatus[status].push(index);
|
|
uploaderProxy.onStatusChange(id, undefined, status);
|
},
|
|
retrieve: function(optionalFilter) {
|
if (qq.isObject(optionalFilter) && data.length) {
|
if (optionalFilter.id !== undefined) {
|
return getDataByIds(optionalFilter.id);
|
}
|
|
else if (optionalFilter.uuid !== undefined) {
|
return getDataByUuids(optionalFilter.uuid);
|
}
|
|
else if (optionalFilter.status) {
|
return getDataByStatus(optionalFilter.status);
|
}
|
}
|
else {
|
return qq.extend([], data, true);
|
}
|
},
|
|
reset: function() {
|
data = [];
|
byId = {};
|
byUuid = {};
|
byStatus = {};
|
},
|
|
setStatus: function(id, newStatus) {
|
var dataIndex = byId[id],
|
oldStatus = data[dataIndex].status,
|
byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], dataIndex);
|
|
byStatus[oldStatus].splice(byStatusOldStatusIndex, 1);
|
|
data[dataIndex].status = newStatus;
|
|
if (byStatus[newStatus] === undefined) {
|
byStatus[newStatus] = [];
|
}
|
byStatus[newStatus].push(dataIndex);
|
|
uploaderProxy.onStatusChange(id, oldStatus, newStatus);
|
},
|
|
uuidChanged: function(id, newUuid) {
|
var dataIndex = byId[id],
|
oldUuid = data[dataIndex].uuid;
|
|
data[dataIndex].uuid = newUuid;
|
byUuid[newUuid] = dataIndex;
|
delete byUuid[oldUuid];
|
},
|
|
nameChanged: function(id, newName) {
|
var dataIndex = byId[id];
|
|
data[dataIndex].name = newName;
|
}
|
};
|
|
return api;
|
};
|
|
qq.status = {
|
SUBMITTING: "submitting",
|
SUBMITTED: "submitted",
|
REJECTED: "rejected",
|
QUEUED: "queued",
|
CANCELED: "canceled",
|
UPLOADING: "uploading",
|
UPLOAD_RETRYING: "retrying upload",
|
UPLOAD_SUCCESSFUL: "upload successful",
|
UPLOAD_FAILED: "upload failed",
|
DELETE_FAILED: "delete failed",
|
DELETING: "deleting",
|
DELETED: "deleted"
|
};
|
;qq.FineUploaderBasic = function(o) {
|
this._options = {
|
debug: false,
|
button: null,
|
multiple: true,
|
maxConnections: 3,
|
disableCancelForFormUploads: false,
|
autoUpload: true,
|
request: {
|
endpoint: '/server/upload',
|
params: {},
|
paramsInBody: true,
|
customHeaders: {},
|
forceMultipart: true,
|
inputName: 'qqfile',
|
uuidName: 'qquuid',
|
totalFileSizeName: 'qqtotalfilesize',
|
filenameParam: 'qqfilename'
|
},
|
validation: {
|
allowedExtensions: [],
|
sizeLimit: 0,
|
minSizeLimit: 0,
|
itemLimit: 0,
|
stopOnFirstInvalidFile: true,
|
acceptFiles: null
|
},
|
callbacks: {
|
onSubmit: function(id, name){},
|
onSubmitted: function(id, name){},
|
onComplete: function(id, name, responseJSON, maybeXhr){},
|
onCancel: function(id, name){},
|
onUpload: function(id, name){},
|
onUploadChunk: function(id, name, chunkData){},
|
onResume: function(id, fileName, chunkData){},
|
onProgress: function(id, name, loaded, total){},
|
onError: function(id, name, reason, maybeXhrOrXdr) {},
|
onAutoRetry: function(id, name, attemptNumber) {},
|
onManualRetry: function(id, name) {},
|
onValidateBatch: function(fileOrBlobData) {},
|
onValidate: function(fileOrBlobData) {},
|
onSubmitDelete: function(id) {},
|
onDelete: function(id){},
|
onDeleteComplete: function(id, xhrOrXdr, isError){},
|
onPasteReceived: function(blob) {},
|
onStatusChange: function(id, oldStatus, newStatus) {}
|
},
|
messages: {
|
typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.",
|
sizeError: "{file} is too large, maximum file size is {sizeLimit}.",
|
minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.",
|
emptyError: "{file} is empty, please select files again without it.",
|
noFilesError: "No files to upload.",
|
tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.",
|
retryFailTooManyItems: "Retry failed - you have reached your file limit.",
|
onLeave: "The files are being uploaded, if you leave now the upload will be cancelled."
|
},
|
retry: {
|
enableAuto: false,
|
maxAutoAttempts: 3,
|
autoAttemptDelay: 5,
|
preventRetryResponseProperty: 'preventRetry'
|
},
|
classes: {
|
buttonHover: 'qq-upload-button-hover',
|
buttonFocus: 'qq-upload-button-focus'
|
},
|
chunking: {
|
enabled: false,
|
partSize: 2000000,
|
paramNames: {
|
partIndex: 'qqpartindex',
|
partByteOffset: 'qqpartbyteoffset',
|
chunkSize: 'qqchunksize',
|
totalFileSize: 'qqtotalfilesize',
|
totalParts: 'qqtotalparts'
|
}
|
},
|
resume: {
|
enabled: false,
|
id: null,
|
cookiesExpireIn: 7, //days
|
paramNames: {
|
resuming: "qqresume"
|
}
|
},
|
formatFileName: function(fileOrBlobName) {
|
if (fileOrBlobName !== undefined && fileOrBlobName.length > 33) {
|
fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14);
|
}
|
return fileOrBlobName;
|
},
|
text: {
|
defaultResponseError: "Upload failure reason unknown",
|
sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB']
|
},
|
deleteFile : {
|
enabled: false,
|
method: "DELETE",
|
endpoint: '/server/upload',
|
customHeaders: {},
|
params: {}
|
},
|
cors: {
|
expected: false,
|
sendCredentials: false,
|
allowXdr: false
|
},
|
blobs: {
|
defaultName: 'misc_data'
|
},
|
paste: {
|
targetElement: null,
|
defaultName: 'pasted_image'
|
},
|
camera: {
|
ios: false
|
}
|
};
|
|
qq.extend(this._options, o, true);
|
|
this._handleCameraAccess();
|
|
this._wrapCallbacks();
|
this._disposeSupport = new qq.DisposeSupport();
|
|
this._filesInProgress = [];
|
this._storedIds = [];
|
this._autoRetries = [];
|
this._retryTimeouts = [];
|
this._preventRetries = [];
|
|
this._netUploadedOrQueued = 0;
|
this._netUploaded = 0;
|
this._uploadData = this._createUploadDataTracker();
|
|
this._paramsStore = this._createParamsStore("request");
|
this._deleteFileParamsStore = this._createParamsStore("deleteFile");
|
|
this._endpointStore = this._createEndpointStore("request");
|
this._deleteFileEndpointStore = this._createEndpointStore("deleteFile");
|
|
this._handler = this._createUploadHandler();
|
this._deleteHandler = this._createDeleteHandler();
|
|
if (this._options.button){
|
this._button = this._createUploadButton(this._options.button);
|
}
|
|
if (this._options.paste.targetElement) {
|
this._pasteHandler = this._createPasteHandler();
|
}
|
|
this._preventLeaveInProgress();
|
};
|
|
qq.FineUploaderBasic.prototype = {
|
log: function(str, level) {
|
if (this._options.debug && (!level || level === 'info')) {
|
qq.log('[FineUploader ' + qq.version + '] ' + str);
|
}
|
else if (level && level !== 'info') {
|
qq.log('[FineUploader ' + qq.version + '] ' + str, level);
|
|
}
|
},
|
setParams: function(params, id) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (id == null) {
|
this._options.request.params = params;
|
}
|
else {
|
this._paramsStore.setParams(params, id);
|
}
|
},
|
setDeleteFileParams: function(params, id) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (id == null) {
|
this._options.deleteFile.params = params;
|
}
|
else {
|
this._deleteFileParamsStore.setParams(params, id);
|
}
|
},
|
setEndpoint: function(endpoint, id) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (id == null) {
|
this._options.request.endpoint = endpoint;
|
}
|
else {
|
this._endpointStore.setEndpoint(endpoint, id);
|
}
|
},
|
getInProgress: function() {
|
return this._filesInProgress.length;
|
},
|
getNetUploads: function() {
|
return this._netUploaded;
|
},
|
uploadStoredFiles: function() {
|
var idToUpload;
|
|
if (this._storedIds.length === 0) {
|
this._itemError('noFilesError');
|
}
|
else {
|
while (this._storedIds.length) {
|
idToUpload = this._storedIds.shift();
|
this._filesInProgress.push(idToUpload);
|
this._handler.upload(idToUpload);
|
}
|
}
|
},
|
clearStoredFiles: function(){
|
this._storedIds = [];
|
},
|
retry: function(id) {
|
if (this._onBeforeManualRetry(id)) {
|
this._netUploadedOrQueued++;
|
this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
|
this._handler.retry(id);
|
return true;
|
}
|
else {
|
return false;
|
}
|
},
|
cancel: function(id) {
|
this._handler.cancel(id);
|
},
|
cancelAll: function() {
|
var storedIdsCopy = [],
|
self = this;
|
|
qq.extend(storedIdsCopy, this._storedIds);
|
qq.each(storedIdsCopy, function(idx, storedFileId) {
|
self.cancel(storedFileId);
|
});
|
|
this._handler.cancelAll();
|
},
|
reset: function() {
|
this.log("Resetting uploader...");
|
|
this._handler.reset();
|
this._filesInProgress = [];
|
this._storedIds = [];
|
this._autoRetries = [];
|
this._retryTimeouts = [];
|
this._preventRetries = [];
|
this._button.reset();
|
this._paramsStore.reset();
|
this._endpointStore.reset();
|
this._netUploadedOrQueued = 0;
|
this._netUploaded = 0;
|
this._uploadData.reset();
|
|
if (this._pasteHandler) {
|
this._pasteHandler.reset();
|
}
|
},
|
addFiles: function(filesOrInputs, params, endpoint) {
|
var self = this,
|
verifiedFilesOrInputs = [],
|
fileOrInputIndex, fileOrInput, fileIndex;
|
|
if (filesOrInputs) {
|
if (!qq.isFileList(filesOrInputs)) {
|
filesOrInputs = [].concat(filesOrInputs);
|
}
|
|
for (fileOrInputIndex = 0; fileOrInputIndex < filesOrInputs.length; fileOrInputIndex+=1) {
|
fileOrInput = filesOrInputs[fileOrInputIndex];
|
|
if (qq.isFileOrInput(fileOrInput)) {
|
if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) {
|
for (fileIndex = 0; fileIndex < fileOrInput.files.length; fileIndex++) {
|
verifiedFilesOrInputs.push(fileOrInput.files[fileIndex]);
|
}
|
}
|
else {
|
verifiedFilesOrInputs.push(fileOrInput);
|
}
|
}
|
else {
|
self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn');
|
}
|
}
|
|
this.log('Received ' + verifiedFilesOrInputs.length + ' files or inputs.');
|
this._prepareItemsForUpload(verifiedFilesOrInputs, params, endpoint);
|
}
|
},
|
addBlobs: function(blobDataOrArray, params, endpoint) {
|
if (blobDataOrArray) {
|
var blobDataArray = [].concat(blobDataOrArray),
|
verifiedBlobDataList = [],
|
self = this;
|
|
qq.each(blobDataArray, function(idx, blobData) {
|
if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) {
|
verifiedBlobDataList.push({
|
blob: blobData,
|
name: self._options.blobs.defaultName
|
});
|
}
|
else if (qq.isObject(blobData) && blobData.blob && blobData.name) {
|
verifiedBlobDataList.push(blobData);
|
}
|
else {
|
self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error");
|
}
|
});
|
|
this._prepareItemsForUpload(verifiedBlobDataList, params, endpoint);
|
}
|
else {
|
this.log("undefined or non-array parameter passed into addBlobs", "error");
|
}
|
},
|
getUuid: function(id) {
|
return this._handler.getUuid(id);
|
},
|
getResumableFilesData: function() {
|
return this._handler.getResumableFilesData();
|
},
|
getSize: function(id) {
|
return this._handler.getSize(id);
|
},
|
getName: function(id) {
|
return this._handler.getName(id);
|
},
|
setName: function(id, newName) {
|
this._handler.setName(id, newName);
|
this._uploadData.nameChanged(id, newName);
|
},
|
getFile: function(fileOrBlobId) {
|
return this._handler.getFile(fileOrBlobId);
|
},
|
deleteFile: function(id) {
|
this._onSubmitDelete(id);
|
},
|
setDeleteFileEndpoint: function(endpoint, id) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (id == null) {
|
this._options.deleteFile.endpoint = endpoint;
|
}
|
else {
|
this._deleteFileEndpointStore.setEndpoint(endpoint, id);
|
}
|
},
|
doesExist: function(fileOrBlobId) {
|
return this._handler.isValid(fileOrBlobId);
|
},
|
getUploads: function(optionalFilter) {
|
return this._uploadData.retrieve(optionalFilter);
|
},
|
_handleCheckedCallback: function(details) {
|
var self = this,
|
callbackRetVal = details.callback();
|
|
if (qq.isPromise(callbackRetVal)) {
|
this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier);
|
return callbackRetVal.then(
|
function(successParam) {
|
self.log(details.name + " promise success for " + details.identifier);
|
details.onSuccess(successParam);
|
},
|
function() {
|
if (details.onFailure) {
|
self.log(details.name + " promise failure for " + details.identifier);
|
details.onFailure();
|
}
|
else {
|
self.log(details.name + " promise failure for " + details.identifier);
|
}
|
});
|
}
|
|
if (callbackRetVal !== false) {
|
details.onSuccess(callbackRetVal);
|
}
|
else {
|
if (details.onFailure) {
|
this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback.")
|
details.onFailure();
|
}
|
else {
|
this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed.")
|
}
|
}
|
|
return callbackRetVal;
|
},
|
_createUploadButton: function(element){
|
var self = this;
|
|
var button = new qq.UploadButton({
|
element: element,
|
multiple: this._options.multiple && qq.supportedFeatures.ajaxUploading,
|
acceptFiles: this._options.validation.acceptFiles,
|
onChange: function(input){
|
self._onInputChange(input);
|
},
|
hoverClass: this._options.classes.buttonHover,
|
focusClass: this._options.classes.buttonFocus
|
});
|
|
this._disposeSupport.addDisposer(function() { button.dispose(); });
|
return button;
|
},
|
_createUploadHandler: function(){
|
var self = this;
|
|
return new qq.UploadHandler({
|
debug: this._options.debug,
|
forceMultipart: this._options.request.forceMultipart,
|
maxConnections: this._options.maxConnections,
|
customHeaders: this._options.request.customHeaders,
|
inputName: this._options.request.inputName,
|
uuidParamName: this._options.request.uuidName,
|
filenameParam: this._options.request.filenameParam,
|
totalFileSizeParamName: this._options.request.totalFileSizeName,
|
cors: this._options.cors,
|
demoMode: this._options.demoMode,
|
paramsInBody: this._options.request.paramsInBody,
|
paramsStore: this._paramsStore,
|
endpointStore: this._endpointStore,
|
chunking: this._options.chunking,
|
resume: this._options.resume,
|
blobs: this._options.blobs,
|
log: function(str, level) {
|
self.log(str, level);
|
},
|
onProgress: function(id, name, loaded, total){
|
self._onProgress(id, name, loaded, total);
|
self._options.callbacks.onProgress(id, name, loaded, total);
|
},
|
onComplete: function(id, name, result, xhr){
|
self._onComplete(id, name, result, xhr);
|
self._options.callbacks.onComplete(id, name, result, xhr);
|
},
|
onCancel: function(id, name) {
|
return self._handleCheckedCallback({
|
name: "onCancel",
|
callback: qq.bind(self._options.callbacks.onCancel, self, id, name),
|
onSuccess: qq.bind(self._onCancel, self, id, name),
|
identifier: id
|
});
|
},
|
onUpload: function(id, name){
|
self._onUpload(id, name);
|
self._options.callbacks.onUpload(id, name);
|
},
|
onUploadChunk: function(id, name, chunkData){
|
self._options.callbacks.onUploadChunk(id, name, chunkData);
|
},
|
onResume: function(id, name, chunkData) {
|
return self._options.callbacks.onResume(id, name, chunkData);
|
},
|
onAutoRetry: function(id, name, responseJSON, xhr) {
|
self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty];
|
|
if (self._shouldAutoRetry(id, name, responseJSON)) {
|
self._maybeParseAndSendUploadError(id, name, responseJSON, xhr);
|
self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1);
|
self._onBeforeAutoRetry(id, name);
|
|
self._retryTimeouts[id] = setTimeout(function() {
|
self._onAutoRetry(id, name, responseJSON)
|
}, self._options.retry.autoAttemptDelay * 1000);
|
|
return true;
|
}
|
else {
|
return false;
|
}
|
},
|
onUuidChanged: function(id, newUuid) {
|
self._uploadData.uuidChanged(id, newUuid);
|
}
|
});
|
},
|
_createDeleteHandler: function() {
|
var self = this;
|
|
return new qq.DeleteFileAjaxRequestor({
|
method: this._options.deleteFile.method,
|
maxConnections: this._options.maxConnections,
|
uuidParamName: this._options.request.uuidName,
|
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, xhrOrXdr, isError) {
|
self._onDeleteComplete(id, xhrOrXdr, isError);
|
self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError);
|
}
|
|
});
|
},
|
_createPasteHandler: function() {
|
var self = this;
|
|
return new qq.PasteSupport({
|
targetElement: this._options.paste.targetElement,
|
callbacks: {
|
log: function(str, level) {
|
self.log(str, level);
|
},
|
pasteReceived: function(blob) {
|
self._handleCheckedCallback({
|
name: "onPasteReceived",
|
callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob),
|
onSuccess: qq.bind(self._handlePasteSuccess, self, blob),
|
identifier: "pasted image"
|
});
|
}
|
}
|
});
|
},
|
_createUploadDataTracker: function() {
|
var self = this;
|
|
return new qq.UploadData({
|
getName: function(id) {
|
return self.getName(id);
|
},
|
getUuid: function(id) {
|
return self.getUuid(id);
|
},
|
getSize: function(id) {
|
return self.getSize(id);
|
},
|
onStatusChange: function(id, oldStatus, newStatus) {
|
self._onUploadStatusChange(id, oldStatus, newStatus);
|
self._options.callbacks.onStatusChange(id, oldStatus, newStatus);
|
}
|
});
|
},
|
_onUploadStatusChange: function(id, oldStatus, newStatus) {
|
//nothing to do in the basic uploader
|
},
|
_handlePasteSuccess: function(blob, extSuppliedName) {
|
var extension = blob.type.split("/")[1],
|
name = extSuppliedName;
|
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (name == null) {
|
name = this._options.paste.defaultName;
|
}
|
|
name += '.' + extension;
|
|
this.addBlobs({
|
name: name,
|
blob: blob
|
});
|
},
|
_preventLeaveInProgress: function(){
|
var self = this;
|
|
this._disposeSupport.attach(window, 'beforeunload', function(e){
|
if (!self._filesInProgress.length){return;}
|
|
var e = e || window.event;
|
// for ie, ff
|
e.returnValue = self._options.messages.onLeave;
|
// for webkit
|
return self._options.messages.onLeave;
|
});
|
},
|
_onSubmit: function(id, name) {
|
this._netUploadedOrQueued++;
|
|
if (this._options.autoUpload) {
|
this._filesInProgress.push(id);
|
}
|
},
|
_onProgress: function(id, name, loaded, total) {
|
//nothing to do yet in core uploader
|
},
|
_onComplete: function(id, name, result, xhr) {
|
if (!result.success) {
|
this._netUploadedOrQueued--;
|
this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED);
|
}
|
else {
|
this._netUploaded++;
|
this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL);
|
}
|
|
this._removeFromFilesInProgress(id);
|
this._maybeParseAndSendUploadError(id, name, result, xhr);
|
},
|
_onCancel: function(id, name) {
|
this._netUploadedOrQueued--;
|
|
this._removeFromFilesInProgress(id);
|
|
clearTimeout(this._retryTimeouts[id]);
|
|
var storedItemIndex = qq.indexOf(this._storedIds, id);
|
if (!this._options.autoUpload && storedItemIndex >= 0) {
|
this._storedIds.splice(storedItemIndex, 1);
|
}
|
|
this._uploadData.setStatus(id, qq.status.CANCELED);
|
},
|
_isDeletePossible: function() {
|
if (!this._options.deleteFile.enabled) {
|
return false;
|
}
|
|
if (this._options.cors.expected) {
|
if (qq.supportedFeatures.deleteFileCorsXhr) {
|
return true;
|
}
|
|
if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) {
|
return true;
|
}
|
|
return false;
|
}
|
|
return true;
|
},
|
_onSubmitDelete: function(id, onSuccessCallback) {
|
if (this._isDeletePossible()) {
|
return this._handleCheckedCallback({
|
name: "onSubmitDelete",
|
callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id),
|
onSuccess: onSuccessCallback || qq.bind(this._deleteHandler.sendDelete, this, id, this.getUuid(id)),
|
identifier: id
|
});
|
}
|
else {
|
this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " +
|
"due to CORS on a user agent that does not support pre-flighting.", "warn");
|
return false;
|
}
|
},
|
_onDelete: function(id) {
|
this._uploadData.setStatus(id, qq.status.DELETING);
|
},
|
_onDeleteComplete: function(id, xhrOrXdr, isError) {
|
var name = this._handler.getName(id);
|
|
if (isError) {
|
this._uploadData.setStatus(id, qq.status.DELETE_FAILED);
|
this.log("Delete request for '" + name + "' has failed.", "error");
|
|
// For error reporing, we only have accesss to the response status if this is not
|
// an `XDomainRequest`.
|
if (xhrOrXdr.withCredentials === undefined) {
|
this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr);
|
}
|
else {
|
this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr);
|
}
|
}
|
else {
|
this._netUploadedOrQueued--;
|
this._netUploaded--;
|
this._handler.expunge(id);
|
this._uploadData.setStatus(id, qq.status.DELETED);
|
this.log("Delete request for '" + name + "' has succeeded.");
|
}
|
},
|
_removeFromFilesInProgress: function(id) {
|
var index = qq.indexOf(this._filesInProgress, id);
|
if (index >= 0) {
|
this._filesInProgress.splice(index, 1);
|
}
|
},
|
_onUpload: function(id, name) {
|
this._uploadData.setStatus(id, qq.status.UPLOADING);
|
},
|
_onInputChange: function(input){
|
if (qq.supportedFeatures.ajaxUploading) {
|
this.addFiles(input.files);
|
}
|
else {
|
this.addFiles(input);
|
}
|
|
this._button.reset();
|
},
|
_onBeforeAutoRetry: function(id, name) {
|
this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "...");
|
},
|
_onAutoRetry: function(id, name, responseJSON) {
|
this.log("Retrying " + name + "...");
|
this._autoRetries[id]++;
|
this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING);
|
this._handler.retry(id);
|
},
|
_shouldAutoRetry: function(id, name, responseJSON) {
|
if (!this._preventRetries[id] && this._options.retry.enableAuto) {
|
if (this._autoRetries[id] === undefined) {
|
this._autoRetries[id] = 0;
|
}
|
|
return this._autoRetries[id] < this._options.retry.maxAutoAttempts;
|
}
|
|
return false;
|
},
|
//return false if we should not attempt the requested retry
|
_onBeforeManualRetry: function(id) {
|
var itemLimit = this._options.validation.itemLimit;
|
|
if (this._preventRetries[id]) {
|
this.log("Retries are forbidden for id " + id, 'warn');
|
return false;
|
}
|
else if (this._handler.isValid(id)) {
|
var fileName = this._handler.getName(id);
|
|
if (this._options.callbacks.onManualRetry(id, fileName) === false) {
|
return false;
|
}
|
|
if (itemLimit > 0 && this._netUploadedOrQueued+1 > itemLimit) {
|
this._itemError("retryFailTooManyItems");
|
return false;
|
}
|
|
this.log("Retrying upload for '" + fileName + "' (id: " + id + ")...");
|
this._filesInProgress.push(id);
|
return true;
|
}
|
else {
|
this.log("'" + id + "' is not a valid file ID", 'error');
|
return false;
|
}
|
},
|
_maybeParseAndSendUploadError: function(id, name, response, xhr) {
|
//assuming no one will actually set the response code to something other than 200 and still set 'success' to true
|
if (!response.success){
|
if (xhr && xhr.status !== 200 && !response.error) {
|
this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr);
|
}
|
else {
|
var errorReason = response.error ? response.error : this._options.text.defaultResponseError;
|
this._options.callbacks.onError(id, name, errorReason, xhr);
|
}
|
}
|
},
|
_prepareItemsForUpload: function(items, params, endpoint) {
|
var validationDescriptors = this._getValidationDescriptors(items);
|
|
this._handleCheckedCallback({
|
name: "onValidateBatch",
|
callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors),
|
onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint),
|
identifier: "batch validation"
|
});
|
},
|
_upload: function(blobOrFileContainer, params, endpoint) {
|
var id = this._handler.add(blobOrFileContainer),
|
name = this._handler.getName(id);
|
|
this._uploadData.added(id);
|
|
if (params) {
|
this.setParams(params, id);
|
}
|
|
if (endpoint) {
|
this.setEndpoint(endpoint, id);
|
}
|
|
this._handleCheckedCallback({
|
name: "onSubmit",
|
callback: qq.bind(this._options.callbacks.onSubmit, this, id, name),
|
onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name),
|
onFailure: qq.bind(this._fileOrBlobRejected, this, id, name),
|
identifier: id
|
});
|
},
|
_onSubmitCallbackSuccess: function(id, name) {
|
this._uploadData.setStatus(id, qq.status.SUBMITTED);
|
|
this._onSubmit.apply(this, arguments);
|
this._onSubmitted.apply(this, arguments);
|
this._options.callbacks.onSubmitted.apply(this, arguments);
|
|
if (this._options.autoUpload) {
|
if (!this._handler.upload(id)) {
|
this._uploadData.setStatus(id, qq.status.QUEUED);
|
}
|
}
|
else {
|
this._storeForLater(id);
|
}
|
},
|
_onSubmitted: function(id) {
|
//nothing to do in the base uploader
|
},
|
_storeForLater: function(id) {
|
this._storedIds.push(id);
|
},
|
_onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint) {
|
var errorMessage,
|
itemLimit = this._options.validation.itemLimit,
|
proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued + validationDescriptors.length;
|
|
if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) {
|
if (items.length > 0) {
|
this._handleCheckedCallback({
|
name: "onValidate",
|
callback: qq.bind(this._options.callbacks.onValidate, this, items[0]),
|
onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint),
|
onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint),
|
identifier: "Item '" + items[0].name + "', size: " + items[0].size
|
});
|
}
|
else {
|
this._itemError("noFilesError");
|
}
|
}
|
else {
|
errorMessage = this._options.messages.tooManyItemsError
|
.replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued)
|
.replace(/\{itemLimit\}/g, itemLimit);
|
this._batchError(errorMessage);
|
}
|
},
|
_onValidateCallbackSuccess: function(items, index, params, endpoint) {
|
var nextIndex = index+1,
|
validationDescriptor = this._getValidationDescriptor(items[index]),
|
validItem = false;
|
|
if (this._validateFileOrBlobData(items[index], validationDescriptor)) {
|
validItem = true;
|
this._upload(items[index], params, endpoint);
|
}
|
|
this._maybeProcessNextItemAfterOnValidateCallback(validItem, items, nextIndex, params, endpoint);
|
},
|
_onValidateCallbackFailure: function(items, index, params, endpoint) {
|
var nextIndex = index+ 1;
|
|
this._fileOrBlobRejected(undefined, items[0].name);
|
|
this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint);
|
},
|
_maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) {
|
var self = this;
|
|
if (items.length > index) {
|
if (validItem || !this._options.validation.stopOnFirstInvalidFile) {
|
//use setTimeout to prevent a stack overflow with a large number of files in the batch & non-promissory callbacks
|
setTimeout(function() {
|
var validationDescriptor = self._getValidationDescriptor(items[index]);
|
|
self._handleCheckedCallback({
|
name: "onValidate",
|
callback: qq.bind(self._options.callbacks.onValidate, self, items[index]),
|
onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint),
|
onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint),
|
identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size
|
});
|
}, 0);
|
}
|
}
|
},
|
_validateFileOrBlobData: function(item, validationDescriptor) {
|
var name = validationDescriptor.name,
|
size = validationDescriptor.size,
|
valid = true;
|
|
if (this._options.callbacks.onValidate(validationDescriptor) === false) {
|
valid = false;
|
}
|
|
if (qq.isFileOrInput(item) && !this._isAllowedExtension(name)){
|
this._itemError('typeError', name);
|
valid = false;
|
|
}
|
else if (size === 0){
|
this._itemError('emptyError', name);
|
valid = false;
|
|
}
|
else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){
|
this._itemError('sizeError', name);
|
valid = false;
|
|
}
|
else if (size && size < this._options.validation.minSizeLimit){
|
this._itemError('minSizeError', name);
|
valid = false;
|
}
|
|
if (!valid) {
|
this._fileOrBlobRejected(undefined, name);
|
}
|
|
return valid;
|
},
|
_fileOrBlobRejected: function(id, name) {
|
if (id !== undefined) {
|
this._uploadData.setStatus(id, qq.status.REJECTED);
|
}
|
},
|
_itemError: function(code, maybeNameOrNames) {
|
var message = this._options.messages[code],
|
allowedExtensions = [],
|
names = [].concat(maybeNameOrNames),
|
name = names[0],
|
extensionsForMessage, placeholderMatch;
|
|
function r(name, replacement){ message = message.replace(name, replacement); }
|
|
qq.each(this._options.validation.allowedExtensions, function(idx, allowedExtension) {
|
/**
|
* If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the
|
* `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details.
|
*/
|
if (qq.isString(allowedExtension)) {
|
allowedExtensions.push(allowedExtension);
|
}
|
});
|
|
extensionsForMessage = allowedExtensions.join(', ').toLowerCase();
|
|
r('{file}', this._options.formatFileName(name));
|
r('{extensions}', extensionsForMessage);
|
r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit));
|
r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit));
|
|
placeholderMatch = message.match(/(\{\w+\})/g);
|
if (placeholderMatch !== null) {
|
qq.each(placeholderMatch, function(idx, placeholder) {
|
r(placeholder, names[idx]);
|
});
|
}
|
|
this._options.callbacks.onError(null, name, message, undefined);
|
|
return message;
|
},
|
_batchError: function(message) {
|
this._options.callbacks.onError(null, null, message, undefined);
|
},
|
_isAllowedExtension: function(fileName){
|
var allowed = this._options.validation.allowedExtensions,
|
valid = false;
|
|
if (!allowed.length) {
|
return true;
|
}
|
|
qq.each(allowed, function(idx, allowedExt) {
|
/**
|
* If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the
|
* `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details.
|
*/
|
if (qq.isString(allowedExt)) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
var extRegex = new RegExp('\\.' + allowedExt + "$", 'i');
|
|
if (fileName.match(extRegex) != null) {
|
valid = true;
|
return false;
|
}
|
}
|
});
|
|
return valid;
|
},
|
_formatSize: function(bytes){
|
var i = -1;
|
do {
|
bytes = bytes / 1000;
|
i++;
|
} while (bytes > 999);
|
|
return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i];
|
},
|
_wrapCallbacks: function() {
|
var self, safeCallback;
|
|
self = this;
|
|
safeCallback = function(name, callback, args) {
|
try {
|
return callback.apply(self, args);
|
}
|
catch (exception) {
|
self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error');
|
}
|
};
|
|
for (var prop in this._options.callbacks) {
|
(function() {
|
var callbackName, callbackFunc;
|
callbackName = prop;
|
callbackFunc = self._options.callbacks[callbackName];
|
self._options.callbacks[callbackName] = function() {
|
return safeCallback(callbackName, callbackFunc, arguments);
|
};
|
}());
|
}
|
},
|
_parseFileOrBlobDataName: function(fileOrBlobData) {
|
var name;
|
|
if (qq.isFileOrInput(fileOrBlobData)) {
|
if (fileOrBlobData.value) {
|
// it is a file input
|
// get input value and remove path to normalize
|
name = fileOrBlobData.value.replace(/.*(\/|\\)/, "");
|
} else {
|
// fix missing properties in Safari 4 and firefox 11.0a2
|
name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name;
|
}
|
}
|
else {
|
name = fileOrBlobData.name;
|
}
|
|
return name;
|
},
|
_parseFileOrBlobDataSize: function(fileOrBlobData) {
|
var size;
|
|
if (qq.isFileOrInput(fileOrBlobData)) {
|
if (!fileOrBlobData.value){
|
// fix missing properties in Safari 4 and firefox 11.0a2
|
size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size;
|
}
|
}
|
else {
|
size = fileOrBlobData.blob.size;
|
}
|
|
return size;
|
},
|
_getValidationDescriptor: function(fileOrBlobData) {
|
var name, size, fileDescriptor;
|
|
fileDescriptor = {};
|
name = this._parseFileOrBlobDataName(fileOrBlobData);
|
size = this._parseFileOrBlobDataSize(fileOrBlobData);
|
|
fileDescriptor.name = name;
|
if (size !== undefined) {
|
fileDescriptor.size = size;
|
}
|
|
return fileDescriptor;
|
},
|
_getValidationDescriptors: function(files) {
|
var self = this,
|
fileDescriptors = [];
|
|
qq.each(files, function(idx, file) {
|
fileDescriptors.push(self._getValidationDescriptor(file));
|
});
|
|
return fileDescriptors;
|
},
|
_createParamsStore: function(type) {
|
var paramsStore = {},
|
self = this;
|
|
return {
|
setParams: function(params, id) {
|
var paramsCopy = {};
|
qq.extend(paramsCopy, params);
|
paramsStore[id] = paramsCopy;
|
},
|
|
getParams: function(id) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
var paramsCopy = {};
|
|
if (id != null && paramsStore[id]) {
|
qq.extend(paramsCopy, paramsStore[id]);
|
}
|
else {
|
qq.extend(paramsCopy, self._options[type].params);
|
}
|
|
return paramsCopy;
|
},
|
|
remove: function(fileId) {
|
return delete paramsStore[fileId];
|
},
|
|
reset: function() {
|
paramsStore = {};
|
}
|
};
|
},
|
_createEndpointStore: function(type) {
|
var endpointStore = {},
|
self = this;
|
|
return {
|
setEndpoint: function(endpoint, id) {
|
endpointStore[id] = endpoint;
|
},
|
|
getEndpoint: function(id) {
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (id != null && endpointStore[id]) {
|
return endpointStore[id];
|
}
|
|
return self._options[type].endpoint;
|
},
|
|
remove: function(fileId) {
|
return delete endpointStore[fileId];
|
},
|
|
reset: function() {
|
endpointStore = {};
|
}
|
};
|
},
|
_handleCameraAccess: function() {
|
if (this._options.camera.ios && qq.ios()) {
|
this._options.multiple = false;
|
|
if (this._options.validation.acceptFiles === null) {
|
this._options.validation.acceptFiles = "image/*;capture=camera";
|
}
|
else {
|
this._options.validation.acceptFiles += ",image/*;capture=camera";
|
}
|
}
|
}
|
};
|
;/*globals qq, document*/
|
qq.DragAndDrop = function(o) {
|
"use strict";
|
|
var options, dz,
|
droppedFiles = [],
|
disposeSupport = new qq.DisposeSupport();
|
|
options = {
|
dropZoneElements: [],
|
hideDropZonesBeforeEnter: false,
|
allowMultipleItems: true,
|
classes: {
|
dropActive: null
|
},
|
callbacks: new qq.DragAndDrop.callbacks()
|
};
|
|
qq.extend(options, o, true);
|
|
setupDragDrop();
|
|
function uploadDroppedFiles(files) {
|
options.callbacks.dropLog('Grabbed ' + files.length + " dropped files.");
|
dz.dropDisabled(false);
|
options.callbacks.processingDroppedFilesComplete(files);
|
}
|
|
function traverseFileTree(entry) {
|
var dirReader, i,
|
parseEntryPromise = new qq.Promise();
|
|
if (entry.isFile) {
|
entry.file(function(file) {
|
droppedFiles.push(file);
|
parseEntryPromise.success();
|
},
|
function(fileError) {
|
options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error");
|
parseEntryPromise.failure();
|
});
|
}
|
else if (entry.isDirectory) {
|
dirReader = entry.createReader();
|
dirReader.readEntries(function(entries) {
|
var entriesLeft = entries.length;
|
|
for (i = 0; i < entries.length; i+=1) {
|
traverseFileTree(entries[i]).done(function() {
|
entriesLeft-=1;
|
|
if (entriesLeft === 0) {
|
parseEntryPromise.success();
|
}
|
});
|
}
|
|
if (!entries.length) {
|
parseEntryPromise.success();
|
}
|
}, function(fileError) {
|
options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error");
|
parseEntryPromise.failure();
|
});
|
}
|
|
return parseEntryPromise;
|
}
|
|
function handleDataTransfer(dataTransfer) {
|
var i, items, entry,
|
pendingFolderPromises = [],
|
handleDataTransferPromise = new qq.Promise();
|
|
options.callbacks.processingDroppedFiles();
|
dz.dropDisabled(true);
|
|
if (dataTransfer.files.length > 1 && !options.allowMultipleItems) {
|
options.callbacks.processingDroppedFilesComplete([]);
|
options.callbacks.dropError('tooManyFilesError', "");
|
dz.dropDisabled(false);
|
handleDataTransferPromise.failure();
|
}
|
else {
|
droppedFiles = [];
|
|
if (qq.isFolderDropSupported(dataTransfer)) {
|
items = dataTransfer.items;
|
|
for (i = 0; i < items.length; i+=1) {
|
entry = items[i].webkitGetAsEntry();
|
if (entry) {
|
//due to a bug in Chrome's File System API impl - #149735
|
if (entry.isFile) {
|
droppedFiles.push(items[i].getAsFile());
|
}
|
|
else {
|
pendingFolderPromises.push(traverseFileTree(entry).done(function() {
|
pendingFolderPromises.pop();
|
if (pendingFolderPromises.length === 0) {
|
handleDataTransferPromise.success();
|
}
|
}));
|
}
|
}
|
}
|
}
|
else {
|
droppedFiles = dataTransfer.files;
|
}
|
|
if (pendingFolderPromises.length === 0) {
|
handleDataTransferPromise.success();
|
}
|
}
|
|
return handleDataTransferPromise;
|
}
|
|
function setupDropzone(dropArea){
|
dz = new qq.UploadDropZone({
|
element: dropArea,
|
onEnter: function(e){
|
qq(dropArea).addClass(options.classes.dropActive);
|
e.stopPropagation();
|
},
|
onLeaveNotDescendants: function(e){
|
qq(dropArea).removeClass(options.classes.dropActive);
|
},
|
onDrop: function(e){
|
if (options.hideDropZonesBeforeEnter) {
|
qq(dropArea).hide();
|
}
|
qq(dropArea).removeClass(options.classes.dropActive);
|
|
handleDataTransfer(e.dataTransfer).done(function() {
|
uploadDroppedFiles(droppedFiles);
|
});
|
}
|
});
|
|
disposeSupport.addDisposer(function() {
|
dz.dispose();
|
});
|
|
if (options.hideDropZonesBeforeEnter) {
|
qq(dropArea).hide();
|
}
|
}
|
|
function isFileDrag(dragEvent) {
|
var fileDrag;
|
|
qq.each(dragEvent.dataTransfer.types, function(key, val) {
|
if (val === 'Files') {
|
fileDrag = true;
|
return false;
|
}
|
});
|
|
return fileDrag;
|
}
|
|
function setupDragDrop(){
|
var dropZones = options.dropZoneElements;
|
|
qq.each(dropZones, function(idx, dropZone) {
|
setupDropzone(dropZone);
|
})
|
|
// IE <= 9 does not support the File API used for drag+drop uploads
|
if (dropZones.length && (!qq.ie() || qq.ie10())) {
|
disposeSupport.attach(document, 'dragenter', function(e) {
|
if (!dz.dropDisabled() && isFileDrag(e)) {
|
qq.each(dropZones, function(idx, dropZone) {
|
qq(dropZone).css({display: 'block'});
|
});
|
}
|
});
|
}
|
disposeSupport.attach(document, 'dragleave', function(e){
|
if (options.hideDropZonesBeforeEnter && qq.FineUploader.prototype._leaving_document_out(e)) {
|
qq.each(dropZones, function(idx, dropZone) {
|
qq(dropZone).hide();
|
});
|
}
|
});
|
disposeSupport.attach(document, 'drop', function(e){
|
if (options.hideDropZonesBeforeEnter) {
|
qq.each(dropZones, function(idx, dropZone) {
|
qq(dropZone).hide();
|
});
|
}
|
e.preventDefault();
|
});
|
}
|
|
return {
|
setupExtraDropzone: function(element) {
|
options.dropZoneElements.push(element);
|
setupDropzone(element);
|
},
|
|
removeDropzone: function(element) {
|
var i,
|
dzs = options.dropZoneElements;
|
|
for(i in dzs) {
|
if (dzs[i] === element) {
|
return dzs.splice(i, 1);
|
}
|
}
|
},
|
|
dispose: function() {
|
disposeSupport.dispose();
|
dz.dispose();
|
}
|
};
|
};
|
|
qq.DragAndDrop.callbacks = function() {
|
return {
|
processingDroppedFiles: function() {},
|
processingDroppedFilesComplete: function(files) {},
|
dropError: function(code, errorSpecifics) {
|
qq.log("Drag & drop error code '" + code + " with these specifics: '" + errorSpecifics + "'", "error");
|
},
|
dropLog: function(message, level) {
|
qq.log(message, level);
|
}
|
}
|
}
|
|
qq.UploadDropZone = function(o){
|
"use strict";
|
|
var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport();
|
|
options = {
|
element: null,
|
onEnter: function(e){},
|
onLeave: function(e){},
|
// is not fired when leaving element by hovering descendants
|
onLeaveNotDescendants: function(e){},
|
onDrop: function(e){}
|
};
|
|
qq.extend(options, o);
|
element = options.element;
|
|
function dragover_should_be_canceled(){
|
return qq.safari() || (qq.firefox() && qq.windows());
|
}
|
|
function disableDropOutside(e){
|
// run only once for all instances
|
if (!dropOutsideDisabled ){
|
|
// for these cases we need to catch onDrop to reset dropArea
|
if (dragover_should_be_canceled){
|
disposeSupport.attach(document, 'dragover', function(e){
|
e.preventDefault();
|
});
|
} else {
|
disposeSupport.attach(document, 'dragover', function(e){
|
if (e.dataTransfer){
|
e.dataTransfer.dropEffect = 'none';
|
e.preventDefault();
|
}
|
});
|
}
|
|
dropOutsideDisabled = true;
|
}
|
}
|
|
function isValidFileDrag(e){
|
// e.dataTransfer currently causing IE errors
|
// IE9 does NOT support file API, so drag-and-drop is not possible
|
if (qq.ie() && !qq.ie10()) {
|
return false;
|
}
|
|
var effectTest, dt = e.dataTransfer,
|
// do not check dt.types.contains in webkit, because it crashes safari 4
|
isSafari = qq.safari();
|
|
// dt.effectAllowed is none in Safari 5
|
// dt.types.contains check is for firefox
|
effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none';
|
return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files')));
|
}
|
|
function isOrSetDropDisabled(isDisabled) {
|
if (isDisabled !== undefined) {
|
preventDrop = isDisabled;
|
}
|
return preventDrop;
|
}
|
|
function attachEvents(){
|
disposeSupport.attach(element, 'dragover', function(e){
|
if (!isValidFileDrag(e)) {
|
return;
|
}
|
|
var effect = qq.ie() ? null : e.dataTransfer.effectAllowed;
|
if (effect === 'move' || effect === 'linkMove'){
|
e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
|
} else {
|
e.dataTransfer.dropEffect = 'copy'; // for Chrome
|
}
|
|
e.stopPropagation();
|
e.preventDefault();
|
});
|
|
disposeSupport.attach(element, 'dragenter', function(e){
|
if (!isOrSetDropDisabled()) {
|
if (!isValidFileDrag(e)) {
|
return;
|
}
|
options.onEnter(e);
|
}
|
});
|
|
disposeSupport.attach(element, 'dragleave', function(e){
|
if (!isValidFileDrag(e)) {
|
return;
|
}
|
|
options.onLeave(e);
|
|
var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
|
// do not fire when moving a mouse over a descendant
|
if (qq(this).contains(relatedTarget)) {
|
return;
|
}
|
|
options.onLeaveNotDescendants(e);
|
});
|
|
disposeSupport.attach(element, 'drop', function(e){
|
if (!isOrSetDropDisabled()) {
|
if (!isValidFileDrag(e)) {
|
return;
|
}
|
|
e.preventDefault();
|
options.onDrop(e);
|
}
|
});
|
}
|
|
disableDropOutside();
|
attachEvents();
|
|
return {
|
dropDisabled: function(isDisabled) {
|
return isOrSetDropDisabled(isDisabled);
|
},
|
|
dispose: function() {
|
disposeSupport.dispose();
|
}
|
};
|
};
|
;/**
|
* Class that creates upload widget with drag-and-drop and file list
|
* @inherits qq.FineUploaderBasic
|
*/
|
qq.FineUploader = function(o){
|
// call parent constructor
|
qq.FineUploaderBasic.apply(this, arguments);
|
|
// additional options
|
qq.extend(this._options, {
|
element: null,
|
listElement: null,
|
dragAndDrop: {
|
extraDropzones: [],
|
hideDropzones: true,
|
disableDefaultDropzone: false
|
},
|
text: {
|
uploadButton: 'Upload a file',
|
cancelButton: 'Cancel',
|
retryButton: 'Retry',
|
deleteButton: 'Delete',
|
failUpload: 'Upload failed',
|
dragZone: 'Drop files here to upload',
|
dropProcessing: 'Processing dropped files...',
|
formatProgress: "{percent}% of {total_size}",
|
waitingForResponse: "Processing..."
|
},
|
template: '<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>' +
|
(this._options.editFilename && this._options.editFilename.enabled ? '<span class="qq-edit-filename-icon"></span>' : '') +
|
'<span class="qq-upload-file"></span>' +
|
(this._options.editFilename && this._options.editFilename.enabled ? '<input class="qq-edit-filename" tabindex="0" type="text">' : '') +
|
'<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',
|
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',
|
editFilenameInput: 'qq-edit-filename',
|
|
success: 'qq-upload-success',
|
fail: 'qq-upload-fail',
|
|
successIcon: null,
|
failIcon: null,
|
editNameIcon: 'qq-edit-filename-icon',
|
editable: 'qq-editable',
|
|
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",
|
unsupportedBrowser: "Unrecoverable error - this browser does not permit file uploading of any kind."
|
},
|
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,
|
prependFiles: false
|
},
|
paste: {
|
promptForName: false,
|
namePromptMessage: "Please name this image"
|
},
|
editFilename: {
|
enabled: false
|
},
|
showMessage: function(message){
|
setTimeout(function() {
|
window.alert(message);
|
}, 0);
|
},
|
showConfirm: function(message, okCallback, cancelCallback) {
|
setTimeout(function() {
|
var result = window.confirm(message);
|
if (result) {
|
okCallback();
|
}
|
else if (cancelCallback) {
|
cancelCallback();
|
}
|
}, 0);
|
},
|
showPrompt: function(message, defaultValue) {
|
var promise = new qq.Promise(),
|
retVal = window.prompt(message, defaultValue);
|
|
/*jshint eqeqeq: true, eqnull: true*/
|
if (retVal != null && qq.trimStr(retVal).length > 0) {
|
promise.success(retVal);
|
}
|
else {
|
promise.failure("Undefined or invalid user-supplied value.");
|
}
|
|
return promise;
|
}
|
}, true);
|
|
// overwrite options with user supplied
|
qq.extend(this._options, o, true);
|
|
if (!qq.supportedFeatures.uploading || (this._options.cors.expected && !qq.supportedFeatures.uploadCors)) {
|
this._options.element.innerHTML = "<div>" + this._options.messages.unsupportedBrowser + "</div>"
|
}
|
else {
|
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._deleteRetryOrCancelClickHandler = this._bindDeleteRetryOrCancelClickEvent();
|
|
// A better approach would be to check specifically for focusin event support by querying the DOM API,
|
// but the DOMFocusIn event is not exposed as a property, so we have to resort to UA string sniffing.
|
this._focusinEventSupported = !qq.firefox();
|
|
if (this._isEditFilenameEnabled()) {
|
this._filenameClickHandler = this._bindFilenameClickEvent();
|
this._filenameInputFocusInHandler = this._bindFilenameInputFocusInEvent();
|
this._filenameInputFocusHandler = this._bindFilenameInputFocusEvent();
|
}
|
|
this._dnd = this._setupDragAndDrop();
|
|
if (this._options.paste.targetElement && this._options.paste.promptForName) {
|
this._setupPastePrompt();
|
}
|
|
this._totalFilesInBatch = 0;
|
this._filesInBatchAddedToUi = 0;
|
}
|
};
|
|
// 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.removeDropzone(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._dnd.dispose();
|
this._dnd = this._setupDragAndDrop();
|
|
this._totalFilesInBatch = 0;
|
this._filesInBatchAddedToUi = 0;
|
},
|
_removeFileItem: function(fileId) {
|
var item = this.getItemByFileId(fileId);
|
qq(item).remove();
|
},
|
_setupDragAndDrop: function() {
|
var self = this,
|
dropProcessingEl = this._find(this._element, 'dropProcessing'),
|
dropZoneElements = this._options.dragAndDrop.extraDropzones,
|
preventSelectFiles;
|
|
preventSelectFiles = function(event) {
|
event.preventDefault();
|
};
|
|
if (!this._options.dragAndDrop.disableDefaultDropzone) {
|
dropZoneElements.push(this._find(this._options.element, 'drop'));
|
}
|
|
return new qq.DragAndDrop({
|
dropZoneElements: dropZoneElements,
|
hideDropZonesBeforeEnter: this._options.dragAndDrop.hideDropzones,
|
allowMultipleItems: this._options.multiple,
|
classes: {
|
dropActive: this._options.classes.dropActive
|
},
|
callbacks: {
|
processingDroppedFiles: function() {
|
var input = self._button.getInput();
|
|
qq(dropProcessingEl).css({display: 'block'});
|
qq(input).attach('click', preventSelectFiles);
|
},
|
processingDroppedFilesComplete: function(files) {
|
var input = self._button.getInput();
|
|
qq(dropProcessingEl).hide();
|
qq(input).detach('click', preventSelectFiles);
|
|
if (files) {
|
self.addFiles(files);
|
}
|
},
|
dropError: function(code, errorData) {
|
self._itemError(code, errorData);
|
},
|
dropLog: function(message, level) {
|
self.log(message, level);
|
}
|
}
|
});
|
},
|
_bindDeleteRetryOrCancelClickEvent: function() {
|
var self = this;
|
|
return new qq.DeleteRetryOrCancelClickHandler({
|
listElement: this._listElement,
|
classes: this._classes,
|
log: function(message, lvl) {
|
self.log(message, lvl);
|
},
|
onDeleteFile: function(fileId) {
|
self.deleteFile(fileId);
|
},
|
onCancel: function(fileId) {
|
self.cancel(fileId);
|
},
|
onRetry: function(fileId) {
|
var item = self.getItemByFileId(fileId);
|
|
qq(item).removeClass(self._classes.retryable);
|
self.retry(fileId);
|
},
|
onGetName: function(fileId) {
|
return self.getName(fileId);
|
}
|
});
|
},
|
_isEditFilenameEnabled: function() {
|
return this._options.editFilename.enabled && !this._options.autoUpload;
|
},
|
_filenameEditHandler: function() {
|
var self = this;
|
|
return {
|
listElement: this._listElement,
|
classes: this._classes,
|
log: function(message, lvl) {
|
self.log(message, lvl);
|
},
|
onGetUploadStatus: function(fileId) {
|
return self.getUploads({id: fileId}).status;
|
},
|
onGetName: function(fileId) {
|
return self.getName(fileId);
|
},
|
onSetName: function(fileId, newName) {
|
var item = self.getItemByFileId(fileId),
|
qqFilenameDisplay = qq(self._find(item, 'file')),
|
formattedFilename = self._options.formatFileName(newName);
|
|
qqFilenameDisplay.setText(formattedFilename);
|
self.setName(fileId, newName);
|
},
|
onGetInput: function(item) {
|
return self._find(item, 'editFilenameInput');
|
},
|
onEditingStatusChange: function(fileId, isEditing) {
|
var item = self.getItemByFileId(fileId),
|
qqInput = qq(self._find(item, 'editFilenameInput')),
|
qqFilenameDisplay = qq(self._find(item, 'file')),
|
qqEditFilenameIcon = qq(self._find(item, 'editNameIcon')),
|
editableClass = self._classes.editable;
|
|
if (isEditing) {
|
qqInput.addClass('qq-editing');
|
|
qqFilenameDisplay.hide();
|
qqEditFilenameIcon.removeClass(editableClass);
|
}
|
else {
|
qqInput.removeClass('qq-editing');
|
qqFilenameDisplay.css({display: ''});
|
qqEditFilenameIcon.addClass(editableClass);
|
}
|
|
// Force IE8 and older to repaint
|
qq(item).addClass('qq-temp').removeClass('qq-temp');
|
}
|
};
|
},
|
_onUploadStatusChange: function(id, oldStatus, newStatus) {
|
if (this._isEditFilenameEnabled()) {
|
var item = this.getItemByFileId(id),
|
editableClass = this._classes.editable,
|
qqFilenameDisplay, qqEditFilenameIcon;
|
|
// Status for a file exists before it has been added to the DOM, so we must be careful here.
|
if (item && newStatus !== qq.status.SUBMITTED) {
|
qqFilenameDisplay = qq(this._find(item, 'file'));
|
qqEditFilenameIcon = qq(this._find(item, 'editNameIcon'));
|
|
qqFilenameDisplay.removeClass(editableClass);
|
qqEditFilenameIcon.removeClass(editableClass);
|
}
|
}
|
},
|
_bindFilenameInputFocusInEvent: function() {
|
var spec = qq.extend({}, this._filenameEditHandler());
|
|
return new qq.FilenameInputFocusInHandler(spec);
|
},
|
_bindFilenameInputFocusEvent: function() {
|
var spec = qq.extend({}, this._filenameEditHandler());
|
|
return new qq.FilenameInputFocusHandler(spec);
|
},
|
_bindFilenameClickEvent: function() {
|
var spec = qq.extend({}, this._filenameEditHandler());
|
|
return new qq.FilenameClickHandler(spec);
|
},
|
_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);
|
},
|
// The file item has been added to the DOM.
|
_onSubmitted: function(id) {
|
// If the edit filename feature is enabled, mark the filename element as "editable" and the associated edit icon
|
if (this._isEditFilenameEnabled()) {
|
var item = this.getItemByFileId(id),
|
qqFilenameDisplay = qq(this._find(item, 'file')),
|
qqEditFilenameIcon = qq(this._find(item, 'editNameIcon')),
|
editableClass = this._classes.editable;
|
|
qqFilenameDisplay.addClass(editableClass);
|
qqEditFilenameIcon.addClass(editableClass);
|
|
// If the focusin event is not supported, we must add a focus handler to the newly create edit filename text input
|
if (!this._focusinEventSupported) {
|
this._filenameInputFocusHandler.addHandler(this._find(item, 'editFilenameInput'));
|
}
|
}
|
},
|
// 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.supportedFeatures.ajaxUploading) {
|
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) {
|
var item = this.getItemByFileId(id);
|
|
if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) {
|
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;
|
}
|
else {
|
qq(item).addClass(this._classes.retryable);
|
return false;
|
}
|
},
|
_onSubmitDelete: function(id) {
|
var onSuccessCallback = qq.bind(this._onSubmitDeleteSuccess, this, id);
|
|
qq.FineUploaderBasic.prototype._onSubmitDelete.call(this, id, onSuccessCallback);
|
},
|
_onSubmitDeleteSuccess: function(id) {
|
if (this._options.deleteFile.forceConfirm) {
|
this._showDeleteConfirm(id);
|
}
|
else {
|
this._sendDeleteRequest(id);
|
}
|
},
|
_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.supportedFeatures.ajaxUploading) {
|
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();
|
}
|
|
if (this._options.display.prependFiles) {
|
this._prependItem(item);
|
}
|
else {
|
this._listElement.appendChild(item);
|
}
|
this._filesInBatchAddedToUi += 1;
|
|
if (this._options.display.fileSizeOnSubmit && qq.supportedFeatures.ajaxUploading) {
|
this._displayFileSize(id);
|
}
|
},
|
_prependItem: function(item) {
|
var parentEl = this._listElement,
|
beforeEl = parentEl.firstChild;
|
|
if (this._totalFilesInBatch > 1 && this._filesInBatchAddedToUi > 0) {
|
beforeEl = qq(parentEl).children()[this._filesInBatchAddedToUi - 1].nextSibling;
|
|
}
|
|
parentEl.insertBefore(item, beforeEl);
|
},
|
_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);
|
},
|
_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.supportedFeatures.ajaxUploading) {
|
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'});
|
},
|
_itemError: function(code, name){
|
var message = qq.FineUploaderBasic.prototype._itemError.apply(this, arguments);
|
this._options.showMessage(message);
|
},
|
_batchError: function(message) {
|
qq.FineUploaderBasic.prototype._batchError.apply(this, arguments);
|
this._options.showMessage(message);
|
},
|
_setupPastePrompt: function() {
|
var self = this;
|
|
this._options.callbacks.onPasteReceived = function() {
|
var message = self._options.paste.namePromptMessage,
|
defaultVal = self._options.paste.defaultName;
|
|
return self._options.showPrompt(message, defaultVal);
|
};
|
},
|
_fileOrBlobRejected: function(id, name) {
|
this._totalFilesInBatch -= 1;
|
qq.FineUploaderBasic.prototype._fileOrBlobRejected.apply(this, arguments);
|
},
|
_prepareItemsForUpload: function(items, params, endpoint) {
|
this._totalFilesInBatch = items.length;
|
this._filesInBatchAddedToUi = 0;
|
qq.FineUploaderBasic.prototype._prepareItemsForUpload.apply(this, arguments);
|
}
|
});
|
;/** Generic class for sending non-upload ajax requests and handling the associated responses **/
|
/*globals qq, XMLHttpRequest*/
|
qq.AjaxRequestor = function (o) {
|
"use strict";
|
|
var log, shouldParamsBeInQueryString,
|
queue = [],
|
requestState = [],
|
options = {
|
method: 'POST',
|
maxConnections: 3,
|
customHeaders: {},
|
endpointStore: {},
|
paramsStore: {},
|
mandatedParams: {},
|
successfulResponseCodes: {
|
"DELETE": [200, 202, 204],
|
"POST": [200, 204]
|
},
|
cors: {
|
expected: false,
|
sendCredentials: false
|
},
|
log: function (str, level) {},
|
onSend: function (id) {},
|
onComplete: function (id, xhrOrXdr, isError) {},
|
onCancel: function (id) {}
|
};
|
|
qq.extend(options, o);
|
log = options.log;
|
shouldParamsBeInQueryString = options.method === 'GET' || options.method === 'DELETE';
|
|
|
// [Simple methods](http://www.w3.org/TR/cors/#simple-method)
|
// are defined by the W3C in the CORS spec as a list of methods that, in part,
|
// make a CORS request eligible to be exempt from preflighting.
|
function isSimpleMethod() {
|
return qq.indexOf(["GET", "POST", "HEAD"], options.method) >= 0;
|
}
|
|
// [Simple headers](http://www.w3.org/TR/cors/#simple-header)
|
// are defined by the W3C in the CORS spec as a list of headers that, in part,
|
// make a CORS request eligible to be exempt from preflighting.
|
function containsNonSimpleHeaders(headers) {
|
var containsNonSimple = false;
|
|
qq.each(containsNonSimple, function(idx, header) {
|
if (qq.indexOf(["Accept", "Accept-Language", "Content-Language", "Content-Type"], header) < 0) {
|
containsNonSimple = true;
|
return false;
|
}
|
});
|
|
return containsNonSimple;
|
}
|
|
function isXdr(xhr) {
|
//The `withCredentials` test is a commonly accepted way to determine if XHR supports CORS.
|
return options.cors.expected && xhr.withCredentials === undefined;
|
}
|
|
// Returns either a new `XMLHttpRequest` or `XDomainRequest` instance.
|
function getCorsAjaxTransport() {
|
var xhrOrXdr;
|
|
if (window.XMLHttpRequest) {
|
xhrOrXdr = new XMLHttpRequest();
|
|
if (xhrOrXdr.withCredentials === undefined) {
|
xhrOrXdr = new XDomainRequest();
|
}
|
}
|
|
return xhrOrXdr;
|
}
|
|
// Returns either a new XHR/XDR instance, or an existing one for the associated `File` or `Blob`.
|
function getXhrOrXdr(id, dontCreateIfNotExist) {
|
var xhrOrXdr = requestState[id].xhr;
|
|
if (!xhrOrXdr && !dontCreateIfNotExist) {
|
if (options.cors.expected) {
|
xhrOrXdr = getCorsAjaxTransport();
|
}
|
else {
|
xhrOrXdr = new XMLHttpRequest();
|
}
|
|
requestState[id].xhr = xhrOrXdr;
|
}
|
|
return xhrOrXdr;
|
}
|
|
// 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, xdrError) {
|
var xhr = getXhrOrXdr(id),
|
method = options.method,
|
isError = xdrError === false;
|
|
dequeue(id);
|
|
if (isError) {
|
log(method + " request for " + id + " has failed", "error");
|
}
|
else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) {
|
isError = true;
|
log(method + " request for " + id + " has failed - response code " + xhr.status, "error");
|
}
|
|
options.onComplete(id, xhr, isError);
|
}
|
|
function getParams(id) {
|
var params = {},
|
additionalParams = requestState[id].additionalParams,
|
mandatedParams = options.mandatedParams;
|
|
if (options.paramsStore.getParams) {
|
params = options.paramsStore.getParams(id);
|
}
|
|
if (additionalParams) {
|
qq.each(additionalParams, function (name, val) {
|
params[name] = val;
|
});
|
}
|
|
if (mandatedParams) {
|
qq.each(mandatedParams, function (name, val) {
|
params[name] = val;
|
});
|
}
|
|
return params;
|
}
|
|
function sendRequest(id) {
|
var xhr = getXhrOrXdr(id),
|
method = options.method,
|
params = getParams(id),
|
url;
|
|
options.onSend(id);
|
|
url = createUrl(id, params);
|
|
// XDR and XHR status detection APIs differ a bit.
|
if (isXdr(xhr)) {
|
xhr.onload = getXdrLoadHandler(id);
|
xhr.onerror = getXdrErrorHandler(id);
|
}
|
else {
|
xhr.onreadystatechange = getXhrReadyStateChangeHandler(id);
|
}
|
|
// The last parameter is assumed to be ignored if we are actually using `XDomainRequest`.
|
xhr.open(method, url, true);
|
|
// Instruct the transport to send cookies along with the CORS request,
|
// unless we are using `XDomainRequest`, which is not capable of this.
|
if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) {
|
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;
|
}
|
}
|
|
// Invoked by the UA to indicate a number of possible states that describe
|
// a live `XMLHttpRequest` transport.
|
function getXhrReadyStateChangeHandler(id) {
|
return function () {
|
if (getXhrOrXdr(id).readyState === 4) {
|
onComplete(id);
|
}
|
};
|
}
|
|
// This will be called by IE to indicate **success** for an associated
|
// `XDomainRequest` transported request.
|
function getXdrLoadHandler(id) {
|
return function () {
|
onComplete(id);
|
}
|
}
|
|
// This will be called by IE to indicate **failure** for an associated
|
// `XDomainRequest` transported request.
|
function getXdrErrorHandler(id) {
|
return function () {
|
onComplete(id, true);
|
}
|
}
|
|
function setHeaders(id) {
|
var xhr = getXhrOrXdr(id),
|
customHeaders = options.customHeaders;
|
|
// If this is a CORS request and a simple method with simple headers are used
|
// on an `XMLHttpRequest`, exclude these specific non-simple headers
|
// in an attempt to prevent preflighting. `XDomainRequest` does not support setting
|
// request headers, so we will take this into account as well.
|
if (isXdr(xhr)) {
|
if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) {
|
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
xhr.setRequestHeader("Cache-Control", "no-cache");
|
}
|
}
|
|
// Assuming that all POST and PUT requests will need to be URL encoded.
|
// The payload of a POST `XDomainRequest` also needs to be URL encoded, but we
|
// can't set the Content-Type when using this transport.
|
if ((options.method === "POST" || options.method === "PUT") && !isXdr(xhr)) {
|
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
}
|
|
// `XDomainRequest` doesn't allow you to set any headers.
|
if (!isXdr(xhr)) {
|
qq.each(customHeaders, function (name, val) {
|
xhr.setRequestHeader(name, val);
|
});
|
}
|
}
|
|
function cancelRequest(id) {
|
var xhr = getXhrOrXdr(id, true),
|
method = options.method;
|
|
if (xhr) {
|
// The event handlers we remove/unregister is dependant on whether we are
|
// using `XDomainRequest` or `XMLHttpRequest`.
|
if (isXdr(xhr)) {
|
xhr.onerror = null;
|
xhr.onload = null;
|
}
|
else {
|
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[options.method], responseCode) >= 0;
|
}
|
|
return {
|
send: function (id, addToPath, additionalParams) {
|
requestState[id] = {
|
addToPath: addToPath,
|
additionalParams: additionalParams
|
};
|
|
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,
|
validMethods = ["POST", "DELETE"],
|
options = {
|
method: "DELETE",
|
uuidParamName: "qquuid",
|
endpointStore: {},
|
maxConnections: 3,
|
customHeaders: {},
|
paramsStore: {},
|
demoMode: false,
|
cors: {
|
expected: false,
|
sendCredentials: false
|
},
|
log: function(str, level) {},
|
onDelete: function(id) {},
|
onDeleteComplete: function(id, xhrOrXdr, isError) {}
|
};
|
|
qq.extend(options, o);
|
|
if (qq.indexOf(validMethods, getNormalizedMethod()) < 0) {
|
throw new Error("'" + getNormalizedMethod() + "' is not a supported method for delete file requests!");
|
}
|
|
function getNormalizedMethod() {
|
return options.method.toUpperCase();
|
}
|
|
function getMandatedParams() {
|
if (getNormalizedMethod() === "POST") {
|
return {
|
"_method": "DELETE"
|
};
|
}
|
|
return {};
|
}
|
|
requestor = new qq.AjaxRequestor({
|
method: getNormalizedMethod(),
|
endpointStore: options.endpointStore,
|
paramsStore: options.paramsStore,
|
mandatedParams: getMandatedParams(),
|
maxConnections: options.maxConnections,
|
customHeaders: options.customHeaders,
|
demoMode: options.demoMode,
|
log: options.log,
|
onSend: options.onDelete,
|
onComplete: options.onDeleteComplete,
|
cors: options.cors
|
});
|
|
|
return {
|
sendDelete: function(id, uuid) {
|
var additionalOptions = {};
|
|
options.log("Submitting delete file request for " + id);
|
|
if (getNormalizedMethod() === "DELETE") {
|
requestor.send(id, uuid);
|
}
|
else {
|
additionalOptions[options.uuidParamName] = uuid;
|
requestor.send(id, null, additionalOptions);
|
}
|
}
|
};
|
};
|
;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, handlerImpl, api;
|
|
// Default options, can be overridden by the user
|
options = {
|
debug: false,
|
forceMultipart: true,
|
paramsInBody: false,
|
paramsStore: {},
|
endpointStore: {},
|
filenameParam: 'qqfilename',
|
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"
|
}
|
},
|
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){},
|
onUuidChanged: function(id, newUuid){}
|
|
};
|
qq.extend(options, o);
|
|
log = options.log;
|
|
/**
|
* Removes element from queue, starts upload of next
|
*/
|
function dequeue(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.supportedFeatures.ajaxUploading) {
|
handlerImpl = new qq.UploadHandlerXhr(options, dequeue, options.onUuidChanged, log);
|
}
|
else {
|
handlerImpl = new qq.UploadHandlerForm(options, dequeue, options.onUuidChanged, log);
|
}
|
|
function cancelSuccess(id) {
|
log('Cancelling ' + id);
|
options.paramsStore.remove(id);
|
dequeue(id);
|
}
|
|
|
api = {
|
/**
|
* 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){
|
handlerImpl.upload(id);
|
return true;
|
}
|
|
return false;
|
},
|
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) {
|
var cancelRetVal = handlerImpl.cancel(id);
|
|
if (qq.isPromise(cancelRetVal)) {
|
cancelRetVal.then(function() {
|
cancelSuccess(id);
|
});
|
}
|
else if (cancelRetVal !== false) {
|
cancelSuccess(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);
|
},
|
// Update/change the name of the associated file.
|
// This updated name should be sent as a parameter.
|
setName: function(id, newName) {
|
handlerImpl.setName(id, newName);
|
},
|
/**
|
* 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);
|
}
|
},
|
reset: function() {
|
log('Resetting upload handler');
|
api.cancelAll();
|
queue = [];
|
handlerImpl.reset();
|
},
|
expunge: function(id) {
|
return handlerImpl.expunge(id);
|
},
|
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 [];
|
}
|
};
|
|
return api;
|
};
|
;/*globals qq, document, setTimeout*/
|
/*globals clearTimeout*/
|
qq.UploadHandlerForm = function(o, uploadCompleteCallback, onUuidChanged, logCallback) {
|
"use strict";
|
|
var options = o,
|
inputs = [],
|
uuids = [],
|
newNames = [],
|
detachLoadEvents = {},
|
postMessageCallbackTimers = {},
|
uploadComplete = uploadCompleteCallback,
|
log = logCallback,
|
corsMessageReceiver = new qq.WindowReceiveMessage({log: log}),
|
onloadCallbacks = {},
|
formHandlerInstanceId = qq.getUniqueId(),
|
api;
|
|
|
function detachLoadEvent(id) {
|
if (detachLoadEvents[id] !== undefined) {
|
detachLoadEvents[id]();
|
delete detachLoadEvents[id];
|
}
|
}
|
|
function registerPostMessageCallback(iframe, callback) {
|
var iframeName = iframe.id,
|
fileId = getFileIdForIframeName(iframeName);
|
|
onloadCallbacks[uuids[fileId]] = callback;
|
|
detachLoadEvents[fileId] = qq(iframe).attach('load', function() {
|
if (inputs[fileId]) {
|
log("Received iframe load event for CORS upload request (iframe name " + iframeName + ")");
|
|
postMessageCallbackTimers[iframeName] = setTimeout(function() {
|
var errorMessage = "No valid message received from loaded iframe for iframe name " + iframeName;
|
log(errorMessage, "error");
|
callback({
|
error: errorMessage
|
});
|
}, 1000);
|
}
|
});
|
|
corsMessageReceiver.receiveMessage(iframeName, function(message) {
|
log("Received the following window message: '" + message + "'");
|
var response = parseResponse(getFileIdForIframeName(iframeName), message),
|
uuid = response.uuid,
|
onloadCallback;
|
|
if (uuid && onloadCallbacks[uuid]) {
|
log("Handling response for iframe name " + iframeName);
|
clearTimeout(postMessageCallbackTimers[iframeName]);
|
delete postMessageCallbackTimers[iframeName];
|
|
detachLoadEvent(iframeName);
|
|
onloadCallback = onloadCallbacks[uuid];
|
|
delete onloadCallbacks[uuid];
|
corsMessageReceiver.stopReceivingMessages(iframeName);
|
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(id, 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 = parseResponse(id, innerHtml);
|
}
|
catch(error) {
|
log('Error when attempting to parse form upload response (' + error + ")", 'error');
|
response = {success: false};
|
}
|
|
return response;
|
}
|
|
function parseResponse(id, innerHtmlOrMessage) {
|
var response;
|
|
try {
|
response = qq.parseJson(innerHtmlOrMessage);
|
|
if (response.newUuid !== undefined) {
|
log("Server requested UUID change from '" + uuids[id] + "' to '" + response.newUuid + "'");
|
uuids[id] = response.newUuid;
|
onUuidChanged(id, response.newUuid);
|
}
|
}
|
catch(error) {
|
log('Error when attempting to parse iframe upload response (' + error + ')', 'error');
|
response = {};
|
}
|
|
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 iframeName = getIframeName(id),
|
iframe = qq.toElement('<iframe src="javascript:false;" name="' + iframeName + '" />');
|
|
iframe.setAttribute('id', iframeName);
|
|
iframe.style.display = 'none';
|
document.body.appendChild(iframe);
|
|
return iframe;
|
}
|
|
/**
|
* Creates form, that will be submitted to iframe
|
*/
|
function createForm(id, iframe){
|
var params = options.paramsStore.getParams(id),
|
protocol = options.demoMode ? "GET" : "POST",
|
form = qq.toElement('<form method="' + protocol + '" enctype="multipart/form-data"></form>'),
|
endpoint = options.endpointStore.getEndpoint(id),
|
url = endpoint;
|
|
params[options.uuidParamName] = uuids[id];
|
|
if (newNames[id] !== undefined) {
|
params[options.filenameParam] = newNames[id];
|
}
|
|
if (!options.paramsInBody) {
|
url = qq.obj2url(params, endpoint);
|
}
|
else {
|
qq.obj2Inputs(params, form);
|
}
|
|
form.setAttribute('action', url);
|
form.setAttribute('target', iframe.name);
|
form.style.display = 'none';
|
document.body.appendChild(form);
|
|
return form;
|
}
|
|
function expungeFile(id) {
|
delete inputs[id];
|
delete uuids[id];
|
delete detachLoadEvents[id];
|
|
if (options.cors.expected) {
|
clearTimeout(postMessageCallbackTimers[id]);
|
delete postMessageCallbackTimers[id];
|
corsMessageReceiver.stopReceivingMessages(id);
|
}
|
|
var iframe = document.getElementById(getIframeName(id));
|
if (iframe) {
|
// to cancel request set src to something else
|
// we use src="javascript:false;" because it doesn't
|
// trigger ie6 prompt on https
|
iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
|
|
qq(iframe).remove();
|
}
|
}
|
|
function getFileIdForIframeName(iframeName) {
|
return iframeName.split("_")[0];
|
}
|
|
function getIframeName(fileId) {
|
return fileId + "_" + formHandlerInstanceId;
|
}
|
|
|
api = {
|
add: function(fileInput) {
|
fileInput.setAttribute('name', options.inputName);
|
|
var id = inputs.push(fileInput) - 1;
|
uuids[id] = qq.getUniqueId();
|
|
// remove file input from DOM
|
if (fileInput.parentNode){
|
qq(fileInput).remove();
|
}
|
|
return id;
|
},
|
getName: function(id) {
|
/*jslint regexp: true*/
|
|
if (newNames[id] !== undefined) {
|
return newNames[id];
|
}
|
else if (api.isValid(id)) {
|
// get input value and remove path to normalize
|
return inputs[id].value.replace(/.*(\/|\\)/, "");
|
}
|
else {
|
log(id + " is not a valid item ID.", "error");
|
}
|
},
|
setName: function(id, newName) {
|
newNames[id] = newName;
|
},
|
isValid: function(id) {
|
return inputs[id] !== undefined;
|
},
|
reset: function() {
|
inputs = [];
|
uuids = [];
|
newNames = [];
|
detachLoadEvents = {};
|
formHandlerInstanceId = qq.getUniqueId();
|
},
|
expunge: function(id) {
|
return expungeFile(id);
|
},
|
getUuid: function(id) {
|
return uuids[id];
|
},
|
cancel: function(id) {
|
var onCancelRetVal = options.onCancel(id, api.getName(id));
|
|
if (qq.isPromise(onCancelRetVal)) {
|
return onCancelRetVal.then(function() {
|
expungeFile(id);
|
});
|
}
|
else if (onCancelRetVal !== false) {
|
expungeFile(id);
|
return true;
|
}
|
|
return false;
|
},
|
|
upload: function(id) {
|
var input = inputs[id],
|
fileName = api.getName(id),
|
iframe = createIframe(id),
|
form;
|
|
if (!input){
|
throw new Error('file with passed id was not added, or already uploaded or cancelled');
|
}
|
|
options.onUpload(id, api.getName(id));
|
|
form = createForm(id, iframe);
|
form.appendChild(input);
|
|
attachLoadEvent(iframe, function(responseFromMessage){
|
log('iframe loaded');
|
|
var response = responseFromMessage ? responseFromMessage : getIframeContentJson(id, iframe);
|
|
detachLoadEvent(id);
|
|
//we can't remove an iframe if the iframe doesn't belong to the same domain
|
if (!options.cors.expected) {
|
qq(iframe).remove();
|
}
|
|
if (!response.success) {
|
if (options.onAutoRetry(id, fileName, response)) {
|
return;
|
}
|
}
|
options.onComplete(id, fileName, response);
|
uploadComplete(id);
|
});
|
|
log('Sending upload request for ' + id);
|
form.submit();
|
qq(form).remove();
|
}
|
};
|
|
return api;
|
};
|
;/*globals qq, File, XMLHttpRequest, FormData, Blob*/
|
qq.UploadHandlerXhr = function(o, uploadCompleteCallback, onUuidChanged, logCallback) {
|
"use strict";
|
|
var options = o,
|
uploadComplete = uploadCompleteCallback,
|
log = logCallback,
|
fileState = [],
|
cookieItemDelimiter = "|",
|
chunkFiles = options.chunking.enabled && qq.supportedFeatures.chunking,
|
resumeEnabled = options.resume.enabled && chunkFiles && qq.supportedFeatures.resume,
|
resumeId = getResumeId(),
|
multipart = options.forceMultipart || options.paramsInBody,
|
api;
|
|
|
function addChunkingSpecificParams(id, params, chunkData) {
|
var size = api.getSize(id),
|
name = api.getName(id);
|
|
params[options.chunking.paramNames.partIndex] = chunkData.part;
|
params[options.chunking.paramNames.partByteOffset] = chunkData.start;
|
params[options.chunking.paramNames.chunkSize] = chunkData.size;
|
params[options.chunking.paramNames.totalParts] = chunkData.count;
|
params[options.totalFileSizeParamName] = size;
|
|
/**
|
* When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
|
* or an empty string. So, we will need to include the actual file name as a param in this case.
|
*/
|
if (multipart) {
|
params[options.filenameParam] = 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,
|
newName = fileState[id].newName;
|
|
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.filenameParam] = blobData.name;
|
}
|
}
|
|
if (newName !== undefined) {
|
params[options.filenameParam] = newName;
|
}
|
|
//build query string
|
if (!options.paramsInBody) {
|
if (!multipart) {
|
params[options.inputName] = newName || name;
|
}
|
url = qq.obj2url(params, endpoint);
|
}
|
|
xhr.open(method, url, true);
|
|
if (options.cors.expected && options.cors.sendCredentials) {
|
xhr.withCredentials = true;
|
}
|
|
if (multipart) {
|
if (options.paramsInBody) {
|
qq.obj2FormData(params, formData);
|
}
|
|
formData.append(options.inputName, fileOrBlob);
|
return formData;
|
}
|
|
return fileOrBlob;
|
}
|
|
function setHeaders(id, xhr) {
|
var extraHeaders = options.customHeaders,
|
fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
|
|
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
xhr.setRequestHeader("Cache-Control", "no-cache");
|
|
if (!multipart) {
|
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
//NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
|
xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
|
}
|
|
qq.each(extraHeaders, function(name, val) {
|
xhr.setRequestHeader(name, val);
|
});
|
}
|
|
function handleCompletedItem(id, response, xhr) {
|
var name = api.getName(id),
|
size = api.getSize(id);
|
|
fileState[id].attemptingResume = false;
|
|
options.onProgress(id, name, size, size);
|
options.onComplete(id, name, response, xhr);
|
|
if (fileState[id]) {
|
delete fileState[id].xhr;
|
}
|
|
uploadComplete(id);
|
}
|
|
function uploadNextChunk(id) {
|
var chunkIdx = fileState[id].remainingChunkIdxs[0],
|
chunkData = getChunkData(id, chunkIdx),
|
xhr = createXhr(id),
|
size = api.getSize(id),
|
name = api.getName(id),
|
toSend, params;
|
|
if (fileState[id].loaded === undefined) {
|
fileState[id].loaded = 0;
|
}
|
|
if (resumeEnabled && fileState[id].file) {
|
persistChunkData(id, chunkData);
|
}
|
|
xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
|
|
xhr.upload.onprogress = function(e) {
|
if (e.lengthComputable) {
|
var totalLoaded = e.loaded + fileState[id].loaded,
|
estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
|
|
options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
|
}
|
};
|
|
options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
|
|
params = options.paramsStore.getParams(id);
|
addChunkingSpecificParams(id, params, chunkData);
|
|
if (fileState[id].attemptingResume) {
|
addResumeSpecificParams(params);
|
}
|
|
toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
|
setHeaders(id, xhr);
|
|
log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
|
xhr.send(toSend);
|
}
|
|
function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
|
var chunkData = getChunkData(id, chunkIdx),
|
blobSize = chunkData.size,
|
overhead = requestSize - blobSize,
|
size = api.getSize(id),
|
chunkCount = chunkData.count,
|
initialRequestOverhead = fileState[id].initialRequestOverhead,
|
overheadDiff = overhead - initialRequestOverhead;
|
|
fileState[id].lastRequestOverhead = overhead;
|
|
if (chunkIdx === 0) {
|
fileState[id].lastChunkIdxProgress = 0;
|
fileState[id].initialRequestOverhead = overhead;
|
fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
|
}
|
else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
|
fileState[id].lastChunkIdxProgress = chunkIdx;
|
fileState[id].estTotalRequestsSize += overheadDiff;
|
}
|
|
return fileState[id].estTotalRequestsSize;
|
}
|
|
function getLastRequestOverhead(id) {
|
if (multipart) {
|
return fileState[id].lastRequestOverhead;
|
}
|
else {
|
return 0;
|
}
|
}
|
|
function handleSuccessfullyCompletedChunk(id, response, xhr) {
|
var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
|
chunkData = getChunkData(id, chunkIdx);
|
|
fileState[id].attemptingResume = false;
|
fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
|
|
if (fileState[id].remainingChunkIdxs.length > 0) {
|
uploadNextChunk(id);
|
}
|
else {
|
if (resumeEnabled) {
|
deletePersistedChunkData(id);
|
}
|
|
handleCompletedItem(id, response, xhr);
|
}
|
}
|
|
function isErrorResponse(xhr, response) {
|
return xhr.status !== 200 || !response.success || response.reset;
|
}
|
|
function parseResponse(id, xhr) {
|
var response;
|
|
try {
|
response = qq.parseJson(xhr.responseText);
|
|
if (response.newUuid !== undefined) {
|
log("Server requested UUID change from '" + fileState[id].uuid + "' to '" + response.newUuid + "'");
|
fileState[id].uuid = response.newUuid;
|
onUuidChanged(id, response.newUuid);
|
}
|
}
|
catch(error) {
|
log('Error when attempting to parse xhr response text (' + error + ')', 'error');
|
response = {};
|
}
|
|
return response;
|
}
|
|
function handleResetResponse(id) {
|
log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
|
|
if (resumeEnabled) {
|
deletePersistedChunkData(id);
|
fileState[id].attemptingResume = false;
|
}
|
|
fileState[id].remainingChunkIdxs = [];
|
delete fileState[id].loaded;
|
delete fileState[id].estTotalRequestsSize;
|
delete fileState[id].initialRequestOverhead;
|
}
|
|
function handleResetResponseOnResumeAttempt(id) {
|
fileState[id].attemptingResume = false;
|
log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
|
handleResetResponse(id);
|
api.upload(id, true);
|
}
|
|
function handleNonResetErrorResponse(id, response, xhr) {
|
var name = api.getName(id);
|
|
if (options.onAutoRetry(id, name, response, xhr)) {
|
return;
|
}
|
else {
|
handleCompletedItem(id, response, xhr);
|
}
|
}
|
|
function onComplete(id, xhr) {
|
var response;
|
|
// the request was aborted/cancelled
|
if (!fileState[id]) {
|
return;
|
}
|
|
log("xhr - server response received for " + id);
|
log("responseText = " + xhr.responseText);
|
response = parseResponse(id, xhr);
|
|
if (isErrorResponse(xhr, response)) {
|
if (response.reset) {
|
handleResetResponse(id);
|
}
|
|
if (fileState[id].attemptingResume && response.reset) {
|
handleResetResponseOnResumeAttempt(id);
|
}
|
else {
|
handleNonResetErrorResponse(id, response, xhr);
|
}
|
}
|
else if (chunkFiles) {
|
handleSuccessfullyCompletedChunk(id, response, xhr);
|
}
|
else {
|
handleCompletedItem(id, response, xhr);
|
}
|
}
|
|
function getChunkDataForCallback(chunkData) {
|
return {
|
partIndex: chunkData.part,
|
startByte: chunkData.start + 1,
|
endByte: chunkData.end,
|
totalParts: chunkData.count
|
};
|
}
|
|
function getReadyStateChangeHandler(id, xhr) {
|
return function() {
|
if (xhr.readyState === 4) {
|
onComplete(id, xhr);
|
}
|
};
|
}
|
|
function persistChunkData(id, chunkData) {
|
var fileUuid = api.getUuid(id),
|
lastByteSent = fileState[id].loaded,
|
initialRequestOverhead = fileState[id].initialRequestOverhead,
|
estTotalRequestsSize = fileState[id].estTotalRequestsSize,
|
cookieName = getChunkDataCookieName(id),
|
cookieValue = fileUuid +
|
cookieItemDelimiter + chunkData.part +
|
cookieItemDelimiter + lastByteSent +
|
cookieItemDelimiter + initialRequestOverhead +
|
cookieItemDelimiter + estTotalRequestsSize,
|
cookieExpDays = options.resume.cookiesExpireIn;
|
|
qq.setCookie(cookieName, cookieValue, cookieExpDays);
|
}
|
|
function deletePersistedChunkData(id) {
|
if (fileState[id].file) {
|
var cookieName = getChunkDataCookieName(id);
|
qq.deleteCookie(cookieName);
|
}
|
}
|
|
function getPersistedChunkData(id) {
|
var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
|
filename = api.getName(id),
|
sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
|
|
if (chunkCookieValue) {
|
sections = chunkCookieValue.split(cookieItemDelimiter);
|
|
if (sections.length === 5) {
|
uuid = sections[0];
|
partIndex = parseInt(sections[1], 10);
|
lastByteSent = parseInt(sections[2], 10);
|
initialRequestOverhead = parseInt(sections[3], 10);
|
estTotalRequestsSize = parseInt(sections[4], 10);
|
|
return {
|
uuid: uuid,
|
part: partIndex,
|
lastByteSent: lastByteSent,
|
initialRequestOverhead: initialRequestOverhead,
|
estTotalRequestsSize: estTotalRequestsSize
|
};
|
}
|
else {
|
log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
|
}
|
}
|
}
|
|
function getChunkDataCookieName(id) {
|
var filename = api.getName(id),
|
fileSize = api.getSize(id),
|
maxChunkSize = options.chunking.partSize,
|
cookieName;
|
|
cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
|
|
if (resumeId !== undefined) {
|
cookieName += cookieItemDelimiter + resumeId;
|
}
|
|
return cookieName;
|
}
|
|
function getResumeId() {
|
if (options.resume.id !== null &&
|
options.resume.id !== undefined &&
|
!qq.isFunction(options.resume.id) &&
|
!qq.isObject(options.resume.id)) {
|
|
return options.resume.id;
|
}
|
}
|
|
function calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex) {
|
var currentChunkIndex;
|
|
for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
|
fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
|
}
|
|
uploadNextChunk(id);
|
}
|
|
function onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume) {
|
firstChunkIndex = persistedChunkInfoForResume.part;
|
fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
|
fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
|
fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
|
fileState[id].attemptingResume = true;
|
log('Resuming ' + name + " at partition index " + firstChunkIndex);
|
|
calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
|
}
|
|
function handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex) {
|
var name = api.getName(id),
|
firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part),
|
onResumeRetVal;
|
|
onResumeRetVal = options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume));
|
if (qq.isPromise(onResumeRetVal)) {
|
log("Waiting for onResume promise to be fulfilled for " + id);
|
onResumeRetVal.then(
|
function() {
|
onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume);
|
},
|
function() {
|
log("onResume promise fulfilled - failure indicated. Will not resume.")
|
calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
|
}
|
);
|
}
|
else if (onResumeRetVal !== false) {
|
onResumeSuccess(id, name, firstChunkIndex, persistedChunkInfoForResume);
|
}
|
else {
|
log("onResume callback returned false. Will not resume.");
|
calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
|
}
|
}
|
|
function handleFileChunkingUpload(id, retry) {
|
var firstChunkIndex = 0,
|
persistedChunkInfoForResume;
|
|
if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
|
fileState[id].remainingChunkIdxs = [];
|
|
if (resumeEnabled && !retry && fileState[id].file) {
|
persistedChunkInfoForResume = getPersistedChunkData(id);
|
if (persistedChunkInfoForResume) {
|
handlePossibleResumeAttempt(id, persistedChunkInfoForResume, firstChunkIndex);
|
}
|
else {
|
calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
|
}
|
}
|
else {
|
calculateRemainingChunkIdxsAndUpload(id, firstChunkIndex);
|
}
|
}
|
else {
|
uploadNextChunk(id);
|
}
|
}
|
|
function handleStandardFileUpload(id) {
|
var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
|
name = api.getName(id),
|
xhr, params, toSend;
|
|
fileState[id].loaded = 0;
|
|
xhr = createXhr(id);
|
|
xhr.upload.onprogress = function(e){
|
if (e.lengthComputable){
|
fileState[id].loaded = e.loaded;
|
options.onProgress(id, name, e.loaded, e.total);
|
}
|
};
|
|
xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
|
|
params = options.paramsStore.getParams(id);
|
toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
|
setHeaders(id, xhr);
|
|
log('Sending upload request for ' + id);
|
xhr.send(toSend);
|
}
|
|
function expungeItem(id) {
|
var xhr = fileState[id].xhr;
|
|
if (xhr) {
|
xhr.onreadystatechange = null;
|
xhr.abort();
|
}
|
|
if (resumeEnabled) {
|
deletePersistedChunkData(id);
|
}
|
|
delete fileState[id];
|
}
|
|
api = {
|
/**
|
* Adds File or Blob to the queue
|
* Returns id to use with upload, cancel
|
**/
|
add: function(fileOrBlobData){
|
var id, persistedChunkData,
|
uuid = qq.getUniqueId();
|
|
if (qq.isFile(fileOrBlobData)) {
|
id = fileState.push({file: fileOrBlobData}) - 1;
|
}
|
else if (qq.isBlob(fileOrBlobData.blob)) {
|
id = fileState.push({blobData: fileOrBlobData}) - 1;
|
}
|
else {
|
throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
|
}
|
|
if (resumeEnabled) {
|
persistedChunkData = getPersistedChunkData(id);
|
|
if (persistedChunkData) {
|
uuid = persistedChunkData.uuid;
|
}
|
}
|
|
fileState[id].uuid = uuid;
|
|
return id;
|
},
|
getName: function(id) {
|
if (api.isValid(id)) {
|
var file = fileState[id].file,
|
blobData = fileState[id].blobData,
|
newName = fileState[id].newName;
|
|
if (newName !== undefined) {
|
return newName;
|
}
|
else if (file) {
|
// fix missing name in Safari 4
|
//NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
|
return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
|
}
|
else {
|
return blobData.name;
|
}
|
}
|
else {
|
log(id + " is not a valid item ID.", "error");
|
}
|
},
|
setName: function(id, newName) {
|
fileState[id].newName = newName;
|
},
|
getSize: function(id) {
|
/*jshint eqnull: true*/
|
var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
|
|
if (qq.isFileOrInput(fileOrBlob)) {
|
return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
|
}
|
else {
|
return fileOrBlob.size;
|
}
|
},
|
getFile: function(id) {
|
if (fileState[id]) {
|
return fileState[id].file || fileState[id].blobData.blob;
|
}
|
},
|
isValid: function(id) {
|
return fileState[id] !== undefined;
|
},
|
reset: function() {
|
fileState = [];
|
},
|
expunge: function(id) {
|
return expungeItem(id);
|
},
|
getUuid: function(id) {
|
return fileState[id].uuid;
|
},
|
/**
|
* Sends the file identified by id to the server
|
*/
|
upload: function(id, retry) {
|
var name = this.getName(id);
|
|
if (this.isValid(id)) {
|
options.onUpload(id, name);
|
|
if (chunkFiles) {
|
handleFileChunkingUpload(id, retry);
|
}
|
else {
|
handleStandardFileUpload(id);
|
}
|
}
|
},
|
cancel: function(id) {
|
var onCancelRetVal = options.onCancel(id, this.getName(id));
|
|
if (qq.isPromise(onCancelRetVal)) {
|
return onCancelRetVal.then(function() {
|
expungeItem(id);
|
});
|
}
|
else if (onCancelRetVal !== false) {
|
expungeItem(id);
|
return true;
|
}
|
|
return false;
|
},
|
getResumableFilesData: function() {
|
var matchingCookieNames = [],
|
resumableFilesData = [];
|
|
if (chunkFiles && resumeEnabled) {
|
if (resumeId === undefined) {
|
matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
|
cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
|
}
|
else {
|
matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
|
cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
|
cookieItemDelimiter + resumeId + "="));
|
}
|
|
qq.each(matchingCookieNames, function(idx, cookieName) {
|
var cookiesNameParts = cookieName.split(cookieItemDelimiter);
|
var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
|
|
resumableFilesData.push({
|
name: decodeURIComponent(cookiesNameParts[1]),
|
size: cookiesNameParts[2],
|
uuid: cookieValueParts[0],
|
partIdx: cookieValueParts[1]
|
});
|
});
|
|
return resumableFilesData;
|
}
|
return [];
|
}
|
};
|
|
return api;
|
};
|
;// Base handler for UI (FineUploader mode) events.
|
// Some more specific handlers inherit from this one.
|
qq.UiEventHandler = function(s, protectedApi) {
|
"use strict";
|
|
var disposer = new qq.DisposeSupport(),
|
spec = {
|
eventType: 'click',
|
attachTo: null,
|
onHandled: function(target, event) {}
|
},
|
// This makes up the "public" API methods that will be accessible
|
// to instances constructing a base or child handler
|
publicApi = {
|
addHandler: function(element) {
|
addHandler(element);
|
},
|
|
dispose: function() {
|
disposer.dispose();
|
}
|
};
|
|
|
|
function addHandler(element) {
|
disposer.attach(element, spec.eventType, function(event) {
|
// Only in IE: the `event` is a property of the `window`.
|
event = event || window.event;
|
|
// On older browsers, we must check the `srcElement` instead of the `target`.
|
var target = event.target || event.srcElement;
|
|
spec.onHandled(target, event);
|
});
|
}
|
|
// These make up the "protected" API methods that children of this base handler will utilize.
|
qq.extend(protectedApi, {
|
// Find the ID of the associated file by looking for an
|
// expando property present on each file item in the DOM.
|
getItemFromEventTarget: function(target) {
|
var item = target.parentNode;
|
|
while(item.qqFileId === undefined) {
|
item = item.parentNode;
|
}
|
|
return item;
|
},
|
|
getFileIdFromItem: function(item) {
|
return item.qqFileId;
|
},
|
|
getDisposeSupport: function() {
|
return disposer;
|
}
|
});
|
|
|
qq.extend(spec, s);
|
|
if (spec.attachTo) {
|
addHandler(spec.attachTo);
|
}
|
|
return publicApi;
|
};
|
;qq.DeleteRetryOrCancelClickHandler = function(s) {
|
"use strict";
|
|
var inheritedInternalApi = {},
|
spec = {
|
listElement: document,
|
log: function(message, lvl) {},
|
classes: {
|
cancel: 'qq-upload-cancel',
|
deleteButton: 'qq-upload-delete',
|
retry: 'qq-upload-retry'
|
},
|
onDeleteFile: function(fileId) {},
|
onCancel: function(fileId) {},
|
onRetry: function(fileId) {},
|
onGetName: function(fileId) {}
|
};
|
|
function examineEvent(target, event) {
|
if (qq(target).hasClass(spec.classes.cancel)
|
|| qq(target).hasClass(spec.classes.retry)
|
|| qq(target).hasClass(spec.classes.deleteButton)) {
|
|
var item = inheritedInternalApi.getItemFromEventTarget(target),
|
fileId = inheritedInternalApi.getFileIdFromItem(item);
|
|
qq.preventDefault(event);
|
|
spec.log(qq.format("Detected valid cancel, retry, or delete click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
|
deleteRetryOrCancel(target, fileId);
|
}
|
}
|
|
function deleteRetryOrCancel(target, fileId) {
|
if (qq(target).hasClass(spec.classes.deleteButton)) {
|
spec.onDeleteFile(fileId);
|
}
|
else if (qq(target).hasClass(spec.classes.cancel)) {
|
spec.onCancel(fileId);
|
}
|
else {
|
spec.onRetry(fileId);
|
}
|
}
|
|
qq.extend(spec, s);
|
|
spec.eventType = 'click';
|
spec.onHandled = examineEvent;
|
spec.attachTo = spec.listElement;
|
|
qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
|
};
|
;// Handles edit-related events on a file item (FineUploader mode). This is meant to be a parent handler.
|
// Children will delegate to this handler when specific edit-related actions are detected.
|
qq.FilenameEditHandler = function(s, inheritedInternalApi) {
|
"use strict";
|
|
var spec = {
|
listElement: null,
|
log: function(message, lvl) {},
|
classes: {
|
file: 'qq-upload-file'
|
},
|
onGetUploadStatus: function(fileId) {},
|
onGetName: function(fileId) {},
|
onSetName: function(fileId, newName) {},
|
onGetInput: function(item) {},
|
onEditingStatusChange: function(fileId, isEditing) {}
|
},
|
publicApi;
|
|
function getFilenameSansExtension(fileId) {
|
var filenameSansExt = spec.onGetName(fileId),
|
extIdx = filenameSansExt.lastIndexOf('.');
|
|
if (extIdx > 0) {
|
filenameSansExt = filenameSansExt.substr(0, extIdx);
|
}
|
|
return filenameSansExt;
|
}
|
|
function getOriginalExtension(fileId) {
|
var origName = spec.onGetName(fileId),
|
extIdx = origName.lastIndexOf('.');
|
|
if (extIdx > 0) {
|
return origName.substr(extIdx, origName.length - extIdx);
|
}
|
}
|
|
// Callback iff the name has been changed
|
function handleNameUpdate(newFilenameInputEl, fileId) {
|
var newName = newFilenameInputEl.value,
|
origExtension;
|
|
if (newName !== undefined && qq.trimStr(newName).length > 0) {
|
origExtension = getOriginalExtension(fileId);
|
|
if (origExtension !== undefined) {
|
newName = newName + getOriginalExtension(fileId);
|
}
|
|
spec.onSetName(fileId, newName);
|
}
|
|
spec.onEditingStatusChange(fileId, false);
|
}
|
|
// The name has been updated if the filename edit input loses focus.
|
function registerInputBlurHandler(inputEl, fileId) {
|
inheritedInternalApi.getDisposeSupport().attach(inputEl, 'blur', function() {
|
handleNameUpdate(inputEl, fileId)
|
});
|
}
|
|
// The name has been updated if the user presses enter.
|
function registerInputEnterKeyHandler(inputEl, fileId) {
|
inheritedInternalApi.getDisposeSupport().attach(inputEl, 'keyup', function(event) {
|
|
var code = event.keyCode || event.which;
|
|
if (code === 13) {
|
handleNameUpdate(inputEl, fileId)
|
}
|
});
|
}
|
|
qq.extend(spec, s);
|
|
spec.attachTo = spec.listElement;
|
|
publicApi = qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));
|
|
qq.extend(inheritedInternalApi, {
|
handleFilenameEdit: function(fileId, target, item, focusInput) {
|
var newFilenameInputEl = spec.onGetInput(item);
|
|
spec.onEditingStatusChange(fileId, true);
|
|
newFilenameInputEl.value = getFilenameSansExtension(fileId);
|
|
if (focusInput) {
|
newFilenameInputEl.focus();
|
}
|
|
registerInputBlurHandler(newFilenameInputEl, fileId);
|
registerInputEnterKeyHandler(newFilenameInputEl, fileId);
|
}
|
});
|
|
return publicApi;
|
};
|
;// Child of FilenameEditHandler. Used to detect click events on filename display elements.
|
qq.FilenameClickHandler = function(s) {
|
"use strict";
|
|
var inheritedInternalApi = {},
|
spec = {
|
log: function(message, lvl) {},
|
classes: {
|
file: 'qq-upload-file',
|
editNameIcon: 'qq-edit-filename-icon'
|
},
|
onGetUploadStatus: function(fileId) {},
|
onGetName: function(fileId) {}
|
};
|
|
qq.extend(spec, s);
|
|
// This will be called by the parent handler when a `click` event is received on the list element.
|
function examineEvent(target, event) {
|
if (qq(target).hasClass(spec.classes.file) || qq(target).hasClass(spec.classes.editNameIcon)) {
|
var item = inheritedInternalApi.getItemFromEventTarget(target),
|
fileId = inheritedInternalApi.getFileIdFromItem(item),
|
status = spec.onGetUploadStatus(fileId);
|
|
// We only allow users to change filenames of files that have been submitted but not yet uploaded.
|
if (status === qq.status.SUBMITTED) {
|
spec.log(qq.format("Detected valid filename click event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
|
qq.preventDefault(event);
|
|
inheritedInternalApi.handleFilenameEdit(fileId, target, item, true);
|
}
|
}
|
}
|
|
spec.eventType = 'click';
|
spec.onHandled = examineEvent;
|
|
return qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
|
};
|
;// Child of FilenameEditHandler. Used to detect focusin events on file edit input elements.
|
qq.FilenameInputFocusInHandler = function(s, inheritedInternalApi) {
|
"use strict";
|
|
var spec = {
|
listElement: null,
|
classes: {
|
editFilenameInput: 'qq-edit-filename'
|
},
|
onGetUploadStatus: function(fileId) {},
|
log: function(message, lvl) {}
|
};
|
|
if (!inheritedInternalApi) {
|
inheritedInternalApi = {};
|
}
|
|
// This will be called by the parent handler when a `focusin` event is received on the list element.
|
function handleInputFocus(target, event) {
|
if (qq(target).hasClass(spec.classes.editFilenameInput)) {
|
var item = inheritedInternalApi.getItemFromEventTarget(target),
|
fileId = inheritedInternalApi.getFileIdFromItem(item),
|
status = spec.onGetUploadStatus(fileId);
|
|
if (status === qq.status.SUBMITTED) {
|
spec.log(qq.format("Detected valid filename input focus event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
|
inheritedInternalApi.handleFilenameEdit(fileId, target, item);
|
}
|
}
|
}
|
|
spec.eventType = 'focusin';
|
spec.onHandled = handleInputFocus;
|
|
qq.extend(spec, s);
|
|
return qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
|
};
|
;/**
|
* Child of FilenameInputFocusInHandler. Used to detect focus events on file edit input elements. This child module is only
|
* needed for UAs that do not support the focusin event. Currently, only Firefox lacks this event.
|
*
|
* @param spec Overrides for default specifications
|
*/
|
qq.FilenameInputFocusHandler = function(spec) {
|
"use strict";
|
|
spec.eventType = 'focus';
|
spec.attachTo = null;
|
|
return qq.extend(this, new qq.FilenameInputFocusInHandler(spec, {}));
|
};
|
;/*globals jQuery, qq*/
|
(function($) {
|
"use strict";
|
var uploader, $el, init, dataStore, pluginOption, pluginOptions, addCallbacks, transformVariables, isValidCommand,
|
delegateCommand;
|
|
pluginOptions = ['uploaderType'];
|
|
init = function (options) {
|
if (options) {
|
var xformedOpts = transformVariables(options);
|
addCallbacks(xformedOpts);
|
|
if (pluginOption('uploaderType') === 'basic') {
|
uploader(new qq.FineUploaderBasic(xformedOpts));
|
}
|
else {
|
uploader(new qq.FineUploader(xformedOpts));
|
}
|
}
|
|
return $el;
|
};
|
|
dataStore = function(key, val) {
|
var data = $el.data('fineuploader');
|
|
if (val) {
|
if (data === undefined) {
|
data = {};
|
}
|
data[key] = val;
|
$el.data('fineuploader', data);
|
}
|
else {
|
if (data === undefined) {
|
return null;
|
}
|
return data[key];
|
}
|
};
|
|
//the underlying Fine Uploader instance is stored in jQuery's data stored, associated with the element
|
// tied to this instance of the plug-in
|
uploader = function(instanceToStore) {
|
return dataStore('uploader', instanceToStore);
|
};
|
|
pluginOption = function(option, optionVal) {
|
return dataStore(option, optionVal);
|
};
|
|
//implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and
|
// return the result of executing the bound handler back to Fine Uploader
|
addCallbacks = function(transformedOpts) {
|
var callbacks = transformedOpts.callbacks = {},
|
uploaderInst = new qq.FineUploaderBasic();
|
|
$.each(uploaderInst._options.callbacks, function(prop, func) {
|
var name, $callbackEl;
|
|
name = /^on(\w+)/.exec(prop)[1];
|
name = name.substring(0, 1).toLowerCase() + name.substring(1);
|
$callbackEl = $el;
|
|
callbacks[prop] = function() {
|
var args = Array.prototype.slice.call(arguments);
|
|
return $callbackEl.triggerHandler(name, args);
|
};
|
});
|
};
|
|
//transform jQuery objects into HTMLElements, and pass along all other option properties
|
transformVariables = function(source, dest) {
|
var xformed, arrayVals;
|
|
if (dest === undefined) {
|
if (source.uploaderType !== 'basic') {
|
xformed = { element : $el[0] };
|
}
|
else {
|
xformed = {};
|
}
|
}
|
else {
|
xformed = dest;
|
}
|
|
$.each(source, function(prop, val) {
|
if ($.inArray(prop, pluginOptions) >= 0) {
|
pluginOption(prop, val);
|
}
|
else if (val instanceof $) {
|
xformed[prop] = val[0];
|
}
|
else if ($.isPlainObject(val)) {
|
xformed[prop] = {};
|
transformVariables(val, xformed[prop]);
|
}
|
else if ($.isArray(val)) {
|
arrayVals = [];
|
$.each(val, function(idx, arrayVal) {
|
if (arrayVal instanceof $) {
|
$.merge(arrayVals, arrayVal);
|
}
|
else {
|
arrayVals.push(arrayVal);
|
}
|
});
|
xformed[prop] = arrayVals;
|
}
|
else {
|
xformed[prop] = val;
|
}
|
});
|
|
if (dest === undefined) {
|
return xformed;
|
}
|
};
|
|
isValidCommand = function(command) {
|
return $.type(command) === "string" &&
|
!command.match(/^_/) && //enforce private methods convention
|
uploader()[command] !== undefined;
|
};
|
|
//assuming we have already verified that this is a valid command, call the associated function in the underlying
|
// Fine Uploader instance (passing along the arguments from the caller) and return the result of the call back to the caller
|
delegateCommand = function(command) {
|
var xformedArgs = [],
|
origArgs = Array.prototype.slice.call(arguments, 1),
|
retVal;
|
|
transformVariables(origArgs, xformedArgs);
|
|
retVal = uploader()[command].apply(uploader(), xformedArgs);
|
|
// If the command is returning an `HTMLElement` or `HTMLDocument`, wrap it in a `jQuery` object
|
if(typeof retVal === "object"
|
&& (retVal.nodeType === 1 || retVal.nodeType === 9)
|
&& retVal.cloneNode) {
|
|
retVal = $(retVal);
|
}
|
|
return retVal;
|
};
|
|
$.fn.fineUploader = function(optionsOrCommand) {
|
var self = this, selfArgs = arguments, retVals = [];
|
|
this.each(function(index, el) {
|
$el = $(el);
|
|
if (uploader() && isValidCommand(optionsOrCommand)) {
|
retVals.push(delegateCommand.apply(self, selfArgs));
|
|
if (self.length === 1) {
|
return false;
|
}
|
}
|
else if (typeof optionsOrCommand === 'object' || !optionsOrCommand) {
|
init.apply(self, selfArgs);
|
}
|
else {
|
$.error('Method ' + optionsOrCommand + ' does not exist on jQuery.fineUploader');
|
}
|
});
|
|
if (retVals.length === 1) {
|
return retVals[0];
|
}
|
else if (retVals.length > 1) {
|
return retVals;
|
}
|
|
return this;
|
};
|
|
}(jQuery));
|
;/*globals jQuery, qq*/
|
(function($) {
|
"use strict";
|
var rootDataKey = "fineUploaderDnd",
|
$el;
|
|
function init (options) {
|
if (!options) {
|
options = {};
|
}
|
|
options.dropZoneElements = [$el];
|
var xformedOpts = transformVariables(options);
|
addCallbacks(xformedOpts);
|
dnd(new qq.DragAndDrop(xformedOpts));
|
|
return $el;
|
};
|
|
function dataStore(key, val) {
|
var data = $el.data(rootDataKey);
|
|
if (val) {
|
if (data === undefined) {
|
data = {};
|
}
|
data[key] = val;
|
$el.data(rootDataKey, data);
|
}
|
else {
|
if (data === undefined) {
|
return null;
|
}
|
return data[key];
|
}
|
};
|
|
function dnd(instanceToStore) {
|
return dataStore('dndInstance', instanceToStore);
|
};
|
|
function addCallbacks(transformedOpts) {
|
var callbacks = transformedOpts.callbacks = {},
|
dndInst = new qq.FineUploaderBasic();
|
|
$.each(new qq.DragAndDrop.callbacks(), function(prop, func) {
|
var name = prop,
|
$callbackEl;
|
|
$callbackEl = $el;
|
|
callbacks[prop] = function() {
|
var args = Array.prototype.slice.call(arguments),
|
jqueryHandlerResult = $callbackEl.triggerHandler(name, args);
|
|
return jqueryHandlerResult;
|
};
|
});
|
};
|
|
//transform jQuery objects into HTMLElements, and pass along all other option properties
|
function transformVariables(source, dest) {
|
var xformed, arrayVals;
|
|
if (dest === undefined) {
|
xformed = {};
|
}
|
else {
|
xformed = dest;
|
}
|
|
$.each(source, function(prop, val) {
|
if (val instanceof $) {
|
xformed[prop] = val[0];
|
}
|
else if ($.isPlainObject(val)) {
|
xformed[prop] = {};
|
transformVariables(val, xformed[prop]);
|
}
|
else if ($.isArray(val)) {
|
arrayVals = [];
|
$.each(val, function(idx, arrayVal) {
|
if (arrayVal instanceof $) {
|
$.merge(arrayVals, arrayVal);
|
}
|
else {
|
arrayVals.push(arrayVal);
|
}
|
});
|
xformed[prop] = arrayVals;
|
}
|
else {
|
xformed[prop] = val;
|
}
|
});
|
|
if (dest === undefined) {
|
return xformed;
|
}
|
};
|
|
function isValidCommand(command) {
|
return $.type(command) === "string" &&
|
command === "dispose" &&
|
dnd()[command] !== undefined;
|
};
|
|
function delegateCommand(command) {
|
var xformedArgs = [], origArgs = Array.prototype.slice.call(arguments, 1);
|
transformVariables(origArgs, xformedArgs);
|
return dnd()[command].apply(dnd(), xformedArgs);
|
};
|
|
$.fn.fineUploaderDnd = function(optionsOrCommand) {
|
var self = this, selfArgs = arguments, retVals = [];
|
|
this.each(function(index, el) {
|
$el = $(el);
|
|
if (dnd() && isValidCommand(optionsOrCommand)) {
|
retVals.push(delegateCommand.apply(self, selfArgs));
|
|
if (self.length === 1) {
|
return false;
|
}
|
}
|
else if (typeof optionsOrCommand === 'object' || !optionsOrCommand) {
|
init.apply(self, selfArgs);
|
}
|
else {
|
$.error("Method " + optionsOrCommand + " does not exist in Fine Uploader's DnD module.");
|
}
|
});
|
|
if (retVals.length === 1) {
|
return retVals[0];
|
}
|
else if (retVals.length > 1) {
|
return retVals;
|
}
|
|
return this;
|
};
|
|
}(jQuery));
|
|
/*! 2013-07-24 */
|