2 * KeyboardNavigation.js
4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * This class handles keyboard navigation of controls and elements.
14 * @class tinymce.ui.KeyboardNavigation
16 define("tinymce/ui/KeyboardNavigation", [
18 ], function(DomUtils) {
22 * Create a new KeyboardNavigation instance to handle the focus for a specific element.
25 * @param {Object} settings the settings object to define how keyboard navigation works.
27 * @setting {tinymce.ui.Control} root the root control navigation focus movement is scoped to this root.
28 * @setting {Array} items an array containing the items to move focus between. Every object in this array must have an
29 * id attribute which maps to the actual DOM element and it must be able to have focus i.e. tabIndex=-1.
30 * @setting {Function} onCancel the callback for when the user presses escape or otherwise indicates canceling.
31 * @setting {Function} onAction (optional) the action handler to call when the user activates an item.
32 * @setting {Boolean} enableLeftRight (optional, default) when true, the up/down arrows move through items.
33 * @setting {Boolean} enableUpDown (optional) when true, the up/down arrows move through items.
34 * Note for both up/down and left/right explicitly set both enableLeftRight and enableUpDown to true.
36 return function(settings) {
37 var root = settings.root, enableUpDown = settings.enableUpDown !== false;
38 var enableLeftRight = settings.enableLeftRight !== false;
39 var items = settings.items, focussedId;
42 * Initializes the items array if needed. This will collect items/elements
43 * from the specified root control.
45 function initItems() {
50 // Root is a container then get child elements using the UI API
51 root.find('*').each(function(ctrl) {
53 items.push(ctrl.getEl());
57 // Root is a control/widget then get the child elements of that control
58 var elements = root.getEl().getElementsByTagName('*');
59 for (var i = 0; i < elements.length; i++) {
60 if (elements[i].id && elements[i]) {
61 items.push(elements[i]);
69 * Returns the currently focused element.
72 * @return {Element} Currently focused element.
74 function getFocusElement() {
75 return document.getElementById(focussedId);
79 * Returns the currently focused elements wai aria role.
82 * @param {Element} elm Optional element to get role from.
83 * @return {String} Role of specified element.
85 function getRole(elm) {
86 elm = elm || getFocusElement();
88 return elm && elm.getAttribute('role');
92 * Returns the role of the parent element.
95 * @param {Element} elm Optional element to get parent role from.
96 * @return {String} Role of the first parent that has a role.
98 function getParentRole(elm) {
99 var role, parent = elm || getFocusElement();
101 while ((parent = parent.parentNode)) {
102 if ((role = getRole(parent))) {
109 * Returns an wai aria property by name.
112 * @param {String} name Name of the aria property to get for example "disabled".
113 * @return {String} Aria property value.
115 function getAriaProp(name) {
116 var elm = document.getElementById(focussedId);
119 return elm.getAttribute('aria-' + name);
124 * Executes the onAction event callback. This is when the user presses enter/space.
129 var focusElm = getFocusElement();
131 if (focusElm && (focusElm.nodeName == "TEXTAREA" || focusElm.type == "text")) {
135 if (settings.onAction) {
136 settings.onAction(focussedId);
138 DomUtils.fire(getFocusElement(), 'click', {keyboard: true});
145 * Cancels the current navigation. The same as pressing the Esc key.
152 if (settings.onCancel) {
153 if ((focusElm = getFocusElement())) {
159 settings.root.fire('cancel');
164 * Moves the focus to the next or previous item. It will wrap to start/end if it can't move.
167 * @param {Number} dir Direction for move -1 or 1.
169 function moveFocus(dir) {
170 var idx = -1, focusElm, i;
171 var visibleItems = [];
173 function isVisible(elm) {
174 var rootElm = root ? root.getEl() : document.body;
176 while (elm && elm != rootElm) {
177 if (elm.style.display == 'none') {
181 elm = elm.parentNode;
189 // TODO: Optimize this, will be slow on lots of items
190 i = visibleItems.length;
191 for (i = 0; i < items.length; i++) {
192 if (isVisible(items[i])) {
193 visibleItems.push(items[i]);
197 i = visibleItems.length;
199 if (visibleItems[i].id === focussedId) {
207 idx = visibleItems.length - 1;
208 } else if (idx >= visibleItems.length) {
212 focusElm = visibleItems[idx];
214 focussedId = focusElm.id;
216 if (settings.actOnFocus) {
222 * Moves focus to the first item or the last focused item if root is a toolbar.
225 * @return {[type]} [description]
227 function focusFirst() {
230 rootRole = getRole(settings.root.getEl());
235 if (rootRole == 'toolbar' && items[i].id === focussedId) {
244 // Handle accessible keys
245 root.on('keydown', function(e) {
246 var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
247 var DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32, DOM_VK_TAB = 9;
252 if (enableLeftRight) {
253 if (settings.leftAction) {
254 settings.leftAction();
259 preventDefault = true;
264 if (enableLeftRight) {
265 if (getRole() == 'menuitem' && getParentRole() == 'menu') {
266 if (getAriaProp('haspopup')) {
273 preventDefault = true;
280 preventDefault = true;
286 if (getRole() == 'menuitem' && getParentRole() == 'menubar') {
288 } else if (getRole() == 'button' && getAriaProp('haspopup')) {
294 preventDefault = true;
299 preventDefault = true;
309 preventDefault = true;
316 preventDefault = action();
320 if (preventDefault) {
327 root.on('focusin', function(e) {
329 focussedId = e.target.id;
333 moveFocus: moveFocus,
334 focusFirst: focusFirst,