/** * EGroupware eTemplate2 - JS Number object * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @link http://www.egroupware.org * @author Nathan Gray * @copyright Nathan Gray 2011 * @version $Id$ */ "use strict"; /*egw:uses et2_core_inputWidget; phpgwapi.jquery.jquery.html5_upload; */ /** * Class which implements file upload * * @augments et2_inputWidget */ var et2_file = et2_inputWidget.extend( { attributes: { "multiple": { "name": "Multiple files", "type": "boolean", "default": false, "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." }, "max_file_size": { "name": "Maximum file size", "type": "integer", "default":"8388608", "description": "Largest file accepted, in bytes. Subject to server limitations. 8Mb = 8388608" }, "mime": { "name": "Allowed file types", "type": "string", "default": et2_no_init, "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" }, "blur": { "name": "Placeholder", "type": "string", "default": "", "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." }, "progress": { "name": "Progress node", "type": "string", "default": et2_no_init, "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" }, "onStart": { "name": "Start event handler", "type": "any", "default": et2_no_init, "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." }, "onFinish": { "name": "Finish event handler", "type": "any", "default": et2_no_init, "description": "A (js) function called when all files to be uploaded are finished." }, drop_target: { "name": "Optional, additional drop target for HTML5 uploads", "type": "string", "default": et2_no_init, "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" } }, asyncOptions: {}, /** * Constructor * * @memberOf et2_file */ init: function() { this._super.apply(this, arguments); this.node = null; this.input = null; this.progress = null; if(!this.options.id) { console.warn("File widget needs an ID. Used 'file_widget'."); this.options.id = "file_widget"; } // Legacy - id ending in [] means multiple if(this.options.id.substr(-2) == "[]") { this.options.multiple = true; } // Set up the URL to have the request ID & the widget ID var instance = this.getInstanceManager(); var self = this; this.asyncOptions = jQuery.extend({ // Callbacks onStart: function(event, file_count) { // Hide any previous errors self.hideMessage(); return self.onStart(event, file_count); }, onFinish: function(event, file_count) { self.onFinish.apply(self, [event, file_count]); // Fire legacy change action when done self.change(self.input); }, onStartOne: function(event, file_name, index, file_count) { // Here 'this' is the input if(self.checkMime(this.files[index])) { return self.createStatus(event,file_name, index,file_count); } else { // Wrong mime type - show in the list of files return self.createStatus( self.egw().lang("File is of wrong type (%1 != %2)!", this.files[index].type, self.options.mime), file_name ); } }, onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);}, onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);}, onError: function(event, name, error) { return self.onError(event,name,error);}, sendBoundary: window.FormData || jQuery.browser.mozilla, beforeSend: function(form) { return self.beforeSend(form);}, url: egw.ajaxUrl(self.egw().getAppName()+".etemplate_widget_file.ajax_upload.etemplate"), autoclear: !(this.options.onchange) },this.asyncOptions); this.asyncOptions.fieldName = this.options.id; this.createInputWidget(); }, destroy: function() { this._super.apply(this, arguments); this.set_drop_target(null); this.node = null; this.input = null; this.progress = null; }, createInputWidget: function() { this.node = $j(document.createElement("div")).addClass("et2_file"); this.input = $j(document.createElement("input")) .attr("type", "file").attr("placeholder", this.options.blur) .appendTo(this.node); // Check for File interface, should fall back to normal form submit if missing if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") { this.input.html5_upload(this.asyncOptions); } else { // This may be a problem submitting via ajax } if(this.options.progress) { var widget = this.getRoot().getWidgetById(this.options.progress); if(widget) { //may be not available at createInputWidget time this.progress = $j(widget.getDOMNode()); } } if(!this.progress) { this.progress = $j(document.createElement("div")).appendTo(this.node); } this.progress.addClass("progress"); if(this.options.multiple) { this.input.attr("multiple","multiple"); } this.setDOMNode(this.node[0]); }, /** * Set a widget or DOM node as a HTML5 file drop target * * @param String new_target widget ID or DOM node ID to be used as a new target */ set_drop_target: function(new_target) { // Cancel old drop target if(this.options.drop_target) { var widget = this.getRoot().getWidgetById(this.options.drop_target); var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); $j(drop_target).off("."+this.id); } this.options.drop_target = new_target; if(!this.options.drop_target) return; // Set up new drop target var widget = this.getRoot().getWidgetById(this.options.drop_target); var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); if(drop_target) { // Tell jQuery to include this property jQuery.event.props.push('dataTransfer'); var self = this; drop_target.ondrop =function(event) { return false; }; $j(drop_target) .on("drop."+this.id, jQuery.proxy(function(event) { event.stopPropagation(); event.preventDefault(); this.input.removeClass("ui-state-active"); if(event.dataTransfer && event.dataTransfer.files.length > 0) { this.input[0].files = event.dataTransfer.files; } return false; }, this)) .on("dragenter."+this.id,function(e) { // Accept the drop if at least one mimetype matches // Individual files will be rejected later var mime_ok = false; for(var file in e.dataTransfer.files) { mime_ok = mime_ok || self.checkMime(file); } if(!self.disabled && mime_ok) { self.input.addClass("ui-state-active"); } // Returning true cancels, return false to allow return self.disabled || !mime_ok; }) .on("dragleave."+this.id, function(e) { self.input.removeClass("ui-state-active"); // Returning true cancels, return false to allow return self.disabled }) .on("dragover."+this.id, function(e) { // Returning true cancels, return false to allow return self.disabled; }) .on("dragend."+this.id, false); } else { this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); } }, attachToDOM: function() { this._super.apply(this, arguments); // Override parent's change, file widget will fire change when finished uploading this.input.unbind("change.et2_inputWidget"); }, getValue: function() { var value = this.options.value ? this.options.value : this.input.val(); return value; }, /** * Set the value of the file widget. * * If you pass a FileList or list of files, it will trigger the async upload * * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. */ set_value: function(value) { if(!value || typeof value == "undefined") { value = {}; } if(jQuery.isEmptyObject(value)) { this.options.value = {}; this.progress.empty(); // Reset the HTML element this.input.wrap('
').closest('form').get(0).reset(); this.input.unwrap(); return; } if(typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) { this.input[0].files = value; } }, getInputNode: function() { return this.input[0]; }, set_mime: function(mime) { if(!mime) { this.options.mime = null; } if(mime.indexOf("/") != 0) { // Lower case it now, if it's not a regex this.options.mime = mime.toLowerCase(); } else { // Convert into a js regex var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); this.options.mime = new RegExp(parts[1],parts.length > 2 ? parts[2] : ""); } }, set_multiple: function(_multiple) { this.options.multiple = _multiple; if(_multiple) { return this.input.attr("multiple", "multiple"); } return this.input.removeAttr("multiple"); }, /** * Check to see if the provided file's mimetype matches * * @param f File object * @return boolean */ checkMime: function(f) { // If missing, let the server handle it if(!this.options.mime || !f.type) return true; var is_preg = (typeof this.options.mime == "object"); if(!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) { return true; } // Not right mime return false; }, /** * Add in the request id */ beforeSend: function(form) { var instance = this.getInstanceManager(); // Form object available, mess with it directly if(form) { return form.append("request_id", instance.etemplate_exec_id); } // No Form object, add in as string return 'Content-Disposition: form-data; name="request_id"\r\n\r\n'+instance.etemplate_exec_id+'\r\n'; }, /** * Disables submit buttons while uploading */ onStart: function(event, file_count) { this.disabled_buttons = $j("input[type='submit'], button").not("[disabled]").attr("disabled", true); event.data = this; if(this.options.onStart) return et2_call(this.options.onStart, event, file_count); return true; }, /** * Re-enables submit buttons when done */ onFinish: function(event, file_count) { this.disabled_buttons.attr("disabled", false); event.data = this; if(this.options.onFinish) return et2_call(this.options.onFinish, event, file_count); }, /** * Creates the elements used for displaying the file, and it's upload status, and * attaches them to the DOM * * @param _event Either the event, or an error message */ createStatus: function(_event, file_name, index, file_count) { var error = (typeof _event == "object" ? "" : _event); if(this.input[0].files[index]) { var file = this.input[0].files[index]; if(file.size > this.options.max_file_size) { error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); } } if(this.options.progress) { var widget = this.getRoot().getWidgetById(this.options.progress); if(widget) { this.progress = $j(widget.getDOMNode()); this.progress.addClass("progress"); } } if(this.progress) { var status = $j("
  • "+file_name +"

  • ") .appendTo(this.progress); $j("div.remove",status).click(this, this.cancel); if(error != "") { status.addClass("message ui-state-error"); status.append("
    "+error+""); $j(".progressBar",status).css("display", "none"); } } return error == ""; }, onProgress: function(event, percent, name, number, total) { if(this.progress) { $j("li[file='"+name+"'] > span.progressBar > p").css("width", Math.ceil(percent*100)+"%"); } return true; }, onError: function(event, name, error) { console.warn(event,name,error); }, /** * A file upload is finished, update the UI */ finishUpload: function(event, response, name, number, total) { if(typeof response == 'string') response = jQuery.parseJSON(response); if(response.response[0] && typeof response.response[0].data.length == 'undefined') { if(typeof this.options.value != 'object') this.options.value = {}; for(var key in response.response[0].data) { if(typeof response.response[0].data[key] == "string") { // Message from server - probably error $j("[file='"+name+"']",this.progress) .addClass("error") .css("display", "block") .text(response.response[0].data[key]); } else { this.options.value[key] = response.response[0].data[key]; if(this.progress) { $j("[file='"+name+"']",this.progress).addClass("message success"); } } } } else if (this.progress) { $j("[file='"+name+"']",this.progress) .addClass("ui-state-error") .css("display", "block") .text(this.egw().lang("Server error")); } if(this.options.onFinishOne) { return et2_call(this.options.onFinishOne,event,response,name,number,total); } return true; }, /** * Remove a file from the list of values */ remove_file: function(filename) { //console.info(filename); for(var key in this.options.value) { if(this.options.value[key].name == filename) { delete this.options.value[key]; $j('[file="'+filename+'"]',this.node).remove(); return; } } }, /** * Cancel a file - event callback */ cancel: function(e) { e.preventDefault(); // Look for file name in list var target = $j(e.target).parents("li.message"); e.data.remove_file.apply(e.data,[target.attr("file")]); // In case it didn't make it to the list (error) target.remove(); $j(e.target).remove(); } }); et2_register_widget(et2_file, ["file"]);