Merged PR 16: Transform every single class components into functional component
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This improve greatly the performance and the code cleaning.
It allows us to separate the inseparable class methods into modules functions
This commit is contained in:
Eric Nguyen 2022-08-09 15:15:56 +00:00
parent 1fc11adbaa
commit d9e06537e8
33 changed files with 1298 additions and 1261 deletions

View file

@ -0,0 +1,269 @@
import { Dispatch, SetStateAction } from 'react';
import { HistoryState } from "../../Interfaces/HistoryState";
import { Configuration } from '../../Interfaces/Configuration';
import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel';
import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor';
/**
* Select a container
* @param container Selected container
*/
export function SelectContainer(
container: ContainerModel,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.MainContainer === null) {
throw new Error('[SelectContainer] Tried to select a container while there is no main container!');
}
const mainContainerClone = structuredClone(current.MainContainer);
const SelectedContainer = findContainerById(mainContainerClone, container.properties.id);
if (SelectedContainer === undefined) {
throw new Error('[SelectContainer] Cannot find container among children of main container!');
}
setHistory(history.concat([{
MainContainer: mainContainerClone,
TypeCounters: Object.assign({}, current.TypeCounters),
SelectedContainer,
SelectedContainerId: SelectedContainer.properties.id
}]));
setHistoryCurrentStep(history.length);
}
export function DeleteContainer(
containerId: string,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[historyCurrentStep];
if (current.MainContainer === null) {
throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container');
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container = findContainerById(mainContainerClone, containerId);
if (container === undefined) {
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
}
if (container === mainContainerClone) {
// TODO: Implement alert
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !');
}
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
if (container.parent != null) {
const index = container.parent.children.indexOf(container);
if (index > -1) {
container.parent.children.splice(index, 1);
}
}
setHistory(history.concat([{
SelectedContainer: null,
SelectedContainerId: '',
MainContainer: mainContainerClone,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
}
/**
* Add a new container to a selected container
* @param type The type of container
* @returns void
*/
export function AddContainerToSelectedContainer(
type: string,
configuration: Configuration,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
return;
}
const parent = current.SelectedContainer;
AddContainer(
parent.children.length,
type,
parent.properties.id,
configuration,
fullHistory,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
);
}
export function AddContainer(
index: number,
type: string,
parentId: string,
configuration: Configuration,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.MainContainer === null ||
current.MainContainer === undefined) {
return;
}
// Get the preset properties from the API
const properties = configuration.AvailableContainers
.find(option => option.Type === type);
if (properties === undefined) {
throw new Error(`[AddContainer] Object type not found. Found: ${type}`);
}
// Set the counter of the object type in order to assign an unique id
const newCounters = Object.assign({}, current.TypeCounters);
if (newCounters[type] === null ||
newCounters[type] === undefined) {
newCounters[type] = 0;
} else {
newCounters[type]++;
}
const count = newCounters[type];
// Create maincontainer model
const clone: IContainerModel = structuredClone(current.MainContainer);
// Find the parent
const parentClone: IContainerModel | undefined = findContainerById(
clone, parentId
);
if (parentClone === null || parentClone === undefined) {
throw new Error('[AddContainer] Container model was not found among children of the main container!');
}
let x = 0;
if (index !== 0) {
const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1);
if (lastChild !== undefined) {
x = lastChild.properties.x + Number(lastChild.properties.width);
}
}
// Create the container
const newContainer = new ContainerModel(
parentClone,
{
id: `${type}-${count}`,
parentId: parentClone.properties.id,
x,
y: 0,
width: properties?.Width,
height: parentClone.properties.height,
...properties.Style
},
[],
{
type
}
);
// And push it the the parent children
if (index === parentClone.children.length) {
parentClone.children.push(newContainer);
} else {
parentClone.children.splice(index, 0, newContainer);
}
// Update the state
setHistory(history.concat([{
MainContainer: clone,
TypeCounters: newCounters,
SelectedContainer: parentClone,
SelectedContainerId: parentClone.properties.id
}]));
setHistoryCurrentStep(history.length);
}
/**
* Handled the property change event in the properties form
* @param key Property name
* @param value New value of the property
* @returns void
*/
export function OnPropertyChange(
key: string,
value: string | number,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
if (current.MainContainer === null ||
current.MainContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before the main container was added');
}
if (parent === null) {
const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer);
(selectedContainerClone.properties as any)[key] = value;
setHistory(history.concat([{
SelectedContainer: selectedContainerClone,
SelectedContainerId: selectedContainerClone.properties.id,
MainContainer: selectedContainerClone,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
return;
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
(container.properties as any)[key] = value;
setHistory(history.concat([{
SelectedContainer: container,
SelectedContainerId: container.properties.id,
MainContainer: mainContainerClone,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
}

View file

@ -0,0 +1,24 @@
svg {
height: 100%;
width: 100%;
}
text {
font-size: 18px;
font-weight: 800;
fill: none;
fill-opacity: 0;
stroke: #000000;
stroke-width: 1px;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-opacity: 1;
transform: translateX(-50%);
transform-box: fill-box;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}

View file

@ -0,0 +1,115 @@
import React from 'react';
import './Editor.scss';
import { Configuration } from '../../Interfaces/Configuration';
import { SVG } from '../SVG/SVG';
import { HistoryState } from '../../Interfaces/HistoryState';
import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations';
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save';
import { onKeyDown } from './Shortcuts';
interface IEditorProps {
configuration: Configuration
history: HistoryState[]
historyCurrentStep: number
}
export interface IEditorState {
history: HistoryState[]
historyCurrentStep: number
configuration: Configuration
}
export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: number): HistoryState[] => history.slice(0, historyCurrentStep + 1);
export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep];
const Editor: React.FunctionComponent<IEditorProps> = (props) => {
const [history, setHistory] = React.useState<HistoryState[]>([...props.history]);
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(0);
React.useEffect(() => {
window.addEventListener('keyup', (event) => onKeyDown(
event,
history,
historyCurrentStep,
setHistoryCurrentStep
));
return () => {
window.removeEventListener('keyup', (event) => onKeyDown(
event,
history,
historyCurrentStep,
setHistoryCurrentStep
));
};
});
const configuration = props.configuration;
const current = getCurrentHistoryState(history, historyCurrentStep);
return (
<div className="App font-sans h-full">
<UI
current={current}
history={history}
historyCurrentStep={historyCurrentStep}
AvailableContainers={configuration.AvailableContainers}
SelectContainer={(container) => SelectContainer(
container,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
DeleteContainer={(containerId: string) => DeleteContainer(
containerId,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
OnPropertyChange={(key, value) => OnPropertyChange(
key, value,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
type,
configuration,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
AddContainer={(index, type, parentId) => AddContainer(
index,
type,
parentId,
configuration,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
SaveEditorAsJSON={() => SaveEditorAsJSON(
history,
historyCurrentStep,
configuration
)}
SaveEditorAsSVG={() => SaveEditorAsSVG()}
LoadState={(move) => setHistoryCurrentStep(move)}
/>
<SVG
width={Number(current.MainContainer?.properties.width)}
height={Number(current.MainContainer?.properties.height)}
selected={current.SelectedContainer}
>
{ current.MainContainer }
</SVG>
</div>
);
};
export default Editor;

View file

@ -0,0 +1,41 @@
import { HistoryState } from "../../Interfaces/HistoryState";
import { Configuration } from '../../Interfaces/Configuration';
import { getCircularReplacer } from '../../utils/saveload';
import { ID } from '../SVG/SVG';
import { IEditorState } from './Editor';
export function SaveEditorAsJSON(
history: HistoryState[],
historyCurrentStep: number,
configuration: Configuration
): void {
const exportName = 'state';
const spaces = import.meta.env.DEV ? 4 : 0;
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration
};
const data = JSON.stringify(editorState, getCircularReplacer(), spaces);
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', `${exportName}.json`);
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
export function SaveEditorAsSVG(): void {
const svgWrapper = document.getElementById(ID) as HTMLElement;
const svg = svgWrapper.querySelector('svg') as SVGSVGElement;
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const svgUrl = URL.createObjectURL(svgBlob);
const downloadLink = document.createElement('a');
downloadLink.href = svgUrl;
downloadLink.download = 'newesttree.svg';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}

View file

@ -0,0 +1,23 @@
import { Dispatch, SetStateAction } from 'react';
import { HistoryState } from "../../Interfaces/HistoryState";
export function onKeyDown(
event: KeyboardEvent,
history: HistoryState[],
historyCurrentStep: number,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
event.preventDefault();
if (event.isComposing || event.keyCode === 229) {
return;
}
if (event.key === 'z' &&
event.ctrlKey &&
historyCurrentStep > 0) {
setHistoryCurrentStep(historyCurrentStep - 1);
} else if (event.key === 'y' &&
event.ctrlKey &&
historyCurrentStep < history.length - 1) {
setHistoryCurrentStep(historyCurrentStep + 1);
}
}