slackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangouts
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.
451 lines
15 KiB
451 lines
15 KiB
9 years ago
|
/**
|
||
|
* A mixin for groups of Focusable things (Components, Widgets, etc) that
|
||
|
* should respond to arrow keys to navigate among the peers, but keep only
|
||
|
* one of the peers focusable by default (tabIndex=0)
|
||
|
*
|
||
|
* Some examples: Toolbars, Radio groups, Tab bars
|
||
|
*/
|
||
|
|
||
|
Ext.define('Ext.util.FocusableContainer', {
|
||
|
extend: 'Ext.Mixin',
|
||
|
|
||
|
requires: [
|
||
|
'Ext.util.KeyNav'
|
||
|
],
|
||
|
|
||
|
mixinConfig: {
|
||
|
id: 'focusablecontainer',
|
||
|
|
||
|
before: {
|
||
|
onAdd: 'onFocusableChildAdd',
|
||
|
onRemove: 'onFocusableChildRemove',
|
||
|
destroy: 'destroyFocusableContainer',
|
||
|
onFocusEnter: 'onFocusEnter'
|
||
|
},
|
||
|
|
||
|
after: {
|
||
|
afterRender: 'initFocusableContainer',
|
||
|
onFocusLeave: 'onFocusLeave'
|
||
|
}
|
||
|
},
|
||
|
|
||
|
isFocusableContainer: true,
|
||
|
|
||
|
/**
|
||
|
* @cfg {Boolean} [enableFocusableContainer=true] Enable or disable
|
||
|
* navigation with arrow keys for this FocusableContainer. This option may
|
||
|
* be useful with nested FocusableContainers such as Grid column headers,
|
||
|
* when only the root container should handle keyboard events.
|
||
|
*/
|
||
|
enableFocusableContainer: true,
|
||
|
|
||
|
/**
|
||
|
* @cfg {Number} [activeChildTabIndex=0] DOM tabIndex attribute to set on the
|
||
|
* active Focusable child of this container when using the "Roaming tabindex"
|
||
|
* technique. Set this value to > 0 to precisely control the tabbing order
|
||
|
* of the components/containers on the page.
|
||
|
*/
|
||
|
activeChildTabIndex: 0,
|
||
|
|
||
|
/**
|
||
|
* @cfg {Number} [inactiveChildTabIndex=-1] DOM tabIndex attribute to set on
|
||
|
* inactive Focusable children of this container when using the "Roaming tabindex"
|
||
|
* technique. This value rarely needs to be changed from its default.
|
||
|
*/
|
||
|
inactiveChildTabIndex: -1,
|
||
|
|
||
|
privates: {
|
||
|
initFocusableContainer: function() {
|
||
|
// Allow nested containers to optionally disable
|
||
|
// children containers' behavior
|
||
|
if (this.enableFocusableContainer) {
|
||
|
this.doInitFocusableContainer();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
doInitFocusableContainer: function() {
|
||
|
var me = this,
|
||
|
el;
|
||
|
|
||
|
el = me.getFocusableContainerEl();
|
||
|
|
||
|
// We set tabIndex on the focusable container el so that the user
|
||
|
// could tab into it; we catch its focus event and focus a child instead
|
||
|
me.activateFocusableContainerEl(el);
|
||
|
|
||
|
me.mon(el, 'mousedown', me.onFocusableContainerMousedown, me);
|
||
|
|
||
|
me.focusableKeyNav = me.createFocusableContainerKeyNav(el);
|
||
|
},
|
||
|
|
||
|
createFocusableContainerKeyNav: function(el) {
|
||
|
var me = this;
|
||
|
|
||
|
return new Ext.util.KeyNav(el, {
|
||
|
ignoreInputFields: true,
|
||
|
scope: me,
|
||
|
|
||
|
tab: me.onFocusableContainerTabKey,
|
||
|
enter: me.onFocusableContainerEnterKey,
|
||
|
space: me.onFocusableContainerSpaceKey,
|
||
|
up: me.onFocusableContainerUpKey,
|
||
|
down: me.onFocusableContainerDownKey,
|
||
|
left: me.onFocusableContainerLeftKey,
|
||
|
right: me.onFocusableContainerRightKey
|
||
|
});
|
||
|
},
|
||
|
|
||
|
destroyFocusableContainer: function() {
|
||
|
if (this.enableFocusableContainer) {
|
||
|
this.doDestroyFocusableContainer();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
doDestroyFocusableContainer: function() {
|
||
|
var keyNav = this.focusableKeyNav;
|
||
|
|
||
|
if (keyNav) {
|
||
|
keyNav.destroy();
|
||
|
delete this.focusableKeyNav;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// Default FocusableContainer implies a flat list of focusable children
|
||
|
getFocusables: function() {
|
||
|
return this.items.items;
|
||
|
},
|
||
|
|
||
|
initDefaultFocusable: function(beforeRender) {
|
||
|
var me = this,
|
||
|
activeIndex = me.activeChildTabIndex,
|
||
|
haveFocusable = false,
|
||
|
items, item, i, len, tabIdx;
|
||
|
|
||
|
items = me.getFocusables();
|
||
|
len = items.length;
|
||
|
|
||
|
if (!len) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Check if any child Focusable is already active.
|
||
|
// Note that we're not determining *which* focusable child
|
||
|
// to focus here, only that we have some focusables.
|
||
|
for (i = 0; i < len; i++) {
|
||
|
item = items[i];
|
||
|
|
||
|
if (item.focusable) {
|
||
|
haveFocusable = true;
|
||
|
tabIdx = item.getTabIndex();
|
||
|
|
||
|
if (tabIdx != null && tabIdx >= activeIndex) {
|
||
|
return item;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// No interactive children found, no point in going further
|
||
|
if (!haveFocusable) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// No child is focusable by default, so the first *interactive*
|
||
|
// one gets initial childTabIndex. We are not looking for a focusable
|
||
|
// child here because it may not be focusable yet if this happens
|
||
|
// before rendering; we assume that an interactive child will become
|
||
|
// focusable later and now activateFocusable() will just assign it
|
||
|
// the respective tabIndex.
|
||
|
item = me.findNextFocusableChild(null, true, items, beforeRender);
|
||
|
|
||
|
if (item) {
|
||
|
me.activateFocusable(item);
|
||
|
}
|
||
|
|
||
|
return item;
|
||
|
},
|
||
|
|
||
|
clearFocusables: function() {
|
||
|
var me = this,
|
||
|
items = me.getFocusables(),
|
||
|
len = items.length,
|
||
|
item, i;
|
||
|
|
||
|
for (i = 0; i < len; i++) {
|
||
|
item = items[i];
|
||
|
|
||
|
if (item.focusable) {
|
||
|
me.deactivateFocusable(item);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
activateFocusable: function(child, /* optional */ newTabIndex) {
|
||
|
var activeIndex = newTabIndex != null ? newTabIndex : this.activeChildTabIndex;
|
||
|
|
||
|
child.setTabIndex(activeIndex);
|
||
|
},
|
||
|
|
||
|
deactivateFocusable: function(child, /* optional */ newTabIndex) {
|
||
|
var inactiveIndex = newTabIndex != null ? newTabIndex : this.inactiveChildTabIndex;
|
||
|
|
||
|
child.setTabIndex(inactiveIndex);
|
||
|
},
|
||
|
|
||
|
onFocusableContainerTabKey: function() {
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
onFocusableContainerEnterKey: function() {
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
onFocusableContainerSpaceKey: function() {
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
onFocusableContainerUpKey: function(e) {
|
||
|
return this.moveChildFocus(e, false);
|
||
|
},
|
||
|
|
||
|
onFocusableContainerLeftKey: function(e) {
|
||
|
return this.moveChildFocus(e, false);
|
||
|
},
|
||
|
|
||
|
onFocusableContainerRightKey: function(e) {
|
||
|
return this.moveChildFocus(e, true);
|
||
|
},
|
||
|
|
||
|
onFocusableContainerDownKey: function(e) {
|
||
|
return this.moveChildFocus(e, true);
|
||
|
},
|
||
|
|
||
|
getFocusableFromEvent: function(e) {
|
||
|
var child = Ext.Component.fromElement(e.getTarget());
|
||
|
|
||
|
//<debug>
|
||
|
if (!child) {
|
||
|
Ext.Error.raise("No focusable child found for keyboard event!");
|
||
|
}
|
||
|
//</debug>
|
||
|
|
||
|
return child;
|
||
|
},
|
||
|
|
||
|
moveChildFocus: function(e, forward) {
|
||
|
var child = this.getFocusableFromEvent(e);
|
||
|
|
||
|
return this.focusChild(child, forward, e);
|
||
|
},
|
||
|
|
||
|
focusChild: function(child, forward) {
|
||
|
var nextChild = this.findNextFocusableChild(child, forward);
|
||
|
|
||
|
if (nextChild) {
|
||
|
nextChild.focus();
|
||
|
}
|
||
|
|
||
|
return nextChild;
|
||
|
},
|
||
|
|
||
|
findNextFocusableChild: function(child, step, items, beforeRender) {
|
||
|
var item, idx, i, len;
|
||
|
|
||
|
items = items || this.getFocusables();
|
||
|
|
||
|
// If the child is null or undefined, idx will be -1.
|
||
|
// The loop below will account for that, trying to find
|
||
|
// the first focusable child from either end (depending on step)
|
||
|
idx = Ext.Array.indexOf(items, child);
|
||
|
|
||
|
// It's often easier to pass a boolean for 1/-1
|
||
|
step = step === true ? 1 : step === false ? -1 : step;
|
||
|
|
||
|
len = items.length;
|
||
|
i = step > 0 ? (idx < len ? idx + step : 0) : (idx > 0 ? idx + step : len - 1);
|
||
|
|
||
|
for (;; i += step) {
|
||
|
// We're looking for the first or last focusable child
|
||
|
// and we've reached the end of the items, so punt
|
||
|
if (idx < 0 && (i >= len || i < 0)) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Loop over forward
|
||
|
else if (i >= len) {
|
||
|
i = -1; // Iterator will increase it once more
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Loop over backward
|
||
|
else if (i < 0) {
|
||
|
i = len;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Looped to the same item, give up
|
||
|
else if (i === idx) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
item = items[i];
|
||
|
|
||
|
if (!item || !item.focusable) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// This loop can be run either at FocusableContainer init time,
|
||
|
// or later when we need to navigate upon pressing an arrow key.
|
||
|
// When we're navigating, we have to know exactly if the child is
|
||
|
// focusable or not, hence only rendered children will make the cut.
|
||
|
// At the init time item.isFocusable() may return false incorrectly
|
||
|
// just because the item has not been rendered yet and its focusEl
|
||
|
// is not defined, so we don't bother to call isFocus and return
|
||
|
// the first potentially focusable child.
|
||
|
if (beforeRender || (item.isFocusable && item.isFocusable())) {
|
||
|
return item;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
},
|
||
|
|
||
|
getFocusableContainerEl: function() {
|
||
|
return this.el;
|
||
|
},
|
||
|
|
||
|
onFocusableChildAdd: function(child) {
|
||
|
return this.doFocusableChildAdd(child);
|
||
|
},
|
||
|
|
||
|
activateFocusableContainerEl: function(el) {
|
||
|
el = el || this.getFocusableContainerEl();
|
||
|
|
||
|
el.set({ tabindex: this.activeChildTabIndex });
|
||
|
},
|
||
|
|
||
|
deactivateFocusableContainerEl: function(el) {
|
||
|
el = el || this.getFocusableContainerEl();
|
||
|
|
||
|
el.set({ tabindex: this.inactiveChildTabIndex });
|
||
|
},
|
||
|
|
||
|
doFocusableChildAdd: function(child) {
|
||
|
if (child.focusable) {
|
||
|
child.focusableContainer = this;
|
||
|
this.deactivateFocusable(child);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
onFocusableChildRemove: function(child) {
|
||
|
return this.doFocusableChildRemove(child);
|
||
|
},
|
||
|
|
||
|
doFocusableChildRemove: function(child) {
|
||
|
// If the focused child is being removed, we deactivate the FocusableContainer
|
||
|
// So that it returns to the tabbing order.
|
||
|
// For example, locking a grid column must return the owning HeaderContainer to tabbability
|
||
|
if (child === this.lastFocusedChild) {
|
||
|
this.lastFocusedChild = null;
|
||
|
this.activateFocusableContainerEl();
|
||
|
}
|
||
|
delete child.focusableContainer;
|
||
|
},
|
||
|
|
||
|
onFocusableContainerMousedown: function(e, target) {
|
||
|
var targetCmp = Ext.Component.fromElement(target);
|
||
|
|
||
|
// Capture the timestamp for the mousedown. If we're navigating into the container itself
|
||
|
// via the mouse we don't want to default focus the first child like we would when using
|
||
|
// the keyboard. By the time we get to the focusenter handling, we don't know what has caused
|
||
|
// the focus to be triggered, so if the timestamp falls within some small epsilon, the focus enter
|
||
|
// has been caused via the mouse and we can react accordingly.
|
||
|
this.mousedownTimestamp = targetCmp === this ? Ext.Date.now() : 0;
|
||
|
},
|
||
|
|
||
|
onFocusEnter: function(e) {
|
||
|
var me = this,
|
||
|
target = e.toComponent,
|
||
|
mousedownTimestamp = me.mousedownTimestamp,
|
||
|
epsilon = 50,
|
||
|
child;
|
||
|
|
||
|
me.mousedownTimestamp = 0;
|
||
|
if (target === me) {
|
||
|
if (!mousedownTimestamp || Ext.Date.now() - mousedownTimestamp > epsilon) {
|
||
|
child = me.initDefaultFocusable();
|
||
|
|
||
|
if (child) {
|
||
|
me.deactivateFocusableContainerEl();
|
||
|
child.focus();
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
me.deactivateFocusableContainerEl();
|
||
|
}
|
||
|
|
||
|
return target;
|
||
|
},
|
||
|
|
||
|
onFocusLeave: function(e) {
|
||
|
var me = this,
|
||
|
lastFocused = me.lastFocusedChild;
|
||
|
|
||
|
if (!me.isDestroyed) {
|
||
|
me.clearFocusables();
|
||
|
|
||
|
if (lastFocused) {
|
||
|
me.activateFocusable(lastFocused);
|
||
|
}
|
||
|
else {
|
||
|
me.activateFocusableContainerEl();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
beforeFocusableChildBlur: Ext.privateFn,
|
||
|
afterFocusableChildBlur: Ext.privateFn,
|
||
|
|
||
|
beforeFocusableChildFocus: function(child) {
|
||
|
var me = this;
|
||
|
|
||
|
me.clearFocusables();
|
||
|
me.activateFocusable(child);
|
||
|
|
||
|
if (child.needArrowKeys) {
|
||
|
me.guardFocusableChild(child);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
guardFocusableChild: function(child) {
|
||
|
var me = this,
|
||
|
index = me.activeChildTabIndex,
|
||
|
guard;
|
||
|
|
||
|
guard = me.findNextFocusableChild(child, -1);
|
||
|
|
||
|
if (guard) {
|
||
|
guard.setTabIndex(index);
|
||
|
}
|
||
|
|
||
|
guard = me.findNextFocusableChild(child, 1);
|
||
|
|
||
|
if (guard) {
|
||
|
guard.setTabIndex(index);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
afterFocusableChildFocus: function(child) {
|
||
|
this.lastFocusedChild = child;
|
||
|
},
|
||
|
|
||
|
// TODO
|
||
|
onFocusableChildShow: Ext.privateFn,
|
||
|
onFocusableChildHide: Ext.privateFn,
|
||
|
onFocusableChildEnable: Ext.privateFn,
|
||
|
onFocusableChildDisable: Ext.privateFn,
|
||
|
onFocusableChildMasked: Ext.privateFn,
|
||
|
onFocusableChildDestroy: Ext.privateFn,
|
||
|
onFocusableChildUpdate: Ext.privateFn
|
||
|
}
|
||
|
});
|