UNPKG

handsontable

Version:

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

521 lines (514 loc) 19.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"; import "core-js/modules/esnext.iterator.filter.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 { EmptyDataStateUI } from "./ui.mjs"; import { isObject } from "../../helpers/object.mjs"; import * as C from "../../i18n/constants.mjs"; export const PLUGIN_KEY = 'emptyDataState'; export const PLUGIN_PRIORITY = 370; export const EMPTY_DATA_STATE_CLASS_NAME = `ht-${PLUGIN_KEY}`; const SOURCE = Object.freeze({ UNKNOWN: 'unknown', FILTERS: 'filters' }); const SHORTCUTS_CONTEXT_NAME = `plugin:${PLUGIN_KEY}`; /** * @plugin EmptyDataState * @class EmptyDataState * * @description * The empty data state plugin provides a empty data state overlay system for Handsontable. * It displays a empty data state overlay with customizable message. * * In order to enable the empty data state mechanism, {@link Options#emptyDataState} option must be set to `true`. * * The plugin provides several configuration options to customize the empty data state behavior and appearance: * - `message`: Message to display in the empty data state overlay. * - `title`: Title to display in the empty data state overlay. * - `description`: Description to display in the empty data state overlay. * - `buttons`: Buttons to display in the empty data state overlay. * - `text`: Text to display in the button. * - `type`: Type of the button. * - `callback`: Callback function to call when the button is clicked. * * @example * ::: only-for javascript * ```javascript * // Enable empty data state plugin with default messages * emptyDataState: true, * * // Enable empty data state plugin with custom message * emptyDataState: { * message: 'No data available', * }, * * // Enable empty data state plugin with custom message and buttons for any source * emptyDataState: { * message: { * title: 'No data available', * description: 'There’s nothing to display yet.', * buttons: [{ text: 'Reset filters', type: 'secondary', callback: () => {} }], * }, * }, * * // Enable empty data state plugin with custom message and buttons for specific source * emptyDataState: { * message: (source) => { * switch (source) { * case "filters": * return { * title: 'No data available', * description: 'There’s nothing to display yet.', * buttons: [{ text: 'Reset filters', type: 'secondary', callback: () => {} }], * }; * default: * return { * title: 'No data available', * description: 'There’s nothing to display yet.', * }; * } * }, * }, * ``` * ::: * * ::: only-for react * ```jsx * // Enable empty data state plugin with default messages * <HotTable emptyDataState={true} />; * * // Enable empty data state plugin with custom message * <HotTable emptyDataState={{ message: 'No data available' }} />; * * // Enable empty data state plugin with custom message and buttons for any source * <HotTable emptyDataState={{ * message: { * title: 'No data available', * description: 'There’s nothing to display yet.', * buttons: [{ text: 'Reset filters', type: 'secondary', callback: () => {} }], * } * }} />; * * // Enable empty data state plugin with custom message and buttons for specific source * <HotTable emptyDataState={{ * message: (source) => { * switch (source) { * case "filters": * return { * title: 'No data available', * description: 'There’s nothing to display yet.', * buttons: [{ text: 'Reset filters', type: 'secondary', callback: () => {} }], * }; * default: * return { * title: 'No data available', * description: 'There’s nothing to display yet.', * }; * } * } * }} />; * ``` * ::: * * ::: only-for angular * ```ts * // Enable empty data state plugin with default messages * hotSettings: Handsontable.GridSettings = { * emptyDataState: true * } * * // Enable empty data state plugin with custom message * hotSettings: Handsontable.GridSettings = { * emptyDataState: { * message: 'No data available' * } * } * * // Enable empty data state plugin with custom message and buttons for any source * hotSettings: Handsontable.GridSettings = { * emptyDataState: { * message: { * title: 'No data available', * description: 'There’s nothing to display yet.', * buttons: [{ text: 'Reset filters', type: 'secondary', callback: () => {} }], * }, * }, * }, * * // Enable empty data state plugin with custom message and buttons for specific source * hotSettings: Handsontable.GridSettings = { * emptyDataState: { * message: (source) => { * switch (source) { * case "filters": * return { * title: 'No data available for filters', * description: 'There’s nothing to display yet.', * buttons: [{ text: 'Reset filters', type: 'secondary', callback: () => {} }], * }; * default: * return { * title: 'No data available', * description: 'There’s nothing to display yet.', * }; * } * } * } * } * ``` * * ```html * <hot-table [settings]="hotSettings"></hot-table> * ``` * ::: */ var _isVisible = /*#__PURE__*/new WeakMap(); var _ui = /*#__PURE__*/new WeakMap(); var _observer = /*#__PURE__*/new WeakMap(); var _hasFilterConditions = /*#__PURE__*/new WeakMap(); var _selectionState = /*#__PURE__*/new WeakMap(); var _EmptyDataState_brand = /*#__PURE__*/new WeakSet(); export class EmptyDataState extends BasePlugin { constructor() { super(...arguments); /** * Registers the DOM listeners. */ _classPrivateMethodInitSpec(this, _EmptyDataState_brand); /** * Flag indicating if emptyDataState is currently visible. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _isVisible, false); /** * UI instance of the emptyDataState plugin. * * @type {EmptyDataStateUI} */ _classPrivateFieldInitSpec(this, _ui, null); /** * MutationObserver instance for monitoring DOM changes. * * @type {MutationObserver} */ _classPrivateFieldInitSpec(this, _observer, null); /** * Flag indicating if there are filter conditions. * * @type {number} */ _classPrivateFieldInitSpec(this, _hasFilterConditions, false); /** * Keeps the selection state that will be restored after the overlay 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 { message: undefined }; } static get SETTINGS_VALIDATORS() { return { message: value => typeof value === 'string' || typeof value === 'function' || isObject(value) && (typeof (value === null || value === void 0 ? void 0 : value.title) === 'undefined' || typeof (value === null || value === void 0 ? void 0 : 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 === null || value === void 0 ? void 0 : value.buttons.every(item => typeof item === 'object' && typeof item.text === 'string' && typeof item.type === 'string' && ['primary', 'secondary'].includes(item.type) && typeof item.callback === 'function'))) || value === undefined }; } /** * 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 EmptyDataStateUI({ rootElement: this.hot.rootGridElement, rootDocument: this.hot.rootDocument })); _assertClassBrand(_EmptyDataState_brand, this, _registerFocusScope).call(this); _assertClassBrand(_EmptyDataState_brand, this, _registerEvents).call(this); _assertClassBrand(_EmptyDataState_brand, this, _registerObservers).call(this); } this.addHook('afterInit', () => _assertClassBrand(_EmptyDataState_brand, this, _onAfterInit).call(this)); this.addHook('afterRender', () => _assertClassBrand(_EmptyDataState_brand, this, _onAfterRender).call(this)); this.addHook('afterRowSequenceCacheUpdate', () => _assertClassBrand(_EmptyDataState_brand, this, _toggleEmptyDataState).call(this)); this.addHook('afterColumnSequenceCacheUpdate', () => _assertClassBrand(_EmptyDataState_brand, this, _toggleEmptyDataState).call(this)); this.addHook('beforeFilter', conditions => _assertClassBrand(_EmptyDataState_brand, this, _onBeforeFilter).call(this, conditions)); super.enablePlugin(); } /** * Update plugin state after Handsontable settings update. */ updatePlugin() { this.disablePlugin(); this.enablePlugin(); _assertClassBrand(_EmptyDataState_brand, this, _update).call(this); if (this.isVisible()) { _classPrivateFieldGet(_ui, this).show(); } super.updatePlugin(); } /** * Disable plugin for this Handsontable instance. */ disablePlugin() { _assertClassBrand(_EmptyDataState_brand, this, _unregisterFocusScope).call(this); _assertClassBrand(_EmptyDataState_brand, this, _disconnectObservers).call(this); _classPrivateFieldGet(_ui, this).destroy(); _classPrivateFieldSet(_ui, this, null); super.disablePlugin(); } /** * Check if the plugin is currently visible. * * @returns {boolean} */ isVisible() { return _classPrivateFieldGet(_isVisible, this); } /** * Destroy plugin instance. */ destroy() { var _classPrivateFieldGet2; _classPrivateFieldSet(_isVisible, this, false); (_classPrivateFieldGet2 = _classPrivateFieldGet(_ui, this)) === null || _classPrivateFieldGet2 === void 0 || _classPrivateFieldGet2.destroy(); _classPrivateFieldSet(_ui, this, null); _classPrivateFieldSet(_observer, this, null); _classPrivateFieldSet(_hasFilterConditions, this, false); _classPrivateFieldSet(_selectionState, this, null); super.destroy(); } } function _registerEvents() { this.eventManager.addEventListener(_classPrivateFieldGet(_ui, this).getElement(), 'wheel', event => _assertClassBrand(_EmptyDataState_brand, this, _onMouseWheel).call(this, event)); } /** * Registers the mutation observers for the emptyDataState plugin. */ function _registerObservers() { // Observe the root element for changes and move the emptyDataState element to the correct position _classPrivateFieldSet(_observer, this, new MutationObserver(() => { if (!this.hot) { return; } const element = _classPrivateFieldGet(_ui, this).getElement(); if (this.hot.rootGridElement.nextElementSibling !== element) { this.hot.rootGridElement.after(element); } })); _classPrivateFieldGet(_observer, this).observe(this.hot.rootWrapperElement, { childList: true }); } /** * Disconnects the mutation observers for the emptyDataState plugin. */ function _disconnectObservers() { _classPrivateFieldGet(_observer, this).disconnect(); _classPrivateFieldSet(_observer, this, null); } /** * Registers the focus scope for the emptyDataState plugin. */ function _registerFocusScope() { this.hot.getFocusScopeManager().registerScope(PLUGIN_KEY, _classPrivateFieldGet(_ui, this).getElement(), { shortcutsContextName: SHORTCUTS_CONTEXT_NAME, runOnlyIf: () => this.isVisible(), onActivate: focusSource => { var _classPrivateFieldGet3; const focusableElements = (_classPrivateFieldGet3 = _classPrivateFieldGet(_ui, this)) === null || _classPrivateFieldGet3 === void 0 ? void 0 : _classPrivateFieldGet3.getFocusableElements(); if (focusableElements.length > 0) { if (focusSource === 'tab_from_above') { focusableElements.at(0).focus(); } else if (focusSource === 'tab_from_below') { focusableElements.at(-1).focus(); } } } }); } /** * Unregisters the focus scope for the emptyDataState plugin. */ function _unregisterFocusScope() { this.hot.getFocusScopeManager().unregisterScope(PLUGIN_KEY); } /** * Get the message by the source for the emptyDataState. * * @param {string} source - The source. * @returns {object} The message. */ function _getMessage(source) { var _message, _message2, _message3; let message; if (typeof this.getSetting('message') === 'function') { message = this.getSetting('message')(source); } else { message = this.getSetting('message'); } // If the message is a string, set the title if (typeof message === 'string') { message = { title: message }; } // If the message is not set, set the default message object if (!((_message = message) !== null && _message !== void 0 && _message.title) && !((_message2 = message) !== null && _message2 !== void 0 && _message2.description) && !((_message3 = message) !== null && _message3 !== void 0 && _message3.buttons)) { message = {}; if (source === SOURCE.FILTERS) { message.title = this.hot.getTranslatedPhrase(C.EMPTY_DATA_STATE_TITLE_FILTERS); message.description = this.hot.getTranslatedPhrase(C.EMPTY_DATA_STATE_DESCRIPTION_FILTERS); message.buttons = [{ text: this.hot.getTranslatedPhrase(C.EMPTY_DATA_STATE_BUTTONS_FILTERS_RESET), type: 'secondary', callback: () => { const filtersPlugin = this.hot.getPlugin('filters'); if (filtersPlugin) { filtersPlugin.clearConditions(); filtersPlugin.filter(); } } }]; } else { message.title = this.hot.getTranslatedPhrase(C.EMPTY_DATA_STATE_TITLE); message.description = this.hot.getTranslatedPhrase(C.EMPTY_DATA_STATE_DESCRIPTION); } } return message; } /** * Toggle visibility and content of the emptyDataState. * * Shows emptyDataState when table has no data or when all data is hidden by filters. * */ function _toggleEmptyDataState() { if (!this.hot.view) { return; } if (this.hot.view.countRenderableColumns() === 0 || this.hot.view.countRenderableRows() === 0) { _assertClassBrand(_EmptyDataState_brand, this, _show).call(this); } else { _assertClassBrand(_EmptyDataState_brand, this, _hide).call(this); } } /** * Shows the emptyDataState overlay. */ function _show() { if (_classPrivateFieldGet(_isVisible, this)) { return; } this.hot.runHooks('beforeEmptyDataStateShow'); _assertClassBrand(_EmptyDataState_brand, this, _update).call(this); _classPrivateFieldGet(_ui, this).show(); _classPrivateFieldSet(_isVisible, this, true); _classPrivateFieldSet(_selectionState, this, this.hot.selection.exportSelection()); this.hot.getFocusScopeManager().activateScope(PLUGIN_KEY); this.hot.runHooks('afterEmptyDataStateShow'); } /** * Updates the content of the emptyDataState overlay. */ function _update() { if (_classPrivateFieldGet(_hasFilterConditions, this)) { _classPrivateFieldGet(_ui, this).updateContent(_assertClassBrand(_EmptyDataState_brand, this, _getMessage).call(this, SOURCE.FILTERS)); } else { _classPrivateFieldGet(_ui, this).updateContent(_assertClassBrand(_EmptyDataState_brand, this, _getMessage).call(this, SOURCE.UNKNOWN)); } } /** * Hides the emptyDataState overlay. */ function _hide() { var _classPrivateFieldGet4; if (!_classPrivateFieldGet(_isVisible, this)) { return; } this.hot.runHooks('beforeEmptyDataStateHide'); _classPrivateFieldGet(_ui, this).hide(); _classPrivateFieldSet(_isVisible, this, false); this.hot.getFocusScopeManager().deactivateScope(PLUGIN_KEY); if (((_classPrivateFieldGet4 = _classPrivateFieldGet(_selectionState, this)) === null || _classPrivateFieldGet4 === void 0 ? void 0 : _classPrivateFieldGet4.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('afterEmptyDataStateHide'); } /** * Handles the mouse wheel event. * * @param {WheelEvent} event - The wheel event. */ function _onMouseWheel(event) { const deltaX = Number.isNaN(event.deltaX) ? -1 * event.wheelDeltaX : event.deltaX; if (deltaX !== 0 && this.hot.view.hasHorizontalScroll() && !this.hot.view.isHorizontallyScrollableByWindow()) { this.hot.view.setTableScrollPosition({ left: this.hot.view.getTableScrollPosition().left + deltaX }); event.preventDefault(); } } /** * Called after the initialization of the table is completed. * It toggles the emptyDataState. */ function _onAfterInit() { _assertClassBrand(_EmptyDataState_brand, this, _toggleEmptyDataState).call(this); this.hot.render(); } /** * Called after the rendering of the table is completed. * It updates the height and class names of the emptyDataState element. */ function _onAfterRender() { var _classPrivateFieldGet5; if ((_classPrivateFieldGet5 = _classPrivateFieldGet(_ui, this)) !== null && _classPrivateFieldGet5 !== void 0 && _classPrivateFieldGet5.getElement() && this.isVisible()) { _classPrivateFieldGet(_ui, this).updateSize(this.hot.view); _classPrivateFieldGet(_ui, this).updateClassNames(this.hot.view); } } /** * Called before the filtering of the table is completed. * It updates the flag indicating if there are filter conditions. * * @param {Array} conditions - The filter conditions. */ function _onBeforeFilter(conditions) { _classPrivateFieldSet(_hasFilterConditions, this, (conditions === null || conditions === void 0 ? void 0 : conditions.length) > 0); if (this.isVisible()) { _assertClassBrand(_EmptyDataState_brand, this, _update).call(this); } }