UNPKG

@highcharts/dashboards

Version:
711 lines (710 loc) 24.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: * - Sebastian Bochan * - Wojciech Chmiel * - Gøran Slettemark * - Sophie Bremer * * */ import { isCell } from '../Layout/Cell.js'; import { isCellHTML } from '../Layout/CellHTML.js'; import EditGlobals from './EditGlobals.js'; import EditRenderer from './EditRenderer.js'; import CellEditToolbar from './Toolbar/CellEditToolbar.js'; import RowEditToolbar from './Toolbar/RowEditToolbar.js'; import SidebarPopup from './SidebarPopup.js'; import EditContextMenu from './EditContextMenu.js'; import DragDrop from '../Actions/DragDrop.js'; import Resizer from '../Actions/Resizer.js'; import ConfirmationPopup from './ConfirmationPopup.js'; import GUIElement from '../Layout/GUIElement.js'; import Globals from '../Globals.js'; import Layout from '../Layout/Layout.js'; import { addEvent, createElement, css, merge } from '../../Shared/Utilities.js'; /* * * * Class * * */ class EditMode { /* * * * Constructor * * */ /** * Edit mode constructor. * @internal * * @param board * Board instance * * @param options * Edit mode options */ constructor(board, options) { /* * * * Properties * * */ /** * @internal */ this.active = false; /** * Whether the board is generated with custom HTML. */ this.customHTMLMode = false; /** * URL from which the icons will be fetched. */ this.iconsURLPrefix = 'https://code.highcharts.com/dashboards/4.2.1/gfx/dashboards-icons/'; this.iconsURLPrefix = (options && options.iconsURLPrefix) || this.iconsURLPrefix; this.options = merge( // Default options. { confirmationPopup: { close: { icon: this.iconsURLPrefix + 'close.svg' } }, contextMenu: { icon: this.iconsURLPrefix + 'menu.svg' }, dragDrop: { enabled: true }, viewFullscreen: { enabled: true }, enabled: true, resize: { enabled: true }, settings: { enabled: true }, toolbars: { cell: { enabled: true }, row: { enabled: true } }, tools: { addComponentBtn: { enabled: true, icon: this.iconsURLPrefix + 'add.svg' } } }, options || {}); this.board = board; this.lang = merge({}, EditGlobals.lang, this.options.lang); board.boardWrapper = board.container; if (board.guiEnabled) { this.initLayout(); } this.isInitialized = false; this.isContextDetectionActive = false; this.tools = {}; if (board.editModeEnabled) { this.customHTMLMode = !this.board.layoutsWrapper; this.contextPointer = { isVisible: false, element: createElement('div', { className: EditGlobals.classNames.contextDetectionPointer }, {}, board.container) }; this.createTools(); this.confirmationPopup = new ConfirmationPopup(board.container, this.iconsURLPrefix, this, this.options.confirmationPopup); // Create edit overlay. this.editOverlay = createElement('div', { className: EditGlobals.classNames.editOverlay }, {}, board.container); this.isEditOverlayActive = false; board.fullscreen = new Globals.win.Dashboards.Fullscreen(board); if (this.customHTMLMode) { board.container.classList.add(Globals.classNames.boardContainer); } } } /* * * * Functions * * */ /** * Event to fire on click of the context button. * @internal */ onContextBtnClick() { const editMode = this; // Toggle context menu visibility. if (editMode.tools.contextMenu) { if (!editMode.tools.contextMenu.isVisible) { editMode.tools.contextMenu .updatePosition(editMode.tools.contextButtonElement); } editMode.tools.contextMenu.setVisible(!editMode.tools.contextMenu.isVisible); } } /** * Activate or deactivate edit mode. */ toggleEditMode() { const editMode = this; if (editMode.active) { editMode.deactivate(); } else { editMode.activate(); } } /** * Init the instance of edit mode. * @internal */ init() { const editMode = this; if (this.options.resize?.enabled && !editMode.customHTMLMode) { editMode.resizer = new Resizer(editMode, editMode.options.resize); } editMode.dragDrop = new DragDrop(editMode, editMode.options.dragDrop); // Init rowToolbar. if (editMode.options.toolbars?.row?.enabled && !editMode.rowToolbar && !editMode.customHTMLMode) { editMode.rowToolbar = new RowEditToolbar(editMode); } // Init cellToolbar. if (editMode.options.toolbars?.cell?.enabled && !editMode.cellToolbar) { editMode.cellToolbar = new CellEditToolbar(editMode); } // Init Sidebar. if (!editMode.sidebar) { editMode.sidebar = new SidebarPopup(this.board.container, this.iconsURLPrefix, editMode); } editMode.isInitialized = true; } /** * Init events for edit mode. * @internal */ initEvents() { const editMode = this, board = editMode.board; if (this.customHTMLMode) { const length = board.mountedComponents.length; for (let i = 0, iEnd = length; i < iEnd; ++i) { editMode.setCellEvents(board.mountedComponents[i].cell); } } else { for (let i = 0, iEnd = board.layouts.length; i < iEnd; ++i) { editMode.setLayoutEvents(board.layouts[i]); } } addEvent(document, 'keydown', (e) => { if (e.key === 'Escape' && editMode.isActive()) { editMode.hideToolbars(['cell', 'row']); editMode.editCellContext = void 0; editMode.resizer?.disableResizer(); } }); if (editMode.cellToolbar) { // Stop context detection when mouse on cell toolbar. addEvent(editMode.cellToolbar.container, 'mouseenter', function () { editMode.stopContextDetection(); }); addEvent(editMode.cellToolbar.container, 'mouseleave', function () { editMode.isContextDetectionActive = true; }); } if (editMode.rowToolbar) { // Stop context detection when mouse on row toolbar. addEvent(editMode.rowToolbar.container, 'mouseenter', function () { editMode.stopContextDetection(); }); addEvent(editMode.rowToolbar.container, 'mouseleave', function () { editMode.isContextDetectionActive = true; }); } const elementForEvents = this.customHTMLMode ? board.container : board.layoutsWrapper; addEvent(elementForEvents, 'mousemove', editMode.onDetectContext.bind(editMode)); addEvent(elementForEvents, 'click', editMode.onContextConfirm.bind(editMode)); addEvent(elementForEvents, 'mouseleave', () => { editMode.hideContextPointer(); }); } /** * Initialize the container for the layouts. * @internal * */ initLayout() { const board = this.board; // Clear the container from any content. board.container.innerHTML = ''; // Add container for the board. board.container = createElement('div', { className: Globals.classNames.boardContainer }, {}, board.boardWrapper); // Create layouts wrapper. board.layoutsWrapper = createElement('div', { className: Globals.classNames.layoutsWrapper }, {}, board.container); if (board.options.gui) { this.setLayouts(board.options.gui); } } /** * Creates a new layouts and adds it to the dashboard based on the options. * @internal * * @param guiOptions * The GUI options for the layout. * */ setLayouts(guiOptions) { const board = this.board, layoutsOptions = guiOptions.layouts; for (let i = 0, iEnd = layoutsOptions.length; i < iEnd; ++i) { board.layouts.push(new Layout(board, merge({}, guiOptions.layoutOptions, layoutsOptions[i]))); } } /** * Set events for the layout. * @internal */ setLayoutEvents(layout) { const editMode = this; for (let j = 0, jEnd = layout.rows.length; j < jEnd; ++j) { const row = layout.rows[j]; editMode.setRowEvents(row); for (let k = 0, kEnd = row.cells.length; k < kEnd; ++k) { editMode.setCellEvents(row.cells[k]); } } } /** * Set events for the row. * @internal */ setRowEvents(row) { const editMode = this; // Init dragDrop row events. if (editMode.dragDrop) { const dragDrop = editMode.dragDrop; addEvent(row.container, 'mouseenter', function () { if (editMode.isContextDetectionActive) { editMode.mouseRowContext = row; } }); addEvent(row.container, 'mousemove', function (e) { if (dragDrop.isActive && e.target === row.container) { dragDrop.mouseRowContext = row; } }); addEvent(row.container, 'mouseleave', function () { if (dragDrop.isActive && dragDrop.mouseRowContext === row) { dragDrop.mouseRowContext = void 0; } if (editMode.isContextDetectionActive) { editMode.mouseRowContext = void 0; } }); } } /** * Set events for the cell. * @internal */ setCellEvents(cell) { const editMode = this; if (isCellHTML(cell)) { addEvent(cell.container, 'mouseenter', function () { if (editMode.isContextDetectionActive) { editMode.mouseCellContext = cell; } }); } else { if (cell.nestedLayout) { editMode.setLayoutEvents(cell.nestedLayout); } else if (editMode.cellToolbar && cell.container) { addEvent(cell.container, 'mouseenter', function () { if (editMode.isContextDetectionActive) { editMode.mouseCellContext = cell; } }); // Init dragDrop cell events only when using layouts. if ((editMode.dragDrop || editMode.resizer)) { const dragDrop = editMode.dragDrop; addEvent(cell.container, 'mousemove', function (e) { if (dragDrop && dragDrop.isActive && e.target === cell.container) { dragDrop.mouseCellContext = cell; dragDrop.mouseRowContext = void 0; } }); addEvent(cell.container, 'mouseleave', function () { if (dragDrop && dragDrop.isActive && dragDrop.mouseCellContext === cell) { dragDrop.mouseCellContext = void 0; } if (editMode.isContextDetectionActive) { editMode.mouseCellContext = void 0; } }); } } } } /** * Activate the edit mode. * @internal */ activate() { const editMode = this; // Init edit mode. if (!editMode.isInitialized) { editMode.init(); editMode.initEvents(); } // Set edit mode active class to dashboard. editMode.board.container.classList.add(EditGlobals.classNames.editModeEnabled); if (this.addComponentBtn) { this.addComponentBtn.style.display = 'block'; } editMode.active = true; editMode.isContextDetectionActive = true; } /** * Deactivate the edit mode. * @internal */ deactivate() { const editMode = this, dashboardCnt = editMode.board.container; dashboardCnt.classList.remove(EditGlobals.classNames.editModeEnabled); // Hide toolbars. editMode.hideToolbars(); // Remove highlight from the context row if exists. if (this.editCellContext && isCell(this.editCellContext)) { this.editCellContext.row?.setHighlight(); } // TODO all buttons should be deactivated. if (this.addComponentBtn) { this.addComponentBtn.style.display = 'none'; } if (editMode.resizer) { editMode.resizer.disableResizer(); } // Disable responsive width and restore elements to their original // positions and sizes. if (this.board.layoutsWrapper) { this.board.layoutsWrapper.style.width = '100%'; } this.board.reflow(); editMode.active = false; editMode.stopContextDetection(); this.editCellContext = void 0; this.potentialCellContext = void 0; } /** * Function to check whether the edit mode is activated. * @internal * * @returns * Whether the edit mode is activated. */ isActive() { return this.active; } /** * Method for hiding edit toolbars. * @internal * * @param toolbarTypes * The array of toolbar names to hide ('cell', 'row', 'sidebar'). */ hideToolbars(toolbarTypes) { const editMode = this, toolbarsToHide = toolbarTypes || ['cell', 'row', 'sidebar']; for (let i = 0, iEnd = toolbarsToHide.length; i < iEnd; ++i) { switch (toolbarsToHide[i]) { case 'cell': { if (editMode.cellToolbar && editMode.cellToolbar.isVisible) { editMode.cellToolbar.hide(); } break; } case 'row': { if (editMode.rowToolbar && editMode.rowToolbar.isVisible) { editMode.rowToolbar.hide(); } break; } case 'sidebar': { if (editMode.sidebar && editMode.sidebar.isVisible) { editMode.sidebar.hide(); } break; } default: { break; } } } } /** * Method for hiding edit toolbars. * @internal * * @param toolbarTypes * The array of toolbar names to hide ('cell', 'row', 'sidebar'). * * @param currentCell * The cell reference for toolbar. * */ showToolbars(toolbarTypes, currentCell) { const editMode = this, toolbarsToShow = toolbarTypes || ['cell', 'row', 'sidebar']; for (let i = 0, iEnd = toolbarsToShow.length; i < iEnd; ++i) { switch (toolbarsToShow[i]) { case 'cell': { if (currentCell && editMode.cellToolbar) { editMode.cellToolbar.showToolbar(currentCell); } break; } case 'row': { if (currentCell && currentCell.row && editMode.rowToolbar) { editMode.rowToolbar.showToolbar(currentCell.row); } break; } case 'sidebar': { if (editMode.sidebar && !editMode.sidebar.isVisible) { editMode.sidebar.show(); } break; } default: { break; } } } } /** * Creates the buttons such as `addComponent` button, context menu button * and its container. * @internal */ createTools() { const editMode = this; const { board, options, tools } = editMode; // Create tools container tools.container = document.createElement('div'); tools.container.classList.add(EditGlobals.classNames.editTools); if (board.layoutsWrapper) { // For the generated layout board.layoutsWrapper.parentNode.insertBefore(tools.container, board.layoutsWrapper); } else { // For the custom layout board.container.insertBefore(tools.container, board.container.firstChild); } // Create context a menu button or edit mode toggle if (options.contextMenu && options.contextMenu.enabled) { if (options.contextMenu.items?.length) { tools.contextButtonElement = EditRenderer.renderContextButton(tools.container, editMode); // Init contextMenu if doesn't exist. if (!editMode.tools.contextMenu) { editMode.tools.contextMenu = new EditContextMenu(editMode.board.container, editMode.options.contextMenu || {}, editMode); } } else { // Render the edit mode toggle when no items are provided tools.standaloneEditToggle = EditRenderer.renderToggle(tools.container, { id: EditContextMenu.items.editMode.id, name: EditContextMenu.items.editMode.id, className: EditGlobals.classNames.editStandaloneToggle, title: editMode.lang.editMode, value: editMode.isActive(), lang: editMode.lang, langKey: 'editMode', onchange() { editMode.toggleEditMode(); } }); } } // Create add component button if (options.tools?.addComponentBtn?.enabled && options.toolbars?.cell?.enabled && !this.customHTMLMode) { const addIconURL = options.tools.addComponentBtn.icon; this.addComponentBtn = EditRenderer.renderButton(tools.container, { className: EditGlobals.classNames.editToolsBtn, icon: addIconURL, text: this.lang.addComponent, callback: () => { // Sidebar trigger if (editMode.sidebar) { editMode.sidebar.show(); editMode.setEditOverlay(); } }, style: { display: 'none' } }); } } /** * Event fired when detecting context on drag&drop. */ onDetectContext() { const editMode = this; if (!editMode.isActive() || !editMode.isContextDetectionActive || (!editMode.mouseCellContext && !editMode.mouseRowContext) || (editMode.dragDrop || {}).isActive) { return; } let cellContext; let rowContext; if (editMode.mouseCellContext) { cellContext = editMode.mouseCellContext; } else if (editMode.mouseRowContext) { rowContext = editMode.mouseRowContext; cellContext = rowContext.layout.parentCell; } this.potentialCellContext = cellContext; if (cellContext) { const cellContextOffsets = GUIElement .getOffsets(cellContext, editMode.board.container); const { width, height } = GUIElement .getDimFromOffsets(cellContextOffsets); editMode.showContextPointer(cellContextOffsets.left, cellContextOffsets.top, width, height); } } /** * Stops the context detection. */ stopContextDetection() { this.isContextDetectionActive = false; if (this.dragDrop) { this.dragDrop.mouseCellContext = void 0; } this.mouseCellContext = void 0; this.hideContextPointer(); } /** * Confirms the selected context. */ onContextConfirm() { if (this.isContextDetectionActive && this.potentialCellContext && this.editCellContext !== this.potentialCellContext) { this.setEditCellContext(this.potentialCellContext, this.editCellContext); } } /** * Sets the edit cell context. * @internal */ setEditCellContext(editCellContext, oldEditCellContext) { const editMode = this; const oldContext = oldEditCellContext; if (isCellHTML(editCellContext) || isCellHTML(oldContext)) { editMode.editCellContext = editCellContext; editMode.cellToolbar?.showToolbar(editCellContext); } else { const oldContextRow = oldContext?.row; editMode.editCellContext = editCellContext; editMode.showToolbars(['row', 'cell'], editCellContext); if (!oldContextRow || oldContextRow !== editCellContext.row) { if (oldContextRow) { // Remove highlight from the previous row. oldContextRow.setHighlight(true); } // Add highlight to the context row. if (editCellContext.row) { editCellContext.row.setHighlight(); } } if (editMode.resizer) { editMode.resizer.setSnapPositions(editCellContext); } } } /** * Method for showing and positioning context pointer. * @internal */ showContextPointer(left, top, width, height) { if (!this.contextPointer) { return; } this.contextPointer.isVisible = true; css(this.contextPointer.element, { display: 'block', left: left + 'px', top: top + 'px', height: height + 'px', width: width + 'px' }); } /** * Method for hiding context pointer. * @internal */ hideContextPointer() { if (this.contextPointer?.isVisible) { this.contextPointer.isVisible = false; this.contextPointer.element.style.display = 'none'; } } /** * Adds/Removes the edit mode overlay. * @internal * * @param remove * Whether the edit overlay should be removed. */ setEditOverlay(remove) { const editMode = this, cnt = editMode.editOverlay, isSet = cnt?.classList.contains(EditGlobals.classNames.editOverlayActive); if (!remove && !isSet) { cnt?.classList.add(EditGlobals.classNames.editOverlayActive); editMode.isEditOverlayActive = true; } else if (remove && isSet) { cnt?.classList.remove(EditGlobals.classNames.editOverlayActive); editMode.isEditOverlayActive = false; } } } /* * * * Default Export * * */ export default EditMode;