handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
202 lines (193 loc) • 7.23 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import { createUniqueMap } from "../utils/dataStructures/uniqueMap.mjs";
import { stopImmediatePropagation } from "../helpers/dom/event.mjs";
import { createContext, isContextObject } from "./context.mjs";
import { useRecorder } from "./recorder.mjs";
import { toSingleLine } from "../helpers/templateLiteralTag.mjs";
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* The `ShortcutManager` API lets you store and manage [keyboard shortcut contexts](@/guides/navigation/keyboard-shortcuts/keyboard-shortcuts.md#keyboard-shortcut-contexts) ([`ShortcutContext`](@/api/shortcutContext.md)).
*
* Each `ShortcutManager` object:
* - Stores and manages its own set of keyboard shortcut contexts.
* - Listens to the [`KeyboardEvent`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) events and runs actions for them.
*
* @alias ShortcutManager
* @class ShortcutManager
* @param {object} options The manager's options
* @param {EventTarget} options.ownerWindow A starting `window` element
* @param {Function} options.handleEvent A condition on which `event` is handled.
* @param {Function} options.beforeKeyDown A hook fired before the `keydown` event is handled. You can use it to [block a keyboard shortcut's actions](@/guides/navigation/keyboard-shortcuts/keyboard-shortcuts.md#block-a-keyboard-shortcut-s-actions).
* @param {Function} options.afterKeyDown A hook fired after the `keydown` event is handled
*/
export const createShortcutManager = _ref => {
let {
ownerWindow,
handleEvent,
beforeKeyDown,
afterKeyDown
} = _ref;
/**
* A unique map that stores keyboard shortcut contexts.
*
* @type {UniqueMap}
*/
const CONTEXTS = createUniqueMap({
errorIdExists: keys => `The "${keys}" context name is already registered.`
});
/**
* The name of the active [`ShortcutContext`](@/api/shortcutContext.md).
*
* @type {string}
*/
let activeContextName = 'grid';
/**
* Create a new [`ShortcutContext`](@/api/shortcutContext.md) object.
*
* @memberof ShortcutManager#
* @param {string} contextName The name of the new shortcut context
* @param {string} [scope='table'] The scope of the shortcut: `'table'` or `'global'`
* @returns {object}
*/
const addContext = function (contextName) {
let scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'table';
const context = createContext(contextName, scope);
CONTEXTS.addItem(contextName, context);
return context;
};
/**
* Get the ID of the active [`ShortcutContext`](@/api/shortcutContext.md).
*
* @memberof ShortcutManager#
* @returns {string}
*/
const getActiveContextName = () => {
return activeContextName;
};
/**
* Get a keyboard shortcut context by its name.
*
* @memberof ShortcutManager#
* @param {string} contextName The name of the shortcut context
* @returns {object|undefined} A [`ShortcutContext`](@/api/shortcutContext.md) object that stores registered shortcuts
*/
const getContext = contextName => {
return CONTEXTS.getItem(contextName);
};
/**
* Start listening to keyboard shortcuts within a given [`ShortcutContext`](@/api/shortcutContext.md).
*
* @memberof ShortcutManager#
* @param {string} contextName The name of the shortcut context
*/
const setActiveContextName = contextName => {
if (!CONTEXTS.hasItem(contextName)) {
throw new Error(toSingleLine`You've tried to activate the "${contextName}" shortcut context\x20
that does not exist. Before activation, register the context using the "addContext" method.`);
}
activeContextName = contextName;
};
/**
* This variable relates to the `captureCtrl` shortcut option,
* which allows for capturing the state of the Control/Meta modifier key.
* Some of the default keyboard shortcuts related to cell selection need this feature for working properly.
*
* @type {boolean}
*/
let isCtrlKeySilenced = false;
/**
* A callback function for listening events from the recorder.
*
* @param {KeyboardEvent} event The keyboard event.
* @param {string[]} keys Names of the shortcut's keys,
* (coming from [`KeyboardEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values)),
* in lowercase or uppercase, unified across browsers.
* @param {object | string} context The context object or name.
* @returns {boolean}
*/
const recorderCallback = function (event, keys) {
let context = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getActiveContextName();
const activeContext = isContextObject(context) ? context : getContext(context);
let isExecutionCancelled = false;
if (!activeContext.hasShortcut(keys)) {
return isExecutionCancelled;
}
// Processing just actions being in stack at the moment of shortcut pressing (without respecting additions/removals performed dynamically).
const shortcuts = activeContext.getShortcuts(keys);
for (let index = 0; index < shortcuts.length; index++) {
const {
callback,
runOnlyIf,
preventDefault,
stopPropagation,
captureCtrl,
forwardToContext
} = shortcuts[index];
if (runOnlyIf(event) === true) {
isCtrlKeySilenced = captureCtrl;
isExecutionCancelled = callback(event, keys) === false;
isCtrlKeySilenced = false;
if (preventDefault) {
event.preventDefault();
}
if (stopPropagation) {
stopImmediatePropagation(event);
event.stopPropagation();
}
if (isExecutionCancelled) {
break;
}
if (forwardToContext) {
recorderCallback(event, keys, forwardToContext);
}
}
}
return isExecutionCancelled;
};
/**
* Handle the event with the scope of the active context.
*
* @param {KeyboardEvent} event The keyboard event.
* @returns {boolean}
*/
const handleEventWithScope = event => {
const context = getActiveContextName();
const activeContext = isContextObject(context) ? context : getContext(context);
return handleEvent(event, activeContext.scope);
};
/**
* Internal key recorder.
*
* @private
*/
const keyRecorder = useRecorder(ownerWindow, handleEventWithScope, beforeKeyDown, afterKeyDown, recorderCallback);
keyRecorder.mount();
return {
addContext,
getActiveContextName,
getContext,
setActiveContextName,
/**
* Returns whether `control` or `meta` keys are pressed.
*
* @memberof ShortcutManager#
* @type {Function}
* @returns {boolean}
*/
isCtrlPressed: () => !isCtrlKeySilenced && (keyRecorder.isPressed('control') || keyRecorder.isPressed('meta')),
/**
* Release every previously pressed key.
*
* @type {Function}
* @memberof ShortcutManager#
*/
releasePressedKeys: () => keyRecorder.releasePressedKeys(),
/**
* Destroy a context manager instance.
*
* @type {Function}
* @memberof ShortcutManager#
*/
destroy: () => keyRecorder.unmount()
};
};