4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5 <title>The source code</title>
6 <link href="../prettify/prettify.css" type="text/css" rel="stylesheet" />
7 <script type="text/javascript" src="../prettify/prettify.js"></script>
8 <style type="text/css">
9 .highlight { display: block; background-color: #ddd; }
11 <script type="text/javascript">
12 function highlight() {
13 document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
17 <body onload="prettyPrint(); highlight();">
18 <pre class="prettyprint lang-js"><span id='Ext-FocusManager'>/**
19 </span> * @class Ext.FocusManager
21 The FocusManager is responsible for globally:
23 1. Managing component focus
24 2. Providing basic keyboard navigation
25 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
27 To activate the FocusManager, simply call {@link #enable `Ext.FocusManager.enable();`}. In turn, you may
28 deactivate the FocusManager by subsequently calling {@link #disable `Ext.FocusManager.disable();`}. The
29 FocusManager is disabled by default.
31 To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
33 Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
34 that would like to have navigation between its child {@link Ext.Component}'s. The {@link Ext.container.Container} can simply
35 call {@link #subscribe Ext.FocusManager.subscribe} to take advantage of this feature, and can at any time call
36 {@link #unsubscribe Ext.FocusManager.unsubscribe} to turn the navigation off.
40 * @author Jarred Nicholls <jarred@sencha.com>
41 * @docauthor Jarred Nicholls <jarred@sencha.com>
43 Ext.define('Ext.FocusManager', {
45 alternateClassName: 'Ext.FocusMgr',
48 observable: 'Ext.util.Observable'
52 'Ext.ComponentManager',
58 <span id='Ext-FocusManager-property-enabled'> /**
59 </span> * @property {Boolean} enabled
60 * Whether or not the FocusManager is currently enabled
64 <span id='Ext-FocusManager-property-focusedCmp'> /**
65 </span> * @property {Ext.Component} focusedCmp
66 * The currently focused component. Defaults to `undefined`.
70 focusElementCls: Ext.baseCSSPrefix + 'focus-element',
72 focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
74 <span id='Ext-FocusManager-property-whitelist'> /**
75 </span> * @property {Array} whitelist
76 * A list of xtypes that should ignore certain navigation input keys and
77 * allow for the default browser event/behavior. These input keys include:
86 * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
87 * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
88 * the user to move the input cursor left and right, and to delete characters, etc.
90 * This whitelist currently defaults to `['textfield']`.
110 constructor: function() {
112 CQ = Ext.ComponentQuery;
115 <span id='Ext-FocusManager-event-beforecomponentfocus'> /**
116 </span> * @event beforecomponentfocus
117 * Fires before a component becomes focused. Return `false` to prevent
118 * the component from gaining focus.
119 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
120 * @param {Ext.Component} cmp The component that is being focused
121 * @param {Ext.Component} previousCmp The component that was previously focused,
122 * or `undefined` if there was no previously focused component.
125 'beforecomponentfocus',
127 <span id='Ext-FocusManager-event-componentfocus'> /**
128 </span> * @event componentfocus
129 * Fires after a component becomes focused.
130 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
131 * @param {Ext.Component} cmp The component that has been focused
132 * @param {Ext.Component} previousCmp The component that was previously focused,
133 * or `undefined` if there was no previously focused component.
138 <span id='Ext-FocusManager-event-disable'> /**
139 </span> * @event disable
140 * Fires when the FocusManager is disabled
141 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
145 <span id='Ext-FocusManager-event-enable'> /**
146 </span> * @event enable
147 * Fires when the FocusManager is enabled
148 * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
153 // Setup KeyNav that's bound to document to catch all
154 // unhandled/bubbled key events for navigation
155 me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), {
159 backspace: me.focusLast,
160 enter: me.navigateIn,
162 tab: me.navigateSiblings
164 //space: me.navigateIn,
166 //left: me.navigateSiblings,
167 //right: me.navigateSiblings,
168 //down: me.navigateSiblings,
169 //up: me.navigateSiblings
173 me.subscribers = Ext.create('Ext.util.HashMap');
176 // Setup some ComponentQuery pseudos
177 Ext.apply(CQ.pseudos, {
178 focusable: function(cmps) {
179 var len = cmps.length,
184 isFocusable = function(x) {
185 return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}');
188 for (; i < len; i++) {
190 if (isFocusable(c)) {
198 nextFocus: function(cmps, idx, step) {
200 idx = parseInt(idx, 10);
202 var len = cmps.length,
206 for (; i != idx; i += step) {
209 } else if (i < 0) {
214 if (CQ.is(c, ':focusable')) {
216 } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
217 return [c.placeholder];
224 prevFocus: function(cmps, idx) {
225 return this.nextFocus(cmps, idx, -1);
228 root: function(cmps) {
229 var len = cmps.length,
234 for (; i < len; i++) {
246 <span id='Ext-FocusManager-method-addXTypeToWhitelist'> /**
247 </span> * Adds the specified xtype to the {@link #whitelist}.
248 * @param {String/Array} xtype Adds the xtype(s) to the {@link #whitelist}.
250 addXTypeToWhitelist: function(xtype) {
253 if (Ext.isArray(xtype)) {
254 Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
258 if (!Ext.Array.contains(me.whitelist, xtype)) {
259 me.whitelist.push(xtype);
263 clearComponent: function(cmp) {
264 clearTimeout(this.cmpFocusDelay);
265 if (!cmp.isDestroyed) {
270 <span id='Ext-FocusManager-method-disable'> /**
271 </span> * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
273 disable: function() {
283 Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
287 // Stop handling key navigation
290 // disable focus for all components
291 me.setFocusAll(false);
293 me.fireEvent('disable', me);
296 <span id='Ext-FocusManager-method-enable'> /**
297 </span> * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
298 * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options:
299 - focusFrame : Boolean
300 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
303 enable: function(options) {
306 if (options === true) {
307 options = { focusFrame: true };
309 me.options = options = options || {};
315 // Handle components that are newly added after we are enabled
316 Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
320 // Start handling key navigation
323 // enable focus for all components
324 me.setFocusAll(true, options);
326 // Finally, let's focus our global focus el so we start fresh
328 delete me.focusedCmp;
331 me.fireEvent('enable', me);
334 focusLast: function(e) {
337 if (me.isWhitelisted(me.focusedCmp)) {
341 // Go back to last focused item
342 if (me.previousFocusedCmp) {
343 me.previousFocusedCmp.focus();
347 getRootComponents: function() {
349 CQ = Ext.ComponentQuery,
350 inline = CQ.query(':focusable:root:not([floating])'),
351 floating = CQ.query(':focusable:root[floating]');
353 // Floating items should go to the top of our root stack, and be ordered
354 // by their z-index (highest first)
355 floating.sort(function(a, b) {
356 return a.el.getZIndex() > b.el.getZIndex();
359 return floating.concat(inline);
362 initDOM: function(options) {
365 cls = me.focusFrameCls;
368 Ext.onReady(me.initDOM, me);
372 // Create global focus element
374 me.focusEl = Ext.getBody().createChild({
376 cls: me.focusElementCls,
381 // Create global focus frame
382 if (!me.focusFrame && options.focusFrame) {
383 me.focusFrame = Ext.getBody().createChild({
386 { cls: cls + '-top' },
387 { cls: cls + '-bottom' },
388 { cls: cls + '-left' },
389 { cls: cls + '-right' }
391 style: 'top: -100px; left: -100px;'
393 me.focusFrame.setVisibilityMode(Ext.core.Element.DISPLAY);
394 me.focusFrameWidth = me.focusFrame.child('.' + cls + '-top').getHeight();
395 me.focusFrame.hide().setLeftTop(0, 0);
399 isWhitelisted: function(cmp) {
400 return cmp && Ext.Array.some(this.whitelist, function(x) {
401 return cmp.isXType(x);
405 navigateIn: function(e) {
407 focusedCmp = me.focusedCmp,
412 // No focus yet, so focus the first root cmp on the page
413 rootCmps = me.getRootComponents();
414 if (rootCmps.length) {
418 // Drill into child ref items of the focused cmp, if applicable.
419 // This works for any Component with a getRefItems implementation.
420 firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0];
424 // Let's try to fire a click event, as if it came from the mouse
425 if (Ext.isFunction(focusedCmp.onClick)) {
427 focusedCmp.onClick(e);
434 navigateOut: function(e) {
438 if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
444 // In some browsers (Chrome) FocusManager can handle this before other
445 // handlers. Ext Windows have their own Esc key handling, so we need to
446 // return true here to allow the event to bubble.
450 navigateSiblings: function(e, source, parent) {
454 EO = Ext.EventObject,
455 goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
456 checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
457 nextSelector = goBack ? 'prev' : 'next',
458 idx, next, focusedCmp;
460 focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
461 if (!focusedCmp && !parent) {
465 if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
469 parent = parent || focusedCmp.up();
471 idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1;
472 next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0];
473 if (next && focusedCmp !== next) {
480 onComponentBlur: function(cmp, e) {
483 if (me.focusedCmp === cmp) {
484 me.previousFocusedCmp = cmp;
485 delete me.focusedCmp;
489 me.focusFrame.hide();
493 onComponentCreated: function(hash, id, cmp) {
494 this.setFocus(cmp, true, this.options);
497 onComponentDestroy: function(cmp) {
498 this.setFocus(cmp, false);
501 onComponentFocus: function(cmp, e) {
503 chain = me.focusChain;
505 if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
506 me.clearComponent(cmp);
508 // Check our focus chain, so we don't run into a never ending recursion
509 // If we've attempted (unsuccessfully) to focus this component before,
510 // then we're caught in a loop of child->parent->...->child and we
511 // need to cut the loop off rather than feed into it.
516 // Try to focus the parent instead
517 var parent = cmp.up();
519 // Add component to our focus chain to detect infinite focus loop
520 // before we fire off an attempt to focus our parent.
521 // See the comments above.
522 chain[cmp.id] = true;
529 // Clear our focus chain when we have a focusable component
532 // Defer focusing for 90ms so components can do a layout/positioning
533 // and give us an ability to buffer focuses
534 clearTimeout(me.cmpFocusDelay);
535 if (arguments.length !== 2) {
536 me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]);
540 if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
541 me.clearComponent(cmp);
547 // If we have a focus frame, show it around the focused component
548 if (me.shouldShowFocusFrame(cmp)) {
549 var cls = '.' + me.focusFrameCls + '-',
551 fw = me.focusFrameWidth,
552 box = cmp.el.getPageBox(),
554 // Size the focus frame's t/b/l/r according to the box
555 // This leaves a hole in the middle of the frame so user
556 // interaction w/ the mouse can continue
561 ft = ff.child(cls + 'top'),
562 fb = ff.child(cls + 'bottom'),
563 fl = ff.child(cls + 'left'),
564 fr = ff.child(cls + 'right');
566 ft.setWidth(bw - 2).setLeftTop(bl + 1, bt);
567 fb.setWidth(bw - 2).setLeftTop(bl + 1, bt + bh - fw);
568 fl.setHeight(bh - 2).setLeftTop(bl, bt + 1);
569 fr.setHeight(bh - 2).setLeftTop(bl + bw - fw, bt + 1);
574 me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
577 onComponentHide: function(cmp) {
579 CQ = Ext.ComponentQuery,
585 focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
586 cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
589 me.clearComponent(focusedCmp);
593 me.clearComponent(cmp);
596 parent = CQ.query('^:focusable', cmp)[0];
603 removeDOM: function() {
606 // If we are still enabled globally, or there are still subscribers
607 // then we will halt here, since our DOM stuff is still being used
608 if (me.enabled || me.subscribers.length) {
617 delete me.focusFrame;
618 delete me.focusFrameWidth;
621 <span id='Ext-FocusManager-method-removeXTypeFromWhitelist'> /**
622 </span> * Removes the specified xtype from the {@link #whitelist}.
623 * @param {String/Array} xtype Removes the xtype(s) from the {@link #whitelist}.
625 removeXTypeFromWhitelist: function(xtype) {
628 if (Ext.isArray(xtype)) {
629 Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
633 Ext.Array.remove(me.whitelist, xtype);
636 setFocus: function(cmp, focusable, options) {
640 needsTabIndex = function(n) {
641 return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
642 && n.tabIndex <= 0;
645 options = options || {};
647 // Come back and do this after the component is rendered
649 cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
653 el = cmp.getFocusEl();
656 // Decorate the component's focus el for focus-ability
657 if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
660 focusFrame: options.focusFrame
663 // Only set -1 tabIndex if we need it
664 // inputs, buttons, and anchor tags do not need it,
665 // and neither does any DOM that has it set already
666 // programmatically or in markup.
667 if (needsTabIndex(dom)) {
668 data.tabIndex = dom.tabIndex;
673 focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
674 blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
678 hide: me.onComponentHide,
679 close: me.onComponentHide,
680 beforedestroy: me.onComponentDestroy,
684 me.focusData[cmp.id] = data;
686 data = me.focusData[cmp.id];
687 if ('tabIndex' in data) {
688 dom.tabIndex = data.tabIndex;
690 el.un('focus', data.focusFn, me);
691 el.un('blur', data.blurFn, me);
692 cmp.un('hide', me.onComponentHide, me);
693 cmp.un('close', me.onComponentHide, me);
694 cmp.un('beforedestroy', me.onComponentDestroy, me);
696 delete me.focusData[cmp.id];
701 setFocusAll: function(focusable, options) {
703 cmps = Ext.ComponentManager.all.getArray(),
708 for (; i < len; i++) {
709 me.setFocus(cmps[i], focusable, options);
713 setupSubscriberKeys: function(container, keys) {
715 el = container.getFocusEl(),
718 backspace: me.focusLast,
719 enter: me.navigateIn,
724 navSiblings = function(e) {
725 if (me.focusedCmp === container) {
726 // Root the sibling navigation to this container, so that we
727 // can automatically dive into the container, rather than forcing
728 // the user to hit the enter key to dive in.
729 return me.navigateSiblings(e, me, container);
731 return me.navigateSiblings(e);
735 Ext.iterate(keys, function(key, cb) {
736 handlers[key] = function(e) {
737 var ret = navSiblings(e);
739 if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
747 return Ext.create('Ext.util.KeyNav', el, handlers);
750 shouldShowFocusFrame: function(cmp) {
752 opts = me.options || {};
754 if (!me.focusFrame || !cmp) {
759 if (opts.focusFrame) {
763 if (me.focusData[cmp.id].focusFrame) {
770 <span id='Ext-FocusManager-method-subscribe'> /**
771 </span> * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s.
772 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management.
773 * @param {Boolean/Object} options An object of the following options:
774 - keys : Array/Object
775 An array containing the string names of navigation keys to be supported. The allowed values are:
782 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.:
785 left: this.onLeftKey,
786 right: this.onRightKey,
790 - focusFrame : Boolean (optional)
791 `true` to show the focus frame around a component when it is focused. Defaults to `false`.
794 subscribe: function(container, options) {
798 subs = me.subscribers,
800 // Recursively add focus ability as long as a descendent container isn't
801 // itself subscribed to the FocusManager, or else we'd have unwanted side
802 // effects for subscribing a descendent container twice.
803 safeSetFocus = function(cmp) {
804 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
805 EA.forEach(cmp.query('>'), safeSetFocus);
806 me.setFocus(cmp, true, options);
807 cmp.on('add', data.onAdd, me);
808 } else if (!cmp.isContainer) {
809 me.setFocus(cmp, true, options);
813 // We only accept containers
814 if (!container || !container.isContainer) {
818 if (!container.rendered) {
819 container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
823 // Init the DOM, incase this is the first time it will be used
826 // Create key navigation for subscriber based on keys option
827 data.keyNav = me.setupSubscriberKeys(container, options.keys);
829 // We need to keep track of components being added to our subscriber
830 // and any containers nested deeply within it (omg), so let's do that.
831 // Components that are removed are globally handled.
832 // Also keep track of destruction of our container for auto-unsubscribe.
833 data.onAdd = function(ct, cmp, idx) {
836 container.on('beforedestroy', me.unsubscribe, me);
838 // Now we setup focusing abilities for the container and all its components
839 safeSetFocus(container);
841 // Add to our subscribers list
842 subs.add(container.id, data);
845 <span id='Ext-FocusManager-method-unsubscribe'> /**
846 </span> * Unsubscribes an {@link Ext.container.Container} from keyboard focus management.
847 * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager.
850 unsubscribe: function(container) {
853 subs = me.subscribers,
856 // Recursively remove focus ability as long as a descendent container isn't
857 // itself subscribed to the FocusManager, or else we'd have unwanted side
858 // effects for unsubscribing an ancestor container.
859 safeSetFocus = function(cmp) {
860 if (cmp.isContainer && !subs.containsKey(cmp.id)) {
861 EA.forEach(cmp.query('>'), safeSetFocus);
862 me.setFocus(cmp, false);
863 cmp.un('add', data.onAdd, me);
864 } else if (!cmp.isContainer) {
865 me.setFocus(cmp, false);
869 if (!container || !subs.containsKey(container.id)) {
873 data = subs.get(container.id);
874 data.keyNav.destroy();
875 container.un('beforedestroy', me.unsubscribe, me);
876 subs.removeAtKey(container.id);
877 safeSetFocus(container);