Sindbad~EG File Manager
/**
* @class
* @classdesc Keyboard navigation for accessibility support
* @extends H5P.EventDispatcher
*/
H5P.KeyboardNav = (function (EventDispatcher) {
/**
* Construct a new KeyboardNav
* @constructor
*/
function KeyboardNav() {
EventDispatcher.call(this);
/** @member {boolean} */
this.selectability = true;
/** @member {HTMLElement[]|EventTarget[]} */
this.elements = [];
}
KeyboardNav.prototype = Object.create(EventDispatcher.prototype);
KeyboardNav.prototype.constructor = KeyboardNav;
/**
* Adds a new element to navigation
*
* @param {HTMLElement} el The element
* @public
*/
KeyboardNav.prototype.addElement = function(el){
const keyDown = this.handleKeyDown.bind(this);
const onClick = this.onClick.bind(this);
el.addEventListener('keydown', keyDown);
el.addEventListener('click', onClick);
// add to array to navigate over
this.elements.push({
el: el,
keyDown: keyDown,
onClick: onClick,
});
if(this.elements.length === 1){ // if first
this.setTabbableAt(0);
}
};
/**
* Select the previous element in the list. Select the last element,
* if the current element is the first element in the list.
*
* @param {Number} index The index of currently selected element
* @public
* @fires KeyboardNav#previousOption
*/
KeyboardNav.prototype.previousOption = function (index) {
var isFirstElement = index === 0;
if (isFirstElement) {
return;
}
this.focusOnElementAt(isFirstElement ? (this.elements.length - 1) : (index - 1));
/**
* Previous option event
*
* @event KeyboardNav#previousOption
* @type KeyboardNavigationEventData
*/
this.trigger('previousOption', this.createEventPayload(index));
};
/**
* Select the next element in the list. Select the first element,
* if the current element is the first element in the list.
*
* @param {Number} index The index of the currently selected element
* @public
* @fires KeyboardNav#previousOption
*/
KeyboardNav.prototype.nextOption = function (index) {
var isLastElement = index === this.elements.length - 1;
if (isLastElement) {
return;
}
this.focusOnElementAt(isLastElement ? 0 : (index + 1));
/**
* Previous option event
*
* @event KeyboardNav#nextOption
* @type KeyboardNavigationEventData
*/
this.trigger('nextOption', this.createEventPayload(index));
};
/**
* Focus on an element by index
*
* @param {Number} index The index of the element to focus on
* @public
*/
KeyboardNav.prototype.focusOnElementAt = function (index) {
this.setTabbableAt(index);
this.getElements()[index].focus();
};
/**
* Disable possibility to select a word trough click and space or enter
*
* @public
*/
KeyboardNav.prototype.disableSelectability = function () {
this.elements.forEach(function (el) {
el.el.removeEventListener('keydown', el.keyDown);
el.el.removeEventListener('click', el.onClick);
}.bind(this));
this.selectability = false;
};
/**
* Enable possibility to select a word trough click and space or enter
*
* @public
*/
KeyboardNav.prototype.enableSelectability = function () {
this.elements.forEach(function (el) {
el.el.addEventListener('keydown', el.keyDown);
el.el.addEventListener('click', el.onClick);
}.bind(this));
this.selectability = true;
};
/**
* Sets tabbable on a single element in the list, by index
* Also removes tabbable from all other elements in the list
*
* @param {Number} index The index of the element to set tabbale on
* @public
*/
KeyboardNav.prototype.setTabbableAt = function (index) {
this.removeAllTabbable();
this.getElements()[index].setAttribute('tabindex', '0');
};
/**
* Remove tabbable from all entries
*
* @public
*/
KeyboardNav.prototype.removeAllTabbable = function () {
this.elements.forEach(function(el){
el.el.removeAttribute('tabindex');
});
};
/**
* Toggles 'aria-selected' on an element, if selectability == true
*
* @param {EventTarget|HTMLElement} el The element to select/unselect
* @private
* @fires KeyboardNav#select
*/
KeyboardNav.prototype.toggleSelect = function(el){
if(this.selectability) {
// toggle selection
el.setAttribute('aria-selected', !isElementSelected(el));
// focus current
el.setAttribute('tabindex', '0');
el.focus();
var index = this.getElements().indexOf(el);
/**
* Previous option event
*
* @event KeyboardNav#select
* @type KeyboardNavigationEventData
*/
this.trigger('select', this.createEventPayload(index));
}
};
/**
* Handles key down
*
* @param {KeyboardEvent} event Keyboard event
* @private
*/
KeyboardNav.prototype.handleKeyDown = function(event){
var index;
switch (event.which) {
case 13: // Enter
case 32: // Space
// Select
this.toggleSelect(event.target);
event.preventDefault();
break;
case 37: // Left Arrow
case 38: // Up Arrow
// Go to previous Option
index = this.getElements().indexOf(event.currentTarget);
this.previousOption(index);
event.preventDefault();
break;
case 39: // Right Arrow
case 40: // Down Arrow
// Go to next Option
index = this.getElements().indexOf(event.currentTarget);
this.nextOption(index);
event.preventDefault();
break;
}
};
/**
* Get only elements from elements array
* @returns {Array}
*/
KeyboardNav.prototype.getElements = function () {
return this.elements.map(function (el) {
return el.el;
});
};
/**
* Handles element click. Toggles 'aria-selected' on element
*
* @param {MouseEvent} event Mouse click event
* @private
*/
KeyboardNav.prototype.onClick = function(event){
this.toggleSelect(event.currentTarget);
};
/**
* Creates a paylod for event that is fired
*
* @param {Number} index
* @return {KeyboardNavigationEventData}
*/
KeyboardNav.prototype.createEventPayload = function(index){
/**
* Data that is passed along as the event parameter
*
* @typedef {Object} KeyboardNavigationEventData
* @property {HTMLElement} element
* @property {number} index
* @property {boolean} selected
*/
return {
element: this.getElements()[index],
index: index,
selected: isElementSelected(this.getElements()[index])
};
};
/**
* Sets aria-selected="true" on an element
*
* @param {HTMLElement} el The element to set selected
* @return {boolean}
*/
var isElementSelected = function(el){
return el.getAttribute('aria-selected') === 'true';
};
return KeyboardNav;
})(H5P.EventDispatcher);
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists