1 <!DOCTYPE html><html><head><title>Sencha Documentation Project</title><link rel="stylesheet" href="../reset.css" type="text/css"><link rel="stylesheet" href="../prettify.css" type="text/css"><link rel="stylesheet" href="../prettify_sa.css" type="text/css"><script type="text/javascript" src="../prettify.js"></script></head><body onload="prettyPrint()"><pre class="prettyprint"><pre><span id='Ext-FocusManager'>/**
2 </span> * @class Ext.FocusManager
4 The FocusManager is responsible for globally:
6 1. Managing component focus
7 2. Providing basic keyboard navigation
8 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
10 To activate the FocusManager, simply call {@link #enable `Ext.FocusManager.enable();`}. In turn, you may
11 deactivate the FocusManager by subsequently calling {@link #disable `Ext.FocusManager.disable();`}. The
12 FocusManager is disabled by default.
14 To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
16 Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
17 that would like to have navigation between its child {@link Ext.Component}'s. The {@link Ext.container.Container} can simply
18 call {@link #subscribe Ext.FocusManager.subscribe} to take advantage of this feature, and can at any time call
19 {@link #unsubscribe Ext.FocusManager.unsubscribe} to turn the navigation off.
23 * @author Jarred Nicholls <jarred@sencha.com>
24 * @docauthor Jarred Nicholls <jarred@sencha.com>
26 Ext.define('Ext.FocusManager', {
28 alternateClassName: 'Ext.FocusMgr',
31 observable: 'Ext.util.Observable'
35 'Ext.ComponentManager',
41 <span id='Ext-FocusManager-property-enabled'> /**
42 </span> * @property {Boolean} enabled
43 * Whether or not the FocusManager is currently enabled
47 <span id='Ext-FocusManager-property-focusedCmp'> /**
48 </span> * @property {Ext.Component} focusedCmp
49 * The currently focused component. Defaults to `undefined`.
53 focusElementCls: Ext.baseCSSPrefix + 'focus-element',
55 focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
57 <span id='Ext-FocusManager-property-whitelist'> /**
58 </span> * @property {Array} whitelist
59 * A list of xtypes that should ignore certain navigation input keys and
60 * allow for the default browser event/behavior. These input keys include:
69 * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
70 * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
71 * the user to move the input cursor left and right, and to delete characters, etc.
73 * This whitelist currently defaults to `['textfield']`.
93 constructor: function() {
95 CQ = Ext.ComponentQuery;
98 <span id='Ext-FocusManager-event-beforecomponentfocus'> /**
99 </span> * @event beforecomponentfocus
100 * Fires before a component becomes focused. Return `false` to prevent
101 * the component from gaining focus.
102 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
103 * @param {Ext.Component} cmp The component that is being focused
104 * @param {Ext.Component} previousCmp The component that was previously focused,
105 * or `undefined` if there was no previously focused component.
108 'beforecomponentfocus',
110 <span id='Ext-FocusManager-event-componentfocus'> /**
111 </span> * @event componentfocus
112 * Fires after a component becomes focused.
113 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
114 * @param {Ext.Component} cmp The component that has been focused
115 * @param {Ext.Component} previousCmp The component that was previously focused,
116 * or `undefined` if there was no previously focused component.
121 <span id='Ext-FocusManager-event-disable'> /**
122 </span> * @event disable
123 * Fires when the FocusManager is disabled
124 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
128 <span id='Ext-FocusManager-event-enable'> /**
129 </span> * @event enable
130 * Fires when the FocusManager is enabled
131 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
136 // Setup KeyNav that's bound to document to catch all
137 // unhandled/bubbled key events for navigation
138 me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), {
142 backspace: me.focusLast,
143 enter: me.navigateIn,
145 tab: me.navigateSiblings
147 //space: me.navigateIn,
149 //left: me.navigateSiblings,
150 //right: me.navigateSiblings,
151 //down: me.navigateSiblings,
152 //up: me.navigateSiblings
156 me.subscribers = Ext.create('Ext.util.HashMap');
159 // Setup some ComponentQuery pseudos
160 Ext.apply(CQ.pseudos, {
161 focusable: function(cmps) {
162 var len = cmps.length,
167 isFocusable = function(x) {
168 return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}');
171 for (; i < len; i++) {
173 if (isFocusable(c)) {
181 nextFocus: function(cmps, idx, step) {
183 idx = parseInt(idx, 10);
185 var len = cmps.length,
189 for (; i != idx; i += step) {
192 } else if (i < 0) {
197 if (CQ.is(c, ':focusable')) {
199 } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
200 return [c.placeholder];
207 prevFocus: function(cmps, idx) {
208 return this.nextFocus(cmps, idx, -1);
211 root: function(cmps) {
212 var len = cmps.length,
217 for (; i < len; i++) {
229 <span id='Ext-FocusManager-method-addXTypeToWhitelist'> /**
230 </span> * Adds the specified xtype to the {@link #whitelist}.
231 * @param {String/Array} xtype Adds the xtype(s) to the {@link #whitelist}.
233 addXTypeToWhitelist: function(xtype) {
236 if (Ext.isArray(xtype)) {
237 Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
241 if (!Ext.Array.contains(me.whitelist, xtype)) {
242 me.whitelist.push(xtype);
246 clearComponent: function(cmp) {
247 clearTimeout(this.cmpFocusDelay);
248 if (!cmp.isDestroyed) {
253 <span id='Ext-FocusManager-method-disable'> /**
254 </span> * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
256 disable: function() {
266 Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
270 // Stop handling key navigation
273 // disable focus for all components
274 me.setFocusAll(false);
276 me.fireEvent('disable', me);
279 <span id='Ext-FocusManager-method-enable'> /**
280 </span> * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
281 * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options:
282 - focusFrame : Boolean
283 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
286 enable: function(options) {
289 if (options === true) {
290 options = { focusFrame: true };
292 me.options = options = options || {};
298 // Handle components that are newly added after we are enabled
299 Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
303 // Start handling key navigation
306 // enable focus for all components
307 me.setFocusAll(true, options);
309 // Finally, let's focus our global focus el so we start fresh
311 delete me.focusedCmp;
314 me.fireEvent('enable', me);
317 focusLast: function(e) {
320 if (me.isWhitelisted(me.focusedCmp)) {
324 // Go back to last focused item
325 if (me.previousFocusedCmp) {
326 me.previousFocusedCmp.focus();
330 getRootComponents: function() {
332 CQ = Ext.ComponentQuery,
333 inline = CQ.query(':focusable:root:not([floating])'),
334 floating = CQ.query(':focusable:root[floating]');
336 // Floating items should go to the top of our root stack, and be ordered
337 // by their z-index (highest first)
338 floating.sort(function(a, b) {
339 return a.el.getZIndex() > b.el.getZIndex();
342 return floating.concat(inline);
345 initDOM: function(options) {
348 cls = me.focusFrameCls;
351 Ext.onReady(me.initDOM, me);
355 // Create global focus element
357 me.focusEl = Ext.getBody().createChild({
359 cls: me.focusElementCls,
364 // Create global focus frame
365 if (!me.focusFrame && options.focusFrame) {
366 me.focusFrame = Ext.getBody().createChild({
369 { cls: cls + '-top' },
370 { cls: cls + '-bottom' },
371 { cls: cls + '-left' },
372 { cls: cls + '-right' }
374 style: 'top: -100px; left: -100px;'
376 me.focusFrame.setVisibilityMode(Ext.core.Element.DISPLAY);
377 me.focusFrameWidth = me.focusFrame.child('.' + cls + '-top').getHeight();
378 me.focusFrame.hide().setLeftTop(0, 0);
382 isWhitelisted: function(cmp) {
383 return cmp && Ext.Array.some(this.whitelist, function(x) {
384 return cmp.isXType(x);
388 navigateIn: function(e) {
390 focusedCmp = me.focusedCmp,
395 // No focus yet, so focus the first root cmp on the page
396 rootCmps = me.getRootComponents();
397 if (rootCmps.length) {
401 // Drill into child ref items of the focused cmp, if applicable.
402 // This works for any Component with a getRefItems implementation.
403 firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0];
407 // Let's try to fire a click event, as if it came from the mouse
408 if (Ext.isFunction(focusedCmp.onClick)) {
410 focusedCmp.onClick(e);
417 navigateOut: function(e) {
421 if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
429 navigateSiblings: function(e, source, parent) {
433 EO = Ext.EventObject,
434 goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
435 checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
436 nextSelector = goBack ? 'prev' : 'next',
437 idx, next, focusedCmp;
439 focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
440 if (!focusedCmp && !parent) {
444 if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
448 parent = parent || focusedCmp.up();
450 idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1;
451 next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0];
452 if (next && focusedCmp !== next) {
459 onComponentBlur: function(cmp, e) {
462 if (me.focusedCmp === cmp) {
463 me.previousFocusedCmp = cmp;
464 delete me.focusedCmp;
468 me.focusFrame.hide();
472 onComponentCreated: function(hash, id, cmp) {
473 this.setFocus(cmp, true, this.options);
476 onComponentDestroy: function(cmp) {
477 this.setFocus(cmp, false);
480 onComponentFocus: function(cmp, e) {
482 chain = me.focusChain;
484 if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
485 me.clearComponent(cmp);
487 // Check our focus chain, so we don't run into a never ending recursion
488 // If we've attempted (unsuccessfully) to focus this component before,
489 // then we're caught in a loop of child->parent->...->child and we
490 // need to cut the loop off rather than feed into it.
495 // Try to focus the parent instead
496 var parent = cmp.up();
498 // Add component to our focus chain to detect infinite focus loop
499 // before we fire off an attempt to focus our parent.
500 // See the comments above.
501 chain[cmp.id] = true;
508 // Clear our focus chain when we have a focusable component
511 // Defer focusing for 90ms so components can do a layout/positioning
512 // and give us an ability to buffer focuses
513 clearTimeout(me.cmpFocusDelay);
514 if (arguments.length !== 2) {
515 me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]);
519 if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
520 me.clearComponent(cmp);
526 // If we have a focus frame, show it around the focused component
527 if (me.shouldShowFocusFrame(cmp)) {
528 var cls = '.' + me.focusFrameCls + '-',
530 fw = me.focusFrameWidth,
531 box = cmp.el.getPageBox(),
533 // Size the focus frame's t/b/l/r according to the box
534 // This leaves a hole in the middle of the frame so user
535 // interaction w/ the mouse can continue
540 ft = ff.child(cls + 'top'),
541 fb = ff.child(cls + 'bottom'),
542 fl = ff.child(cls + 'left'),
543 fr = ff.child(cls + 'right');
545 ft.setWidth(bw - 2).setLeftTop(bl + 1, bt);
546 fb.setWidth(bw - 2).setLeftTop(bl + 1, bt + bh - fw);
547 fl.setHeight(bh - 2).setLeftTop(bl, bt + 1);
548 fr.setHeight(bh - 2).setLeftTop(bl + bw - fw, bt + 1);
553 me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
556 onComponentHide: function(cmp) {
558 CQ = Ext.ComponentQuery,
564 focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
565 cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
568 me.clearComponent(focusedCmp);
572 me.clearComponent(cmp);
575 parent = CQ.query('^:focusable', cmp)[0];
582 removeDOM: function() {
585 // If we are still enabled globally, or there are still subscribers
586 // then we will halt here, since our DOM stuff is still being used
587 if (me.enabled || me.subscribers.length) {
596 delete me.focusFrame;
597 delete me.focusFrameWidth;
600 <span id='Ext-FocusManager-method-removeXTypeFromWhitelist'> /**
601 </span> * Removes the specified xtype from the {@link #whitelist}.
602 * @param {String/Array} xtype Removes the xtype(s) from the {@link #whitelist}.
604 removeXTypeFromWhitelist: function(xtype) {
607 if (Ext.isArray(xtype)) {
608 Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
612 Ext.Array.remove(me.whitelist, xtype);
615 setFocus: function(cmp, focusable, options) {
619 needsTabIndex = function(n) {
620 return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
621 && n.tabIndex <= 0;
624 options = options || {};
626 // Come back and do this after the component is rendered
628 cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
632 el = cmp.getFocusEl();
635 // Decorate the component's focus el for focus-ability
636 if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
639 focusFrame: options.focusFrame
642 // Only set -1 tabIndex if we need it
643 // inputs, buttons, and anchor tags do not need it,
644 // and neither does any DOM that has it set already
645 // programmatically or in markup.
646 if (needsTabIndex(dom)) {
647 data.tabIndex = dom.tabIndex;
652 focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
653 blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
657 hide: me.onComponentHide,
658 close: me.onComponentHide,
659 beforedestroy: me.onComponentDestroy,
663 me.focusData[cmp.id] = data;
665 data = me.focusData[cmp.id];
666 if ('tabIndex' in data) {
667 dom.tabIndex = data.tabIndex;
669 el.un('focus', data.focusFn, me);
670 el.un('blur', data.blurFn, me);
671 cmp.un('hide', me.onComponentHide, me);
672 cmp.un('close', me.onComponentHide, me);
673 cmp.un('beforedestroy', me.onComponentDestroy, me);
675 delete me.focusData[cmp.id];
680 setFocusAll: function(focusable, options) {
682 cmps = Ext.ComponentManager.all.getArray(),
687 for (; i < len; i++) {
688 me.setFocus(cmps[i], focusable, options);
692 setupSubscriberKeys: function(container, keys) {
694 el = container.getFocusEl(),
697 backspace: me.focusLast,
698 enter: me.navigateIn,
703 navSiblings = function(e) {
704 if (me.focusedCmp === container) {
705 // Root the sibling navigation to this container, so that we
706 // can automatically dive into the container, rather than forcing
707 // the user to hit the enter key to dive in.
708 return me.navigateSiblings(e, me, container);
710 return me.navigateSiblings(e);
714 Ext.iterate(keys, function(key, cb) {
715 handlers[key] = function(e) {
716 var ret = navSiblings(e);
718 if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
726 return Ext.create('Ext.util.KeyNav', el, handlers);
729 shouldShowFocusFrame: function(cmp) {
731 opts = me.options || {};
733 if (!me.focusFrame || !cmp) {
738 if (opts.focusFrame) {
742 if (me.focusData[cmp.id].focusFrame) {
749 <span id='Ext-FocusManager-method-subscribe'> /**
750 </span> * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s.
751 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management.
752 * @param {Boolean/Object} options An object of the following options:
753 - keys : Array/Object
754 An array containing the string names of navigation keys to be supported. The allowed values are:
761 Or, an object containing those key names as keys with `true` or a callback function as their value. A scope may also be passed. E.g.:
764 left: this.onLeftKey,
765 right: this.onRightKey,
769 - focusFrame : Boolean (optional)
770 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
773 subscribe: function(container, options) {
777 subs = me.subscribers,
779 // Recursively add focus ability as long as a descendent container isn't
780 // itself subscribed to the FocusManager, or else we'd have unwanted side
781 // effects for subscribing a descendent container twice.
782 safeSetFocus = function(cmp) {
783 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
784 EA.forEach(cmp.query('>'), safeSetFocus);
785 me.setFocus(cmp, true, options);
786 cmp.on('add', data.onAdd, me);
787 } else if (!cmp.isContainer) {
788 me.setFocus(cmp, true, options);
792 // We only accept containers
793 if (!container || !container.isContainer) {
797 if (!container.rendered) {
798 container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
802 // Init the DOM, incase this is the first time it will be used
805 // Create key navigation for subscriber based on keys option
806 data.keyNav = me.setupSubscriberKeys(container, options.keys);
808 // We need to keep track of components being added to our subscriber
809 // and any containers nested deeply within it (omg), so let's do that.
810 // Components that are removed are globally handled.
811 // Also keep track of destruction of our container for auto-unsubscribe.
812 data.onAdd = function(ct, cmp, idx) {
815 container.on('beforedestroy', me.unsubscribe, me);
817 // Now we setup focusing abilities for the container and all its components
818 safeSetFocus(container);
820 // Add to our subscribers list
821 subs.add(container.id, data);
824 <span id='Ext-FocusManager-method-unsubscribe'> /**
825 </span> * Unsubscribes an {@link Ext.container.Container} from keyboard focus management.
826 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager.
829 unsubscribe: function(container) {
832 subs = me.subscribers,
835 // Recursively remove focus ability as long as a descendent container isn't
836 // itself subscribed to the FocusManager, or else we'd have unwanted side
837 // effects for unsubscribing an ancestor container.
838 safeSetFocus = function(cmp) {
839 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
840 EA.forEach(cmp.query('>'), safeSetFocus);
841 me.setFocus(cmp, false);
842 cmp.un('add', data.onAdd, me);
843 } else if (!cmp.isContainer) {
844 me.setFocus(cmp, false);
848 if (!container || !subs.containsKey(container.id)) {
852 data = subs.get(container.id);
853 data.keyNav.destroy();
854 container.un('beforedestroy', me.unsubscribe, me);
855 subs.removeAtKey(container.id);
856 safeSetFocus(container);
859 });</pre></pre></body></html>