/** * 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);