/*global jQuery:false, alert:false */ /** @preserve Software from "jQuery Filedrop 0.1.0", a jQuery plugin for html5 dragging files, is Copyright (c) Resopollution and is provided under the following license: -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- */ /* * Default text - jQuery plugin for html5 dragging files from desktop to browser * * Author: Weixi Yen * * Email: [Firstname][Lastname]@gmail.com * * Copyright (c) 2010 Resopollution * * Licensed under the MIT license: * http://www.opensource.org/licenses/mit-license.php * * Project home: * http://www.github.com/weixiyen/jquery-filedrop * * Version: 0.1.0 * * Features: * Allows sending of extra parameters with file. * Works with Firefox 3.6+ * Future-compliant with HTML5 spec (will work with Webkit browsers and IE9) * Usage: * See README at project homepage * */ ;(function($) { var default_opts = { fallback_id: '', url: '', refresh: 1000, paramname: 'userfile', allowedfiletypes:[], raw_droppable:[], maxfiles: 25, // Ignored if queuefiles is set > 0 maxfilesize: 1, // MB file size limit queuefiles: 0, // Max files before queueing (for large volume uploads) queuewait: 200, // Queue wait time if full data: {}, headers: {}, drop: empty, rawDrop: empty, dragStart: empty, dragEnter: empty, dragOver: empty, dragLeave: empty, docEnter: empty, docOver: empty, docLeave: empty, beforeEach: empty, afterAll: empty, rename: empty, error: function(err, response, file, i) { alert(err); }, uploadStarted: empty, uploadFinished: empty, progressUpdated: empty, globalProgressUpdated: empty, speedUpdated: empty }, errors = ["BrowserNotSupported", "TooManyFiles", "FileTooLarge", "FileTypeNotAllowed", "NotFound", "NotReadable", "AbortError", "ReadError"], doc_leave_timer, stop_loop = false, files_count = 0, files; $.fn.filedrop = function(options) { var opts = $.extend({}, default_opts, options), global_progress = [] // Zulip modification: keep a pointer to the object that the function // was invoked on. caller = this; this.on('drop', drop).on('dragstart', opts.dragStart).on('dragenter', dragEnter).on('dragover', dragOver).on('dragleave', dragLeave); this.on('paste', paste); this.on('imagedata-upload.zulip', uploadRawImageData); $(document).on('drop', docDrop).on('dragenter', docEnter).on('dragover', docOver).on('dragleave', docLeave); $('#' + opts.fallback_id).change(function(e) { opts.drop(e); files = e.target.files; files_count = files.length; upload(); }); function drop(e) { if (!e.originalEvent.dataTransfer) { return; } files = e.originalEvent.dataTransfer.files; function has_type(dom_stringlist, type) { var j; for (j = 0; j < dom_stringlist.length; j++) { if (dom_stringlist[j] === type) { return true; } } return false; } if (files.length === 0) { var i; for (i = 0; i < opts.raw_droppable.length; i++) { var type = opts.raw_droppable[i]; if (has_type(e.originalEvent.dataTransfer.types, type)) { opts.rawDrop(e.originalEvent.dataTransfer.getData(type)); return false; } } } if( opts.drop.call(this, e) === false ) return false; if (files === null || files === undefined || files.length === 0) { opts.error(errors[0]); return false; } files_count = files.length; upload(); e.preventDefault(); return false; } function sendRawImageData(event, image) { function finished_callback(serverResponse, timeDiff, xhr) { return opts.uploadFinished(-1, image.file, serverResponse, timeDiff, xhr); } var url_params = "?mimetype=" + encodeURIComponent(image.type); do_xhr("pasted_image", image.data, image.type, {file: image.file}, url_params, finished_callback, function () {}); } function uploadRawImageData(event, image) { // Call the user callback to initialize the drop event if( opts.drop.call(this, undefined) === false ) return false; sendRawImageData(event, image); } function paste(event) { if (event.originalEvent.clipboardData === undefined || event.originalEvent.clipboardData.items === undefined) { return; } // Check if any of the items are strings, and if they are, // then return, since we want the default browser behavior // to deal with those. var itemsLength = event.originalEvent.clipboardData.items.length; for (var i = 0; i < itemsLength; i++) { if (event.originalEvent.clipboardData.items[i].kind === "string") { return; } } // Take the first image pasted in the clipboard var match_re = /image.*/; var item; $.each(event.originalEvent.clipboardData.items, function (idx, this_event) { if (this_event.type.match(match_re)) { item = this_event; return false; } }); if (item === undefined) { return; } // Call the user callback to initialize the drop event if( opts.drop.call(this, event) === false ) return false; // Read the data of the drop in as binary data, and send it to the server var data = item.getAsFile(); var reader = new FileReader(); reader.onload = function(event) { sendRawImageData(event, {type: data.type, data: event.target.result, file: data}); }; reader.readAsBinaryString(data); opts.uploadStarted(undefined, data); } function getBuilder(filename, filedata, mime, boundary) { var dashdash = '--', crlf = '\r\n', builder = ''; if (opts.data) { var params = $.param(opts.data).replace(/\+/g, '%20').split(/&/); $.each(params, function() { var pair = this.split("=", 2), name = decodeURIComponent(pair[0]), val = decodeURIComponent(pair[1]); builder += dashdash; builder += boundary; builder += crlf; builder += 'Content-Disposition: form-data; name="' + name + '"'; builder += crlf; builder += crlf; builder += val; builder += crlf; }); } builder += dashdash; builder += boundary; builder += crlf; builder += 'Content-Disposition: form-data; name="' + opts.paramname + '"'; builder += '; filename="' + encodeURIComponent(filename) + '"'; builder += crlf; builder += 'Content-Type: ' + mime; builder += crlf; builder += crlf; builder += filedata; builder += crlf; builder += dashdash; builder += boundary; builder += dashdash; builder += crlf; return builder; } function progress(e) { if (e.lengthComputable) { var percentage = Math.round((e.loaded * 100) / e.total); if (this.currentProgress !== percentage) { this.currentProgress = percentage; opts.progressUpdated(this.index, this.file, this.currentProgress); global_progress[this.global_progress_index] = this.currentProgress; globalProgress(); var elapsed = new Date().getTime(); var diffTime = elapsed - this.currentStart; if (diffTime >= opts.refresh) { var diffData = e.loaded - this.startData; var speed = diffData / diffTime; // KB per second opts.speedUpdated(this.index, this.file, speed); this.startData = e.loaded; this.currentStart = elapsed; } } } } function globalProgress() { if (global_progress.length === 0) { return; } var total = 0, index; for (index in global_progress) { if(global_progress.hasOwnProperty(index)) { total = total + global_progress[index]; } } opts.globalProgressUpdated(Math.round(total / global_progress.length)); } function do_xhr(filename, filedata, mime, upload_args, extra_url_args, finished_callback, on_error) { var xhr = new XMLHttpRequest(), start_time = new Date().getTime(), global_progress_index = global_progress.length, boundary = '------multipartformboundary' + (new Date()).getTime(), upload = xhr.upload; // Zulip modification: Shunt the XHR into the parent object so we // can interrupt it later. caller.data("filedrop_xhr", xhr); if (opts.withCredentials) { xhr.withCredentials = opts.withCredentials; } var builder = builder = getBuilder(filename, filedata, mime, boundary); upload = $.extend(upload, upload_args); upload.downloadStartTime = start_time; upload.currentStart = start_time; upload.currentProgress = 0; upload.global_progress_index = global_progress_index; upload.startData = 0; upload.addEventListener("progress", progress, false); // Allow url to be a method if (jQuery.isFunction(opts.url)) { xhr.open("POST", opts.url() + extra_url_args, true); } else { xhr.open("POST", opts.url + extra_url_args, true); } xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary); // Add headers $.each(opts.headers, function(k, v) { xhr.setRequestHeader(k, v); }); xhr.sendAsBinary(builder); global_progress[global_progress_index] = 0; globalProgress(); xhr.onload = function() { var serverResponse = null; if (xhr.responseText) { try { serverResponse = JSON.parse(xhr.responseText); } catch (e) { serverResponse = xhr.responseText; } } var now = new Date().getTime(), timeDiff = now - start_time, result = finished_callback(serverResponse, timeDiff, xhr); // Make sure the global progress is updated global_progress[global_progress_index] = 100; globalProgress(); if (result === false) { stop_loop = true; } // Pass any errors to the error option if (xhr.status < 200 || xhr.status > 299) { on_error(xhr.status, serverResponse); } }; } // Respond to an upload function upload() { stop_loop = false; if (!files) { opts.error(errors[0]); return false; } if (opts.allowedfiletypes.push && opts.allowedfiletypes.length) { for(var fileIndex = files.length;fileIndex--;) { if(!files[fileIndex].type || $.inArray(files[fileIndex].type, opts.allowedfiletypes) < 0) { opts.error(errors[3], null, files[fileIndex], fileIndex); return false; } } } var filesDone = 0, filesRejected = 0; if (files_count > opts.maxfiles && opts.queuefiles === 0) { opts.error(errors[1]); return false; } // Define queues to manage upload process var workQueue = []; var processingQueue = []; var doneQueue = []; // Add everything to the workQueue for (var i = 0; i < files_count; i++) { workQueue.push(i); } // Helper function to enable pause of processing to wait // for in process queue to complete var pause = function(timeout) { setTimeout(process, timeout); return; }; // Process an upload, recursive var process = function() { var fileIndex; if (stop_loop) { return false; } // Check to see if are in queue mode if (opts.queuefiles > 0 && processingQueue.length >= opts.queuefiles) { return pause(opts.queuewait); } else { // Take first thing off work queue fileIndex = workQueue[0]; workQueue.splice(0, 1); // Add to processing queue processingQueue.push(fileIndex); } try { if (beforeEach(files[fileIndex]) !== false) { if (fileIndex === files_count) { return; } var reader = new FileReader(), max_file_size = 1048576 * opts.maxfilesize; reader.index = fileIndex; if (files[fileIndex].size > max_file_size) { opts.error(errors[2], null, files[fileIndex], fileIndex); // Remove from queue processingQueue.forEach(function(value, key) { if (value === fileIndex) { processingQueue.splice(key, 1); } }); filesRejected++; return true; } reader.onerror = function(e) { switch(e.target.error.code) { case e.target.error.NOT_FOUND_ERR: opts.error(errors[4]); return false; case e.target.error.NOT_READABLE_ERR: opts.error(errors[5]); return false; case e.target.error.ABORT_ERR: opts.error(errors[6]); return false; default: opts.error(errors[7]); return false; }; }; reader.onloadend = !opts.beforeSend ? send : function (e) { opts.beforeSend(files[fileIndex], fileIndex, function () { send(e); }); }; reader.readAsBinaryString(files[fileIndex]); } else { filesRejected++; } } catch (err) { // Remove from queue processingQueue.forEach(function(value, key) { if (value === fileIndex) { processingQueue.splice(key, 1); } }); opts.error(errors[0]); return false; } // If we still have work to do, if (workQueue.length > 0) { process(); } }; var send = function(e) { var fileIndex = ((typeof(e.srcElement) === "undefined") ? e.target : e.srcElement).index; // Sometimes the index is not attached to the // event object. Find it by size. Hack for sure. if (e.target.index === undefined) { e.target.index = getIndexBySize(e.total); } var file = files[e.target.index], index = e.target.index; function finished_callback(serverResponse, timeDiff, xhr) { filesDone++; var result = opts.uploadFinished(index, file, serverResponse, timeDiff, xhr); if (filesDone === (files_count - filesRejected)) { afterAll(); } // Remove from processing queue processingQueue.forEach(function(value, key) { if (value === fileIndex) { processingQueue.splice(key, 1); } }); // Add to donequeue doneQueue.push(fileIndex); return result; } function on_error(status_code, response) { opts.error(status_code, response, file, fileIndex); } var fileName, fileData = e.target.result; if (typeof newName === "string") { fileName = newName; } else { fileName = file.name; } var extra_opts = { file: files[e.target.index], index: e.target.index }; do_xhr(fileName, fileData, file.type, extra_opts, "", finished_callback, on_error); opts.uploadStarted(index, file, files_count); }; // Initiate the processing loop process(); } function getIndexBySize(size) { for (var i = 0; i < files_count; i++) { if (files[i].size === size) { return i; } } return undefined; } function rename(name) { return opts.rename(name); } function beforeEach(file) { return opts.beforeEach(file); } function afterAll() { return opts.afterAll(); } function dragEnter(e) { clearTimeout(doc_leave_timer); e.preventDefault(); opts.dragEnter.call(this, e); } function dragOver(e) { clearTimeout(doc_leave_timer); e.preventDefault(); opts.docOver.call(this, e); opts.dragOver.call(this, e); } function dragLeave(e) { clearTimeout(doc_leave_timer); opts.dragLeave.call(this, e); e.stopPropagation(); } function docDrop(e) { e.preventDefault(); opts.docLeave.call(this, e); return false; } function docEnter(e) { clearTimeout(doc_leave_timer); e.preventDefault(); opts.docEnter.call(this, e); return false; } function docOver(e) { clearTimeout(doc_leave_timer); e.preventDefault(); opts.docOver.call(this, e); return false; } function docLeave(e) { doc_leave_timer = setTimeout((function(_this) { return function() { opts.docLeave.call(_this, e); }; })(this), 200); } return this; }; function empty() {} try { if (XMLHttpRequest.prototype.sendAsBinary) { return; } XMLHttpRequest.prototype.sendAsBinary = function(datastr) { function byteValue(x) { return x.charCodeAt(0) & 0xff; } var ords = Array.prototype.map.call(datastr, byteValue); var ui8a = new Uint8Array(ords); this.send(ui8a.buffer); }; } catch (e) {} })(jQuery);