]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/util/FocusableContainer.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / util / FocusableContainer.js
CommitLineData
6527f429
DM
1/**\r
2 * A mixin for groups of Focusable things (Components, Widgets, etc) that\r
3 * should respond to arrow keys to navigate among the peers, but keep only\r
4 * one of the peers tabbable by default (tabIndex=0)\r
5 *\r
6 * Some examples: Toolbars, Radio groups, Tab bars, Panel headers, Menus\r
7 */\r
8\r
9Ext.define('Ext.util.FocusableContainer', {\r
10 extend: 'Ext.Mixin',\r
11 \r
12 requires: [\r
13 'Ext.util.KeyNav'\r
14 ],\r
15 \r
16 mixinConfig: {\r
17 id: 'focusablecontainer',\r
18 \r
19 before: {\r
20 onAdd: 'onFocusableChildAdd',\r
21 onRemove: 'onFocusableChildRemove',\r
22 destroy: 'destroyFocusableContainer',\r
23 onFocusEnter: 'onFocusEnter'\r
24 },\r
25 \r
26 after: {\r
27 afterRender: 'initFocusableContainer',\r
28 onFocusLeave: 'onFocusLeave',\r
29 afterShow: 'activateFocusableContainerEl'\r
30 }\r
31 },\r
32 \r
33 isFocusableContainer: true,\r
34 \r
35 /**\r
36 * @cfg {Boolean} [enableFocusableContainer=true] Enable or disable\r
37 * navigation with arrow keys for this FocusableContainer. This option may\r
38 * be useful with nested FocusableContainers such as Grid column headers,\r
39 * when only the root container should handle keyboard events.\r
40 */\r
41 enableFocusableContainer: true,\r
42 \r
43 /**\r
44 * @cfg {Number} [activeChildTabIndex=0] DOM tabIndex attribute to set on the\r
45 * active Focusable child of this container when using the "Roaming tabindex"\r
46 * technique. Set this value to > 0 to precisely control the tabbing order\r
47 * of the components/containers on the page.\r
48 */\r
49 activeChildTabIndex: 0,\r
50 \r
51 /**\r
52 * @cfg {Number} [inactiveChildTabIndex=-1] DOM tabIndex attribute to set on\r
53 * inactive Focusable children of this container when using the "Roaming tabindex"\r
54 * technique. This value rarely needs to be changed from its default.\r
55 */\r
56 inactiveChildTabIndex: -1,\r
57 \r
58 privates: {\r
59 initFocusableContainer: function(clearChildren) {\r
60 var items, i, len;\r
61 \r
62 // Allow nested containers to optionally disable\r
63 // children containers' behavior\r
64 if (this.enableFocusableContainer) {\r
65 clearChildren = clearChildren != null ? clearChildren : true;\r
66 this.doInitFocusableContainer(clearChildren);\r
67 }\r
68 \r
69 // A FocusableContainer instance such as a toolbar could have decided\r
70 // to opt out of FC behavior for some reason; it could have happened\r
71 // after all or almost all child items have been initialized with\r
72 // focusableContainer reference. We need to clean this up if we're not\r
73 // going to behave like a FocusableContainer after all.\r
74 else {\r
75 items = this.getFocusables();\r
76 \r
77 for (i = 0, len = items.length; i < len; i++) {\r
78 items[i].focusableContainer = null;\r
79 }\r
80 }\r
81 },\r
82 \r
83 doInitFocusableContainer: function(clearChildren) {\r
84 var me = this,\r
85 el, child;\r
86 \r
87 el = me.getFocusableContainerEl();\r
88 \r
89 // This flag allows post factum initialization of the focusable container,\r
90 // i.e. when container was empty initially and then some tabbable children\r
91 // were added and we need to clear their tabIndices after priming our own\r
92 // element's tabIndex.\r
93 // This is useful for Panel and Window headers that might have tools\r
94 // added dynamically.\r
95 if (clearChildren) {\r
96 me.clearFocusables();\r
97 }\r
98 \r
99 // If we have no potentially focusable children, or all potentially focusable\r
100 // children are presently disabled, don't init the container el tabIndex.\r
101 // There is no point in tabbing into container when it can't shift focus\r
102 // to a child.\r
103 child = me.findNextFocusableChild({ step: 1, beforeRender: true });\r
104 \r
105 if (child) {\r
106 // We set tabIndex on the focusable container el so that the user\r
107 // could tab into it; we catch its focus event and focus a child instead\r
108 me.activateFocusableContainerEl(el);\r
109 }\r
110 \r
111 // Unsightly long names help to avoid possible clashing with class\r
112 // or instance properties. We have to be extra careful in a mixin!\r
113 me.focusableContainerMouseListener = me.mon(\r
114 el, 'mousedown', me.onFocusableContainerMousedown, me\r
115 );\r
116 \r
117 // Having keyNav doesn't hurt when container el is not focusable\r
118 me.focusableKeyNav = me.createFocusableContainerKeyNav(el);\r
119 },\r
120 \r
121 createFocusableContainerKeyNav: function(el) {\r
122 var me = this;\r
123 \r
124 return new Ext.util.KeyNav(el, {\r
125 eventName: 'keydown',\r
126 ignoreInputFields: true,\r
127 scope: me,\r
128\r
129 tab: me.onFocusableContainerTabKey,\r
130 enter: me.onFocusableContainerEnterKey,\r
131 space: me.onFocusableContainerSpaceKey,\r
132 up: me.onFocusableContainerUpKey,\r
133 down: me.onFocusableContainerDownKey,\r
134 left: me.onFocusableContainerLeftKey,\r
135 right: me.onFocusableContainerRightKey\r
136 });\r
137 },\r
138 \r
139 destroyFocusableContainer: function() {\r
140 if (this.enableFocusableContainer) {\r
141 this.doDestroyFocusableContainer();\r
142 }\r
143 },\r
144 \r
145 doDestroyFocusableContainer: function() {\r
146 var me = this;\r
147 \r
148 if (me.keyNav) {\r
149 me.keyNav.destroy();\r
150 }\r
151 \r
152 if (me.focusableContainerMouseListener) {\r
153 me.focusableContainerMouseListener.destroy();\r
154 }\r
155 \r
156 me.focusableKeyNav = me.focusableContainerMouseListener = null;\r
157 },\r
158 \r
159 // Default FocusableContainer implies a flat list of focusable children\r
160 getFocusables: function() {\r
161 return this.items.items;\r
162 },\r
163\r
164 initDefaultFocusable: function(beforeRender) {\r
165 var me = this,\r
166 activeIndex = me.activeChildTabIndex,\r
167 haveFocusable = false,\r
168 items, item, i, len, tabIdx;\r
169\r
170 items = me.getFocusables();\r
171 len = items.length;\r
172\r
173 if (!len) {\r
174 return;\r
175 }\r
176\r
177 // Check if any child Focusable is already active.\r
178 // Note that we're not determining *which* focusable child\r
179 // to focus here, only that we have some focusables.\r
180 for (i = 0; i < len; i++) {\r
181 item = items[i];\r
182\r
183 if (item.focusable && !item.disabled) {\r
184 haveFocusable = true;\r
185 tabIdx = item.getTabIndex();\r
186\r
187 if (tabIdx != null && tabIdx >= activeIndex) {\r
188 return item;\r
189 }\r
190 }\r
191 }\r
192\r
193 // No interactive children found, no point in going further\r
194 if (!haveFocusable) {\r
195 return;\r
196 }\r
197\r
198 // No child is focusable by default, so the first *interactive*\r
199 // one gets initial childTabIndex. We are not looking for a focusable\r
200 // child here because it may not be focusable yet if this happens\r
201 // before rendering; we assume that an interactive child will become\r
202 // focusable later and now activateFocusable() will just assign it\r
203 // the respective tabIndex.\r
204 item = me.findNextFocusableChild({\r
205 beforeRender: beforeRender,\r
206 items: items,\r
207 step: true\r
208 });\r
209\r
210 if (item) {\r
211 me.activateFocusable(item);\r
212 }\r
213\r
214 return item;\r
215 },\r
216\r
217 clearFocusables: function() {\r
218 var me = this,\r
219 items = me.getFocusables(),\r
220 len = items.length,\r
221 item, i;\r
222\r
223 for (i = 0; i < len; i++) {\r
224 item = items[i];\r
225\r
226 if (item.focusable && !item.disabled) {\r
227 me.deactivateFocusable(item);\r
228 }\r
229 }\r
230 },\r
231\r
232 activateFocusable: function(child, /* optional */ newTabIndex) {\r
233 var activeIndex = newTabIndex != null ? newTabIndex : this.activeChildTabIndex;\r
234\r
235 child.setTabIndex(activeIndex);\r
236 },\r
237\r
238 deactivateFocusable: function(child, /* optional */ newTabIndex) {\r
239 var inactiveIndex = newTabIndex != null ? newTabIndex : this.inactiveChildTabIndex;\r
240\r
241 child.setTabIndex(inactiveIndex);\r
242 },\r
243\r
244 onFocusableContainerTabKey: function() {\r
245 return true;\r
246 },\r
247\r
248 onFocusableContainerEnterKey: function() {\r
249 return true;\r
250 },\r
251\r
252 onFocusableContainerSpaceKey: function() {\r
253 return true;\r
254 },\r
255\r
256 onFocusableContainerUpKey: function(e) {\r
257 // Default action is to scroll the nearest vertically scrollable container\r
258 e.preventDefault();\r
259 \r
260 return this.moveChildFocus(e, false);\r
261 },\r
262 \r
263 onFocusableContainerDownKey: function(e) {\r
264 // Ditto\r
265 e.preventDefault();\r
266 \r
267 return this.moveChildFocus(e, true);\r
268 },\r
269 \r
270 onFocusableContainerLeftKey: function(e) {\r
271 // Default action is to scroll the nearest horizontally scrollable container\r
272 e.preventDefault();\r
273 \r
274 return this.moveChildFocus(e, false);\r
275 },\r
276 \r
277 onFocusableContainerRightKey: function(e) {\r
278 // Ditto\r
279 e.preventDefault();\r
280 \r
281 return this.moveChildFocus(e, true);\r
282 },\r
283 \r
284 getFocusableFromEvent: function(e) {\r
285 var child = Ext.Component.fromElement(e.getTarget());\r
286 \r
287 //<debug>\r
288 if (!child) {\r
289 Ext.raise("No focusable child found for keyboard event!");\r
290 }\r
291 //</debug>\r
292 \r
293 return child;\r
294 },\r
295 \r
296 moveChildFocus: function(e, forward) {\r
297 var child = this.getFocusableFromEvent(e);\r
298 \r
299 return this.focusChild(child, forward, e);\r
300 },\r
301 \r
302 focusChild: function(child, forward) {\r
303 var nextChild = this.findNextFocusableChild({\r
304 child: child,\r
305 step: forward\r
306 });\r
307 \r
308 if (nextChild) {\r
309 nextChild.focus();\r
310 }\r
311 \r
312 return nextChild;\r
313 },\r
314 \r
315 findNextFocusableChild: function(options) {\r
316 // This method is private, so options should always be provided\r
317 var beforeRender = options.beforeRender,\r
318 items, item, child, step, idx, i, len;\r
319 \r
320 items = options.items || this.getFocusables();\r
321 step = options.step != null ? options.step : 1;\r
322 child = options.child;\r
323 \r
324 // If the child is null or undefined, idx will be -1.\r
325 // The loop below will account for that, trying to find\r
326 // the first focusable child from either end (depending on step)\r
327 idx = Ext.Array.indexOf(items, child);\r
328 \r
329 // It's often easier to pass a boolean for 1/-1\r
330 step = step === true ? 1 : step === false ? -1 : step;\r
331 \r
332 len = items.length;\r
333 i = step > 0 ? (idx < len ? idx + step : 0) : (idx > 0 ? idx + step : len - 1);\r
334 \r
335 for (;; i += step) {\r
336 // We're looking for the first or last focusable child\r
337 // and we've reached the end of the items, so punt\r
338 if (idx < 0 && (i >= len || i < 0)) {\r
339 return null;\r
340 }\r
341 \r
342 // Loop over forward\r
343 else if (i >= len) {\r
344 i = -1; // Iterator will increase it once more\r
345 continue;\r
346 }\r
347 \r
348 // Loop over backward\r
349 else if (i < 0) {\r
350 i = len;\r
351 continue;\r
352 }\r
353 \r
354 // Looped to the same item, give up\r
355 else if (i === idx) {\r
356 return null;\r
357 }\r
358 \r
359 item = items[i];\r
360 \r
361 if (!item || !item.focusable || item.disabled) {\r
362 continue;\r
363 }\r
364 \r
365 // This loop can be run either at FocusableContainer init time,\r
366 // or later when we need to navigate upon pressing an arrow key.\r
367 // When we're navigating, we have to know exactly if the child is\r
368 // focusable or not, hence only rendered children will make the cut.\r
369 // At the init time item.isFocusable() may return false incorrectly\r
370 // just because the item has not been rendered yet and its focusEl\r
371 // is not defined, so we don't bother to call isFocusable and return\r
372 // the first potentially focusable child.\r
373 if (beforeRender || (item.isFocusable && item.isFocusable())) {\r
374 return item;\r
375 }\r
376 }\r
377 \r
378 return null;\r
379 },\r
380 \r
381 getFocusableContainerEl: function() {\r
382 return this.el;\r
383 },\r
384 \r
385 onFocusableChildAdd: function(child) {\r
386 if (this.enableFocusableContainer) {\r
387 return this.doFocusableChildAdd(child);\r
388 }\r
389 },\r
390 \r
391 activateFocusableContainerEl: function(el) {\r
392 el = el || this.getFocusableContainerEl();\r
393 \r
394 // Might not yet be rendered\r
395 if (el) {\r
396 el.set({ tabIndex: this.activeChildTabIndex });\r
397 }\r
398 },\r
399 \r
400 deactivateFocusableContainerEl: function(el) {\r
401 el = el || this.getFocusableContainerEl();\r
402 \r
403 if (el) {\r
404 el.set({ tabIndex: undefined });\r
405 }\r
406 },\r
407 \r
408 isFocusableContainerActive: function() {\r
409 var me = this,\r
410 isActive = false,\r
411 el, child, focusEl;\r
412 \r
413 el = me.getFocusableContainerEl();\r
414 \r
415 if (el && el.isTabbable && el.isTabbable()) {\r
416 isActive = true;\r
417 }\r
418 else {\r
419 child = me.lastFocusedChild;\r
420 focusEl = child && child.getFocusEl && child.getFocusEl();\r
421 \r
422 if (focusEl && focusEl.isTabbable && focusEl.isTabbable()) {\r
423 isActive = true;\r
424 }\r
425 }\r
426 \r
427 return isActive;\r
428 },\r
429 \r
430 doFocusableChildAdd: function(child) {\r
431 if (child.focusable) {\r
432 child.focusableContainer = this;\r
433 }\r
434 },\r
435 \r
436 onFocusableChildRemove: function(child) {\r
437 if (this.enableFocusableContainer) {\r
438 return this.doFocusableChildRemove(child);\r
439 }\r
440 \r
441 child.focusableContainer = null;\r
442 },\r
443 \r
444 doFocusableChildRemove: function(child) {\r
445 // If the focused child is being removed, we deactivate the FocusableContainer\r
446 // So that it returns to the tabbing order.\r
447 // For example, locking a grid column must return the owning HeaderContainer\r
448 // to tabbability\r
449 if (child === this.lastFocusedChild) {\r
450 this.lastFocusedChild = null;\r
451 this.activateFocusableContainerEl();\r
452 }\r
453 },\r
454 \r
455 onFocusableContainerMousedown: function(e, target) {\r
456 var targetCmp = Ext.Component.fromElement(target);\r
457 \r
458 // Capture the timestamp for the mousedown. If we're navigating\r
459 // into the container itself via the mouse we don't want to\r
460 // default focus the first child like we would when using the keyboard.\r
461 // By the time we get to the focusenter handling, we don't know what has caused\r
462 // the focus to be triggered, so if the timestamp falls within some small epsilon,\r
463 // the focus enter has been caused via the mouse and we can react accordingly.\r
464 this.mousedownTimestamp = targetCmp === this ? Ext.Date.now() : 0;\r
465 \r
466 // Prevent focusing the container itself. DO NOT remove this clause, it is\r
467 // untestable by our unit tests: injecting mousedown events will not cause\r
468 // default action in the browser, the element never gets focus and tests\r
469 // never fail. See http://www.w3.org/TR/DOM-Level-3-Events/#trusted-events\r
470 if (targetCmp === this) {\r
471 e.preventDefault();\r
472 }\r
473 },\r
474\r
475 onFocusEnter: function(e) {\r
476 var me = this,\r
477 target = e.toComponent,\r
478 mousedownTimestamp = me.mousedownTimestamp,\r
479 epsilon = 50,\r
480 child;\r
481 \r
482 if (!me.enableFocusableContainer) {\r
483 return null;\r
484 }\r
485 \r
486 me.mousedownTimestamp = 0;\r
487 \r
488 if (target === me) {\r
489 if (!mousedownTimestamp || Ext.Date.now() - mousedownTimestamp > epsilon) {\r
490 child = me.initDefaultFocusable();\r
491\r
492 if (child) {\r
493 me.deactivateFocusableContainerEl();\r
494 child.focus();\r
495 }\r
496 }\r
497 }\r
498 else {\r
499 me.deactivateFocusableContainerEl();\r
500 }\r
501 \r
502 return target;\r
503 },\r
504\r
505 onFocusLeave: function(e) {\r
506 var me = this,\r
507 lastFocused = me.lastFocusedChild;\r
508 \r
509 if (!me.enableFocusableContainer) {\r
510 return;\r
511 }\r
512\r
513 if (!me.destroyed && !me.destroying) {\r
514 me.clearFocusables();\r
515\r
516 if (lastFocused && !lastFocused.disabled) {\r
517 me.activateFocusable(lastFocused);\r
518 }\r
519 else {\r
520 me.activateFocusableContainerEl();\r
521 }\r
522 }\r
523 },\r
524 \r
525 beforeFocusableChildBlur: Ext.privateFn,\r
526 afterFocusableChildBlur: Ext.privateFn,\r
527 \r
528 beforeFocusableChildFocus: function(child) {\r
529 var me = this;\r
530 \r
531 if (!me.enableFocusableContainer) {\r
532 return;\r
533 }\r
534 \r
535 me.clearFocusables();\r
536 me.activateFocusable(child);\r
537 \r
538 if (child.needArrowKeys) {\r
539 me.guardFocusableChild(child);\r
540 }\r
541 },\r
542 \r
543 guardFocusableChild: function(child) {\r
544 var me = this,\r
545 index = me.activeChildTabIndex,\r
546 guard;\r
547 \r
548 guard = me.findNextFocusableChild({ child: child, step: -1 });\r
549 \r
550 if (guard) {\r
551 guard.setTabIndex(index);\r
552 }\r
553 \r
554 guard = me.findNextFocusableChild({ child: child, step: 1 });\r
555 \r
556 if (guard) {\r
557 guard.setTabIndex(index);\r
558 }\r
559 },\r
560 \r
561 afterFocusableChildFocus: function(child) {\r
562 if (!this.enableFocusableContainer) {\r
563 return;\r
564 }\r
565 \r
566 this.lastFocusedChild = child;\r
567 },\r
568 \r
569 beforeFocusableChildEnable: Ext.privateFn,\r
570 \r
571 onFocusableChildEnable: function(child) {\r
572 var me = this;\r
573 \r
574 if (!me.enableFocusableContainer) {\r
575 return;\r
576 }\r
577 \r
578 // Some Components like Buttons do not render tabIndex attribute\r
579 // when they start their lifecycle disabled, or remove tabIndex\r
580 // if they get disabled later. Subsequently, such Components will\r
581 // reset their tabIndex to default configured value upon enabling.\r
582 // We don't want these children to be tabbable so we reset their\r
583 // tabIndex yet again, unless this child is the last focused one.\r
584 if (child !== me.lastFocusedChild) {\r
585 me.deactivateFocusable(child);\r
586 \r
587 if (!me.isFocusableContainerActive()) {\r
588 me.activateFocusableContainerEl();\r
589 }\r
590 }\r
591 },\r
592 \r
593 beforeFocusableChildDisable: function(child) {\r
594 var me = this,\r
595 nextTarget;\r
596 \r
597 if (!me.enableFocusableContainer || me.destroying || me.destroyed) {\r
598 return;\r
599 }\r
600 \r
601 // When currently focused child is about to be disabled,\r
602 // it may lose the focus as well. For example, Buttons\r
603 // will remove tabIndex attribute upon disabling, which\r
604 // in turn will throw focus to the document body and cause\r
605 // onFocusLeave to fire on the FocusableContainer.\r
606 // We're focusing the next sibling to prevent that.\r
607 if (child.hasFocus) {\r
608 nextTarget = me.findNextFocusableChild({ child: child }) ||\r
609 child.findFocusTarget();\r
610 \r
611 // Note that it is entirely possible not to find the nextTarget,\r
612 // e.g. when we're disabling the last button in a toolbar rendered\r
613 // directly into document body. We don't have a good way to handle\r
614 // such cases at present.\r
615 if (nextTarget) {\r
616 nextTarget.focus();\r
617 }\r
618 }\r
619 },\r
620 \r
621 onFocusableChildDisable: function(child) {\r
622 var me = this,\r
623 lastFocused = me.lastFocusedChild,\r
624 firstFocusableChild;\r
625 \r
626 if (!me.enableFocusableContainer || me.destroying || me.destroyed) {\r
627 return;\r
628 }\r
629 \r
630 // If the disabled child was the last focused item of this\r
631 // FocusableContainer, we have to reset the tabbability of\r
632 // our container element.\r
633 if (child === lastFocused) {\r
634 me.activateFocusableContainerEl();\r
635 }\r
636 \r
637 // It is also possible that the disabled child was the last\r
638 // focusable child of this container, in which case we need\r
639 // to make the container untabbable.\r
640 firstFocusableChild = me.findNextFocusableChild({ step: 1 });\r
641 \r
642 if (!firstFocusableChild) {\r
643 me.deactivateFocusableContainerEl();\r
644 }\r
645 },\r
646 \r
647 // TODO\r
648 onFocusableChildShow: Ext.privateFn,\r
649 onFocusableChildHide: Ext.privateFn,\r
650 onFocusableChildMasked: Ext.privateFn,\r
651 onFocusableChildDestroy: Ext.privateFn,\r
652 onFocusableChildUpdate: Ext.privateFn\r
653 }\r
654});\r