import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react'; import './Editor.scss'; import { IConfiguration } from '../../Interfaces/IConfiguration'; import { SVG } from '../SVG/SVG'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { UI } from '../UI/UI'; import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange, AddContainers } from './Actions/ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save'; import { OnKey } from './Actions/Shortcuts'; import { events as EVENTS } from '../../Events/EditorEvents'; import { IEditorState } from '../../Interfaces/IEditorState'; import { MAX_HISTORY } from '../../utils/default'; import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations'; import { FindContainerById } from '../../utils/itertools'; import { IMenuAction, Menu } from '../Menu/Menu'; import { GetAction } from './Actions/ContextMenuActions'; interface IEditorProps { root: Element | Document configuration: IConfiguration history: IHistoryState[] historyCurrentStep: number } function InitActions( menuActions: Map, configuration: IConfiguration, history: IHistoryState[], historyCurrentStep: number, setNewHistory: (newHistory: IHistoryState[]) => void ): void { menuActions.set( 'elements-sidebar-row', [{ text: 'Delete', action: (target: HTMLElement) => { const id = target.id; const newHistory = DeleteContainer( id, history, historyCurrentStep ); setNewHistory(newHistory); } }] ); menuActions.set( 'symbols-sidebar-row', [{ text: 'Delete', action: (target: HTMLElement) => { const id = target.id; const newHistory = DeleteSymbol( id, history, historyCurrentStep ); setNewHistory(newHistory); } }] ); // API Actions for (const availableContainer of configuration.AvailableContainers) { if (availableContainer.Actions === undefined || availableContainer.Actions === null) { continue; } for (const action of availableContainer.Actions) { if (menuActions.get(availableContainer.Type) === undefined) { menuActions.set(availableContainer.Type, []); } const currentState = GetCurrentHistoryState(history, historyCurrentStep); const newAction: IMenuAction = { text: action.Label, action: GetAction( action, currentState, configuration, history, historyCurrentStep, setNewHistory ) }; menuActions.get(availableContainer.Type)?.push(newAction); } } } function UseShortcuts( history: IHistoryState[], historyCurrentStep: number, setHistoryCurrentStep: Dispatch> ): void { useEffect(() => { function OnKeyUp(event: KeyboardEvent): void { return OnKey( event, history, historyCurrentStep, setHistoryCurrentStep ); } window.addEventListener('keyup', OnKeyUp); return () => { window.removeEventListener('keyup', OnKeyUp); }; }); } function UseWindowEvents( root: Element | Document, history: IHistoryState[], historyCurrentStep: number, configuration: IConfiguration, editorRef: React.RefObject, setNewHistory: (newHistory: IHistoryState[]) => void ): void { useEffect(() => { const editorState: IEditorState = { history, historyCurrentStep, configuration }; const funcs = new Map void>(); for (const event of EVENTS) { function Func(eventInitDict?: CustomEventInit): void { return event.func( root, editorState, setNewHistory, eventInitDict ); } editorRef.current?.addEventListener(event.name, Func); funcs.set(event.name, Func); } return () => { for (const event of EVENTS) { const func = funcs.get(event.name); if (func === undefined) { continue; } editorRef.current?.removeEventListener(event.name, func); } }; }); } /** * Return a macro function to use both setHistory * and setHistoryCurrentStep at the same time * @param setHistory * @param setHistoryCurrentStep * @returns */ function UseNewHistoryState( setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): (newHistory: IHistoryState[]) => void { return (newHistory) => { setHistory(newHistory); setHistoryCurrentStep(newHistory.length - 1); }; } export function Editor(props: IEditorProps): JSX.Element { // States const [history, setHistory] = React.useState(structuredClone(props.history)); const [historyCurrentStep, setHistoryCurrentStep] = React.useState(props.historyCurrentStep); const editorRef = useRef(null); const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep); // Events UseShortcuts(history, historyCurrentStep, setHistoryCurrentStep); UseWindowEvents( props.root, history, historyCurrentStep, props.configuration, editorRef, setNewHistory ); // Context Menu const menuActions = new Map(); InitActions( menuActions, props.configuration, history, historyCurrentStep, setNewHistory ); // Render const configuration = props.configuration; const current = GetCurrentHistoryState(history, historyCurrentStep); const selected = FindContainerById(current.mainContainer, current.selectedContainerId); return (
setNewHistory( SelectContainer( container, history, historyCurrentStep ))} deleteContainer={(containerId: string) => setNewHistory( DeleteContainer( containerId, history, historyCurrentStep ))} onPropertyChange={(key, value, type) => setNewHistory( OnPropertyChange( key, value, type, selected, history, historyCurrentStep ))} addContainer={(type) => { const newHistory = AddContainerToSelectedContainer( type, selected, configuration, history, historyCurrentStep ); if (newHistory !== null) { setNewHistory(newHistory); } }} addSymbol={(type) => setNewHistory( AddSymbol( type, configuration, history, historyCurrentStep ))} onSymbolPropertyChange={(key, value) => setNewHistory( OnSymbolPropertyChange( key, value, history, historyCurrentStep ))} selectSymbol={(symbolId) => setNewHistory( SelectSymbol( symbolId, history, historyCurrentStep ))} deleteSymbol={(symbolId) => setNewHistory( DeleteSymbol( symbolId, history, historyCurrentStep ))} saveEditorAsJSON={() => SaveEditorAsJSON( history, historyCurrentStep, configuration )} saveEditorAsSVG={() => SaveEditorAsSVG()} loadState={(move) => setHistoryCurrentStep(move)} /> {current.mainContainer} editorRef.current} actions={menuActions} className="z-30 transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl" />
); } export function UpdateCounters(counters: Record, type: string): void { if (counters[type] === null || counters[type] === undefined) { counters[type] = 0; } else { counters[type]++; } } export function GetCurrentHistory(history: IHistoryState[], historyCurrentStep: number): IHistoryState[] { return history.slice( Math.max(0, history.length - MAX_HISTORY), historyCurrentStep + 1 ); } export function GetCurrentHistoryState(history: IHistoryState[], historyCurrentStep: number): IHistoryState { return history[historyCurrentStep]; }