handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
485 lines (475 loc) • 18.3 kB
JavaScript
import "core-js/modules/es.error.cause.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 { installFocusDetector } from "../../utils/focusDetector.mjs";
import { isObject } from "../../helpers/object.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:
* - `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();
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);
/**
* 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 => 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()
}));
_classPrivateFieldGet(_ui, this).addLocalHook('clickDialogElement', () => _assertClassBrand(_Dialog_brand, this, _onDialogClick).call(this));
_classPrivateFieldSet(_focusDetector, this, 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();
}
}
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);
}
}