<!DOCTYPE html>
|
<html>
|
<head>
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<title>The source code</title>
|
<link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
|
<script type="text/javascript" src="../resources/prettify/prettify.js"></script>
|
<style type="text/css">
|
.highlight { display: block; background-color: #ddd; }
|
</style>
|
<script type="text/javascript">
|
function highlight() {
|
document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
|
}
|
</script>
|
</head>
|
<body onload="prettyPrint(); highlight();">
|
<pre class="prettyprint lang-js"><span id='Ext-FocusManager'>/**
|
</span> * The FocusManager is responsible for globally:
|
*
|
* 1. Managing component focus
|
* 2. Providing basic keyboard navigation
|
* 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
|
*
|
* To activate the FocusManager, simply call `Ext.FocusManager.enable();`. In turn, you may
|
* deactivate the FocusManager by subsequently calling `Ext.FocusManager.disable();`. The
|
* FocusManager is disabled by default.
|
*
|
* To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #method-enable}.
|
*
|
* Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
|
* that would like to have navigation between its child {@link Ext.Component}'s.
|
*
|
* @author Jarred Nicholls <jarred@sencha.com>
|
* @docauthor Jarred Nicholls <jarred@sencha.com>
|
*/
|
Ext.define('Ext.FocusManager', {
|
singleton: true,
|
alternateClassName: ['Ext.FocusMgr' ],
|
|
mixins: {
|
observable: 'Ext.util.Observable'
|
},
|
|
requires: [
|
'Ext.AbstractComponent',
|
'Ext.Component',
|
'Ext.ComponentManager',
|
'Ext.ComponentQuery',
|
'Ext.util.HashMap',
|
'Ext.util.KeyNav'
|
],
|
|
<span id='Ext-FocusManager-property-enabled'> /**
|
</span> * @property {Boolean} enabled
|
* Whether or not the FocusManager is currently enabled
|
*/
|
enabled: false,
|
|
<span id='Ext-FocusManager-property-focusedCmp'> /**
|
</span> * @property {Ext.Component} focusedCmp
|
* The currently focused component.
|
*/
|
|
focusElementCls: Ext.baseCSSPrefix + 'focus-element',
|
|
<span id='Ext-FocusManager-property-focusFrameCls'> focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
|
</span>
|
<span id='Ext-FocusManager-property-whitelist'> /**
|
</span> * @property {String[]} whitelist
|
* A list of xtypes that should ignore certain navigation input keys and
|
* allow for the default browser event/behavior. These input keys include:
|
*
|
* 1. Backspace
|
* 2. Delete
|
* 3. Left
|
* 4. Right
|
* 5. Up
|
* 6. Down
|
*
|
* The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
|
* that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
|
* the user to move the input cursor left and right, and to delete characters, etc.
|
*/
|
whitelist: [
|
'textfield'
|
],
|
|
<span id='Ext-FocusManager-method-constructor'> constructor: function(config) {
|
</span> var me = this,
|
CQ = Ext.ComponentQuery;
|
|
me.mixins.observable.constructor.call(me, config);
|
|
me.addEvents(
|
<span id='Ext-FocusManager-event-beforecomponentfocus'> /**
|
</span> * @event beforecomponentfocus
|
* Fires before a component becomes focused. Return `false` to prevent
|
* the component from gaining focus.
|
* @param {Ext.FocusManager} fm A reference to the FocusManager singleton
|
* @param {Ext.Component} cmp The component that is being focused
|
* @param {Ext.Component} previousCmp The component that was previously focused,
|
* or `undefined` if there was no previously focused component.
|
*/
|
'beforecomponentfocus',
|
|
<span id='Ext-FocusManager-event-componentfocus'> /**
|
</span> * @event componentfocus
|
* Fires after a component becomes focused.
|
* @param {Ext.FocusManager} fm A reference to the FocusManager singleton
|
* @param {Ext.Component} cmp The component that has been focused
|
* @param {Ext.Component} previousCmp The component that was previously focused,
|
* or `undefined` if there was no previously focused component.
|
*/
|
'componentfocus',
|
|
<span id='Ext-FocusManager-event-disable'> /**
|
</span> * @event disable
|
* Fires when the FocusManager is disabled
|
* @param {Ext.FocusManager} fm A reference to the FocusManager singleton
|
*/
|
'disable',
|
|
<span id='Ext-FocusManager-event-enable'> /**
|
</span> * @event enable
|
* Fires when the FocusManager is enabled
|
* @param {Ext.FocusManager} fm A reference to the FocusManager singleton
|
*/
|
'enable'
|
);
|
|
me.focusTask = new Ext.util.DelayedTask(me.handleComponentFocus, me);
|
|
// Gain control on Component focus, blur, hide and destroy
|
Ext.override(Ext.AbstractComponent, {
|
onFocus: function() {
|
this.callParent(arguments);
|
if (me.enabled && this.hasFocus) {
|
Array.prototype.unshift.call(arguments, this);
|
me.onComponentFocus.apply(me, arguments);
|
}
|
},
|
onBlur: function() {
|
this.callParent(arguments);
|
if (me.enabled && !this.hasFocus) {
|
Array.prototype.unshift.call(arguments, this);
|
me.onComponentBlur.apply(me, arguments);
|
}
|
},
|
onDestroy: function() {
|
this.callParent(arguments);
|
if (me.enabled) {
|
Array.prototype.unshift.call(arguments, this);
|
me.onComponentDestroy.apply(me, arguments);
|
}
|
}
|
});
|
Ext.override(Ext.Component, {
|
afterHide: function() {
|
this.callParent(arguments);
|
if (me.enabled) {
|
Array.prototype.unshift.call(arguments, this);
|
me.onComponentHide.apply(me, arguments);
|
}
|
}
|
});
|
// Setup KeyNav that's bound to document to catch all
|
// unhandled/bubbled key events for navigation
|
me.keyNav = new Ext.util.KeyNav(Ext.getDoc(), {
|
disabled: true,
|
scope: me,
|
|
backspace: me.focusLast,
|
enter: me.navigateIn,
|
esc: me.navigateOut,
|
tab: me.navigateSiblings,
|
space: me.navigateIn,
|
del: me.focusLast,
|
left: me.navigateSiblings,
|
right: me.navigateSiblings,
|
down: me.navigateSiblings,
|
up: me.navigateSiblings
|
});
|
|
me.focusData = {};
|
me.subscribers = new Ext.util.HashMap();
|
me.focusChain = {};
|
|
// Setup some ComponentQuery pseudos
|
Ext.apply(CQ.pseudos, {
|
// Return the single next focusable sibling from the current idx in either direction (step -1 or 1)
|
nextFocus: function(cmps, idx, step) {
|
step = step || 1;
|
idx = parseInt(idx, 10);
|
|
var len = cmps.length,
|
i = idx, c;
|
|
for (;;) {
|
// Increment index, and loop round if off either end
|
if ((i += step) >= len) {
|
i = 0;
|
} else if (i < 0) {
|
i = len - 1;
|
}
|
|
// As soon as we loop back to the starting index, give up, there are no focusable siblings.
|
if (i === idx) {
|
return [];
|
}
|
|
// If we have found a focusable sibling, return it
|
if ((c = cmps[i]).isFocusable()) {
|
return [c];
|
}
|
}
|
|
return [];
|
},
|
|
prevFocus: function(cmps, idx) {
|
return this.nextFocus(cmps, idx, -1);
|
},
|
|
root: function(cmps) {
|
var len = cmps.length,
|
results = [],
|
i = 0,
|
c;
|
|
for (; i < len; i++) {
|
c = cmps[i];
|
if (!c.ownerCt) {
|
results.push(c);
|
}
|
}
|
|
return results;
|
}
|
});
|
},
|
|
<span id='Ext-FocusManager-method-addXTypeToWhitelist'> /**
|
</span> * Adds the specified xtype to the {@link #whitelist}.
|
* @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
|
*/
|
addXTypeToWhitelist: function(xtype) {
|
var me = this;
|
|
if (Ext.isArray(xtype)) {
|
Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
|
return;
|
}
|
|
if (!Ext.Array.contains(me.whitelist, xtype)) {
|
me.whitelist.push(xtype);
|
}
|
},
|
|
<span id='Ext-FocusManager-method-clearComponent'> clearComponent: function(cmp) {
|
</span> clearTimeout(this.cmpFocusDelay);
|
if (!cmp.isDestroyed) {
|
cmp.blur();
|
}
|
},
|
|
<span id='Ext-FocusManager-method-disable'> /**
|
</span> * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
|
*/
|
disable: function() {
|
var me = this;
|
|
if (!me.enabled) {
|
return;
|
}
|
|
delete me.options;
|
me.enabled = false;
|
|
me.removeDOM();
|
|
// Stop handling key navigation
|
me.keyNav.disable();
|
|
me.fireEvent('disable', me);
|
},
|
|
<span id='Ext-FocusManager-method-enable'> /**
|
</span> * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
|
* @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object
|
* with the following options:
|
* @param {Boolean} [options.focusFrame=false] `true` to show the focus frame around a component when it is focused.
|
*/
|
enable: function(options) {
|
var me = this;
|
|
if (options === true) {
|
options = { focusFrame: true };
|
}
|
me.options = options = options || {};
|
|
if (me.enabled) {
|
return;
|
}
|
|
// When calling addFocusListener on Containers, the FocusManager must be enabled, otherwise it won't do it.
|
me.enabled = true;
|
me.initDOM(options);
|
|
// Start handling key navigation
|
me.keyNav.enable();
|
|
// Finally, let's focus our global focus el so we start fresh
|
me.focusEl.focus();
|
delete me.focusedCmp;
|
|
me.fireEvent('enable', me);
|
},
|
|
<span id='Ext-FocusManager-method-focusLast'> focusLast: function(e) {
|
</span> var me = this;
|
|
if (me.isWhitelisted(me.focusedCmp)) {
|
return true;
|
}
|
|
// Go back to last focused item
|
if (me.previousFocusedCmp) {
|
me.previousFocusedCmp.focus();
|
}
|
},
|
|
<span id='Ext-FocusManager-method-getRootComponents'> getRootComponents: function() {
|
</span> var CQ = Ext.ComponentQuery,
|
inline = CQ.query(':focusable:root:not([floating])'),
|
floating = CQ.query(':focusable:root[floating]');
|
|
// Floating items should go to the top of our root stack, and be ordered
|
// by their z-index (highest first)
|
floating.sort(function(a, b) {
|
return a.el.getZIndex() > b.el.getZIndex();
|
});
|
|
return floating.concat(inline);
|
},
|
|
<span id='Ext-FocusManager-method-initDOM'> initDOM: function(options) {
|
</span> var me = this,
|
cls = me.focusFrameCls,
|
needListeners = Ext.ComponentQuery.query('{getFocusEl()}:not([focusListenerAdded])'),
|
i = 0, len = needListeners.length;
|
|
if (!Ext.isReady) {
|
return Ext.onReady(me.initDOM, me);
|
}
|
|
// When we are enabled, we must ensure that all Components which return a focusEl that is *not naturally focusable*
|
// have focus/blur listeners enabled to then trigger onFocus/onBlur handling so that we get to know about their focus action.
|
// These listeners are not added at initialization unless the FocusManager is enabled at that time.
|
for (; i < len; i++) {
|
needListeners[i].addFocusListener();
|
}
|
|
// Make the document body the global focus element
|
if (!me.focusEl) {
|
me.focusEl = Ext.getBody();
|
me.focusEl.dom.tabIndex = -1;
|
}
|
|
// Create global focus frame
|
if (!me.focusFrame && options.focusFrame) {
|
me.focusFrame = Ext.getBody().createChild({
|
cls: cls,
|
children: [
|
{ cls: cls + '-top' },
|
{ cls: cls + '-bottom' },
|
{ cls: cls + '-left' },
|
{ cls: cls + '-right' }
|
],
|
style: 'top: -100px; left: -100px;'
|
});
|
me.focusFrame.setVisibilityMode(Ext.Element.DISPLAY);
|
me.focusFrame.hide().setLocalXY(0, 0);
|
}
|
},
|
|
<span id='Ext-FocusManager-method-isWhitelisted'> isWhitelisted: function(cmp) {
|
</span> return cmp && Ext.Array.some(this.whitelist, function(x) {
|
return cmp.isXType(x);
|
});
|
},
|
|
<span id='Ext-FocusManager-method-navigateIn'> navigateIn: function(e) {
|
</span> var me = this,
|
focusedCmp = me.focusedCmp,
|
defaultRoot,
|
firstChild;
|
|
if (me.isWhitelisted(focusedCmp)) {
|
return true;
|
}
|
|
if (!focusedCmp) {
|
// No focus yet, so focus the first root cmp on the page
|
defaultRoot = me.getRootComponents()[0];
|
if (defaultRoot) {
|
// If the default root is based upon the body, then it will already be focused, and will not fire a focus event to
|
// trigger its own onFocus processing, so we have to programatically blur it first.
|
if (defaultRoot.getFocusEl() === me.focusEl) {
|
me.focusEl.blur();
|
}
|
defaultRoot.focus();
|
}
|
} else {
|
// Drill into child ref items of the focused cmp, if applicable.
|
// This works for any Component with a getRefItems implementation.
|
firstChild = focusedCmp.hasFocus ? Ext.ComponentQuery.query('>:focusable', focusedCmp)[0] : focusedCmp;
|
if (firstChild) {
|
firstChild.focus();
|
} else {
|
// Let's try to fire a click event, as if it came from the mouse
|
if (Ext.isFunction(focusedCmp.onClick)) {
|
e.button = 0;
|
focusedCmp.onClick(e);
|
if (focusedCmp.isVisible(true)) {
|
focusedCmp.focus();
|
} else {
|
me.navigateOut();
|
}
|
}
|
}
|
}
|
},
|
|
<span id='Ext-FocusManager-method-navigateOut'> navigateOut: function(e) {
|
</span> var me = this,
|
parent;
|
|
if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
|
me.focusEl.focus();
|
} else {
|
parent.focus();
|
}
|
|
// In some browsers (Chrome) FocusManager can handle this before other
|
// handlers. Ext Windows have their own Esc key handling, so we need to
|
// return true here to allow the event to bubble.
|
return true;
|
},
|
|
<span id='Ext-FocusManager-method-navigateSiblings'> navigateSiblings: function(e, source, parent) {
|
</span> var me = this,
|
src = source || me,
|
key = e.getKey(),
|
EO = Ext.EventObject,
|
goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
|
checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
|
nextSelector = goBack ? 'prev' : 'next',
|
idx, next, focusedCmp, siblings;
|
|
focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
|
if (!focusedCmp && !parent) {
|
return true;
|
}
|
|
if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
|
return true;
|
}
|
|
// If no focused Component, or a root level one was focused, then siblings are root components.
|
if (!focusedCmp || focusedCmp.is(':root')) {
|
siblings = me.getRootComponents();
|
} else {
|
// Else if the focused component has a parent, get siblings from there
|
parent = parent || focusedCmp.up();
|
if (parent) {
|
siblings = parent.getRefItems();
|
}
|
}
|
|
|
// Navigate if we have found siblings.
|
if (siblings) {
|
idx = focusedCmp ? Ext.Array.indexOf(siblings, focusedCmp) : -1;
|
next = Ext.ComponentQuery.query(':' + nextSelector + 'Focus(' + idx + ')', siblings)[0];
|
if (next && focusedCmp !== next) {
|
next.focus();
|
return next;
|
}
|
}
|
},
|
|
<span id='Ext-FocusManager-method-onComponentBlur'> onComponentBlur: function(cmp, e) {
|
</span> var me = this;
|
|
if (me.focusedCmp === cmp) {
|
me.previousFocusedCmp = cmp;
|
delete me.focusedCmp;
|
}
|
|
if (me.focusFrame) {
|
me.focusFrame.hide();
|
}
|
},
|
|
<span id='Ext-FocusManager-method-onComponentFocus'> onComponentFocus: function(cmp, e) {
|
</span> var me = this,
|
chain = me.focusChain,
|
parent;
|
|
if (!cmp.isFocusable()) {
|
me.clearComponent(cmp);
|
|
// Check our focus chain, so we don't run into a never ending recursion
|
// If we've attempted (unsuccessfully) to focus this component before,
|
// then we're caught in a loop of child->parent->...->child and we
|
// need to cut the loop off rather than feed into it.
|
if (chain[cmp.id]) {
|
return;
|
}
|
|
// Try to focus the parent instead
|
parent = cmp.up();
|
if (parent) {
|
// Add component to our focus chain to detect infinite focus loop
|
// before we fire off an attempt to focus our parent.
|
// See the comments above.
|
chain[cmp.id] = true;
|
parent.focus();
|
}
|
|
return;
|
}
|
// Clear our focus chain when we have a focusable component
|
me.focusChain = {};
|
|
// Capture the focusEl to frame now.
|
// Button returns its encapsulating element during the focus phase
|
// So that element gets styled and framed.
|
me.focusTask.delay(10, null, null, [cmp, cmp.getFocusEl()]);
|
},
|
|
<span id='Ext-FocusManager-method-handleComponentFocus'> handleComponentFocus: function(cmp, focusEl) {
|
</span> var me = this,
|
cls,
|
ff,
|
box,
|
bt,
|
bl,
|
bw,
|
bh,
|
ft,
|
fb,
|
fl,
|
fr;
|
|
if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
|
me.clearComponent(cmp);
|
return;
|
}
|
|
me.focusedCmp = cmp;
|
|
// If we have a focus frame, show it around the focused component
|
if (me.shouldShowFocusFrame(cmp)) {
|
cls = '.' + me.focusFrameCls + '-';
|
ff = me.focusFrame;
|
|
// focusEl may in fact be a descendant component to which to delegate focus
|
box = (focusEl.dom ? focusEl : focusEl.el).getBox();
|
|
// Size the focus frame's t/b/l/r according to the box
|
// This leaves a hole in the middle of the frame so user
|
// interaction w/ the mouse can continue
|
bt = box.top;
|
bl = box.left;
|
bw = box.width;
|
bh = box.height;
|
ft = ff.child(cls + 'top');
|
fb = ff.child(cls + 'bottom');
|
fl = ff.child(cls + 'left');
|
fr = ff.child(cls + 'right');
|
|
ft.setWidth(bw).setLocalXY(bl, bt);
|
fb.setWidth(bw).setLocalXY(bl, bt + bh - 2);
|
fl.setHeight(bh - 2).setLocalXY(bl, bt + 2);
|
fr.setHeight(bh - 2).setLocalXY(bl + bw - 2, bt + 2);
|
|
ff.show();
|
}
|
|
me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
|
},
|
|
<span id='Ext-FocusManager-method-onComponentHide'> onComponentHide: function(cmp) {
|
</span> var me = this,
|
cmpHadFocus = false,
|
focusedCmp = me.focusedCmp,
|
parent;
|
|
if (focusedCmp) {
|
// See if the Component being hidden was the focused Component, or owns the focused Component
|
// In these cases, focus needs to be removed from the focused Component to the nearest focusable ancestor
|
cmpHadFocus = cmp.hasFocus || (cmp.isContainer && cmp.isAncestor(me.focusedCmp));
|
}
|
|
me.clearComponent(cmp);
|
|
// Move focus onto the nearest focusable ancestor, or this is there is none
|
if (cmpHadFocus && (parent = cmp.up(':focusable'))) {
|
parent.focus();
|
} else {
|
me.focusEl.focus();
|
}
|
},
|
|
<span id='Ext-FocusManager-method-onComponentDestroy'> onComponentDestroy: function() {
|
</span>
|
},
|
|
<span id='Ext-FocusManager-method-removeDOM'> removeDOM: function() {
|
</span> var me = this;
|
|
// If we are still enabled globally, or there are still subscribers
|
// then we will halt here, since our DOM stuff is still being used
|
if (me.enabled || me.subscribers.length) {
|
return;
|
}
|
|
Ext.destroy(
|
me.focusFrame
|
);
|
delete me.focusEl;
|
delete me.focusFrame;
|
},
|
|
<span id='Ext-FocusManager-method-removeXTypeFromWhitelist'> /**
|
</span> * Removes the specified xtype from the {@link #whitelist}.
|
* @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
|
*/
|
removeXTypeFromWhitelist: function(xtype) {
|
var me = this;
|
|
if (Ext.isArray(xtype)) {
|
Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
|
return;
|
}
|
|
Ext.Array.remove(me.whitelist, xtype);
|
},
|
|
<span id='Ext-FocusManager-method-setupSubscriberKeys'> setupSubscriberKeys: function(container, keys) {
|
</span> var me = this,
|
el = container.getFocusEl(),
|
scope = keys.scope,
|
handlers = {
|
backspace: me.focusLast,
|
enter: me.navigateIn,
|
esc: me.navigateOut,
|
scope: me
|
},
|
|
navSiblings = function(e) {
|
if (me.focusedCmp === container) {
|
// Root the sibling navigation to this container, so that we
|
// can automatically dive into the container, rather than forcing
|
// the user to hit the enter key to dive in.
|
return me.navigateSiblings(e, me, container);
|
} else {
|
return me.navigateSiblings(e);
|
}
|
};
|
|
Ext.iterate(keys, function(key, cb) {
|
handlers[key] = function(e) {
|
var ret = navSiblings(e);
|
|
if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
|
return true;
|
}
|
|
return ret;
|
};
|
}, me);
|
|
return new Ext.util.KeyNav(el, handlers);
|
},
|
|
<span id='Ext-FocusManager-method-shouldShowFocusFrame'> shouldShowFocusFrame: function(cmp) {
|
</span> var me = this,
|
opts = me.options || {};
|
|
// Do not show a focus frame if
|
// 1. We are configured not to.
|
// 2. No Component was passed
|
if (!me.focusFrame || !cmp) {
|
return false;
|
}
|
|
// Global trumps
|
if (opts.focusFrame) {
|
return true;
|
}
|
|
if (me.focusData[cmp.id].focusFrame) {
|
return true;
|
}
|
|
return false;
|
}
|
});</pre>
|
</body>
|
</html>
|