Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / FocusManager.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
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.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.FocusManager
17
18 The FocusManager is responsible for globally:
19
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.
23
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.
27
28 To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
29
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.
34
35  * @singleton
36  * @markdown
37  * @author Jarred Nicholls <jarred@sencha.com>
38  * @docauthor Jarred Nicholls <jarred@sencha.com>
39  */
40 Ext.define('Ext.FocusManager', {
41     singleton: true,
42     alternateClassName: 'Ext.FocusMgr',
43
44     mixins: {
45         observable: 'Ext.util.Observable'
46     },
47
48     requires: [
49         'Ext.ComponentManager',
50         'Ext.ComponentQuery',
51         'Ext.util.HashMap',
52         'Ext.util.KeyNav'
53     ],
54
55     /**
56      * @property {Boolean} enabled
57      * Whether or not the FocusManager is currently enabled
58      */
59     enabled: false,
60
61     /**
62      * @property {Ext.Component} focusedCmp
63      * The currently focused component. Defaults to `undefined`.
64      * @markdown
65      */
66
67     focusElementCls: Ext.baseCSSPrefix + 'focus-element',
68
69     focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
70
71     /**
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:
75      *
76      * 1. Backspace
77      * 2. Delete
78      * 3. Left
79      * 4. Right
80      * 5. Up
81      * 6. Down
82      *
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.
86      *
87      * This whitelist currently defaults to `['textfield']`.
88      * @markdown
89      */
90     whitelist: [
91         'textfield'
92     ],
93
94     tabIndexWhitelist: [
95         'a',
96         'button',
97         'embed',
98         'frame',
99         'iframe',
100         'img',
101         'input',
102         'object',
103         'select',
104         'textarea'
105     ],
106
107     constructor: function() {
108         var me = this,
109             CQ = Ext.ComponentQuery;
110
111         me.addEvents(
112             /**
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.
120              * @markdown
121              */
122             'beforecomponentfocus',
123
124             /**
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.
131              * @markdown
132              */
133             'componentfocus',
134
135             /**
136              * @event disable
137              * Fires when the FocusManager is disabled
138              * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
139              */
140             'disable',
141
142             /**
143              * @event enable
144              * Fires when the FocusManager is enabled
145              * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
146              */
147             'enable'
148         );
149
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(), {
153             disabled: true,
154             scope: me,
155
156             backspace: me.focusLast,
157             enter: me.navigateIn,
158             esc: me.navigateOut,
159             tab: me.navigateSiblings
160
161             //space: me.navigateIn,
162             //del: me.focusLast,
163             //left: me.navigateSiblings,
164             //right: me.navigateSiblings,
165             //down: me.navigateSiblings,
166             //up: me.navigateSiblings
167         });
168
169         me.focusData = {};
170         me.subscribers = Ext.create('Ext.util.HashMap');
171         me.focusChain = {};
172
173         // Setup some ComponentQuery pseudos
174         Ext.apply(CQ.pseudos, {
175             focusable: function(cmps) {
176                 var len = cmps.length,
177                     results = [],
178                     i = 0,
179                     c,
180
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()}');
183                     };
184
185                 for (; i < len; i++) {
186                     c = cmps[i];
187                     if (isFocusable(c)) {
188                         results.push(c);
189                     }
190                 }
191
192                 return results;
193             },
194
195             nextFocus: function(cmps, idx, step) {
196                 step = step || 1;
197                 idx = parseInt(idx, 10);
198
199                 var len = cmps.length,
200                     i = idx + step,
201                     c;
202
203                 for (; i != idx; i += step) {
204                     if (i >= len) {
205                         i = 0;
206                     } else if (i < 0) {
207                         i = len - 1;
208                     }
209
210                     c = cmps[i];
211                     if (CQ.is(c, ':focusable')) {
212                         return [c];
213                     } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
214                         return [c.placeholder];
215                     }
216                 }
217
218                 return [];
219             },
220
221             prevFocus: function(cmps, idx) {
222                 return this.nextFocus(cmps, idx, -1);
223             },
224
225             root: function(cmps) {
226                 var len = cmps.length,
227                     results = [],
228                     i = 0,
229                     c;
230
231                 for (; i < len; i++) {
232                     c = cmps[i];
233                     if (!c.ownerCt) {
234                         results.push(c);
235                     }
236                 }
237
238                 return results;
239             }
240         });
241     },
242
243     /**
244      * Adds the specified xtype to the {@link #whitelist}.
245      * @param {String/Array} xtype Adds the xtype(s) to the {@link #whitelist}.
246      */
247     addXTypeToWhitelist: function(xtype) {
248         var me = this;
249
250         if (Ext.isArray(xtype)) {
251             Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
252             return;
253         }
254
255         if (!Ext.Array.contains(me.whitelist, xtype)) {
256             me.whitelist.push(xtype);
257         }
258     },
259
260     clearComponent: function(cmp) {
261         clearTimeout(this.cmpFocusDelay);
262         if (!cmp.isDestroyed) {
263             cmp.blur();
264         }
265     },
266
267     /**
268      * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
269      */
270     disable: function() {
271         var me = this;
272
273         if (!me.enabled) {
274             return;
275         }
276
277         delete me.options;
278         me.enabled = false;
279
280         Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
281
282         me.removeDOM();
283
284         // Stop handling key navigation
285         me.keyNav.disable();
286
287         // disable focus for all components
288         me.setFocusAll(false);
289
290         me.fireEvent('disable', me);
291     },
292
293     /**
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`.
298      * @markdown
299      */
300     enable: function(options) {
301         var me = this;
302
303         if (options === true) {
304             options = { focusFrame: true };
305         }
306         me.options = options = options || {};
307
308         if (me.enabled) {
309             return;
310         }
311
312         // Handle components that are newly added after we are enabled
313         Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
314
315         me.initDOM(options);
316
317         // Start handling key navigation
318         me.keyNav.enable();
319
320         // enable focus for all components
321         me.setFocusAll(true, options);
322
323         // Finally, let's focus our global focus el so we start fresh
324         me.focusEl.focus();
325         delete me.focusedCmp;
326
327         me.enabled = true;
328         me.fireEvent('enable', me);
329     },
330
331     focusLast: function(e) {
332         var me = this;
333
334         if (me.isWhitelisted(me.focusedCmp)) {
335             return true;
336         }
337
338         // Go back to last focused item
339         if (me.previousFocusedCmp) {
340             me.previousFocusedCmp.focus();
341         }
342     },
343
344     getRootComponents: function() {
345         var me = this,
346             CQ = Ext.ComponentQuery,
347             inline = CQ.query(':focusable:root:not([floating])'),
348             floating = CQ.query(':focusable:root[floating]');
349
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();
354         });
355
356         return floating.concat(inline);
357     },
358
359     initDOM: function(options) {
360         var me = this,
361             sp = '&#160',
362             cls = me.focusFrameCls;
363
364         if (!Ext.isReady) {
365             Ext.onReady(me.initDOM, me);
366             return;
367         }
368
369         // Create global focus element
370         if (!me.focusEl) {
371             me.focusEl = Ext.getBody().createChild({
372                 tabIndex: '-1',
373                 cls: me.focusElementCls,
374                 html: sp
375             });
376         }
377
378         // Create global focus frame
379         if (!me.focusFrame && options.focusFrame) {
380             me.focusFrame = Ext.getBody().createChild({
381                 cls: cls,
382                 children: [
383                     { cls: cls + '-top' },
384                     { cls: cls + '-bottom' },
385                     { cls: cls + '-left' },
386                     { cls: cls + '-right' }
387                 ],
388                 style: 'top: -100px; left: -100px;'
389             });
390             me.focusFrame.setVisibilityMode(Ext.core.Element.DISPLAY);
391             me.focusFrameWidth = me.focusFrame.child('.' + cls + '-top').getHeight();
392             me.focusFrame.hide().setLeftTop(0, 0);
393         }
394     },
395
396     isWhitelisted: function(cmp) {
397         return cmp && Ext.Array.some(this.whitelist, function(x) {
398             return cmp.isXType(x);
399         });
400     },
401
402     navigateIn: function(e) {
403         var me = this,
404             focusedCmp = me.focusedCmp,
405             rootCmps,
406             firstChild;
407
408         if (!focusedCmp) {
409             // No focus yet, so focus the first root cmp on the page
410             rootCmps = me.getRootComponents();
411             if (rootCmps.length) {
412                 rootCmps[0].focus();
413             }
414         } else {
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];
418             if (firstChild) {
419                 firstChild.focus();
420             } else {
421                 // Let's try to fire a click event, as if it came from the mouse
422                 if (Ext.isFunction(focusedCmp.onClick)) {
423                     e.button = 0;
424                     focusedCmp.onClick(e);
425                     focusedCmp.focus();
426                 }
427             }
428         }
429     },
430
431     navigateOut: function(e) {
432         var me = this,
433             parent;
434
435         if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
436             me.focusEl.focus();
437         } else {
438             parent.focus();
439         }
440
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.
444         return true;
445     },
446
447     navigateSiblings: function(e, source, parent) {
448         var me = this,
449             src = source || me,
450             key = e.getKey(),
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;
456
457         focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
458         if (!focusedCmp && !parent) {
459             return;
460         }
461
462         if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
463             return true;
464         }
465
466         parent = parent || focusedCmp.up();
467         if (parent) {
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) {
471                 next.focus();
472                 return next;
473             }
474         }
475     },
476
477     onComponentBlur: function(cmp, e) {
478         var me = this;
479
480         if (me.focusedCmp === cmp) {
481             me.previousFocusedCmp = cmp;
482             delete me.focusedCmp;
483         }
484
485         if (me.focusFrame) {
486             me.focusFrame.hide();
487         }
488     },
489
490     onComponentCreated: function(hash, id, cmp) {
491         this.setFocus(cmp, true, this.options);
492     },
493
494     onComponentDestroy: function(cmp) {
495         this.setFocus(cmp, false);
496     },
497
498     onComponentFocus: function(cmp, e) {
499         var me = this,
500             chain = me.focusChain;
501
502         if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
503             me.clearComponent(cmp);
504
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.
509             if (chain[cmp.id]) {
510                 return;
511             }
512
513             // Try to focus the parent instead
514             var parent = cmp.up();
515             if (parent) {
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;
520                 parent.focus();
521             }
522
523             return;
524         }
525
526         // Clear our focus chain when we have a focusable component
527         me.focusChain = {};
528
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]);
534             return;
535         }
536
537         if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
538             me.clearComponent(cmp);
539             return;
540         }
541
542         me.focusedCmp = cmp;
543
544         // If we have a focus frame, show it around the focused component
545         if (me.shouldShowFocusFrame(cmp)) {
546             var cls = '.' + me.focusFrameCls + '-',
547                 ff = me.focusFrame,
548                 fw = me.focusFrameWidth,
549                 box = cmp.el.getPageBox(),
550
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
554                 bt = box.top,
555                 bl = box.left,
556                 bw = box.width,
557                 bh = box.height,
558                 ft = ff.child(cls + 'top'),
559                 fb = ff.child(cls + 'bottom'),
560                 fl = ff.child(cls + 'left'),
561                 fr = ff.child(cls + 'right');
562
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);
567
568             ff.show();
569         }
570
571         me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
572     },
573
574     onComponentHide: function(cmp) {
575         var me = this,
576             CQ = Ext.ComponentQuery,
577             cmpHadFocus = false,
578             focusedCmp,
579             parent;
580
581         if (me.focusedCmp) {
582             focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
583             cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
584
585             if (focusedCmp) {
586                 me.clearComponent(focusedCmp);
587             }
588         }
589
590         me.clearComponent(cmp);
591
592         if (cmpHadFocus) {
593             parent = CQ.query('^:focusable', cmp)[0];
594             if (parent) {
595                 parent.focus();
596             }
597         }
598     },
599
600     removeDOM: function() {
601         var me = this;
602
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) {
606             return;
607         }
608
609         Ext.destroy(
610             me.focusEl,
611             me.focusFrame
612         );
613         delete me.focusEl;
614         delete me.focusFrame;
615         delete me.focusFrameWidth;
616     },
617
618     /**
619      * Removes the specified xtype from the {@link #whitelist}.
620      * @param {String/Array} xtype Removes the xtype(s) from the {@link #whitelist}.
621      */
622     removeXTypeFromWhitelist: function(xtype) {
623         var me = this;
624
625         if (Ext.isArray(xtype)) {
626             Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
627             return;
628         }
629
630         Ext.Array.remove(me.whitelist, xtype);
631     },
632
633     setFocus: function(cmp, focusable, options) {
634         var me = this,
635             el, dom, data,
636
637             needsTabIndex = function(n) {
638                 return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
639                     && n.tabIndex <= 0;
640             };
641
642         options = options || {};
643
644         // Come back and do this after the component is rendered
645         if (!cmp.rendered) {
646             cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
647             return;
648         }
649
650         el = cmp.getFocusEl();
651         dom = el.dom;
652
653         // Decorate the component's focus el for focus-ability
654         if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
655             if (focusable) {
656                 data = {
657                     focusFrame: options.focusFrame
658                 };
659
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;
666                     dom.tabIndex = -1;
667                 }
668
669                 el.on({
670                     focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
671                     blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
672                     scope: me
673                 });
674                 cmp.on({
675                     hide: me.onComponentHide,
676                     close: me.onComponentHide,
677                     beforedestroy: me.onComponentDestroy,
678                     scope: me
679                 });
680
681                 me.focusData[cmp.id] = data;
682             } else {
683                 data = me.focusData[cmp.id];
684                 if ('tabIndex' in data) {
685                     dom.tabIndex = data.tabIndex;
686                 }
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);
692
693                 delete me.focusData[cmp.id];
694             }
695         }
696     },
697
698     setFocusAll: function(focusable, options) {
699         var me = this,
700             cmps = Ext.ComponentManager.all.getArray(),
701             len = cmps.length,
702             cmp,
703             i = 0;
704
705         for (; i < len; i++) {
706             me.setFocus(cmps[i], focusable, options);
707         }
708     },
709
710     setupSubscriberKeys: function(container, keys) {
711         var me = this,
712             el = container.getFocusEl(),
713             scope = keys.scope,
714             handlers = {
715                 backspace: me.focusLast,
716                 enter: me.navigateIn,
717                 esc: me.navigateOut,
718                 scope: me
719             },
720
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);
727                 } else {
728                     return me.navigateSiblings(e);
729                 }
730             };
731
732         Ext.iterate(keys, function(key, cb) {
733             handlers[key] = function(e) {
734                 var ret = navSiblings(e);
735
736                 if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
737                     return true;
738                 }
739
740                 return ret;
741             };
742         }, me);
743
744         return Ext.create('Ext.util.KeyNav', el, handlers);
745     },
746
747     shouldShowFocusFrame: function(cmp) {
748         var me = this,
749             opts = me.options || {};
750
751         if (!me.focusFrame || !cmp) {
752             return false;
753         }
754
755         // Global trumps
756         if (opts.focusFrame) {
757             return true;
758         }
759
760         if (me.focusData[cmp.id].focusFrame) {
761             return true;
762         }
763
764         return false;
765     },
766
767     /**
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:
773
774             - 'left'
775             - 'right'
776             - 'up'
777             - 'down'
778
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.:
780
781                 {
782                     left: this.onLeftKey,
783                     right: this.onRightKey,
784                     scope: this
785                 }
786
787         - focusFrame : Boolean (optional)
788             `true` to show the focus frame around a component when it is focused. Defaults to `false`.
789      * @markdown
790      */
791     subscribe: function(container, options) {
792         var me = this,
793             EA = Ext.Array,
794             data = {},
795             subs = me.subscribers,
796
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);
807                 }
808             };
809
810         // We only accept containers
811         if (!container || !container.isContainer) {
812             return;
813         }
814
815         if (!container.rendered) {
816             container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
817             return;
818         }
819
820         // Init the DOM, incase this is the first time it will be used
821         me.initDOM(options);
822
823         // Create key navigation for subscriber based on keys option
824         data.keyNav = me.setupSubscriberKeys(container, options.keys);
825
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) {
831             safeSetFocus(cmp);
832         };
833         container.on('beforedestroy', me.unsubscribe, me);
834
835         // Now we setup focusing abilities for the container and all its components
836         safeSetFocus(container);
837
838         // Add to our subscribers list
839         subs.add(container.id, data);
840     },
841
842     /**
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.
845      * @markdown
846      */
847     unsubscribe: function(container) {
848         var me = this,
849             EA = Ext.Array,
850             subs = me.subscribers,
851             data,
852
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);
863                 }
864             };
865
866         if (!container || !subs.containsKey(container.id)) {
867             return;
868         }
869
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);
875         me.removeDOM();
876     }
877 });