Source of js/Ext.ux.UploadPanel.js:
  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 310 2008-08-14 17:23:48Z 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. this.doLayout();
  529.  
  530. } // eo onAddFile
  531. // }}}
  532. // {{{
  533. /**
  534. * destroys child components
  535. * @private
  536. */
  537. ,onDestroy:function() {
  538.  
  539. // destroy uploader
  540. if(this.uploader) {
  541. this.uploader.stopAll();
  542. this.uploader.purgeListeners();
  543. this.uploader = null;
  544. }
  545.  
  546. // destroy view
  547. if(this.view) {
  548. this.view.purgeListeners();
  549. this.view.destroy();
  550. this.view = null;
  551. }
  552.  
  553. // destroy store
  554. if(this.store) {
  555. this.store.purgeListeners();
  556. this.store.destroy();
  557. this.store = null;
  558. }
  559.  
  560. } // eo function onDestroy
  561. // }}}
  562. // {{{
  563. /**
  564. * progress event handler
  565. * @private
  566. * @param {Ext.ux.FileUploader} uploader
  567. * @param {Object} data progress data
  568. * @param {Ext.data.Record} record
  569. */
  570. ,onProgress:function(uploader, data, record) {
  571. var bytesTotal, bytesUploaded, pctComplete, state, idx, item, width, pgWidth;
  572. if(record) {
  573. state = record.get('state');
  574. bytesTotal = record.get('bytesTotal') || 1;
  575. bytesUploaded = record.get('bytesUploaded') || 0;
  576. if('uploading' === state) {
  577. pctComplete = Math.round(1000 * bytesUploaded/bytesTotal) / 10;
  578. }
  579. else if('done' === state) {
  580. pctComplete = 100;
  581. }
  582. else {
  583. pctComplete = 0;
  584. }
  585. record.set('pctComplete', pctComplete);
  586.  
  587. idx = this.store.indexOf(record);
  588. item = Ext.get(this.view.getNode(idx));
  589. if(item) {
  590. width = item.getWidth();
  591. item.applyStyles({'background-position':width * pctComplete / 100 + 'px'});
  592. }
  593. }
  594. } // eo function onProgress
  595. // }}}
  596. // {{{
  597. /**
  598. * called when file remove icon is clicked - performs the remove
  599. * @private
  600. * @param {Ext.data.Record}
  601. */
  602. ,onRemoveFile:function(record) {
  603. if(true !== this.eventsSuspended && false === this.fireEvent('beforefileremove', this, this.store, record)) {
  604. return;
  605. }
  606.  
  607. // remove DOM elements
  608. var inp = record.get('input');
  609. var wrap = inp.up('em');
  610. inp.remove();
  611. if(wrap) {
  612. wrap.remove();
  613. }
  614.  
  615. // remove record from store
  616. this.store.remove(record);
  617.  
  618. var count = this.store.getCount();
  619. this.uploadBtn.setDisabled(!count);
  620. this.removeAllBtn.setDisabled(!count);
  621.  
  622. if(true !== this.eventsSuspended) {
  623. this.fireEvent('fileremove', this, this.store);
  624. this.syncShadow();
  625. }
  626. } // eo function onRemoveFile
  627. // }}}
  628. // {{{
  629. /**
  630. * Remove All/Stop All button click handler
  631. * @private
  632. */
  633. ,onRemoveAllClick:function(btn) {
  634. if(true === this.uploading) {
  635. this.stopAll();
  636. }
  637. else {
  638. this.removeAll();
  639. }
  640. } // eo function onRemoveAllClick
  641.  
  642. ,stopAll:function() {
  643. this.uploader.stopAll();
  644. } // eo function stopAll
  645. // }}}
  646. // {{{
  647. /**
  648. * DataView click handler
  649. * @private
  650. */
  651. ,onViewClick:function(view, index, node, e) {
  652. var t = e.getTarget('div:any(.ux-up-icon-queued|.ux-up-icon-failed|.ux-up-icon-done|.ux-up-icon-stopped)');
  653. if(t) {
  654. this.onRemoveFile(this.store.getAt(index));
  655. }
  656. t = e.getTarget('div.ux-up-icon-uploading');
  657. if(t) {
  658. this.uploader.stopUpload(this.store.getAt(index));
  659. }
  660. } // eo function onViewClick
  661. // }}}
  662. // {{{
  663. /**
  664. * tells uploader to upload
  665. * @private
  666. */
  667. ,onUpload:function() {
  668. if(true !== this.eventsSuspended && false === this.fireEvent('beforeupload', this)) {
  669. return false;
  670. }
  671. this.uploader.upload();
  672. } // eo function onUpload
  673. // }}}
  674. // {{{
  675. /**
  676. * url setter
  677. */
  678. ,setUrl:function(url) {
  679. this.url = url;
  680. this.uploader.setUrl(url);
  681. } // eo function setUrl
  682. // }}}
  683. // {{{
  684. /**
  685. * path setter
  686. */
  687. ,setPath:function(path) {
  688. this.uploader.setPath(path);
  689. } // eo function setPath
  690. // }}}
  691. // {{{
  692. /**
  693. * Updates buttons states depending on uploading state
  694. * @private
  695. */
  696. ,updateButtons:function() {
  697. if(true === this.uploading) {
  698. this.addBtn.disable();
  699. this.uploadBtn.disable();
  700. this.removeAllBtn.setIconClass(this.stopIconCls);
  701. this.removeAllBtn.getEl().child(this.removeAllBtn.buttonSelector).dom[this.removeAllBtn.tooltipType] = this.stopAllText;
  702. }
  703. else {
  704. this.addBtn.enable();
  705. this.uploadBtn.enable();
  706. this.removeAllBtn.setIconClass(this.removeAllIconCls);
  707. this.removeAllBtn.getEl().child(this.removeAllBtn.buttonSelector).dom[this.removeAllBtn.tooltipType] = this.removeAllText;
  708. }
  709. } // eo function updateButtons
  710. // }}}
  711. // {{{
  712. /**
  713. * Removes all files from store and destroys file inputs
  714. */
  715. ,removeAll:function() {
  716. var suspendState = this.eventsSuspended;
  717. if(false !== this.eventsSuspended && false === this.fireEvent('beforequeueclear', this, this.store)) {
  718. return false;
  719. }
  720. this.suspendEvents();
  721.  
  722. this.store.each(this.onRemoveFile, this);
  723.  
  724. this.eventsSuspended = suspendState;
  725. if(true !== this.eventsSuspended) {
  726. this.fireEvent('queueclear', this, this.store);
  727. }
  728. this.syncShadow();
  729. } // eo function removeAll
  730. // }}}
  731. // {{{
  732. /**
  733. * synchronize context menu shadow if we're in contextmenu
  734. * @private
  735. */
  736. ,syncShadow:function() {
  737. if(this.contextmenu && this.contextmenu.shadow) {
  738. this.contextmenu.getEl().shadow.show(this.contextmenu.getEl());
  739. }
  740. } // eo function syncShadow
  741. // }}}
  742.  
  743. }); // eo extend
  744.  
  745. // register xtype
  746. Ext.reg('uploadpanel', Ext.ux.UploadPanel);
  747.  
  748. // eof