Skip to content
Snippets Groups Projects
Select Git revision
  • 2af09a9231e1afb2fcd76e9f7a7185693a394dc6
  • master default protected
  • 1.31
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
  • 4.29.0
  • 4.28.0
  • 4.27.0
  • 4.26.0
  • 4.25.5
  • 4.25.4
  • 4.25.3
  • 4.25.2
  • 4.25.1
23 results

focusmanager.mjs

Blame
  • 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;
        }
    
    
    }