Something went wrong on our end
Select Git revision
focusmanager.mjs
-
Volker Schukai authoredVolker Schukai authored
focusmanager.mjs 5.43 KiB
/**
* Copyright schukai GmbH and contributors 2022. All Rights Reserved.
* Node module: @schukai/monster
* This file is licensed under the AGPLv3 License.
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
*/
import {extend} from "../data/extend.mjs";
import {BaseWithOptions} from "../types/basewithoptions.mjs";
import {getGlobalObject} from "../types/global.mjs";
import {isArray} from "../types/is.mjs";
import {Stack} from "../types/stack.mjs";
import {validateInstance, validateString} from "../types/validate.mjs";
import {instanceSymbol} from '../constants.mjs';
export {FocusManager}
/**
* @private
* @type {string}
*/
const KEY_DOCUMENT = 'document';
/**
* @private
* @type {string}
*/
const KEY_CONTEXT = 'context';
/**
* @private
* @type {Symbol}
*/
const stackSymbol = Symbol('stack');
/**
* With the focusmanager the focus can be stored in a document, recalled and moved.
*
* @license AGPLv3
* @since 1.25.0
* @copyright schukai GmbH
* @memberOf Monster.DOM
* @throws {Error} unsupported locale
* @summary Handle the focus
*/
class FocusManager extends BaseWithOptions {
/**
*
* @param {Object|undefined} options
*/
constructor(options) {
super(options);
validateInstance(this.getOption(KEY_DOCUMENT), HTMLDocument);
this[stackSymbol] = new Stack();
}
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
* @since 2.1.0
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/dom/focusmanager");
}
/**
* @property {HTMLDocument} document the document object into which the node is to be appended
*/
get defaults() {
return extend({}, super.defaults, {
[KEY_DOCUMENT]: getGlobalObject('document'),
[KEY_CONTEXT]: undefined,
})
}
/**
* Remembers the current focus on a stack.
* Several focus can be stored.
*
* @return {Monster.DOM.FocusManager}
*/
storeFocus() {
const active = this.getActive();
if (active instanceof Node) {
this[stackSymbol].push(active)
}
return this;
}
/**
* The last focus on the stack is set again
*
* @return {Monster.DOM.FocusManager}
*/
restoreFocus() {
const last = this[stackSymbol].pop();
if (last instanceof Node) {
this.focus(last);
}
return this;
}
/**
*
* @param {Node} element
* @param {boolean} preventScroll
* @throws {TypeError} value is not an instance of
* @return {Monster.DOM.FocusManager}
*/
focus(element, preventScroll) {
validateInstance(element, Node)
element.focus({
preventScroll: preventScroll ?? false
})
return this;
}
/**
*
* @return {Element}
*/
getActive() {
return this.getOption(KEY_DOCUMENT).activeElement;
}
/**
* Select all elements that can be focused
*
* @param {string|undefined} query
* @return {array}
* @throws {TypeError} value is not an instance of
*/
getFocusable(query) {
let contextElement = this.getOption(KEY_CONTEXT);
if (contextElement === undefined) {
contextElement = this.getOption(KEY_DOCUMENT);
}
validateInstance(contextElement, Node)
if (query !== undefined) {
validateString(query);
}
return [...contextElement.querySelectorAll(
'details, button, input, [tabindex]:not([tabindex="-1"]), select, textarea, a[href], body'
)].filter((element) => {
if (query !== undefined && !element.matches(query)) {
return false;
}
if (element.hasAttribute('disabled')) return false;
if (element.getAttribute("aria-hidden") === 'true') return false;
const rect = element.getBoundingClientRect();
if(rect.width===0) return false;
if(rect.height===0) return false;
return true;
});
}
/**
* @param {string} query
* @return {Monster.DOM.FocusManager}
*/
focusNext(query) {
const current = this.getActive();
const focusable = this.getFocusable(query);
if (!isArray(focusable) || focusable.length === 0) {
return this;
}
if (current instanceof Node) {
let index = focusable.indexOf(current);
if (index > -1) {
this.focus(focusable[index + 1] || focusable[0]);
} else {
this.focus(focusable[0]);
}
} else {
this.focus(focusable[0])
}
return this;
}
/**
* @param {string} query
* @return {Monster.DOM.FocusManager}
*/
focusPrev(query) {
const current = this.getActive();
const focusable = this.getFocusable(query);
if (!isArray(focusable) || focusable.length === 0) {
return this;
}
if (current instanceof Node) {
let index = focusable.indexOf(current);
if (index > -1) {
this.focus(focusable[index - 1] || focusable[focusable.length - 1]);
} else {
this.focus(focusable[focusable.length - 1]);
}
} else {
this.focus(focusable[focusable.length - 1])
}
return this;
}
}