zulip/static/third/jquery-filedrop/jquery.filedrop.js

681 lines
20 KiB
JavaScript

/*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
max_file_upload_size: 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.
var 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 dataIsImage(data) {
// Check if the clipboard data is actually an image or a thumbnail of the
// copied text.
var text = data.getData('text/html');
if (!text) {
// No html is present, when pasting from image viewers or a screenshot
return true;
}
try {
var html = $.parseHTML(text);
} catch(e) {
// This is really a problem with the software, where we copied the text
// from - but we just let the default browser behavior prevail
return false;
}
// Some software like MS Word adds an image thumbnail, when text is
// copied. We would like to paste the actual text, instead of the
// thumbnail image in this case.
// When an image copied in a (modern?) Browser, a 'text/html' item is
// present in the clipboard, which has an img tag for the copied image
// (along with may be a meta tag)
var allowedTags = ["META", "IMG"];
for (var i=0; i < html.length; i += 1){
if (allowedTags.indexOf(html[i].nodeName) < 0){
return false;
}
}
return true;
}
function paste(event) {
if (event.originalEvent.clipboardData === undefined ||
event.originalEvent.clipboardData.items === undefined) {
return;
}
// Check if the data in the clipboard is really an image, or just a
// thumbnail of the copied text.
if (!dataIsImage(event.originalEvent.clipboardData)){
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);
// Once the upload has started, the event needn't be processed further. This seems to be required on Safari to
// prevent the Copied Image URL from being pasted along with the uploaded image URL.
event.stopPropagation();
event.preventDefault();
}
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.max_file_upload_size;
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);