/** * */Ext.define('Ext.form.field.FileButton', { extend: 'Ext.button.Button', alias: 'widget.filebutton', childEls: [ 'fileInputEl' ], inputCls: Ext.baseCSSPrefix + 'form-file-input', cls: Ext.baseCSSPrefix + 'form-file-btn', preventDefault: false, // Button element *looks* focused but it should never really receive focus itself, // and with it being a <div></div> we don't need to render tabindex attribute at all tabIndex: undefined, // IE and Edge implement File input as two elements: text box and a button, // both are focusable and have a tab stop. Since we make file input transparent, // this results in users having to press Tab key twice with no visible action // just to go past our File input widget. There is no way to configure this behavior. // The workaround is as follows: we place two tabbable elements around the file input, // and forward the focus to the file input element whenever either guard is tabbed // into. We also intercept Tab keydown events on the file input, and fudge focus // before keyup so that when default action happens the focus will go outside // of the widget just like it should. // This mechanism is quite similar to what we use in Window component for trapping // focus, and in floating mixin to allow tabbing out of the floater. useTabGuards: Ext.isIE || Ext.isEdge, promptCalled: false, autoEl: { tag: 'div', unselectable: 'on' }, /* * This <input type="file"/> element is placed above the button element to intercept * mouse clicks, as well as receive focus. This is the only way to make browser file input * dialog open on user action, and populate the file input value when file(s) are selected. * The tabIndex value here comes from the template arguments generated in getTemplateArgs * method below; it is copied from the owner FileInput's tabIndex property. */ afterTpl: [ '<input id="{id}-fileInputEl" data-ref="fileInputEl" class="{childElCls} {inputCls}" ', 'type="file" size="1" name="{inputName}" unselectable="on" ', '<tpl if="accept != null">accept="{accept}"</tpl>', '<tpl if="tabIndex != null">tabindex="{tabIndex}"</tpl>', '>' ], keyMap: null, ariaEl: 'fileInputEl', /** * @private */ getAfterMarkup: function(values) { return this.lookupTpl('afterTpl').apply(values); }, getTemplateArgs: function() { var me = this, args; args = me.callParent(); args.inputCls = me.inputCls; args.inputName = me.inputName || me.id; args.tabIndex = me.tabIndex != null ? me.tabIndex : null; args.accept = me.accept || null; args.role = me.ariaRole; return args; }, afterRender: function() { var me = this, listeners, cfg; me.callParent(arguments); // We place focus and blur listeners on fileInputEl to activate Button's // focus and blur style treatment listeners = { scope: me, mousedown: me.handlePrompt, keydown: me.handlePrompt, change: me.fireChange, focus: me.onFileFocus, blur: me.onFileBlur, destroyable: true }; if (me.useTabGuards) { cfg = { tag: 'span', role: 'button', 'aria-hidden': 'true', 'data-tabguard': 'true', style: { height: 0, width: 0 } }; cfg.tabIndex = me.tabIndex != null ? me.tabIndex : 0; // We are careful about inserting tab guards *around* the fileInputEl. // Keep in mind that IE8/9 have framed buttons so DOM structure // can be complex. me.beforeInputGuard = me.el.createChild(cfg, me.fileInputEl); me.afterInputGuard = me.el.createChild(cfg); me.afterInputGuard.insertAfter(me.fileInputEl); me.beforeInputGuard.on('focus', me.onInputGuardFocus, me); me.afterInputGuard.on('focus', me.onInputGuardFocus, me); listeners.keydown = me.onFileInputKeydown; } me.fileInputElListeners = me.fileInputEl.on(listeners); }, doDestroy: function() { var me = this; if (me.fileInputElListeners) { me.fileInputElListeners.destroy(); } if (me.beforeInputGuard) { me.beforeInputGuard.destroy(); me.beforeInputGuard = null; } if (me.afterInputGuard) { me.afterInputGuard.destroy(); me.afterInputGuard = null; } me.callParent(); }, fireChange: function(e) { this.fireEvent('change', this, e, this.fileInputEl.dom.value); }, /** * @private * Creates the file input element. It is inserted into the trigger button component, made * invisible, and floated on top of the button's other content so that it will receive the * button's clicks. */ createFileInput: function(isTemporary) { var me = this, fileInputEl, listeners; fileInputEl = me.fileInputEl = me.el.createChild({ name: me.inputName || me.id, id: !isTemporary ? me.id + '-fileInputEl' : undefined, cls: me.inputCls + (me.getInherited().rtl ? ' ' + Ext.baseCSSPrefix + 'rtl' : ''), tag: 'input', type: 'file', size: 1, unselectable: 'on' }, me.afterInputGuard); // Nothing special happens outside of IE/Edge // This is our focusEl fileInputEl.dom.setAttribute('data-componentid', me.id); if (me.tabIndex != null) { me.setTabIndex(me.tabIndex); } if (me.accept) { fileInputEl.dom.setAttribute('accept', me.accept); } // We place focus and blur listeners on fileInputEl to activate Button's // focus and blur style treatment listeners = { scope: me, change: me.fireChange, mousedown: me.handlePrompt, keydown: me.handlePrompt, focus: me.onFileFocus, blur: me.onFileBlur }; if (me.useTabGuards) { listeners.keydown = me.onFileInputKeydown; } fileInputEl.on(listeners); }, handlePrompt: function(e) { var key; if (e.type == 'keydown') { key = e.getKey(); // We need this conditional here because IE doesn't open the prompt on ENTER this.promptCalled = ((!Ext.isIE && key === e.ENTER) || key === e.SPACE) ? true : false; } else { this.promptCalled = true; } }, onFileFocus: function(e) { var ownerCt = this.ownerCt; if (!this.hasFocus) { this.onFocus(e); } if (ownerCt && !ownerCt.hasFocus) { ownerCt.onFocus(e); } }, onFileBlur: function(e) { var ownerCt = this.ownerCt; // We should not go ahead with blur if this was called because // the fileInput was clicked and the upload window is causing this event if (this.promptCalled) { this.promptCalled = false; e.preventDefault(); return; } if (this.hasFocus) { this.onBlur(e); } if (ownerCt && ownerCt.hasFocus) { ownerCt.onBlur(e); } }, onInputGuardFocus: function(e) { this.fileInputEl.focus(); }, onFileInputKeydown: function(e) { var key = e.getKey(), focusTo; if (key === e.TAB) { focusTo = e.shiftKey ? this.beforeInputGuard : this.afterInputGuard; if (focusTo) { // We need to skip the next focus to avoid it bouncing back // to the input field. focusTo.suspendEvent('focus'); focusTo.focus(); // In IE focus events are asynchronous so we can't enable focus event // in the same event loop. Ext.defer(function() { focusTo.resumeEvent('focus'); }, 1); } } else if (key === e.ENTER || key === e.SPACE) { this.handlePrompt(e); } // Returning true will allow the event to take default action return true; }, reset: function(remove) { var me = this; if (remove) { me.fileInputEl.destroy(); } me.createFileInput(!remove); if (remove) { me.ariaEl = me.fileInputEl; } }, restoreInput: function(el) { var me = this; me.fileInputEl.destroy(); el = Ext.get(el); if (me.useTabGuards) { el.insertBefore(me.afterInputGuard); } else { me.el.appendChild(el); } me.fileInputEl = el; }, onDisable: function() { this.callParent(); this.fileInputEl.dom.disabled = true; }, onEnable: function() { this.callParent(); this.fileInputEl.dom.disabled = false; }, privates: { getFocusEl: function() { return this.fileInputEl; }, getFocusClsEl: function() { return this.el; }, setTabIndex: function(tabIndex) { var me = this; if (!me.focusable) { return; } me.tabIndex = tabIndex; if (!me.rendered || me.destroying || me.destroyed) { return; } if (me.useTabGuards) { me.fileInputEl.dom.setAttribute('tabIndex', -1); me.beforeInputGuard.dom.setAttribute('tabIndex', tabIndex); me.afterInputGuard.dom.setAttribute('tabIndex', tabIndex); } else { me.fileInputEl.dom.setAttribute('tabIndex', tabIndex); } } }});