/**
* plupload.html5.js
*
* Copyright 2009, Moxiecode Systems AB
* Released under GPL License.
*
* License: http://www.plupload.com/license
* Contributing: http://www.plupload.com/contributing
*/
// JSLint defined globals
/*global plupload:false, File:false, window:false, atob:false, FormData:false */
(function(plupload) {
function scaleImage(image_data_url, max_width, max_height, mime, callback) {
var canvas, context, img, data, scale;
// Setup canvas and context
canvas = document.createElement("canvas");
canvas.style.display = 'none';
document.body.appendChild(canvas);
context = canvas.getContext('2d');
// Load image
img = new Image();
img.onload = function() {
var width, height, percentage;
scale = Math.min(max_width / img.width, max_height / img.height);
if (scale < 1) {
width = Math.round(img.width * scale);
height = Math.round(img.height * scale);
} else {
width = img.width;
height = img.height;
}
// Scale image and canvas
canvas.width = width;
canvas.height = height;
context.drawImage(img, 0, 0, width, height);
// Remove data prefix information and grab the base64 encoded data and decode it
data = canvas.toDataURL(mime);
data = data.substring(data.indexOf('base64,') + 7);
data = atob(data);
// Remove canvas and execute callback with decoded image data
canvas.parentNode.removeChild(canvas);
callback({success : true, data : data});
};
img.src = image_data_url;
}
/**
* HMTL5 implementation. This runtime supports these features: dragdrop, jpgresize, pngresize.
*
* @static
* @class plupload.runtimes.Html5
* @extends plupload.Runtime
*/
plupload.runtimes.Html5 = plupload.addRuntime("html5", {
/**
* Returns a list of supported features for the runtime.
*
* @return {Object} Name/value object with supported features.
*/
getFeatures : function() {
var xhr, hasXhrSupport, hasProgress, dataAccessSupport, sliceSupport;
hasXhrSupport = hasProgress = dataAccessSupport = sliceSupport = false;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
hasProgress = !!xhr.upload;
hasXhrSupport = !!(xhr.sendAsBinary || xhr.upload);
}
// Check for support for various features
if (hasXhrSupport) {
dataAccessSupport = !!(File && File.prototype.getAsDataURL);
sliceSupport = !!(File && File.prototype.slice);
}
return {
// Detect drag/drop file support by sniffing, will try to find a better way
html5: hasXhrSupport, // This is a special one that we check inside the init call
dragdrop: window.mozInnerScreenX !== undefined || sliceSupport,
jpgresize: dataAccessSupport,
pngresize: dataAccessSupport,
multipart: dataAccessSupport || !!window.FileReader || !!window.FormData,
progress: hasProgress,
chunking: sliceSupport || dataAccessSupport
};
},
/**
* Initializes the upload runtime.
*
* @method init
* @param {plupload.Uploader} uploader Uploader instance that needs to be initialized.
* @param {function} callback Callback to execute when the runtime initializes or fails to initialize. If it succeeds an object with a parameter name success will be set to true.
*/
init : function(uploader, callback) {
var html5files = {}, features;
function addSelectedFiles(native_files) {
var file, i, files = [], id;
// Add the selected files to the file queue
for (i = 0; i < native_files.length; i++) {
file = native_files[i];
// Store away gears blob internally
id = plupload.guid();
html5files[id] = file;
// Expose id, name and size
files.push(new plupload.File(id, file.fileName, file.fileSize));
}
// Trigger FilesAdded event if we added any
if (files.length) {
uploader.trigger("FilesAdded", files);
}
}
// No HTML5 upload support
features = this.getFeatures();
if (!features.html5) {
callback({success : false});
return;
}
uploader.bind("Init", function(up) {
var inputContainer, mimes = [], i, y, filters = up.settings.filters, ext, type, container = document.body;
// Create input container and insert it at an absolute position within the browse button
inputContainer = document.createElement('div');
inputContainer.id = up.id + '_html5_container';
// Convert extensions to mime types list
for (i = 0; i < filters.length; i++) {
ext = filters[i].extensions.split(/,/);
for (y = 0; y < ext.length; y++) {
type = plupload.mimeTypes[ext[y]];
if (type) {
mimes.push(type);
}
}
}
plupload.extend(inputContainer.style, {
position : 'absolute',
background : uploader.settings.shim_bgcolor || 'transparent',
width : '100px',
height : '100px',
overflow : 'hidden',
zIndex : 99999,
opacity : uploader.settings.shim_bgcolor ? '' : 0 // Force transparent if bgcolor is undefined
});
inputContainer.className = 'plupload html5';
if (uploader.settings.container) {
container = document.getElementById(uploader.settings.container);
container.style.position = 'relative';
}
container.appendChild(inputContainer);
// Insert the input inide the input container
inputContainer.innerHTML = '';
document.getElementById(uploader.id + '_html5').onchange = function() {
// Add the selected files from file input
addSelectedFiles(this.files);
// Clearing the value enables the user to select the same file again if they want to
this.value = '';
};
});
// Add drop handler
uploader.bind("PostInit", function() {
var dropElm = document.getElementById(uploader.settings.drop_element);
if (dropElm) {
// Block browser default drag over
plupload.addEvent(dropElm, 'dragover', function(e) {
e.preventDefault();
});
// Attach drop handler and grab files
plupload.addEvent(dropElm, 'drop', function(e) {
var dataTransfer = e.dataTransfer;
// Add dropped files
if (dataTransfer && dataTransfer.files) {
addSelectedFiles(dataTransfer.files);
}
e.preventDefault();
});
}
});
uploader.bind("Refresh", function(up) {
var browseButton, browsePos, browseSize;
browseButton = document.getElementById(uploader.settings.browse_button);
browsePos = plupload.getPos(browseButton, document.getElementById(up.settings.container));
browseSize = plupload.getSize(browseButton);
plupload.extend(document.getElementById(uploader.id + '_html5_container').style, {
top : browsePos.y + 'px',
left : browsePos.x + 'px',
width : browseSize.w + 'px',
height : browseSize.h + 'px'
});
});
uploader.bind("UploadFile", function(up, file) {
var settings = up.settings, nativeFile, resize;
function sendBinaryBlob(blob) {
var chunk = 0, loaded = 0;
function uploadNextChunk() {
var chunkBlob = blob, xhr, upload, chunks, args, multipartDeltaSize = 0,
boundary = '----pluploadboundary' + plupload.guid(), chunkSize, curChunkSize,
dashdash = '--', crlf = '\r\n', multipartBlob = '';
// File upload finished
if (file.status == plupload.DONE || file.status == plupload.FAILED || up.state == plupload.STOPPED) {
return;
}
// Standard arguments
args = {name : file.target_name || file.name};
// Only add chunking args if needed
if (settings.chunk_size && features.chunking) {
chunkSize = settings.chunk_size;
chunks = Math.ceil(file.size / chunkSize);
curChunkSize = Math.min(chunkSize, file.size - (chunk * chunkSize));
// Blob is string so we need to fake chunking, this is not
// ideal since the whole file is loaded into memory
if (typeof(blob) == 'string') {
chunkBlob = blob.substring(chunk * chunkSize, chunk * chunkSize + curChunkSize);
} else {
// Slice the chunk
chunkBlob = blob.slice(chunk * chunkSize, curChunkSize);
}
// Setup query string arguments
args.chunk = chunk;
args.chunks = chunks;
} else {
curChunkSize = file.size;
}
// Setup XHR object
xhr = new XMLHttpRequest();
upload = xhr.upload;
// Do we have upload progress support
if (upload) {
upload.onprogress = function(e) {
file.loaded = Math.min(file.size, loaded + e.loaded - multipartDeltaSize); // Loaded can be larger than file size due to multipart encoding
up.trigger('UploadProgress', file);
};
}
xhr.open("post", plupload.buildUrl(up.settings.url, args), true);
xhr.onreadystatechange = function() {
var httpStatus, chunkArgs;
if (xhr.readyState == 4) {
// Getting the HTTP status might fail on some Gecko versions
try {
httpStatus = xhr.status;
} catch (ex) {
httpStatus = 0;
}
// Is error status
if (httpStatus >= 400) {
up.trigger('Error', {
code : plupload.HTTP_ERROR,
message : 'HTTP Error.',
file : file,
status : httpStatus
});
} else {
// Handle chunk response
if (chunks) {
chunkArgs = {
chunk : chunk,
chunks : chunks,
response : xhr.responseText,
status : httpStatus
};
up.trigger('ChunkUploaded', file, chunkArgs);
loaded += curChunkSize;
// Stop upload
if (chunkArgs.cancelled) {
file.status = plupload.FAILED;
return;
}
file.loaded = Math.min(file.size, (chunk + 1) * chunkSize);
} else {
file.loaded = file.size;
}
up.trigger('UploadProgress', file);
// Check if file is uploaded
if (!chunks || ++chunk >= chunks) {
file.status = plupload.DONE;
up.trigger('FileUploaded', file, {
response : xhr.responseText,
status : httpStatus
});
} else {
// Still chunks left
uploadNextChunk();
}
}
}
};
// Set custom headers
plupload.each(up.settings.headers, function(value, name) {
xhr.setRequestHeader(name, value);
});
// Build multipart request
if (up.settings.multipart && features.multipart) {
// Has FormData support like Chrome 6+, Safari 5+, Firefox 4
if (!xhr.sendAsBinary) {
var data = new FormData();
// Add multipart params
plupload.each(up.settings.multipart_params, function(value, name) {
data.append(name, value);
});
// Add file and send it
data.append(up.settings.file_data_name, chunkBlob);
xhr.send(data);
return;
}
// Gecko multipart request
xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
// Append mutlipart parameters
plupload.each(up.settings.multipart_params, function(value, name) {
multipartBlob += dashdash + boundary + crlf +
'Content-Disposition: form-data; name="' + name + '"' + crlf + crlf;
multipartBlob += value + crlf;
});
// Build RFC2388 blob
multipartBlob += dashdash + boundary + crlf +
'Content-Disposition: form-data; name="' + up.settings.file_data_name + '"; filename="' + file.name + '"' + crlf +
'Content-Type: application/octet-stream' + crlf + crlf +
chunkBlob + crlf +
dashdash + boundary + dashdash + crlf;
multipartDeltaSize = multipartBlob.length - chunkBlob.length;
chunkBlob = multipartBlob;
} else {
// Binary stream header
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
}
if (xhr.sendAsBinary) {
xhr.sendAsBinary(chunkBlob); // Gecko
} else {
xhr.send(chunkBlob); // WebKit
}
}
// Start uploading chunks
uploadNextChunk();
}
nativeFile = html5files[file.id];
resize = up.settings.resize;
if (features.jpgresize) {
// Resize image if it's a supported format and resize is enabled
if (resize && /\.(png|jpg|jpeg)$/i.test(file.name)) {
scaleImage(nativeFile.getAsDataURL(), resize.width, resize.height, /\.png$/i.test(file.name) ? 'image/png' : 'image/jpeg', function(res) {
// If it was scaled send the scaled image if it failed then
// send the raw image and let the server do the scaling
if (res.success) {
file.size = res.data.length;
sendBinaryBlob(res.data);
} else {
sendBinaryBlob(nativeFile.getAsBinary());
}
});
} else {
sendBinaryBlob(nativeFile.getAsBinary());
}
} else {
sendBinaryBlob(nativeFile);
}
});
callback({success : true});
}
});
})(plupload);