wblog/vendor/assets/javascripts/jquery.html5-fileupload.js

514 lines
18 KiB
JavaScript

/*
* jQuery HTML5 File Upload
*
* Author: timdream at gmail.com
* Web: http://timc.idv.tw/html5-file-upload/
*
* Ajax File Upload that use real xhr,
* built with getAsBinary, sendAsBinary, FormData, FileReader, ArrayBuffer, BlobBuilder and etc.
* works in Firefox 3, Chrome 5, Safari 5 and higher
*
* Image resizing and uploading currently works in Fx 3 and up, and Chrome 9 (dev) and up only.
* Extra settings will allow current Webkit users to upload the original image
* or send the resized image in base64 form.
*
* Usage:
* $.fileUploadSupported // a boolean value indicates if the browser is supported.
* $.imageUploadSupported // a boolean value indicates if the browser could resize image and upload in binary form.
* $.fileUploadAsBase64Supported // a boolean value indicate if the browser upload files in based64.
* $.imageUploadAsBase64Supported // a boolean value indicate if the browser could resize image and upload in based64.
* $('input[type=file]').fileUpload(ajaxSettings); //Make a input[type=file] select-and-send file upload widget
* $('#any-element').fileUpload(ajaxSettings); //Make a element receive dropped file
* //TBD $('form#fileupload').fileUpload(ajaxSettings); //Send a ajax form with file
* //TBD $('canvas').fileUpload(ajaxSettings); //Upload given canvas as if it's an png image.
*
* ajaxSettings is the object contains $.ajax settings that will be passed to.
* Available extended settings are:
* fileType:
* regexp check against filename extension; You should always checked it again on server-side.
* e.g. /^(gif|jpe?g|png|tiff?)$/i for images
* fileMaxSize:
* Maxium file size allowed in bytes. Use scientific notation for converience.
* e.g. 1E4 for 1KB, 1E8 for 1MB, 1E9 for 10MB.
* If you really care the difference between 1024 and 1000, use Math.pow(2, 10)
* fileError(info, textStatus, textDescription):
* callback function when there is any error preventing file upload to start,
* $.ajax and ajax events won't be called when error.
* Use $.noop to overwrite default alert function.
* imageMaxWidth, imageMaxHeight:
* Use any of the two settings to enable client-size image resizing.
* Image will be resized to fit into given rectangle.
* File size and type limit checking will be ignored.
* allowUploadOriginalImage:
* Set to true if you accept original image to be uploaded as a fallback
* when image resizing functionality is not availible (such as Webkit browsers).
* File size and type limit will be enforced.
* allowDataInBase64:
* Alternatively, you may wish to resize the image anyway and send the data
* in base64. The data will be 133% larger and you will need to process it further with
* server-side script.
* This setting might work with browsers which could read file but cannot send it in original
* binary (no known browser are designed this way though)
* forceResize:
* Set to true will cause the image being re-sampled even if the resized image
* has the same demension as the original one.
* imageType:
* Acceptable values are: 'jpeg', 'png', or 'auto'.
*
* TBD:
* ability to change settings after binding (you can unbind and bind again as a workaround)
* multipole file handling
* form intergation
*
*/
(function($) {
// Don't do logging if window.log function does not exist.
var log = window.log || $.noop;
// jQuery.ajax config
var config = {
fileError: function (info, textStatus, textDescription) {
window.alert(textDescription);
}
};
// Feature detection
// Read as binary string: FileReader API || Gecko-specific function (Fx3)
var canReadAsBinaryString = (window.FileReader || window.File.prototype.getAsBinary);
// Read file using FormData interface
var canReadFormData = !!(window.FormData);
// Read file into data: URL: FileReader API || Gecko-specific function (Fx3)
var canReadAsBase64 = (window.FileReader || window.File.prototype.getAsDataURL);
var canResizeImageToBase64 = !!(document.createElement('canvas').toDataURL);
var canResizeImageToBinaryString = canResizeImageToBase64 && window.atob;
var canResizeImageToFile = !!(document.createElement('canvas').mozGetAsFile);
// Send file in multipart/form-data with binary xhr (Gecko-specific function)
// || xhr.send(blob) that sends blob made with ArrayBuffer.
var canSendBinaryString = (
(window.XMLHttpRequest && window.XMLHttpRequest.prototype.sendAsBinary)
|| (window.ArrayBuffer && window.BlobBuilder)
);
// Send file as in FormData object
var canSendFormData = !!(window.FormData);
// Send image base64 data by extracting data: URL
var canSendImageInBase64 = !!(document.createElement('canvas').toDataURL);
var isSupported = (
(canReadAsBinaryString && canSendBinaryString)
|| (canReadFormData && canSendFormData)
);
var isImageSupported = (
canReadAsBase64 && (
(canResizeImageToBinaryString && canSendBinaryString)
|| (canResizeImageToFile && canSendFormData)
)
);
var isSupportedInBase64 = canReadAsBase64;
var isImageSupportedInBase64 = canReadAsBase64 && canResizeImageToBase64;
var dataURLtoBase64 = function (dataurl) {
return dataurl.substring(dataurl.indexOf(',')+1, dataurl.length);
}
// Step 1: check file info and attempt to read the file
// paramaters: Ajax settings, File object
var handleFile = function (settings, file) {
var info = {
// properties of standard File object || Gecko 1.9 properties
type: file.type || '', // MIME type
size: file.size || file.fileSize,
name: file.name || file.fileName
};
settings.resizeImage = !!(settings.imageMaxWidth || settings.imageMaxHeight);
if (settings.resizeImage && !isImageSupported && settings.allowUploadOriginalImage) {
log('WARN: Fall back to upload original un-resized image.');
settings.resizeImage = false;
}
if (settings.resizeImage) {
settings.imageMaxWidth = settings.imageMaxWidth || Infinity;
settings.imageMaxHeight = settings.imageMaxHeight || Infinity;
}
if (!settings.resizeImage) {
if (settings.fileType && settings.fileType.test) {
// Not using MIME types
if (!settings.fileType.test(info.name.substr(info.name.lastIndexOf('.')+1))) {
log('ERROR: Invalid Filetype.');
settings.fileError.call(this, info, 'INVALID_FILETYPE', 'Invalid filetype.');
return;
}
}
if (settings.fileMaxSize && file.size > settings.fileMaxSize) {
log('ERROR: File exceeds size limit.');
settings.fileError.call(this, info, 'FILE_EXCEEDS_SIZE_LIMIT', 'File exceeds size limit.');
return;
}
}
if (!settings.resizeImage && canReadFormData) {
log('INFO: Bypass file reading, insert file object into FormData object directly.');
handleForm(settings, 'file', file, info);
} else if (window.FileReader) {
log('INFO: Using FileReader to do asynchronously file reading.');
var reader = new FileReader();
reader.onerror = function (ev) {
if (ev.target.error) {
switch (ev.target.error) {
case 8:
log('ERROR: File not found.');
settings.fileError.call(this, info, 'FILE_NOT_FOUND', 'File not found.');
break;
case 24:
log('ERROR: File not readable.');
settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.');
break;
case 18:
log('ERROR: File cannot be access due to security constrant.');
settings.fileError.call(this, info, 'SECURITY_ERROR', 'File cannot be access due to security constrant.');
break;
case 20: //User Abort
break;
}
}
}
if (!settings.resizeImage) {
if (canSendBinaryString) {
reader.onloadend = function (ev) {
var bin = ev.target.result;
handleForm(settings, 'bin', bin, info);
};
reader.readAsBinaryString(file);
} else if (settings.allowDataInBase64) {
reader.onloadend = function (ev) {
handleForm(
settings,
'base64',
dataURLtoBase64(ev.target.result),
info
);
};
reader.readAsDataURL(file);
} else {
log('ERROR: No available method to extract file; allowDataInBase64 not set.');
settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.');
}
} else {
reader.onloadend = function (ev) {
var dataurl = ev.target.result;
handleImage(settings, dataurl, info);
};
reader.readAsDataURL(file);
}
} else if (window.File.prototype.getAsBinary) {
log('WARN: FileReader does not exist, UI will be blocked when reading big file.');
if (!settings.resizeImage) {
try {
var bin = file.getAsBinary();
} catch (e) {
log('ERROR: File not readable.');
settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.');
return;
}
handleForm(settings, 'bin', bin, info);
} else {
try {
var bin = file.getAsDataURL();
} catch (e) {
log('ERROR: File not readable.');
settings.fileError.call(this, info, 'IO_ERROR', 'File not readable.');
return;
}
handleImage(settings, dataurl, info);
}
} else {
log('ERROR: No available method to extract file; this browser is not supported.');
settings.fileError.call(this, info, 'NOT_SUPPORT', 'ERROR: No available method to extract file; this browser is not supported.');
}
};
// step 1.5: inject file into <img>, paste the pixels into <canvas>,
// read the final image
var handleImage = function (settings, dataurl, info) {
var img = new Image();
img.onerror = function () {
log('ERROR: <img> failed to load, file is not a supported image format.');
settings.fileError.call(this, info, 'FILE_NOT_IMAGE', 'File is not a supported image format.');
};
img.onload = function () {
var ratio = Math.max(
img.width/settings.imageMaxWidth,
img.height/settings.imageMaxHeight,
1
);
var d = {
w: Math.floor(Math.max(img.width/ratio, 1)),
h: Math.floor(Math.max(img.height/ratio, 1))
}
log(
'INFO: Original image size: ' + img.width.toString(10) + 'x' + img.height.toString(10)
+ ', resized image size: ' + d.w + 'x' + d.h + '.'
);
if (!settings.forceResize && img.width === d.w && img.height === d.h) {
log('INFO: Image demension is the same, send the original file.');
if (canResizeImageToBinaryString) {
handleForm(
settings,
'bin',
window.atob(dataURLtoBase64(dataurl)),
info
);
} else if (settings.allowDataInBase64) {
handleForm(
settings,
'base64',
dataURLtoBase64(dataurl),
info
);
} else {
log('ERROR: No available method to send the original file; allowDataInBase64 not set.');
settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.');
}
return;
}
var canvas = document.createElement('canvas');
canvas.setAttribute('width', d.w);
canvas.setAttribute('height', d.h);
canvas.getContext('2d').drawImage(
img,
0,
0,
img.width,
img.height,
0,
0,
d.w,
d.h
);
if (!settings.imageType || settings.imageType === 'auto') {
if (info.type === 'image/jpeg') settings.imageType = 'jpeg';
else settings.imageType = 'png';
}
var ninfo = {
type: 'image/' + settings.imageType,
name: info.name.substr(0, info.name.indexOf('.')) + '.resized.' + settings.imageType
};
if (canResizeImageToFile && canSendFormData) {
// Gecko 2 (Fx4) non-standard function
var nfile = canvas.mozGetAsFile(
ninfo.name,
'image/' + settings.imageType
);
ninfo.size = file.size || file.fileSize;
handleForm(
settings,
'file',
nfile,
ninfo
);
} else if (canResizeImageToBinaryString && canSendBinaryString) {
// Read the image as DataURL, convert it back to binary string.
var bin = window.atob(dataURLtoBase64(canvas.toDataURL('image/' + settings.imageType)));
ninfo.size = bin.length;
handleForm(
settings,
'bin',
bin,
ninfo
);
} else if (settings.allowDataInBase64 && canResizeImageToBase64 && canSendImageInBase64) {
handleForm(
settings,
'base64',
dataURLtoBase64(canvas.toDataURL('image/' + settings.imageType)),
ninfo
);
} else {
log('ERROR: No available method to extract image; allowDataInBase64 not set.');
settings.fileError.call(this, info, 'NO_BIN_SUPPORT_AND_BASE64_NOT_SET', 'No available method to extract file; allowDataInBase64 not set.');
}
}
img.src = dataurl;
}
// Step 2: construct form data and send the file
// paramaters: Ajax settings, File object, binary string of file || null, file info assoc array
var handleForm = function (settings, type, data, info) {
if (canSendFormData && type === 'file') {
// FormData API saves the day
log('INFO: Using FormData to construct form.');
var formdata = new FormData();
formdata.append('Filedata', data);
// Prevent jQuery form convert FormData object into string.
settings.processData = false;
// Prevent jQuery from overwrite automatically generated xhr content-Type header
// by unsetting the default contentType and inject data only right before xhr.send()
settings.contentType = null;
settings.__beforeSend = settings.beforeSend;
settings.beforeSend = function (xhr, s) {
s.data = formdata;
if (s.__beforeSend) return s.__beforeSend.call(this, xhr, s);
}
//settings.data = formdata;
} else if (canSendBinaryString && type === 'bin') {
log('INFO: Concat our own multipart/form-data data string.');
// A placeholder MIME type
if (!info.type) info.type = 'application/octet-stream';
if (/[^\x20-\x7E]/.test(info.name)) {
log('INFO: Filename contains non-ASCII code, do UTF8-binary string conversion.');
info.name_bin = unescape(encodeURIComponent(info.name));
}
//filtered out non-ASCII chars in filenames
// info.name = info.name.replace(/[^\x20-\x7E]/g, '_');
// multipart/form-data boundary
var bd = 'xhrupload-' + parseInt(Math.random()*(2 << 16));
settings.contentType = 'multipart/form-data; boundary=' + bd;
var formdata = '--' + bd + '\n' // RFC 1867 Format, simulate form file upload
+ 'content-disposition: form-data; name="Filedata";'
+ ' filename="' + (info.name_bin || info.name) + '"\n'
+ 'Content-Type: ' + info.type + '\n\n'
+ data + '\n\n'
+ '--' + bd + '--';
if (window.XMLHttpRequest.prototype.sendAsBinary) {
// Use xhr.sendAsBinary that takes binary string
log('INFO: Pass binary string to xhr.');
settings.data = formdata;
} else {
// make a blob
log('INFO: Convert binary string into Blob.');
var buf = new ArrayBuffer(formdata.length);
var view = new Uint8Array(buf);
$.each(
formdata,
function (i, o) {
view[i] = o.charCodeAt(0);
}
);
var bb = new BlobBuilder();
bb.append(buf);
var blob = bb.getBlob();
settings.processData = false;
settings.__beforeSend = settings.beforeSend;
settings.beforeSend = function (xhr, s) {
s.data = blob;
if (s.__beforeSend) return s.__beforeSend.call(this, xhr, s);
};
}
} else if (settings.allowDataInBase64 && type === 'base64') {
log('INFO: Concat our own multipart/form-data data string; send the file in base64 because binary xhr is not supported.');
// A placeholder MIME type
if (!info.type) info.type = 'application/octet-stream';
// multipart/form-data boundary
var bd = 'xhrupload-' + parseInt(Math.random()*(2 << 16));
settings.contentType = 'multipart/form-data; boundary=' + bd;
settings.data = '--' + bd + '\n' // RFC 1867 Format, simulate form file upload
+ 'content-disposition: form-data; name="Filedata";'
+ ' filename="' + encodeURIComponent(info.name) + '.base64"\n'
+ 'Content-Transfer-Encoding: base64\n' // Vaild MIME header, but won't work with PHP file upload handling.
+ 'Content-Type: ' + info.type + '\n\n'
+ data + '\n\n'
+ '--' + bd + '--';
} else {
log('ERROR: Data is not given in processable form.');
settings.fileError.call(this, info, 'INTERNAL_ERROR', 'Data is not given in processable form.');
return;
}
xhrupload(settings);
};
// Step 3: start sending out file
var xhrupload = function (settings) {
log('INFO: Sending file.');
if (typeof settings.data === 'string' && canSendBinaryString) {
log('INFO: Using xhr.sendAsBinary.');
settings.___beforeSend = settings.beforeSend;
settings.beforeSend = function (xhr, s) {
xhr.send = xhr.sendAsBinary;
if (s.___beforeSend) return s.___beforeSend.call(this, xhr, s);
}
}
$.ajax(settings);
};
$.fn.fileUpload = function(settings) {
this.each(function(i, el) {
if ($(el).is('input[type=file]')) {
log('INFO: binding onchange event to a input[type=file].');
$(el).bind(
'change',
function () {
if (!this.files.length) {
log('ERROR: no file selected.');
return;
} else if (this.files.length > 1) {
log('WARN: Multiple file upload not implemented yet, only first file will be uploaded.');
}
handleFile($.extend({}, config, settings), this.files[0]);
if (this.form.length === 1) {
this.form.reset();
} else {
log('WARN: Unable to reset file selection, upload won\'t be triggered again if user selects the same file.');
}
return;
}
);
}
if ($(el).is('form')) {
log('ERROR: <form> not implemented yet.');
} else {
log('INFO: binding ondrop event.');
$(el).bind(
'dragover', // dragover behavior should be blocked for drop to invoke.
function(ev) {
return false;
}
).bind(
'drop',
function (ev) {
if (!ev.originalEvent.dataTransfer.files) {
log('ERROR: No FileList object present; user might had dropped text.');
return false;
}
if (!ev.originalEvent.dataTransfer.files.length) {
log('ERROR: User had dropped a virual file (e.g. "My Computer")');
return false;
}
if (!ev.originalEvent.dataTransfer.files.length > 1) {
log('WARN: Multiple file upload not implemented yet, only first file will be uploaded.');
}
handleFile($.extend({}, config, settings), ev.originalEvent.dataTransfer.files[0]);
return false;
}
);
}
});
return this;
};
$.fileUploadSupported = isSupported;
$.imageUploadSupported = isImageSupported;
$.fileUploadAsBase64Supported = isSupportedInBase64;
$.imageUploadAsBase64Supported = isImageSupportedInBase64;
})(jQuery);