310 lines
8.8 KiB
TypeScript
310 lines
8.8 KiB
TypeScript
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<string, IMenuAction[]>,
|
|
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<SetStateAction<number>>
|
|
): 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<HTMLDivElement>,
|
|
setNewHistory: (newHistory: IHistoryState[]) => void
|
|
): void {
|
|
useEffect(() => {
|
|
const editorState: IEditorState = {
|
|
history,
|
|
historyCurrentStep,
|
|
configuration
|
|
};
|
|
|
|
const funcs = new Map<string, () => 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<SetStateAction<IHistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): (newHistory: IHistoryState[]) => void {
|
|
return (newHistory) => {
|
|
setHistory(newHistory);
|
|
setHistoryCurrentStep(newHistory.length - 1);
|
|
};
|
|
}
|
|
|
|
export function Editor(props: IEditorProps): JSX.Element {
|
|
// States
|
|
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
|
|
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
|
|
const editorRef = useRef<HTMLDivElement>(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 (
|
|
<div ref={editorRef} className="Editor font-sans h-full">
|
|
<UI
|
|
selectedContainer={selected}
|
|
current={current}
|
|
history={history}
|
|
historyCurrentStep={historyCurrentStep}
|
|
availableContainers={configuration.AvailableContainers}
|
|
availableSymbols={configuration.AvailableSymbols}
|
|
selectContainer={(container) => 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)} />
|
|
<SVG
|
|
width={current.mainContainer?.properties.width}
|
|
height={current.mainContainer?.properties.height}
|
|
selected={selected}
|
|
symbols={current.symbols}
|
|
>
|
|
{current.mainContainer}
|
|
</SVG>
|
|
<Menu
|
|
getListener={() => editorRef.current}
|
|
actions={menuActions}
|
|
className="z-30 transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function UpdateCounters(counters: Record<string, number>, 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];
|
|
}
|