UNPKG

handsontable

Version:

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

600 lines (589 loc) • 24.3 kB
import "core-js/modules/es.error.cause.js"; import "core-js/modules/es.array.at.js"; import "core-js/modules/es.string.at-alternative.js"; import "core-js/modules/esnext.iterator.constructor.js"; import "core-js/modules/esnext.iterator.every.js"; 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"); } import { BasePlugin } from "../base/index.mjs"; import { DialogUI } from "./ui.mjs"; import { isObject } from "../../helpers/object.mjs"; import * as C from "../../i18n/constants.mjs"; export const PLUGIN_KEY = 'dialog'; export const 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: * - `template`: The template to use for the dialog (default: `null`). The error will be thrown when * the template is provided together with the `content` option. * - `type`: The type of the template ('confirm') * - `title`: The title of the dialog * - `description`: The description of the dialog (default: '') * - `buttons`: The buttons to display in the dialog (default: []) * - `text`: The text of the button * - `type`: The type of the button ('primary' | 'secondary') * - `callback`: The callback to trigger when the button is clicked * - `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) * - `role`: The role of the dialog ('dialog' | 'alertdialog') (default: 'dialog') * - `ariaLabel`: The label of the dialog (default: 'Dialog') * - `ariaLabelledby`: The ID of the element that labels the dialog (default: '') * - `ariaDescribedby`: The 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', * } * } * * // Enable dialog plugin using prebuild templates * dialog: { * template: { * type: 'confirm', * title: 'Confirm', * description: 'This is a confirm', * buttons: [ * { * text: 'Ok', * type: 'primary', * callback: () => { * console.log('Ok'); * } * }, * { * text: 'Cancel', * type: 'secondary', * callback: () => { * console.log('Cancel'); * } * }, * ], * }, * } * * // 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 _selectionState = /*#__PURE__*/new WeakMap(); var _Dialog_brand = /*#__PURE__*/new WeakSet(); export class Dialog extends 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); /** * 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 { template: null, content: '', customClassName: '', background: 'solid', contentBackground: false, animation: true, closable: false, a11y: { role: 'dialog', ariaLabel: 'Dialog', ariaLabelledby: '', ariaDescribedby: '' } }; } static get SETTINGS_VALIDATORS() { return { template: value => isObject(value) && typeof ['alert', 'confirm'].includes(value.type) && typeof value.title === 'string' && (typeof (value === null || value === void 0 ? void 0 : value.description) === 'undefined' || typeof (value === null || value === void 0 ? void 0 : value.description) === 'string') && (typeof (value === null || value === void 0 ? void 0 : value.buttons) === 'undefined' || Array.isArray(value === null || value === void 0 ? void 0 : value.buttons) && value.buttons.every(item => typeof item === 'object' && typeof item.text === 'string' && ['primary', 'secondary'].includes(item.type) && (typeof item.callback === 'undefined' || typeof item.callback === 'function'))), 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 => 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 DialogUI({ rootElement: this.hot.rootGridElement, isRtl: this.hot.isRtl() })); } _assertClassBrand(_Dialog_brand, this, _registerShortcuts).call(this); _assertClassBrand(_Dialog_brand, this, _registerFocusScope).call(this); this.addHook('afterViewRender', () => _assertClassBrand(_Dialog_brand, this, _onAfterViewRender).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); _assertClassBrand(_Dialog_brand, this, _unregisterFocusScope).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 {object} options.template The template to use for the dialog (default: `null`). The error will be thrown when * the template is provided together with the `content` option. * @param {'confirm'} options.template.type The type of the template ('confirm'). * @param {string} options.template.title The title of the dialog. * @param {string} options.template.description The description of the dialog. Default: ''. * @param {object[]} options.template.buttons The buttons to display in the dialog. Default: []. * @param {string} options.template.buttons.text The text of the button. * @param {'primary' | 'secondary'} options.template.buttons.type The type of the button. * @param {function(MouseEvent)} options.template.buttons.callback The callback to trigger when the button is clicked. * @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); this.hot.getFocusScopeManager().activateScope(PLUGIN_KEY); _classPrivateFieldSet(_selectionState, this, this.hot.selection.exportSelection()); this.hot.deselectCell(); this.hot.runHooks('afterDialogShow'); } /** * Hide the currently open dialog. * Closes the dialog and restores the focus to the table. */ hide() { var _classPrivateFieldGet2; if (!this.isVisible()) { return; } this.hot.runHooks('beforeDialogHide'); _classPrivateFieldGet(_ui, this).hideDialog(this.getSetting('animation')); _classPrivateFieldSet(_isVisible, this, false); this.hot.getFocusScopeManager().deactivateScope(PLUGIN_KEY); if (((_classPrivateFieldGet2 = _classPrivateFieldGet(_selectionState, this)) === null || _classPrivateFieldGet2 === void 0 ? void 0 : _classPrivateFieldGet2.ranges.length) > 0) { 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 {object} options.template The template to use for the dialog (default: `null`). The error will be thrown when * the template is provided together with the `content` option. * @param {'confirm'} options.template.type The type of the template ('confirm'). * @param {string} options.template.title The title of the dialog. * @param {string} options.template.description The description of the dialog. Default: ''. * @param {object[]} options.template.buttons The buttons to display in the dialog. Default: []. * @param {string} options.template.buttons.text The text of the button. * @param {'primary' | 'secondary'} options.template.buttons.type The type of the button. * @param {function(MouseEvent)} options.template.buttons.callback The callback to trigger when the button is clicked. * @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); const templateValue = this.getSetting('template'); if (templateValue !== Dialog.DEFAULT_SETTINGS.template && this.getSetting('content') !== Dialog.DEFAULT_SETTINGS.content) { throw new Error('The `template` option cannot be used together with the `content` option.'); } if (templateValue) { _classPrivateFieldGet(_ui, this).useTemplate(templateValue.type, { id: this.hot.guid, ...templateValue }); } else { _classPrivateFieldGet(_ui, this).useDefaultTemplate(); } _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') }); } /** * Displays the alert dialog with the specified content. * * @param {string | { title: string, description: string }} message The message to display in the dialog. * Can be a string or an object with `title` and `description` properties. * @param {function(MouseEvent): void} [callback] The callback to trigger when the button is clicked. */ showAlert(message, callback) { const { title = 'Alert', description } = isObject(message) ? message : { title: message }; this.show({ template: { type: 'confirm', title, description, buttons: [{ text: this.hot.getTranslatedPhrase(C.OK), type: 'primary', callback: function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return callback === null || callback === void 0 ? void 0 : callback(...args); } }] }, contentBackground: false, background: 'solid', animation: true, closable: false }); } /** * Displays the confirm dialog with the specified content and options. * * @param {string | { title: string, description: string }} message The message to display in the dialog. * Can be a string or an object with `title` and `description` properties. * @param {function(MouseEvent): void} [onOk] The callback to trigger when the OK button is clicked. * @param {function(MouseEvent): void} [onCancel] The callback to trigger when the Cancel button is clicked. */ showConfirm(message, onOk, onCancel) { const { title = 'Confirm', description } = isObject(message) ? message : { title: message }; this.show({ template: { type: 'confirm', title, description, buttons: [{ text: this.hot.getTranslatedPhrase(C.CANCEL), type: 'secondary', callback: function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return onCancel === null || onCancel === void 0 ? void 0 : onCancel(...args); } }, { text: this.hot.getTranslatedPhrase(C.OK), type: 'primary', callback: function () { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } return onOk === null || onOk === void 0 ? void 0 : onOk(...args); } }] }, contentBackground: true, background: 'semi-transparent', animation: true, closable: false }); } /** * Focus the dialog. */ focus() { _classPrivateFieldGet(_ui, this).focusDialog(); } /** * Destroy dialog and reset plugin state. */ destroy() { var _classPrivateFieldGet3; (_classPrivateFieldGet3 = _classPrivateFieldGet(_ui, this)) === null || _classPrivateFieldGet3 === void 0 || _classPrivateFieldGet3.destroyDialog(); _classPrivateFieldSet(_ui, this, null); _classPrivateFieldSet(_isVisible, this, false); _classPrivateFieldSet(_selectionState, this, null); super.destroy(); } } 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(() => { if (event.shiftKey) { this.hot.runHooks('dialogFocusPreviousElement'); } else { this.hot.runHooks('dialogFocusNextElement'); } }); }, 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); } /** * Registers the focus scope for the dialog plugin. */ function _registerFocusScope() { this.hot.getFocusScopeManager().registerScope(PLUGIN_KEY, _classPrivateFieldGet(_ui, this).getContainer(), { shortcutsContextName: SHORTCUTS_CONTEXT_NAME, type: 'modal', runOnlyIf: () => this.isVisible(), onActivate: focusSource => { const isListening = this.hot.isListening(); const focusableElements = _classPrivateFieldGet(_ui, this).getFocusableElements(); if (focusableElements.length > 0) { if (focusSource === 'tab_from_above') { focusableElements.at(0).focus(); } else if (focusSource === 'tab_from_below') { focusableElements.at(-1).focus(); } } else if (focusSource !== 'tab_from_above' && focusSource !== 'tab_from_below' && isListening && !_classPrivateFieldGet(_ui, this).getContainer().contains(this.hot.rootDocument.activeElement)) { _classPrivateFieldGet(_ui, this).getContainer().focus(); } if (isListening) { this.hot.runHooks('afterDialogFocus', focusSource === 'unknown' ? 'show' : focusSource); } } }); } /** * Unregisters the focus scope for the dialog plugin. */ function _unregisterFocusScope() { this.hot.getFocusScopeManager().unregisterScope(PLUGIN_KEY); } /** * 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 _onAfterViewRender() { 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 = Number.parseFloat(rootWindow.getComputedStyle(dialogInfo).marginTop); _classPrivateFieldGet(_ui, this).updateHeight(height + marginTop); } }