define(['underscore',
'jquery',
'backbone',
"models/DataONEObject",
'collections/ObjectFormats',
"Dropzone",
"text!templates/imageUploader.html",
"corejs"],
function(_, $, Backbone, DataONEObject, ObjectFormats, Dropzone, Template, corejs){
/**
* @class ImageUploaderView
* @classdesc A view that allows a person to upload an image to the repository
* @classcategory Views
*/
var ImageUploaderView = Backbone.View.extend(
/** @lends ImageUploaderView.prototype */{
/**
* The type of View this is
* @type {string}
*/
type: "ImageUploader",
/**
* The HTML tag name to use for this view's element
* @type {string}
*/
tagName: "div",
/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: "image-uploader",
/**
* The DataONEObject or PortalImage that is being edited
* @type {DataONEObject|PortalImage}
*/
model: undefined,
/**
* The URL for the image. If a DataONEObject model is provided to the view
* instead, the url is automatically set to the output of DataONEObject.url()
* @type {string}
*/
url: undefined,
/**
* Text to instruct the user how to upload an image
* @type {string[]}
*/
uploadInstructions: ["Drag & drop an image or click here to upload"],
/**
* The maximum display height of the image preview. This is only used for the
* css height propery, and doesn't influence the size of the saved image. If
* set to false, no css height property is set.
* @type {number}
*/
height: false,
/**
* The display width of the image preview. This is only used for the
* css width propery, and doesn't influence the size of the saved image. If
* set to false, no css width property is set.
* @type {number}
*/
width: false,
/**
* The minimum required height of the image file. If set, the uploader will
* reject images that are shorter than this. If null, any image height is
* accepted.
* @type {number}
*/
minHeight: null,
/**
* The minimum required height of the image file. If set, the uploader will
* reject images that are shorter than this. If null, any image height is
* accepted.
* @type {number}
*/
minWidth: null,
/**
* The maximum height for uploaded files. If a file is taller than this, it
* will be resized without warning before being uploaded. If set to null,
* the image won't be resized based on height (but might be depending on
* maxWidth).
* @type {number}
*/
maxHeight: null,
/**
* The maximum width for uploaded files. If a file is wider than this, it
* will be resized without warning before being uploaded. If set to null,
* the image won't be resized based on width (but might be depending on
* maxHeight).
* @type {number}
*/
maxWidth: null,
/**
* The HTML tag name to insert the uploaded image into. Options are "img",
* in which case the image is inserted as an HTML , or "div", in which
* case the image is inserted as the background of a div.
* @type {string}
*/
imageTagName: "div",
/**
* References to templates for this view. HTML files are converted to Underscore.js templates
*/
template: _.template(Template),
/**
* The events this view will listen to and the associated function to call.
* @type {Object}
*/
events: {
"mouseover .icon-remove.remove" : "previewImageRemove",
"mouseout .icon-remove.remove" : "previewImageRemove"
},
/**
* Creates a new ImageUploaderView
* @param {Object} options - A literal object with options to pass to the view
* @property {DataONEObject} options.model - Gets set as ImageUploaderView.model
* @property {string[]} options.uploadInstructions - Gets set as ImageUploaderView.uploadInstructions
* @property {string} options.url - Gets set as ImageUploaderView.url
* @property {string} options.imageTagName - Gets set as ImageUploaderView.imageTagName
* @property {number} options.height - Gets set as ImageUploaderView.height
* @property {number} options.width - Gets set as ImageUploaderView.width
* @property {number} options.minWidth - Gets set as ImageUploaderView.minWidth
* @property {number} options.minHeight - Gets set as ImageUploaderView.minHeight
* @property {number} options.maxWidth - Gets set as ImageUploaderView.maxWidth
* @property {number} options.maxHeight - Gets set as ImageUploaderView.maxHeight
*/
initialize: function(options){
try {
if( typeof options == "object" ){
this.model = options.model;
this.uploadInstructions = options.uploadInstructions;
this.url = options.url;
this.imageTagName = options.imageTagName;
this.height = options.height;
this.width = options.width;
this.minHeight = options.minHeight;
this.minWidth = options.minWidth;
this.maxHeight = options.maxHeight;
this.maxWidth = options.maxWidth;
if( !this.model ){
this.model = new DataONEObject({
synced: true
});
}
if (!this.url && this.model) {
this.url = this.model.url();
}
}
// Ensure the object formats are cached for uploader's use
if ( typeof MetacatUI.objectFormats === "undefined" ) {
MetacatUI.objectFormats = new ObjectFormats();
MetacatUI.objectFormats.fetch();
}
// Bug fix: Overwrite a dropzone function that causes a bug in Edge 16 &
// 17 browser. If we update our dropzone with a fallback, this function
// should return the fallback element.
Dropzone.prototype.getExistingFallback = function(){
return false
};
// Identify which zones should be drag & drop manually
Dropzone.autoDiscover = false;
} catch (e) {
console.log("ImageUploaderView failed to initialize. Error message: " + e);
}
},
/**
* Renders this view
*/
render: function(){
try{
// Reference to the view
var view = this,
// The overall template which holds two sub-templates
fullTemplate = view.template({
height: this.height,
width: this.width,
uploadInstructions: this.uploadInstructions,
imageTagName: this.imageTagName
}),
// The outer template
dropzoneTemplate = $(fullTemplate).find(".dropzone")[0].outerHTML,
// The inner template inserted when an image is added
previewTemplate = $(fullTemplate)
.find(".dz-preview")[0]
.outerHTML;
// Insert the main template for this view
view.$el.html(dropzoneTemplate);
console.log(view.model.get("imageURL"), view.model.url())
// Add upload & drag and drop functionality to the dropzone div.
// For config details, see: https://www.dropzonejs.com/#configuration
var $dropZone = view.$(".dropzone").dropzone({
url: view.model.get("imageURL") || view.model.url(),
acceptedFiles: "image/*",
addRemoveLinks: false,
maxFiles: 1,
parallelUploads: 1,
uploadMultiple: false,
resizeHeight: view.maxHeight,
resizeWidth: view.maxWidth,
thumbnailHeight: view.maxHeight < view.height ? view.maxHeight : null,
thumbnailWidth: view.maxWidth < view.width ? view.maxWidth : null,
dictInvalidFileType: "This file type is not allowed. Please select an image file",
autoProcessQueue: true,
previewTemplate: previewTemplate,
withCredentials: true,
paramName: "object",
hiddenInputContainer: this.el,
headers: {
"Cache-Control": null,
"X-Requested-With": null,
"Authorization": MetacatUI.appUserModel.createAjaxSettings().headers.Authorization
},
// Override dropzone's function for showing images in the upload zone
// so that we have the option to display them as a background images.
// Check for minimum dimensions at this stage because dropzone has
// calculated the file's height here.
thumbnail: function(file, dataURL){
try {
// Don't bother size check for SVG images since they're vector
var dimCheck = file.type === "image/svg+xml" ? true : view.checkMinDimensions(file.width, file.height);
if(dimCheck != true){
if(file.rejectDimensions){
// Send reason for rejection rejectDimensions function
file.rejectDimensions(dimCheck);
}
} else {
if(file.acceptDimensions){
file.acceptDimensions();
};
view.showImage(file, dataURL);
};
} catch (e) {
console.error("Error generating thumbnail image, error message: " + e);
}
},
// Dropzone will check filetype = options.acceptedFiles. Add functions
// for when the image is too small.
accept: function accept(file, done) {
try {
file.rejectDimensions = function(message) { done(message) };
file.acceptDimensions = function(){ done() };
} catch (e) {
console.error("Error during dropzone's accept function. Error code: " + e);
}
},
// After the file is accepted (correct filetype and min size requirements),
// resize the image if it's too large in height or width, then
// provide image data to a dataOne object model and calulate checksum.
transformFile: function(file, done){
try {
// Only resize images if dimensions are too large.
// Once the image is resized (or not), save the data to the model and get a checksum.
var resizeWidth = (file.width > this.options.resizeWidth) ? this.options.resizeWidth : null;
var resizeHeight = (file.height > this.options.resizeHeight) ? this.options.resizeHeight : null;
if (resizeHeight || resizeWidth) {
return this.resizeImage(file, resizeWidth, resizeHeight, this.options.resizeMethod, function(blob){
view.prepareD1Model(blob, file.name, file.type, done);
});
} else {
return view.prepareD1Model(file, file.name, file.type, done);
}
} catch (e) {
console.error("Error during dropzone's transformFile function. Error code: " + e);
}
},
// Add some required formData right before the image is uploaded
sending: function(file, xhr, formData) {
try {
//Create the system metadata XML & send as blob
var sysMetaXML = view.model.serializeSysMeta();
var xmlBlob = new Blob([sysMetaXML], {type : 'application/xml'});
formData.append("sysmeta", xmlBlob, "sysmeta.xml");
formData.append("pid", view.model.get("id"));
} catch (e) {
console.error("Error during dropzone's sending function. Error code: " + e);
}
},
// If there are any errors during the entire process...
error: function error(file, message, xhr) {
try {
view.trigger("error");
// Give a readable error if it's a server error
if(xhr){
console.error(message);
message = "There was an error uploading your file. Please try again later."
}
// Make sure image isn't showing (src for and style for background images)
$(file.previewElement).find(".image-container").attr({
src: "",
style: ""
});
// Show error using dropzone's default behaviour
this.defaultOptions.error(file, message);
} catch (e) {
console.error("Problem handling error, message: " + e);
}
},
init: function() {
try {
this.on("addedfile", function(file){
// Make sure only the most recently added image is shown in the upload zone
view.limitFileInput();
// Required for parent views to use listenTo() on dropzone events
view.trigger("addedfile");
});
// Hide the remove buttons and text when an image is removed
this.on("removedfile", function(file){
view.previewImageRemove();
// Required for parent views to use listenTo() on dropzone events
view.trigger("removedfile");
});
this.on("success", function(){
view.trigger("successSaving", view.model);
});
} catch (e) {
console.error("Issue initializing dropzone, error message: " + e);
}
}
});
// Save the dropzone element for other functions to access later
view.imageDropzone = $dropZone[0].dropzone;
// Fetch the image if a URL was provided and show thumbnail
if(view.url){
view.showSavedImage();
}
}
catch(error){
console.error("ImageUploaderView could not be rendered, error message: ", error);
}
},
/**
* prepareD1Model - Called once an image file is resized or once it's
* determined the the image does not need to be resized. This function adds
* data about the image added by the user to a new DataOne model, then
* calculates the checksum. When the checksum is finished being calculated,
* calls the callback function (i.e. dropzone's done()).
*
* @param {Blob|File} object Either the Blob or File to be saved to the server
* @param {string} filename the name of the file
* @param {string} filetype the filetype
* @param {function} callback a function to call once the checksum is calculated.
*/
prepareD1Model: function(object, filename, filetype, callback){
try{
var modelAttributes = {
synced: true,
type: "image",
fileName: filename,
mediaType: filetype,
size: object.size,
uploadFile: object
}
// Each file upload must be a new DataONE object
this.model = new DataONEObject(modelAttributes);
this.model.updateID();
this.model.set("obsoletes", null);
this.model.get("accessPolicy").makePublic();
// Start checksum, and call the callback function when it's complete
this.model.stopListening(this.model, "checksumCalculated");
this.model.listenToOnce(this.model, "checksumCalculated", function(){
callback(object);
});
this.model.calculateChecksum();
} catch (exception) {
console.log("there was a problem calculating the checksum, exception: " + exception);
}
},
/**
* limitFileInput - Ensures only the most recently added image is shown in
* the upload zone, as we limit each zone to 1 image but dropzone is
* designed to accept multiple files. Called whenever a file is added to a
* dropzone element.
*/
limitFileInput: function(){
if (this.imageDropzone.files[1]!=null){
this.imageDropzone.removeFile(this.imageDropzone.files[0]);
}
},
/**
* checkMinDimensions - called from dropzone's thumbnail function before the
* image is displayed. Checks that the image meets at least the minimum
* height and width requirements provided to view.minHeight and
* view.minWidth.
*
* @param {number} width the image's height.
* @param {number} height the image's width.
* @return {string|boolean} returns true if the image is at least as wide as and as tall as the given height and width. Otherwise returns an error message to display to the user.
*/
checkMinDimensions: function(width, height){
try{
if(width < this.minWidth && height < this.minHeight){
return("This image is too small. Please choose an image that's at least " + this.minWidth +"px wide and " + this.minHeight + "px tall.");
} else if (width < this.minWidth) {
return("This image is too narrow. Please choose an image that's at least " + this.minWidth +"px wide.")
} else if (height < this.minHeight){
return("This image is too short. Please choose an image that's at least " + this.minHeight +"px tall.")
} else {
// minimum height and width are met. If too large, then image will be resized.
return true
}
} catch(error){
console.log("Error checking the min dimensions of added file. Error message:" + error);
// Better to show an image that's too small in this case.
return true
}
},
/**
* showImage - General function for displaying an image file in the upload zone, whether
* just added or already uploaded. This is the function that we use to override
* dropzone's thumbnail() function. It displays the image as the background of
* a div if this view's imageTagName attribute is set to "div", or as an image
* element if imageTagName is set to "img".
* @param {object} file Information about the image file
* @param {string} dataURL A URL for the image to be displayed
*/
showImage: function(file, dataURL){
try{
// Don't show files that are the wrong size or type
if(!this.url && !file.accepted){
return
};
var previewEl = $(file.previewElement).find(".image-container")[0];
if(this.imageTagName == "img"){
previewEl.src = dataURL;
} else if (this.imageTagName == "div"){
$(previewEl).css("background-image", "url(" + dataURL + ")");
}
} catch(error) {
console.log(error);
this.showError($(file.previewElement));
}
},
/**
* Display an image in the upload zone that's already saved. This gets called
* when an image url is provided to this view.
*/
showSavedImage: function(){
try{
//If there is no URL or the model hasn't been saved yet, then don't show the image
if( !this.url || this.model.isNew() ){
return;
}
// A mock image file to identify the image provided to this view
var imageFile = {
url: this.url
};
// Add it to filelist so excess images can be removed if needed
this.imageDropzone.files[0] = imageFile;
// Call the default addedfile event handler
this.imageDropzone.emit("addedfile", imageFile);
// Show the thumbnail of the file
this.imageDropzone.emit("thumbnail", imageFile, imageFile.url);
// Make sure that there is no progress bar, etc...
this.imageDropzone.emit("complete", imageFile);
}
catch(error){
console.log("image could not be displayed, error message: " + error);
// When the preview image fails to render, show some explanatory text
this.showError($(this.imageDropzone.element));
}
},
/**
* showError - Indicates to the user that the image uploader may not work
* due to browser issues.
* @param {jQuery} dropzoneEl - The dropzone element to show the error for.
*/
showError: function(dropzoneEl){
dropzoneEl.addClass("error");
dropzoneEl.find(".dz-error-message span").text("Error previewing image");
dropzoneEl.tooltip({
placement: "bottom",
trigger: "hover",
title: "Image previews cannot be shown. Your browser may be out-of-date."
});
},
/**
* previewImageRemove - When the user hovers over the remove button,
* indicates to the user that the button will remove the image by 1) changing
* the upload instruction text to a message about removing the image,
* and 2) adding a warning class to the message div.
*/
previewImageRemove: function(e){
try {
if(e){
this.$el.toggleClass("remove-preview");
} else {
this.$el.removeClass("remove-preview");
}
} catch (error) {
console.log(error);
}
}
});
return ImageUploaderView;
});