| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- /**
- * KeyboardNavigation.js
- *
- * Copyright, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
- /**
- * This class handles keyboard navigation of controls and elements.
- *
- * @class tinymce.ui.KeyboardNavigation
- */
- define("tinymce/ui/KeyboardNavigation", [
- ], function() {
- "use strict";
- /**
- * This class handles all keyboard navigation for WAI-ARIA support. Each root container
- * gets an instance of this class.
- *
- * @constructor
- */
- return function(settings) {
- var root = settings.root, focusedElement, focusedControl;
- focusedElement = document.activeElement;
- focusedControl = root.getParentCtrl(focusedElement);
- /**
- * Returns the currently focused elements wai aria role of the currently
- * focused element or specified element.
- *
- * @private
- * @param {Element} elm Optional element to get role from.
- * @return {String} Role of specified element.
- */
- function getRole(elm) {
- elm = elm || focusedElement;
- return elm && elm.getAttribute('role');
- }
- /**
- * Returns the wai role of the parent element of the currently
- * focused element or specified element.
- *
- * @private
- * @param {Element} elm Optional element to get parent role from.
- * @return {String} Role of the first parent that has a role.
- */
- function getParentRole(elm) {
- var role, parent = elm || focusedElement;
- while ((parent = parent.parentNode)) {
- if ((role = getRole(parent))) {
- return role;
- }
- }
- }
- /**
- * Returns a wai aria property by name for example aria-selected.
- *
- * @private
- * @param {String} name Name of the aria property to get for example "disabled".
- * @return {String} Aria property value.
- */
- function getAriaProp(name) {
- var elm = focusedElement;
- if (elm) {
- return elm.getAttribute('aria-' + name);
- }
- }
- /**
- * Is the element a text input element or not.
- *
- * @private
- * @param {Element} elm Element to check if it's an text input element or not.
- * @return {Boolean} True/false if the element is a text element or not.
- */
- function isTextInputElement(elm) {
- var tagName = elm.tagName.toUpperCase();
- // Notice: since type can be "email" etc we don't check the type
- // So all input elements gets treated as text input elements
- return tagName == "INPUT" || tagName == "TEXTAREA";
- }
- /**
- * Returns true/false if the specified element can be focused or not.
- *
- * @private
- * @param {Element} elm DOM element to check if it can be focused or not.
- * @return {Boolean} True/false if the element can have focus.
- */
- function canFocus(elm) {
- if (isTextInputElement(elm) && !elm.hidden) {
- return true;
- }
- if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) {
- return true;
- }
- return false;
- }
- /**
- * Returns an array of focusable visible elements within the specified container element.
- *
- * @private
- * @param {Element} elm DOM element to find focusable elements within.
- * @return {Array} Array of focusable elements.
- */
- function getFocusElements(elm) {
- var elements = [];
- function collect(elm) {
- if (elm.nodeType != 1 || elm.style.display == 'none') {
- return;
- }
- if (canFocus(elm)) {
- elements.push(elm);
- }
- for (var i = 0; i < elm.childNodes.length; i++) {
- collect(elm.childNodes[i]);
- }
- }
- collect(elm || root.getEl());
- return elements;
- }
- /**
- * Returns the navigation root control for the specified control. The navigation root
- * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
- * It will look for parents of the specified target control or the currenty focused control if this option is omitted.
- *
- * @private
- * @param {tinymce.ui.Control} targetControl Optional target control to find root of.
- * @return {tinymce.ui.Control} Navigation root control.
- */
- function getNavigationRoot(targetControl) {
- var navigationRoot, controls;
- targetControl = targetControl || focusedControl;
- controls = targetControl.parents().toArray();
- controls.unshift(targetControl);
- for (var i = 0; i < controls.length; i++) {
- navigationRoot = controls[i];
- if (navigationRoot.settings.ariaRoot) {
- break;
- }
- }
- return navigationRoot;
- }
- /**
- * Focuses the first item in the specified targetControl element or the last aria index if the
- * navigation root has the ariaRemember option enabled.
- *
- * @private
- * @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
- */
- function focusFirst(targetControl) {
- var navigationRoot = getNavigationRoot(targetControl);
- var focusElements = getFocusElements(navigationRoot.getEl());
- if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
- moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
- } else {
- moveFocusToIndex(0, focusElements);
- }
- }
- /**
- * Moves the focus to the specified index within the elements list.
- * This will scope the index to the size of the element list if it changed.
- *
- * @private
- * @param {Number} idx Specified index to move to.
- * @param {Array} elements Array with dom elements to move focus within.
- * @return {Number} Input index or a changed index if it was out of range.
- */
- function moveFocusToIndex(idx, elements) {
- if (idx < 0) {
- idx = elements.length - 1;
- } else if (idx >= elements.length) {
- idx = 0;
- }
- if (elements[idx]) {
- elements[idx].focus();
- }
- return idx;
- }
- /**
- * Moves the focus forwards or backwards.
- *
- * @private
- * @param {Number} dir Direction to move in positive means forward, negative means backwards.
- * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
- */
- function moveFocus(dir, elements) {
- var idx = -1, navigationRoot = getNavigationRoot();
- elements = elements || getFocusElements(navigationRoot.getEl());
- for (var i = 0; i < elements.length; i++) {
- if (elements[i] === focusedElement) {
- idx = i;
- }
- }
- idx += dir;
- navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
- }
- /**
- * Moves the focus to the left this is called by the left key.
- *
- * @private
- */
- function left() {
- var parentRole = getParentRole();
- if (parentRole == "tablist") {
- moveFocus(-1, getFocusElements(focusedElement.parentNode));
- } else if (focusedControl.parent().submenu) {
- cancel();
- } else {
- moveFocus(-1);
- }
- }
- /**
- * Moves the focus to the right this is called by the right key.
- *
- * @private
- */
- function right() {
- var role = getRole(), parentRole = getParentRole();
- if (parentRole == "tablist") {
- moveFocus(1, getFocusElements(focusedElement.parentNode));
- } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
- enter();
- } else {
- moveFocus(1);
- }
- }
- /**
- * Moves the focus to the up this is called by the up key.
- *
- * @private
- */
- function up() {
- moveFocus(-1);
- }
- /**
- * Moves the focus to the up this is called by the down key.
- *
- * @private
- */
- function down() {
- var role = getRole(), parentRole = getParentRole();
- if (role == "menuitem" && parentRole == "menubar") {
- enter();
- } else if (role == "button" && getAriaProp('haspopup')) {
- enter({key: 'down'});
- } else {
- moveFocus(1);
- }
- }
- /**
- * Moves the focus to the next item or previous item depending on shift key.
- *
- * @private
- * @param {DOMEvent} e DOM event object.
- */
- function tab(e) {
- var parentRole = getParentRole();
- if (parentRole == "tablist") {
- var elm = getFocusElements(focusedControl.getEl('body'))[0];
- if (elm) {
- elm.focus();
- }
- } else {
- moveFocus(e.shiftKey ? -1 : 1);
- }
- }
- /**
- * Calls the cancel event on the currently focused control. This is normally done using the Esc key.
- *
- * @private
- */
- function cancel() {
- focusedControl.fire('cancel');
- }
- /**
- * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
- *
- * @private
- * @param {Object} aria Optional aria data to pass along with the enter event.
- */
- function enter(aria) {
- aria = aria || {};
- focusedControl.fire('click', {target: focusedElement, aria: aria});
- }
- root.on('keydown', function(e) {
- function handleNonTabOrEscEvent(e, handler) {
- // Ignore non tab keys for text elements
- if (isTextInputElement(focusedElement)) {
- return;
- }
- if (handler(e) !== false) {
- e.preventDefault();
- }
- }
- if (e.isDefaultPrevented()) {
- return;
- }
- switch (e.keyCode) {
- case 37: // DOM_VK_LEFT
- handleNonTabOrEscEvent(e, left);
- break;
- case 39: // DOM_VK_RIGHT
- handleNonTabOrEscEvent(e, right);
- break;
- case 38: // DOM_VK_UP
- handleNonTabOrEscEvent(e, up);
- break;
- case 40: // DOM_VK_DOWN
- handleNonTabOrEscEvent(e, down);
- break;
- case 27: // DOM_VK_ESCAPE
- cancel();
- break;
- case 14: // DOM_VK_ENTER
- case 13: // DOM_VK_RETURN
- case 32: // DOM_VK_SPACE
- handleNonTabOrEscEvent(e, enter);
- break;
- case 9: // DOM_VK_TAB
- if (tab(e) !== false) {
- e.preventDefault();
- }
- break;
- }
- });
- root.on('focusin', function(e) {
- focusedElement = e.target;
- focusedControl = e.control;
- });
- return {
- focusFirst: focusFirst
- };
- };
- });
|