/**
*
*/
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'
},
/* eslint-disable indent */
/*
* 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>',
'>'
],
/* eslint-enable indent */
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);
}
}
}
});