svg-layout-designer-react/src/Components/Editor/Editor.tsx

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];
}