UNPKG

@highcharts/dashboards

Version:
364 lines (362 loc) 13.3 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: * - Paweł Lysy * - Sebastian Bochan * * */ 'use strict'; import EditRenderer from './EditRenderer.js'; import EditGlobals from './EditGlobals.js'; import ConfirmationPopup from './ConfirmationPopup.js'; import { createElement, fireEvent, merge } from '../../Shared/Utilities.js'; import { error } from '../../Core/Utilities.js'; /* * * * Class * * */ /** * Accordion menu class. */ class AccordionMenu { /* * * * Constructor * * */ constructor(iconsURLPrefix, closeSidebar) { this.changedOptions = {}; this.chartOptionsJSON = {}; this.oldOptionsBuffer = {}; this.waitingForConfirmation = false; this.iconsURLPrefix = iconsURLPrefix; this.closeSidebar = closeSidebar; } /* * * * Functions * * */ /** * Renders the menu for given component. * * @param container * The HTML Element to render the menu in. * * @param component * The component to render the menu for. * @param sidebarMainContainer * The main container of the sidebar. */ renderContent(container, component, sidebarMainContainer) { const { editMode } = component.board; const menu = this; const editableOptions = component.editableOptions.getOptions(); let options; let content; this.component = component; this.oldOptionsBuffer = component.getEditableOptions(); if (editMode) { this.confirmationPopup = new ConfirmationPopup(component.board.container, editMode.iconsURLPrefix, editMode, { close: { icon: '' } }); } const accordionContainer = createElement('div', { className: EditGlobals.classNames.accordionMenu }, {}, container); for (let i = 0, end = editableOptions.length; i < end; i++) { options = editableOptions[i]; content = EditRenderer.renderCollapseHeader(accordionContainer, merge({ iconsURLPrefix: menu.iconsURLPrefix, lang: (component.board?.editMode || EditGlobals).lang }, options)).content; this.renderAccordion(options, content, component); } const buttonContainer = createElement('div', { className: EditGlobals.classNames.accordionMenuButtonsContainer }, {}, sidebarMainContainer); EditRenderer.renderButton(buttonContainer, { text: (component.board?.editMode || EditGlobals) .lang.confirmButton, className: EditGlobals.classNames.popupConfirmBtn, callback: async () => { await this.confirmChanges(); fireEvent(editMode, 'confirmEditing'); } }); EditRenderer.renderButton(buttonContainer, { text: (component.board?.editMode || EditGlobals) .lang.cancelButton, className: EditGlobals.classNames.popupCancelBtn, callback: () => { this.cancelChanges(); fireEvent(editMode, 'cancelEditing'); } }); sidebarMainContainer.appendChild(buttonContainer); } /** * Update the options object with new nested value, based on the property * path. If the objects in the path are not defined, the function will * create them. * * @param propertyPath * Path of the property for which the value should be updated. * Example: ```['chartOptions', 'chart', 'type']``` * * @param value * New value of the property. */ updateOptions(propertyPath, value) { const pathLength = propertyPath.length - 1; let currentLevel = this.changedOptions; let currentChartOptionsLevel; let currentOldChartOptionsBufferLevel; let currentGridOptionsLevel; let currentOldGridOptionsBufferLevel; if (pathLength === 0 && propertyPath[0] === 'chartOptions') { try { const parsedValue = JSON.parse(value); this.chartOptionsJSON = parsedValue; } catch (e) { // TODO: Handle the wrong config passed from the user. error('Dashboards Error: Wrong JSON config structure passed ' + 'as chart options. \n____________\n' + String(e)); } } for (let i = 0; i < pathLength; i++) { const key = propertyPath[i]; if (!currentLevel[key]) { currentLevel[key] = {}; } currentLevel = currentLevel[key]; if (key === 'gridOptions') { const realGridOptions = this.component.grid?.options; if (realGridOptions) { const oldOptionsBuffer = this.oldOptionsBuffer; if (!oldOptionsBuffer.gridOptions) { oldOptionsBuffer.gridOptions = {}; } currentOldGridOptionsBufferLevel = oldOptionsBuffer.gridOptions; currentGridOptionsLevel = realGridOptions; } } else if (currentGridOptionsLevel && currentOldGridOptionsBufferLevel) { currentGridOptionsLevel = currentGridOptionsLevel[key]; if (currentOldGridOptionsBufferLevel[key] === void 0) { currentOldGridOptionsBufferLevel[key] = {}; } currentOldGridOptionsBufferLevel = currentOldGridOptionsBufferLevel[key]; } if (key === 'chartOptions') { const realChartOptions = this.component.chart?.options; if (realChartOptions) { const oldOptionsBuffer = this.oldOptionsBuffer; if (!oldOptionsBuffer.chartOptions) { oldOptionsBuffer.chartOptions = {}; } currentOldChartOptionsBufferLevel = oldOptionsBuffer.chartOptions; currentChartOptionsLevel = realChartOptions; } } else if (currentChartOptionsLevel && currentOldChartOptionsBufferLevel) { currentChartOptionsLevel = currentChartOptionsLevel[key]; if (currentOldChartOptionsBufferLevel[key] === void 0) { currentOldChartOptionsBufferLevel[key] = {}; } currentOldChartOptionsBufferLevel = currentOldChartOptionsBufferLevel[key]; } } const lastKey = propertyPath[pathLength]; currentLevel[lastKey] = value; if (currentOldChartOptionsBufferLevel && currentChartOptionsLevel) { currentOldChartOptionsBufferLevel[lastKey] = (currentOldChartOptionsBufferLevel[lastKey] ?? currentChartOptionsLevel[lastKey]); } // eslint-disable-next-line @typescript-eslint/no-floating-promises this.component?.update(this.changedOptions); } /** * Renders either a basic or nested element. This function can be * recursively called, if there are multiple nested options. * * @param options * Configuration object of the Component options. * * @param parentNode * A container where the accordion is rendered. * * @param component * the component for which the menu should be rendered. */ renderAccordion(options, parentNode, component) { if (options.type === 'nested') { return this.renderNested(parentNode, options, component); } const renderFunction = EditRenderer.getRendererFunction(options.type); const lang = (component.board?.editMode || EditGlobals).lang; if (!renderFunction) { return; } renderFunction(parentNode, { ...options, iconsURLPrefix: this.iconsURLPrefix, value: component.getEditableOptionValue(options.propertyPath), enabledOnOffLabels: options.type === 'toggle', lang, onchange: (value) => this.updateOptions(options.propertyPath || [], value) }); } /** * Render nested menu for the component. * * @param parentElement * HTML element to which the nested structure should be rendered to * * @param options * configuration object for the options * * @param component * The component instance for the options should be rendered */ renderNested(parentElement, options, component) { if (!parentElement || !options.nestedOptions) { return; } const nestedOptions = options.nestedOptions; for (let i = 0, iEnd = nestedOptions.length; i < iEnd; ++i) { const name = nestedOptions[i].name; const accordionOptions = nestedOptions[i].options; const showToggle = !!nestedOptions[i].showToggle; const propertyPath = nestedOptions[i].propertyPath || []; const lang = (component.board?.editMode || EditGlobals).lang; const collapsedHeader = EditRenderer.renderCollapseHeader(parentElement, { name, isEnabled: !!component.getEditableOptionValue(propertyPath), iconsURLPrefix: this.iconsURLPrefix, showToggle: showToggle, onchange: (value) => this.updateOptions(propertyPath, value), isNested: !!accordionOptions, isStandalone: !accordionOptions, lang }); for (let j = 0, jEnd = accordionOptions?.length; j < jEnd; ++j) { this.renderAccordion(merge(accordionOptions[j], { lang, isNested: true }), collapsedHeader.content, component); } } return; } /** * Closes the sidebar discarding changes. If there are any changes, it will * show a confirmation popup. If no changes, it will close the sidebar. */ cancelChanges() { if (Object.keys(this.changedOptions).length < 1) { this.closeSidebar(); } else { this.showCancelConfirmationPopup(); } } /** * Confirms changes made in the component. * * @fires EditMode#componentChanged */ async confirmChanges() { const component = this.component; if (!component) { return; } if (component.type === 'Highcharts' && Object.keys(this.chartOptionsJSON).length) { await component.update({ chartOptions: this.chartOptionsJSON }); } else if (component.type === 'HTML') { const options = this.changedOptions; await component.update(options, true); } fireEvent(component.board.editMode, 'componentChanged', { target: component, changedOptions: merge({}, this.changedOptions), oldOptions: merge({}, this.oldOptionsBuffer) }); this.changedOptions = {}; this.chartOptionsJSON = {}; this.closeSidebar(); } /** * Discards changes made in the component. * * @fires EditMode#componentChangesDiscarded */ async discardChanges() { const component = this.component; if (!component) { return; } await component.update(this.oldOptionsBuffer); fireEvent(component.board.editMode, 'componentChangesDiscarded', { target: component, changedOptions: merge({}, this.changedOptions), oldOptions: merge({}, this.oldOptionsBuffer) }); this.changedOptions = {}; this.chartOptionsJSON = {}; } /** * Shows a confirmation popup when the user tries to discard changes. */ showCancelConfirmationPopup() { const popup = this.confirmationPopup; const editMode = this.component?.board?.editMode; if (!popup || !editMode || this.waitingForConfirmation) { return; } this.waitingForConfirmation = true; popup.show({ text: editMode.lang.confirmDiscardChanges, confirmButton: { value: editMode.lang.confirmButton, callback: async () => { await this.discardChanges(); this.waitingForConfirmation = false; this.closeSidebar(); }, context: this }, cancelButton: { value: editMode.lang.cancelButton, callback: () => { popup.closePopup(); editMode.setEditOverlay(); setTimeout(() => { this.waitingForConfirmation = false; }, 100); } } }); } } /* * * * Default Export * * */ export default AccordionMenu;