Source of js/Ext.ux.FileUploader.js:
  1. // vim: ts=4:sw=4:nu:fdc=4:nospell
  2. /**
  3. * Ext.ux.FileUploader
  4. *
  5. * @author Ing. Jozef Sakáloš
  6. * @version $Id: Ext.ux.FileUploader.js 302 2008-08-03 20:57:33Z jozo $
  7. * @date 15. March 2008
  8. *
  9. * @license Ext.ux.FileUploader 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.FileUploader
  21. * @extends Ext.util.Observable
  22. * @constructor
  23. */
  24. Ext.ux.FileUploader = function(config) {
  25. Ext.apply(this, config);
  26.  
  27. // call parent
  28. Ext.ux.FileUploader.superclass.constructor.apply(this, arguments);
  29.  
  30. // add events
  31. // {{{
  32. this.addEvents(
  33. /**
  34. * @event beforeallstart
  35. * Fires before an upload (of all files) is started. Return false to cancel the event.
  36. * @param {Ext.ux.FileUploader} this
  37. */
  38. 'beforeallstart'
  39. /**
  40. * @event allfinished
  41. * Fires after upload (of all files) is finished
  42. * @param {Ext.ux.FileUploader} this
  43. */
  44. ,'allfinished'
  45. /**
  46. * @event beforefilestart
  47. * Fires before the file upload is started. Return false to cancel the event.
  48. * Fires only when singleUpload = false
  49. * @param {Ext.ux.FileUploader} this
  50. * @param {Ext.data.Record} record upload of which is being started
  51. */
  52. ,'beforefilestart'
  53. /**
  54. * @event filefinished
  55. * Fires when file finished uploading.
  56. * Fires only when singleUpload = false
  57. * @param {Ext.ux.FileUploader} this
  58. * @param {Ext.data.Record} record upload of which has finished
  59. */
  60. ,'filefinished'
  61. /**
  62. * @event progress
  63. * Fires when progress has been updated
  64. * @param {Ext.ux.FileUploader} this
  65. * @param {Object} data Progress data object
  66. * @param {Ext.data.Record} record Only if singleUpload = false
  67. */
  68. ,'progress'
  69. );
  70. // }}}
  71.  
  72. }; // eo constructor
  73.  
  74. Ext.extend(Ext.ux.FileUploader, Ext.util.Observable, {
  75.  
  76. // configuration options
  77. // {{{
  78. /**
  79. * @cfg {Object} baseParams baseParams are sent to server in each request.
  80. */
  81. baseParams:{cmd:'upload',dir:'.'}
  82.  
  83. /**
  84. * @cfg {Boolean} concurrent true to start all requests upon upload start, false to start
  85. * the next request only if previous one has been completed (or failed). Applicable only if
  86. * singleUpload = false
  87. */
  88. ,concurrent:true
  89.  
  90. /**
  91. * @cfg {Boolean} enableProgress true to enable querying server for progress information
  92. */
  93. ,enableProgress:true
  94.  
  95. /**
  96. * @cfg {String} jsonErrorText Text to use for json error
  97. */
  98. ,jsonErrorText:'Cannot decode JSON object'
  99.  
  100. /**
  101. * @cfg {Number} Maximum client file size in bytes
  102. */
  103. ,maxFileSize:524288
  104.  
  105. /**
  106. * @cfg {String} progressIdName Name to give hidden field for upload progress identificator
  107. */
  108. ,progressIdName:'UPLOAD_IDENTIFIER'
  109.  
  110. /**
  111. * @cfg {Number} progressInterval How often (in ms) is progress requested from server
  112. */
  113. ,progressInterval:2000
  114.  
  115. /**
  116. * @cfg {String} progressUrl URL to request upload progress from
  117. */
  118. ,progressUrl:'progress.php'
  119.  
  120. /**
  121. * @cfg {Object} progressMap Mapping of received progress fields to store progress fields
  122. */
  123. ,progressMap:{
  124. bytes_total:'bytesTotal'
  125. ,bytes_uploaded:'bytesUploaded'
  126. ,est_sec:'estSec'
  127. ,files_uploaded:'filesUploaded'
  128. ,speed_average:'speedAverage'
  129. ,speed_last:'speedLast'
  130. ,time_last:'timeLast'
  131. ,time_start:'timeStart'
  132. }
  133. /**
  134. * @cfg {Boolean} singleUpload true to upload files in one form, false to upload one by one
  135. */
  136. ,singleUpload:false
  137.  
  138. /**
  139. * @cfg {Ext.data.Store} store Mandatory. Store that holds files to upload
  140. */
  141.  
  142. /**
  143. * @cfg {String} unknownErrorText Text to use for unknow error
  144. */
  145. ,unknownErrorText:'Unknown error'
  146.  
  147. /**
  148. * @cfg {String} url Mandatory. URL to upload to
  149. */
  150.  
  151. // }}}
  152.  
  153. // private
  154. // {{{
  155. /**
  156. * uploads in progress count
  157. * @private
  158. */
  159. ,upCount:0
  160. // }}}
  161.  
  162. // methods
  163. // {{{
  164. /**
  165. * creates form to use for upload.
  166. * @private
  167. * @return {Ext.Element} form
  168. */
  169. ,createForm:function(record) {
  170. var progressId = parseInt(Math.random() * 1e10, 10);
  171. var form = Ext.getBody().createChild({
  172. tag:'form'
  173. ,action:this.url
  174. ,method:'post'
  175. ,cls:'x-hidden'
  176. ,id:Ext.id()
  177. ,cn:[{
  178. tag:'input'
  179. ,type:'hidden'
  180. ,name:'APC_UPLOAD_PROGRESS'
  181. ,value:progressId
  182. },{
  183. tag:'input'
  184. ,type:'hidden'
  185. ,name:this.progressIdName
  186. ,value:progressId
  187. },{
  188. tag:'input'
  189. ,type:'hidden'
  190. ,name:'MAX_FILE_SIZE'
  191. ,value:this.maxFileSize
  192. }]
  193. });
  194. if(record) {
  195. record.set('form', form);
  196. record.set('progressId', progressId);
  197. }
  198. else {
  199. this.progressId = progressId;
  200. }
  201. return form;
  202.  
  203. } // eo function createForm
  204. // }}}
  205. // {{{
  206. ,deleteForm:function(form, record) {
  207. form.remove();
  208. if(record) {
  209. record.set('form', null);
  210. }
  211. } // eo function deleteForm
  212. // }}}
  213. // {{{
  214. /**
  215. * Fires event(s) on upload finish/error
  216. * @private
  217. */
  218. ,fireFinishEvents:function(options) {
  219. if(true !== this.eventsSuspended && !this.singleUpload) {
  220. this.fireEvent('filefinished', this, options && options.record);
  221. }
  222. if(true !== this.eventsSuspended && 0 === this.upCount) {
  223. this.stopProgress();
  224. this.fireEvent('allfinished', this);
  225. }
  226. } // eo function fireFinishEvents
  227. // }}}
  228. // {{{
  229. /**
  230. * Geg the iframe identified by record
  231. * @private
  232. * @param {Ext.data.Record} record
  233. * @return {Ext.Element} iframe or null if not found
  234. */
  235. ,getIframe:function(record) {
  236. var iframe = null;
  237. var form = record.get('form');
  238. if(form && form.dom && form.dom.target) {
  239. iframe = Ext.get(form.dom.target);
  240. }
  241. return iframe;
  242. } // eo function getIframe
  243. // }}}
  244. // {{{
  245. /**
  246. * returns options for Ajax upload request
  247. * @private
  248. * @param {Ext.data.Record} record
  249. * @param {Object} params params to add
  250. */
  251. ,getOptions:function(record, params) {
  252. var o = {
  253. url:this.url
  254. ,method:'post'
  255. ,isUpload:true
  256. ,scope:this
  257. ,callback:this.uploadCallback
  258. ,record:record
  259. ,params:this.getParams(record, params)
  260. };
  261. return o;
  262. } // eo function getOptions
  263. // }}}
  264. // {{{
  265. /**
  266. * get params to use for request
  267. * @private
  268. * @return {Object} params
  269. */
  270. ,getParams:function(record, params) {
  271. var p = {path:this.path};
  272. Ext.apply(p, this.baseParams || {}, params || {});
  273. return p;
  274. }
  275. // }}}
  276. // {{{
  277. /**
  278. * processes success response
  279. * @private
  280. * @param {Object} options options the request was called with
  281. * @param {Object} response request response object
  282. * @param {Object} o decoded response.responseText
  283. */
  284. ,processSuccess:function(options, response, o) {
  285. var record = false;
  286.  
  287. // all files uploadded ok
  288. if(this.singleUpload) {
  289. this.store.each(function(r) {
  290. r.set('state', 'done');
  291. r.set('error', '');
  292. r.commit();
  293. });
  294. }
  295. else {
  296. record = options.record;
  297. record.set('state', 'done');
  298. record.set('error', '');
  299. record.commit();
  300. }
  301.  
  302. this.deleteForm(options.form, record);
  303.  
  304. } // eo processSuccess
  305. // }}}
  306. // {{{
  307. /**
  308. * processes failure response
  309. * @private
  310. * @param {Object} options options the request was called with
  311. * @param {Object} response request response object
  312. * @param {String/Object} error Error text or JSON decoded object. Optional.
  313. */
  314. ,processFailure:function(options, response, error) {
  315. var record = options.record;
  316. var records;
  317.  
  318. // singleUpload - all files uploaded in one form
  319. if(this.singleUpload) {
  320. // some files may have been successful
  321. records = this.store.queryBy(function(r){
  322. var state = r.get('state');
  323. return 'done' !== state && 'uploading' !== state;
  324. });
  325. records.each(function(record) {
  326. var e = error.errors ? error.errors[record.id] : this.unknownErrorText;
  327. if(e) {
  328. record.set('state', 'failed');
  329. record.set('error', e);
  330. Ext.getBody().appendChild(record.get('input'));
  331. }
  332. else {
  333. record.set('state', 'done');
  334. record.set('error', '');
  335. }
  336. record.commit();
  337. }, this);
  338.  
  339. this.deleteForm(options.form);
  340. }
  341. // multipleUpload - each file uploaded in it's own form
  342. else {
  343. if(error && 'object' === Ext.type(error)) {
  344. record.set('error', error.errors && error.errors[record.id] ? error.errors[record.id] : this.unknownErrorText);
  345. }
  346. else if(error) {
  347. record.set('error', error);
  348. }
  349. else if(response && response.responseText) {
  350. record.set('error', response.responseText);
  351. }
  352. else {
  353. record.set('error', this.unknownErrorText);
  354. }
  355. record.set('state', 'failed');
  356. record.commit();
  357. }
  358. } // eof processFailure
  359. // }}}
  360. // {{{
  361. /**
  362. * Delayed task callback
  363. */
  364. ,requestProgress:function() {
  365. var records, p;
  366. var o = {
  367. url:this.progressUrl
  368. ,method:'post'
  369. ,params:{}
  370. ,scope:this
  371. ,callback:function(options, success, response) {
  372. var o;
  373. if(true !== success) {
  374. return;
  375. }
  376. try {
  377. o = Ext.decode(response.responseText);
  378. }
  379. catch(e) {
  380. return;
  381. }
  382. if('object' !== Ext.type(o) || true !== o.success) {
  383. return;
  384. }
  385.  
  386. if(this.singleUpload) {
  387. this.progress = {};
  388. for(p in o) {
  389. if(this.progressMap[p]) {
  390. this.progress[this.progressMap[p]] = parseInt(o[p], 10);
  391. }
  392. }
  393. if(true !== this.eventsSuspended) {
  394. this.fireEvent('progress', this, this.progress);
  395. }
  396.  
  397. }
  398. else {
  399. for(p in o) {
  400. if(this.progressMap[p] && options.record) {
  401. options.record.set(this.progressMap[p], parseInt(o[p], 10));
  402. }
  403. }
  404. if(options.record) {
  405. options.record.commit();
  406. if(true !== this.eventsSuspended) {
  407. this.fireEvent('progress', this, options.record.data, options.record);
  408. }
  409. }
  410. }
  411. this.progressTask.delay(this.progressInterval);
  412. }
  413. };
  414. if(this.singleUpload) {
  415. o.params[this.progressIdName] = this.progressId;
  416. o.params.APC_UPLOAD_PROGRESS = this.progressId;
  417. Ext.Ajax.request(o);
  418. }
  419. else {
  420. records = this.store.query('state', 'uploading');
  421. records.each(function(r) {
  422. o.params[this.progressIdName] = r.get('progressId');
  423. o.params.APC_UPLOAD_PROGRESS = o.params[this.progressIdName];
  424. o.record = r;
  425. (function() {
  426. Ext.Ajax.request(o);
  427. }).defer(250);
  428. }, this);
  429. }
  430. } // eo function requestProgress
  431. // }}}
  432. // {{{
  433. /**
  434. * path setter
  435. * @private
  436. */
  437. ,setPath:function(path) {
  438. this.path = path;
  439. } // eo setPath
  440. // }}}
  441. // {{{
  442. /**
  443. * url setter
  444. * @private
  445. */
  446. ,setUrl:function(url) {
  447. this.url = url;
  448. } // eo setUrl
  449. // }}}
  450. // {{{
  451. /**
  452. * Starts progress fetching from server
  453. * @private
  454. */
  455. ,startProgress:function() {
  456. if(!this.progressTask) {
  457. this.progressTask = new Ext.util.DelayedTask(this.requestProgress, this);
  458. }
  459. this.progressTask.delay.defer(this.progressInterval / 2, this.progressTask, [this.progressInterval]);
  460. } // eo function startProgress
  461. // }}}
  462. // {{{
  463. /**
  464. * Stops progress fetching from server
  465. * @private
  466. */
  467. ,stopProgress:function() {
  468. if(this.progressTask) {
  469. this.progressTask.cancel();
  470. }
  471. } // eo function stopProgress
  472. // }}}
  473. // {{{
  474. /**
  475. * Stops all currently running uploads
  476. */
  477. ,stopAll:function() {
  478. var records = this.store.query('state', 'uploading');
  479. records.each(this.stopUpload, this);
  480. } // eo function stopAll
  481. // }}}
  482. // {{{
  483. /**
  484. * Stops currently running upload
  485. * @param {Ext.data.Record} record Optional, if not set singleUpload = true is assumed
  486. * and the global stop is initiated
  487. */
  488. ,stopUpload:function(record) {
  489. // single abord
  490. var iframe = false;
  491. if(record) {
  492. iframe = this.getIframe(record);
  493. this.stopIframe(iframe);
  494. this.upCount--;
  495. this.upCount = 0 > this.upCount ? 0 : this.upCount;
  496. record.set('state', 'stopped');
  497. this.fireFinishEvents({record:record});
  498. }
  499. // all abort
  500. else if(this.form) {
  501. iframe = Ext.fly(this.form.dom.target);
  502. this.stopIframe(iframe);
  503. this.upCount = 0;
  504. this.fireFinishEvents();
  505. }
  506.  
  507. } // eo function abortUpload
  508. // }}}
  509. // {{{
  510. /**
  511. * Stops uploading in hidden iframe
  512. * @private
  513. * @param {Ext.Element} iframe
  514. */
  515. ,stopIframe:function(iframe) {
  516. if(iframe) {
  517. try {
  518. iframe.dom.contentWindow.stop();
  519. iframe.remove.defer(250, iframe);
  520. }
  521. catch(e){}
  522. }
  523. } // eo function stopIframe
  524. // }}}
  525. // {{{
  526. /**
  527. * Main public interface function. Preforms the upload
  528. */
  529. ,upload:function() {
  530.  
  531. var records = this.store.queryBy(function(r){return 'done' !== r.get('state');});
  532. if(!records.getCount()) {
  533. return;
  534. }
  535.  
  536. // fire beforeallstart event
  537. if(true !== this.eventsSuspended && false === this.fireEvent('beforeallstart', this)) {
  538. return;
  539. }
  540. if(this.singleUpload) {
  541. this.uploadSingle();
  542. }
  543. else {
  544. records.each(this.uploadFile, this);
  545. }
  546.  
  547. if(true === this.enableProgress) {
  548. this.startProgress();
  549. }
  550.  
  551. } // eo function upload
  552. // }}}
  553. // {{{
  554. /**
  555. * called for both success and failure. Does nearly nothing
  556. * @private
  557. * but dispatches processing to processSuccess and processFailure functions
  558. */
  559. ,uploadCallback:function(options, success, response) {
  560.  
  561. var o;
  562. this.upCount--;
  563. this.form = false;
  564.  
  565. // process ajax success
  566. if(true === success) {
  567. try {
  568. o = Ext.decode(response.responseText);
  569. }
  570. catch(e) {
  571. this.processFailure(options, response, this.jsonErrorText);
  572. this.fireFinishEvents(options);
  573. return;
  574. }
  575. // process command success
  576. if(true === o.success) {
  577. this.processSuccess(options, response, o);
  578. }
  579. // process command failure
  580. else {
  581. this.processFailure(options, response, o);
  582. }
  583. }
  584. // process ajax failure
  585. else {
  586. this.processFailure(options, response);
  587. }
  588.  
  589. this.fireFinishEvents(options);
  590.  
  591. } // eo function uploadCallback
  592. // }}}
  593. // {{{
  594. /**
  595. * Uploads one file
  596. * @param {Ext.data.Record} record
  597. * @param {Object} params Optional. Additional params to use in request.
  598. */
  599. ,uploadFile:function(record, params) {
  600. // fire beforestart event
  601. if(true !== this.eventsSuspended && false === this.fireEvent('beforefilestart', this, record)) {
  602. return;
  603. }
  604.  
  605. // create form for upload
  606. var form = this.createForm(record);
  607.  
  608. // append input to the form
  609. var inp = record.get('input');
  610. inp.set({name:inp.id});
  611. form.appendChild(inp);
  612.  
  613. // get params for request
  614. var o = this.getOptions(record, params);
  615. o.form = form;
  616.  
  617. // set state
  618. record.set('state', 'uploading');
  619. record.set('pctComplete', 0);
  620.  
  621. // increment active uploads count
  622. this.upCount++;
  623.  
  624. // request upload
  625. Ext.Ajax.request(o);
  626.  
  627. // todo:delete after devel
  628. this.getIframe.defer(100, this, [record]);
  629.  
  630. } // eo function uploadFile
  631. // }}}
  632. // {{{
  633. /**
  634. * Uploads all files in single request
  635. */
  636. ,uploadSingle:function() {
  637.  
  638. // get records to upload
  639. var records = this.store.queryBy(function(r){return 'done' !== r.get('state');});
  640. if(!records.getCount()) {
  641. return;
  642. }
  643.  
  644. // create form and append inputs to it
  645. var form = this.createForm();
  646. records.each(function(record) {
  647. var inp = record.get('input');
  648. inp.set({name:inp.id});
  649. form.appendChild(inp);
  650. record.set('state', 'uploading');
  651. }, this);
  652.  
  653. // create options for request
  654. var o = this.getOptions();
  655. o.form = form;
  656.  
  657. // save form for stop
  658. this.form = form;
  659.  
  660. // increment active uploads counter
  661. this.upCount++;
  662.  
  663. // request upload
  664. Ext.Ajax.request(o);
  665.  
  666. } // eo function uploadSingle
  667. // }}}
  668.  
  669. }); // eo extend
  670.  
  671. // register xtype
  672. Ext.reg('fileuploader', Ext.ux.FileUploader);
  673.  
  674. // eof