handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
521 lines (514 loc) • 19.3 kB
JavaScript
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);
}
}