UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

268 lines (257 loc) • 10.1 kB
"use strict"; exports.__esModule = true; exports.createFocusScopeManager = createFocusScopeManager; require("core-js/modules/es.error.cause.js"); require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.filter.js"); require("core-js/modules/esnext.iterator.for-each.js"); var _uniqueMap = require("../utils/dataStructures/uniqueMap"); var _scope = require("./scope"); var _eventListener = require("./eventListener"); var _constants = require("./constants"); var _element = require("../helpers/dom/element"); /** * @typedef {object} FocusScopeManager * @property {function(): string | null} getActiveScopeId Returns the ID of the active scope. * @property {function(string, HTMLElement, object): void} registerScope Registers a new focus scope. * @property {function(string): void} unregisterScope Unregisters a scope by its ID. * @property {function(string): void} activateScope Activates a focus scope by its ID. * @property {function(string): void} deactivateScope Deactivates a scope by its ID. * @property {function(): void} destroy Destroys the focus scope manager. */ /** * Creates a focus scope manager for a Handsontable instance. The manager handles focus * scopes by listening to keydown, focusin, and click events on the document. Based on * the currently focused element, it activates or deactivates the appropriate scope. * Focus scope contains its own boundaries and logic that once activated allows to focus * specific focusable element within the scope container element and/or switch to specific * shortcuts context. * * The manager also automatically updates the {@link Core#isListening} state of the Handsontable * instance based on the current state of the scopes. * * @alias FocusScopeManager * @class FocusScopeManager * @param {Core} hotInstance The Handsontable instance. */ function createFocusScopeManager(hotInstance) { const SCOPES = (0, _uniqueMap.createUniqueMap)({ errorIdExists: name => `The "${name}" focus scope is already registered.` }); const shortcutManager = hotInstance.getShortcutManager(); let activeScope = null; /** * Returns the ID of the active scope. * * @memberof FocusScopeManager# * @returns {string | null} The ID of the active scope. */ function getActiveScopeId() { if (!activeScope) { return null; } return SCOPES.getId(activeScope); } /** * Registers a new focus scope. * * @memberof FocusScopeManager# * @param {string} scopeId Unique identifier for the scope. * @param {HTMLElement} container Container element for the scope. * @param {object} [options] Configuration options. * @param {string} [options.shortcutsContextName='grid'] The name of the shortcuts context to switch to when * the scope is activated. * @param {'modal' | 'inline'} [options.type='inline'] The type of the scope:<br/> * - `modal`: The scope is modal and blocks the rest of the grid from receiving focus.<br/> * - `inline`: The scope is inline and allows the rest of the grid to receive focus in the order of the rendered elements in the DOM. * @param {function(): boolean} [options.runOnlyIf] Whether the scope is enabled or not depends on the custom logic. * @param {function(HTMLElement): boolean} [options.contains] Whether the target element is within the scope. If the option is not * provided, the scope will be activated if the target element is within the container element. * @param {function(): void} [options.onActivate] Callback function to be called when the scope is activated. * The first argument is the source of the activation:<br/> * - `unknown`: The scope is activated by an unknown source.<br/> * - `click`: The scope is activated by a click event.<br/> * - `tab_from_above`: The scope is activated by a tab key press.<br/> * - `tab_from_below`: The scope is activated by a shift+tab key press. * @param {function(): void} [options.onDeactivate] Callback function to be called when the scope is deactivated. * * @example * For regular element (inline scope) * * ```js * hot.getFocusScopeManager().registerScope('myPluginName', containerElement, { * shortcutsContextName: 'plugin:myPluginName', * onActivate: (focusSource) => { * // Focus the internal focusable element within the plugin UI element * }, * }); * ``` * * or for modal scope * * ```js * hot.getFocusScopeManager().registerScope('myPluginName', containerElement, { * shortcutsContextName: 'plugin:myPluginName', * type: 'modal', * runOnlyIf: () => isDialogOpened(), * onActivate: (focusSource) => { * // Focus the internal focusable element within the plugin UI element * }, * }); * ``` */ function registerScope(scopeId, container) { let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; if (SCOPES.hasItem(scopeId)) { throw new Error(`Scope with id "${scopeId}" already registered`); } const scope = (0, _scope.createFocusScope)(hotInstance, container, options); SCOPES.addItem(scopeId, scope); shortcutManager.getOrCreateContext(scope.getShortcutsContextName()); } /** * Unregisters a scope completely. * * @memberof FocusScopeManager# * @param {string} scopeId The scope to remove. */ function unregisterScope(scopeId) { if (!SCOPES.hasItem(scopeId)) { throw new Error(`Scope with id "${scopeId}" not found`); } const scope = SCOPES.getItem(scopeId); scope.destroy(); SCOPES.removeItem(scopeId); } /** * Activates a focus scope by its ID. * * @memberof FocusScopeManager# * @alias FocusScopeManager#activateScope * @param {string} scopeId The ID of the scope to activate. */ function activateScopeById(scopeId) { if (!SCOPES.hasItem(scopeId)) { throw new Error(`Scope with id "${scopeId}" not found`); } activateScope(SCOPES.getItem(scopeId)); } /** * Deactivates a scope by its ID. * * @memberof FocusScopeManager# * @alias FocusScopeManager#deactivateScope * @param {string} scopeId The ID of the scope to deactivate. */ function deactivateScopeById(scopeId) { if (!SCOPES.hasItem(scopeId)) { throw new Error(`Scope with id "${scopeId}" not found`); } deactivateScope(SCOPES.getItem(scopeId)); } /** * Activates a specific scope. * * @param {object} scope The scope to activate. * @param {'unknown' | 'click' | 'tab_from_above' | 'tab_from_below'} focusSource The source of the focus event. */ function activateScope(scope) { let focusSource = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _constants.FOCUS_SOURCES.UNKNOWN; if (activeScope === scope) { return; } if (activeScope !== null) { deactivateScope(activeScope); } activeScope = scope; activeScope.activate(focusSource); shortcutManager.setActiveContextName(scope.getShortcutsContextName()); } /** * Deactivates a scope by its ID. * * @param {object} scope The scope to deactivate. */ function deactivateScope(scope) { updateScopesFocusVisibilityState(); if (activeScope !== scope) { return; } activeScope = null; scope.deactivate(); } /** * Updates the focus scopes state by enabling or disabling them or their focus catchers to make sure * that the next native focus move won't be disturbed. */ function updateScopesFocusVisibilityState() { const scopes = SCOPES.getValues(); const modalScopes = scopes.filter(scope => scope.runOnlyIf() && scope.getType() === 'modal'); scopes.forEach(scope => { if (modalScopes.length > 0 && modalScopes.includes(scope) || modalScopes.length === 0 || scope.hasContainerDetached()) { scope.enable(); } else { scope.disable(); } if (scope === activeScope) { if (scope.contains(hotInstance.rootDocument.activeElement)) { scope.deactivateFocusCatchers(); } else { scope.activateFocusCatchers(); } } else if (scope.runOnlyIf()) { scope.activateFocusCatchers(); } else { scope.deactivateFocusCatchers(); } }); } /** * Activates or deactivates the appropriate scope based on the target element that was * triggered by the focus or click event. * * @param {HTMLElement} target The target element. * @param {'unknown' | 'click' | 'tab_from_above' | 'tab_from_below'} focusSource The source of the focus event. */ function processScopes(target, focusSource) { if (!target.isConnected || !(0, _element.isVisible)(target)) { return; } const allEnabledScopes = SCOPES.getValues().filter(scope => scope.runOnlyIf()); let hasActiveScope = false; allEnabledScopes.forEach(scope => { if (!hasActiveScope && scope.contains(target)) { hasActiveScope = true; if (focusSource !== _constants.FOCUS_SOURCES.UNKNOWN) { hotInstance.listen(); } activateScope(scope, focusSource); } }); if (!hasActiveScope && activeScope) { deactivateScope(activeScope); hotInstance.unlisten(); } } const eventListener = (0, _eventListener.useEventListener)(hotInstance.rootWindow, { onFocus: event => { var _event$target$dataset; processScopes(event.target, (_event$target$dataset = event.target.dataset.htFocusSource) !== null && _event$target$dataset !== void 0 ? _event$target$dataset : _constants.FOCUS_SOURCES.UNKNOWN); }, onClick: event => { processScopes(event.target, _constants.FOCUS_SOURCES.CLICK); }, onTabKeyDown: () => { updateScopesFocusVisibilityState(); } }); eventListener.mount(); return { getActiveScopeId, registerScope, unregisterScope, activateScope: scopeId => activateScopeById(scopeId), deactivateScope: scopeId => deactivateScopeById(scopeId), destroy: () => eventListener.unmount() }; }