Source of js/Ext.ux.form.BrowseButton.js:
  1. Ext.namespace('Ext.ux.form');
  2.  
  3. /**
  4. * @class Ext.ux.form.BrowseButton
  5. * @extends Ext.Button
  6. * Ext.Button that provides a customizable file browse button.
  7. * Clicking this button, pops up a file dialog box for a user to select the file to upload.
  8. * This is accomplished by having a transparent <input type="file"> box above the Ext.Button.
  9. * When a user thinks he or she is clicking the Ext.Button, they're actually clicking the hidden input "Browse..." box.
  10. * Note: this class can be instantiated explicitly or with xtypes anywhere a regular Ext.Button can be except in 2 scenarios:
  11. * - Panel.addButton method both as an instantiated object or as an xtype config object.
  12. * - Panel.buttons config object as an xtype config object.
  13. * These scenarios fail because Ext explicitly creates an Ext.Button in these cases.
  14. * Browser compatibility:
  15. * Internet Explorer 6:
  16. * - no issues
  17. * Internet Explorer 7:
  18. * - no issues
  19. * Firefox 2 - Windows:
  20. * - pointer cursor doesn't display when hovering over the button.
  21. * Safari 3 - Windows:
  22. * - no issues.
  23. * @author loeppky - based on the work done by MaximGB in Ext.ux.UploadDialog (http://extjs.com/forum/showthread.php?t=21558)
  24. * The follow the curosr float div idea also came from MaximGB.
  25. * @see http://extjs.com/forum/showthread.php?t=29032
  26. * @constructor
  27. * Create a new BrowseButton.
  28. * @param {Object} config Configuration options
  29. */
  30. Ext.ux.form.BrowseButton = Ext.extend(Ext.Button, {
  31. /*
  32. * Config options:
  33. */
  34. /**
  35. * @cfg {String} inputFileName
  36. * Name to use for the hidden input file DOM element. Deaults to "file".
  37. */
  38. inputFileName: 'file',
  39. /**
  40. * @cfg {Boolean} debug
  41. * Toggle for turning on debug mode.
  42. * Debug mode doesn't make clipEl transparent so that one can see how effectively it covers the Ext.Button.
  43. * In addition, clipEl is given a green background and floatEl a red background to see how well they are positioned.
  44. */
  45. debug: false,
  46.  
  47.  
  48. /*
  49. * Private constants:
  50. */
  51. /**
  52. * @property FLOAT_EL_WIDTH
  53. * @type Number
  54. * The width (in pixels) of floatEl.
  55. * It should be less than the width of the IE "Browse" button's width (65 pixels), since IE doesn't let you resize it.
  56. * We define this width so we can quickly center floatEl at the mouse cursor without having to make any function calls.
  57. * @private
  58. */
  59. FLOAT_EL_WIDTH: 60,
  60.  
  61. /**
  62. * @property FLOAT_EL_HEIGHT
  63. * @type Number
  64. * The heigh (in pixels) of floatEl.
  65. * It should be less than the height of the "Browse" button's height.
  66. * We define this height so we can quickly center floatEl at the mouse cursor without having to make any function calls.
  67. * @private
  68. */
  69. FLOAT_EL_HEIGHT: 18,
  70.  
  71.  
  72. /*
  73. * Private properties:
  74. */
  75. /**
  76. * @property buttonCt
  77. * @type Ext.Element
  78. * Element that contains the actual Button DOM element.
  79. * We store a reference to it, so we can easily grab its size for sizing the clipEl.
  80. * @private
  81. */
  82. buttonCt: null,
  83. /**
  84. * @property clipEl
  85. * @type Ext.Element
  86. * Element that contains the floatEl.
  87. * This element is positioned to fill the area of Ext.Button and has overflow turned off.
  88. * This keeps floadEl tight to the Ext.Button, and prevents it from masking surrounding elements.
  89. * @private
  90. */
  91. clipEl: null,
  92. /**
  93. * @property floatEl
  94. * @type Ext.Element
  95. * Element that contains the inputFileEl.
  96. * This element is size to be less than or equal to the size of the input file "Browse" button.
  97. * It is then positioned wherever the user moves the cursor, so that their click always clicks the input file "Browse" button.
  98. * Overflow is turned off to preven inputFileEl from masking surrounding elements.
  99. * @private
  100. */
  101. floatEl: null,
  102. /**
  103. * @property inputFileEl
  104. * @type Ext.Element
  105. * Element for the hiden file input.
  106. * @private
  107. */
  108. inputFileEl: null,
  109. /**
  110. * @property originalHandler
  111. * @type Function
  112. * The handler originally defined for the Ext.Button during construction using the "handler" config option.
  113. * We need to null out the "handler" property so that it is only called when a file is selected.
  114. * @private
  115. */
  116. originalHandler: null,
  117. /**
  118. * @property originalScope
  119. * @type Object
  120. * The scope originally defined for the Ext.Button during construction using the "scope" config option.
  121. * While the "scope" property doesn't need to be nulled, to be consistent with originalHandler, we do.
  122. * @private
  123. */
  124. originalScope: null,
  125.  
  126.  
  127. /*
  128. * Protected Ext.Button overrides
  129. */
  130. /**
  131. * @see Ext.Button.initComponent
  132. */
  133. initComponent: function(){
  134. Ext.ux.form.BrowseButton.superclass.initComponent.call(this);
  135. // Store references to the original handler and scope before nulling them.
  136. // This is done so that this class can control when the handler is called.
  137. // There are some cases where the hidden file input browse button doesn't completely cover the Ext.Button.
  138. // The handler shouldn't be called in these cases. It should only be called if a new file is selected on the file system.
  139. this.originalHandler = this.handler;
  140. this.originalScope = this.scope;
  141. this.handler = null;
  142. this.scope = null;
  143. },
  144.  
  145. /**
  146. * @see Ext.Button.onRender
  147. */
  148. onRender: function(ct, position){
  149. Ext.ux.form.BrowseButton.superclass.onRender.call(this, ct, position); // render the Ext.Button
  150. this.buttonCt = this.el.child('.x-btn-center em');
  151. this.buttonCt.position('relative'); // this is important!
  152. var styleCfg = {
  153. position: 'absolute',
  154. overflow: 'hidden',
  155. top: '0px', // default
  156. left: '0px' // default
  157. };
  158. // browser specifics for better overlay tightness
  159. if (Ext.isIE) {
  160. Ext.apply(styleCfg, {
  161. left: '-3px',
  162. top: '-3px'
  163. });
  164. } else if (Ext.isGecko) {
  165. Ext.apply(styleCfg, {
  166. left: '-3px',
  167. top: '-3px'
  168. });
  169. } else if (Ext.isSafari) {
  170. Ext.apply(styleCfg, {
  171. left: '-4px',
  172. top: '-2px'
  173. });
  174. }
  175. this.clipEl = this.buttonCt.createChild({
  176. tag: 'div',
  177. style: styleCfg
  178. });
  179. this.setClipSize();
  180. this.clipEl.on({
  181. 'mousemove': this.onButtonMouseMove,
  182. 'mouseover': this.onButtonMouseMove,
  183. scope: this
  184. });
  185.  
  186. this.floatEl = this.clipEl.createChild({
  187. tag: 'div',
  188. style: {
  189. position: 'absolute',
  190. width: this.FLOAT_EL_WIDTH + 'px',
  191. height: this.FLOAT_EL_HEIGHT + 'px',
  192. overflow: 'hidden'
  193. }
  194. });
  195.  
  196.  
  197. if (this.debug) {
  198. this.clipEl.applyStyles({
  199. 'background-color': 'green'
  200. });
  201. this.floatEl.applyStyles({
  202. 'background-color': 'red'
  203. });
  204. } else {
  205. this.clipEl.setOpacity(0.0);
  206. }
  207.  
  208. // Cover cases where someone tabs to the button:
  209. // Listen to focus of the button so we can translate the focus to the input file el.
  210. var buttonEl = this.el.child(this.buttonSelector);
  211. buttonEl.on('focus', this.onButtonFocus, this);
  212. // In IE, it's possible to tab to the text portion of the input file el.
  213. // We want to listen to keyevents so that if a space is pressed, we "click" the input file el.
  214. if (Ext.isIE) {
  215. this.el.on('keydown', this.onButtonKeyDown, this);
  216. }
  217.  
  218. this.createInputFile();
  219. },
  220.  
  221.  
  222. /*
  223. * Private helper methods:
  224. */
  225. /**
  226. * Sets the size of clipEl so that is covering as much of the button as possible.
  227. * @private
  228. */
  229. setClipSize: function(){
  230. if (this.clipEl) {
  231. var width = this.buttonCt.getWidth();
  232. var height = this.buttonCt.getHeight();
  233. // The button container can have a width and height of zero when it's rendered in a hidden panel.
  234. // This is most noticable when using a card layout, as the items are all rendered but hidden,
  235. // (unless deferredRender is set to true).
  236. // In this case, the clip size can't be determined, so we attempt to set it later.
  237. // This check repeats until the button container has a size.
  238. if (width === 0 || height === 0) {
  239. this.setClipSize.defer(100, this);
  240. } else {
  241. if (Ext.isIE) {
  242. width = width + 5;
  243. height = height + 5;
  244. } else if (Ext.isGecko) {
  245. width = width + 6;
  246. height = height + 6;
  247. } else if (Ext.isSafari) {
  248. width = width + 6;
  249. height = height + 6;
  250. }
  251. this.clipEl.setSize(width, height);
  252. }
  253. }
  254. },
  255.  
  256. /**
  257. * Creates the input file element and adds it to inputFileCt.
  258. * The created input file elementis sized, positioned, and styled appropriately.
  259. * Event handlers for the element are set up, and a tooltip is applied if defined in the original config.
  260. * @private
  261. */
  262. createInputFile: function(){
  263. // When an input file gets detached and set as the child of a different DOM element,
  264. // straggling <em> elements get left behind.
  265. // I don't know why this happens but we delete any <em> elements we can find under the floatEl to prevent a memory leak.
  266. this.floatEl.select('em').each(function(el){
  267. el.remove();
  268. });
  269. this.inputFileEl = this.floatEl.createChild({
  270. tag: 'input',
  271. type: 'file',
  272. size: 1, // must be > 0. It's value doesn't really matter due to our masking div (inputFileCt).
  273. name: this.inputFileName || Ext.id(this.el),
  274. tabindex: this.tabIndex,
  275. // Use the same pointer as an Ext.Button would use. This doesn't work in Firefox.
  276. // This positioning right-aligns the input file to ensure that the "Browse" button is visible.
  277. style: {
  278. position: 'absolute',
  279. cursor: 'pointer',
  280. right: '0px',
  281. top: '0px'
  282. }
  283. });
  284. this.inputFileEl = this.inputFileEl.child('input') || this.inputFileEl;
  285.  
  286. // setup events
  287. this.inputFileEl.on({
  288. 'click': this.onInputFileClick,
  289. 'change': this.onInputFileChange,
  290. 'focus': this.onInputFileFocus,
  291. 'select': this.onInputFileFocus,
  292. 'blur': this.onInputFileBlur,
  293. scope: this
  294. });
  295.  
  296. // add a tooltip
  297. if (this.tooltip) {
  298. if (typeof this.tooltip == 'object') {
  299. Ext.QuickTips.register(Ext.apply({
  300. target: this.inputFileEl
  301. }, this.tooltip));
  302. } else {
  303. this.inputFileEl.dom[this.tooltipType] = this.tooltip;
  304. }
  305. }
  306. },
  307.  
  308. /**
  309. * Redirecting focus to the input file element so the user can press space and select files.
  310. * @param {Event} e focus event.
  311. * @private
  312. */
  313. onButtonFocus: function(e){
  314. if (this.inputFileEl) {
  315. this.inputFileEl.focus();
  316. e.stopEvent();
  317. }
  318. },
  319.  
  320. /**
  321. * Handler for the IE case where once can tab to the text box of an input file el.
  322. * If the key is a space, we simply "click" the inputFileEl.
  323. * @param {Event} e key event.
  324. * @private
  325. */
  326. onButtonKeyDown: function(e){
  327. if (this.inputFileEl && e.getKey() == Ext.EventObject.SPACE) {
  328. this.inputFileEl.dom.click();
  329. e.stopEvent();
  330. }
  331. },
  332.  
  333. /**
  334. * Handler when the cursor moves over the clipEl.
  335. * The floatEl gets centered to the cursor location.
  336. * @param {Event} e mouse event.
  337. * @private
  338. */
  339. onButtonMouseMove: function(e){
  340. var xy = e.getXY();
  341. xy[0] -= this.FLOAT_EL_WIDTH / 2;
  342. xy[1] -= this.FLOAT_EL_HEIGHT / 2;
  343. this.floatEl.setXY(xy);
  344. },
  345.  
  346. /**
  347. * Add the visual enhancement to the button when the input file recieves focus.
  348. * This is the tip for the user that now he/she can press space to select the file.
  349. * @private
  350. */
  351. onInputFileFocus: function(e){
  352. if (!this.isDisabled) {
  353. this.el.addClass("x-btn-over");
  354. }
  355. },
  356.  
  357. /**
  358. * Removes the visual enhancement from the button.
  359. * @private
  360. */
  361. onInputFileBlur: function(e){
  362. this.el.removeClass("x-btn-over");
  363. },
  364.  
  365. /**
  366. * Handler when inputFileEl's "Browse..." button is clicked.
  367. * @param {Event} e click event.
  368. * @private
  369. */
  370. onInputFileClick: function(e){
  371. e.stopPropagation();
  372. },
  373.  
  374. /**
  375. * Handler when inputFileEl changes value (i.e. a new file is selected).
  376. * @private
  377. */
  378. onInputFileChange: function(){
  379. if (this.originalHandler) {
  380. this.originalHandler.call(this.originalScope, this);
  381. }
  382. },
  383.  
  384.  
  385. /*
  386. * Public methods:
  387. */
  388. /**
  389. * Detaches the input file associated with this BrowseButton so that it can be used for other purposed (e.g. uplaoding).
  390. * The returned input file has all listeners and tooltips applied to it by this class removed.
  391. * @param {Boolean} whether to create a new input file element for this BrowseButton after detaching.
  392. * True will prevent creation. Defaults to false.
  393. * @return {Ext.Element} the detached input file element.
  394. */
  395. detachInputFile: function(noCreate){
  396. var result = this.inputFileEl;
  397.  
  398. if (typeof this.tooltip == 'object') {
  399. Ext.QuickTips.unregister(this.inputFileEl);
  400. } else {
  401. this.inputFileEl.dom[this.tooltipType] = null;
  402. }
  403. this.inputFileEl.removeAllListeners();
  404. this.inputFileEl = null;
  405.  
  406. if (!noCreate) {
  407. this.createInputFile();
  408. }
  409. return result;
  410. },
  411.  
  412. /**
  413. * @return {Ext.Element} the input file element attached to this BrowseButton.
  414. */
  415. getInputFile: function(){
  416. return this.inputFileEl;
  417. },
  418.  
  419. /**
  420. * @see Ext.Button.disable
  421. */
  422. disable: function(){
  423. Ext.ux.form.BrowseButton.superclass.disable.call(this);
  424. this.inputFileEl.dom.disabled = true;
  425. },
  426.  
  427. /**
  428. * @see Ext.Button.enable
  429. */
  430. enable: function(){
  431. Ext.ux.form.BrowseButton.superclass.enable.call(this);
  432. this.inputFileEl.dom.disabled = false;
  433. }
  434. });
  435.  
  436. Ext.reg('browsebutton', Ext.ux.form.BrowseButton);
  437.