@highcharts/dashboards
Version:
Highcharts Dashboards framework
370 lines (369 loc) • 11.6 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:
* - Sebastian Bochan
* - Wojciech Chmiel
* - Gøran Slettemark
* - Sophie Bremer
* - Paweł Lysy
* - Karol Kołodziej
*
* */
'use strict';
import Bindings from './Actions/Bindings.js';
import ComponentRegistry from './Components/ComponentRegistry.js';
import DashboardsAccessibility from './Accessibility/DashboardsAccessibility.js';
import DataCursor from '../Data/DataCursor.js';
import DataPool from '../Data/DataPool.js';
import Defaults from './Defaults.js';
import Globals from './Globals.js';
import Layout from './Layout/Layout.js';
import HTMLComponent from './Components/HTMLComponent/HTMLComponent.js';
import { addEvent, createElement, merge, objectEach } from '../Shared/Utilities.js';
import { error, uniqueKey } from '../Core/Utilities.js';
/* *
*
* Class
*
* */
/**
* Class that represents a dashboard.
*
* @example
* const dashboard = Dashboards.board('container', {
* gui: {
* layouts: [{
* id: 'layout-1',
* rows: [{
* cells: [{
* id: 'dashboard-col-0'
* }]
* }]
* }]
* },
* components: [{
* renderTo: 'dashboard-col-0',
* type: 'Highcharts',
* chartOptions: {
* series: [{
* data: [1, 2, 3, 4]
* }]
* }
* }]
* });
*/
class Board {
// Implementation:
static board(renderTo, options, async) {
return new Board(renderTo, options).init(async);
}
/* *
*
* Constructor
*
* */
/**
* Creates a dashboard with components like charts, tables, and HTML
* elements.
*
* @internal
* @param renderTo
* The DOM element to render to, or its id.
*
* @param options
* The options for the dashboard.
*/
constructor(renderTo, options) {
/**
* All types of components available in the dashboard.
* @internal
*/
this.componentTypes = ComponentRegistry.types;
this.options = merge(Defaults.defaultOptions, options);
this.dataPool = new DataPool(options.dataPool);
this.id = uniqueKey();
this.guiEnabled = !options.gui ?
false : this.options?.gui?.enabled;
this.editModeEnabled = !options.editMode ?
false : this.options?.editMode?.enabled;
this.layouts = [];
this.mountedComponents = [];
this.initContainer(renderTo);
this.initEditMode();
// Add table cursors support.
this.dataCursor = new DataCursor();
this.index = Globals.boards.length;
Globals.boards.push(this);
// A11y module
this.a11y = new DashboardsAccessibility(this);
}
// Implementation:
init(async) {
const options = this.options;
const componentPromises = (options.components) ?
this.setComponents(options.components) : [];
// Init events.
this.initEvents();
if (async) {
return Promise.all(componentPromises).then(() => {
options.events?.mounted?.call(this);
return this;
});
}
options.events?.mounted?.call(this);
return this;
}
/**
* Initializes the events.
* @internal
*/
initEvents() {
const board = this, runReflow = () => {
board.reflow();
};
if (typeof ResizeObserver === 'function') {
this.resizeObserver = new ResizeObserver(runReflow);
this.resizeObserver.observe(board.container);
}
else {
const unbind = addEvent(window, 'resize', runReflow);
addEvent(this, 'destroy', unbind);
}
}
/**
* Initialize the container for the dashboard.
* @internal
*
* @param renderTo
* The DOM element to render to, or its id.
*/
initContainer(renderTo) {
const board = this;
if (typeof renderTo === 'string') {
renderTo = window.document.getElementById(renderTo);
}
// Display an error if the renderTo doesn't exist.
if (!renderTo) {
error(13, true);
}
board.container = renderTo;
}
/**
* Inits creating a layouts and setup the EditMode tools.
* @internal
*/
initEditMode() {
const { EditMode } = Globals.win.Dashboards;
if (EditMode) {
this.editMode = new EditMode(this, this.options.editMode);
}
else if (this.editModeEnabled) {
throw new Error('Missing layout.js module');
}
}
/**
* Set the components from options.
* @internal
*
* @param components
* An array of component options.
*
*/
setComponents(components) {
const promises = [];
const board = this;
for (let i = 0, iEnd = components.length; i < iEnd; ++i) {
promises.push(Bindings.addComponent(components[i], board));
}
return promises;
}
/**
* Destroy the whole dashboard, its layouts and elements.
*/
destroy() {
const board = this;
const index = this.index;
// Cancel all data connectors pending requests.
this.dataPool.cancelPendingRequests();
// Destroy layouts. Iterate over a copy, since each layout removes
// itself from `board.layouts` on destroy (#24857).
if (this.guiEnabled) {
for (const layout of (board.layouts || []).slice()) {
layout.destroy();
}
}
else {
for (const mountedComponent of board.mountedComponents) {
mountedComponent.component.destroy();
}
}
// Remove resizeObserver from the board
this.resizeObserver?.unobserve(board.container);
// Destroy container.
if (this.guiEnabled) {
board.container?.remove();
}
// @ToDo Destroy bindings.
// Delete all properties.
objectEach(board, function (val, key) {
delete board[key];
});
Globals.boards[index] = void 0;
return;
}
/**
* Reflow the dashboard. Hide the toolbars and context pointer. Reflow the
* layouts and its cells.
*/
reflow() {
const board = this;
if (board.editMode) {
const editModeTools = board.editMode.tools;
board.editMode.hideToolbars(['cell', 'row']);
board.editMode.hideContextPointer();
// Update expanded context menu container
if (editModeTools.contextMenu) {
editModeTools.contextMenu
.updatePosition(editModeTools.contextButtonElement);
}
}
}
/**
* Update the dashboard with new options.
*
* @param newOptions
* The new options to apply to the dashboard.
*/
update(newOptions) {
const board = this;
// Merge new options with existing ones
board.options = merge(board.options, newOptions);
// Update dataPool if dataPool options changed
if (newOptions.dataPool) {
board.dataPool = new DataPool(board.options.dataPool);
}
// Update guiEnabled and editModeEnabled flags if changed
if (newOptions.gui !== void 0) {
board.guiEnabled = !newOptions.gui ?
false : board.options?.gui?.enabled;
}
if (newOptions.editMode !== void 0) {
board.editModeEnabled = !newOptions.editMode ?
false : board.options?.editMode?.enabled;
}
// Destroy existing components
for (const mountedComponent of board.mountedComponents) {
mountedComponent.component.destroy();
}
board.mountedComponents = [];
// Destroy existing layouts if GUI is enabled
if (board.guiEnabled && board.layouts) {
// Iterate over a copy, since each layout removes itself from
// `board.layouts` on destroy (#24857).
for (const layout of board.layouts.slice()) {
layout.destroy();
}
board.layouts = [];
// Ensure layoutsWrapper exists
if (!board.layoutsWrapper && board.container) {
board.layoutsWrapper = createElement('div', {
className: Globals.classNames.layoutsWrapper
}, {}, board.container);
}
// Create new layouts if they are provided
if (board.options.gui?.layouts) {
const guiOptions = board.options.gui;
for (let i = 0, iEnd = guiOptions.layouts.length; i < iEnd; ++i) {
board.layouts.push(new Layout(board, merge({}, guiOptions.layoutOptions, guiOptions.layouts[i])));
}
// Re-initialize editMode events if editMode exists
if (board.editMode) {
// Re-initialize events for all layouts
let j = 0;
const jEnd = board.layouts.length;
for (j; j < jEnd; ++j) {
board.editMode.setLayoutEvents(board.layouts[j]);
}
}
}
}
// Add new components
if (board.options.components) {
void board.setComponents(board.options.components);
}
}
/**
* Convert the current state of board's options into JSON. The function does
* not support converting functions or events into JSON object.
*
* @returns
* Dashboards options.
*/
getOptions() {
const board = this, options = {
...this.options,
components: []
};
for (let i = 0, iEnd = board.mountedComponents.length; i < iEnd; ++i) {
if (board.mountedComponents[i].cell &&
board.mountedComponents[i].cell.mountedComponent) {
options.components?.push(board.mountedComponents[i].component.getOptions());
}
}
if (this.guiEnabled) {
options.gui = {
layouts: []
};
for (let i = 0, iEnd = board.layouts.length; i < iEnd; ++i) {
options.gui.layouts?.push(board.layouts[i].getOptions());
}
}
else {
delete options.gui;
}
return options;
}
/**
* Get a Dashboards component by its identifier.
*
* @param id
* The identifier of the requested component.
*
* @returns
* The component with the given identifier.
*/
getComponentById(id) {
return this.mountedComponents.find((c) => c.component.id === id)?.component;
}
/**
* Get a Dashboards component by its cell identifier.
*
* @param id
* The identifier of the cell that contains the requested component.
*
* @returns
* The component with the given cell identifier.
*/
getComponentByCellId(id) {
return this.mountedComponents.find((c) => c.cell.id === id)?.component;
}
}
/* *
*
* Registry
*
* */
ComponentRegistry.registerComponent('HTML', HTMLComponent);
/* *
*
* Default Export
*
* */
export default Board;