@highcharts/dashboards
Version:
Highcharts Dashboards framework
384 lines (383 loc) • 12.5 kB
JavaScript
/* *
*
* (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;