UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

228 lines (195 loc) 5.21 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ 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 focus manager the focus can be stored in a document, recalled and moved. * * @license AGPLv3 * @since 1.25.0 * @copyright Volker Schukai * @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. * @return {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) { const 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) { const 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; } }