UNPKG

handsontable

Version:

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

489 lines (478 loc) • 18.4 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); var _base = require("../base"); var _ui2 = require("./ui"); var _focusDetector2 = require("../../utils/focusDetector"); var _object = require("../../helpers/object"); function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } const PLUGIN_KEY = exports.PLUGIN_KEY = 'dialog'; const PLUGIN_PRIORITY = exports.PLUGIN_PRIORITY = 360; const SHORTCUTS_GROUP = PLUGIN_KEY; const SHORTCUTS_CONTEXT_NAME = `plugin:${PLUGIN_KEY}`; /* eslint-disable jsdoc/require-description-complete-sentence */ /** * @plugin Dialog * @class Dialog * * @description * The dialog plugin provides a modal dialog system for Handsontable. It allows you to display custom content in modal dialogs * that overlay the table, providing a way to show notifications, error messages, loading indicators, or any other interactive content. * * In order to enable the dialog mechanism, {@link Options#dialog} option must be set to `true`. * * The plugin provides several configuration options to customize the dialog behavior and appearance: * - `content`: The string or HTMLElement content to display in the dialog (default: '') * - `customClassName`: Custom class name to apply to the dialog (default: '') * - `background`: Dialog background variant 'solid' | 'semi-transparent' (default: 'solid') * - `contentBackground`: Whether to show content background (default: false) * - `animation`: Whether to enable animations (default: true) * - `closable`: Whether the dialog can be closed (default: false) * - `a11y`: Object with accessibility options (default object below) * ```js * { * role: 'dialog', // Role of the dialog 'dialog' | 'alertdialog' (default: 'dialog') * ariaLabel: 'Dialog', // Label for the dialog (default: 'Dialog') * ariaLabelledby: '', // ID of the element that labels the dialog (default: '') * ariaDescribedby: '', // ID of the element that describes the dialog (default: ''), * } * ``` * * @example * * ::: only-for javascript * ```js * // Enable dialog plugin with default options * dialog: true, * * // Enable dialog plugin with custom configuration * dialog: { * content: 'Dialog content', * customClassName: 'custom-dialog', * background: 'semi-transparent', * contentBackground: false, * animation: false, * closable: true, * a11y: { * role: 'dialog', * ariaLabel: 'Dialog', * ariaLabelledby: 'titleID', * ariaDescribedby: 'descriptionID', * } * } * * // Access to dialog plugin instance: * const dialogPlugin = hot.getPlugin('dialog'); * * // Show a dialog programmatically: * dialogPlugin.show({ * content: '<h2>Custom Dialog</h2><p>This is a custom dialog content.</p>', * closable: true, * }); * * // Hide the dialog programmatically: * dialogPlugin.hide(); * * // Check if dialog is visible: * const isVisible = dialogPlugin.isVisible(); * ``` * ::: * * ::: only-for react * ```jsx * const MyComponent = () => { * const hotRef = useRef(null); * * useEffect(() => { * const hot = hotRef.current.hotInstance; * const dialogPlugin = hot.getPlugin('dialog'); * * dialogPlugin.show({ * content: <div> * <h2>React Dialog</h2> * <p>Dialog content rendered with React</p> * </div>, * closable: true * }); * }, []); * * return ( * <HotTable * ref={hotRef} * settings={{ * data: data, * dialog: { * customClassName: 'react-dialog', * closable: true * } * }} * /> * ); * } * ``` * ::: * * ::: only-for angular * ```ts * hotSettings: Handsontable.GridSettings = { * data: data, * dialog: { * customClassName: 'angular-dialog', * closable: true * } * } * ``` * * ```html * <hot-table * [settings]="hotSettings"> * </hot-table> * ``` * ::: */ var _ui = /*#__PURE__*/new WeakMap(); var _isVisible = /*#__PURE__*/new WeakMap(); var _focusDetector = /*#__PURE__*/new WeakMap(); var _selectionState = /*#__PURE__*/new WeakMap(); var _Dialog_brand = /*#__PURE__*/new WeakSet(); class Dialog extends _base.BasePlugin { constructor() { super(...arguments); /** * Register shortcuts responsible for closing the dialog and navigating through the dialog. */ _classPrivateMethodInitSpec(this, _Dialog_brand); /** * UI instance of the dialog plugin. * * @type {DialogUI} */ _classPrivateFieldInitSpec(this, _ui, null); /** * Flag indicating if dialog is currently visible. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _isVisible, false); /** * Focus detector instance. * * @type {FocusDetector} */ _classPrivateFieldInitSpec(this, _focusDetector, null); /** * Keeps the selection state that will be restored after the dialog is closed. * * @type {SelectionState | null} */ _classPrivateFieldInitSpec(this, _selectionState, null); } static get PLUGIN_KEY() { return PLUGIN_KEY; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } static get DEFAULT_SETTINGS() { return { content: '', customClassName: '', background: 'solid', contentBackground: false, animation: true, closable: false, a11y: { role: 'dialog', ariaLabel: 'Dialog', ariaLabelledby: '', ariaDescribedby: '' } }; } static get SETTINGS_VALIDATORS() { return { content: value => typeof value === 'string' || typeof HTMLElement !== 'undefined' && value instanceof HTMLElement || typeof DocumentFragment !== 'undefined' && value instanceof DocumentFragment, customClassName: value => typeof value === 'string', background: value => ['solid', 'semi-transparent'].includes(value), contentBackground: value => typeof value === 'boolean', animation: value => typeof value === 'boolean', closable: value => typeof value === 'boolean', a11y: value => (0, _object.isObject)(value) && (typeof (value === null || value === void 0 ? void 0 : value.role) === 'undefined' || ['dialog', 'alertdialog'].includes(value === null || value === void 0 ? void 0 : value.role)) && (typeof (value === null || value === void 0 ? void 0 : value.ariaLabel) === 'undefined' || typeof (value === null || value === void 0 ? void 0 : value.ariaLabel) === 'string') && (typeof (value === null || value === void 0 ? void 0 : value.ariaLabelledby) === 'undefined' || typeof (value === null || value === void 0 ? void 0 : value.ariaLabelledby) === 'string') && (typeof (value === null || value === void 0 ? void 0 : value.ariaDescribedby) === 'undefined' || typeof (value === null || value === void 0 ? void 0 : value.ariaDescribedby) === 'string') }; } /** * Check if the plugin is enabled in the handsontable settings. * * @returns {boolean} */ isEnabled() { return !!this.hot.getSettings()[PLUGIN_KEY]; } /** * Enable plugin for this Handsontable instance. */ enablePlugin() { if (this.enabled) { return; } if (!_classPrivateFieldGet(_ui, this)) { _classPrivateFieldSet(_ui, this, new _ui2.DialogUI({ rootElement: this.hot.rootGridElement, isRtl: this.hot.isRtl() })); _classPrivateFieldGet(_ui, this).addLocalHook('clickDialogElement', () => _assertClassBrand(_Dialog_brand, this, _onDialogClick).call(this)); _classPrivateFieldSet(_focusDetector, this, (0, _focusDetector2.installFocusDetector)(this.hot, _classPrivateFieldGet(_ui, this).getDialogElement(), { onFocus: from => { this.hot.getShortcutManager().setActiveContextName(SHORTCUTS_CONTEXT_NAME); this.hot.listen(); this.hot.runHooks('afterDialogFocus', `tab_${from}`); } })); } _assertClassBrand(_Dialog_brand, this, _registerShortcuts).call(this); this.addHook('modifyFocusOnTabNavigation', from => _assertClassBrand(_Dialog_brand, this, _onFocusTabNavigation).call(this, from), 1); this.addHook('afterViewRender', () => _assertClassBrand(_Dialog_brand, this, _onAfterRender).call(this)); this.addHook('afterListen', () => _assertClassBrand(_Dialog_brand, this, _onAfterListen).call(this)); this.addHook('afterUnlisten', () => _assertClassBrand(_Dialog_brand, this, _onAfterUnlisten).call(this)); super.enablePlugin(); } /** * Update plugin state after Handsontable settings update. */ updatePlugin() { this.disablePlugin(); this.enablePlugin(); super.updatePlugin(); } /** * Disable plugin for this Handsontable instance. */ disablePlugin() { this.hide(); _assertClassBrand(_Dialog_brand, this, _unregisterShortcuts).call(this); super.disablePlugin(); } /** * Check if the dialog is currently visible. * * @returns {boolean} True if the dialog is visible, false otherwise. */ isVisible() { return _classPrivateFieldGet(_isVisible, this); } /** * Show dialog with given configuration. * Displays the dialog with the specified content and options. * * @param {object} options Dialog configuration object containing content and display options. * @param {string|HTMLElement|DocumentFragment} options.content The content to display in the dialog. Can be a string, HTMLElement, or DocumentFragment. Default: '' * @param {string} options.customClassName Custom CSS class name to apply to the dialog container. Default: '' * @param {'solid'|'semi-transparent'} options.background Dialog background variant. Default: 'solid'. * @param {boolean} options.contentBackground Whether to show content background. Default: false. * @param {boolean} options.animation Whether to enable animations when showing/hiding the dialog. Default: true. * @param {boolean} options.closable Whether the dialog can be closed by user interaction. Default: false. * @param {object} options.a11y Object with accessibility options. * @param {string} options.a11y.role The role of the dialog. Default: 'dialog'. * @param {string} options.a11y.ariaLabel The label of the dialog. Default: 'Dialog'. * @param {string} options.a11y.ariaLabelledby The ID of the element that labels the dialog. Default: ''. * @param {string} options.a11y.ariaDescribedby The ID of the element that describes the dialog. Default: ''. */ show() { let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; if (!this.enabled) { return; } if (this.isVisible()) { this.update(options); return; } this.hot.runHooks('beforeDialogShow'); this.update(options); _classPrivateFieldGet(_ui, this).showDialog(this.getSetting('animation')); _classPrivateFieldSet(_isVisible, this, true); _classPrivateFieldSet(_selectionState, this, this.hot.selection.exportSelection()); this.hot.deselectCell(); this.hot.runHooks('afterDialogShow'); const { activeElement } = this.hot.rootDocument; if (this.hot.rootWrapperElement.contains(activeElement) || this.hot.rootPortalElement.contains(activeElement)) { this.hot.unlisten(); this.hot.getShortcutManager().setActiveContextName(SHORTCUTS_CONTEXT_NAME); this.hot.listen(); _classPrivateFieldGet(_ui, this).focusDialog(); this.hot.runHooks('afterDialogFocus', 'show'); } } /** * Hide the currently open dialog. * Closes the dialog and restores the focus to the table. */ hide() { if (!this.isVisible()) { return; } this.hot.runHooks('beforeDialogHide'); _classPrivateFieldGet(_ui, this).hideDialog(this.getSetting('animation')); this.hot.getShortcutManager().setActiveContextName('grid'); _classPrivateFieldSet(_isVisible, this, false); if (_classPrivateFieldGet(_selectionState, this)) { this.hot.selection.importSelection(_classPrivateFieldGet(_selectionState, this)); this.hot.view.render(); _classPrivateFieldSet(_selectionState, this, null); } else { this.hot.selectCell(0, 0); } this.hot.runHooks('afterDialogHide'); } /** * Update the dialog configuration. * * @param {object} options Dialog configuration object containing content and display options. * @param {string|HTMLElement|DocumentFragment} options.content The content to display in the dialog. Can be a string, HTMLElement, or DocumentFragment. Default: '' * @param {string} options.customClassName Custom CSS class name to apply to the dialog container. Default: '' * @param {'solid'|'semi-transparent'} options.background Dialog background variant. Default: 'solid'. * @param {boolean} options.contentBackground Whether to show content background. Default: false. * @param {boolean} options.animation Whether to enable animations when showing/hiding the dialog. Default: true. * @param {boolean} options.closable Whether the dialog can be closed by user interaction. Default: false. * @param {object} options.a11y Object with accessibility options. * @param {string} options.a11y.role The role of the dialog. Default: 'dialog'. * @param {string} options.a11y.ariaLabel The label of the dialog. Default: 'Dialog'. * @param {string} options.a11y.ariaLabelledby The ID of the element that labels the dialog. Default: ''. * @param {string} options.a11y.ariaDescribedby The ID of the element that describes the dialog. Default: ''. */ update(options) { if (!this.enabled) { return; } this.updatePluginSettings(options); _classPrivateFieldGet(_ui, this).updateDialog({ isVisible: this.isVisible(), content: this.getSetting('content'), customClassName: this.getSetting('customClassName'), background: this.getSetting('background'), contentBackground: this.getSetting('contentBackground'), animation: this.getSetting('animation'), a11y: this.getSetting('a11y') }); } /** * Focus the dialog. */ focus() { _classPrivateFieldGet(_ui, this).focusDialog(); } /** * Handle focus tab navigation event. * * @param {'from_above' | 'from_below'} from The direction from which the focus was modified. * @returns {boolean} Returns `false` to prevent the default focus behavior. */ /** * Destroy dialog and reset plugin state. */ destroy() { var _classPrivateFieldGet2; (_classPrivateFieldGet2 = _classPrivateFieldGet(_ui, this)) === null || _classPrivateFieldGet2 === void 0 || _classPrivateFieldGet2.destroyDialog(); _classPrivateFieldSet(_ui, this, null); _classPrivateFieldSet(_isVisible, this, false); _classPrivateFieldSet(_focusDetector, this, null); _classPrivateFieldSet(_selectionState, this, null); super.destroy(); } } exports.Dialog = Dialog; function _registerShortcuts() { var _manager$getContext; const manager = this.hot.getShortcutManager(); const pluginContext = (_manager$getContext = manager.getContext(SHORTCUTS_CONTEXT_NAME)) !== null && _manager$getContext !== void 0 ? _manager$getContext : manager.addContext(SHORTCUTS_CONTEXT_NAME); pluginContext.addShortcut({ keys: [['Escape']], callback: () => { this.hide(); }, runOnlyIf: () => _classPrivateFieldGet(_isVisible, this) && this.getSetting('closable'), group: SHORTCUTS_GROUP }); pluginContext.addShortcut({ keys: [['Shift', 'Tab'], ['Tab']], preventDefault: false, callback: event => { this.hot._registerTimeout(() => { const { activeElement } = this.hot.rootDocument; if (!_classPrivateFieldGet(_ui, this).isInsideDialog(activeElement)) { this.hot.unlisten(); return; } if (event.shiftKey) { this.hot.runHooks('dialogFocusPreviousElement'); } else { this.hot.runHooks('dialogFocusNextElement'); } }, 0); }, group: SHORTCUTS_GROUP }); } /** * Unregister shortcuts responsible for closing the dialog and navigating through the dialog. */ function _unregisterShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const pluginContext = shortcutManager.getContext(SHORTCUTS_CONTEXT_NAME); pluginContext.removeShortcutsByGroup(SHORTCUTS_GROUP); } function _onFocusTabNavigation(from) { if (this.isVisible()) { _classPrivateFieldGet(_focusDetector, this).focus(from); return false; } } /** * Handle dialog click event. */ function _onDialogClick() { if (this.isVisible() && !this.hot.isListening()) { this.hot.getShortcutManager().setActiveContextName(SHORTCUTS_CONTEXT_NAME); this.hot.runHooks('afterDialogFocus', 'click'); } this.hot.listen(); } /** * Called after the table is listened. */ function _onAfterListen() { _classPrivateFieldGet(_focusDetector, this).deactivate(); } /** * Called after the table is unlistened. */ function _onAfterUnlisten() { _classPrivateFieldGet(_focusDetector, this).activate(); } /** * Called after the rendering of the table is completed. It updates the width and * height of the dialog container to the same size as the table. */ function _onAfterRender() { const { view, rootWrapperElement, rootWindow } = this.hot; const width = view.isHorizontallyScrollableByWindow() ? view.getTotalTableWidth() : view.getWorkspaceWidth(); _classPrivateFieldGet(_ui, this).updateWidth(width); const dialogInfo = rootWrapperElement.querySelector('.hot-display-license-info'); if (dialogInfo) { const height = dialogInfo.offsetHeight; const marginTop = parseFloat(rootWindow.getComputedStyle(dialogInfo).marginTop); _classPrivateFieldGet(_ui, this).updateHeight(height + marginTop); } }