// vim: ts=4:sw=4:nu:fdc=4:nospell 
/** 
* Ext.ux.form.UploadPanel 
* 
* @author Ing. Jozef Sakáloš 
* @version $Id: Ext.ux.UploadPanel.js 310 2008-08-14 17:23:48Z jozo $ 
* @date 13. March 2008 
* 
* @license Ext.ux.form.UploadPanel is licensed under the terms of 
* the Open Source LGPL 3.0 license. Commercial use is permitted to the extent 
* that the code/component(s) do NOT become part of another Open Source or Commercially 
* licensed development library or toolkit without explicit permission. 
* 
* License details: http://www.gnu.org/licenses/lgpl.html 
*/ 
  
/*global Ext */ 
  
/** 
* @class Ext.ux.UploadPanel 
* @extends Ext.Panel 
*/ 
Ext.ux.UploadPanel = Ext.extend(Ext.Panel, { 
  
// configuration options overridable from outside 
// {{{ 
/** 
* @cfg {String} addIconCls icon class for add (file browse) button 
*/ 
addIconCls:'icon-plus' 
  
/** 
* @cfg {String} addText Text on Add button 
*/ 
,addText:'Add' 
  
/** 
* @cfg {Object} baseParams This object is not used directly by FileTreePanel but it is 
* propagated to lower level objects instead. Included here for convenience. 
*/ 
  
/** 
* @cfg {String} bodyStyle style to use for panel body 
*/ 
,bodyStyle:'padding:2px' 
  
/** 
* @cfg {String} buttonsAt Where buttons are placed. Valid values are tbar, bbar, body (defaults to 'tbar') 
*/ 
,buttonsAt:'tbar' 
  
/** 
* @cfg {String} clickRemoveText 
*/ 
,clickRemoveText:'Click to remove' 
  
/** 
* @cfg {String} clickStopText 
*/ 
,clickStopText:'Click to stop' 
  
/** 
* @cfg {String} emptyText empty text for dataview 
*/ 
,emptyText:'No files' 
  
/** 
* @cfg {Boolean} enableProgress true to enable querying server for progress information 
* Passed to underlying uploader. Included here for convenience. 
*/ 
,enableProgress:true 
  
/** 
* @cfg {String} errorText 
*/ 
,errorText:'Error' 
  
/** 
* @cfg {String} fileCls class prefix to use for file type classes 
*/ 
,fileCls:'file' 
  
/** 
* @cfg {String} fileQueuedText File upload status text 
*/ 
,fileQueuedText:'File <b>{0}</b> is queued for upload' 
  
/** 
* @cfg {String} fileDoneText File upload status text 
*/ 
,fileDoneText:'File <b>{0}</b> has been successfully uploaded' 
  
/** 
* @cfg {String} fileFailedText File upload status text 
*/ 
,fileFailedText:'File <b>{0}</b> failed to upload' 
  
/** 
* @cfg {String} fileStoppedText File upload status text 
*/ 
,fileStoppedText:'File <b>{0}</b> stopped by user' 
  
/** 
* @cfg {String} fileUploadingText File upload status text 
*/ 
,fileUploadingText:'Uploading file <b>{0}</b>' 
  
/** 
* @cfg {Number} maxFileSize Maximum upload file size in bytes 
* This config property is propagated down to uploader for convenience 
*/ 
,maxFileSize:524288 
  
/** 
* @cfg {Number} Maximum file name length for short file names 
*/ 
,maxLength:18 
  
/** 
* @cfg {String} removeAllIconCls iconClass to use for Remove All button (defaults to 'icon-cross' 
*/ 
,removeAllIconCls:'icon-cross' 
  
/** 
* @cfg {String} removeAllText text to use for Remove All button tooltip 
*/ 
,removeAllText:'Remove All' 
  
/** 
* @cfg {String} removeIconCls icon class to use for remove file icon 
*/ 
,removeIconCls:'icon-minus' 
  
/** 
* @cfg {String} removeText Remove text 
*/ 
,removeText:'Remove' 
  
/** 
* @cfg {String} selectedClass class for selected item of DataView 
*/ 
,selectedClass:'ux-up-item-selected' 
  
/** 
* @cfg {Boolean} singleUpload true to upload files in one form, false to upload one by one 
* This config property is propagated down to uploader for convenience 
*/ 
,singleUpload:false 
  
/** 
* @cfg {String} stopAllText 
*/ 
,stopAllText:'Stop All' 
  
/** 
* @cfg {String} stopIconCls icon class to use for stop 
*/ 
,stopIconCls:'icon-stop' 
  
/** 
* @cfg {String/Ext.XTemplate} tpl Template for DataView. 
*/ 
  
/** 
* @cfg {String} uploadText Upload text 
*/ 
,uploadText:'Upload' 
  
/** 
* @cfg {String} uploadIconCls icon class to use for upload button 
*/ 
,uploadIconCls:'icon-upload' 
  
/** 
* @cfg {String} workingIconCls iconClass to use for busy indicator 
*/ 
,workingIconCls:'icon-working' 
  
// }}} 
  
// overrides 
// {{{ 
,initComponent:function() { 
  
// {{{ 
// create buttons 
// add (file browse button) configuration 
var addCfg = { 
xtype:'browsebutton' 
,text:this.addText + '...' 
,iconCls:this.addIconCls 
,scope:this 
,handler:this.onAddFile 
}; 
  
// upload button configuration 
var upCfg = { 
xtype:'button' 
,iconCls:this.uploadIconCls 
,text:this.uploadText 
,scope:this 
,handler:this.onUpload 
,disabled:true 
}; 
  
// remove all button configuration 
var removeAllCfg = { 
xtype:'button' 
,iconCls:this.removeAllIconCls 
,tooltip:this.removeAllText 
,scope:this 
,handler:this.onRemoveAllClick 
,disabled:true 
}; 
  
// todo: either to cancel buttons in body or implement it 
if('body' !== this.buttonsAt) { 
this[this.buttonsAt] = [addCfg, upCfg, '->', removeAllCfg]; 
} 
// }}} 
// {{{ 
// create store 
// fields for record 
var fields = [ 
{name:'id', type:'text', system:true} 
,{name:'shortName', type:'text', system:true} 
,{name:'fileName', type:'text', system:true} 
,{name:'filePath', type:'text', system:true} 
,{name:'fileCls', type:'text', system:true} 
,{name:'input', system:true} 
,{name:'form', system:true} 
,{name:'state', type:'text', system:true} 
,{name:'error', type:'text', system:true} 
,{name:'progressId', type:'int', system:true} 
,{name:'bytesTotal', type:'int', system:true} 
,{name:'bytesUploaded', type:'int', system:true} 
,{name:'estSec', type:'int', system:true} 
,{name:'filesUploaded', type:'int', system:true} 
,{name:'speedAverage', type:'int', system:true} 
,{name:'speedLast', type:'int', system:true} 
,{name:'timeLast', type:'int', system:true} 
,{name:'timeStart', type:'int', system:true} 
,{name:'pctComplete', type:'int', system:true} 
]; 
  
// add custom fields if passed 
if(Ext.isArray(this.customFields)) { 
fields.push(this.customFields); 
} 
  
// create store 
this.store = new Ext.data.SimpleStore({ 
id:0 
,fields:fields 
,data:[] 
}); 
// }}} 
// {{{ 
// create view 
Ext.apply(this, { 
items:[{ 
xtype:'dataview' 
,itemSelector:'div.ux-up-item' 
,store:this.store 
,selectedClass:this.selectedClass 
,singleSelect:true 
,emptyText:this.emptyText 
,tpl: this.tpl || new Ext.XTemplate( 
'<tpl for=".">' 
+ '<div class="ux-up-item">' 
// + '<div class="ux-up-indicator">&#160;</div>' 
+ '<div class="ux-up-icon-file {fileCls}">&#160;</div>' 
+ '<div class="ux-up-text x-unselectable" qtip="{fileName}">{shortName}</div>' 
+ '<div id="remove-{[values.input.id]}" class="ux-up-icon-state ux-up-icon-{state}"' 
+ 'qtip="{[this.scope.getQtip(values)]}">&#160;</div>' 
+ '</div>' 
+ '</tpl>' 
, {scope:this} 
) 
,listeners:{click:{scope:this, fn:this.onViewClick}} 
  
}] 
}); 
// }}} 
  
// call parent 
Ext.ux.UploadPanel.superclass.initComponent.apply(this, arguments); 
  
// save useful references 
this.view = this.items.itemAt(0); 
  
// {{{ 
// add events 
this.addEvents( 
/** 
* Fires before the file is added to store. Return false to cancel the add 
* @event beforefileadd 
* @param {Ext.ux.UploadPanel} this 
* @param {Ext.Element} input (type=file) being added 
*/ 
'beforefileadd' 
/** 
* Fires after the file is added to the store 
* @event fileadd 
* @param {Ext.ux.UploadPanel} this 
* @param {Ext.data.Store} store 
* @param {Ext.data.Record} Record (containing the input) that has been added to the store 
*/ 
,'fileadd' 
/** 
* Fires before the file is removed from the store. Return false to cancel the remove 
* @event beforefileremove 
* @param {Ext.ux.UploadPanel} this 
* @param {Ext.data.Store} store 
* @param {Ext.data.Record} Record (containing the input) that is being removed from the store 
*/ 
,'beforefileremove' 
/** 
* Fires after the record (file) has been removed from the store 
* @event fileremove 
* @param {Ext.ux.UploadPanel} this 
* @param {Ext.data.Store} store 
*/ 
,'fileremove' 
/** 
* Fires before all files are removed from the store (queue). Return false to cancel the clear. 
* Events for individual files being removed are suspended while clearing the queue. 
* @event beforequeueclear 
* @param {Ext.ux.UploadPanel} this 
* @param {Ext.data.Store} store 
*/ 
,'beforequeueclear' 
/** 
* Fires after the store (queue) has been cleared 
* Events for individual files being removed are suspended while clearing the queue. 
* @event queueclear 
* @param {Ext.ux.UploadPanel} this 
* @param {Ext.data.Store} store 
*/ 
,'queueclear' 
/** 
* Fires after the upload button is clicked but before any upload is started 
* Return false to cancel the event 
* @param {Ext.ux.UploadPanel} this 
*/ 
,'beforeupload' 
); 
// }}} 
// {{{ 
// relay view events 
this.relayEvents(this.view, [ 
'beforeclick' 
,'beforeselect' 
,'click' 
,'containerclick' 
,'contextmenu' 
,'dblclick' 
,'selectionchange' 
]); 
// }}} 
  
// create uploader 
var config = { 
store:this.store 
,singleUpload:this.singleUpload 
,maxFileSize:this.maxFileSize 
,enableProgress:this.enableProgress 
,url:this.url 
,path:this.path 
}; 
if(this.baseParams) { 
config.baseParams = this.baseParams; 
} 
this.uploader = new Ext.ux.FileUploader(config); 
  
// relay uploader events 
this.relayEvents(this.uploader, [ 
'beforeallstart' 
,'allfinished' 
,'progress' 
]); 
  
// install event handlers 
this.on({ 
beforeallstart:{scope:this, fn:function() { 
this.uploading = true; 
this.updateButtons(); 
}} 
,allfinished:{scope:this, fn:function() { 
this.uploading = false; 
this.updateButtons(); 
}} 
,progress:{fn:this.onProgress.createDelegate(this)} 
}); 
} // eo function initComponent 
// }}} 
// {{{ 
/** 
* onRender override, saves references to buttons 
* @private 
*/ 
,onRender:function() { 
// call parent 
Ext.ux.UploadPanel.superclass.onRender.apply(this, arguments); 
  
// save useful references 
var tb = 'tbar' === this.buttonsAt ? this.getTopToolbar() : this.getBottomToolbar(); 
this.addBtn = Ext.getCmp(tb.items.first().id); 
this.uploadBtn = Ext.getCmp(tb.items.itemAt(1).id); 
this.removeAllBtn = Ext.getCmp(tb.items.last().id); 
} // eo function onRender 
// }}} 
  
// added methods 
// {{{ 
/** 
* called by XTemplate to get qtip depending on state 
* @private 
* @param {Object} values XTemplate values 
*/ 
,getQtip:function(values) { 
var qtip = ''; 
switch(values.state) { 
case 'queued': 
qtip = String.format(this.fileQueuedText, values.fileName); 
qtip += '<br>' + this.clickRemoveText; 
break; 
  
case 'uploading': 
qtip = String.format(this.fileUploadingText, values.fileName); 
qtip += '<br>' + values.pctComplete + '% done'; 
qtip += '<br>' + this.clickStopText; 
break; 
  
case 'done': 
qtip = String.format(this.fileDoneText, values.fileName); 
qtip += '<br>' + this.clickRemoveText; 
break; 
  
case 'failed': 
qtip = String.format(this.fileFailedText, values.fileName); 
qtip += '<br>' + this.errorText + ':' + values.error; 
qtip += '<br>' + this.clickRemoveText; 
break; 
  
case 'stopped': 
qtip = String.format(this.fileStoppedText, values.fileName); 
qtip += '<br>' + this.clickRemoveText; 
break; 
} 
return qtip; 
} // eo function getQtip 
// }}} 
// {{{ 
/** 
* get file name 
* @private 
* @param {Ext.Element} inp Input element containing the full file path 
* @return {String} 
*/ 
,getFileName:function(inp) { 
return inp.getValue().split(/[\/\\]/).pop(); 
} // eo function getFileName 
// }}} 
// {{{ 
/** 
* get file path (excluding the file name) 
* @private 
* @param {Ext.Element} inp Input element containing the full file path 
* @return {String} 
*/ 
,getFilePath:function(inp) { 
return inp.getValue().replace(/[^\/\\]+$/,''); 
} // eo function getFilePath 
// }}} 
// {{{ 
/** 
* returns file class based on name extension 
* @private 
* @param {String} name File name to get class of 
* @return {String} class to use for file type icon 
*/ 
,getFileCls: function(name) { 
var atmp = name.split('.'); 
if(1 === atmp.length) { 
return this.fileCls; 
} 
else { 
return this.fileCls + '-' + atmp.pop().toLowerCase(); 
} 
} 
// }}} 
// {{{ 
/** 
* called when file is added - adds file to store 
* @private 
* @param {Ext.ux.BrowseButton} 
*/ 
,onAddFile:function(bb) { 
if(true !== this.eventsSuspended && false === this.fireEvent('beforefileadd', this, bb.getInputFile())) { 
return; 
} 
var inp = bb.detachInputFile(); 
inp.addClass('x-hidden'); 
var fileName = this.getFileName(inp); 
  
// create new record and add it to store 
var rec = new this.store.recordType({ 
input:inp 
,fileName:fileName 
,filePath:this.getFilePath(inp) 
,shortName: Ext.util.Format.ellipsis(fileName, this.maxLength) 
,fileCls:this.getFileCls(fileName) 
,state:'queued' 
}, inp.id); 
rec.commit(); 
this.store.add(rec); 
  
this.syncShadow(); 
  
this.uploadBtn.enable(); 
this.removeAllBtn.enable(); 
  
if(true !== this.eventsSuspended) { 
this.fireEvent('fileadd', this, this.store, rec); 
} 
  
this.doLayout(); 
  
} // eo onAddFile 
// }}} 
// {{{ 
/** 
* destroys child components 
* @private 
*/ 
,onDestroy:function() { 
  
// destroy uploader 
if(this.uploader) { 
this.uploader.stopAll(); 
this.uploader.purgeListeners(); 
this.uploader = null; 
} 
  
// destroy view 
if(this.view) { 
this.view.purgeListeners(); 
this.view.destroy(); 
this.view = null; 
} 
  
// destroy store 
if(this.store) { 
this.store.purgeListeners(); 
this.store.destroy(); 
this.store = null; 
} 
  
} // eo function onDestroy 
// }}} 
// {{{ 
/** 
* progress event handler 
* @private 
* @param {Ext.ux.FileUploader} uploader 
* @param {Object} data progress data 
* @param {Ext.data.Record} record 
*/ 
,onProgress:function(uploader, data, record) { 
var bytesTotal, bytesUploaded, pctComplete, state, idx, item, width, pgWidth; 
if(record) { 
state = record.get('state'); 
bytesTotal = record.get('bytesTotal') || 1; 
bytesUploaded = record.get('bytesUploaded') || 0; 
if('uploading' === state) { 
pctComplete = Math.round(1000 * bytesUploaded/bytesTotal) / 10; 
} 
else if('done' === state) { 
pctComplete = 100; 
} 
else { 
pctComplete = 0; 
} 
record.set('pctComplete', pctComplete); 
  
idx = this.store.indexOf(record); 
item = Ext.get(this.view.getNode(idx)); 
if(item) { 
width = item.getWidth(); 
item.applyStyles({'background-position':width * pctComplete / 100 + 'px'}); 
} 
} 
} // eo function onProgress 
// }}} 
// {{{ 
/** 
* called when file remove icon is clicked - performs the remove 
* @private 
* @param {Ext.data.Record} 
*/ 
,onRemoveFile:function(record) { 
if(true !== this.eventsSuspended && false === this.fireEvent('beforefileremove', this, this.store, record)) { 
return; 
} 
  
// remove DOM elements 
var inp = record.get('input'); 
var wrap = inp.up('em'); 
inp.remove(); 
if(wrap) { 
wrap.remove(); 
} 
  
// remove record from store 
this.store.remove(record); 
  
var count = this.store.getCount(); 
this.uploadBtn.setDisabled(!count); 
this.removeAllBtn.setDisabled(!count); 
  
if(true !== this.eventsSuspended) { 
this.fireEvent('fileremove', this, this.store); 
this.syncShadow(); 
} 
} // eo function onRemoveFile 
// }}} 
// {{{ 
/** 
* Remove All/Stop All button click handler 
* @private 
*/ 
,onRemoveAllClick:function(btn) { 
if(true === this.uploading) { 
this.stopAll(); 
} 
else { 
this.removeAll(); 
} 
} // eo function onRemoveAllClick 
  
,stopAll:function() { 
this.uploader.stopAll(); 
} // eo function stopAll 
// }}} 
// {{{ 
/** 
* DataView click handler 
* @private 
*/ 
,onViewClick:function(view, index, node, e) { 
var t = e.getTarget('div:any(.ux-up-icon-queued|.ux-up-icon-failed|.ux-up-icon-done|.ux-up-icon-stopped)'); 
if(t) { 
this.onRemoveFile(this.store.getAt(index)); 
} 
t = e.getTarget('div.ux-up-icon-uploading'); 
if(t) { 
this.uploader.stopUpload(this.store.getAt(index)); 
} 
} // eo function onViewClick 
// }}} 
// {{{ 
/** 
* tells uploader to upload 
* @private 
*/ 
,onUpload:function() { 
if(true !== this.eventsSuspended && false === this.fireEvent('beforeupload', this)) { 
return false; 
} 
this.uploader.upload(); 
} // eo function onUpload 
// }}} 
// {{{ 
/** 
* url setter 
*/ 
,setUrl:function(url) { 
this.url = url; 
this.uploader.setUrl(url); 
} // eo function setUrl 
// }}} 
// {{{ 
/** 
* path setter 
*/ 
,setPath:function(path) { 
this.uploader.setPath(path); 
} // eo function setPath 
// }}} 
// {{{ 
/** 
* Updates buttons states depending on uploading state 
* @private 
*/ 
,updateButtons:function() { 
if(true === this.uploading) { 
this.addBtn.disable(); 
this.uploadBtn.disable(); 
this.removeAllBtn.setIconClass(this.stopIconCls); 
this.removeAllBtn.getEl().child(this.removeAllBtn.buttonSelector).dom[this.removeAllBtn.tooltipType] = this.stopAllText; 
} 
else { 
this.addBtn.enable(); 
this.uploadBtn.enable(); 
this.removeAllBtn.setIconClass(this.removeAllIconCls); 
this.removeAllBtn.getEl().child(this.removeAllBtn.buttonSelector).dom[this.removeAllBtn.tooltipType] = this.removeAllText; 
} 
} // eo function updateButtons 
// }}} 
// {{{ 
/** 
* Removes all files from store and destroys file inputs 
*/ 
,removeAll:function() { 
var suspendState = this.eventsSuspended; 
if(false !== this.eventsSuspended && false === this.fireEvent('beforequeueclear', this, this.store)) { 
return false; 
} 
this.suspendEvents(); 
  
this.store.each(this.onRemoveFile, this); 
  
this.eventsSuspended = suspendState; 
if(true !== this.eventsSuspended) { 
this.fireEvent('queueclear', this, this.store); 
} 
this.syncShadow(); 
} // eo function removeAll 
// }}} 
// {{{ 
/** 
* synchronize context menu shadow if we're in contextmenu 
* @private 
*/ 
,syncShadow:function() { 
if(this.contextmenu && this.contextmenu.shadow) { 
this.contextmenu.getEl().shadow.show(this.contextmenu.getEl()); 
} 
} // eo function syncShadow 
// }}} 
  
}); // eo extend 
  
// register xtype 
Ext.reg('uploadpanel', Ext.ux.UploadPanel); 
  
// eof 

