wise-web-client
Version:
Based on Spine MVC framework
490 lines (428 loc) • 17.5 kB
JavaScript
define(['jquery', 'backbone', 'handlebars', './BaseView',
'text!../../../tpl/upload_manager_file.hbr', 'text!../../../tpl/upload_manager_main.hbr',
'jquery.fileupload'
],
function($, Backbone, Handlebars, BaseView, fileTemplate, mainTemplate) {
var context = {};
var mc = {
/**
* This model represents a file.
*
*/
File: Backbone.Model.extend({
state: "pending",
url: function() {
return this.id ? '/api/v1/file/' + this.id : '/api/v1/file/';
},
/**
* Start upload.
*
*/
start: function() {
if (this.isPending()) {
this.get('processor').submit();
this.state = "running";
// Dispatch event
this.trigger('filestarted', this);
}
},
/**
* Cancel a file upload.
*
*/
cancel: function() {
this.get('processor').abort();
this.destroy();
// Dispatch event
this.state = "canceled";
this.trigger('filecanceled', this);
},
/**
* Notify file that progress updated.
*
*/
progress: function(data) {
// Dispatch event
this.trigger('fileprogress', this.get('processor').progress());
},
/**
* Notify file that upload failed.
*
*/
fail: function(error) {
// Dispatch event
this.state = "error";
this.trigger('filefailed', error);
},
/**
* Notify file that upload is done.
*
*/
done: function(result) {
// Dispatch event
this.state = "error";
this.trigger('filedone', result);
},
/**
* Is this file pending to be uploaded ?
*
*/
isPending: function() {
return this.getState() == "pending";
},
/**
* Is this file currently uploading ?
*
*/
isRunning: function() {
return this.getState() == "running";
},
/**
* Is this file uploaded ?
*
*/
isDone: function() {
return this.getState() == "done";
},
/**
* Is this upload in error ?
*
*/
isError: function() {
return this.getState() == "error" || this.getState == "canceled";
},
/**
* Get the file state.
*
*/
getState: function() {
return this.state;
}
}),
/**
* This is a file collection, used to manage the selected
* and processing files.
*
*/
FileCollection: Backbone.Collection.extend({
model: this.File
}),
/**
* A file view, which is the view that manage a single file
* process in the upload manager.
*
*/
FileView: BaseView.extend({
className: 'upload-manager-file row-fluid',
tagName: 'div',
template: Handlebars.compile(fileTemplate),
initialize: function(options) {
this.processUploadMsg = options.processUploadMsg;
this.doneMsg = options.doneMsg;
// Bind model events
this.model.on('destroy', this.remove, this);
this.model.on('fileprogress', this.updateProgress, this);
this.model.on('filefailed', this.hasFailed, this);
this.model.on('filedone', this.hasDone, this);
this.model.on('destroy', this.hasDestroy, this);
// In each case, update view
this.model.on('all', this.update, this);
},
/**
* Render the file item view.
*
*/
render: function() {
$(this.el).html(this.template(this.computeData()));
// Bind events
this.bindEvents();
// Update elements
this.update();
return this;
},
/**
* Update upload progress.
*
*/
updateProgress: function(progress) {
var percent = parseInt(progress.loaded / progress.total * 100, 10);
var progressHTML = percent + '%';
if (percent >= 100 && this.processUploadMsg) {
progressHTML = this.processUploadMsg;
}
$('div.progress', this.el)
.find('.bar')
.css('width', percent + '%')
.parent()
.find('.progress-label')
.html(progressHTML);
},
/**
* File upload has failed.
*
*/
hasFailed: function(error) {
$('span.message', this.el).html('<i class="fa fa-exclamation-triangle"></i> ' + error);
},
/**
* File upload is done.
*
*/
hasDone: function(result) {
this.model.set('id', result._id);
$('span.message', this.el).html('<i class="fa fa-check"></i> ' + (this.doneMsg || 'Uploaded'));
this.pubsub.trigger('file:uploadFinish', {
id: result._id,
context: context
});
var fileName = $('span.name', this.el).html();
$('span.name', this.el).html('<a href="' + this.model.url() + '">' + fileName + '</a>');
},
/**
* File delete/cancel is done.
*
*/
hasDestroy: function(file) {
this.pubsub.trigger('file:destroyFinish', {
id: file.id,
context: context
});
},
/**
* Update view without complete rendering.
*
*/
update: function() {
var when_pending = $('span.size, button#btn-cancel', this.el),
when_running = $('div.progress, button#btn-cancel', this.el),
when_done = $('span.message, button#btn-clear', this.el);
if (this.model.isPending()) {
when_running.add(when_done).addClass('hidden');
when_pending.removeClass('hidden');
} else if (this.model.isRunning()) {
when_pending.add(when_done).addClass('hidden');
when_running.removeClass('hidden');
} else if (this.model.isDone() || this.model.isError()) {
when_pending.add(when_running).addClass('hidden');
when_done.removeClass('hidden');
}
},
/**
* Bind local elements events.
*
*/
bindEvents: function() {
var self = this;
// DOM events
$('button#btn-cancel', this.el).click(function() {
self.model.cancel();
});
$('button#btn-clear', this.el).click(function() {
self.model.destroy();
});
},
/**
* Compute data to be passed to the view.
*
*/
computeData: function() {
return $.extend({}, this.model.get('data'));
}
})
};
var View = BaseView.extend({
/**
* Default options, that will be merged with the passed.
*
*/
defaults: {
uploadUrl: '/upload',
autoUpload: false,
fileUploadId: 'fileupload',
startUploadsId: 'start-uploads-button',
cancelUploadsId: 'cancel-uploads-button',
dataType: 'json'
},
el: 'div.file-uploader',
/**
* An integer used to track the files by a unique
* identifier.
*
*/
template: Handlebars.compile(mainTemplate),
file_id: 0,
/**
* View container class.
*
*/
className: 'container-fluid upload-manager',
/**
* Initialize upload manager options
*
*/
initialize: function(options) {
// Merge options
this.options = $.extend(this.defaults, options);
// Create the file list
this.files = new mc.FileCollection();
// set global context
context = this.options.context || {};
// Create the file-upload wrapper
this.uploadProcess = $('<input id="' + this.options.fileUploadId + '" type="file" name="files[]" multiple="multiple">').fileupload({
dataType: this.options.dataType,
url: this.options.uploadUrl,
formData: this.options.formData,
autoUpload: this.options.autoUpload,
singleFileUploads: true
});
// Add upload process events handlers
this.bindProcessEvents();
// Add local events handlers
this.bindLocal();
//add a handlebar helper
Handlebars.registerHelper('filesize', function(bytes) {
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 Bytes';
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
});
},
/**
* Bind local events.
*
*/
bindLocal: function() {
var self = this;
this.on('fileadd', function(file) {
// Add it to current list
self.files.add(file);
// Create the view
self.renderFile(file);
}).on('fileprogress', function(file, progress) {
file.progress(progress);
}).on('filefail', function(file, error) {
file.fail(error);
}).on('filedone', function(file, data) {
file.done(data.result);
});
// When collection changes
this.files.on('all', this.update, this);
},
/**
* Render a file.
*
*/
renderFile: function(file) {
var file_view = new mc.FileView($.extend(this.options, {
model: file
}));
$('div#file-list').append(file_view.render().$el);
},
/**
* Update the view without full rendering.
*
*/
update: function() {
var with_files_elements = $('button#' + this.options.cancelUploadsId + ', button#' + this.options.startUploadsId, this.el);
var without_files_elements = $('#file-list .no-data', this.el);
if (this.files.length > 0) {
with_files_elements.removeClass('hidden');
without_files_elements.addClass('hidden');
} else {
with_files_elements.addClass('hidden');
without_files_elements.removeClass('hidden');
}
},
/**
* Bind events on the upload processor.
*
*/
bindProcessEvents: function() {
var self = this;
this.uploadProcess.on('fileuploadadd', function(e, data) {
// Create an array in which the file objects
// will be stored.
data.uploadManagerFiles = [];
// A file is added, process for each file.
// Note: every times, the data.files array length is 1 because
// of "singleFileUploads" option.
$.each(data.files, function(index, file_data) {
// Create the file object
file_data.id = self.file_id++;
var file = new mc.File({
id: file_data.id,
name: file_data.name,
data: file_data,
processor: data
});
// Add file in data
data.uploadManagerFiles.push(file);
// Trigger event
self.trigger('fileadd', file);
});
}).on('fileuploadprogress', function(e, data) {
$.each(data.uploadManagerFiles, function(index, file) {
self.trigger('fileprogress', file, data);
});
}).on('fileuploadfail', function(e, data) {
$.each(data.uploadManagerFiles, function(index, file) {
var error = "Unknown error";
if (typeof data.errorThrown == "string") {
error = data.errorThrown;
} else if (typeof data.errorThrown == "object") {
error = data.errorThrown.message;
} else if (data.result) {
if (data.result.error) {
error = data.result.error;
} else if (data.result.files && data.result.files[index] && data.result.files[index].error) {
error = data.result.files[index].error;
} else {
error = "Unknown remote error";
}
}
self.trigger('filefail', file, error);
});
}).on('fileuploaddone', function(e, data) {
$.each(data.uploadManagerFiles, function(index, file) {
self.trigger('filedone', file, data);
});
});
},
/**
* Render the main part of upload manager.
*
*/
render: function() {
this.$el.html(this.template());
// Update view
this.update();
// Add add files handler
var input = $('input#' + this.options.fileUploadId, this.el),
self = this;
input.on('change', function() {
self.uploadProcess.fileupload('add', {
fileInput: $(this)
});
});
// Add cancel all handler
$('button#' + this.options.cancelUploadsId, this.el).click(function() {
while (self.files.length) {
self.files.at(0).cancel();
}
});
// Add start uploads handler
$('button#' + this.options.startUploadsId, this.el).click(function() {
self.files.each(function(file) {
file.start();
});
});
// Render current files
$.each(this.files, function(i, file) {
self.renderFile(file);
});
return this;
}
});
return View;
}
);