Ext.ux.panel.UploadPanel for ExtJs 4
Wit this extension of ExtJs you can use a normal panel to upload multiple files via Plupload
Features
- Multiple uploads at one time.
- Drag&Drop on some browsers.
- Chunk-Uploads with progress-bars for current file & total files with estimated upload-time.
- Can be used as any normal Ext.grid.Panel.
Important
Version 0.1: Use at your own risk. If you like the tool, please link to this page. Based on this extension for ExtJs 3.
Example
...coming soon.
Feedback/Discussion
... can be given at the forum at sencha.com. I would be grateful if you give me feedback on errors or improvements.
// Ext.ux.panel.UploadPanel for ExtJs 4 // Source: http://www.thomatechnik.de/webmaster-tools/extjs-plupload/ // Based on: http://www.sencha.com/forum/showthread.php?98033-Ext.ux.Plupload-Panel-Button-%28file-uploader%29 // Please link to this page if you find this extension usefull // Version 0.1 Ext.define('Ext.ux.panel.UploadPanel', { extend: 'Ext.grid.Panel', alias: 'widget.xuploadpanel', // Configuration title: 'Upload', url: '/upload.php', // URL to your server-side upload-script chunk_size: '512kb', // The chunk-size max_file_size: '100mb', // The max. allowed file-size unique_names: false, // Make sure to use only unique-names multipart: true, // Use multipart-uploads pluploadPath: '/ttcms/ttcms_admin/js/plupload/', // Path to plupload pluploadRuntimes: 'html5,gears,browserplus,silverlight,flash,html4', // All the runtimes you want to use // Texts (language-dependent) texts: { status: ['QUEUED', 'UPLOADING', 'UNKNOWN', 'FAILED', 'DONE'], DragDropAvailable: 'Drag&Drop files here', noDragDropAvailable: 'This Browser doesn\'t support drag&drop.', emptyTextTpl: '<div style="color:#808080; margin:0 auto; text-align:center; top:48%; position:relative;">{0}</div>', cols: ["File", "Size", "State", "Message"], addButtonText: 'add file', uploadButtonText: 'upload', cancelButtonText: 'cancel', deleteButtonText: 'delete', deleteUploadedText: 'delete finished', deleteAllText: 'delete all', deleteSelectedText: 'delete selected', progressCurrentFile: 'current file:', progressTotal: 'total:', statusInvalidSizeText: 'File too big', statusInvalidExtensionText: 'invalid file-type' }, // Internal (do not change) // Grid-View multiSelect: true, viewConfig: { deferEmptyText: false // For showing emptyText }, // Hack: loaded of the actual file (plupload is sometimes a step ahead) loadedFile: 0, constructor: function(config) { // List of files this.success = []; this.failed = []; // Column-Headers config.columns = [ { header: this.texts.cols[0], flex: 1, dataIndex: 'name' }, { header: this.texts.cols[1], flex: 1, align: 'right', dataIndex: 'size', renderer: Ext.util.Format.fileSize }, { header: this.texts.cols[2], flex: 1, dataIndex: 'status', renderer: this.renderStatus }, { header: this.texts.cols[3], flex: 1, dataIndex: 'msg' } ]; // Model and Store if (!Ext.ModelManager.getModel('Plupload')) { Ext.define('Plupload', { extend: 'Ext.data.Model', fields: [ 'id', 'loaded', 'name', 'size', 'percent', 'status', 'msg' ] }); }; config.store = { type: 'json', model: 'Plupload', listeners: { load: this.onStoreLoad, remove: this.onStoreRemove, update: this.onStoreUpdate, scope: this }, proxy: 'memory' }; // Top-Bar this.tbar = { enableOverflow: true, items: [ new Ext.Button({ text: this.texts.addButtonText, itemId: 'addButton', iconCls: config.addButtonCls || 'pluploadAddCls', disabled: true }), new Ext.Button({ text: this.texts.uploadButtonText, handler: this.onStart, scope: this, disabled: true, itemId: 'upload', iconCls: config.uploadButtonCls || 'pluploadUploadCls' }), new Ext.Button({ text: this.texts.cancelButtonText, handler: this.onCancel, scope: this, disabled: true, itemId: 'cancel', iconCls: config.cancelButtonCls || 'pluploadCancelCls' }), new Ext.SplitButton({ text: this.texts.deleteButtonText, handler: this.onDeleteSelected, menu: new Ext.menu.Menu({ items: [ {text: this.texts.deleteUploadedText, handler: this.onDeleteUploaded, scope: this }, '-', {text: this.texts.deleteAllText, handler: this.onDeleteAll, scope: this }, '-', {text: this.texts.deleteSelectedText, handler: this.onDeleteSelected, scope: this } ] }), scope: this, disabled: true, itemId: 'delete', iconCls: config.deleteButtonCls || 'pluploadDeleteCls' }) ] }; // Progress-Bar (bottom) this.progressBarSingle = new Ext.ProgressBar( { flex: 1, animate: true }); this.progressBarAll = new Ext.ProgressBar( { flex: 2, animate: true }); this.bbar = { layout: 'hbox', style: { paddingLeft: '5px' }, items: [ this.texts.progressCurrentFile, this.progressBarSingle, { xtype: 'tbtext', itemId: 'single', style: 'text-align:right', text: '', width:100 }, this.texts.progressTotal, this.progressBarAll, { xtype: 'tbtext', itemId: 'all', style: 'text-align:right', text: '', width:100 }, { xtype: 'tbtext', itemId: 'speed', style: 'text-align:right', text: '', width:100 }, { xtype: 'tbtext', itemId: 'remaining', style: 'text-align:right', text: '', width:100 } ] }; this.callParent(arguments); }, afterRender: function() { this.callParent(arguments); this.initPlUpload(); }, renderStatus: function(value, meta, record, rowIndex, colIndex, store, view) { var s = this.status[value-1]; if (value == 2) { s += " "+record.get("percent")+" %"; } return s; }, /* renderProgress: function(value, meta, record, rowIndex, colIndex, store, view) { return value; console.log("Call renderer", value); var id; if (this.progressBars[rowIndex] === undefined) { console.log("Create Bar"); id = Ext.id(); this.progressBars[rowIndex] = id; Ext.Function.defer(function(id, record) { console.log("Create bar ", id, value, record); var bar = new Ext.ProgressBar( { height: 15, renderTo: id, value: (value / 100) }); this.progressBars[record.id] = bar; console.log("After create bar", id, value, record); }, 25, this, [id, record]); } else { if (Ext.isObject(this.progressBars[rowIndex])) { var bar = this.progressBars[rowIndex]; bar.setValue(value); id = bar.getEl().dom.id; console.log("Fetch bar ", id); } else console.log("Wait for creation"); } return (String.format('<div id="{0}"></div>', id)); }, */ getTopToolbar: function() { var bars = this.getDockedItems('toolbar[dock="top"]'); return bars[0]; }, getBottomToolbar: function() { var bars = this.getDockedItems('toolbar[dock="bottom"]'); return bars[0]; }, initPlUpload: function () { this.uploader = new plupload.Uploader( { url: this.url, runtimes: this.pluploadRuntimes, browse_button: this.getTopToolbar().getComponent('addButton').getEl().dom.id, container: this.getEl().dom.id, max_file_size: this.max_file_size || '', resize: this.resize || '', flash_swf_url: this.pluploadPath+'/plupload.flash.swf', silverlight_xap_url: this.pluploadPath+'plupload.silverlight.xap', filters : this.filters || [], chunk_size: this.chunk_size, unique_names: this.unique_names, multipart: this.multipart, multipart_params: this.multipart_params || null, drop_element: this.getEl().dom.id, required_features: this.required_features || null }); // Events Ext.each(['Init', 'ChunkUploaded', 'FilesAdded', 'FilesRemoved', 'FileUploaded', 'PostInit', 'QueueChanged', 'Refresh', 'StateChanged', 'UploadFile', 'UploadProgress', 'Error' ], function (v) { this.uploader.bind(v, eval("this.Plupload" + v), this); }, this); // Init Plupload this.uploader.init(); }, onDeleteSelected: function () { Ext.each(this.getView().getSelectionModel().getSelection(), function (record) { this.remove_file( record.get( 'id' ) ); }, this ); }, onDeleteAll: function () { this.store.each( function (record) { this.remove_file( record.get( 'id' ) ); }, this ); }, onDeleteUploaded: function () { this.store.each( function (record) { if ( record.get( 'status' ) == 5 ) { this.remove_file( record.get( 'id' ) ); } }, this ); }, onCancel: function () { this.uploader.stop(); this.updateProgress(); }, onStart: function () { this.fireEvent('beforestart', this); if (this.multipart_params) { this.uploader.settings.multipart_params = this.multipart_params; } this.uploader.start(); }, remove_file: function (id) { var fileObj = this.uploader.getFile(id); if (fileObj) { this.uploader.removeFile(fileObj); } else { this.store.remove(this.store.getById(id)); } }, updateStore: function(files) { Ext.each(files, function(data) { this.updateStoreFile(data); }, this); }, updateStoreFile: function (data) { data.msg = data.msg || ''; var record = this.store.getById(data.id); if (record) { record.set(data); record.commit(); } else { this.store.add(data); } }, onStoreLoad: function (store, record, operation) { }, onStoreRemove: function (store, record, operation) { if (!store.data.length) { this.getTopToolbar().getComponent('delete').setDisabled(true); this.uploader.total.reset(); } var id = record.get('id'); Ext.each( this.success, function (v) { if ( v && v.id == id ) { Ext.Array.remove(this.success, v); } }, this ); Ext.each( this.failed, function (v) { if ( v && v.id == id ) { Ext.Array.remove(this.failed, v); } }, this ); }, onStoreUpdate: function (store, record, operation) { var canUpload = false; if (this.uploader.state != 2) { this.store.each(function (record) { if (record.get("status") == 1) { canUpload = true; return false; } }, this); } this.getTopToolbar().getComponent('upload').setDisabled(!canUpload); }, updateProgress: function(file) { var queueProgress = this.uploader.total; // All var total = queueProgress.size; var uploaded = queueProgress.loaded; this.getBottomToolbar().getComponent('all').setText(Ext.util.Format.fileSize(uploaded)+"/"+Ext.util.Format.fileSize(total)); if (total > 0) this.progressBarAll.updateProgress(queueProgress.percent/100, queueProgress.percent+" %"); else this.progressBarAll.updateProgress(0, ' '); // Speed+Remaining var speed = queueProgress.bytesPerSec; if (speed > 0) { var totalSec = parseInt((total-uploaded)/speed); var hours = parseInt( totalSec / 3600 ) % 24; var minutes = parseInt( totalSec / 60 ) % 60; var seconds = totalSec % 60; var timeRemaining = result = (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds); this.getBottomToolbar().getComponent('speed').setText(Ext.util.Format.fileSize(speed)+'/s'); this.getBottomToolbar().getComponent('remaining').setText(timeRemaining); } else { this.getBottomToolbar().getComponent('speed').setText(''); this.getBottomToolbar().getComponent('remaining').setText(''); } // Single if (!file) { this.getBottomToolbar().getComponent('single').setText(''); this.progressBarSingle.updateProgress(0, ' '); } else { total = file.size; //uploaded = file.loaded; // file.loaded sometimes is 1 step ahead, so we can not use it. //uploaded = 0; if (file.percent > 0) uploaded = file.size * file.percent / 100.0; // But this solution is imprecise as well since percent is only a hint uploaded = this.loadedFile; // So we use this Hack to store the value which is one step back this.getBottomToolbar().getComponent('single').setText(Ext.util.Format.fileSize(uploaded)+"/"+Ext.util.Format.fileSize(total)); this.progressBarSingle.updateProgress(file.percent/100, (file.percent).toFixed(0)+" %"); } }, PluploadInit: function(uploader, data) { this.getTopToolbar().getComponent('addButton').setDisabled(false); // console.log("Runtime: ", data.runtime); if (data.runtime == "flash" || data.runtime == "silverlight" || data.runtime == "html4") { this.view.emptyText = this.texts.noDragDropAvailable; } else { this.view.emptyText = this.texts.DragDropAvailable } this.view.emptyText = String.format(this.texts.emptyTextTpl, this.view.emptyText); this.view.refresh(); this.updateProgress(); }, PluploadChunkUploaded: function() { }, PluploadFilesAdded: function(uploader, files) { this.getTopToolbar().getComponent('delete').setDisabled(false); this.updateStore(files); this.updateProgress(); }, PluploadFilesRemoved: function(uploader, files) { Ext.each(files, function (file) { this.store.remove( this.store.getById( file.id ) ); }, this ); this.updateProgress(); }, PluploadFileUploaded: function(uploader, file, status) { var response = Ext.JSON.decode( status.response ); if ( response.success == true ) { file.server_error = 0; this.success.push(file); } else { if ( response.message ) { file.msg = '<span style="color: red">' + response.message + '</span>'; } file.server_error = 1; this.failed.push(file); } this.updateStoreFile(file); this.updateProgress(file); }, PluploadPostInit: function() { }, PluploadQueueChanged: function(uploader) { this.updateProgress(); }, PluploadRefresh: function(uploader) { this.updateStore(uploader.files); this.updateProgress(); }, PluploadStateChanged: function(uploader) { if (uploader.state == 2) { this.fireEvent('uploadstarted', this); this.getTopToolbar().getComponent('cancel').setDisabled(false); } else { this.fireEvent('uploadcomplete', this, this.success, this.failed); this.getTopToolbar().getComponent('cancel').setDisabled(true); } }, PluploadUploadFile: function() { this.loadedFile = 0; }, PluploadUploadProgress: function(uploader, file) { // No chance to stop here - we get no response-text from the server. So just continue if something fails here. Will be fixed in next update, says plupload. if ( file.server_error ) { file.status = 4; } this.updateStoreFile(file); this.updateProgress(file); this.loadedFile = file.loaded; }, PluploadError: function (uploader, data) { data.file.status = 4; if ( data.code == -600 ) { data.file.msg = String.format( '<span style="color: red">{0}</span>', this.texts.statusInvalidSizeText ); } else if ( data.code == -700 ) { data.file.msg = String.format( '<span style="color: red">{0}</span>', this.texts.statusInvalidExtensionText ); } else { data.file.msg = String.format( '<span style="color: red">{2} ({0}: {1})</span>', data.code, data.details, data.message ); } this.updateStoreFile(data.file); this.updateProgress(); } });
You need to add some more code:
// Advance File-Size Ext.util.Format.fileSize = function(value) { if (value > 1) { var s = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; var e = Math.floor(Math.log(value)/Math.log(1024)); if (e > 0) return (value/Math.pow(1024, Math.floor(e))).toFixed(2)+" "+s[e]; else return value+" "+s[e]; } else if (value == 1) { return "1 Byte"; } return '-'; } String.format = function() { var s = arguments[0]; for (var i = 0; i < arguments.length - 1; i++) { var reg = new RegExp("\\{" + i + "\\}", "gm"); s = s.replace(reg, arguments[i + 1]); } return s; }