Subversion Repositories eFlore/Applications.cel

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
27 jpm 1
// vim: ts=4:sw=4:nu:fdc=4:nospell
2
/**
3
 * Ext.ux.form.UploadPanel
4
 *
5
 * @author  Ing. Jozef Sakáloš
6
 * @version $Id: Ext.ux.UploadPanel.js 94 2008-03-24 01:04:27Z jozo $
7
 * @date    13. March 2008
8
 *
9
 * @license Ext.ux.form.UploadPanel is licensed under the terms of
10
 * the Open Source LGPL 3.0 license.  Commercial use is permitted to the extent
11
 * that the code/component(s) do NOT become part of another Open Source or Commercially
12
 * licensed development library or toolkit without explicit permission.
13
 *
14
 * License details: http://www.gnu.org/licenses/lgpl.html
15
 */
16
 
17
/*global Ext */
18
 
19
/**
20
 * @class Ext.ux.UploadPanel
21
 * @extends Ext.Panel
22
 */
23
Ext.ux.UploadPanel = Ext.extend(Ext.Panel, {
24
 
25
	// configuration options overridable from outside
26
	// {{{
27
	/**
28
	 * @cfg {String} addIconCls icon class for add (file browse) button
29
	 */
30
	 addIconCls:'icon-plus'
31
 
32
	/**
33
	 * @cfg {String} addText Text on Add button
34
	 */
35
	,addText:'Add'
36
 
37
	/**
38
	 * @cfg {Object} baseParams This object is not used directly by FileTreePanel but it is
39
	 * propagated to lower level objects instead. Included here for convenience.
40
	 */
41
 
42
	/**
43
	 * @cfg {String} bodyStyle style to use for panel body
44
	 */
45
	,bodyStyle:'padding:2px'
46
 
47
	/**
48
	 * @cfg {String} buttonsAt Where buttons are placed. Valid values are tbar, bbar, body (defaults to 'tbar')
49
	 */
50
	,buttonsAt:'tbar'
51
 
52
	/**
53
	 * @cfg {String} clickRemoveText
54
	 */
55
	,clickRemoveText:'Click to remove'
56
 
57
	/**
58
	 * @cfg {String} clickStopText
59
	 */
60
	,clickStopText:'Click to stop'
61
 
62
	/**
63
	 * @cfg {String} emptyText empty text for dataview
64
	 */
65
	,emptyText:'No files'
66
 
67
	/**
68
	 * @cfg {Boolean} enableProgress true to enable querying server for progress information
69
	 * Passed to underlying uploader. Included here for convenience.
70
	 */
71
	,enableProgress:true
72
 
73
	/**
74
	 * @cfg {String} errorText
75
	 */
76
	,errorText:'Error'
77
 
78
	/**
79
	 * @cfg {String} fileCls class prefix to use for file type classes
80
	 */
81
	,fileCls:'file'
82
 
83
	/**
84
	 * @cfg {String} fileQueuedText File upload status text
85
	 */
86
	,fileQueuedText:'File <b>{0}</b> is queued for upload'
87
 
88
	/**
89
	 * @cfg {String} fileDoneText File upload status text
90
	 */
91
	,fileDoneText:'File <b>{0}</b> has been successfully uploaded'
92
 
93
	/**
94
	 * @cfg {String} fileFailedText File upload status text
95
	 */
96
	,fileFailedText:'File <b>{0}</b> failed to upload'
97
 
98
	/**
99
	 * @cfg {String} fileStoppedText File upload status text
100
	 */
101
	,fileStoppedText:'File <b>{0}</b> stopped by user'
102
 
103
	/**
104
	 * @cfg {String} fileUploadingText File upload status text
105
	 */
106
	,fileUploadingText:'Uploading file <b>{0}</b>'
107
 
108
	/**
109
	 * @cfg {Number} maxFileSize Maximum upload file size in bytes
110
	 * This config property is propagated down to uploader for convenience
111
	 */
112
	,maxFileSize:524288
113
 
114
	/**
115
	 * @cfg {Number} Maximum file name length for short file names
116
	 */
117
	,maxLength:18
118
 
119
	/**
120
	 * @cfg {String} removeAllIconCls iconClass to use for Remove All button (defaults to 'icon-cross'
121
	 */
122
	,removeAllIconCls:'icon-cross'
123
 
124
	/**
125
	 * @cfg {String} removeAllText text to use for Remove All button tooltip
126
	 */
127
	,removeAllText:'Remove All'
128
 
129
	/**
130
	 * @cfg {String} removeIconCls icon class to use for remove file icon
131
	 */
132
	,removeIconCls:'icon-minus'
133
 
134
	/**
135
	 * @cfg {String} removeText Remove text
136
	 */
137
	,removeText:'Remove'
138
 
139
	/**
140
	 * @cfg {String} selectedClass class for selected item of DataView
141
	 */
142
	,selectedClass:'ux-up-item-selected'
143
 
144
	/**
145
	 * @cfg {Boolean} singleUpload true to upload files in one form, false to upload one by one
146
	 * This config property is propagated down to uploader for convenience
147
	 */
148
	,singleUpload:false
149
 
150
	/**
151
	 * @cfg {String} stopAllText
152
	 */
153
	,stopAllText:'Stop All'
154
 
155
	/**
156
	 * @cfg {String} stopIconCls icon class to use for stop
157
	 */
158
	,stopIconCls:'icon-stop'
159
 
160
	/**
161
	 * @cfg {String/Ext.XTemplate} tpl Template for DataView.
162
	 */
163
 
164
	/**
165
	 * @cfg {String} uploadText Upload text
166
	 */
167
	,uploadText:'Upload'
168
 
169
	/**
170
	 * @cfg {String} uploadIconCls icon class to use for upload button
171
	 */
172
	,uploadIconCls:'icon-upload'
173
 
174
	/**
175
	 * @cfg {String} workingIconCls iconClass to use for busy indicator
176
	 */
177
	,workingIconCls:'icon-working'
178
 
179
	// }}}
180
 
181
	// overrides
182
	// {{{
183
	,initComponent:function() {
184
 
185
		// {{{
186
		// create buttons
187
		// add (file browse button) configuration
188
		var addCfg = {
189
			 xtype:'browsebutton'
190
			,text:this.addText + '...'
191
			,iconCls:this.addIconCls
192
			,scope:this
193
			,handler:this.onAddFile
194
		};
195
 
196
		// upload button configuration
197
		var upCfg = {
198
			 xtype:'button'
199
			,iconCls:this.uploadIconCls
200
			,text:this.uploadText
201
			,scope:this
202
			,handler:this.onUpload
203
			,disabled:true
204
		};
205
 
206
		// remove all button configuration
207
		var removeAllCfg = {
208
			 xtype:'button'
209
			,iconCls:this.removeAllIconCls
210
			,tooltip:this.removeAllText
211
			,scope:this
212
			,handler:this.onRemoveAllClick
213
			,disabled:true
214
		};
215
 
216
		// todo: either to cancel buttons in body or implement it
217
		if('body' !== this.buttonsAt) {
218
			this[this.buttonsAt] = [addCfg, upCfg, '->', removeAllCfg];
219
		}
220
		// }}}
221
		// {{{
222
		// create store
223
		// fields for record
224
		var fields = [
225
			 {name:'id', type:'text', system:true}
226
			,{name:'shortName', type:'text', system:true}
227
			,{name:'fileName', type:'text', system:true}
228
			,{name:'filePath', type:'text', system:true}
229
			,{name:'fileCls', type:'text', system:true}
230
			,{name:'input', system:true}
231
			,{name:'form', system:true}
232
			,{name:'state', type:'text', system:true}
233
			,{name:'error', type:'text', system:true}
234
			,{name:'progressId', type:'int', system:true}
235
			,{name:'bytesTotal', type:'int', system:true}
236
			,{name:'bytesUploaded', type:'int', system:true}
237
			,{name:'estSec', type:'int', system:true}
238
			,{name:'filesUploaded', type:'int', system:true}
239
			,{name:'speedAverage', type:'int', system:true}
240
			,{name:'speedLast', type:'int', system:true}
241
			,{name:'timeLast', type:'int', system:true}
242
			,{name:'timeStart', type:'int', system:true}
243
			,{name:'pctComplete', type:'int', system:true}
244
		];
245
 
246
		// add custom fields if passed
247
		if(Ext.isArray(this.customFields)) {
248
			fields.push(this.customFields);
249
		}
250
 
251
		// create store
252
		this.store = new Ext.data.SimpleStore({
253
			 id:0
254
			,fields:fields
255
			,data:[]
256
		});
257
		// }}}
258
		// {{{
259
		// create view
260
		Ext.apply(this, {
261
			items:[{
262
				 xtype:'dataview'
263
				,itemSelector:'div.ux-up-item'
264
				,store:this.store
265
				,selectedClass:this.selectedClass
266
				,singleSelect:true
267
				,emptyText:this.emptyText
268
				,tpl: this.tpl || new Ext.XTemplate(
269
					  '<tpl for=".">'
270
					+ '<div class="ux-up-item">'
271
//					+ '<div class="ux-up-indicator">&#160;</div>'
272
					+ '<div class="ux-up-icon-file {fileCls}">&#160;</div>'
273
					+ '<div class="ux-up-text x-unselectable" qtip="{fileName}">{shortName}</div>'
274
					+ '<div id="remove-{[values.input.id]}" class="ux-up-icon-state ux-up-icon-{state}"'
275
					+ 'qtip="{[this.scope.getQtip(values)]}">&#160;</div>'
276
					+ '</div>'
277
					+ '</tpl>'
278
					, {scope:this}
279
				)
280
				,listeners:{click:{scope:this, fn:this.onViewClick}}
281
 
282
			}]
283
		});
284
		// }}}
285
 
286
		// call parent
287
		Ext.ux.UploadPanel.superclass.initComponent.apply(this, arguments);
288
 
289
		// save useful references
290
		this.view = this.items.itemAt(0);
291
 
292
		// {{{
293
		// add events
294
		this.addEvents(
295
			/**
296
			 * Fires before the file is added to store. Return false to cancel the add
297
			 * @event beforefileadd
298
			 * @param {Ext.ux.UploadPanel} this
299
			 * @param {Ext.Element} input (type=file) being added
300
			 */
301
			'beforefileadd'
302
			/**
303
			 * Fires after the file is added to the store
304
			 * @event fileadd
305
			 * @param {Ext.ux.UploadPanel} this
306
			 * @param {Ext.data.Store} store
307
			 * @param {Ext.data.Record} Record (containing the input) that has been added to the store
308
			 */
309
			,'fileadd'
310
			/**
311
			 * Fires before the file is removed from the store. Return false to cancel the remove
312
			 * @event beforefileremove
313
			 * @param {Ext.ux.UploadPanel} this
314
			 * @param {Ext.data.Store} store
315
			 * @param {Ext.data.Record} Record (containing the input) that is being removed from the store
316
			 */
317
			,'beforefileremove'
318
			/**
319
			 * Fires after the record (file) has been removed from the store
320
			 * @event fileremove
321
			 * @param {Ext.ux.UploadPanel} this
322
			 * @param {Ext.data.Store} store
323
			 */
324
			,'fileremove'
325
			/**
326
			 * Fires before all files are removed from the store (queue). Return false to cancel the clear.
327
			 * Events for individual files being removed are suspended while clearing the queue.
328
			 * @event beforequeueclear
329
			 * @param {Ext.ux.UploadPanel} this
330
			 * @param {Ext.data.Store} store
331
			 */
332
			,'beforequeueclear'
333
			/**
334
			 * Fires after the store (queue) has been cleared
335
			 * Events for individual files being removed are suspended while clearing the queue.
336
			 * @event queueclear
337
			 * @param {Ext.ux.UploadPanel} this
338
			 * @param {Ext.data.Store} store
339
			 */
340
			,'queueclear'
341
			/**
342
			 * Fires after the upload button is clicked but before any upload is started
343
			 * Return false to cancel the event
344
			 * @param {Ext.ux.UploadPanel} this
345
			 */
346
			,'beforeupload'
347
		);
348
		// }}}
349
		// {{{
350
		// relay view events
351
		this.relayEvents(this.view, [
352
			 'beforeclick'
353
			,'beforeselect'
354
			,'click'
355
			,'containerclick'
356
			,'contextmenu'
357
			,'dblclick'
358
			,'selectionchange'
359
		]);
360
		// }}}
361
 
362
		// create uploader
363
		var config = {
364
			 store:this.store
365
			,singleUpload:this.singleUpload
366
			,maxFileSize:this.maxFileSize
367
			,enableProgress:this.enableProgress
368
			,url:this.url
369
			,path:this.path
370
		};
371
		if(this.baseParams) {
372
			config.baseParams = this.baseParams;
373
		}
374
		this.uploader = new Ext.ux.FileUploader(config);
375
 
376
		// relay uploader events
377
		this.relayEvents(this.uploader, [
378
			 'beforeallstart'
379
			,'allfinished'
380
			,'progress'
381
		]);
382
 
383
		// install event handlers
384
		this.on({
385
			 beforeallstart:{scope:this, fn:function() {
386
			 	this.uploading = true;
387
				this.updateButtons();
388
			}}
389
			,allfinished:{scope:this, fn:function() {
390
				this.uploading = false;
391
				this.updateButtons();
392
			}}
393
			,progress:{fn:this.onProgress.createDelegate(this)}
394
		});
395
	} // eo function initComponent
396
	// }}}
397
	// {{{
398
	/**
399
	 * onRender override, saves references to buttons
400
	 * @private
401
	 */
402
	,onRender:function() {
403
		// call parent
404
		Ext.ux.UploadPanel.superclass.onRender.apply(this, arguments);
405
 
406
		// save useful references
407
		var tb = 'tbar' === this.buttonsAt ? this.getTopToolbar() : this.getBottomToolbar();
408
		this.addBtn = Ext.getCmp(tb.items.first().id);
409
		this.uploadBtn = Ext.getCmp(tb.items.itemAt(1).id);
410
		this.removeAllBtn = Ext.getCmp(tb.items.last().id);
411
	} // eo function onRender
412
	// }}}
413
 
414
	// added methods
415
	// {{{
416
	/**
417
	 * called by XTemplate to get qtip depending on state
418
	 * @private
419
	 * @param {Object} values XTemplate values
420
	 */
421
	,getQtip:function(values) {
422
		var qtip = '';
423
		switch(values.state) {
424
			case 'queued':
425
				qtip = String.format(this.fileQueuedText, values.fileName);
426
				qtip += '<br>' + this.clickRemoveText;
427
			break;
428
 
429
			case 'uploading':
430
				qtip = String.format(this.fileUploadingText, values.fileName);
431
				qtip += '<br>' + values.pctComplete + '% done';
432
				qtip += '<br>' + this.clickStopText;
433
			break;
434
 
435
			case 'done':
436
				qtip = String.format(this.fileDoneText, values.fileName);
437
				qtip += '<br>' + this.clickRemoveText;
438
			break;
439
 
440
			case 'failed':
441
				qtip = String.format(this.fileFailedText, values.fileName);
442
				qtip += '<br>' + this.errorText + ':' + values.error;
443
				qtip += '<br>' + this.clickRemoveText;
444
			break;
445
 
446
			case 'stopped':
447
				qtip = String.format(this.fileStoppedText, values.fileName);
448
				qtip += '<br>' + this.clickRemoveText;
449
			break;
450
		}
451
		return qtip;
452
	} // eo function getQtip
453
	// }}}
454
	// {{{
455
	/**
456
	 * get file name
457
	 * @private
458
	 * @param {Ext.Element} inp Input element containing the full file path
459
	 * @return {String}
460
	 */
461
	,getFileName:function(inp) {
462
		return inp.getValue().split(/[\/\\]/).pop();
463
	} // eo function getFileName
464
	// }}}
465
	// {{{
466
	/**
467
	 * get file path (excluding the file name)
468
	 * @private
469
	 * @param {Ext.Element} inp Input element containing the full file path
470
	 * @return {String}
471
	 */
472
	,getFilePath:function(inp) {
473
		return inp.getValue().replace(/[^\/\\]+$/,'');
474
	} // eo function getFilePath
475
	// }}}
476
	// {{{
477
	/**
478
	 * returns file class based on name extension
479
	 * @private
480
	 * @param {String} name File name to get class of
481
	 * @return {String} class to use for file type icon
482
	 */
483
	,getFileCls: function(name) {
484
		var atmp = name.split('.');
485
		if(1 === atmp.length) {
486
			return this.fileCls;
487
		}
488
		else {
489
			return this.fileCls + '-' + atmp.pop().toLowerCase();
490
		}
491
	}
492
	// }}}
493
	// {{{
494
	/**
495
	 * called when file is added - adds file to store
496
	 * @private
497
	 * @param {Ext.ux.BrowseButton}
498
	 */
499
	,onAddFile:function(bb) {
500
		if(true !== this.eventsSuspended && false === this.fireEvent('beforefileadd', this, bb.getInputFile())) {
501
			return;
502
		}
503
		var inp = bb.detachInputFile();
504
		inp.addClass('x-hidden');
505
		var fileName = this.getFileName(inp);
506
 
507
		// create new record and add it to store
508
		var rec = new this.store.recordType({
509
			 input:inp
510
			,fileName:fileName
511
			,filePath:this.getFilePath(inp)
512
			,shortName: Ext.util.Format.ellipsis(fileName, this.maxLength)
513
			,fileCls:this.getFileCls(fileName)
514
			,state:'queued'
515
		}, inp.id);
516
		rec.commit();
517
		this.store.add(rec);
518
 
519
		this.syncShadow();
520
 
521
		this.uploadBtn.enable();
522
		this.removeAllBtn.enable();
523
 
524
		if(true !== this.eventsSuspended) {
525
			this.fireEvent('fileadd', this, this.store, rec);
526
		}
527
 
528
	} // eo onAddFile
529
	// }}}
530
	// {{{
531
	/**
532
	 * destroys child components
533
	 * @private
534
	 */
535
	,onDestroy:function() {
536
 
537
		// destroy uploader
538
		if(this.uploader) {
539
			this.uploader.stopAll();
540
			this.uploader.purgeListeners();
541
			this.uploader = null;
542
		}
543
 
544
		// destroy view
545
		if(this.view) {
546
			this.view.purgeListeners();
547
			this.view.destroy();
548
			this.view = null;
549
		}
550
 
551
		// destroy store
552
		if(this.store) {
553
			this.store.purgeListeners();
554
			this.store.destroy();
555
			this.store = null;
556
		}
557
 
558
	} // eo function onDestroy
559
	// }}}
560
	// {{{
561
	/**
562
	 * progress event handler
563
	 * @private
564
	 * @param {Ext.ux.FileUploader} uploader
565
	 * @param {Object} data progress data
566
	 * @param {Ext.data.Record} record
567
	 */
568
	,onProgress:function(uploader, data, record) {
569
		var bytesTotal, bytesUploaded, pctComplete, state, idx, item, width, pgWidth;
570
		if(record) {
571
			state = record.get('state');
572
			bytesTotal = record.get('bytesTotal') || 1;
573
			bytesUploaded = record.get('bytesUploaded') || 0;
574
			if('uploading' === state) {
575
				pctComplete = Math.round(1000 * bytesUploaded/bytesTotal) / 10;
576
			}
577
			else if('done' === 'state') {
578
				pctComplete = 100;
579
			}
580
			else {
581
				pctComplete = 0;
582
			}
583
			record.set('pctComplete', pctComplete);
584
 
585
			idx = this.store.indexOf(record);
586
			item = Ext.get(this.view.getNode(idx));
587
			if(item) {
588
				width = item.getWidth();
589
				item.applyStyles({'background-position':width * pctComplete / 100 + 'px'});
590
			}
591
		}
592
	} // eo function onProgress
593
	// }}}
594
	// {{{
595
	/**
596
	 * called when file remove icon is clicked - performs the remove
597
	 * @private
598
	 * @param {Ext.data.Record}
599
	 */
600
	,onRemoveFile:function(record) {
601
		if(true !== this.eventsSuspended && false === this.fireEvent('beforefileremove', this, this.store, record)) {
602
			return;
603
		}
604
 
605
		// remove DOM elements
606
		var inp = record.get('input');
607
		var wrap = inp.up('em');
608
		inp.remove();
609
		if(wrap) {
610
			wrap.remove();
611
		}
612
 
613
		// remove record from store
614
		this.store.remove(record);
615
 
616
		var count = this.store.getCount();
617
		this.uploadBtn.setDisabled(!count);
618
		this.removeAllBtn.setDisabled(!count);
619
 
620
		if(true !== this.eventsSuspended) {
621
			this.fireEvent('fileremove', this, this.store);
622
			this.syncShadow();
623
		}
624
	} // eo function onRemoveFile
625
	// }}}
626
	// {{{
627
	/**
628
	 * Remove All/Stop All button click handler
629
	 * @private
630
	 */
631
	,onRemoveAllClick:function(btn) {
632
		if(true === this.uploading) {
633
			this.stopAll();
634
		}
635
		else {
636
			this.removeAll();
637
		}
638
	} // eo function onRemoveAllClick
639
 
640
	,stopAll:function() {
641
		this.uploader.stopAll();
642
	} // eo function stopAll
643
	// }}}
644
	// {{{
645
	/**
646
	 * DataView click handler
647
	 * @private
648
	 */
649
	,onViewClick:function(view, index, node, e) {
650
		var t = e.getTarget('div:any(.ux-up-icon-queued|.ux-up-icon-failed|.ux-up-icon-done|.ux-up-icon-stopped)');
651
		if(t) {
652
			this.onRemoveFile(this.store.getAt(index));
653
		}
654
		t = e.getTarget('div.ux-up-icon-uploading');
655
		if(t) {
656
			this.uploader.stopUpload(this.store.getAt(index));
657
		}
658
	} // eo function onViewClick
659
	// }}}
660
	// {{{
661
	/**
662
	 * tells uploader to upload
663
	 * @private
664
	 */
665
	,onUpload:function() {
666
		if(true !== this.eventsSuspended && false === this.fireEvent('beforeupload', this)) {
667
			return false;
668
		}
669
		this.uploader.upload();
670
	} // eo function onUpload
671
	// }}}
672
	// {{{
673
	/**
674
	 * url setter
675
	 */
676
	,setUrl:function(url) {
677
		this.url = url;
678
		this.uploader.setUrl(url);
679
	} // eo function setUrl
680
	// }}}
681
	// {{{
682
	/**
683
	 * path setter
684
	 */
685
	,setPath:function(path) {
686
		this.uploader.setPath(path);
687
	} // eo function setPath
688
	// }}}
689
	// {{{
690
	/**
691
	 * Updates buttons states depending on uploading state
692
	 * @private
693
	 */
694
	,updateButtons:function() {
695
		if(true === this.uploading) {
696
			this.addBtn.disable();
697
			this.uploadBtn.disable();
698
			this.removeAllBtn.setIconClass(this.stopIconCls);
699
			this.removeAllBtn.getEl().child(this.removeAllBtn.buttonSelector).dom[this.removeAllBtn.tooltipType] = this.stopAllText;
700
		}
701
		else {
702
			this.addBtn.enable();
703
			this.uploadBtn.enable();
704
			this.removeAllBtn.setIconClass(this.removeAllIconCls);
705
			this.removeAllBtn.getEl().child(this.removeAllBtn.buttonSelector).dom[this.removeAllBtn.tooltipType] = this.removeAllText;
706
		}
707
	} // eo function updateButtons
708
	// }}}
709
	// {{{
710
	/**
711
	 * Removes all files from store and destroys file inputs
712
	 */
713
	,removeAll:function() {
714
		var suspendState = this.eventsSuspended;
715
		if(false !== this.eventsSuspended && false === this.fireEvent('beforequeueclear', this, this.store)) {
716
			return false;
717
		}
718
		this.suspendEvents();
719
 
720
		this.store.each(this.onRemoveFile, this);
721
 
722
		this.eventsSuspended = suspendState;
723
		if(true !== this.eventsSuspended) {
724
			this.fireEvent('queueclear', this, this.store);
725
		}
726
		this.syncShadow();
727
	} // eo function removeAll
728
	// }}}
729
	// {{{
730
	/**
731
	 * synchronize context menu shadow if we're in contextmenu
732
	 * @private
733
	 */
734
	,syncShadow:function() {
735
		if(this.contextmenu && this.contextmenu.shadow) {
736
			this.contextmenu.getEl().shadow.show(this.contextmenu.getEl());
737
		}
738
	} // eo function syncShadow
739
	// }}}
740
 
741
}); // eo extend
742
 
743
// register xtype
744
Ext.reg('uploadpanel', Ext.ux.UploadPanel);
745
 
746
// eof