FocusManager.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. /**
  2. * The FocusManager is responsible for globally:
  3. *
  4. * 1. Managing component focus
  5. * 2. Providing basic keyboard navigation
  6. * 3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
  7. *
  8. * To activate the FocusManager, simply call `Ext.FocusManager.enable();`. In turn, you may
  9. * deactivate the FocusManager by subsequently calling `Ext.FocusManager.disable();`. The
  10. * FocusManager is disabled by default.
  11. *
  12. * To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #method-enable}.
  13. *
  14. * Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
  15. * that would like to have navigation between its child {@link Ext.Component}'s.
  16. *
  17. * @author Jarred Nicholls <jarred@sencha.com>
  18. * @docauthor Jarred Nicholls <jarred@sencha.com>
  19. */
  20. Ext.define('Ext.FocusManager', {
  21. singleton: true,
  22. alternateClassName: ['Ext.FocusMgr' ],
  23. mixins: {
  24. observable: 'Ext.util.Observable'
  25. },
  26. requires: [
  27. 'Ext.AbstractComponent',
  28. 'Ext.Component',
  29. 'Ext.ComponentManager',
  30. 'Ext.ComponentQuery',
  31. 'Ext.util.HashMap',
  32. 'Ext.util.KeyNav'
  33. ],
  34. /**
  35. * @property {Boolean} enabled
  36. * Whether or not the FocusManager is currently enabled
  37. */
  38. enabled: false,
  39. /**
  40. * @property {Ext.Component} focusedCmp
  41. * The currently focused component.
  42. */
  43. focusElementCls: Ext.baseCSSPrefix + 'focus-element',
  44. focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
  45. /**
  46. * @property {String[]} whitelist
  47. * A list of xtypes that should ignore certain navigation input keys and
  48. * allow for the default browser event/behavior. These input keys include:
  49. *
  50. * 1. Backspace
  51. * 2. Delete
  52. * 3. Left
  53. * 4. Right
  54. * 5. Up
  55. * 6. Down
  56. *
  57. * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
  58. * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
  59. * the user to move the input cursor left and right, and to delete characters, etc.
  60. */
  61. whitelist: [
  62. 'textfield'
  63. ],
  64. constructor: function(config) {
  65. var me = this,
  66. CQ = Ext.ComponentQuery;
  67. me.mixins.observable.constructor.call(me, config);
  68. me.addEvents(
  69. /**
  70. * @event beforecomponentfocus
  71. * Fires before a component becomes focused. Return `false` to prevent
  72. * the component from gaining focus.
  73. * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
  74. * @param {Ext.Component} cmp The component that is being focused
  75. * @param {Ext.Component} previousCmp The component that was previously focused,
  76. * or `undefined` if there was no previously focused component.
  77. */
  78. 'beforecomponentfocus',
  79. /**
  80. * @event componentfocus
  81. * Fires after a component becomes focused.
  82. * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
  83. * @param {Ext.Component} cmp The component that has been focused
  84. * @param {Ext.Component} previousCmp The component that was previously focused,
  85. * or `undefined` if there was no previously focused component.
  86. */
  87. 'componentfocus',
  88. /**
  89. * @event disable
  90. * Fires when the FocusManager is disabled
  91. * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
  92. */
  93. 'disable',
  94. /**
  95. * @event enable
  96. * Fires when the FocusManager is enabled
  97. * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
  98. */
  99. 'enable'
  100. );
  101. me.focusTask = new Ext.util.DelayedTask(me.handleComponentFocus, me);
  102. // Gain control on Component focus, blur, hide and destroy
  103. Ext.override(Ext.AbstractComponent, {
  104. onFocus: function() {
  105. this.callParent(arguments);
  106. if (me.enabled && this.hasFocus) {
  107. Array.prototype.unshift.call(arguments, this);
  108. me.onComponentFocus.apply(me, arguments);
  109. }
  110. },
  111. onBlur: function() {
  112. this.callParent(arguments);
  113. if (me.enabled && !this.hasFocus) {
  114. Array.prototype.unshift.call(arguments, this);
  115. me.onComponentBlur.apply(me, arguments);
  116. }
  117. },
  118. onDestroy: function() {
  119. this.callParent(arguments);
  120. if (me.enabled) {
  121. Array.prototype.unshift.call(arguments, this);
  122. me.onComponentDestroy.apply(me, arguments);
  123. }
  124. }
  125. });
  126. Ext.override(Ext.Component, {
  127. afterHide: function() {
  128. this.callParent(arguments);
  129. if (me.enabled) {
  130. Array.prototype.unshift.call(arguments, this);
  131. me.onComponentHide.apply(me, arguments);
  132. }
  133. }
  134. });
  135. // Setup KeyNav that's bound to document to catch all
  136. // unhandled/bubbled key events for navigation
  137. me.keyNav = new Ext.util.KeyNav(Ext.getDoc(), {
  138. disabled: true,
  139. scope: me,
  140. backspace: me.focusLast,
  141. enter: me.navigateIn,
  142. esc: me.navigateOut,
  143. tab: me.navigateSiblings,
  144. space: me.navigateIn,
  145. del: me.focusLast,
  146. left: me.navigateSiblings,
  147. right: me.navigateSiblings,
  148. down: me.navigateSiblings,
  149. up: me.navigateSiblings
  150. });
  151. me.focusData = {};
  152. me.subscribers = new Ext.util.HashMap();
  153. me.focusChain = {};
  154. // Setup some ComponentQuery pseudos
  155. Ext.apply(CQ.pseudos, {
  156. focusable: function(cmps) {
  157. var len = cmps.length,
  158. results = [],
  159. i = 0,
  160. c;
  161. for (; i < len; i++) {
  162. c = cmps[i];
  163. if (c.isFocusable()) {
  164. results.push(c);
  165. }
  166. }
  167. return results;
  168. },
  169. // Return the single next focusable sibling from the current idx in either direction (step -1 or 1)
  170. nextFocus: function(cmps, idx, step) {
  171. step = step || 1;
  172. idx = parseInt(idx, 10);
  173. var len = cmps.length,
  174. i = idx, c;
  175. for (;;) {
  176. // Increment index, and loop round if off either end
  177. if ((i += step) >= len) {
  178. i = 0;
  179. } else if (i < 0) {
  180. i = len - 1;
  181. }
  182. // As soon as we loop back to the starting index, give up, there are no focusable siblings.
  183. if (i === idx) {
  184. return [];
  185. }
  186. // If we have found a focusable sibling, return it
  187. if ((c = cmps[i]).isFocusable()) {
  188. return [c];
  189. }
  190. }
  191. return [];
  192. },
  193. prevFocus: function(cmps, idx) {
  194. return this.nextFocus(cmps, idx, -1);
  195. },
  196. root: function(cmps) {
  197. var len = cmps.length,
  198. results = [],
  199. i = 0,
  200. c;
  201. for (; i < len; i++) {
  202. c = cmps[i];
  203. if (!c.ownerCt) {
  204. results.push(c);
  205. }
  206. }
  207. return results;
  208. }
  209. });
  210. },
  211. /**
  212. * Adds the specified xtype to the {@link #whitelist}.
  213. * @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
  214. */
  215. addXTypeToWhitelist: function(xtype) {
  216. var me = this;
  217. if (Ext.isArray(xtype)) {
  218. Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
  219. return;
  220. }
  221. if (!Ext.Array.contains(me.whitelist, xtype)) {
  222. me.whitelist.push(xtype);
  223. }
  224. },
  225. clearComponent: function(cmp) {
  226. clearTimeout(this.cmpFocusDelay);
  227. if (!cmp.isDestroyed) {
  228. cmp.blur();
  229. }
  230. },
  231. /**
  232. * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
  233. */
  234. disable: function() {
  235. var me = this;
  236. if (!me.enabled) {
  237. return;
  238. }
  239. delete me.options;
  240. me.enabled = false;
  241. me.removeDOM();
  242. // Stop handling key navigation
  243. me.keyNav.disable();
  244. me.fireEvent('disable', me);
  245. },
  246. /**
  247. * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
  248. * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object
  249. * with the following options:
  250. * @param {Boolean} [focusFrame=false] `true` to show the focus frame around a component when it is focused.
  251. */
  252. enable: function(options) {
  253. var me = this;
  254. if (options === true) {
  255. options = { focusFrame: true };
  256. }
  257. me.options = options = options || {};
  258. if (me.enabled) {
  259. return;
  260. }
  261. // When calling addFocusListener on Containers, the FocusManager must be enabled, otherwise it won't do it.
  262. me.enabled = true;
  263. me.initDOM(options);
  264. // Start handling key navigation
  265. me.keyNav.enable();
  266. // Finally, let's focus our global focus el so we start fresh
  267. me.focusEl.focus();
  268. delete me.focusedCmp;
  269. me.fireEvent('enable', me);
  270. },
  271. focusLast: function(e) {
  272. var me = this;
  273. if (me.isWhitelisted(me.focusedCmp)) {
  274. return true;
  275. }
  276. // Go back to last focused item
  277. if (me.previousFocusedCmp) {
  278. me.previousFocusedCmp.focus();
  279. }
  280. },
  281. getRootComponents: function() {
  282. var me = this,
  283. CQ = Ext.ComponentQuery,
  284. inline = CQ.query(':focusable:root:not([floating])'),
  285. floating = CQ.query(':focusable:root[floating]');
  286. // Floating items should go to the top of our root stack, and be ordered
  287. // by their z-index (highest first)
  288. floating.sort(function(a, b) {
  289. return a.el.getZIndex() > b.el.getZIndex();
  290. });
  291. return floating.concat(inline);
  292. },
  293. initDOM: function(options) {
  294. var me = this,
  295. cls = me.focusFrameCls,
  296. needListeners = Ext.ComponentQuery.query('{getFocusEl()}:not([focusListenerAdded])'),
  297. i = 0, len = needListeners.length;
  298. if (!Ext.isReady) {
  299. return Ext.onReady(me.initDOM, me);
  300. }
  301. // When we are enabled, we must ensure that all Components which return a focusEl that is *not naturally focusable*
  302. // have focus/blur listeners enabled to then trigger onFocus/onBlur handling so that we get to know about their focus action.
  303. // These listeners are not added at initialization unless the FocusManager is enabled at that time.
  304. for (; i < len; i++) {
  305. needListeners[i].addFocusListener();
  306. }
  307. // Make the document body the global focus element
  308. if (!me.focusEl) {
  309. me.focusEl = Ext.getBody();
  310. me.focusEl.dom.tabIndex = -1;
  311. }
  312. // Create global focus frame
  313. if (!me.focusFrame && options.focusFrame) {
  314. me.focusFrame = Ext.getBody().createChild({
  315. cls: cls,
  316. children: [
  317. { cls: cls + '-top' },
  318. { cls: cls + '-bottom' },
  319. { cls: cls + '-left' },
  320. { cls: cls + '-right' }
  321. ],
  322. style: 'top: -100px; left: -100px;'
  323. });
  324. me.focusFrame.setVisibilityMode(Ext.Element.DISPLAY);
  325. me.focusFrame.hide().setLeftTop(0, 0);
  326. }
  327. },
  328. isWhitelisted: function(cmp) {
  329. return cmp && Ext.Array.some(this.whitelist, function(x) {
  330. return cmp.isXType(x);
  331. });
  332. },
  333. navigateIn: function(e) {
  334. var me = this,
  335. focusedCmp = me.focusedCmp,
  336. defaultRoot,
  337. firstChild;
  338. if (me.isWhitelisted(focusedCmp)) {
  339. return true;
  340. }
  341. if (!focusedCmp) {
  342. // No focus yet, so focus the first root cmp on the page
  343. defaultRoot = me.getRootComponents()[0];
  344. if (defaultRoot) {
  345. // If the default root is based upon the body, then it will already be focused, and will not fire a focus event to
  346. // trigger its own onFocus processing, so we have to programatically blur it first.
  347. if (defaultRoot.getFocusEl() === me.focusEl) {
  348. me.focusEl.blur();
  349. }
  350. defaultRoot.focus();
  351. }
  352. } else {
  353. // Drill into child ref items of the focused cmp, if applicable.
  354. // This works for any Component with a getRefItems implementation.
  355. firstChild = focusedCmp.hasFocus ? Ext.ComponentQuery.query('>:focusable', focusedCmp)[0] : focusedCmp;
  356. if (firstChild) {
  357. firstChild.focus();
  358. } else {
  359. // Let's try to fire a click event, as if it came from the mouse
  360. if (Ext.isFunction(focusedCmp.onClick)) {
  361. e.button = 0;
  362. focusedCmp.onClick(e);
  363. if (focusedCmp.isVisible(true)) {
  364. focusedCmp.focus();
  365. } else {
  366. me.navigateOut();
  367. }
  368. }
  369. }
  370. }
  371. },
  372. navigateOut: function(e) {
  373. var me = this,
  374. parent;
  375. if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
  376. me.focusEl.focus();
  377. } else {
  378. parent.focus();
  379. }
  380. // In some browsers (Chrome) FocusManager can handle this before other
  381. // handlers. Ext Windows have their own Esc key handling, so we need to
  382. // return true here to allow the event to bubble.
  383. return true;
  384. },
  385. navigateSiblings: function(e, source, parent) {
  386. var me = this,
  387. src = source || me,
  388. key = e.getKey(),
  389. EO = Ext.EventObject,
  390. goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
  391. checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
  392. nextSelector = goBack ? 'prev' : 'next',
  393. idx, next, focusedCmp, siblings;
  394. focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
  395. if (!focusedCmp && !parent) {
  396. return true;
  397. }
  398. if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
  399. return true;
  400. }
  401. // If no focused Component, or a root level one was focused, then siblings are root components.
  402. if (!focusedCmp || focusedCmp.is(':root')) {
  403. siblings = me.getRootComponents();
  404. } else {
  405. // Else if the focused component has a parent, get siblings from there
  406. parent = parent || focusedCmp.up();
  407. if (parent) {
  408. siblings = parent.getRefItems();
  409. }
  410. }
  411. // Navigate if we have found siblings.
  412. if (siblings) {
  413. idx = focusedCmp ? Ext.Array.indexOf(siblings, focusedCmp) : -1;
  414. next = Ext.ComponentQuery.query(':' + nextSelector + 'Focus(' + idx + ')', siblings)[0];
  415. if (next && focusedCmp !== next) {
  416. next.focus();
  417. return next;
  418. }
  419. }
  420. },
  421. onComponentBlur: function(cmp, e) {
  422. var me = this;
  423. if (me.focusedCmp === cmp) {
  424. me.previousFocusedCmp = cmp;
  425. delete me.focusedCmp;
  426. }
  427. if (me.focusFrame) {
  428. me.focusFrame.hide();
  429. }
  430. },
  431. onComponentFocus: function(cmp, e) {
  432. var me = this,
  433. chain = me.focusChain,
  434. parent;
  435. if (!cmp.isFocusable()) {
  436. me.clearComponent(cmp);
  437. // Check our focus chain, so we don't run into a never ending recursion
  438. // If we've attempted (unsuccessfully) to focus this component before,
  439. // then we're caught in a loop of child->parent->...->child and we
  440. // need to cut the loop off rather than feed into it.
  441. if (chain[cmp.id]) {
  442. return;
  443. }
  444. // Try to focus the parent instead
  445. parent = cmp.up();
  446. if (parent) {
  447. // Add component to our focus chain to detect infinite focus loop
  448. // before we fire off an attempt to focus our parent.
  449. // See the comments above.
  450. chain[cmp.id] = true;
  451. parent.focus();
  452. }
  453. return;
  454. }
  455. // Clear our focus chain when we have a focusable component
  456. me.focusChain = {};
  457. // Capture the focusEl to frame now.
  458. // Button returns its encapsulating element during the focus phase
  459. // So that element gets styled and framed.
  460. me.focusTask.delay(10, null, null, [cmp, cmp.getFocusEl()]);
  461. },
  462. handleComponentFocus: function(cmp, focusEl) {
  463. var me = this,
  464. cls,
  465. ff,
  466. fw,
  467. box,
  468. bt,
  469. bl,
  470. bw,
  471. bh,
  472. ft,
  473. fb,
  474. fl,
  475. fr;
  476. if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
  477. me.clearComponent(cmp);
  478. return;
  479. }
  480. me.focusedCmp = cmp;
  481. // If we have a focus frame, show it around the focused component
  482. if (me.shouldShowFocusFrame(cmp)) {
  483. cls = '.' + me.focusFrameCls + '-';
  484. ff = me.focusFrame;
  485. box = focusEl.getPageBox();
  486. // Size the focus frame's t/b/l/r according to the box
  487. // This leaves a hole in the middle of the frame so user
  488. // interaction w/ the mouse can continue
  489. bt = box.top;
  490. bl = box.left;
  491. bw = box.width;
  492. bh = box.height;
  493. ft = ff.child(cls + 'top');
  494. fb = ff.child(cls + 'bottom');
  495. fl = ff.child(cls + 'left');
  496. fr = ff.child(cls + 'right');
  497. ft.setWidth(bw).setLeftTop(bl, bt);
  498. fb.setWidth(bw).setLeftTop(bl, bt + bh - 2);
  499. fl.setHeight(bh - 2).setLeftTop(bl, bt + 2);
  500. fr.setHeight(bh - 2).setLeftTop(bl + bw - 2, bt + 2);
  501. ff.show();
  502. }
  503. me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
  504. },
  505. onComponentHide: function(cmp) {
  506. var me = this,
  507. cmpHadFocus = false,
  508. focusedCmp = me.focusedCmp,
  509. parent;
  510. if (focusedCmp) {
  511. // See if the Component being hidden was the focused Component, or owns the focused Component
  512. // In these cases, focus needs to be removed from the focused Component to the nearest focusable ancestor
  513. cmpHadFocus = cmp.hasFocus || (cmp.isContainer && cmp.isAncestor(me.focusedCmp));
  514. }
  515. me.clearComponent(cmp);
  516. // Move focus onto the nearest focusable ancestor, or this is there is none
  517. if (cmpHadFocus && (parent = cmp.up(':focusable'))) {
  518. parent.focus();
  519. } else {
  520. me.focusEl.focus();
  521. }
  522. },
  523. onComponentDestroy: function() {
  524. },
  525. removeDOM: function() {
  526. var me = this;
  527. // If we are still enabled globally, or there are still subscribers
  528. // then we will halt here, since our DOM stuff is still being used
  529. if (me.enabled || me.subscribers.length) {
  530. return;
  531. }
  532. Ext.destroy(
  533. me.focusFrame
  534. );
  535. delete me.focusEl;
  536. delete me.focusFrame;
  537. },
  538. /**
  539. * Removes the specified xtype from the {@link #whitelist}.
  540. * @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
  541. */
  542. removeXTypeFromWhitelist: function(xtype) {
  543. var me = this;
  544. if (Ext.isArray(xtype)) {
  545. Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
  546. return;
  547. }
  548. Ext.Array.remove(me.whitelist, xtype);
  549. },
  550. setupSubscriberKeys: function(container, keys) {
  551. var me = this,
  552. el = container.getFocusEl(),
  553. scope = keys.scope,
  554. handlers = {
  555. backspace: me.focusLast,
  556. enter: me.navigateIn,
  557. esc: me.navigateOut,
  558. scope: me
  559. },
  560. navSiblings = function(e) {
  561. if (me.focusedCmp === container) {
  562. // Root the sibling navigation to this container, so that we
  563. // can automatically dive into the container, rather than forcing
  564. // the user to hit the enter key to dive in.
  565. return me.navigateSiblings(e, me, container);
  566. } else {
  567. return me.navigateSiblings(e);
  568. }
  569. };
  570. Ext.iterate(keys, function(key, cb) {
  571. handlers[key] = function(e) {
  572. var ret = navSiblings(e);
  573. if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
  574. return true;
  575. }
  576. return ret;
  577. };
  578. }, me);
  579. return new Ext.util.KeyNav(el, handlers);
  580. },
  581. shouldShowFocusFrame: function(cmp) {
  582. var me = this,
  583. opts = me.options || {},
  584. cmpFocusEl = cmp.getFocusEl(),
  585. cmpFocusElTag = Ext.getDom(cmpFocusEl).tagName;
  586. // Do not show a focus frame if
  587. // 1. We are configured not to.
  588. // 2. No Component was passed
  589. if (!me.focusFrame || !cmp) {
  590. return false;
  591. }
  592. // Global trumps
  593. if (opts.focusFrame) {
  594. return true;
  595. }
  596. if (me.focusData[cmp.id].focusFrame) {
  597. return true;
  598. }
  599. return false;
  600. }
  601. });