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 `Ext.FocusManager.enable();`. In turn, you may
25 deactivate the FocusManager by subsequently calling `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.
36 * @author Jarred Nicholls <jarred@sencha.com>
37 * @docauthor Jarred Nicholls <jarred@sencha.com>
39 Ext.define('Ext.FocusManager', {
41 alternateClassName: 'Ext.FocusMgr',
44 observable: 'Ext.util.Observable'
48 'Ext.ComponentManager',
55 * @property {Boolean} enabled
56 * Whether or not the FocusManager is currently enabled
61 * @property {Ext.Component} focusedCmp
62 * The currently focused component. Defaults to `undefined`.
65 focusElementCls: Ext.baseCSSPrefix + 'focus-element',
67 focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
70 * @property {String[]} whitelist
71 * A list of xtypes that should ignore certain navigation input keys and
72 * allow for the default browser event/behavior. These input keys include:
81 * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
82 * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
83 * the user to move the input cursor left and right, and to delete characters, etc.
102 constructor: function() {
104 CQ = Ext.ComponentQuery;
108 * @event beforecomponentfocus
109 * Fires before a component becomes focused. Return `false` to prevent
110 * the component from gaining focus.
111 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
112 * @param {Ext.Component} cmp The component that is being focused
113 * @param {Ext.Component} previousCmp The component that was previously focused,
114 * or `undefined` if there was no previously focused component.
116 'beforecomponentfocus',
119 * @event componentfocus
120 * Fires after a component becomes focused.
121 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
122 * @param {Ext.Component} cmp The component that has been focused
123 * @param {Ext.Component} previousCmp The component that was previously focused,
124 * or `undefined` if there was no previously focused component.
130 * Fires when the FocusManager is disabled
131 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
137 * Fires when the FocusManager is enabled
138 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
143 // Setup KeyNav that's bound to document to catch all
144 // unhandled/bubbled key events for navigation
145 me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), {
149 backspace: me.focusLast,
150 enter: me.navigateIn,
152 tab: me.navigateSiblings
154 //space: me.navigateIn,
156 //left: me.navigateSiblings,
157 //right: me.navigateSiblings,
158 //down: me.navigateSiblings,
159 //up: me.navigateSiblings
163 me.subscribers = Ext.create('Ext.util.HashMap');
166 // Setup some ComponentQuery pseudos
167 Ext.apply(CQ.pseudos, {
168 focusable: function(cmps) {
169 var len = cmps.length,
174 isFocusable = function(x) {
175 return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}');
178 for (; i < len; i++) {
180 if (isFocusable(c)) {
188 nextFocus: function(cmps, idx, step) {
190 idx = parseInt(idx, 10);
192 var len = cmps.length,
196 for (; i != idx; i += step) {
204 if (CQ.is(c, ':focusable')) {
206 } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
207 return [c.placeholder];
214 prevFocus: function(cmps, idx) {
215 return this.nextFocus(cmps, idx, -1);
218 root: function(cmps) {
219 var len = cmps.length,
224 for (; i < len; i++) {
237 * Adds the specified xtype to the {@link #whitelist}.
238 * @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
240 addXTypeToWhitelist: function(xtype) {
243 if (Ext.isArray(xtype)) {
244 Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
248 if (!Ext.Array.contains(me.whitelist, xtype)) {
249 me.whitelist.push(xtype);
253 clearComponent: function(cmp) {
254 clearTimeout(this.cmpFocusDelay);
255 if (!cmp.isDestroyed) {
261 * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
263 disable: function() {
273 Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
277 // Stop handling key navigation
280 // disable focus for all components
281 me.setFocusAll(false);
283 me.fireEvent('disable', me);
287 * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
288 * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options:
289 - focusFrame : Boolean
290 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
293 enable: function(options) {
296 if (options === true) {
297 options = { focusFrame: true };
299 me.options = options = options || {};
305 // Handle components that are newly added after we are enabled
306 Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
310 // Start handling key navigation
313 // enable focus for all components
314 me.setFocusAll(true, options);
316 // Finally, let's focus our global focus el so we start fresh
318 delete me.focusedCmp;
321 me.fireEvent('enable', me);
324 focusLast: function(e) {
327 if (me.isWhitelisted(me.focusedCmp)) {
331 // Go back to last focused item
332 if (me.previousFocusedCmp) {
333 me.previousFocusedCmp.focus();
337 getRootComponents: function() {
339 CQ = Ext.ComponentQuery,
340 inline = CQ.query(':focusable:root:not([floating])'),
341 floating = CQ.query(':focusable:root[floating]');
343 // Floating items should go to the top of our root stack, and be ordered
344 // by their z-index (highest first)
345 floating.sort(function(a, b) {
346 return a.el.getZIndex() > b.el.getZIndex();
349 return floating.concat(inline);
352 initDOM: function(options) {
355 cls = me.focusFrameCls;
358 Ext.onReady(me.initDOM, me);
362 // Create global focus element
364 me.focusEl = Ext.getBody().createChild({
366 cls: me.focusElementCls,
371 // Create global focus frame
372 if (!me.focusFrame && options.focusFrame) {
373 me.focusFrame = Ext.getBody().createChild({
376 { cls: cls + '-top' },
377 { cls: cls + '-bottom' },
378 { cls: cls + '-left' },
379 { cls: cls + '-right' }
381 style: 'top: -100px; left: -100px;'
383 me.focusFrame.setVisibilityMode(Ext.Element.DISPLAY);
384 me.focusFrameWidth = 2;
385 me.focusFrame.hide().setLeftTop(0, 0);
389 isWhitelisted: function(cmp) {
390 return cmp && Ext.Array.some(this.whitelist, function(x) {
391 return cmp.isXType(x);
395 navigateIn: function(e) {
397 focusedCmp = me.focusedCmp,
402 // No focus yet, so focus the first root cmp on the page
403 rootCmps = me.getRootComponents();
404 if (rootCmps.length) {
408 // Drill into child ref items of the focused cmp, if applicable.
409 // This works for any Component with a getRefItems implementation.
410 firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0];
414 // Let's try to fire a click event, as if it came from the mouse
415 if (Ext.isFunction(focusedCmp.onClick)) {
417 focusedCmp.onClick(e);
424 navigateOut: function(e) {
428 if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
434 // In some browsers (Chrome) FocusManager can handle this before other
435 // handlers. Ext Windows have their own Esc key handling, so we need to
436 // return true here to allow the event to bubble.
440 navigateSiblings: function(e, source, parent) {
444 EO = Ext.EventObject,
445 goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
446 checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
447 nextSelector = goBack ? 'prev' : 'next',
448 idx, next, focusedCmp;
450 focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
451 if (!focusedCmp && !parent) {
455 if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
459 parent = parent || focusedCmp.up();
461 idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1;
462 next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0];
463 if (next && focusedCmp !== next) {
470 onComponentBlur: function(cmp, e) {
473 if (me.focusedCmp === cmp) {
474 me.previousFocusedCmp = cmp;
475 delete me.focusedCmp;
479 me.focusFrame.hide();
483 onComponentCreated: function(hash, id, cmp) {
484 this.setFocus(cmp, true, this.options);
487 onComponentDestroy: function(cmp) {
488 this.setFocus(cmp, false);
491 onComponentFocus: function(cmp, e) {
493 chain = me.focusChain;
495 if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
496 me.clearComponent(cmp);
498 // Check our focus chain, so we don't run into a never ending recursion
499 // If we've attempted (unsuccessfully) to focus this component before,
500 // then we're caught in a loop of child->parent->...->child and we
501 // need to cut the loop off rather than feed into it.
506 // Try to focus the parent instead
507 var parent = cmp.up();
509 // Add component to our focus chain to detect infinite focus loop
510 // before we fire off an attempt to focus our parent.
511 // See the comments above.
512 chain[cmp.id] = true;
519 // Clear our focus chain when we have a focusable component
522 // Defer focusing for 90ms so components can do a layout/positioning
523 // and give us an ability to buffer focuses
524 clearTimeout(me.cmpFocusDelay);
525 if (arguments.length !== 2) {
526 me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]);
530 if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
531 me.clearComponent(cmp);
537 // If we have a focus frame, show it around the focused component
538 if (me.shouldShowFocusFrame(cmp)) {
539 var cls = '.' + me.focusFrameCls + '-',
541 fw = me.focusFrameWidth,
542 box = cmp.el.getPageBox(),
544 // Size the focus frame's t/b/l/r according to the box
545 // This leaves a hole in the middle of the frame so user
546 // interaction w/ the mouse can continue
551 ft = ff.child(cls + 'top'),
552 fb = ff.child(cls + 'bottom'),
553 fl = ff.child(cls + 'left'),
554 fr = ff.child(cls + 'right');
556 ft.setWidth(bw).setLeftTop(bl, bt);
557 fb.setWidth(bw).setLeftTop(bl, bt + bh - fw);
558 fl.setHeight(bh - fw - fw).setLeftTop(bl, bt + fw);
559 fr.setHeight(bh - fw - fw).setLeftTop(bl + bw - fw, bt + fw);
564 me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
567 onComponentHide: function(cmp) {
569 CQ = Ext.ComponentQuery,
575 focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
576 cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
579 me.clearComponent(focusedCmp);
583 me.clearComponent(cmp);
586 parent = CQ.query('^:focusable', cmp)[0];
593 removeDOM: function() {
596 // If we are still enabled globally, or there are still subscribers
597 // then we will halt here, since our DOM stuff is still being used
598 if (me.enabled || me.subscribers.length) {
607 delete me.focusFrame;
608 delete me.focusFrameWidth;
612 * Removes the specified xtype from the {@link #whitelist}.
613 * @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
615 removeXTypeFromWhitelist: function(xtype) {
618 if (Ext.isArray(xtype)) {
619 Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
623 Ext.Array.remove(me.whitelist, xtype);
626 setFocus: function(cmp, focusable, options) {
630 needsTabIndex = function(n) {
631 return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
635 options = options || {};
637 // Come back and do this after the component is rendered
639 cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
643 el = cmp.getFocusEl();
646 // Decorate the component's focus el for focus-ability
647 if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
650 focusFrame: options.focusFrame
653 // Only set -1 tabIndex if we need it
654 // inputs, buttons, and anchor tags do not need it,
655 // and neither does any DOM that has it set already
656 // programmatically or in markup.
657 if (needsTabIndex(dom)) {
658 data.tabIndex = dom.tabIndex;
663 focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
664 blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
668 hide: me.onComponentHide,
669 close: me.onComponentHide,
670 beforedestroy: me.onComponentDestroy,
674 me.focusData[cmp.id] = data;
676 data = me.focusData[cmp.id];
677 if ('tabIndex' in data) {
678 dom.tabIndex = data.tabIndex;
680 el.un('focus', data.focusFn, me);
681 el.un('blur', data.blurFn, me);
682 cmp.un('hide', me.onComponentHide, me);
683 cmp.un('close', me.onComponentHide, me);
684 cmp.un('beforedestroy', me.onComponentDestroy, me);
686 delete me.focusData[cmp.id];
691 setFocusAll: function(focusable, options) {
693 cmps = Ext.ComponentManager.all.getArray(),
698 for (; i < len; i++) {
699 me.setFocus(cmps[i], focusable, options);
703 setupSubscriberKeys: function(container, keys) {
705 el = container.getFocusEl(),
708 backspace: me.focusLast,
709 enter: me.navigateIn,
714 navSiblings = function(e) {
715 if (me.focusedCmp === container) {
716 // Root the sibling navigation to this container, so that we
717 // can automatically dive into the container, rather than forcing
718 // the user to hit the enter key to dive in.
719 return me.navigateSiblings(e, me, container);
721 return me.navigateSiblings(e);
725 Ext.iterate(keys, function(key, cb) {
726 handlers[key] = function(e) {
727 var ret = navSiblings(e);
729 if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
737 return Ext.create('Ext.util.KeyNav', el, handlers);
740 shouldShowFocusFrame: function(cmp) {
742 opts = me.options || {};
744 if (!me.focusFrame || !cmp) {
749 if (opts.focusFrame) {
753 if (me.focusData[cmp.id].focusFrame) {
761 * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s.
762 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management.
763 * @param {Boolean/Object} options An object of the following options
764 * @param {Array/Object} options.keys
765 * An array containing the string names of navigation keys to be supported. The allowed values are:
772 * 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.:
775 * left: this.onLeftKey,
776 * right: this.onRightKey,
780 * @param {Boolean} options.focusFrame
781 * `true` to show the focus frame around a component when it is focused. Defaults to `false`.
783 subscribe: function(container, options) {
787 subs = me.subscribers,
789 // Recursively add focus ability as long as a descendent container isn't
790 // itself subscribed to the FocusManager, or else we'd have unwanted side
791 // effects for subscribing a descendent container twice.
792 safeSetFocus = function(cmp) {
793 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
794 EA.forEach(cmp.query('>'), safeSetFocus);
795 me.setFocus(cmp, true, options);
796 cmp.on('add', data.onAdd, me);
797 } else if (!cmp.isContainer) {
798 me.setFocus(cmp, true, options);
802 // We only accept containers
803 if (!container || !container.isContainer) {
807 if (!container.rendered) {
808 container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
812 // Init the DOM, incase this is the first time it will be used
815 // Create key navigation for subscriber based on keys option
816 data.keyNav = me.setupSubscriberKeys(container, options.keys);
818 // We need to keep track of components being added to our subscriber
819 // and any containers nested deeply within it (omg), so let's do that.
820 // Components that are removed are globally handled.
821 // Also keep track of destruction of our container for auto-unsubscribe.
822 data.onAdd = function(ct, cmp, idx) {
825 container.on('beforedestroy', me.unsubscribe, me);
827 // Now we setup focusing abilities for the container and all its components
828 safeSetFocus(container);
830 // Add to our subscribers list
831 subs.add(container.id, data);
835 * Unsubscribes an {@link Ext.container.Container} from keyboard focus management.
836 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager.
838 unsubscribe: function(container) {
841 subs = me.subscribers,
844 // Recursively remove focus ability as long as a descendent container isn't
845 // itself subscribed to the FocusManager, or else we'd have unwanted side
846 // effects for unsubscribing an ancestor container.
847 safeSetFocus = function(cmp) {
848 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
849 EA.forEach(cmp.query('>'), safeSetFocus);
850 me.setFocus(cmp, false);
851 cmp.un('add', data.onAdd, me);
852 } else if (!cmp.isContainer) {
853 me.setFocus(cmp, false);
857 if (!container || !subs.containsKey(container.id)) {
861 data = subs.get(container.id);
862 data.keyNav.destroy();
863 container.un('beforedestroy', me.unsubscribe, me);
864 subs.removeAtKey(container.id);
865 safeSetFocus(container);