3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.FocusManager
18 The FocusManager is responsible for globally:
20 1. Managing component focus
21 2. Providing basic keyboard navigation
22 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
24 To activate the FocusManager, simply call {@link #enable `Ext.FocusManager.enable();`}. In turn, you may
25 deactivate the FocusManager by subsequently calling {@link #disable `Ext.FocusManager.disable();`}. The
26 FocusManager is disabled by default.
28 To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
30 Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
31 that would like to have navigation between its child {@link Ext.Component}'s. The {@link Ext.container.Container} can simply
32 call {@link #subscribe Ext.FocusManager.subscribe} to take advantage of this feature, and can at any time call
33 {@link #unsubscribe Ext.FocusManager.unsubscribe} to turn the navigation off.
37 * @author Jarred Nicholls <jarred@sencha.com>
38 * @docauthor Jarred Nicholls <jarred@sencha.com>
40 Ext.define('Ext.FocusManager', {
42 alternateClassName: 'Ext.FocusMgr',
45 observable: 'Ext.util.Observable'
49 'Ext.ComponentManager',
56 * @property {Boolean} enabled
57 * Whether or not the FocusManager is currently enabled
62 * @property {Ext.Component} focusedCmp
63 * The currently focused component. Defaults to `undefined`.
67 focusElementCls: Ext.baseCSSPrefix + 'focus-element',
69 focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
72 * @property {Array} whitelist
73 * A list of xtypes that should ignore certain navigation input keys and
74 * allow for the default browser event/behavior. These input keys include:
83 * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
84 * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
85 * the user to move the input cursor left and right, and to delete characters, etc.
87 * This whitelist currently defaults to `['textfield']`.
107 constructor: function() {
109 CQ = Ext.ComponentQuery;
113 * @event beforecomponentfocus
114 * Fires before a component becomes focused. Return `false` to prevent
115 * the component from gaining focus.
116 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
117 * @param {Ext.Component} cmp The component that is being focused
118 * @param {Ext.Component} previousCmp The component that was previously focused,
119 * or `undefined` if there was no previously focused component.
122 'beforecomponentfocus',
125 * @event componentfocus
126 * Fires after a component becomes focused.
127 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
128 * @param {Ext.Component} cmp The component that has been focused
129 * @param {Ext.Component} previousCmp The component that was previously focused,
130 * or `undefined` if there was no previously focused component.
137 * Fires when the FocusManager is disabled
138 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
144 * Fires when the FocusManager is enabled
145 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
150 // Setup KeyNav that's bound to document to catch all
151 // unhandled/bubbled key events for navigation
152 me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), {
156 backspace: me.focusLast,
157 enter: me.navigateIn,
159 tab: me.navigateSiblings
161 //space: me.navigateIn,
163 //left: me.navigateSiblings,
164 //right: me.navigateSiblings,
165 //down: me.navigateSiblings,
166 //up: me.navigateSiblings
170 me.subscribers = Ext.create('Ext.util.HashMap');
173 // Setup some ComponentQuery pseudos
174 Ext.apply(CQ.pseudos, {
175 focusable: function(cmps) {
176 var len = cmps.length,
181 isFocusable = function(x) {
182 return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}');
185 for (; i < len; i++) {
187 if (isFocusable(c)) {
195 nextFocus: function(cmps, idx, step) {
197 idx = parseInt(idx, 10);
199 var len = cmps.length,
203 for (; i != idx; i += step) {
211 if (CQ.is(c, ':focusable')) {
213 } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
214 return [c.placeholder];
221 prevFocus: function(cmps, idx) {
222 return this.nextFocus(cmps, idx, -1);
225 root: function(cmps) {
226 var len = cmps.length,
231 for (; i < len; i++) {
244 * Adds the specified xtype to the {@link #whitelist}.
245 * @param {String/Array} xtype Adds the xtype(s) to the {@link #whitelist}.
247 addXTypeToWhitelist: function(xtype) {
250 if (Ext.isArray(xtype)) {
251 Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
255 if (!Ext.Array.contains(me.whitelist, xtype)) {
256 me.whitelist.push(xtype);
260 clearComponent: function(cmp) {
261 clearTimeout(this.cmpFocusDelay);
262 if (!cmp.isDestroyed) {
268 * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
270 disable: function() {
280 Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
284 // Stop handling key navigation
287 // disable focus for all components
288 me.setFocusAll(false);
290 me.fireEvent('disable', me);
294 * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
295 * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options:
296 - focusFrame : Boolean
297 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
300 enable: function(options) {
303 if (options === true) {
304 options = { focusFrame: true };
306 me.options = options = options || {};
312 // Handle components that are newly added after we are enabled
313 Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
317 // Start handling key navigation
320 // enable focus for all components
321 me.setFocusAll(true, options);
323 // Finally, let's focus our global focus el so we start fresh
325 delete me.focusedCmp;
328 me.fireEvent('enable', me);
331 focusLast: function(e) {
334 if (me.isWhitelisted(me.focusedCmp)) {
338 // Go back to last focused item
339 if (me.previousFocusedCmp) {
340 me.previousFocusedCmp.focus();
344 getRootComponents: function() {
346 CQ = Ext.ComponentQuery,
347 inline = CQ.query(':focusable:root:not([floating])'),
348 floating = CQ.query(':focusable:root[floating]');
350 // Floating items should go to the top of our root stack, and be ordered
351 // by their z-index (highest first)
352 floating.sort(function(a, b) {
353 return a.el.getZIndex() > b.el.getZIndex();
356 return floating.concat(inline);
359 initDOM: function(options) {
362 cls = me.focusFrameCls;
365 Ext.onReady(me.initDOM, me);
369 // Create global focus element
371 me.focusEl = Ext.getBody().createChild({
373 cls: me.focusElementCls,
378 // Create global focus frame
379 if (!me.focusFrame && options.focusFrame) {
380 me.focusFrame = Ext.getBody().createChild({
383 { cls: cls + '-top' },
384 { cls: cls + '-bottom' },
385 { cls: cls + '-left' },
386 { cls: cls + '-right' }
388 style: 'top: -100px; left: -100px;'
390 me.focusFrame.setVisibilityMode(Ext.core.Element.DISPLAY);
391 me.focusFrameWidth = me.focusFrame.child('.' + cls + '-top').getHeight();
392 me.focusFrame.hide().setLeftTop(0, 0);
396 isWhitelisted: function(cmp) {
397 return cmp && Ext.Array.some(this.whitelist, function(x) {
398 return cmp.isXType(x);
402 navigateIn: function(e) {
404 focusedCmp = me.focusedCmp,
409 // No focus yet, so focus the first root cmp on the page
410 rootCmps = me.getRootComponents();
411 if (rootCmps.length) {
415 // Drill into child ref items of the focused cmp, if applicable.
416 // This works for any Component with a getRefItems implementation.
417 firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0];
421 // Let's try to fire a click event, as if it came from the mouse
422 if (Ext.isFunction(focusedCmp.onClick)) {
424 focusedCmp.onClick(e);
431 navigateOut: function(e) {
435 if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
441 // In some browsers (Chrome) FocusManager can handle this before other
442 // handlers. Ext Windows have their own Esc key handling, so we need to
443 // return true here to allow the event to bubble.
447 navigateSiblings: function(e, source, parent) {
451 EO = Ext.EventObject,
452 goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
453 checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
454 nextSelector = goBack ? 'prev' : 'next',
455 idx, next, focusedCmp;
457 focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
458 if (!focusedCmp && !parent) {
462 if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
466 parent = parent || focusedCmp.up();
468 idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1;
469 next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0];
470 if (next && focusedCmp !== next) {
477 onComponentBlur: function(cmp, e) {
480 if (me.focusedCmp === cmp) {
481 me.previousFocusedCmp = cmp;
482 delete me.focusedCmp;
486 me.focusFrame.hide();
490 onComponentCreated: function(hash, id, cmp) {
491 this.setFocus(cmp, true, this.options);
494 onComponentDestroy: function(cmp) {
495 this.setFocus(cmp, false);
498 onComponentFocus: function(cmp, e) {
500 chain = me.focusChain;
502 if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
503 me.clearComponent(cmp);
505 // Check our focus chain, so we don't run into a never ending recursion
506 // If we've attempted (unsuccessfully) to focus this component before,
507 // then we're caught in a loop of child->parent->...->child and we
508 // need to cut the loop off rather than feed into it.
513 // Try to focus the parent instead
514 var parent = cmp.up();
516 // Add component to our focus chain to detect infinite focus loop
517 // before we fire off an attempt to focus our parent.
518 // See the comments above.
519 chain[cmp.id] = true;
526 // Clear our focus chain when we have a focusable component
529 // Defer focusing for 90ms so components can do a layout/positioning
530 // and give us an ability to buffer focuses
531 clearTimeout(me.cmpFocusDelay);
532 if (arguments.length !== 2) {
533 me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]);
537 if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
538 me.clearComponent(cmp);
544 // If we have a focus frame, show it around the focused component
545 if (me.shouldShowFocusFrame(cmp)) {
546 var cls = '.' + me.focusFrameCls + '-',
548 fw = me.focusFrameWidth,
549 box = cmp.el.getPageBox(),
551 // Size the focus frame's t/b/l/r according to the box
552 // This leaves a hole in the middle of the frame so user
553 // interaction w/ the mouse can continue
558 ft = ff.child(cls + 'top'),
559 fb = ff.child(cls + 'bottom'),
560 fl = ff.child(cls + 'left'),
561 fr = ff.child(cls + 'right');
563 ft.setWidth(bw - 2).setLeftTop(bl + 1, bt);
564 fb.setWidth(bw - 2).setLeftTop(bl + 1, bt + bh - fw);
565 fl.setHeight(bh - 2).setLeftTop(bl, bt + 1);
566 fr.setHeight(bh - 2).setLeftTop(bl + bw - fw, bt + 1);
571 me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
574 onComponentHide: function(cmp) {
576 CQ = Ext.ComponentQuery,
582 focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
583 cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
586 me.clearComponent(focusedCmp);
590 me.clearComponent(cmp);
593 parent = CQ.query('^:focusable', cmp)[0];
600 removeDOM: function() {
603 // If we are still enabled globally, or there are still subscribers
604 // then we will halt here, since our DOM stuff is still being used
605 if (me.enabled || me.subscribers.length) {
614 delete me.focusFrame;
615 delete me.focusFrameWidth;
619 * Removes the specified xtype from the {@link #whitelist}.
620 * @param {String/Array} xtype Removes the xtype(s) from the {@link #whitelist}.
622 removeXTypeFromWhitelist: function(xtype) {
625 if (Ext.isArray(xtype)) {
626 Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
630 Ext.Array.remove(me.whitelist, xtype);
633 setFocus: function(cmp, focusable, options) {
637 needsTabIndex = function(n) {
638 return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
642 options = options || {};
644 // Come back and do this after the component is rendered
646 cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
650 el = cmp.getFocusEl();
653 // Decorate the component's focus el for focus-ability
654 if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
657 focusFrame: options.focusFrame
660 // Only set -1 tabIndex if we need it
661 // inputs, buttons, and anchor tags do not need it,
662 // and neither does any DOM that has it set already
663 // programmatically or in markup.
664 if (needsTabIndex(dom)) {
665 data.tabIndex = dom.tabIndex;
670 focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
671 blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
675 hide: me.onComponentHide,
676 close: me.onComponentHide,
677 beforedestroy: me.onComponentDestroy,
681 me.focusData[cmp.id] = data;
683 data = me.focusData[cmp.id];
684 if ('tabIndex' in data) {
685 dom.tabIndex = data.tabIndex;
687 el.un('focus', data.focusFn, me);
688 el.un('blur', data.blurFn, me);
689 cmp.un('hide', me.onComponentHide, me);
690 cmp.un('close', me.onComponentHide, me);
691 cmp.un('beforedestroy', me.onComponentDestroy, me);
693 delete me.focusData[cmp.id];
698 setFocusAll: function(focusable, options) {
700 cmps = Ext.ComponentManager.all.getArray(),
705 for (; i < len; i++) {
706 me.setFocus(cmps[i], focusable, options);
710 setupSubscriberKeys: function(container, keys) {
712 el = container.getFocusEl(),
715 backspace: me.focusLast,
716 enter: me.navigateIn,
721 navSiblings = function(e) {
722 if (me.focusedCmp === container) {
723 // Root the sibling navigation to this container, so that we
724 // can automatically dive into the container, rather than forcing
725 // the user to hit the enter key to dive in.
726 return me.navigateSiblings(e, me, container);
728 return me.navigateSiblings(e);
732 Ext.iterate(keys, function(key, cb) {
733 handlers[key] = function(e) {
734 var ret = navSiblings(e);
736 if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
744 return Ext.create('Ext.util.KeyNav', el, handlers);
747 shouldShowFocusFrame: function(cmp) {
749 opts = me.options || {};
751 if (!me.focusFrame || !cmp) {
756 if (opts.focusFrame) {
760 if (me.focusData[cmp.id].focusFrame) {
768 * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s.
769 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management.
770 * @param {Boolean/Object} options An object of the following options:
771 - keys : Array/Object
772 An array containing the string names of navigation keys to be supported. The allowed values are:
779 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.:
782 left: this.onLeftKey,
783 right: this.onRightKey,
787 - focusFrame : Boolean (optional)
788 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
791 subscribe: function(container, options) {
795 subs = me.subscribers,
797 // Recursively add focus ability as long as a descendent container isn't
798 // itself subscribed to the FocusManager, or else we'd have unwanted side
799 // effects for subscribing a descendent container twice.
800 safeSetFocus = function(cmp) {
801 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
802 EA.forEach(cmp.query('>'), safeSetFocus);
803 me.setFocus(cmp, true, options);
804 cmp.on('add', data.onAdd, me);
805 } else if (!cmp.isContainer) {
806 me.setFocus(cmp, true, options);
810 // We only accept containers
811 if (!container || !container.isContainer) {
815 if (!container.rendered) {
816 container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
820 // Init the DOM, incase this is the first time it will be used
823 // Create key navigation for subscriber based on keys option
824 data.keyNav = me.setupSubscriberKeys(container, options.keys);
826 // We need to keep track of components being added to our subscriber
827 // and any containers nested deeply within it (omg), so let's do that.
828 // Components that are removed are globally handled.
829 // Also keep track of destruction of our container for auto-unsubscribe.
830 data.onAdd = function(ct, cmp, idx) {
833 container.on('beforedestroy', me.unsubscribe, me);
835 // Now we setup focusing abilities for the container and all its components
836 safeSetFocus(container);
838 // Add to our subscribers list
839 subs.add(container.id, data);
843 * Unsubscribes an {@link Ext.container.Container} from keyboard focus management.
844 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager.
847 unsubscribe: function(container) {
850 subs = me.subscribers,
853 // Recursively remove focus ability as long as a descendent container isn't
854 // itself subscribed to the FocusManager, or else we'd have unwanted side
855 // effects for unsubscribing an ancestor container.
856 safeSetFocus = function(cmp) {
857 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
858 EA.forEach(cmp.query('>'), safeSetFocus);
859 me.setFocus(cmp, false);
860 cmp.un('add', data.onAdd, me);
861 } else if (!cmp.isContainer) {
862 me.setFocus(cmp, false);
866 if (!container || !subs.containsKey(container.id)) {
870 data = subs.get(container.id);
871 data.keyNav.destroy();
872 container.un('beforedestroy', me.unsubscribe, me);
873 subs.removeAtKey(container.id);
874 safeSetFocus(container);