243 lines
7.4 KiB
TypeScript
243 lines
7.4 KiB
TypeScript
import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
|
import './Editor.scss';
|
|
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
|
import { UI } from '../UI/UI';
|
|
import { SelectContainer, DeleteContainer, OnPropertyChange, ReplaceByContainer } from './Actions/ContainerOperations';
|
|
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
|
|
import { OnKey } from './Actions/Shortcuts';
|
|
import { UseCustomEvents, UseEditorListener } from '../../Events/EditorEvents';
|
|
import { MAX_HISTORY } from '../../utils/default';
|
|
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations';
|
|
import { FindContainerById } from '../../utils/itertools';
|
|
import { Menu } from '../Menu/Menu';
|
|
import { InitActions } from './Actions/ContextMenuActions';
|
|
import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer';
|
|
import { IReplaceContainer } from '../../Interfaces/IReplaceContainer';
|
|
|
|
interface IEditorProps {
|
|
root: Element | Document
|
|
configuration: IConfiguration
|
|
history: IHistoryState[]
|
|
historyCurrentStep: number
|
|
}
|
|
|
|
function UseShortcuts(
|
|
history: IHistoryState[],
|
|
historyCurrentStep: number,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
|
|
deleteAction: () => void
|
|
): void {
|
|
useEffect(() => {
|
|
function OnKeyUp(event: KeyboardEvent): void {
|
|
return OnKey(
|
|
event,
|
|
history,
|
|
historyCurrentStep,
|
|
setHistoryCurrentStep,
|
|
deleteAction
|
|
);
|
|
}
|
|
|
|
window.addEventListener('keyup', OnKeyUp);
|
|
return () => {
|
|
window.removeEventListener('keyup', OnKeyUp);
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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[], historyCurrentStep?: number) => void {
|
|
return (newHistory, historyCurrentStep?: number) => {
|
|
setHistory(newHistory);
|
|
setHistoryCurrentStep(historyCurrentStep !== undefined && historyCurrentStep !== null ? historyCurrentStep : 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 [replaceContainer, setReplaceContainer] = React.useState<IReplaceContainer>({ isReplacing: false, id: undefined, category: undefined });
|
|
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep);
|
|
|
|
// Events
|
|
UseShortcuts(
|
|
history,
|
|
historyCurrentStep,
|
|
setHistoryCurrentStep,
|
|
() => {
|
|
const current = GetCurrentHistoryState(history, historyCurrentStep);
|
|
setNewHistory(
|
|
DeleteContainer(current.selectedContainerId, history, historyCurrentStep)
|
|
);
|
|
}
|
|
);
|
|
UseCustomEvents(
|
|
props.root,
|
|
history,
|
|
historyCurrentStep,
|
|
props.configuration,
|
|
editorRef,
|
|
setNewHistory
|
|
);
|
|
UseEditorListener(
|
|
props.root,
|
|
history,
|
|
historyCurrentStep,
|
|
props.configuration
|
|
);
|
|
|
|
// Context Menu
|
|
const menuActions = new Map();
|
|
InitActions(
|
|
menuActions,
|
|
props.configuration,
|
|
history,
|
|
historyCurrentStep,
|
|
setNewHistory,
|
|
setHistoryCurrentStep,
|
|
setReplaceContainer
|
|
);
|
|
|
|
// Render
|
|
const configuration = props.configuration;
|
|
const current = GetCurrentHistoryState(history, historyCurrentStep);
|
|
const selected = FindContainerById(current.containers, current.selectedContainerId);
|
|
|
|
return (
|
|
<div ref={editorRef} className="Editor font-sans h-full ">
|
|
<UI
|
|
editorState={{
|
|
configuration: props.configuration,
|
|
history,
|
|
historyCurrentStep
|
|
}}
|
|
replaceContainer={replaceContainer}
|
|
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
|
|
))}
|
|
addOrReplaceContainer={(type) => {
|
|
if (selected === null || selected === undefined) {
|
|
return;
|
|
}
|
|
if (replaceContainer.isReplacing && replaceContainer.id !== undefined) {
|
|
const newHistory = ReplaceByContainer(
|
|
replaceContainer.id,
|
|
type,
|
|
configuration,
|
|
history,
|
|
historyCurrentStep
|
|
);
|
|
setReplaceContainer({ isReplacing: false, id: undefined, category: undefined });
|
|
setNewHistory(newHistory);
|
|
} else {
|
|
setNewHistory(AddContainerToSelectedContainer(
|
|
type,
|
|
selected,
|
|
configuration,
|
|
history,
|
|
historyCurrentStep
|
|
));
|
|
}
|
|
}}
|
|
addContainerAt={(index, type, parent) => setNewHistory(
|
|
AddContainer(
|
|
index,
|
|
type,
|
|
parent,
|
|
configuration,
|
|
history,
|
|
historyCurrentStep
|
|
)
|
|
)}
|
|
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)}
|
|
setReplaceContainer={setReplaceContainer}
|
|
/>
|
|
<Menu
|
|
getListener={() => editorRef.current}
|
|
configuration={configuration}
|
|
actions={menuActions}
|
|
className="z-30 transition-opacity rounded bg-slate-200 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];
|
|
}
|