outlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplace
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
682 lines
22 KiB
682 lines
22 KiB
/** |
|
* A mixin for individually focusable things (Components, Widgets, etc) |
|
*/ |
|
Ext.define('Ext.util.Focusable', { |
|
mixinId: 'focusable', |
|
|
|
/** |
|
* @property {Boolean} hasFocus |
|
* `true` if this component has focus. |
|
* @private |
|
*/ |
|
hasFocus: false, |
|
|
|
|
|
/** |
|
* @property {Boolean} focusable |
|
* |
|
* `true` for interactive Components, `false` for static Components. |
|
* For Containers, this property reflects interactiveness of the |
|
* Container itself, not its children. See {@link #isFocusable}. |
|
* |
|
* **Note:** Plain components are static, so not focusable. |
|
*/ |
|
focusable: false, |
|
|
|
/** |
|
* @cfg {Number} [tabIndex] DOM tabIndex attribute for this Focusable |
|
*/ |
|
|
|
/** |
|
* @cfg {String} [focusCls='x-focus'] CSS class that will be added to focused |
|
* Component, and removed when Component blurs. |
|
*/ |
|
focusCls: 'focus', |
|
|
|
/** |
|
* @event focus |
|
* Fires when this Component receives focus. |
|
* @param {Ext.Component} this |
|
* @param {Ext.event.Event} event The focus event. |
|
*/ |
|
|
|
/** |
|
* @event blur |
|
* Fires when this Component loses focus. |
|
* @param {Ext.Component} this |
|
* @param {Ext.event.Event} event The blur event. |
|
*/ |
|
|
|
/** |
|
* Template method to do any Focusable related initialization that |
|
* does not involve event listeners creation. |
|
* @protected |
|
*/ |
|
initFocusable: Ext.emptyFn, |
|
|
|
/** |
|
* Template method to do any event listener initialization for a Focusable. |
|
* This generally happens after the focusEl is available. |
|
* @protected |
|
*/ |
|
initFocusableEvents: function() { |
|
// If *not* naturally focusable, then we look for the tabIndex property |
|
// to be defined which indicates that the element should be made focusable. |
|
this.initFocusableElement(); |
|
}, |
|
|
|
/** |
|
* Returns the focus styling holder element associated with this Focusable. |
|
* By default it is the same element as {@link #getFocusEl getFocusEl}. |
|
* |
|
* @return {Ext.Element} The focus styling element. |
|
* @protected |
|
*/ |
|
getFocusClsEl: function() { |
|
return this.getFocusEl(); |
|
}, |
|
|
|
/** |
|
* Returns the focus holder element associated with this Focusable. At the |
|
* level of the Focusable base, this function returns `this.el` (or for Widgets, |
|
* `this.element`). |
|
* |
|
* Subclasses with embedded focusable elements (such as Window, Field and Button) |
|
* should override this for use by {@link Ext.util.Focusable#method-focus focus} |
|
* method. |
|
* |
|
* @return {Ext.Element} |
|
* @protected |
|
*/ |
|
getFocusEl: function () { |
|
return this.element || this.el; |
|
}, |
|
|
|
destroyFocusable: function() { |
|
this.focusListeners = Ext.destroy(this.focusListeners); |
|
delete this.focusTask; |
|
}, |
|
|
|
enableFocusable: Ext.emptyFn, |
|
|
|
disableFocusable: function() { |
|
var me = this, |
|
focusTarget, |
|
focusCls = me.focusCls, |
|
focusClsEl; |
|
|
|
// If this is disabled while focused, by default, focus would return to document.body. |
|
// This must be avoided, both for the convenience of keyboard users, and also |
|
// for when focus is tracked within a tree, such as below an expanded ComboBox. |
|
if (me.hasFocus) { |
|
focusTarget = me.findFocusTarget(); |
|
if (focusTarget) { |
|
focusTarget.focus(); |
|
} |
|
} |
|
focusClsEl = me.getFocusClsEl(); |
|
|
|
if (focusCls && focusClsEl) { |
|
focusClsEl.removeCls(me.removeClsWithUI(focusCls, true)); |
|
} |
|
}, |
|
|
|
/** |
|
* Determine if this Focusable can receive focus at this time. |
|
* |
|
* Note that Containers can be non-focusable themselves while delegating |
|
* focus treatment to a child Component; see |
|
* {@link Ext.container.Container #defaultFocus} for more information. |
|
* |
|
* @param {Boolean} [deep=false] Optionally determine if the container itself |
|
* is focusable, or if container's focus is delegated to a child component |
|
* and that child is focusable. |
|
* |
|
* @return {Boolean} True if component is focusable, false if not. |
|
*/ |
|
isFocusable: function(deep) { |
|
var me = this, |
|
focusEl; |
|
|
|
if (!me.focusable && (!me.isContainer || !deep)) { |
|
return false; |
|
} |
|
|
|
focusEl = me.getFocusEl(); |
|
|
|
if (focusEl && me.canFocus()) { |
|
|
|
// getFocusEl might return a Component if a Container wishes to |
|
// delegate focus to a descendant. Both Component and Element |
|
// implement isFocusable, so always ask that. |
|
return focusEl.isFocusable(deep); |
|
} |
|
|
|
return false; |
|
}, |
|
|
|
canFocus: function() { |
|
var me = this; |
|
|
|
// Containers may have focusable children while being non-focusable |
|
// themselves; this is why we only account for me.focusable for |
|
// ordinary Components here and below. |
|
return (me.isContainer || me.focusable) && me.rendered && |
|
!me.destroying && !me.isDestroyed && !me.disabled && |
|
me.isVisible(true); |
|
}, |
|
|
|
/** |
|
* Try to focus this component. |
|
* |
|
* If this component is disabled, a close relation will be targeted for focus instead |
|
* to keep focus localized for keyboard users. |
|
* @param {Mixed} [selectText] If applicable, `true` to also select all the text in this component, or an array consisting of start and end (defaults to start) position of selection. |
|
* @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds). |
|
* @param {Function} [callback] Only needed if the `delay` parameter is used. A function to call upon focus. |
|
* @param {Function} [scope] Only needed if the `delay` parameter is used. The scope (`this` reference) in which to execute the callback. |
|
* @return {Ext.Component} The focused Component. Usually `this` Component. Some Containers may |
|
* delegate focus to a descendant Component ({@link Ext.window.Window Window}s can do this through their |
|
* {@link Ext.window.Window#defaultFocus defaultFocus} config option. If this component is disabled, a closely |
|
* related component will be focused and that will be returned. |
|
*/ |
|
focus: function(selectText, delay, callback, scope) { |
|
var me = this, |
|
focusTarget, focusElDom, containerScrollTop; |
|
|
|
if ((!me.focusable && !me.isContainer) || me.isDestroyed || me.destroying) { |
|
return; |
|
} |
|
|
|
// If delay is wanted, queue a call to this function. |
|
if (delay) { |
|
me.getFocusTask().delay(Ext.isNumber(delay) ? delay : 10, me.focus, me, [selectText, false, callback, scope]); |
|
return me; |
|
} |
|
|
|
// An immediate focus call must cancel any outstanding delayed focus calls. |
|
me.cancelFocus(); |
|
|
|
// Assignment in conditional here to avoid calling getFocusEl() |
|
// if me.canFocus() returns false |
|
if (me.canFocus()) { |
|
if (focusTarget = me.getFocusEl()) { |
|
|
|
// getFocusEl might return a Component if a Container wishes to delegate focus to a |
|
// descendant via its defaultFocus configuration. |
|
if (focusTarget.isComponent) { |
|
return focusTarget.focus(selectText, delay, callback, scope); |
|
} |
|
|
|
focusElDom = focusTarget.dom; |
|
|
|
// If it was an Element with a dom property |
|
if (focusElDom) { |
|
|
|
// Not a natural focus holding element, add a tab index to make it |
|
// programmatically focusable |
|
if (focusTarget.needsTabIndex()) { |
|
focusElDom.tabIndex = -1; |
|
} |
|
|
|
if (me.floating) { |
|
containerScrollTop = me.container.dom.scrollTop; |
|
} |
|
|
|
// Focus the element. |
|
// The Ext.event.publisher.Focus publisher listens for global focus changes and |
|
// The ComponentManager responds by invoking the onFocusEnter and onFocusLeave methods |
|
// of the components involved. |
|
focusTarget.focus(); |
|
|
|
if (selectText) { |
|
if (Ext.isArray(selectText)) { |
|
if (me.selectText) { |
|
me.selectText.apply(me, selectText); |
|
} |
|
} else if (focusElDom.select) { |
|
// This method both focuses and selects the element. |
|
focusElDom.select(); |
|
} else if (me.selectText) { |
|
me.selectText(); |
|
} |
|
} |
|
|
|
// Call the callback when focus is done |
|
Ext.callback(callback, scope); |
|
} |
|
|
|
// Focusing a floating Component brings it to the front of its stack. |
|
// this is performed by its zIndexManager. Pass preventFocus true to avoid recursion. |
|
if (me.floating) { |
|
// Every component that doesn't have preventFocus set gets a delayed call to focus(). |
|
// Only bring to front if the current component isn't the manager's topmost component. |
|
if (me !== me.zIndexManager.getActive()) { |
|
me.toFront(true); |
|
} |
|
|
|
if (containerScrollTop !== undefined) { |
|
me.container.dom.scrollTop = containerScrollTop; |
|
} |
|
} |
|
} |
|
} else { |
|
// If we are asked to focus while not able to focus though disablement/invisibility etc, |
|
// focus may revert to document.body if the current focus is being hidden or destroyed. |
|
// This must be avoided, both for the convenience of keyboard users, and also |
|
// for when focus is tracked within a tree, such as below an expanded ComboBox. |
|
focusTarget = me.findFocusTarget(); |
|
if (focusTarget) { |
|
return focusTarget.focus(selectText, delay, callback, scope); |
|
} |
|
} |
|
|
|
return me; |
|
}, |
|
|
|
/** |
|
* Cancel any deferred focus on this component |
|
* @protected |
|
*/ |
|
cancelFocus: function() { |
|
var task = this.getFocusTask(); |
|
|
|
if (task) { |
|
task.cancel(); |
|
} |
|
}, |
|
|
|
/** |
|
* Template method to do any pre-blur processing. |
|
* @protected |
|
* @param {Ext.event.Event} e The event object |
|
*/ |
|
beforeBlur: Ext.emptyFn, |
|
|
|
// private |
|
onBlur: function(e) { |
|
var me = this, |
|
container = me.focusableContainer, |
|
focusCls = me.focusCls, |
|
focusClsEl; |
|
|
|
if (!me.focusable || me.destroying) { |
|
return; |
|
} |
|
|
|
me.beforeBlur(e); |
|
|
|
if (container) { |
|
container.beforeFocusableChildBlur(me, e); |
|
} |
|
|
|
focusClsEl = me.getFocusClsEl(); |
|
|
|
if (focusCls && focusClsEl) { |
|
focusClsEl.removeCls(me.removeClsWithUI(focusCls, true)); |
|
} |
|
|
|
if (me.validateOnBlur) { |
|
me.validate(); |
|
} |
|
|
|
me.hasFocus = false; |
|
me.fireEvent('blur', me, e); |
|
me.postBlur(e); |
|
|
|
if (container) { |
|
container.afterFocusableChildBlur(me, e); |
|
} |
|
}, |
|
|
|
/** |
|
* Template method to do any post-blur processing. |
|
* @protected |
|
* @param {Ext.event.Event} e The event object |
|
*/ |
|
postBlur: Ext.emptyFn, |
|
|
|
/** |
|
* Template method to do any pre-focus processing. |
|
* @protected |
|
* @param {Ext.event.Event} e The event object |
|
*/ |
|
beforeFocus: Ext.emptyFn, |
|
|
|
// private |
|
onFocus: function(e) { |
|
var me = this, |
|
container = me.focusableContainer, |
|
focusCls = me.focusCls, |
|
focusClsEl; |
|
|
|
if (!me.focusable) { |
|
return; |
|
} |
|
|
|
if (me.canFocus()) { |
|
me.beforeFocus(e); |
|
|
|
if (container) { |
|
container.beforeFocusableChildFocus(me, e); |
|
} |
|
|
|
focusClsEl = me.getFocusClsEl(); |
|
|
|
if (focusCls && focusClsEl) { |
|
focusClsEl.addCls(me.addClsWithUI(focusCls, true)); |
|
} |
|
|
|
if (!me.hasFocus) { |
|
me.hasFocus = true; |
|
me.fireEvent('focus', me, e); |
|
} |
|
|
|
me.postFocus(e); |
|
|
|
if (container) { |
|
container.afterFocusableChildFocus(me, e); |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Template method to do any post-focus processing. |
|
* @protected |
|
* @param {Ext.event.Event} e The event object |
|
*/ |
|
postFocus: Ext.emptyFn, |
|
|
|
/** |
|
* Return the actual tabIndex for this Focusable. |
|
* |
|
* @return {Number} tabIndex attribute value |
|
*/ |
|
getTabIndex: function() { |
|
var me = this, |
|
el, index; |
|
|
|
if (!me.focusable) { |
|
return; |
|
} |
|
|
|
el = me.getFocusEl(); |
|
|
|
if (el) { |
|
// getFocusEl may return a child Component |
|
if (el.isComponent) { |
|
index = el.getTabIndex(); |
|
} |
|
|
|
// We can't query el.dom.tabIndex because IE8 will return 0 |
|
// when tabIndex attribute is not present. |
|
else if (el.isElement) { |
|
index = el.getAttribute(Ext.Element.tabIndexAttributeName); |
|
} |
|
|
|
// A component can be configured with el: '#id' to look up |
|
// its main element from the DOM rather than render it; in |
|
// such case getTabIndex() may happen to be called before |
|
// said lookup has happened; indeterminate result follows. |
|
else { |
|
return; |
|
} |
|
|
|
me.tabIndex = index; |
|
} |
|
else { |
|
index = me.tabIndex; |
|
} |
|
|
|
return index - 0; // getAttribute returns a string |
|
}, |
|
|
|
/** |
|
* Set the tabIndex property for this Focusable. If the focusEl |
|
* is avalable, set tabIndex attribute on it, too. |
|
* |
|
* @param {Number} newTabIndex new tabIndex to set |
|
*/ |
|
setTabIndex: function(newTabIndex) { |
|
var me = this, |
|
el; |
|
|
|
if (!me.focusable) { |
|
return; |
|
} |
|
|
|
me.tabIndex = newTabIndex; |
|
el = me.getFocusEl(); |
|
|
|
if (el) { |
|
// getFocusEl may return a child Component |
|
if (el.isComponent) { |
|
el.setTabIndex(newTabIndex); |
|
} |
|
|
|
// Or if a component is configured with el: '#id', it may |
|
// still be a string by the time setTabIndex is called from |
|
// FocusableContainer. |
|
else if (el.isElement) { |
|
el.set({ tabindex: newTabIndex }); |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* @template |
|
* @protected |
|
* Called when focus enters this Component's hierarchy |
|
* @param {Ext.event.Event} e |
|
*/ |
|
onFocusEnter: function(e) { |
|
var me = this; |
|
|
|
// Prioritize focusing back to a previous *Component* |
|
// This more correctly handles the case of a Component becoming |
|
// unfocusable in some way during the intervening time while this was visible. |
|
me.previousFocus = e.fromComponent || e.relatedTarget; |
|
me.containsFocus = true; |
|
me.fireEvent('focusenter', me, e); |
|
}, |
|
|
|
/** |
|
* @template |
|
* @protected |
|
* Called when focus exits from this Component's hierarchy |
|
* @param {Ext.event.Event} e |
|
*/ |
|
onFocusLeave: function(e) { |
|
var me = this; |
|
|
|
me.previousFocus = null; |
|
me.containsFocus = false; |
|
me.fireEvent('focusleave', me, e); |
|
}, |
|
|
|
privates: { |
|
|
|
/** |
|
* Returns focus to the cached previously focused Component or element. |
|
* |
|
* Usually called by onHide. |
|
* |
|
* @private |
|
*/ |
|
revertFocus: function() { |
|
var me = this, |
|
focusTarget = me.previousFocus; |
|
|
|
me.previousFocus = null; |
|
|
|
// If this about to be hidden component contains focus... |
|
// Before hiding, restore focus to what was focused when we were shown. |
|
if (focusTarget && me.containsFocus) { |
|
|
|
// If reverting back to a Component, it will re-route to a close focusable relation |
|
// if it is not now focusable. |
|
if (focusTarget.isComponent) { |
|
focusTarget.focus(); |
|
} |
|
// Reverting to an element. |
|
else { |
|
focusTarget = Ext.fly(focusTarget); |
|
// TODO: Remove extra check when IE8 retires. |
|
if (Ext.isIE8 || (focusTarget.isFocusable && focusTarget.isFocusable())) { |
|
focusTarget.focus(); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Finds an alternate Component to focus if this Component is disabled while focused, or |
|
* focused while disabled, or otherwise unable to focus. |
|
* |
|
* In both cases, focus must not be lost to document.body, but must move to an intuitively |
|
* connectable Component, either a sibling, or uncle or nephew. |
|
* |
|
* This is both for the convenience of keyboard users, and also for when focus is tracked |
|
* within a Component tree such as for ComboBoxes and their dropdowns. |
|
* |
|
* For example, a ComboBox with a PagingToolbar in is BoundList. If the "Next Page" |
|
* button is hit, the LoadMask shows and focuses, the next page is the last page, so |
|
* the "Next Page" button is disabled. When the LoadMask hides, it attempt to focus the |
|
* last focused Component which is the disabled "Next Page" button. In this situation, |
|
* focus should move to a sibling within the PagingToolbar. |
|
* |
|
* @return {Ext.Component} A closely related focusable Component to which focus can move. |
|
* @private |
|
*/ |
|
findFocusTarget: function() { |
|
var me = this, |
|
owner, |
|
focusTargets; |
|
|
|
for (owner = me.up(':not([disabled])'); owner; owner = owner.up(':not([disabled])')) { |
|
// Use CQ to find a target that is focusable, and not this Component. |
|
// Cannot use owner.child() because the parent might not be a Container. |
|
// Non-Container Components may still have ownership relationships with |
|
// other Components. eg: BoundList with PagingToolbar |
|
focusTargets = Ext.ComponentQuery.query(':focusable:not([hasFocus])', owner); |
|
if (focusTargets.length) { |
|
return focusTargets[0]; |
|
} |
|
|
|
// We found no focusable siblings in our owner, but the owner may itself be focusable, |
|
// it is not always a Container - could be the owning Field of a BoundList. |
|
if (owner.isFocusable && owner.isFocusable()) { |
|
return owner; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* Sets up the focus listener on this Component's {@link #getFocusEl focusEl} if it has one. |
|
* |
|
* Form Components which must implicitly participate in tabbing order usually have a naturally |
|
* focusable element as their {@link #getFocusEl focusEl}, and it is the DOM event of that |
|
* receiving focus which drives the Component's `onFocus` handling, and the DOM event of it |
|
* being blurred which drives the `onBlur` handling. |
|
* @private |
|
*/ |
|
initFocusableElement: function() { |
|
var me = this, |
|
tabIndex = me.tabIndex, |
|
focusEl = me.getFocusEl(), |
|
needsTabIndex; |
|
|
|
if (focusEl && !focusEl.isComponent) { |
|
needsTabIndex = focusEl.needsTabIndex(); |
|
|
|
// Add the tabIndex only is it's needed to make it tabbable, or there is a user-configured tabIndex |
|
if (needsTabIndex || tabIndex != null) { |
|
focusEl.dom.setAttribute('tabindex', tabIndex); |
|
} |
|
|
|
// This attribute is a shortcut to look up a Component by its Elements |
|
// It only makes sense on focusable elements, so we set it here |
|
focusEl.dom.setAttribute(Ext.Component.componentIdAttribute, me.id); |
|
} |
|
}, |
|
|
|
// private |
|
getFocusTask: function() { |
|
if (!this.focusTask) { |
|
this.focusTask = Ext.focusTask; |
|
} |
|
|
|
return this.focusTask; |
|
}, |
|
|
|
// private |
|
blur: function() { |
|
var me = this, |
|
focusEl; |
|
|
|
if (!me.focusable || !me.rendered) { |
|
return; |
|
} |
|
|
|
focusEl = me.getFocusEl(); |
|
|
|
if (focusEl) { |
|
me.blurring = true; |
|
focusEl.blur(); |
|
delete me.blurring; |
|
} |
|
|
|
return me; |
|
}, |
|
|
|
disableTabbing: function() { |
|
var me = this, |
|
el = me.el, |
|
focusEl; |
|
|
|
if (!me.focusable) { |
|
return; |
|
} |
|
|
|
focusEl = me.getFocusEl(); |
|
|
|
if (el) { |
|
el.saveChildrenTabbableState(); |
|
} |
|
|
|
if (focusEl) { |
|
focusEl = focusEl.isComponent ? focusEl.getFocusEl() : focusEl; |
|
focusEl.saveTabbableState(); |
|
} |
|
}, |
|
|
|
enableTabbing: function() { |
|
var me = this, |
|
el = me.el, |
|
focusEl; |
|
|
|
if (!me.focusable) { |
|
return; |
|
} |
|
|
|
focusEl = me.getFocusEl(); |
|
|
|
if (focusEl) { |
|
focusEl = focusEl.isComponent ? focusEl.getFocusEl() : focusEl; |
|
focusEl.restoreTabbableState(); |
|
} |
|
|
|
if (el) { |
|
el.restoreChildrenTabbableState(); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
function() { |
|
// One global DelayedTask to assign focus |
|
// So that the last focus call wins. |
|
if (!Ext.focusTask) { |
|
Ext.focusTask = new Ext.util.DelayedTask(); |
|
} |
|
});
|
|
|