UNPKG

@highcharts/dashboards

Version:
384 lines (383 loc) 12.5 kB
/* * * * (c) 2009-2026 Highsoft AS * * Integration of this software requires a license. * - For commercial use, see www.highcharts.com/license * - For non-commercial, see www.highcharts.com/license-eula * * * Authors: * - Karol Kołodziej * - Dawid Draguła * * */ 'use strict'; import Component from '../Component.js'; import GridSyncs from './GridSyncs/GridSyncs.js'; import GridComponentDefaults from './GridComponentDefaults.js'; import { hasDataTableProvider } from './GridDataProvider.js'; import DU from '../../Utilities.js'; import { diffObjects, getStyle, merge } from '../../../Shared/Utilities.js'; const { deepClone } = DU; /* * * * Class * * */ /** * Grid Component for Highcharts Dashboards. * @private */ class GridComponent extends Component { /* * * * Constructor * * */ constructor(cell, options, board) { options = merge(GridComponent.defaultOptions, options); super(cell, options, board); this.options = options; this.type = 'Grid'; this.setOptions(); } /* * * * Functions * * */ async update(options, shouldRerender = true) { const previousGridDataTableId = this.getGridDataTable(true)?.id; // Avoid triggering GridComponent.render() from Component.update(). // That render starts a fire-and-forget renderViewport() which can // race with the awaited redraw() below when connector data changes. await super.update(options, false); this.setOptions(); const grid = this.grid; const table = this.getDataTable(); if (grid && this.options.connector && previousGridDataTableId !== table?.id) { this.recreateGrid(shouldRerender); this.emit({ type: 'afterUpdate' }); return; } if (grid && shouldRerender) { super.render(); } if (grid) { void grid.update(options.gridOptions, false); if ( // #24067 - Update dataTable in options when changed. options.gridOptions?.dataTable && this.options.gridOptions) { this.options.gridOptions.dataTable = options.gridOptions.dataTable; } await grid.redraw(); this.options.gridOptions = this.getGridOptionsSnapshot(grid); if (shouldRerender) { this.finalizeGridRender(); } } else if (shouldRerender) { this.render(); } this.emit({ type: 'afterUpdate' }); } render() { super.render(); if (!this.grid) { this.grid = this.constructGrid(); } else { void this.grid.renderViewport(); } this.finalizeGridRender(); return this; } resize(width, height) { if (height) { this.contentElement.style.minHeight = '0'; } else if (height === null) { this.contentElement.style.removeProperty('min-height'); } this.resizeDynamicContent(width, height); this.grid?.viewport?.reflow(); } onTableChanged() { const { grid } = this; if (!grid) { return; } // Check if the grid is of the legacy version (not using the data // provider). if (!('dataProvider' in grid)) { // eslint-disable-next-line no-console console.warn('GridComponent: Legacy Grid detected. Using legacy handler ' + 'for table changes. Consider upgrading the Highcharts Grid ' + 'Library to the latest version.'); this.onTableChangedLegacy(); return; } if (!grid?.dataProvider || !hasDataTableProvider(grid.dataProvider) || !this.connectorHandlers?.length) { return; } const dataTable = this.getDataTable()?.getModified(); if (!dataTable) { this.recreateGrid(true); return; } if (!grid.options?.header) { // If the header is not defined, we need to check if the column // names have changed, so we can update the whole grid. If they // have not changed, we can just update the rows (more efficient). const newColumnIds = dataTable.getColumnIds(); const { enabledColumns, columnPolicy } = grid; let index = 0; for (const newColumn of newColumnIds) { if (columnPolicy.getIndividualColumnOptions(newColumn) ?.enabled === false) { continue; } if (enabledColumns?.[index] !== newColumn) { // If the visible columns have changed, // update the whole grid. this.recreateGrid(true); return; } index++; } } if (this.getGridDataTable() !== dataTable) { this.recreateGrid(true); return; } // Data has changed and the whole grid is not re-rendered, so mark in // the querying that data table was modified. grid.querying.shouldBeUpdated = true; // If the column names have not changed, just update the rows. void grid.viewport?.updateRows(); } /** * Legacy handler for table changes. */ onTableChangedLegacy() { const { grid } = this; if (!grid) { return; } const dataTable = this.getDataTable()?.getModified(); if (!dataTable) { void grid.update({ dataTable: void 0 }); return; } if (!grid.options?.header) { // If the header is not defined, we need to check if the column // names have changed, so we can update the whole grid. If they // have not changed, we can just update the rows (more efficient). const newColumnIds = dataTable.getColumnIds(); const { enabledColumns, columnPolicy } = grid; let index = 0; for (const newColumn of newColumnIds) { if (columnPolicy.getIndividualColumnOptions(newColumn) ?.enabled === false) { continue; } if (enabledColumns?.[index] !== newColumn) { // If the visible columns have changed, // update the whole grid. void grid.update({ dataTable }); return; } index++; } } // Workaround for legacy Grid component. grid.dataTable = dataTable; // Data has changed and the whole grid is not re-rendered, so mark in // the querying that data table was modified. grid.querying.shouldBeUpdated = true; // If the column names have not changed, just update the rows. void grid.viewport?.updateRows(); } getEditableOptions() { const componentOptions = this.options; const gridOptions = this.grid ? this.getGridOptionsSnapshot(this.grid) : void 0; return deepClone(merge({ gridOptions: gridOptions }, componentOptions), ['editableOptions', 'dataTable']); } getOptionsOnDrop(sidebar) { const connectorsIds = sidebar.editMode.board.dataPool.getConnectorIds(); let options = { type: 'Grid' }; if (connectorsIds.length) { options = { ...options, connector: { id: connectorsIds[0] } }; } return options; } /** * Get the Grid Component's options. * * @returns * Grid Component's options. * * @internal */ getOptions() { const optionsCopy = merge(this.options); optionsCopy.gridOptions = this.grid ? this.getGridOptionsSnapshot(this.grid) : void 0; // Remove the table from the options copy if the connector is set. if (optionsCopy.connector?.id) { delete optionsCopy.gridOptions?.dataTable; } else if (optionsCopy.gridOptions?.dataTable?.id) { optionsCopy.gridOptions.dataTable = { columns: optionsCopy.gridOptions.dataTable.columns }; } return { ...diffObjects(optionsCopy, GridComponent.defaultOptions), type: 'Grid' }; } /** * Destroys the data grid component. */ destroy() { this.sync.stop(); this.grid?.destroy(); super.destroy(); } /** * Sets the options for the data grid component content container. */ setOptions() { const options = this.options, gridClassName = options.gridClassName, gridID = options.gridID; if (gridClassName) { this.contentElement.classList.value = GridComponentDefaults.gridClassName + ' ' + gridClassName; } if (gridID) { this.contentElement.id = gridID; } } finalizeGridRender() { const { grid } = this; if (!grid) { return; } grid.initialContainerHeight = getStyle(this.parentElement, 'height', true) || 0; this.sync.start(); this.emit({ type: 'afterRender' }); } getGridOptionsSnapshot(grid) { const gridOptions = merge(grid.getOptions()); if (!this.options.connector) { return gridOptions; } delete gridOptions.dataTable; if (gridOptions.data?.providerType === 'local') { delete gridOptions.data.dataTable; delete gridOptions.data.columns; if (Object.keys(gridOptions.data).length === 1 && gridOptions.data.providerType === 'local') { delete gridOptions.data; } } return gridOptions; } getGridOptionsWithConnectorData() { const gridOptions = merge(this.options.gridOptions) ?? {}; if (!this.options.connector) { return gridOptions; } delete gridOptions.dataTable; if (gridOptions.data?.providerType === 'local') { delete gridOptions.data.dataTable; delete gridOptions.data.columns; if (Object.keys(gridOptions.data).length === 1 && gridOptions.data.providerType === 'local') { delete gridOptions.data; } } const dataTable = this.getDataTable(); if (dataTable) { gridOptions.data = merge(gridOptions.data?.providerType === 'local' ? gridOptions.data : {}, { providerType: 'local', dataTable: dataTable.getModified() }); } return gridOptions; } recreateGrid(shouldRerender) { this.sync.stop(); this.grid?.destroy(); delete this.grid; if (shouldRerender) { this.render(); return; } this.grid = this.constructGrid(); this.finalizeGridRender(); } getGridDataTable(presentation = false) { const dataProvider = this.grid?.dataProvider; return hasDataTableProvider(dataProvider) ? dataProvider.getDataTable(presentation) : void 0; } /** * Function to create the Grid. * * @returns The Grid. */ constructGrid() { const DGN = GridComponent.GridNamespace; if (!DGN) { throw new Error('Grid not connected.'); } const gridOptions = this.getGridOptionsWithConnectorData(); const gridInstance = new DGN.Grid(this.contentElement, gridOptions ?? {}); this.options.gridOptions = this.getGridOptionsSnapshot(gridInstance); return gridInstance; } } /* * * * Static Properties * * */ /** * Predefined sync config for the Grid Component. */ GridComponent.predefinedSyncConfig = GridSyncs; /** * The default options for the Grid Component. */ GridComponent.defaultOptions = merge(Component.defaultOptions, GridComponentDefaults); /* * * * Default Export * * */ export default GridComponent;