v1.0
只读模式 group, 分享 评论更多问题 博客标签总是存在一个
This commit is contained in:
482
public/tinymce/classes/ui/KeyboardNavigation.js
Normal file → Executable file
482
public/tinymce/classes/ui/KeyboardNavigation.js
Normal file → Executable file
@ -14,89 +14,51 @@
|
||||
* @class tinymce.ui.KeyboardNavigation
|
||||
*/
|
||||
define("tinymce/ui/KeyboardNavigation", [
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(DomUtils) {
|
||||
], function() {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Create a new KeyboardNavigation instance to handle the focus for a specific element.
|
||||
* This class handles all keyboard navigation for WAI-ARIA support. Each root container
|
||||
* gets an instance of this class.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} settings the settings object to define how keyboard navigation works.
|
||||
*
|
||||
* @setting {tinymce.ui.Control} root the root control navigation focus movement is scoped to this root.
|
||||
* @setting {Array} items an array containing the items to move focus between. Every object in this array must have an
|
||||
* id attribute which maps to the actual DOM element and it must be able to have focus i.e. tabIndex=-1.
|
||||
* @setting {Function} onCancel the callback for when the user presses escape or otherwise indicates canceling.
|
||||
* @setting {Function} onAction (optional) the action handler to call when the user activates an item.
|
||||
* @setting {Boolean} enableLeftRight (optional, default) when true, the up/down arrows move through items.
|
||||
* @setting {Boolean} enableUpDown (optional) when true, the up/down arrows move through items.
|
||||
* Note for both up/down and left/right explicitly set both enableLeftRight and enableUpDown to true.
|
||||
*/
|
||||
return function(settings) {
|
||||
var root = settings.root, enableUpDown = settings.enableUpDown !== false;
|
||||
var enableLeftRight = settings.enableLeftRight !== false;
|
||||
var items = settings.items, focussedId;
|
||||
var root = settings.root, focusedElement, focusedControl;
|
||||
|
||||
/**
|
||||
* Initializes the items array if needed. This will collect items/elements
|
||||
* from the specified root control.
|
||||
*/
|
||||
function initItems() {
|
||||
if (!items) {
|
||||
items = [];
|
||||
|
||||
if (root.find) {
|
||||
// Root is a container then get child elements using the UI API
|
||||
root.find('*').each(function(ctrl) {
|
||||
if (ctrl.canFocus) {
|
||||
items.push(ctrl.getEl());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Root is a control/widget then get the child elements of that control
|
||||
var elements = root.getEl().getElementsByTagName('*');
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
if (elements[i].id && elements[i]) {
|
||||
items.push(elements[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
focusedElement = document.activeElement;
|
||||
} catch (ex) {
|
||||
// IE sometimes fails to return a proper element
|
||||
focusedElement = document.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently focused element.
|
||||
*
|
||||
* @private
|
||||
* @return {Element} Currently focused element.
|
||||
*/
|
||||
function getFocusElement() {
|
||||
return document.getElementById(focussedId);
|
||||
}
|
||||
focusedControl = root.getParentCtrl(focusedElement);
|
||||
|
||||
/**
|
||||
* Returns the currently focused elements wai aria role.
|
||||
* 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 || getFocusElement();
|
||||
elm = elm || focusedElement;
|
||||
|
||||
return elm && elm.getAttribute('role');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the role of the parent element.
|
||||
* 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 || getFocusElement();
|
||||
var role, parent = elm || focusedElement;
|
||||
|
||||
while ((parent = parent.parentNode)) {
|
||||
if ((role = getRole(parent))) {
|
||||
@ -106,14 +68,14 @@ define("tinymce/ui/KeyboardNavigation", [
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an wai aria property by name.
|
||||
* 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 = document.getElementById(focussedId);
|
||||
var elm = focusedElement;
|
||||
|
||||
if (elm) {
|
||||
return elm.getAttribute('aria-' + name);
|
||||
@ -121,218 +83,316 @@ define("tinymce/ui/KeyboardNavigation", [
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the onAction event callback. This is when the user presses enter/space.
|
||||
* 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 action() {
|
||||
var focusElm = getFocusElement();
|
||||
function isTextInputElement(elm) {
|
||||
var tagName = elm.tagName.toUpperCase();
|
||||
|
||||
if (focusElm && (focusElm.nodeName == "TEXTAREA" || focusElm.type == "text")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.onAction) {
|
||||
settings.onAction(focussedId);
|
||||
} else {
|
||||
DomUtils.fire(getFocusElement(), 'click', {keyboard: true});
|
||||
}
|
||||
|
||||
return true;
|
||||
// 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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current navigation. The same as pressing the Esc key.
|
||||
* Returns true/false if the specified element can be focused or not.
|
||||
*
|
||||
* @method cancel
|
||||
* @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 cancel() {
|
||||
var focusElm;
|
||||
|
||||
if (settings.onCancel) {
|
||||
if ((focusElm = getFocusElement())) {
|
||||
focusElm.blur();
|
||||
}
|
||||
|
||||
settings.onCancel();
|
||||
} else {
|
||||
settings.root.fire('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the focus to the next or previous item. It will wrap to start/end if it can't move.
|
||||
*
|
||||
* @method moveFocus
|
||||
* @param {Number} dir Direction for move -1 or 1.
|
||||
*/
|
||||
function moveFocus(dir) {
|
||||
var idx = -1, focusElm, i;
|
||||
var visibleItems = [];
|
||||
|
||||
function isVisible(elm) {
|
||||
var rootElm = root ? root.getEl() : document.body;
|
||||
|
||||
while (elm && elm != rootElm) {
|
||||
if (elm.style.display == 'none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
elm = elm.parentNode;
|
||||
}
|
||||
|
||||
function canFocus(elm) {
|
||||
if (isTextInputElement(elm) && !elm.hidden) {
|
||||
return true;
|
||||
}
|
||||
|
||||
initItems();
|
||||
if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Optimize this, will be slow on lots of items
|
||||
i = visibleItems.length;
|
||||
for (i = 0; i < items.length; i++) {
|
||||
if (isVisible(items[i])) {
|
||||
visibleItems.push(items[i]);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
i = visibleItems.length;
|
||||
while (i--) {
|
||||
if (visibleItems[i].id === focussedId) {
|
||||
idx = 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 currently 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;
|
||||
if (idx < 0) {
|
||||
idx = visibleItems.length - 1;
|
||||
} else if (idx >= visibleItems.length) {
|
||||
idx = 0;
|
||||
}
|
||||
navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
|
||||
}
|
||||
|
||||
focusElm = visibleItems[idx];
|
||||
focusElm.focus();
|
||||
focussedId = focusElm.id;
|
||||
/**
|
||||
* Moves the focus to the left this is called by the left key.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function left() {
|
||||
var parentRole = getParentRole();
|
||||
|
||||
if (settings.actOnFocus) {
|
||||
action();
|
||||
if (parentRole == "tablist") {
|
||||
moveFocus(-1, getFocusElements(focusedElement.parentNode));
|
||||
} else if (focusedControl.parent().submenu) {
|
||||
cancel();
|
||||
} else {
|
||||
moveFocus(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves focus to the first item or the last focused item if root is a toolbar.
|
||||
* Moves the focus to the right this is called by the right key.
|
||||
*
|
||||
* @method focusFirst
|
||||
* @return {[type]} [description]
|
||||
* @private
|
||||
*/
|
||||
function focusFirst() {
|
||||
var i, rootRole;
|
||||
function right() {
|
||||
var role = getRole(), parentRole = getParentRole();
|
||||
|
||||
rootRole = getRole(settings.root.getEl());
|
||||
initItems();
|
||||
if (parentRole == "tablist") {
|
||||
moveFocus(1, getFocusElements(focusedElement.parentNode));
|
||||
} else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
|
||||
enter();
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
}
|
||||
|
||||
i = items.length;
|
||||
while (i--) {
|
||||
if (rootRole == 'toolbar' && items[i].id === focussedId) {
|
||||
items[i].focus();
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
items[0].focus();
|
||||
}
|
||||
|
||||
// Handle accessible keys
|
||||
root.on('keydown', function(e) {
|
||||
var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
|
||||
var DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32, DOM_VK_TAB = 9;
|
||||
var preventDefault;
|
||||
if (e.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.keyCode) {
|
||||
case DOM_VK_LEFT:
|
||||
if (enableLeftRight) {
|
||||
if (settings.leftAction) {
|
||||
settings.leftAction();
|
||||
} else {
|
||||
moveFocus(-1);
|
||||
}
|
||||
|
||||
preventDefault = true;
|
||||
}
|
||||
case 37: // DOM_VK_LEFT
|
||||
handleNonTabOrEscEvent(e, left);
|
||||
break;
|
||||
|
||||
case DOM_VK_RIGHT:
|
||||
if (enableLeftRight) {
|
||||
if (getRole() == 'menuitem' && getParentRole() == 'menu') {
|
||||
if (getAriaProp('haspopup')) {
|
||||
action();
|
||||
}
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
|
||||
preventDefault = true;
|
||||
}
|
||||
case 39: // DOM_VK_RIGHT
|
||||
handleNonTabOrEscEvent(e, right);
|
||||
break;
|
||||
|
||||
case DOM_VK_UP:
|
||||
if (enableUpDown) {
|
||||
moveFocus(-1);
|
||||
preventDefault = true;
|
||||
}
|
||||
case 38: // DOM_VK_UP
|
||||
handleNonTabOrEscEvent(e, up);
|
||||
break;
|
||||
|
||||
case DOM_VK_DOWN:
|
||||
if (enableUpDown) {
|
||||
if (getRole() == 'menuitem' && getParentRole() == 'menubar') {
|
||||
action();
|
||||
} else if (getRole() == 'button' && getAriaProp('haspopup')) {
|
||||
action();
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
|
||||
preventDefault = true;
|
||||
}
|
||||
case 40: // DOM_VK_DOWN
|
||||
handleNonTabOrEscEvent(e, down);
|
||||
break;
|
||||
|
||||
case DOM_VK_TAB:
|
||||
preventDefault = true;
|
||||
|
||||
if (e.shiftKey) {
|
||||
moveFocus(-1);
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case DOM_VK_ESCAPE:
|
||||
preventDefault = true;
|
||||
case 27: // DOM_VK_ESCAPE
|
||||
cancel();
|
||||
break;
|
||||
|
||||
case DOM_VK_ENTER:
|
||||
case DOM_VK_RETURN:
|
||||
case DOM_VK_SPACE:
|
||||
preventDefault = action();
|
||||
case 14: // DOM_VK_ENTER
|
||||
case 13: // DOM_VK_RETURN
|
||||
case 32: // DOM_VK_SPACE
|
||||
handleNonTabOrEscEvent(e, enter);
|
||||
break;
|
||||
}
|
||||
|
||||
if (preventDefault) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
case 9: // DOM_VK_TAB
|
||||
if (tab(e) !== false) {
|
||||
e.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Init on focus in
|
||||
root.on('focusin', function(e) {
|
||||
initItems();
|
||||
focussedId = e.target.id;
|
||||
focusedElement = e.target;
|
||||
focusedControl = e.control;
|
||||
});
|
||||
|
||||
return {
|
||||
moveFocus: moveFocus,
|
||||
focusFirst: focusFirst,
|
||||
cancel: cancel
|
||||
focusFirst: focusFirst
|
||||
};
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user