├── .gitignore ├── CHANGELOG.md ├── README.md ├── docs ├── README.md └── jsduck-config.json ├── lib └── upload │ ├── BrowseButton.js │ ├── Dialog.js │ ├── Item.js │ ├── ItemGridPanel.js │ ├── LegacyDialog.js │ ├── Manager.js │ ├── Panel.js │ ├── Queue.js │ ├── StatusBar.js │ ├── Store.js │ ├── css │ └── upload.css │ ├── data │ └── Connection.js │ ├── header │ ├── AbstractFilenameEncoder.js │ └── Base64FilenameEncoder.js │ ├── img │ ├── accept.png │ ├── ajax-loader1.gif │ ├── ajax-loader2.gif │ ├── ajax-loader3.gif │ ├── arrow_up.png │ ├── cancel.png │ ├── delete.png │ ├── exclamation.png │ ├── folder.png │ ├── help.png │ ├── loading.gif │ └── tick.png │ └── uploader │ ├── AbstractUploader.js │ ├── AbstractXhrUploader.js │ ├── DummyUploader.js │ ├── ExtJsUploader.js │ ├── FormDataUploader.js │ └── LegacyExtJsUploader.js └── public ├── _common.php ├── _config.php.dist ├── app.js ├── docs ├── external └── upload ├── img └── extjs-upload-widget.jpeg ├── index-ext4.html ├── index.html ├── upload.php └── upload_multipart.php /.gitignore: -------------------------------------------------------------------------------- 1 | public/extjs 2 | public/_config.php 3 | docs/generated 4 | .buildpath 5 | .project 6 | .settings -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Ext JS Upload Widget Changelog 2 | 3 | ## v1.1.1 4 | (2013-11-05) 5 | 6 | - Option to re-try upload when errors occur (#9) 7 | - Add the DummyUploader to the example (#11) 8 | - Use "itemId" instead of "id" property (#14) 9 | - Fixed: The "Browse" button loses the "multiple" attribute (#10) 10 | - Fixed: Handle utf-8 encoded filenames properly (#15) 11 | 12 | 13 | ## v1.1.0 14 | (2013-06-12) 15 | 16 | - implemented the core functionality as a panel rather than a dialog 17 | - multiple uploader implementations support 18 | - you can now "inject" your own uploader implementation 19 | - new FormDataUploader implementing multipart upload 20 | - updated examples and documentation 21 | 22 | 23 | ## v1.0.1 24 | (2013-06-10) 25 | 26 | - increased default connection timeout 27 | - removed obsolete code 28 | 29 | 30 | ## v1.0.0 31 | (2012-09-27) 32 | 33 | - initial release 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File upload widget for Sencha Ext JS 2 | 3 | ## Features 4 | 5 | - flexible and easily integratable 6 | - uses the native File API (HTML5) 7 | - allows selecting and uploading of multiple files at once 8 | - supports both raw PUT/POST upload and multipart upload 9 | - you can easily write and integrate your own upload mechanism, while preserving all (or most) of the UI functionality 10 | - displays upload progress 11 | - supports asynchronous (simultaneous) upload 12 | 13 | ## Online demo 14 | 15 | - [Demo included in this repository](http://debug.cz/demo/upload/) 16 | 17 | ## Requirements 18 | 19 | - [Sencha Ext JS 5.x](http://www.sencha.com/products/extjs/) 20 | - browser supporting the [File API](http://www.w3.org/TR/FileAPI/) - see more info about [browser compatibility](http://caniuse.com/fileapi) 21 | - server side to process the uploaded files 22 | 23 | ## Installation 24 | 25 | Clone the repository somewhere on your system and add the path with the prefix to `Ext.Loader`: 26 | 27 | Ext.Loader.setPath({ 28 | 'Ext.ux.upload' : '/my/path/to/extjs-upload-widget/lib/upload' 29 | }); 30 | 31 | ## Basic usage 32 | 33 | In the most simple case, you can just open the dialog and pass the `uploadUrl` paramter: 34 | 35 | var dialog = Ext.create('Ext.ux.upload.Dialog', { 36 | dialogTitle: 'My Upload Widget', 37 | uploadUrl: 'upload.php' 38 | }); 39 | 40 | dialog.show(); 41 | 42 | `Ext.ux.upload.Dialog` is just a simple wrapper window. The core functionality is implemented in the `Ext.uz.upload.Panel` object, so you can implement your own dialog and pass the panel: 43 | 44 | var myDialog = Ext.create('MyDialog', { 45 | items: [ 46 | Ext.create('Ext.ux.upload.Panel', { 47 | uploadUrl: 'upload.php' 48 | }); 49 | ] 50 | }); 51 | 52 | ## Uploaders 53 | 54 | The intention behind the uploaders implementation is to have the upload process decoupled from the UI as much as possible. This allows us to create alternative uploader implementations to serve our use case and at the same time, we don't need to touch the UI. 55 | 56 | Currently, these uploaders are implemented: 57 | 58 | - __ExtJsUploader__ (default) - uploads the file by sending the raw file data in the body of a _XmlHttpRequest_. File metadata are sent through request HTTP headers. Actually, the standard `Ext.data.Connection` object is used with a small tweak to allow progress reporting. 59 | - __FormDataUploader__ - uploads the file through a _XmlHttpRequest_ as if it was submitted with a form. 60 | 61 | Each uploader requires different processing at the backend side. Check the `public/upload.php` file for the __ExtJsUploader__ and the `public/upload_multipart.php` for the __FormDataUploader__. 62 | 63 | ## Advanced usage 64 | 65 | The default uploader is the __ExtJsUploader__. If you want to use an alternative uploader, you need to pass the uploader class name to the upload panel: 66 | 67 | var panel = Ext.create('Ext.ux.upload.Panel', { 68 | uploader: 'Ext.ux.upload.uploader.FormDataUploader', 69 | uploaderOptions: { 70 | url: 'upload_multipart.php', 71 | timeout: 120*1000 72 | } 73 | }); 74 | 75 | Or you can pass the uploader instance: 76 | 77 | var panel = Ext.create('Ext.ux.upload.Panel', { 78 | uploader: Ext.create('Ext.ux.upload.uploader.FormDataUploader', { 79 | url: 'upload_multipart.php', 80 | timeout: 120*1000 81 | }); 82 | }); 83 | 84 | ## Running the example 85 | 86 | 87 | ### Requirements: 88 | 89 | - web server with PHP support 90 | - Ext JS v4.x instance 91 | 92 | Clone the repository and make the `public` directory accessible through your web server. Open the `public/_config.php` file and set the _upload_dir_ option to point to a directory the web server can write to. If you just want to test the upload process and you don't really want to save the uploaded files, you can set the _fake_ option to true and no files will be written to the disk. 93 | 94 | The example `index.html` expects to find the Ext JS instance in the `public/extjs` directory. You can create a link to the instance or copy it there. 95 | 96 | 97 | 98 | ## Documentation 99 | 100 | - [API Docs](http://debug.cz/demo/upload/docs/generated/) 101 | 102 | ## Other links 103 | 104 | - [More info in the blogpost](http://blog.debug.cz/2012/05/file-upload-widget-for-extjs-4x.html) 105 | - [Sencha forums post](http://www.sencha.com/forum/showthread.php?205365-File-upload-widget-using-File-API-and-Ext.data.Connection) 106 | 107 | ## TODO 108 | 109 | - add more uploader implementations 110 | - add drag'n'drop support 111 | - improve documentation 112 | 113 | ## License 114 | 115 | - [BSD 3 Clause](http://debug.cz/license/bsd-3-clause) 116 | 117 | ## Author 118 | 119 | - [Ivan Novakov](http://novakov.cz/) 120 | 121 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | The documentation is generated with the [JSDuck tool](https://github.com/senchalabs/jsduck). 5 | From this (`doc/`) directory run: 6 | 7 | jsduck --config jsduck-config.json 8 | 9 | -------------------------------------------------------------------------------- /docs/jsduck-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "--title" : "Ext JS Upload Widget", 3 | "--output" : "./generated", 4 | "--seo" : true, 5 | "--external" : [ 6 | "Ext.Base", "Ext.util.Observable", "Ext.data.Connection", "Ext.window.Window", "Ext.util.MixedCollection", 7 | "Ext.panel.Panel", "Ext.panel.Panel", "Ext.form.field.File", "Ext.grid.Panel", "Ext.toolbar.Toolbar", 8 | "Ext.selection.CheckboxModel", "FileList", "File" 9 | ], 10 | "--" : [ 11 | "../lib" 12 | ] 13 | } -------------------------------------------------------------------------------- /lib/upload/BrowseButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A "browse" button for selecting multiple files for upload. 3 | * 4 | */ 5 | Ext.define('Ext.ux.upload.BrowseButton', { 6 | extend : 'Ext.form.field.File', 7 | 8 | buttonOnly : true, 9 | 10 | iconCls : 'ux-mu-icon-action-browse', 11 | buttonText : 'Browse...', 12 | 13 | initComponent : function() { 14 | 15 | Ext.apply(this, { 16 | buttonConfig : { 17 | iconCls : this.iconCls, 18 | text : this.buttonText 19 | } 20 | }); 21 | 22 | this.on('afterrender', function() { 23 | /* 24 | * Fixing the issue when adding an icon to the button - the text does not render properly. OBSOLETE - from 25 | * ExtJS v4.1 the internal implementation has changed, there is no button object anymore. 26 | */ 27 | /* 28 | if (this.iconCls) { 29 | // this.button.removeCls('x-btn-icon'); 30 | // var width = this.button.getWidth(); 31 | // this.setWidth(width); 32 | } 33 | */ 34 | 35 | // Allow picking multiple files at once. 36 | this.setMultipleInputAttribute(); 37 | 38 | }, this); 39 | 40 | this.on('change', function(field, value, options) { 41 | var files = this.fileInputEl.dom.files; 42 | if (files.length) { 43 | this.fireEvent('fileselected', this, files); 44 | } 45 | }, this); 46 | 47 | this.callParent(arguments); 48 | }, 49 | 50 | reset : function() { 51 | this.callParent(arguments); 52 | this.setMultipleInputAttribute(); 53 | }, 54 | 55 | setMultipleInputAttribute : function(inputEl) { 56 | inputEl = inputEl || this.fileInputEl; 57 | inputEl.dom.setAttribute('multiple', '1'); 58 | } 59 | 60 | // OBSOLETE - the method is not used by the superclass anymore 61 | /* 62 | createFileInput : function() { 63 | this.callParent(arguments); 64 | this.fileInputEl.dom.setAttribute('multiple', '1'); 65 | } 66 | */ 67 | 68 | } 69 | ); 70 | -------------------------------------------------------------------------------- /lib/upload/Dialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The main upload dialog. 3 | * 4 | * Mostly, this may be the only object you need to interact with. Just initialize it and show it: 5 | * 6 | * @example 7 | * var dialog = Ext.create('Ext.ux.upload.Dialog', { 8 | * dialogTitle: 'My Upload Widget', 9 | * uploadUrl: 'upload.php' 10 | * }); 11 | * dialog.show(); 12 | * 13 | */ 14 | Ext.define('Ext.ux.upload.Dialog', { 15 | extend : 'Ext.window.Window', 16 | 17 | /** 18 | * @cfg {Number} [width=700] 19 | */ 20 | width : 700, 21 | 22 | /** 23 | * @cfg {Number} [height=500] 24 | */ 25 | height : 500, 26 | 27 | border : 0, 28 | 29 | config : { 30 | /** 31 | * @cfg {String} 32 | * 33 | * The title of the dialog. 34 | */ 35 | dialogTitle : '', 36 | 37 | /** 38 | * @cfg {boolean} [synchronous=false] 39 | * 40 | * If true, all files are uploaded in a sequence, otherwise files are uploaded simultaneously (asynchronously). 41 | */ 42 | synchronous : true, 43 | 44 | /** 45 | * @cfg {String} uploadUrl (required) 46 | * 47 | * The URL to upload files to. 48 | */ 49 | uploadUrl : '', 50 | 51 | /** 52 | * @cfg {Object} 53 | * 54 | * Params passed to the uploader object and sent along with the request. It depends on the implementation of the 55 | * uploader object, for example if the {@link Ext.ux.upload.uploader.ExtJsUploader} is used, the params are sent 56 | * as GET params. 57 | */ 58 | uploadParams : {}, 59 | 60 | /** 61 | * @cfg {Object} 62 | * 63 | * Extra HTTP headers to be added to the HTTP request uploading the file. 64 | */ 65 | uploadExtraHeaders : {}, 66 | 67 | /** 68 | * @cfg {Number} [uploadTimeout=6000] 69 | * 70 | * The time after the upload request times out - in miliseconds. 71 | */ 72 | uploadTimeout : 60000, 73 | 74 | // strings 75 | textClose : 'Close' 76 | }, 77 | 78 | /** 79 | * @private 80 | */ 81 | initComponent : function() { 82 | 83 | if (!Ext.isObject(this.panel)) { 84 | this.panel = Ext.create('Ext.ux.upload.Panel', { 85 | synchronous : this.synchronous, 86 | scope: this.scope, 87 | uploadUrl : this.uploadUrl, 88 | uploadParams : this.uploadParams, 89 | uploadExtraHeaders : this.uploadExtraHeaders, 90 | uploadTimeout : this.uploadTimeout 91 | }); 92 | } 93 | 94 | this.relayEvents(this.panel, [ 95 | 'uploadcomplete' 96 | ]); 97 | 98 | Ext.apply(this, { 99 | title : this.dialogTitle, 100 | layout : 'fit', 101 | items : [ 102 | this.panel 103 | ], 104 | dockedItems : [ 105 | { 106 | xtype : 'toolbar', 107 | dock : 'bottom', 108 | ui : 'footer', 109 | defaults : { 110 | minWidth : this.minButtonWidth 111 | }, 112 | items : [ 113 | '->', 114 | { 115 | text : this.textClose, 116 | cls : 'x-btn-text-icon', 117 | scope : this, 118 | handler : function() { 119 | this.close(); 120 | } 121 | } 122 | ] 123 | } 124 | ] 125 | }); 126 | 127 | this.callParent(arguments); 128 | } 129 | 130 | }); -------------------------------------------------------------------------------- /lib/upload/Item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A single item designated for upload. 3 | * 4 | * It is a simple object wrapping the native file API object. 5 | */ 6 | Ext.define('Ext.ux.upload.Item', { 7 | mixins : { 8 | observable : 'Ext.util.Observable' 9 | }, 10 | 11 | STATUS_READY : 'ready', 12 | STATUS_UPLOADING : 'uploading', 13 | STATUS_UPLOADED : 'uploaded', 14 | STATUS_UPLOAD_ERROR : 'uploaderror', 15 | 16 | progress : null, 17 | status : null, 18 | 19 | /** 20 | * @cfg {Object} fileApiObject (required) 21 | * 22 | * A native file API object 23 | */ 24 | fileApiObject : null, 25 | 26 | /** 27 | * @cfg {String} 28 | * 29 | * The upload error message associated with this file object 30 | */ 31 | uploadErrorMessage : '', 32 | 33 | constructor : function(config) { 34 | this.mixins.observable.constructor.call(this); 35 | 36 | this.initConfig(config); 37 | 38 | Ext.apply(this, { 39 | status : this.STATUS_READY, 40 | progress : 0 41 | }); 42 | }, 43 | 44 | reset : function() { 45 | this.uploadErrorMessage = ''; 46 | this.setStatus(this.STATUS_READY); 47 | this.setProgress(0); 48 | }, 49 | 50 | getFileApiObject : function() { 51 | return this.fileApiObject; 52 | }, 53 | 54 | getId : function() { 55 | return this.getFilename(); 56 | }, 57 | 58 | getName : function() { 59 | return this.getProperty('name'); 60 | }, 61 | 62 | getFilename : function() { 63 | return this.getName(); 64 | }, 65 | 66 | getSize : function() { 67 | return this.getProperty('size'); 68 | }, 69 | 70 | getType : function() { 71 | return this.getProperty('type'); 72 | }, 73 | 74 | getProperty : function(propertyName) { 75 | if (this.fileApiObject) { 76 | return this.fileApiObject[propertyName]; 77 | } 78 | 79 | return null; 80 | }, 81 | 82 | getProgress : function() { 83 | return this.progress; 84 | }, 85 | 86 | getProgressPercent : function() { 87 | var progress = this.getProgress(); 88 | if (!progress) { 89 | return 0; 90 | } 91 | 92 | var percent = Ext.util.Format.number((progress / this.getSize()) * 100, '0'); 93 | if (percent > 100) { 94 | percent = 100; 95 | } 96 | 97 | return percent; 98 | }, 99 | 100 | setProgress : function(progress) { 101 | this.progress = progress; 102 | this.fireEvent('progressupdate', this); 103 | }, 104 | 105 | getStatus : function() { 106 | return this.status; 107 | }, 108 | 109 | setStatus : function(status) { 110 | this.status = status; 111 | this.fireEvent('changestatus', this, status); 112 | }, 113 | 114 | hasStatus : function(status) { 115 | var itemStatus = this.getStatus(); 116 | 117 | if (Ext.isArray(status) && Ext.Array.contains(status, itemStatus)) { 118 | return true; 119 | } 120 | 121 | if (itemStatus === status) { 122 | return true; 123 | } 124 | 125 | return false; 126 | }, 127 | 128 | isReady : function() { 129 | return (this.status == this.STATUS_READY); 130 | }, 131 | 132 | isUploaded : function() { 133 | return (this.status == this.STATUS_UPLOADED); 134 | }, 135 | 136 | setUploaded : function() { 137 | this.setProgress(this.getSize()); 138 | this.setStatus(this.STATUS_UPLOADED); 139 | }, 140 | 141 | isUploadError : function() { 142 | return (this.status == this.STATUS_UPLOAD_ERROR); 143 | }, 144 | 145 | getUploadErrorMessage : function() { 146 | return this.uploadErrorMessage; 147 | }, 148 | 149 | setUploadError : function(message) { 150 | this.uploadErrorMessage = message; 151 | this.setStatus(this.STATUS_UPLOAD_ERROR); 152 | }, 153 | 154 | setUploading : function() { 155 | this.setStatus(this.STATUS_UPLOADING); 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /lib/upload/ItemGridPanel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The grid displaying the list of uploaded files (queue). 3 | * 4 | * @class Ext.ux.upload.ItemGridPanel 5 | * @extends Ext.grid.Panel 6 | */ 7 | Ext.define('Ext.ux.upload.ItemGridPanel', { 8 | extend : 'Ext.grid.Panel', 9 | 10 | requires : [ 11 | 'Ext.selection.CheckboxModel', 'Ext.ux.upload.Store' 12 | ], 13 | 14 | layout : 'fit', 15 | border : 0, 16 | 17 | viewConfig : { 18 | scrollOffset : 40 19 | }, 20 | 21 | config : { 22 | queue : null, 23 | 24 | textFilename : 'Filename', 25 | textSize : 'Size', 26 | textType : 'Type', 27 | textStatus : 'Status', 28 | textProgress : '%' 29 | }, 30 | 31 | initComponent : function() { 32 | 33 | if (this.queue) { 34 | this.queue.on('queuechange', this.onQueueChange, this); 35 | this.queue.on('itemchangestatus', this.onQueueItemChangeStatus, this); 36 | this.queue.on('itemprogressupdate', this.onQueueItemProgressUpdate, this); 37 | } 38 | 39 | Ext.apply(this, { 40 | store : Ext.create('Ext.ux.upload.Store'), 41 | selModel : Ext.create('Ext.selection.CheckboxModel', { 42 | checkOnly : true 43 | }), 44 | columns : [ 45 | { 46 | xtype : 'rownumberer', 47 | width : 50 48 | }, { 49 | dataIndex : 'filename', 50 | header : this.textFilename, 51 | flex : 1 52 | }, { 53 | dataIndex : 'size', 54 | header : this.textSize, 55 | width : 100, 56 | renderer : function(value) { 57 | return Ext.util.Format.fileSize(value); 58 | } 59 | }, { 60 | dataIndex : 'type', 61 | header : this.textType, 62 | width : 150 63 | }, { 64 | dataIndex : 'status', 65 | header : this.textStatus, 66 | width : 50, 67 | align : 'right', 68 | renderer : this.statusRenderer 69 | }, { 70 | dataIndex : 'progress', 71 | header : this.textProgress, 72 | width : 50, 73 | align : 'right', 74 | renderer : function(value) { 75 | if (!value) { 76 | value = 0; 77 | } 78 | return value + '%'; 79 | } 80 | }, { 81 | dataIndex : 'message', 82 | width : 1, 83 | hidden : true 84 | } 85 | ] 86 | }); 87 | 88 | this.callParent(arguments); 89 | }, 90 | 91 | onQueueChange : function(queue) { 92 | this.loadQueueItems(queue.getItems()); 93 | }, 94 | 95 | onQueueItemChangeStatus : function(queue, item, status) { 96 | this.updateStatus(item); 97 | }, 98 | 99 | onQueueItemProgressUpdate : function(queue, item) { 100 | this.updateStatus(item); 101 | }, 102 | 103 | /** 104 | * Loads the internal store with the supplied queue items. 105 | * 106 | * @param {Array} items 107 | */ 108 | loadQueueItems : function(items) { 109 | var data = []; 110 | var i; 111 | 112 | for (i = 0; i < items.length; i++) { 113 | data.push([ 114 | items[i].getFilename(), 115 | items[i].getSize(), 116 | items[i].getType(), 117 | items[i].getStatus(), 118 | items[i].getProgressPercent() 119 | ]); 120 | } 121 | 122 | this.loadStoreData(data); 123 | }, 124 | 125 | loadStoreData : function(data, append) { 126 | this.store.loadData(data, append); 127 | }, 128 | 129 | getSelectedRecords : function() { 130 | return this.getSelectionModel().getSelection(); 131 | }, 132 | 133 | updateStatus : function(item) { 134 | var record = this.getRecordByFilename(item.getFilename()); 135 | if (!record) { 136 | return; 137 | } 138 | 139 | var itemStatus = item.getStatus(); 140 | // debug.log('[' + item.getStatus() + '] [' + record.get('status') + ']'); 141 | if (itemStatus != record.get('status')) { 142 | this.scrollIntoView(record); 143 | 144 | record.set('status', item.getStatus()); 145 | if (item.isUploadError()) { 146 | record.set('tooltip', item.getUploadErrorMessage()); 147 | } 148 | } 149 | 150 | record.set('progress', item.getProgressPercent()); 151 | record.commit(); 152 | }, 153 | 154 | getRecordByFilename : function(filename) { 155 | var index = this.store.findExact('filename', filename); 156 | if (-1 == index) { 157 | return null; 158 | } 159 | 160 | return this.store.getAt(index); 161 | }, 162 | 163 | getIndexByRecord : function(record) { 164 | return this.store.findExact('filename', record.get('filename')); 165 | }, 166 | 167 | statusRenderer : function(value, metaData, record, rowIndex, colIndex, store) { 168 | var iconCls = 'ux-mu-icon-upload-' + value; 169 | var tooltip = record.get('tooltip'); 170 | if (tooltip) { 171 | value = tooltip; 172 | } else { 173 | 'upload_status_' + value; 174 | } 175 | value = ''; 176 | return value; 177 | }, 178 | 179 | scrollIntoView : function(record) { 180 | 181 | var index = this.getIndexByRecord(record); 182 | if (-1 == index) { 183 | return; 184 | } 185 | 186 | this.getView().focusRow(index); 187 | return; 188 | var rowEl = Ext.get(this.getView().getRow(index)); 189 | // var rowEl = this.getView().getRow(index); 190 | if (!rowEl) { 191 | return; 192 | } 193 | 194 | var gridEl = this.getEl(); 195 | 196 | // debug.log(rowEl.dom); 197 | // debug.log(gridEl.getBottom()); 198 | 199 | if (rowEl.getBottom() > gridEl.getBottom()) { 200 | rowEl.dom.scrollIntoView(gridEl); 201 | } 202 | } 203 | }); -------------------------------------------------------------------------------- /lib/upload/LegacyDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The main upload dialog. 3 | * 4 | * Mostly, this will be the only object you need to interact with. Just initialize it and show it: 5 | * 6 | * @example 7 | * var dialog = Ext.create('Ext.ux.upload.Dialog', { 8 | * dialogTitle: 'My Upload Widget', 9 | * uploadUrl: 'upload.php' 10 | * }); 11 | * 12 | * dialog.show(); 13 | * 14 | */ 15 | Ext.define('Ext.ux.upload.Dialog', { 16 | extend : 'Ext.window.Window', 17 | 18 | requires : [ 19 | 'Ext.ux.upload.ItemGridPanel', 20 | 'Ext.ux.upload.Manager', 21 | 'Ext.ux.upload.StatusBar', 22 | 'Ext.ux.upload.BrowseButton', 23 | 'Ext.ux.upload.Queue' 24 | ], 25 | 26 | /** 27 | * @cfg {Number} [width=700] 28 | */ 29 | width : 700, 30 | 31 | /** 32 | * @cfg {Number} [height=500] 33 | */ 34 | height : 500, 35 | 36 | config : { 37 | /** 38 | * @cfg {String} 39 | * 40 | * The title of the dialog. 41 | */ 42 | dialogTitle : '', 43 | 44 | /** 45 | * @cfg {boolean} [synchronous=false] 46 | * 47 | * If true, all files are uploaded in a sequence, otherwise files are uploaded simultaneously (asynchronously). 48 | */ 49 | synchronous : true, 50 | 51 | /** 52 | * @cfg {String} uploadUrl (required) 53 | * 54 | * The URL to upload files to. 55 | */ 56 | uploadUrl : '', 57 | 58 | /** 59 | * @cfg {Object} 60 | * 61 | * Params passed to the uploader object and sent along with the request. It depends on the implementation of the 62 | * uploader object, for example if the {@link Ext.ux.upload.uploader.ExtJsUploader} is used, the params are sent 63 | * as GET params. 64 | */ 65 | uploadParams : {}, 66 | 67 | /** 68 | * @cfg {Object} 69 | * 70 | * Extra HTTP headers to be added to the HTTP request uploading the file. 71 | */ 72 | uploadExtraHeaders : {}, 73 | 74 | /** 75 | * @cfg {Number} [uploadTimeout=6000] 76 | * 77 | * The time after the upload request times out - in miliseconds. 78 | */ 79 | uploadTimeout : 60000, 80 | 81 | // dialog strings 82 | textOk : 'OK', 83 | textClose : 'Close', 84 | textUpload : 'Upload', 85 | textBrowse : 'Browse', 86 | textAbort : 'Abort', 87 | textRemoveSelected : 'Remove selected', 88 | textRemoveAll : 'Remove all', 89 | 90 | // grid strings 91 | textFilename : 'Filename', 92 | textSize : 'Size', 93 | textType : 'Type', 94 | textStatus : 'Status', 95 | textProgress : '%', 96 | 97 | // status toolbar strings 98 | selectionMessageText : 'Selected {0} file(s), {1}', 99 | uploadMessageText : 'Upload progress {0}% ({1} of {2} souborů)', 100 | 101 | // browse button 102 | buttonText : 'Browse...' 103 | }, 104 | 105 | /** 106 | * @property {Ext.ux.upload.Queue} 107 | */ 108 | queue : null, 109 | 110 | /** 111 | * @property {Ext.ux.upload.ItemGridPanel} 112 | */ 113 | grid : null, 114 | 115 | /** 116 | * @property {Ext.ux.upload.Manager} 117 | */ 118 | uploadManager : null, 119 | 120 | /** 121 | * @property {Ext.ux.upload.StatusBar} 122 | */ 123 | statusBar : null, 124 | 125 | /** 126 | * @property {Ext.ux.upload.BrowseButton} 127 | */ 128 | browseButton : null, 129 | 130 | /** 131 | * @private 132 | */ 133 | initComponent : function() { 134 | 135 | this.queue = this.initQueue(); 136 | 137 | this.grid = Ext.create('Ext.ux.upload.ItemGridPanel', { 138 | queue : this.queue, 139 | textFilename : this.textFilename, 140 | textSize : this.textSize, 141 | textType : this.textType, 142 | textStatus : this.textStatus, 143 | textProgress : this.textProgress 144 | }); 145 | 146 | this.uploadManager = Ext.create('Ext.ux.upload.Manager', { 147 | url : this.uploadUrl, 148 | synchronous : this.synchronous, 149 | params : this.uploadParams, 150 | extraHeaders : this.uploadExtraHeaders, 151 | uploadTimeout : this.uploadTimeout 152 | }); 153 | 154 | this.uploadManager.on('uploadcomplete', this.onUploadComplete, this); 155 | this.uploadManager.on('itemuploadsuccess', this.onItemUploadSuccess, this); 156 | this.uploadManager.on('itemuploadfailure', this.onItemUploadFailure, this); 157 | 158 | this.statusBar = Ext.create('Ext.ux.upload.StatusBar', { 159 | dock : 'bottom', 160 | selectionMessageText : this.selectionMessageText, 161 | uploadMessageText : this.uploadMessageText 162 | }); 163 | 164 | Ext.apply(this, { 165 | title : this.dialogTitle, 166 | autoScroll : true, 167 | layout : 'fit', 168 | uploading : false, 169 | items : [ 170 | this.grid 171 | ], 172 | dockedItems : [ 173 | this.getTopToolbarConfig(), 174 | { 175 | xtype : 'toolbar', 176 | dock : 'bottom', 177 | ui : 'footer', 178 | defaults : { 179 | minWidth : this.minButtonWidth 180 | }, 181 | items : [ 182 | '->', 183 | { 184 | text : this.textClose, 185 | // iconCls : 'ux-mu-icon-action-ok', 186 | cls : 'x-btn-text-icon', 187 | scope : this, 188 | handler : function() { 189 | this.close(); 190 | } 191 | } 192 | ] 193 | }, 194 | this.statusBar 195 | ] 196 | }); 197 | 198 | this.on('afterrender', function() { 199 | this.stateInit(); 200 | }, this); 201 | 202 | this.callParent(arguments); 203 | }, 204 | 205 | /** 206 | * @private 207 | * 208 | * Returns the config object for the top toolbar. 209 | * 210 | * @return {Array} 211 | */ 212 | getTopToolbarConfig : function() { 213 | 214 | this.browseButton = Ext.create('Ext.ux.upload.BrowseButton', { 215 | id : 'button_browse', 216 | buttonText : this.buttonText 217 | }); 218 | this.browseButton.on('fileselected', this.onFileSelection, this); 219 | 220 | return { 221 | xtype : 'toolbar', 222 | dock : 'top', 223 | items : [ 224 | this.browseButton, 225 | '-', 226 | { 227 | id : 'button_upload', 228 | text : this.textUpload, 229 | iconCls : 'ux-mu-icon-action-upload', 230 | scope : this, 231 | handler : this.onInitUpload 232 | }, 233 | '-', 234 | { 235 | id : 'button_abort', 236 | text : this.textAbort, 237 | iconCls : 'ux-mu-icon-action-abort', 238 | scope : this, 239 | handler : this.onAbortUpload, 240 | disabled : true 241 | }, 242 | '->', 243 | { 244 | id : 'button_remove_selected', 245 | text : this.textRemoveSelected, 246 | iconCls : 'ux-mu-icon-action-remove', 247 | scope : this, 248 | handler : this.onMultipleRemove 249 | }, 250 | '-', 251 | { 252 | id : 'button_remove_all', 253 | text : this.textRemoveAll, 254 | iconCls : 'ux-mu-icon-action-remove', 255 | scope : this, 256 | handler : this.onRemoveAll 257 | } 258 | ] 259 | } 260 | }, 261 | 262 | /** 263 | * @private 264 | * 265 | * Initializes and returns the queue object. 266 | * 267 | * @return {Ext.ux.upload.Queue} 268 | */ 269 | initQueue : function() { 270 | var queue = Ext.create('Ext.ux.upload.Queue'); 271 | 272 | queue.on('queuechange', this.onQueueChange, this); 273 | 274 | return queue; 275 | }, 276 | 277 | onInitUpload : function() { 278 | if (!this.queue.getCount()) { 279 | return; 280 | } 281 | 282 | this.stateUpload(); 283 | this.startUpload(); 284 | }, 285 | 286 | onAbortUpload : function() { 287 | this.uploadManager.abortUpload(); 288 | this.finishUpload(); 289 | this.switchState(); 290 | }, 291 | 292 | onUploadComplete : function(manager, queue, errorCount) { 293 | this.finishUpload(); 294 | this.stateInit(); 295 | this.fireEvent('uploadcomplete', this, manager, queue.getUploadedItems(), errorCount); 296 | }, 297 | 298 | /** 299 | * @private 300 | * 301 | * Executes after files has been selected for upload through the "Browse" button. Updates the upload queue with the 302 | * new files. 303 | * 304 | * @param {Ext.ux.upload.BrowseButton} input 305 | * @param {FileList} files 306 | */ 307 | onFileSelection : function(input, files) { 308 | this.queue.clearUploadedItems(); 309 | this.queue.addFiles(files); 310 | this.browseButton.reset(); 311 | }, 312 | 313 | /** 314 | * @private 315 | * 316 | * Executes if there is a change in the queue. Updates the related components (grid, toolbar). 317 | * 318 | * @param {Ext.ux.upload.Queue} queue 319 | */ 320 | onQueueChange : function(queue) { 321 | this.updateStatusBar(); 322 | 323 | this.switchState(); 324 | }, 325 | 326 | /** 327 | * @private 328 | * 329 | * Executes upon hitting the "multiple remove" button. Removes all selected items from the queue. 330 | */ 331 | onMultipleRemove : function() { 332 | var records = this.grid.getSelectedRecords(); 333 | if (!records.length) { 334 | return; 335 | } 336 | 337 | var keys = []; 338 | var i; 339 | var num = records.length; 340 | 341 | for (i = 0; i < num; i++) { 342 | keys.push(records[i].get('filename')); 343 | } 344 | 345 | this.queue.removeItemsByKey(keys); 346 | }, 347 | 348 | onRemoveAll : function() { 349 | this.queue.clearItems(); 350 | }, 351 | 352 | onItemUploadSuccess : function(item, info) { 353 | 354 | }, 355 | 356 | onItemUploadFailure : function(item, info) { 357 | 358 | }, 359 | 360 | startUpload : function() { 361 | this.uploading = true; 362 | this.uploadManager.uploadQueue(this.queue); 363 | }, 364 | 365 | finishUpload : function() { 366 | this.uploading = false; 367 | }, 368 | 369 | isUploadActive : function() { 370 | return this.uploading; 371 | }, 372 | 373 | updateStatusBar : function() { 374 | if (!this.statusBar) { 375 | return; 376 | } 377 | 378 | var numFiles = this.queue.getCount(); 379 | 380 | this.statusBar.setSelectionMessage(this.queue.getCount(), this.queue.getTotalBytes()); 381 | }, 382 | 383 | getButton : function(id) { 384 | return Ext.ComponentMgr.get(id); 385 | }, 386 | 387 | switchButtons : function(info) { 388 | var id; 389 | for (id in info) { 390 | this.switchButton(id, info[id]); 391 | } 392 | }, 393 | 394 | switchButton : function(id, on) { 395 | var button = this.getButton(id); 396 | 397 | if (button) { 398 | if (on) { 399 | button.enable(); 400 | } else { 401 | button.disable(); 402 | } 403 | } 404 | }, 405 | 406 | switchState : function() { 407 | if (this.uploading) { 408 | this.stateUpload(); 409 | } else if (this.queue.getCount()) { 410 | this.stateQueue(); 411 | } else { 412 | this.stateInit(); 413 | } 414 | }, 415 | 416 | stateInit : function() { 417 | this.switchButtons({ 418 | 'button_browse' : 1, 419 | 'button_upload' : 0, 420 | 'button_abort' : 0, 421 | 'button_remove_all' : 1, 422 | 'button_remove_selected' : 1 423 | }); 424 | }, 425 | 426 | stateQueue : function() { 427 | this.switchButtons({ 428 | 'button_browse' : 1, 429 | 'button_upload' : 1, 430 | 'button_abort' : 0, 431 | 'button_remove_all' : 1, 432 | 'button_remove_selected' : 1 433 | }); 434 | }, 435 | 436 | stateUpload : function() { 437 | this.switchButtons({ 438 | 'button_browse' : 0, 439 | 'button_upload' : 0, 440 | 'button_abort' : 1, 441 | 'button_remove_all' : 1, 442 | 'button_remove_selected' : 1 443 | }); 444 | } 445 | 446 | }); -------------------------------------------------------------------------------- /lib/upload/Manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The object is responsible for uploading the queue. 3 | * 4 | */ 5 | Ext.define('Ext.ux.upload.Manager', { 6 | mixins : { 7 | observable : 'Ext.util.Observable' 8 | }, 9 | 10 | requires : [ 11 | 'Ext.ux.upload.uploader.AbstractUploader' 12 | ], 13 | 14 | uploader : null, 15 | uploaderOptions : null, 16 | synchronous : true, 17 | filenameEncoder : null, 18 | 19 | DEFAULT_UPLOADER_CLASS : 'Ext.ux.upload.uploader.ExtJsUploader', 20 | 21 | constructor : function(config) { 22 | this.mixins.observable.constructor.call(this); 23 | 24 | this.initConfig(config); 25 | 26 | if (!(this.uploader instanceof Ext.ux.upload.uploader.AbstractUploader)) { 27 | var uploaderClass = this.DEFAULT_UPLOADER_CLASS; 28 | if (Ext.isString(this.uploader)) { 29 | uploaderClass = this.uploader; 30 | } 31 | 32 | var uploaderOptions = this.uploaderOptions || {}; 33 | Ext.applyIf(uploaderOptions, { 34 | success : this.onUploadSuccess, 35 | failure : this.onUploadFailure, 36 | progress : this.onUploadProgress, 37 | filenameEncoder : this.filenameEncoder 38 | }); 39 | 40 | this.uploader = Ext.create(uploaderClass, uploaderOptions); 41 | } 42 | 43 | this.mon(this.uploader, 'uploadsuccess', this.onUploadSuccess, this); 44 | this.mon(this.uploader, 'uploadfailure', this.onUploadFailure, this); 45 | this.mon(this.uploader, 'uploadprogress', this.onUploadProgress, this); 46 | 47 | Ext.apply(this, { 48 | syncQueue : null, 49 | currentQueue : null, 50 | uploadActive : false, 51 | errorCount : 0 52 | }); 53 | }, 54 | 55 | uploadQueue : function(queue) { 56 | if (this.uploadActive) { 57 | return; 58 | } 59 | 60 | this.startUpload(queue); 61 | 62 | if (this.synchronous) { 63 | this.uploadQueueSync(queue); 64 | return; 65 | } 66 | 67 | this.uploadQueueAsync(queue); 68 | 69 | }, 70 | 71 | uploadQueueSync : function(queue) { 72 | this.uploadNextItemSync(); 73 | }, 74 | 75 | uploadNextItemSync : function() { 76 | if (!this.uploadActive) { 77 | return; 78 | } 79 | 80 | var item = this.currentQueue.getFirstReadyItem(); 81 | if (!item) { 82 | return; 83 | } 84 | 85 | this.uploader.uploadItem(item); 86 | }, 87 | 88 | uploadQueueAsync : function(queue) { 89 | var i; 90 | var num = queue.getCount(); 91 | 92 | for (i = 0; i < num; i++) { 93 | this.uploader.uploadItem(queue.getAt(i)); 94 | } 95 | }, 96 | 97 | startUpload : function(queue) { 98 | queue.reset(); 99 | 100 | this.uploadActive = true; 101 | this.currentQueue = queue; 102 | this.fireEvent('beforeupload', this, queue); 103 | }, 104 | 105 | finishUpload : function() { 106 | this.fireEvent('uploadcomplete', this, this.currentQueue, this.errorCount); 107 | }, 108 | 109 | resetUpload : function() { 110 | this.currentQueue = null; 111 | this.uploadActive = false; 112 | this.errorCount = 0; 113 | }, 114 | 115 | abortUpload : function() { 116 | this.uploader.abortUpload(); 117 | this.currentQueue.recoverAfterAbort(); 118 | this.resetUpload(); 119 | 120 | this.fireEvent('abortupload', this, this.currentQueue); 121 | }, 122 | 123 | afterItemUpload : function(item, info) { 124 | if (this.synchronous) { 125 | this.uploadNextItemSync(); 126 | } 127 | 128 | if (!this.currentQueue.existUploadingItems()) { 129 | this.finishUpload(); 130 | } 131 | }, 132 | 133 | onUploadSuccess : function(item, info) { 134 | item.setUploaded(); 135 | 136 | this.fireEvent('itemuploadsuccess', this, item, info); 137 | 138 | this.afterItemUpload(item, info); 139 | }, 140 | 141 | onUploadFailure : function(item, info) { 142 | item.setUploadError(info.message); 143 | 144 | this.fireEvent('itemuploadfailure', this, item, info); 145 | this.errorCount++; 146 | 147 | this.afterItemUpload(item, info); 148 | }, 149 | 150 | onUploadProgress : function(item, event) { 151 | item.setProgress(event.loaded); 152 | } 153 | }); 154 | -------------------------------------------------------------------------------- /lib/upload/Panel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The main upload panel, which ties all the functionality together. 3 | * 4 | * In the most basic case you need just to set the upload URL: 5 | * 6 | * @example 7 | * var uploadPanel = Ext.create('Ext.ux.upload.Panel', { 8 | * uploaderOptions: { 9 | * url: '/api/upload' 10 | * } 11 | * }); 12 | * 13 | * It uses the default ExtJsUploader to perform the actual upload. If you want to use another uploade, for 14 | * example the FormDataUploader, you can pass the name of the class: 15 | * 16 | * @example 17 | * var uploadPanel = Ext.create('Ext.ux.upload.Panel', { 18 | * uploader: 'Ext.ux.upload.uploader.FormDataUploader', 19 | * uploaderOptions: { 20 | * url: '/api/upload', 21 | * timeout: 120*1000 22 | * } 23 | * }); 24 | * 25 | * Or event an instance of the uploader: 26 | * 27 | * @example 28 | * var formDataUploader = Ext.create('Ext.ux.upload.uploader.FormDataUploader', { 29 | * url: '/api/upload' 30 | * }); 31 | * 32 | * var uploadPanel = Ext.create('Ext.ux.upload.Panel', { 33 | * uploader: formDataUploader 34 | * }); 35 | * 36 | */ 37 | Ext.define('Ext.ux.upload.Panel', { 38 | extend : 'Ext.panel.Panel', 39 | 40 | requires : [ 41 | 'Ext.ux.upload.ItemGridPanel', 42 | 'Ext.ux.upload.Manager', 43 | 'Ext.ux.upload.StatusBar', 44 | 'Ext.ux.upload.BrowseButton', 45 | 'Ext.ux.upload.Queue' 46 | ], 47 | 48 | config : { 49 | 50 | /** 51 | * @cfg {Object/String} 52 | * 53 | * The name of the uploader class or the uploader object itself. If not set, the default uploader will 54 | * be used. 55 | */ 56 | uploader : null, 57 | 58 | /** 59 | * @cfg {Object} 60 | * 61 | * Configuration object for the uploader. Configuration options included in this object override the 62 | * options 'uploadUrl', 'uploadParams', 'uploadExtraHeaders', 'uploadTimeout'. 63 | */ 64 | uploaderOptions : null, 65 | 66 | /** 67 | * @cfg {boolean} [synchronous=false] 68 | * 69 | * If true, all files are uploaded in a sequence, otherwise files are uploaded simultaneously (asynchronously). 70 | */ 71 | synchronous : true, 72 | 73 | /** 74 | * @cfg {String} uploadUrl 75 | * 76 | * The URL to upload files to. Not required if configured uploader instance is passed to this panel. 77 | */ 78 | uploadUrl : '', 79 | 80 | /** 81 | * @cfg {Object} 82 | * 83 | * Params passed to the uploader object and sent along with the request. It depends on the implementation of the 84 | * uploader object, for example if the {@link Ext.ux.upload.uploader.ExtJsUploader} is used, the params are sent 85 | * as GET params. 86 | */ 87 | uploadParams : {}, 88 | 89 | /** 90 | * @cfg {Object} 91 | * 92 | * Extra HTTP headers to be added to the HTTP request uploading the file. 93 | */ 94 | uploadExtraHeaders : {}, 95 | 96 | /** 97 | * @cfg {Number} [uploadTimeout=6000] 98 | * 99 | * The time after the upload request times out - in miliseconds. 100 | */ 101 | uploadTimeout : 60000, 102 | 103 | /** 104 | * @cfg {Object/String} 105 | * 106 | * Encoder object/class used to encode the filename header. Usually used, when the filename 107 | * contains non-ASCII characters. If an encoder is used, the server backend has to be 108 | * modified accordingly to decode the value. 109 | */ 110 | filenameEncoder : null, 111 | 112 | // strings 113 | textOk : 'OK', 114 | textUpload : 'Upload', 115 | textBrowse : 'Browse', 116 | textAbort : 'Abort', 117 | textRemoveSelected : 'Remove selected', 118 | textRemoveAll : 'Remove all', 119 | 120 | // grid strings 121 | textFilename : 'Filename', 122 | textSize : 'Size', 123 | textType : 'Type', 124 | textStatus : 'Status', 125 | textProgress : '%', 126 | 127 | // status toolbar strings 128 | selectionMessageText : 'Selected {0} file(s), {1}', 129 | uploadMessageText : 'Upload progress {0}% ({1} of {2} souborů)', 130 | 131 | // browse button 132 | buttonText : 'Browse...' 133 | }, 134 | 135 | /** 136 | * @property {Ext.ux.upload.Queue} 137 | * @private 138 | */ 139 | queue : null, 140 | 141 | /** 142 | * @property {Ext.ux.upload.ItemGridPanel} 143 | * @private 144 | */ 145 | grid : null, 146 | 147 | /** 148 | * @property {Ext.ux.upload.Manager} 149 | * @private 150 | */ 151 | uploadManager : null, 152 | 153 | /** 154 | * @property {Ext.ux.upload.StatusBar} 155 | * @private 156 | */ 157 | statusBar : null, 158 | 159 | /** 160 | * @property {Ext.ux.upload.BrowseButton} 161 | * @private 162 | */ 163 | browseButton : null, 164 | 165 | /** 166 | * @private 167 | */ 168 | initComponent : function() { 169 | 170 | this.queue = this.initQueue(); 171 | 172 | this.grid = Ext.create('Ext.ux.upload.ItemGridPanel', { 173 | queue : this.queue, 174 | textFilename : this.textFilename, 175 | textSize : this.textSize, 176 | textType : this.textType, 177 | textStatus : this.textStatus, 178 | textProgress : this.textProgress 179 | }); 180 | 181 | this.uploadManager = this.createUploadManager(); 182 | 183 | this.uploadManager.on('uploadcomplete', this.onUploadComplete, this); 184 | this.uploadManager.on('itemuploadsuccess', this.onItemUploadSuccess, this); 185 | this.uploadManager.on('itemuploadfailure', this.onItemUploadFailure, this); 186 | 187 | this.statusBar = Ext.create('Ext.ux.upload.StatusBar', { 188 | dock : 'bottom', 189 | selectionMessageText : this.selectionMessageText, 190 | uploadMessageText : this.uploadMessageText 191 | }); 192 | 193 | Ext.apply(this, { 194 | title : this.dialogTitle, 195 | autoScroll : true, 196 | layout : 'fit', 197 | uploading : false, 198 | items : [ 199 | this.grid 200 | ], 201 | dockedItems : [ 202 | this.getTopToolbarConfig(), this.statusBar 203 | ] 204 | }); 205 | 206 | this.on('afterrender', function() { 207 | this.stateInit(); 208 | }, this); 209 | 210 | this.callParent(arguments); 211 | }, 212 | 213 | createUploadManager : function() { 214 | var uploaderOptions = this.getUploaderOptions() || {}; 215 | 216 | Ext.applyIf(uploaderOptions, { 217 | url : this.uploadUrl, 218 | params : this.uploadParams, 219 | extraHeaders : this.uploadExtraHeaders, 220 | timeout : this.uploadTimeout 221 | }); 222 | 223 | var uploadManager = Ext.create('Ext.ux.upload.Manager', { 224 | uploader : this.uploader, 225 | uploaderOptions : uploaderOptions, 226 | synchronous : this.getSynchronous(), 227 | filenameEncoder : this.getFilenameEncoder() 228 | }); 229 | 230 | return uploadManager; 231 | }, 232 | 233 | /** 234 | * @private 235 | * 236 | * Returns the config object for the top toolbar. 237 | * 238 | * @return {Array} 239 | */ 240 | getTopToolbarConfig : function() { 241 | 242 | this.browseButton = Ext.create('Ext.ux.upload.BrowseButton', { 243 | itemId : 'button_browse', 244 | buttonText : this.buttonText 245 | }); 246 | this.browseButton.on('fileselected', this.onFileSelection, this); 247 | 248 | return { 249 | xtype : 'toolbar', 250 | itemId : 'topToolbar', 251 | dock : 'top', 252 | items : [ 253 | this.browseButton, 254 | '-', 255 | { 256 | itemId : 'button_upload', 257 | text : this.textUpload, 258 | iconCls : 'ux-mu-icon-action-upload', 259 | scope : this, 260 | handler : this.onInitUpload 261 | }, 262 | '-', 263 | { 264 | itemId : 'button_abort', 265 | text : this.textAbort, 266 | iconCls : 'ux-mu-icon-action-abort', 267 | scope : this, 268 | handler : this.onAbortUpload, 269 | disabled : true 270 | }, 271 | '->', 272 | { 273 | itemId : 'button_remove_selected', 274 | text : this.textRemoveSelected, 275 | iconCls : 'ux-mu-icon-action-remove', 276 | scope : this, 277 | handler : this.onMultipleRemove 278 | }, 279 | '-', 280 | { 281 | itemId : 'button_remove_all', 282 | text : this.textRemoveAll, 283 | iconCls : 'ux-mu-icon-action-remove', 284 | scope : this, 285 | handler : this.onRemoveAll 286 | } 287 | ] 288 | } 289 | }, 290 | 291 | /** 292 | * @private 293 | * 294 | * Initializes and returns the queue object. 295 | * 296 | * @return {Ext.ux.upload.Queue} 297 | */ 298 | initQueue : function() { 299 | var queue = Ext.create('Ext.ux.upload.Queue'); 300 | 301 | queue.on('queuechange', this.onQueueChange, this); 302 | 303 | return queue; 304 | }, 305 | 306 | onInitUpload : function() { 307 | if (!this.queue.getCount()) { 308 | return; 309 | } 310 | 311 | this.stateUpload(); 312 | this.startUpload(); 313 | }, 314 | 315 | onAbortUpload : function() { 316 | this.uploadManager.abortUpload(); 317 | this.finishUpload(); 318 | this.switchState(); 319 | }, 320 | 321 | onUploadComplete : function(manager, queue, errorCount) { 322 | this.finishUpload(); 323 | if (errorCount) { 324 | this.stateQueue(); 325 | } else { 326 | this.stateInit(); 327 | } 328 | this.fireEvent('uploadcomplete', this, manager, queue.getUploadedItems(), errorCount); 329 | manager.resetUpload(); 330 | }, 331 | 332 | /** 333 | * @private 334 | * 335 | * Executes after files has been selected for upload through the "Browse" button. Updates the upload queue with the 336 | * new files. 337 | * 338 | * @param {Ext.ux.upload.BrowseButton} input 339 | * @param {FileList} files 340 | */ 341 | onFileSelection : function(input, files) { 342 | this.queue.clearUploadedItems(); 343 | this.queue.addFiles(files); 344 | this.browseButton.reset(); 345 | }, 346 | 347 | /** 348 | * @private 349 | * 350 | * Executes if there is a change in the queue. Updates the related components (grid, toolbar). 351 | * 352 | * @param {Ext.ux.upload.Queue} queue 353 | */ 354 | onQueueChange : function(queue) { 355 | this.updateStatusBar(); 356 | 357 | this.switchState(); 358 | }, 359 | 360 | /** 361 | * @private 362 | * 363 | * Executes upon hitting the "multiple remove" button. Removes all selected items from the queue. 364 | */ 365 | onMultipleRemove : function() { 366 | var records = this.grid.getSelectedRecords(); 367 | if (!records.length) { 368 | return; 369 | } 370 | 371 | var keys = []; 372 | var i; 373 | var num = records.length; 374 | 375 | for (i = 0; i < num; i++) { 376 | keys.push(records[i].get('filename')); 377 | } 378 | 379 | this.queue.removeItemsByKey(keys); 380 | }, 381 | 382 | onRemoveAll : function() { 383 | this.queue.clearItems(); 384 | }, 385 | 386 | onItemUploadSuccess : function(manager, item, info) { 387 | 388 | }, 389 | 390 | onItemUploadFailure : function(manager, item, info) { 391 | 392 | }, 393 | 394 | startUpload : function() { 395 | this.uploading = true; 396 | this.uploadManager.uploadQueue(this.queue); 397 | }, 398 | 399 | finishUpload : function() { 400 | this.uploading = false; 401 | }, 402 | 403 | isUploadActive : function() { 404 | return this.uploading; 405 | }, 406 | 407 | updateStatusBar : function() { 408 | if (!this.statusBar) { 409 | return; 410 | } 411 | 412 | var numFiles = this.queue.getCount(); 413 | 414 | this.statusBar.setSelectionMessage(this.queue.getCount(), this.queue.getTotalBytes()); 415 | }, 416 | 417 | getButton : function(itemId) { 418 | var topToolbar = this.getDockedComponent('topToolbar'); 419 | if (topToolbar) { 420 | return topToolbar.getComponent(itemId); 421 | } 422 | return null; 423 | }, 424 | 425 | switchButtons : function(info) { 426 | var itemId; 427 | for (itemId in info) { 428 | this.switchButton(itemId, info[itemId]); 429 | } 430 | }, 431 | 432 | switchButton : function(itemId, on) { 433 | var button = this.getButton(itemId); 434 | 435 | if (button) { 436 | if (on) { 437 | button.enable(); 438 | } else { 439 | button.disable(); 440 | } 441 | } 442 | }, 443 | 444 | switchState : function() { 445 | if (this.uploading) { 446 | this.stateUpload(); 447 | } else if (this.queue.getCount()) { 448 | this.stateQueue(); 449 | } else { 450 | this.stateInit(); 451 | } 452 | }, 453 | 454 | stateInit : function() { 455 | this.switchButtons({ 456 | 'button_browse' : 1, 457 | 'button_upload' : 0, 458 | 'button_abort' : 0, 459 | 'button_remove_all' : 1, 460 | 'button_remove_selected' : 1 461 | }); 462 | }, 463 | 464 | stateQueue : function() { 465 | this.switchButtons({ 466 | 'button_browse' : 1, 467 | 'button_upload' : 1, 468 | 'button_abort' : 0, 469 | 'button_remove_all' : 1, 470 | 'button_remove_selected' : 1 471 | }); 472 | }, 473 | 474 | stateUpload : function() { 475 | this.switchButtons({ 476 | 'button_browse' : 0, 477 | 'button_upload' : 0, 478 | 'button_abort' : 1, 479 | 'button_remove_all' : 1, 480 | 'button_remove_selected' : 1 481 | }); 482 | } 483 | 484 | }); 485 | -------------------------------------------------------------------------------- /lib/upload/Queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Data structure managing the upload file queue. 3 | * 4 | */ 5 | Ext.define('Ext.ux.upload.Queue', { 6 | extend : 'Ext.util.MixedCollection', 7 | 8 | requires : [ 9 | 'Ext.ux.upload.Item' 10 | ], 11 | 12 | /** 13 | * Constructor. 14 | * 15 | * @param {Object} config 16 | */ 17 | constructor : function(config) { 18 | 19 | this.callParent(arguments); 20 | 21 | this.on('clear', function() { 22 | this.fireEvent('queuechange', this); 23 | }, this); 24 | 25 | }, 26 | 27 | /** 28 | * Adds files to the queue. 29 | * 30 | * @param {FileList} fileList 31 | */ 32 | addFiles : function(fileList) { 33 | var i; 34 | var items = []; 35 | var num = fileList.length; 36 | 37 | if (!num) { 38 | return; 39 | } 40 | 41 | for (i = 0; i < num; i++) { 42 | items.push(this.createItem(fileList[i])); 43 | } 44 | 45 | this.addAll(items); 46 | 47 | this.fireEvent('multiadd', this, items); 48 | this.fireEvent('queuechange', this); 49 | }, 50 | 51 | /** 52 | * Uploaded files are removed, the rest are set as ready. 53 | */ 54 | reset : function() { 55 | this.clearUploadedItems(); 56 | 57 | this.each(function(item) { 58 | item.reset(); 59 | }, this); 60 | }, 61 | 62 | /** 63 | * Returns all queued items. 64 | * 65 | * @return {Ext.ux.upload.Item[]} 66 | */ 67 | getItems : function() { 68 | return this.getRange(); 69 | }, 70 | 71 | /** 72 | * Returns an array of items by the specified status. 73 | * 74 | * @param {String/Array} 75 | * @return {Ext.ux.upload.Item[]} 76 | */ 77 | getItemsByStatus : function(status) { 78 | var itemsByStatus = []; 79 | 80 | this.each(function(item, index, items) { 81 | if (item.hasStatus(status)) { 82 | itemsByStatus.push(item); 83 | } 84 | }); 85 | 86 | return itemsByStatus; 87 | }, 88 | 89 | /** 90 | * Returns an array of items, that have already been uploaded. 91 | * 92 | * @return {Ext.ux.upload.Item[]} 93 | */ 94 | getUploadedItems : function() { 95 | return this.getItemsByStatus('uploaded'); 96 | }, 97 | 98 | /** 99 | * Returns an array of items, that have not been uploaded yet. 100 | * 101 | * @return {Ext.ux.upload.Item[]} 102 | */ 103 | getUploadingItems : function() { 104 | return this.getItemsByStatus([ 105 | 'ready', 'uploading' 106 | ]); 107 | }, 108 | 109 | /** 110 | * Returns true, if there are items, that are currently being uploaded. 111 | * 112 | * @return {Boolean} 113 | */ 114 | existUploadingItems : function() { 115 | return (this.getUploadingItems().length > 0); 116 | }, 117 | 118 | /** 119 | * Returns the first "ready" item in the queue (with status STATUS_READY). 120 | * 121 | * @return {Ext.ux.upload.Item/null} 122 | */ 123 | getFirstReadyItem : function() { 124 | var items = this.getRange(); 125 | var num = this.getCount(); 126 | var i; 127 | 128 | for (i = 0; i < num; i++) { 129 | if (items[i].isReady()) { 130 | return items[i]; 131 | } 132 | } 133 | 134 | return null; 135 | }, 136 | 137 | /** 138 | * Clears all items from the queue. 139 | */ 140 | clearItems : function() { 141 | this.clear(); 142 | }, 143 | 144 | /** 145 | * Removes the items, which have been already uploaded, from the queue. 146 | */ 147 | clearUploadedItems : function() { 148 | this.removeItems(this.getUploadedItems()); 149 | }, 150 | 151 | /** 152 | * Removes items from the queue. 153 | * 154 | * @param {Ext.ux.upload.Item[]} items 155 | */ 156 | removeItems : function(items) { 157 | var num = items.length; 158 | var i; 159 | 160 | if (!num) { 161 | return; 162 | } 163 | 164 | for (i = 0; i < num; i++) { 165 | this.remove(items[i]); 166 | } 167 | 168 | this.fireEvent('queuechange', this); 169 | }, 170 | 171 | /** 172 | * Removes the items identified by the supplied array of keys. 173 | * 174 | * @param {Array} itemKeys 175 | */ 176 | removeItemsByKey : function(itemKeys) { 177 | var i; 178 | var num = itemKeys.length; 179 | 180 | if (!num) { 181 | return; 182 | } 183 | 184 | for (i = 0; i < num; i++) { 185 | this.removeItemByKey(itemKeys[i]); 186 | } 187 | 188 | this.fireEvent('multiremove', this, itemKeys); 189 | this.fireEvent('queuechange', this); 190 | }, 191 | 192 | /** 193 | * Removes a single item by its key. 194 | * 195 | * @param {String} key 196 | */ 197 | removeItemByKey : function(key) { 198 | this.removeAtKey(key); 199 | }, 200 | 201 | /** 202 | * Perform cleanup, after the upload has been aborted. 203 | */ 204 | recoverAfterAbort : function() { 205 | this.each(function(item) { 206 | if (!item.isUploaded() && !item.isReady()) { 207 | item.reset(); 208 | } 209 | }); 210 | }, 211 | 212 | /** 213 | * @private 214 | * 215 | * Initialize and return a new queue item for the corresponding File object. 216 | * 217 | * @param {File} file 218 | * @return {Ext.ux.upload.Item} 219 | */ 220 | createItem : function(file) { 221 | 222 | var item = Ext.create('Ext.ux.upload.Item', { 223 | fileApiObject : file 224 | }); 225 | 226 | item.on('changestatus', this.onItemChangeStatus, this); 227 | item.on('progressupdate', this.onItemProgressUpdate, this); 228 | 229 | return item; 230 | }, 231 | 232 | /** 233 | * A getKey() implementation to determine the key of an item in the collection. 234 | * 235 | * @param {Ext.ux.upload.Item} item 236 | * @return {String} 237 | */ 238 | getKey : function(item) { 239 | return item.getId(); 240 | }, 241 | 242 | onItemChangeStatus : function(item, status) { 243 | this.fireEvent('itemchangestatus', this, item, status); 244 | }, 245 | 246 | onItemProgressUpdate : function(item) { 247 | this.fireEvent('itemprogressupdate', this, item); 248 | }, 249 | 250 | /** 251 | * Returns true, if the item is the last item in the queue. 252 | * 253 | * @param {Ext.ux.upload.Item} item 254 | * @return {boolean} 255 | */ 256 | isLast : function(item) { 257 | var lastItem = this.last(); 258 | if (lastItem && item.getId() == lastItem.getId()) { 259 | return true; 260 | } 261 | 262 | return false; 263 | }, 264 | 265 | /** 266 | * Returns total bytes of all files in the queue. 267 | * 268 | * @return {number} 269 | */ 270 | getTotalBytes : function() { 271 | var bytes = 0; 272 | 273 | this.each(function(item, index, length) { 274 | bytes += item.getSize(); 275 | }, this); 276 | 277 | return bytes; 278 | } 279 | }); -------------------------------------------------------------------------------- /lib/upload/StatusBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Upload status bar. 3 | * 4 | * @class Ext.ux.upload.StatusBar 5 | * @extends Ext.toolbar.Toolbar 6 | */ 7 | Ext.define('Ext.ux.upload.StatusBar', { 8 | extend : 'Ext.toolbar.Toolbar', 9 | 10 | config : { 11 | selectionMessageText : 'Selected {0} file(s), {1}', 12 | uploadMessageText : 'Upload progress {0}% ({1} of {2} file(s))', 13 | textComponentId : 'mu-status-text' 14 | }, 15 | 16 | initComponent : function() { 17 | 18 | Ext.apply(this, { 19 | items : [ 20 | { 21 | xtype : 'tbtext', 22 | itemId : this.textComponentId, 23 | text : ' ' 24 | } 25 | ] 26 | }); 27 | 28 | this.callParent(arguments); 29 | }, 30 | 31 | setText : function(text) { 32 | this.getComponent(this.textComponentId).setText(text); 33 | }, 34 | 35 | setSelectionMessage : function(fileCount, byteCount) { 36 | this.setText(Ext.String.format(this.selectionMessageText, fileCount, Ext.util.Format.fileSize(byteCount))); 37 | }, 38 | 39 | setUploadMessage : function(progressPercent, uploadedFiles, totalFiles) { 40 | this.setText(Ext.String.format(this.uploadMessageText, progressPercent, uploadedFiles, totalFiles)); 41 | } 42 | 43 | }); -------------------------------------------------------------------------------- /lib/upload/Store.js: -------------------------------------------------------------------------------- 1 | Ext.define('Ext.ux.upload.Store', { 2 | extend : 'Ext.data.Store', 3 | 4 | fields : [ 5 | { 6 | name : 'filename', 7 | type : 'string' 8 | }, { 9 | name : 'size', 10 | type : 'integer' 11 | }, { 12 | name : 'type', 13 | type : 'string' 14 | }, { 15 | name : 'status', 16 | type : 'string' 17 | }, { 18 | name : 'message', 19 | type : 'string' 20 | } 21 | ], 22 | 23 | proxy : { 24 | type : 'memory', 25 | reader : { 26 | type : 'array', 27 | idProperty : 'filename' 28 | } 29 | } 30 | }); -------------------------------------------------------------------------------- /lib/upload/css/upload.css: -------------------------------------------------------------------------------- 1 | @CHARSET "UTF-8"; 2 | 3 | .ux-mu-status-value { 4 | float: right; 5 | min-width: 16px; 6 | height: 16px; 7 | background-repeat: no-repeat; 8 | margin: 0 3px 0 2px; 9 | cursor: pointer; 10 | overflow: hidden; 11 | } 12 | 13 | .icon-help { 14 | background-image: url(../img/help.png) !important; 15 | } 16 | 17 | /* 18 | * Action icons 19 | */ 20 | .ux-mu-icon-action-ok { 21 | background-image: url(../img/tick.png) !important; 22 | } 23 | 24 | .ux-mu-icon-action-upload { 25 | background-image: url(../img/arrow_up.png) !important; 26 | } 27 | 28 | .ux-mu-icon-action-abort { 29 | background-image: url(../img/cancel.png) !important; 30 | } 31 | 32 | .ux-mu-icon-action-remove { 33 | background-image: url(../img/delete.png) !important; 34 | } 35 | 36 | .ux-mu-icon-action-browse { 37 | background-image: url(../img/folder.png) !important; 38 | } 39 | 40 | /* 41 | * Upload status icons 42 | */ 43 | .ux-mu-icon-upload-ready { /* 44 | background-image: url(../img/ready.png) !important; 45 | */ 46 | 47 | } 48 | 49 | .ux-mu-icon-upload-uploading { 50 | background-image: url(../img/loading.gif) !important; 51 | } 52 | 53 | .ux-mu-icon-upload-uploaded { 54 | background-image: url(../img/accept.png) !important; 55 | } 56 | 57 | .ux-mu-icon-upload-uploaderror { 58 | background-image: url(../img/exclamation.png) !important; 59 | } -------------------------------------------------------------------------------- /lib/upload/data/Connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modified Ext.data.Connection object, adapted to be able to report progress. 3 | */ 4 | Ext.define('Ext.ux.upload.data.Connection', { 5 | extend : 'Ext.data.Connection', 6 | 7 | /** 8 | * @cfg {Function} 9 | * 10 | * Callback fired when a progress event occurs (xhr.upload.onprogress). 11 | */ 12 | progressCallback : null, 13 | 14 | request : function(options) { 15 | var progressCallback = options.progress; 16 | if (progressCallback) { 17 | this.progressCallback = progressCallback; 18 | } 19 | 20 | this.callParent(arguments); 21 | }, 22 | 23 | getXhrInstance : function() { 24 | var xhr = this.callParent(arguments); 25 | 26 | if (this.progressCallback) { 27 | xhr.upload.onprogress = this.progressCallback; 28 | } 29 | 30 | return xhr; 31 | } 32 | }); -------------------------------------------------------------------------------- /lib/upload/header/AbstractFilenameEncoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract filename encoder. 3 | */ 4 | Ext.define('Ext.ux.upload.header.AbstractFilenameEncoder', { 5 | 6 | config : {}, 7 | 8 | type : 'generic', 9 | 10 | encode : function(filename) {}, 11 | 12 | getType : function() { 13 | return this.type; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /lib/upload/header/Base64FilenameEncoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base64 filename encoder - uses the built-in function window.btoa(). 3 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Window.btoa 4 | */ 5 | Ext.define('Ext.ux.upload.header.Base64FilenameEncoder', { 6 | extend : 'Ext.ux.upload.header.AbstractFilenameEncoder', 7 | 8 | config : {}, 9 | 10 | type : 'base64', 11 | 12 | encode : function(filename) { 13 | return window.btoa(unescape(encodeURIComponent(filename))); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /lib/upload/img/accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/accept.png -------------------------------------------------------------------------------- /lib/upload/img/ajax-loader1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/ajax-loader1.gif -------------------------------------------------------------------------------- /lib/upload/img/ajax-loader2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/ajax-loader2.gif -------------------------------------------------------------------------------- /lib/upload/img/ajax-loader3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/ajax-loader3.gif -------------------------------------------------------------------------------- /lib/upload/img/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/arrow_up.png -------------------------------------------------------------------------------- /lib/upload/img/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/cancel.png -------------------------------------------------------------------------------- /lib/upload/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/delete.png -------------------------------------------------------------------------------- /lib/upload/img/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/exclamation.png -------------------------------------------------------------------------------- /lib/upload/img/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/folder.png -------------------------------------------------------------------------------- /lib/upload/img/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/help.png -------------------------------------------------------------------------------- /lib/upload/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/loading.gif -------------------------------------------------------------------------------- /lib/upload/img/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/lib/upload/img/tick.png -------------------------------------------------------------------------------- /lib/upload/uploader/AbstractUploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract uploader object. 3 | * 4 | * The uploader object implements the the upload itself - transports data to the server. This is an "abstract" object 5 | * used as a base object for all uploader objects. 6 | * 7 | */ 8 | Ext.define('Ext.ux.upload.uploader.AbstractUploader', { 9 | mixins : { 10 | observable : 'Ext.util.Observable' 11 | }, 12 | 13 | /** 14 | * @cfg {Number} [maxFileSize=50000000] 15 | * 16 | * (NOT IMPLEMENTED) The maximum file size allowed to be uploaded. 17 | */ 18 | maxFileSize : 50000000, 19 | 20 | /** 21 | * @cfg {String} url (required) 22 | * 23 | * The server URL to upload to. 24 | */ 25 | url : '', 26 | 27 | /** 28 | * @cfg {Number} [timeout=60000] 29 | * 30 | * The connection timeout in miliseconds. 31 | */ 32 | timeout : 60 * 1000, 33 | 34 | /** 35 | * @cfg {String} [contentType='application/binary'] 36 | * 37 | * The content type announced in the HTTP headers. It is autodetected if possible, but if autodetection 38 | * cannot be done, this value is set as content type header. 39 | */ 40 | contentType : 'application/binary', 41 | 42 | /** 43 | * @cfg {String} [filenameHeader='X-File-Name'] 44 | * 45 | * The name of the HTTP header containing the filename. 46 | */ 47 | filenameHeader : 'X-File-Name', 48 | 49 | /** 50 | * @cfg {String} [sizeHeader='X-File-Size'] 51 | * 52 | * The name of the HTTP header containing the size of the file. 53 | */ 54 | sizeHeader : 'X-File-Size', 55 | 56 | /** 57 | * @cfg {String} [typeHeader='X-File-Type'] 58 | * 59 | * The name of the HTTP header containing the MIME type of the file. 60 | */ 61 | typeHeader : 'X-File-Type', 62 | 63 | /** 64 | * @cfg {Object} 65 | * 66 | * Additional parameters to be sent with the upload request. 67 | */ 68 | params : {}, 69 | 70 | /** 71 | * @cfg {Object} 72 | * 73 | * Extra headers to be sent with the upload request. 74 | */ 75 | extraHeaders : {}, 76 | 77 | /** 78 | * @cfg {Object/String} 79 | * 80 | * Encoder object/class used to encode the filename header. Usually used, when the filename 81 | * contains non-ASCII characters. 82 | */ 83 | filenameEncoder : null, 84 | 85 | filenameEncoderHeader : 'X-Filename-Encoder', 86 | 87 | /** 88 | * Constructor. 89 | * @param {Object} [config] 90 | */ 91 | constructor : function(config) { 92 | this.mixins.observable.constructor.call(this); 93 | 94 | this.initConfig(config); 95 | }, 96 | 97 | /** 98 | * @protected 99 | */ 100 | initHeaders : function(item) { 101 | var headers = this.extraHeaders || {}, 102 | filename = item.getFilename(); 103 | 104 | /* 105 | * If there is a filename encoder defined - use it to encode the filename 106 | * in the header and set the type of the encoder as an additional header. 107 | */ 108 | var filenameEncoder = this.initFilenameEncoder(); 109 | if (filenameEncoder) { 110 | filename = filenameEncoder.encode(filename); 111 | headers[this.filenameEncoderHeader] = filenameEncoder.getType(); 112 | } 113 | headers[this.filenameHeader] = filename; 114 | headers[this.sizeHeader] = item.getSize(); 115 | headers[this.typeHeader] = item.getType(); 116 | 117 | return headers; 118 | }, 119 | 120 | /** 121 | * @abstract 122 | * 123 | * Upload a single item (file). 124 | * **Implement in subclass** 125 | * 126 | * @param {Ext.ux.upload.Item} item 127 | */ 128 | uploadItem : function(item) {}, 129 | 130 | /** 131 | * @abstract 132 | * 133 | * Aborts the current upload. 134 | * **Implement in subclass** 135 | */ 136 | abortUpload : function() {}, 137 | 138 | /** 139 | * @protected 140 | */ 141 | initFilenameEncoder : function() { 142 | if (Ext.isString(this.filenameEncoder)) { 143 | this.filenameEncoder = Ext.create(this.filenameEncoder); 144 | } 145 | 146 | if (Ext.isObject(this.filenameEncoder)) { 147 | return this.filenameEncoder; 148 | } 149 | 150 | return null; 151 | } 152 | 153 | }); 154 | -------------------------------------------------------------------------------- /lib/upload/uploader/AbstractXhrUploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract uploader with features common for all XHR based uploaders. 3 | */ 4 | Ext.define('Ext.ux.upload.uploader.AbstractXhrUploader', { 5 | extend : 'Ext.ux.upload.uploader.AbstractUploader', 6 | 7 | onUploadSuccess : function(response, options, item) { 8 | var info = { 9 | success : true, 10 | message : '', 11 | response : response 12 | }; 13 | 14 | if (response.responseText) { 15 | var responseJson = Ext.decode(response.responseText); 16 | if (responseJson) { 17 | Ext.apply(info, { 18 | success : responseJson.success, 19 | message : responseJson.message 20 | }); 21 | 22 | var eventName = info.success ? 'uploadsuccess' : 'uploadfailure'; 23 | this.fireEvent(eventName, item, info); 24 | return; 25 | } 26 | } 27 | 28 | this.fireEvent('uploadsuccess', item, info); 29 | }, 30 | 31 | onUploadFailure : function(response, options, item) { 32 | var info = { 33 | success : false, 34 | message : 'http error', 35 | response : response 36 | }; 37 | 38 | this.fireEvent('uploadfailure', item, info); 39 | }, 40 | 41 | onUploadProgress : function(event, item) { 42 | this.fireEvent('uploadprogress', item, event); 43 | } 44 | }); -------------------------------------------------------------------------------- /lib/upload/uploader/DummyUploader.js: -------------------------------------------------------------------------------- 1 | Ext.define('Ext.ux.upload.uploader.DummyUploader', { 2 | extend : 'Ext.ux.upload.uploader.AbstractUploader', 3 | 4 | delay : 1000, 5 | 6 | uploadItem : function(item) { 7 | item.setUploading(); 8 | 9 | var task = new Ext.util.DelayedTask(function() { 10 | this.fireEvent('uploadsuccess', item, { 11 | success : true, 12 | message : 'OK', 13 | response : null 14 | }); 15 | }, this); 16 | 17 | task.delay(this.delay); 18 | }, 19 | 20 | abortUpload : function() { 21 | } 22 | }); -------------------------------------------------------------------------------- /lib/upload/uploader/ExtJsUploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Uploader implementation - with the Connection object in ExtJS 4 3 | * 4 | */ 5 | Ext.define('Ext.ux.upload.uploader.ExtJsUploader', { 6 | extend : 'Ext.ux.upload.uploader.AbstractXhrUploader', 7 | 8 | requires : [ 9 | 'Ext.ux.upload.data.Connection' 10 | ], 11 | 12 | config : { 13 | /** 14 | * @cfg {String} [method='PUT'] 15 | * 16 | * The HTTP method to be used. 17 | */ 18 | method : 'PUT', 19 | 20 | /** 21 | * @cfg {Ext.data.Connection} 22 | * 23 | * If set, this connection object will be used when uploading files. 24 | */ 25 | connection : null 26 | }, 27 | 28 | /** 29 | * @property 30 | * @private 31 | * 32 | * The connection object. 33 | */ 34 | conn : null, 35 | 36 | /** 37 | * @private 38 | * 39 | * Initializes and returns the connection object. 40 | * 41 | * @return {Ext.ux.upload.data.Connection} 42 | */ 43 | initConnection : function() { 44 | var conn, 45 | url = this.url; 46 | 47 | if (this.connection instanceof Ext.data.Connection) { 48 | conn = this.connection; 49 | } else { 50 | 51 | if (this.params) { 52 | url = Ext.urlAppend(url, Ext.urlEncode(this.params)); 53 | } 54 | 55 | conn = Ext.create('Ext.ux.upload.data.Connection', { 56 | disableCaching : true, 57 | method : this.method, 58 | url : url, 59 | timeout : this.timeout, 60 | defaultHeaders : { 61 | 'Content-Type' : this.contentType, 62 | 'X-Requested-With' : 'XMLHttpRequest' 63 | } 64 | }); 65 | } 66 | 67 | return conn; 68 | }, 69 | 70 | /** 71 | * @protected 72 | */ 73 | initHeaders : function(item) { 74 | var headers = this.callParent(arguments); 75 | 76 | headers['Content-Type'] = item.getType(); 77 | 78 | return headers; 79 | }, 80 | 81 | /** 82 | * Implements {@link Ext.ux.upload.uploader.AbstractUploader#uploadItem} 83 | * 84 | * @param {Ext.ux.upload.Item} item 85 | */ 86 | uploadItem : function(item) { 87 | var file = item.getFileApiObject(); 88 | if (!file) { 89 | return; 90 | } 91 | 92 | item.setUploading(); 93 | 94 | this.conn = this.initConnection(); 95 | 96 | /* 97 | * Passing the File object directly as the "rawFata" option. 98 | * Specs: 99 | * https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#the-send()-method 100 | * http://dev.w3.org/2006/webapi/FileAPI/#blob 101 | */ 102 | this.conn.request({ 103 | scope : this, 104 | headers : this.initHeaders(item), 105 | rawData : file, 106 | 107 | success : Ext.Function.bind(this.onUploadSuccess, this, [ 108 | item 109 | ], true), 110 | failure : Ext.Function.bind(this.onUploadFailure, this, [ 111 | item 112 | ], true), 113 | progress : Ext.Function.bind(this.onUploadProgress, this, [ 114 | item 115 | ], true) 116 | }); 117 | 118 | }, 119 | 120 | /** 121 | * Implements {@link Ext.ux.upload.uploader.AbstractUploader#abortUpload} 122 | */ 123 | abortUpload : function() { 124 | if (this.conn) { 125 | /* 126 | * If we don't suspend the events, the connection abortion will cause a failure event. 127 | */ 128 | this.suspendEvents(); 129 | this.conn.abort(); 130 | this.resumeEvents(); 131 | } 132 | } 133 | }); -------------------------------------------------------------------------------- /lib/upload/uploader/FormDataUploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Uploader implementation which uses a FormData object to send files through XHR requests. 3 | * 4 | */ 5 | Ext.define('Ext.ux.upload.uploader.FormDataUploader', { 6 | extend : 'Ext.ux.upload.uploader.AbstractXhrUploader', 7 | 8 | requires : [ 9 | 'Ext.ux.upload.data.Connection' 10 | ], 11 | 12 | method : 'POST', 13 | xhr : null, 14 | 15 | initConnection : function() { 16 | if (this.params) { 17 | this.url = Ext.urlAppend(this.url, Ext.urlEncode(this.params)); 18 | } 19 | 20 | var xhr = new XMLHttpRequest(), 21 | method = this.method, 22 | url = this.url; 23 | 24 | xhr.open(method, url, true); 25 | 26 | this.abortXhr = function() { 27 | this.suspendEvents(); 28 | xhr.abort(); 29 | this.resumeEvents(); 30 | }; 31 | 32 | return xhr; 33 | }, 34 | 35 | uploadItem : function(item) { 36 | var file = item.getFileApiObject(); 37 | 38 | item.setUploading(); 39 | 40 | var formData = new FormData(); 41 | formData.append(file.name, file); 42 | 43 | var xhr = this.initConnection(); 44 | 45 | xhr.setRequestHeader(this.filenameHeader, file.name); 46 | xhr.setRequestHeader(this.sizeHeader, file.size); 47 | xhr.setRequestHeader(this.typeHeader, file.type); 48 | 49 | var loadendhandler = Ext.Function.bind(this.onLoadEnd, this, [ 50 | item 51 | ], true); 52 | 53 | var progresshandler = Ext.Function.bind(this.onUploadProgress, this, [ 54 | item 55 | ], true); 56 | 57 | xhr.addEventListener('loadend', loadendhandler, true); 58 | xhr.upload.addEventListener("progress", progresshandler, true); 59 | 60 | xhr.send(formData); 61 | }, 62 | 63 | /** 64 | * Implements {@link Ext.ux.upload.uploader.AbstractUploader#abortUpload} 65 | */ 66 | abortUpload : function() { 67 | this.abortXhr(); 68 | }, 69 | 70 | /** 71 | * @protected 72 | * 73 | * A placeholder for the abort procedure. 74 | */ 75 | abortXhr : function() { 76 | }, 77 | 78 | onLoadEnd : function(event, item) { 79 | var response = event.target; 80 | 81 | if (response.status != 200) { 82 | return this.onUploadFailure(response, null, item); 83 | } 84 | 85 | return this.onUploadSuccess(response, null, item); 86 | } 87 | }); -------------------------------------------------------------------------------- /lib/upload/uploader/LegacyExtJsUploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Uploader implementation - with the Connection object in ExtJS 4 3 | * 4 | */ 5 | Ext.define('Ext.ux.upload.uploader.ExtJsUploader', { 6 | extend : 'Ext.ux.upload.uploader.AbstractUploader', 7 | 8 | requires : [ 9 | 'Ext.ux.upload.data.Connection' 10 | ], 11 | 12 | /** 13 | * @property 14 | * 15 | * The connection object. 16 | */ 17 | conn : null, 18 | 19 | /** 20 | * @private 21 | * 22 | * Initializes and returns the connection object. 23 | * 24 | * @return {Ext.ux.upload.data.Connection} 25 | */ 26 | initConnection : function() { 27 | var url = this.url; 28 | if (this.params) { 29 | url = Ext.urlAppend(url, Ext.urlEncode(this.params)); 30 | } 31 | 32 | var conn = Ext.create('Ext.ux.upload.data.Connection', { 33 | disableCaching : true, 34 | method : this.method, 35 | url : url, 36 | timeout : this.timeout, 37 | defaultHeaders : { 38 | 'Content-Type' : this.contentType, 39 | 'X-Requested-With' : 'XMLHttpRequest' 40 | } 41 | }); 42 | 43 | return conn; 44 | }, 45 | 46 | /** 47 | * Implements {@link Ext.ux.upload.uploader.AbstractUploader#uploadItem} 48 | * 49 | * @param {Ext.ux.upload.Item} item 50 | */ 51 | uploadItem : function(item) { 52 | var file = item.getFileApiObject(); 53 | if (!file) { 54 | return; 55 | } 56 | 57 | item.setUploading(); 58 | 59 | this.conn = this.initConnection(); 60 | 61 | this.conn.request({ 62 | scope : this, 63 | headers : this.initHeaders(item), 64 | xmlData : file, 65 | 66 | success : Ext.Function.bind(this.onUploadSuccess, this, [ 67 | item 68 | ], true), 69 | failure : Ext.Function.bind(this.onUploadFailure, this, [ 70 | item 71 | ], true), 72 | progress : Ext.Function.bind(this.onUploadProgress, this, [ 73 | item 74 | ], true) 75 | }); 76 | }, 77 | 78 | /** 79 | * Implements {@link Ext.ux.upload.uploader.AbstractUploader#abortUpload} 80 | */ 81 | abortUpload : function() { 82 | if (this.conn) { 83 | this.conn.abort(); 84 | } 85 | }, 86 | 87 | onUploadSuccess : function(response, options, item) { 88 | var info = { 89 | success : false, 90 | message : 'general error', 91 | response : response 92 | }; 93 | 94 | if (response.responseText) { 95 | var responseJson = Ext.decode(response.responseText); 96 | if (responseJson && responseJson.success) { 97 | Ext.apply(info, { 98 | success : responseJson.success, 99 | message : responseJson.message 100 | }); 101 | 102 | this.fireEvent('uploadsuccess', item, info); 103 | return; 104 | } 105 | 106 | Ext.apply(info, { 107 | message : responseJson.message 108 | }); 109 | } 110 | 111 | this.fireEvent('uploadfailure', item, info); 112 | }, 113 | 114 | onUploadFailure : function(response, options, item) { 115 | var info = { 116 | success : false, 117 | message : 'http error', 118 | response : response 119 | }; 120 | 121 | this.fireEvent('uploadfailure', item, info); 122 | }, 123 | 124 | onUploadProgress : function(event, item) { 125 | this.fireEvent('uploadprogress', item, event); 126 | } 127 | }); -------------------------------------------------------------------------------- /public/_common.php: -------------------------------------------------------------------------------- 1 | $success, 13 | 'message' => $message 14 | ); 15 | 16 | echo json_encode($response); 17 | exit(); 18 | } 19 | 20 | function _error($message) 21 | { 22 | return _response(false, $message); 23 | } -------------------------------------------------------------------------------- /public/_config.php.dist: -------------------------------------------------------------------------------- 1 | '/tmp/test-upload-dir', 4 | 'fake' => false 5 | ); -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | Ext.Loader.setPath({ 2 | 'Ext.ux' : 'external' 3 | }); 4 | 5 | Ext.application({ 6 | 7 | requires : [ 8 | 'Ext.ux.upload.Dialog' 9 | ], 10 | 11 | name : 'Example', 12 | 13 | appFolder : 'app', 14 | 15 | launch : function() { 16 | debug = console; 17 | 18 | Ext.create('Ext.container.Viewport', { 19 | layout : 'fit' 20 | }); 21 | 22 | var appPanel = Ext.create('Ext.window.Window', { 23 | title : 'Files', 24 | width : 600, 25 | height : 400, 26 | closable : false, 27 | modal : true, 28 | bodyPadding : 5, 29 | 30 | uploadComplete : function(items) { 31 | var output = 'Uploaded files:
'; 32 | Ext.Array.each(items, function(item) { 33 | output += item.getFilename() + ' (' + item.getType() + ', ' 34 | + Ext.util.Format.fileSize(item.getSize()) + ')' + '
'; 35 | }); 36 | 37 | this.update(output); 38 | } 39 | }); 40 | 41 | appPanel.syncCheckbox = Ext.create('Ext.form.field.Checkbox', { 42 | inputValue : true, 43 | checked : true 44 | }); 45 | 46 | appPanel.addDocked({ 47 | xtype : 'toolbar', 48 | dock : 'top', 49 | items : [ 50 | { 51 | xtype : 'button', 52 | text : 'Raw PUT/POST Upload', 53 | scope : appPanel, 54 | handler : function() { 55 | 56 | var uploadPanel = Ext.create('Ext.ux.upload.Panel', { 57 | uploaderOptions : { 58 | url : 'upload.php' 59 | }, 60 | filenameEncoder : 'Ext.ux.upload.header.Base64FilenameEncoder', 61 | synchronous : appPanel.syncCheckbox.getValue() 62 | }); 63 | 64 | var uploadDialog = Ext.create('Ext.ux.upload.Dialog', { 65 | dialogTitle : 'My Upload Dialog', 66 | panel : uploadPanel 67 | }); 68 | 69 | this.mon(uploadDialog, 'uploadcomplete', function(uploadPanel, manager, items, errorCount) { 70 | this.uploadComplete(items); 71 | if (!errorCount) { 72 | uploadDialog.close(); 73 | } 74 | }, this); 75 | 76 | uploadDialog.show(); 77 | } 78 | }, '-', { 79 | xtype : 'button', 80 | text : 'Multipart Upload', 81 | scope : appPanel, 82 | handler : function() { 83 | 84 | var uploadPanel = Ext.create('Ext.ux.upload.Panel', { 85 | uploader : 'Ext.ux.upload.uploader.FormDataUploader', 86 | uploaderOptions : { 87 | url : 'upload_multipart.php' 88 | }, 89 | synchronous : appPanel.syncCheckbox.getValue() 90 | }); 91 | 92 | var uploadDialog = Ext.create('Ext.ux.upload.Dialog', { 93 | dialogTitle : 'My Upload Dialog', 94 | panel : uploadPanel 95 | }); 96 | 97 | this.mon(uploadDialog, 'uploadcomplete', function(uploadPanel, manager, items, errorCount) { 98 | this.uploadComplete(items); 99 | if (!errorCount) { 100 | uploadDialog.close(); 101 | } 102 | }, this); 103 | 104 | uploadDialog.show(); 105 | } 106 | }, '-', { 107 | xtype : 'button', 108 | text : 'Dummy upload', 109 | scope : appPanel, 110 | handler : function() { 111 | 112 | var uploadPanel = Ext.create('Ext.ux.upload.Panel', { 113 | uploader : 'Ext.ux.upload.uploader.DummyUploader', 114 | synchronous : appPanel.syncCheckbox.getValue() 115 | }); 116 | 117 | var uploadDialog = Ext.create('Ext.ux.upload.Dialog', { 118 | dialogTitle : 'My Upload Dialog', 119 | panel : uploadPanel 120 | }); 121 | 122 | this.mon(uploadDialog, 'uploadcomplete', function(uploadPanel, manager, items, errorCount) { 123 | this.uploadComplete(items); 124 | if (!errorCount) { 125 | uploadDialog.close(); 126 | } 127 | }, this); 128 | 129 | uploadDialog.show(); 130 | } 131 | }, '->', appPanel.syncCheckbox, 'Synchronous upload' 132 | ] 133 | }) 134 | 135 | appPanel.show(); 136 | } 137 | 138 | }); -------------------------------------------------------------------------------- /public/docs: -------------------------------------------------------------------------------- 1 | ../docs -------------------------------------------------------------------------------- /public/external/upload: -------------------------------------------------------------------------------- 1 | ../../lib/upload -------------------------------------------------------------------------------- /public/img/extjs-upload-widget.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan-novakov/extjs-upload-widget/70c49c62c17a08015158e8da939ab37bb9146ce5/public/img/extjs-upload-widget.jpeg -------------------------------------------------------------------------------- /public/index-ext4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | File upload 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | File upload 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/upload.php: -------------------------------------------------------------------------------- 1 | $fileData) { 21 | if ($fileData['error'] !== 0) { 22 | _error(sprintf("Upload error '%d'", $fileData['error'])); 23 | } 24 | 25 | $fileName = htmlspecialchars($fileData['name']); 26 | $mimeType = $fileData['type']; 27 | $fileSize = $fileData['size']; 28 | 29 | $targetFile = $config['upload_dir'] . '/' . $fileName; 30 | 31 | if (! $config['fake']) { 32 | if (! move_uploaded_file($fileData['tmp_name'], $targetFile)) { 33 | _error('Error saving uploaded file'); 34 | } 35 | } 36 | } 37 | 38 | _log(sprintf("[multipart] Uploaded %s, %s, %d byte(s)", $fileName, $mimeType, $fileSize)); 39 | _response(); 40 | --------------------------------------------------------------------------------