Merged PR 16: Transform every single class components into functional component
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:
parent
1fc11adbaa
commit
d9e06537e8
33 changed files with 1298 additions and 1261 deletions
|
@ -10,6 +10,7 @@ steps:
|
|||
- node ./test-server/node-http.js &
|
||||
- pnpm install
|
||||
- pnpm run test:nowatch
|
||||
- pnpm run build
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
|
@ -23,3 +24,4 @@ steps:
|
|||
- node ./test-server/node-http.js &
|
||||
- pnpm install
|
||||
- pnpm run test:nowatch
|
||||
- pnpm run build
|
|
@ -37,6 +37,7 @@ steps:
|
|||
jobs
|
||||
pnpm i
|
||||
pnpm run test:nowatch
|
||||
pnpm run build
|
||||
kill -2 %1 2>/dev/null
|
||||
displayName: 'Test on Node.js 16.x LTS'
|
||||
|
||||
|
@ -51,5 +52,6 @@ steps:
|
|||
jobs
|
||||
pnpm i
|
||||
pnpm run test:nowatch
|
||||
pnpm run build
|
||||
kill -2 %1 2>/dev/null
|
||||
displayName: 'Test on Node.js 18.x Latest'
|
240
src/App.tsx
240
src/App.tsx
|
@ -1,240 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import './App.scss';
|
||||
import { MainMenu } from './Components/MainMenu/MainMenu';
|
||||
import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel';
|
||||
import Editor, { IEditorState } from './Editor';
|
||||
import { Configuration } from './Interfaces/Configuration';
|
||||
import { Revive } from './utils/saveload';
|
||||
|
||||
export interface IHistoryState {
|
||||
MainContainer: IContainerModel | null
|
||||
SelectedContainer: IContainerModel | null
|
||||
SelectedContainerId: string
|
||||
TypeCounters: Record<string, number>
|
||||
}
|
||||
|
||||
// App will never have props
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface IAppProps {
|
||||
}
|
||||
|
||||
interface IAppState {
|
||||
configuration: Configuration
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
isLoaded: boolean
|
||||
}
|
||||
|
||||
export class App extends React.Component<IAppProps> {
|
||||
public state: IAppState;
|
||||
|
||||
constructor(props: IAppProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
configuration: {
|
||||
AvailableContainers: [],
|
||||
AvailableSymbols: [],
|
||||
MainContainer: {
|
||||
Type: 'EmptyContainer',
|
||||
Width: 3000,
|
||||
Height: 200,
|
||||
Style: {}
|
||||
}
|
||||
},
|
||||
history: [],
|
||||
historyCurrentStep: 0,
|
||||
isLoaded: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const state = urlParams.get('state');
|
||||
|
||||
if (state === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(state)
|
||||
.then(
|
||||
async(response) => await response.json(),
|
||||
(error) => { throw new Error(error); }
|
||||
)
|
||||
.then((data: IEditorState) => {
|
||||
this.LoadState(data);
|
||||
}, (error) => { throw new Error(error); });
|
||||
}
|
||||
|
||||
public NewEditor(): void {
|
||||
// Fetch the configuration from the API
|
||||
fetchConfiguration()
|
||||
.then((configuration: Configuration) => {
|
||||
// Set the main container from the given properties of the API
|
||||
const MainContainer = new ContainerModel(
|
||||
null,
|
||||
{
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: configuration.MainContainer.Width,
|
||||
height: configuration.MainContainer.Height,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
);
|
||||
|
||||
// Save the configuration and the new MainContainer
|
||||
// and default the selected container to it
|
||||
this.setState({
|
||||
configuration,
|
||||
history:
|
||||
[
|
||||
{
|
||||
MainContainer,
|
||||
SelectedContainer: MainContainer,
|
||||
TypeCounters: {}
|
||||
}
|
||||
],
|
||||
historyCurrentStep: 0,
|
||||
isLoaded: true
|
||||
});
|
||||
}, (error) => {
|
||||
// TODO: Implement an alert component
|
||||
console.warn('[NewEditor] Could not fetch resource from API. Returning default.', error);
|
||||
const MainContainer = new ContainerModel(
|
||||
null,
|
||||
{
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: DEFAULT_CONFIG.MainContainer.Width,
|
||||
height: DEFAULT_CONFIG.MainContainer.Height,
|
||||
fillOpacity: DEFAULT_CONFIG.MainContainer.Style.fillOpacity,
|
||||
stroke: DEFAULT_CONFIG.MainContainer.Style.stroke,
|
||||
}
|
||||
);
|
||||
|
||||
// Save the configuration and the new MainContainer
|
||||
// and default the selected container to it
|
||||
this.setState({
|
||||
configuration: DEFAULT_CONFIG,
|
||||
history:
|
||||
[
|
||||
{
|
||||
MainContainer,
|
||||
SelectedContainer: MainContainer,
|
||||
TypeCounters: {}
|
||||
}
|
||||
],
|
||||
historyCurrentStep: 0,
|
||||
isLoaded: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public LoadEditor(files: FileList | null): void {
|
||||
if (files === null) {
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const result = reader.result as string;
|
||||
const editorState: IEditorState = JSON.parse(result);
|
||||
|
||||
this.LoadState(editorState);
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
private LoadState(editorState: IEditorState): void {
|
||||
Revive(editorState);
|
||||
|
||||
this.setState({
|
||||
configuration: editorState.configuration,
|
||||
history: editorState.history,
|
||||
historyCurrentStep: editorState.historyCurrentStep,
|
||||
isLoaded: true
|
||||
});
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.isLoaded) {
|
||||
return (
|
||||
<div>
|
||||
<Editor
|
||||
configuration={this.state.configuration}
|
||||
history={this.state.history}
|
||||
historyCurrentStep={this.state.historyCurrentStep}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='bg-blue-100 h-full w-full'>
|
||||
<MainMenu
|
||||
newEditor={() => this.NewEditor()}
|
||||
loadEditor={(files: FileList | null) => this.LoadEditor(files)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the configuration from the API
|
||||
* @returns {Configation} The model of the configuration for the application
|
||||
*/
|
||||
export async function fetchConfiguration(): Promise<Configuration> {
|
||||
const url = `${import.meta.env.VITE_API_URL}`;
|
||||
// The test library cannot use the Fetch API
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
if (window.fetch) {
|
||||
return await fetch(url, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(async(response) =>
|
||||
await response.json()
|
||||
) as Configuration;
|
||||
}
|
||||
return await new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.onreadystatechange = function() { // Call a function when the state changes.
|
||||
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
||||
resolve(JSON.parse(this.responseText));
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_CONFIG: Configuration = {
|
||||
AvailableContainers: [
|
||||
{
|
||||
Type: 'Container',
|
||||
Width: 75,
|
||||
Height: 100,
|
||||
Style: {
|
||||
fillOpacity: 0,
|
||||
stroke: 'green'
|
||||
}
|
||||
}
|
||||
],
|
||||
AvailableSymbols: [],
|
||||
MainContainer: {
|
||||
Type: 'Container',
|
||||
Width: 2000,
|
||||
Height: 100,
|
||||
Style: {
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { fetchConfiguration } from '../App';
|
||||
import { fetchConfiguration } from './api';
|
||||
|
||||
describe.concurrent('API test', () => {
|
||||
it('Load environment', () => {
|
30
src/Components/API/api.ts
Normal file
30
src/Components/API/api.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Configuration } from '../../Interfaces/Configuration';
|
||||
|
||||
/**
|
||||
* Fetch the configuration from the API
|
||||
* @returns {Configation} The model of the configuration for the application
|
||||
*/
|
||||
export async function fetchConfiguration(): Promise<Configuration> {
|
||||
const url = `${import.meta.env.VITE_API_URL}`;
|
||||
// The test library cannot use the Fetch API
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
if (window.fetch) {
|
||||
return await fetch(url, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(async(response) =>
|
||||
await response.json()
|
||||
) as Configuration;
|
||||
}
|
||||
return await new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.onreadystatechange = function() { // Call a function when the state changes.
|
||||
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
||||
resolve(JSON.parse(this.responseText));
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
79
src/Components/App/App.tsx
Normal file
79
src/Components/App/App.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import './App.scss';
|
||||
import { MainMenu } from '../MainMenu/MainMenu';
|
||||
import { ContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import Editor, { IEditorState } from '../Editor/Editor';
|
||||
import { LoadState } from './Load';
|
||||
import { LoadEditor, NewEditor } from './MenuActions';
|
||||
import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
|
||||
|
||||
// App will never have props
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface IAppProps {
|
||||
}
|
||||
|
||||
export const App: React.FunctionComponent<IAppProps> = (props) => {
|
||||
const [isLoaded, setLoaded] = useState<boolean>(false);
|
||||
|
||||
const defaultMainContainer = new ContainerModel(
|
||||
null,
|
||||
DEFAULT_MAINCONTAINER_PROPS
|
||||
);
|
||||
|
||||
const [editorState, setEditorState] = useState<IEditorState>({
|
||||
configuration: DEFAULT_CONFIG,
|
||||
history: [{
|
||||
MainContainer: defaultMainContainer,
|
||||
SelectedContainer: defaultMainContainer,
|
||||
SelectedContainerId: defaultMainContainer.properties.id,
|
||||
TypeCounters: {}
|
||||
}],
|
||||
historyCurrentStep: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const state = urlParams.get('state');
|
||||
|
||||
if (state === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(state)
|
||||
.then(
|
||||
async(response) => await response.json(),
|
||||
(error) => { throw new Error(error); }
|
||||
)
|
||||
.then((data: IEditorState) => {
|
||||
LoadState(data, setEditorState, setLoaded);
|
||||
}, (error) => { throw new Error(error); });
|
||||
});
|
||||
|
||||
if (isLoaded) {
|
||||
return (
|
||||
<div>
|
||||
<Editor
|
||||
configuration={editorState.configuration}
|
||||
history={editorState.history}
|
||||
historyCurrentStep={editorState.historyCurrentStep}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-blue-100 h-full w-full'>
|
||||
<MainMenu
|
||||
newEditor={() => NewEditor(
|
||||
setEditorState, setLoaded
|
||||
)}
|
||||
loadEditor={(files: FileList | null) => LoadEditor(
|
||||
files,
|
||||
setEditorState,
|
||||
setLoaded
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
13
src/Components/App/Load.ts
Normal file
13
src/Components/App/Load.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Revive } from '../../utils/saveload';
|
||||
import { IEditorState } from '../Editor/Editor';
|
||||
|
||||
export function LoadState(
|
||||
editorState: IEditorState,
|
||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||
setLoaded: Dispatch<SetStateAction<boolean>>
|
||||
): void {
|
||||
Revive(editorState);
|
||||
setEditorState(editorState);
|
||||
setLoaded(true);
|
||||
}
|
71
src/Components/App/MenuActions.ts
Normal file
71
src/Components/App/MenuActions.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Configuration } from '../../Interfaces/Configuration';
|
||||
import { ContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { fetchConfiguration } from '../API/api';
|
||||
import { IEditorState } from '../Editor/Editor';
|
||||
import { LoadState } from './Load';
|
||||
|
||||
export function NewEditor(
|
||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||
setLoaded: Dispatch<SetStateAction<boolean>>
|
||||
): void {
|
||||
// Fetch the configuration from the API
|
||||
fetchConfiguration()
|
||||
.then((configuration: Configuration) => {
|
||||
// Set the main container from the given properties of the API
|
||||
const MainContainer = new ContainerModel(
|
||||
null,
|
||||
{
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: configuration.MainContainer.Width,
|
||||
height: configuration.MainContainer.Height,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
);
|
||||
|
||||
// Save the configuration and the new MainContainer
|
||||
// and default the selected container to it
|
||||
const editorState: IEditorState = {
|
||||
configuration,
|
||||
history:
|
||||
[
|
||||
{
|
||||
MainContainer,
|
||||
SelectedContainer: MainContainer,
|
||||
SelectedContainerId: MainContainer.properties.id,
|
||||
TypeCounters: {}
|
||||
}
|
||||
],
|
||||
historyCurrentStep: 0
|
||||
};
|
||||
setEditorState(editorState);
|
||||
setLoaded(true);
|
||||
}, (error) => {
|
||||
// TODO: Implement an alert component
|
||||
console.warn('[NewEditor] Could not fetch resource from API. Using default.', error);
|
||||
setLoaded(true);
|
||||
});
|
||||
}
|
||||
|
||||
export function LoadEditor(
|
||||
files: FileList | null,
|
||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||
setLoaded: Dispatch<SetStateAction<boolean>>
|
||||
): void {
|
||||
if (files === null) {
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const result = reader.result as string;
|
||||
const editorState: IEditorState = JSON.parse(result);
|
||||
|
||||
LoadState(editorState, setEditorState, setLoaded);
|
||||
});
|
||||
reader.readAsText(file);
|
||||
}
|
269
src/Components/Editor/ContainerOperations.ts
Normal file
269
src/Components/Editor/ContainerOperations.ts
Normal 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);
|
||||
}
|
115
src/Components/Editor/Editor.tsx
Normal file
115
src/Components/Editor/Editor.tsx
Normal 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;
|
41
src/Components/Editor/Save.ts
Normal file
41
src/Components/Editor/Save.ts
Normal 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);
|
||||
}
|
23
src/Components/Editor/Shortcuts.ts
Normal file
23
src/Components/Editor/Shortcuts.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -5,22 +5,6 @@ import { ElementsSidebar } from './ElementsSidebar';
|
|||
import { IContainerModel } from '../../Interfaces/ContainerModel';
|
||||
|
||||
describe.concurrent('Elements sidebar', () => {
|
||||
it('No elements', () => {
|
||||
render(<ElementsSidebar
|
||||
MainContainer={null}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={null}
|
||||
onPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
/>);
|
||||
|
||||
expect(screen.getByText(/Elements/i));
|
||||
expect(screen.queryByText('id')).toBeNull();
|
||||
expect(screen.queryByText(/main/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('With a MainContainer', () => {
|
||||
render(<ElementsSidebar
|
||||
MainContainer={{
|
||||
|
@ -42,6 +26,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
onPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
/>);
|
||||
|
||||
expect(screen.getByText(/Elements/i));
|
||||
|
@ -72,6 +57,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
onPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
/>);
|
||||
|
||||
expect(screen.getByText(/Elements/i));
|
||||
|
@ -157,6 +143,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
onPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
/>);
|
||||
|
||||
expect(screen.getByText(/Elements/i));
|
||||
|
@ -210,6 +197,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
onPropertyChange={() => {}}
|
||||
SelectContainer={selectContainer}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
/>);
|
||||
|
||||
expect(screen.getByText(/Elements/i));
|
||||
|
@ -232,6 +220,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
onPropertyChange={() => {}}
|
||||
SelectContainer={selectContainer}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
/>);
|
||||
|
||||
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
|
||||
|
|
|
@ -2,12 +2,14 @@ import * as React from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
import { Properties } from '../Properties/Properties';
|
||||
import { IContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { findContainerById, getDepth, MakeIterator } from '../../utils/itertools';
|
||||
import { getDepth, MakeIterator } from '../../utils/itertools';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
|
||||
import { Point } from '../../Interfaces/Point';
|
||||
|
||||
interface IElementsSidebarProps {
|
||||
MainContainer: IContainerModel | null
|
||||
MainContainer: IContainerModel
|
||||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
SelectedContainer: IContainerModel | null
|
||||
|
@ -17,194 +19,20 @@ interface IElementsSidebarProps {
|
|||
AddContainer: (index: number, type: string, parent: string) => void
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface IElementsSidebarState {
|
||||
isContextMenuOpen: boolean
|
||||
contextMenuPosition: Point
|
||||
onClickContainerId: string
|
||||
}
|
||||
|
||||
export class ElementsSidebar extends React.PureComponent<IElementsSidebarProps> {
|
||||
public state: IElementsSidebarState;
|
||||
public elementRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: IElementsSidebarProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isContextMenuOpen: false,
|
||||
contextMenuPosition: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
onClickContainerId: ''
|
||||
};
|
||||
this.elementRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.elementRef.current?.addEventListener('contextmenu', (event) => this.handleRightClick(event));
|
||||
window.addEventListener('click', (event) => this.handleLeftClick(event));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.elementRef.current?.removeEventListener('contextmenu', (event) => this.handleRightClick(event));
|
||||
window.removeEventListener('click', (event) => this.handleLeftClick(event));
|
||||
}
|
||||
|
||||
public handleRightClick(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(event.target instanceof HTMLButtonElement)) {
|
||||
this.setState({
|
||||
isContextMenuOpen: false,
|
||||
onClickContainerId: ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const contextMenuPosition: Point = { x: event.pageX, y: event.pageY };
|
||||
this.setState({
|
||||
isContextMenuOpen: true,
|
||||
contextMenuPosition,
|
||||
onClickContainerId: event.target.id
|
||||
});
|
||||
}
|
||||
|
||||
public handleLeftClick(event: MouseEvent): void {
|
||||
if (!this.state.isContextMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isContextMenuOpen: false,
|
||||
onClickContainerId: ''
|
||||
});
|
||||
}
|
||||
|
||||
public handleDragOver(event: React.DragEvent): void {
|
||||
event.preventDefault();
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top; // y position within the element.
|
||||
|
||||
if (this.props.MainContainer === null) {
|
||||
throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!');
|
||||
}
|
||||
|
||||
if (target.id === this.props.MainContainer.properties.id) {
|
||||
target.classList.add('border-8');
|
||||
target.classList.remove('border-t-8');
|
||||
target.classList.remove('border-b-8');
|
||||
return;
|
||||
}
|
||||
|
||||
if (y < 12) {
|
||||
target.classList.add('border-t-8');
|
||||
target.classList.remove('border-b-8');
|
||||
target.classList.remove('border-8');
|
||||
} else if (y < 24) {
|
||||
target.classList.add('border-8');
|
||||
target.classList.remove('border-t-8');
|
||||
target.classList.remove('border-b-8');
|
||||
} else {
|
||||
target.classList.add('border-b-8');
|
||||
target.classList.remove('border-8');
|
||||
target.classList.remove('border-t-8');
|
||||
}
|
||||
}
|
||||
|
||||
public handleOnDrop(event: React.DragEvent): void {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer.getData('type');
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
removeBorderClasses(target);
|
||||
|
||||
if (this.props.MainContainer === null) {
|
||||
throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!');
|
||||
}
|
||||
|
||||
const targetContainer: IContainerModel | undefined = findContainerById(
|
||||
this.props.MainContainer,
|
||||
target.id
|
||||
);
|
||||
|
||||
if (targetContainer === undefined) {
|
||||
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
|
||||
}
|
||||
|
||||
if (targetContainer === this.props.MainContainer) {
|
||||
// if the container is the root, only add type as child
|
||||
this.props.AddContainer(
|
||||
targetContainer.children.length,
|
||||
type,
|
||||
targetContainer.properties.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetContainer.parent === null ||
|
||||
targetContainer.parent === undefined) {
|
||||
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
|
||||
}
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top; // y position within the element.
|
||||
|
||||
// locate the hitboxes
|
||||
if (y < 12) {
|
||||
const index = targetContainer.parent.children.indexOf(targetContainer);
|
||||
this.props.AddContainer(
|
||||
index,
|
||||
type,
|
||||
targetContainer.parent.properties.id
|
||||
);
|
||||
} else if (y < 24) {
|
||||
this.props.AddContainer(
|
||||
targetContainer.children.length,
|
||||
type,
|
||||
targetContainer.properties.id);
|
||||
} else {
|
||||
const index = targetContainer.parent.children.indexOf(targetContainer);
|
||||
this.props.AddContainer(
|
||||
index + 1,
|
||||
type,
|
||||
targetContainer.parent.properties.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode {
|
||||
if (this.props.MainContainer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const it = MakeIterator(this.props.MainContainer);
|
||||
for (const container of it) {
|
||||
handleContainer(container);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
let isOpenClasses = '-right-64';
|
||||
if (this.props.isOpen) {
|
||||
isOpenClasses = this.props.isHistoryOpen
|
||||
? 'right-64'
|
||||
: 'right-0';
|
||||
}
|
||||
|
||||
const containerRows: React.ReactNode[] = [];
|
||||
this.iterateChilds((container: IContainerModel) => {
|
||||
function createRows(
|
||||
container: IContainerModel,
|
||||
props: IElementsSidebarProps,
|
||||
containerRows: React.ReactNode[]
|
||||
): void {
|
||||
const depth: number = getDepth(container);
|
||||
const key = container.properties.id.toString();
|
||||
const text = '|\t'.repeat(depth) + key;
|
||||
const selectedClass: string = this.props.SelectedContainer !== undefined &&
|
||||
this.props.SelectedContainer !== null &&
|
||||
this.props.SelectedContainer.properties.id === container.properties.id
|
||||
const selectedClass: string = props.SelectedContainer !== undefined &&
|
||||
props.SelectedContainer !== null &&
|
||||
props.SelectedContainer.properties.id === container.properties.id
|
||||
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
|
||||
: 'bg-slate-300/60 hover:bg-slate-300';
|
||||
|
||||
containerRows.push(
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
|
@ -220,45 +48,102 @@ export class ElementsSidebar extends React.PureComponent<IElementsSidebarProps>
|
|||
}
|
||||
id={key}
|
||||
key={key}
|
||||
onDrop={(event) => this.handleOnDrop(event)}
|
||||
onDragOver={(event) => this.handleDragOver(event)}
|
||||
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
|
||||
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
|
||||
onDragLeave={(event) => handleDragLeave(event)}
|
||||
onClick={() => this.props.SelectContainer(container)}
|
||||
onClick={() => props.SelectContainer(container)}
|
||||
>
|
||||
{ text }
|
||||
</motion.button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElementsSidebarProps): JSX.Element => {
|
||||
// States
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
||||
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
|
||||
const [contextMenuPosition, setContextMenuPosition] = React.useState<Point>({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
const elementRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Event listeners
|
||||
React.useEffect(() => {
|
||||
elementRef.current?.addEventListener(
|
||||
'contextmenu',
|
||||
(event) => handleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
));
|
||||
|
||||
window.addEventListener(
|
||||
'click',
|
||||
(event) => handleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId
|
||||
));
|
||||
|
||||
return () => {
|
||||
elementRef.current?.addEventListener(
|
||||
'contextmenu',
|
||||
(event) => handleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
));
|
||||
|
||||
window.removeEventListener(
|
||||
'click',
|
||||
(event) => handleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId
|
||||
));
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Render
|
||||
let isOpenClasses = '-right-64';
|
||||
if (props.isOpen) {
|
||||
isOpenClasses = props.isHistoryOpen
|
||||
? 'right-64'
|
||||
: 'right-0';
|
||||
}
|
||||
|
||||
const containerRows: React.ReactNode[] = [];
|
||||
|
||||
const it = MakeIterator(props.MainContainer);
|
||||
for (const container of it) {
|
||||
createRows(
|
||||
container,
|
||||
props,
|
||||
containerRows
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-screen w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
|
||||
<div className='bg-slate-100 font-bold sidebar-title'>
|
||||
Elements
|
||||
</div>
|
||||
<div ref={this.elementRef} className='overflow-y-auto overflow-x-hidden text-gray-800 flex-grow'>
|
||||
<div ref={elementRef} className='overflow-y-auto overflow-x-hidden text-gray-800 flex-grow'>
|
||||
{ containerRows }
|
||||
</div>
|
||||
<Menu
|
||||
className='transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl'
|
||||
x={this.state.contextMenuPosition.x}
|
||||
y={this.state.contextMenuPosition.y}
|
||||
isOpen={this.state.isContextMenuOpen}
|
||||
x={contextMenuPosition.x}
|
||||
y={contextMenuPosition.y}
|
||||
isOpen={isContextMenuOpen}
|
||||
>
|
||||
<MenuItem className='contextmenu-item' text='Delete' onClick={() => this.props.DeleteContainer(this.state.onClickContainerId)} />
|
||||
<MenuItem className='contextmenu-item' text='Delete' onClick={() => props.DeleteContainer(onClickContainerId)} />
|
||||
</Menu>
|
||||
<Properties properties={this.props.SelectedContainer?.properties} onChange={this.props.onPropertyChange}></Properties>
|
||||
<Properties properties={props.SelectedContainer?.properties} onChange={props.onPropertyChange}></Properties>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function removeBorderClasses(target: HTMLButtonElement): void {
|
||||
target.classList.remove('border-t-8');
|
||||
target.classList.remove('border-8');
|
||||
target.classList.remove('border-b-8');
|
||||
}
|
||||
|
||||
function handleDragLeave(event: React.DragEvent): void {
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
removeBorderClasses(target);
|
||||
}
|
||||
};
|
||||
|
|
137
src/Components/ElementsSidebar/MouseEventHandlers.ts
Normal file
137
src/Components/ElementsSidebar/MouseEventHandlers.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import { IContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { Point } from '../../Interfaces/Point';
|
||||
import { findContainerById } from '../../utils/itertools';
|
||||
|
||||
export function handleRightClick(
|
||||
event: MouseEvent,
|
||||
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
|
||||
setContextMenuPosition: React.Dispatch<React.SetStateAction<Point>>
|
||||
): void {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(event.target instanceof HTMLButtonElement)) {
|
||||
setIsContextMenuOpen(false);
|
||||
setOnClickContainerId('');
|
||||
return;
|
||||
}
|
||||
|
||||
const contextMenuPosition: Point = { x: event.pageX, y: event.pageY };
|
||||
setIsContextMenuOpen(true);
|
||||
setOnClickContainerId(event.target.id);
|
||||
setContextMenuPosition(contextMenuPosition);
|
||||
}
|
||||
|
||||
export function handleLeftClick(
|
||||
isContextMenuOpen: boolean,
|
||||
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
|
||||
): void {
|
||||
if (!isContextMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContextMenuOpen(false);
|
||||
setOnClickContainerId('');
|
||||
}
|
||||
|
||||
export function removeBorderClasses(target: HTMLButtonElement): void {
|
||||
target.classList.remove('border-t-8');
|
||||
target.classList.remove('border-8');
|
||||
target.classList.remove('border-b-8');
|
||||
}
|
||||
|
||||
export function handleDragLeave(event: React.DragEvent): void {
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
removeBorderClasses(target);
|
||||
}
|
||||
|
||||
export function handleDragOver(
|
||||
event: React.DragEvent,
|
||||
mainContainer: IContainerModel
|
||||
): void {
|
||||
event.preventDefault();
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top; // y position within the element.
|
||||
|
||||
if (target.id === mainContainer.properties.id) {
|
||||
target.classList.add('border-8');
|
||||
target.classList.remove('border-t-8');
|
||||
target.classList.remove('border-b-8');
|
||||
return;
|
||||
}
|
||||
|
||||
if (y < 12) {
|
||||
target.classList.add('border-t-8');
|
||||
target.classList.remove('border-b-8');
|
||||
target.classList.remove('border-8');
|
||||
} else if (y < 24) {
|
||||
target.classList.add('border-8');
|
||||
target.classList.remove('border-t-8');
|
||||
target.classList.remove('border-b-8');
|
||||
} else {
|
||||
target.classList.add('border-b-8');
|
||||
target.classList.remove('border-8');
|
||||
target.classList.remove('border-t-8');
|
||||
}
|
||||
}
|
||||
|
||||
export function handleOnDrop(
|
||||
event: React.DragEvent,
|
||||
mainContainer: IContainerModel,
|
||||
addContainer: (index: number, type: string, parent: string) => void
|
||||
): void {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer.getData('type');
|
||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||
removeBorderClasses(target);
|
||||
|
||||
const targetContainer: IContainerModel | undefined = findContainerById(
|
||||
mainContainer,
|
||||
target.id
|
||||
);
|
||||
|
||||
if (targetContainer === undefined) {
|
||||
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
|
||||
}
|
||||
|
||||
if (targetContainer === mainContainer) {
|
||||
// if the container is the root, only add type as child
|
||||
addContainer(
|
||||
targetContainer.children.length,
|
||||
type,
|
||||
targetContainer.properties.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetContainer.parent === null ||
|
||||
targetContainer.parent === undefined) {
|
||||
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
|
||||
}
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const y = event.clientY - rect.top; // y position within the element.
|
||||
|
||||
// locate the hitboxes
|
||||
if (y < 12) {
|
||||
const index = targetContainer.parent.children.indexOf(targetContainer);
|
||||
addContainer(
|
||||
index,
|
||||
type,
|
||||
targetContainer.parent.properties.id
|
||||
);
|
||||
} else if (y < 24) {
|
||||
addContainer(
|
||||
targetContainer.children.length,
|
||||
type,
|
||||
targetContainer.properties.id);
|
||||
} else {
|
||||
const index = targetContainer.parent.children.indexOf(targetContainer);
|
||||
addContainer(
|
||||
index + 1,
|
||||
type,
|
||||
targetContainer.parent.properties.id
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ const toggleState = (
|
|||
setHidden(!isHidden);
|
||||
};
|
||||
|
||||
const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingButtonProps) => {
|
||||
export const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingButtonProps) => {
|
||||
const [isHidden, setHidden] = React.useState(true);
|
||||
const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100';
|
||||
const icon = isHidden
|
||||
|
@ -34,5 +34,3 @@ const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingButtonPr
|
|||
</button>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default FloatingButton;
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { IHistoryState } from '../../App';
|
||||
import { HistoryState } from "../../Interfaces/HistoryState";
|
||||
|
||||
interface IHistoryProps {
|
||||
history: IHistoryState[]
|
||||
history: HistoryState[]
|
||||
historyCurrentStep: number
|
||||
isOpen: boolean
|
||||
jumpTo: (move: number) => void
|
||||
}
|
||||
|
||||
export class History extends React.PureComponent<IHistoryProps> {
|
||||
public render(): JSX.Element {
|
||||
const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64';
|
||||
export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
|
||||
const isOpenClasses = props.isOpen ? 'right-0' : '-right-64';
|
||||
|
||||
const states = this.props.history.map((step, move) => {
|
||||
const states = props.history.map((step, move) => {
|
||||
const desc = move > 0
|
||||
? `Go to modification n°${move}`
|
||||
: 'Go to the beginning';
|
||||
|
||||
const isCurrent = move === this.props.historyCurrentStep;
|
||||
const isCurrent = move === props.historyCurrentStep;
|
||||
|
||||
const selectedClass = isCurrent
|
||||
? 'bg-blue-500 hover:bg-blue-600'
|
||||
|
@ -30,7 +29,7 @@ export class History extends React.PureComponent<IHistoryProps> {
|
|||
|
||||
<button
|
||||
key={move}
|
||||
onClick={() => this.props.jumpTo(move)}
|
||||
onClick={() => props.jumpTo(move)}
|
||||
className={
|
||||
`w-full elements-sidebar-row whitespace-pre
|
||||
text-left text-sm font-medium transition-all ${selectedClass}`
|
||||
|
@ -54,5 +53,4 @@ export class History extends React.PureComponent<IHistoryProps> {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -36,18 +36,6 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
|
|||
"/>
|
||||
</label>
|
||||
</form>
|
||||
{/* <button
|
||||
onClick={() => setWindowState(WindowState.MAIN)}
|
||||
className='block text-sm
|
||||
mt-8 py-4 px-4
|
||||
rounded-full border-0
|
||||
font-semibold
|
||||
transition-all
|
||||
bg-blue-100 text-blue-700
|
||||
hover:bg-blue-200'
|
||||
>
|
||||
Load
|
||||
</button> */}
|
||||
<button
|
||||
onClick={() => setWindowState(WindowState.MAIN)}
|
||||
className='block text-sm
|
||||
|
|
|
@ -6,27 +6,27 @@ interface IPropertiesProps {
|
|||
onChange: (key: string, value: string) => void
|
||||
}
|
||||
|
||||
export class Properties extends React.PureComponent<IPropertiesProps> {
|
||||
public render(): JSX.Element {
|
||||
if (this.props.properties === undefined) {
|
||||
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
|
||||
if (props.properties === undefined) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const groupInput: React.ReactNode[] = [];
|
||||
Object
|
||||
.entries(this.props.properties)
|
||||
.forEach((pair) => this.handleProperties(pair, groupInput));
|
||||
.entries(props.properties)
|
||||
.forEach((pair) => handleProperties(pair, groupInput, props.onChange));
|
||||
|
||||
return (
|
||||
<div className='p-3 bg-slate-200 h-3/5 overflow-y-auto'>
|
||||
{ groupInput }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public handleProperties = (
|
||||
const handleProperties = (
|
||||
[key, value]: [string, string | number],
|
||||
groupInput: React.ReactNode[]
|
||||
groupInput: React.ReactNode[],
|
||||
onChange: (key: string, value: string) => void
|
||||
): void => {
|
||||
const id = `property-${key}`;
|
||||
const type = 'text';
|
||||
|
@ -43,10 +43,9 @@ export class Properties extends React.PureComponent<IPropertiesProps> {
|
|||
type={type}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(event) => this.props.onChange(key, event.target.value)}
|
||||
onChange={(event) => onChange(key, event.target.value)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,16 +9,15 @@ export interface IContainerProps {
|
|||
|
||||
const GAP = 50;
|
||||
|
||||
export class Container extends React.PureComponent<IContainerProps> {
|
||||
/**
|
||||
* Render the container
|
||||
* @returns Render the container
|
||||
*/
|
||||
public render(): React.ReactNode {
|
||||
const containersElements = this.props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
|
||||
const xText = Number(this.props.model.properties.width) / 2;
|
||||
const yText = Number(this.props.model.properties.height) / 2;
|
||||
const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`;
|
||||
export const Container: React.FC<IContainerProps> = (props: IContainerProps) => {
|
||||
const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
|
||||
const xText = Number(props.model.properties.width) / 2;
|
||||
const yText = Number(props.model.properties.height) / 2;
|
||||
const transform = `translate(${Number(props.model.properties.x)}, ${Number(props.model.properties.y)})`;
|
||||
|
||||
// g style
|
||||
const defaultStyle: React.CSSProperties = {
|
||||
|
@ -30,7 +29,7 @@ export class Container extends React.PureComponent<IContainerProps> {
|
|||
// Rect style
|
||||
const style = Object.assign(
|
||||
JSON.parse(JSON.stringify(defaultStyle)),
|
||||
this.props.model.properties
|
||||
props.model.properties
|
||||
);
|
||||
style.x = 0;
|
||||
style.y = 0;
|
||||
|
@ -38,18 +37,18 @@ export class Container extends React.PureComponent<IContainerProps> {
|
|||
delete style.width;
|
||||
|
||||
// Dimension props
|
||||
const id = `dim-${this.props.model.properties.id}`;
|
||||
const id = `dim-${props.model.properties.id}`;
|
||||
const xStart: number = 0;
|
||||
const xEnd = Number(this.props.model.properties.width);
|
||||
const y = -(GAP * (getDepth(this.props.model) + 1));
|
||||
const xEnd = Number(props.model.properties.width);
|
||||
const y = -(GAP * (getDepth(props.model) + 1));
|
||||
const strokeWidth = 1;
|
||||
const text = (this.props.model.properties.width ?? 0).toString();
|
||||
const text = (props.model.properties.width ?? 0).toString();
|
||||
|
||||
return (
|
||||
<g
|
||||
style={defaultStyle}
|
||||
transform={transform}
|
||||
key={`container-${this.props.model.properties.id}`}
|
||||
key={`container-${props.model.properties.id}`}
|
||||
>
|
||||
<Dimension
|
||||
id={id}
|
||||
|
@ -60,8 +59,8 @@ export class Container extends React.PureComponent<IContainerProps> {
|
|||
text={text}
|
||||
/>
|
||||
<rect
|
||||
width={this.props.model.properties.width}
|
||||
height={this.props.model.properties.height}
|
||||
width={props.model.properties.width}
|
||||
height={props.model.properties.height}
|
||||
style={style}
|
||||
>
|
||||
</rect>
|
||||
|
@ -69,10 +68,9 @@ export class Container extends React.PureComponent<IContainerProps> {
|
|||
x={xText}
|
||||
y={yText}
|
||||
>
|
||||
{this.props.model.properties.id}
|
||||
{props.model.properties.id}
|
||||
</text>
|
||||
{ containersElements }
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,44 +9,42 @@ interface IDimensionProps {
|
|||
strokeWidth: number
|
||||
}
|
||||
|
||||
export class Dimension extends React.PureComponent<IDimensionProps> {
|
||||
public render(): JSX.Element {
|
||||
export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) => {
|
||||
const style: React.CSSProperties = {
|
||||
stroke: 'black'
|
||||
};
|
||||
return (
|
||||
<g key={this.props.id}>
|
||||
<g key={props.id}>
|
||||
<line
|
||||
x1={this.props.xStart}
|
||||
y1={this.props.y - 4 * this.props.strokeWidth}
|
||||
x2={this.props.xStart}
|
||||
y2={this.props.y + 4 * this.props.strokeWidth}
|
||||
strokeWidth={this.props.strokeWidth}
|
||||
x1={props.xStart}
|
||||
y1={props.y - 4 * props.strokeWidth}
|
||||
x2={props.xStart}
|
||||
y2={props.y + 4 * props.strokeWidth}
|
||||
strokeWidth={props.strokeWidth}
|
||||
style={style}
|
||||
/>
|
||||
<line
|
||||
x1={this.props.xStart}
|
||||
y1={this.props.y}
|
||||
x2={this.props.xEnd}
|
||||
y2={this.props.y}
|
||||
strokeWidth={this.props.strokeWidth}
|
||||
x1={props.xStart}
|
||||
y1={props.y}
|
||||
x2={props.xEnd}
|
||||
y2={props.y}
|
||||
strokeWidth={props.strokeWidth}
|
||||
style={style}
|
||||
/>
|
||||
<line
|
||||
x1={this.props.xEnd}
|
||||
y1={this.props.y - 4 * this.props.strokeWidth}
|
||||
x2={this.props.xEnd}
|
||||
y2={this.props.y + 4 * this.props.strokeWidth}
|
||||
strokeWidth={this.props.strokeWidth}
|
||||
x1={props.xEnd}
|
||||
y1={props.y - 4 * props.strokeWidth}
|
||||
x2={props.xEnd}
|
||||
y2={props.y + 4 * props.strokeWidth}
|
||||
strokeWidth={props.strokeWidth}
|
||||
style={style}
|
||||
/>
|
||||
<text
|
||||
x={(this.props.xStart + this.props.xEnd) / 2}
|
||||
y={this.props.y}
|
||||
x={(props.xStart + props.xEnd) / 2}
|
||||
y={props.y}
|
||||
>
|
||||
{this.props.text}
|
||||
{props.text}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -12,59 +12,55 @@ interface ISVGProps {
|
|||
selected: ContainerModel | null
|
||||
}
|
||||
|
||||
interface ISVGState {
|
||||
interface Viewer {
|
||||
viewerWidth: number
|
||||
viewerHeight: number
|
||||
}
|
||||
|
||||
export class SVG extends React.PureComponent<ISVGProps> {
|
||||
public static ID = 'svg';
|
||||
public state: ISVGState;
|
||||
export const ID = 'svg';
|
||||
|
||||
constructor(props: ISVGProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
viewerWidth: window.innerWidth - BAR_WIDTH,
|
||||
viewerHeight: window.innerHeight
|
||||
};
|
||||
}
|
||||
|
||||
resizeViewBox(): void {
|
||||
this.setState({
|
||||
function resizeViewBox(
|
||||
setViewer: React.Dispatch<React.SetStateAction<Viewer>>
|
||||
): void {
|
||||
setViewer({
|
||||
viewerWidth: window.innerWidth - BAR_WIDTH,
|
||||
viewerHeight: window.innerHeight
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener('resize', () => this.resizeViewBox());
|
||||
}
|
||||
export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
|
||||
const [viewer, setViewer] = React.useState<Viewer>({
|
||||
viewerWidth: window.innerWidth,
|
||||
viewerHeight: window.innerHeight
|
||||
});
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener('resize', () => this.resizeViewBox());
|
||||
}
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('resize', () => resizeViewBox(setViewer));
|
||||
|
||||
return () => {
|
||||
window.addEventListener('resize', () => resizeViewBox(setViewer));
|
||||
};
|
||||
});
|
||||
|
||||
render(): JSX.Element {
|
||||
const xmlns = '<http://www.w3.org/2000/svg>';
|
||||
|
||||
const properties = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
xmlns
|
||||
};
|
||||
|
||||
let children: React.ReactNode | React.ReactNode[] = [];
|
||||
if (Array.isArray(this.props.children)) {
|
||||
children = this.props.children.map(child => <Container key={`container-${child.properties.id}`} model={child}/>);
|
||||
} else if (this.props.children !== null) {
|
||||
children = <Container key={`container-${this.props.children.properties.id}`} model={this.props.children}/>;
|
||||
if (Array.isArray(props.children)) {
|
||||
children = props.children.map(child => <Container key={`container-${child.properties.id}`} model={child}/>);
|
||||
} else if (props.children !== null) {
|
||||
children = <Container key={`container-${props.children.properties.id}`} model={props.children}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={SVG.ID} className='ml-16'>
|
||||
<div id={ID} className='ml-16'>
|
||||
<UncontrolledReactSVGPanZoom
|
||||
width={this.state.viewerWidth}
|
||||
height={this.state.viewerHeight}
|
||||
width={viewer.viewerWidth}
|
||||
height={viewer.viewerHeight}
|
||||
background={'#ffffff'}
|
||||
defaultTool='pan'
|
||||
miniatureProps={{
|
||||
|
@ -76,10 +72,9 @@ export class SVG extends React.PureComponent<ISVGProps> {
|
|||
>
|
||||
<svg {...properties}>
|
||||
{ children }
|
||||
<Selector selected={this.props.selected} />
|
||||
<Selector selected={props.selected} />
|
||||
</svg>
|
||||
</UncontrolledReactSVGPanZoom>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fireEvent, render, screen } from '../../utils/test-utils';
|
||||
import Sidebar from './Sidebar';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
describe.concurrent('Sidebar', () => {
|
||||
it('Start default', () => {
|
||||
|
|
|
@ -12,15 +12,14 @@ function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
|
|||
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
|
||||
}
|
||||
|
||||
export default class Sidebar extends React.PureComponent<ISidebarProps> {
|
||||
public render(): JSX.Element {
|
||||
const listElements = this.props.componentOptions.map(componentOption =>
|
||||
export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => {
|
||||
const listElements = props.componentOptions.map(componentOption =>
|
||||
<button
|
||||
className='justify-center transition-all sidebar-component'
|
||||
key={componentOption.Type}
|
||||
id={componentOption.Type}
|
||||
title={componentOption.Type}
|
||||
onClick={() => this.props.buttonOnClick(componentOption.Type)}
|
||||
onClick={() => props.buttonOnClick(componentOption.Type)}
|
||||
draggable={true}
|
||||
onDragStart={(event) => handleDragStart(event)}
|
||||
>
|
||||
|
@ -28,7 +27,7 @@ export default class Sidebar extends React.PureComponent<ISidebarProps> {
|
|||
</button>
|
||||
);
|
||||
|
||||
const isOpenClasses = this.props.isOpen ? 'left-16' : '-left-64';
|
||||
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
|
||||
return (
|
||||
<div className={`fixed z-10 bg-slate-200
|
||||
text-gray-700 transition-all h-screen w-64
|
||||
|
@ -42,5 +41,4 @@ export default class Sidebar extends React.PureComponent<ISidebarProps> {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
|
||||
import Sidebar from '../Sidebar/Sidebar';
|
||||
import { Sidebar } from '../Sidebar/Sidebar';
|
||||
import { History } from '../History/History';
|
||||
import { AvailableContainer } from '../../Interfaces/AvailableContainer';
|
||||
import { ContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { IHistoryState } from '../../App';
|
||||
import { HistoryState } from "../../Interfaces/HistoryState";
|
||||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||
import FloatingButton from '../FloatingButton/FloatingButton';
|
||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||
import { Bar } from '../Bar/Bar';
|
||||
|
||||
interface IUIProps {
|
||||
current: IHistoryState
|
||||
history: IHistoryState[]
|
||||
current: HistoryState
|
||||
history: HistoryState[]
|
||||
historyCurrentStep: number
|
||||
AvailableContainers: AvailableContainer[]
|
||||
SelectContainer: (container: ContainerModel) => void
|
||||
|
@ -24,108 +24,70 @@ interface IUIProps {
|
|||
LoadState: (move: number) => void
|
||||
}
|
||||
|
||||
interface IUIState {
|
||||
isSidebarOpen: boolean
|
||||
isElementsSidebarOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
}
|
||||
export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||
const [isElementsSidebarOpen, setIsElementsSidebarOpen] = React.useState(false);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||
|
||||
export class UI extends React.PureComponent<IUIProps, IUIState> {
|
||||
constructor(props: IUIProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSidebarOpen: true,
|
||||
isElementsSidebarOpen: false,
|
||||
isHistoryOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the components sidebar
|
||||
*/
|
||||
public ToggleSidebar(): void {
|
||||
this.setState({
|
||||
isSidebarOpen: !this.state.isSidebarOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the elements
|
||||
*/
|
||||
public ToggleElementsSidebar(): void {
|
||||
this.setState({
|
||||
isElementsSidebarOpen: !this.state.isElementsSidebarOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the elements
|
||||
*/
|
||||
public ToggleTimeline(): void {
|
||||
this.setState({
|
||||
isHistoryOpen: !this.state.isHistoryOpen
|
||||
});
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
let buttonRightOffsetClasses = 'right-12';
|
||||
if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) {
|
||||
if (isElementsSidebarOpen || isHistoryOpen) {
|
||||
buttonRightOffsetClasses = 'right-72';
|
||||
}
|
||||
if (this.state.isHistoryOpen && this.state.isElementsSidebarOpen) {
|
||||
if (isHistoryOpen && isElementsSidebarOpen) {
|
||||
buttonRightOffsetClasses = 'right-[544px]';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Bar
|
||||
isSidebarOpen={this.state.isSidebarOpen}
|
||||
isElementsSidebarOpen={this.state.isElementsSidebarOpen}
|
||||
isHistoryOpen={this.state.isHistoryOpen}
|
||||
ToggleElementsSidebar={() => this.ToggleElementsSidebar()}
|
||||
ToggleSidebar={() => this.ToggleSidebar()}
|
||||
ToggleTimeline={() => this.ToggleTimeline()}
|
||||
isSidebarOpen={isSidebarOpen}
|
||||
isElementsSidebarOpen={isElementsSidebarOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
ToggleElementsSidebar={() => setIsElementsSidebarOpen(!isElementsSidebarOpen)}
|
||||
ToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||
/>
|
||||
|
||||
<Sidebar
|
||||
componentOptions={this.props.AvailableContainers}
|
||||
isOpen={this.state.isSidebarOpen}
|
||||
buttonOnClick={(type: string) => this.props.AddContainerToSelectedContainer(type)}
|
||||
componentOptions={props.AvailableContainers}
|
||||
isOpen={isSidebarOpen}
|
||||
buttonOnClick={(type: string) => props.AddContainerToSelectedContainer(type)}
|
||||
/>
|
||||
<ElementsSidebar
|
||||
MainContainer={this.props.current.MainContainer}
|
||||
SelectedContainer={this.props.current.SelectedContainer}
|
||||
isOpen={this.state.isElementsSidebarOpen}
|
||||
isHistoryOpen={this.state.isHistoryOpen}
|
||||
onPropertyChange={this.props.OnPropertyChange}
|
||||
SelectContainer={this.props.SelectContainer}
|
||||
DeleteContainer={this.props.DeleteContainer}
|
||||
AddContainer={this.props.AddContainer}
|
||||
MainContainer={props.current.MainContainer}
|
||||
SelectedContainer={props.current.SelectedContainer}
|
||||
isOpen={isElementsSidebarOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
onPropertyChange={props.OnPropertyChange}
|
||||
SelectContainer={props.SelectContainer}
|
||||
DeleteContainer={props.DeleteContainer}
|
||||
AddContainer={props.AddContainer}
|
||||
/>
|
||||
<History
|
||||
history={this.props.history}
|
||||
historyCurrentStep={this.props.historyCurrentStep}
|
||||
isOpen={this.state.isHistoryOpen}
|
||||
jumpTo={this.props.LoadState}
|
||||
history={props.history}
|
||||
historyCurrentStep={props.historyCurrentStep}
|
||||
isOpen={isHistoryOpen}
|
||||
jumpTo={props.LoadState}
|
||||
/>
|
||||
|
||||
<FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
|
||||
<button
|
||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||
title='Export as JSON'
|
||||
onClick={this.props.SaveEditorAsJSON}
|
||||
onClick={props.SaveEditorAsJSON}
|
||||
>
|
||||
<UploadIcon className="heroicon text-white" />
|
||||
</button>
|
||||
<button
|
||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||
title='Export as SVG'
|
||||
onClick={this.props.SaveEditorAsSVG}
|
||||
onClick={props.SaveEditorAsSVG}
|
||||
>
|
||||
<PhotographIcon className="heroicon text-white" />
|
||||
</button>
|
||||
</FloatingButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default UI;
|
||||
|
|
362
src/Editor.tsx
362
src/Editor.tsx
|
@ -1,362 +0,0 @@
|
|||
import React from 'react';
|
||||
import './Editor.scss';
|
||||
import { Configuration } from './Interfaces/Configuration';
|
||||
import { SVG } from './Components/SVG/SVG';
|
||||
import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel';
|
||||
import { findContainerById, MakeIterator } from './utils/itertools';
|
||||
import { IHistoryState } from './App';
|
||||
import { getCircularReplacer } from './utils/saveload';
|
||||
import { UI } from './Components/UI/UI';
|
||||
|
||||
interface IEditorProps {
|
||||
configuration: Configuration
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
}
|
||||
|
||||
export interface IEditorState {
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
// do not use it, use props.configuration
|
||||
// only used for serialization purpose
|
||||
configuration: Configuration
|
||||
}
|
||||
|
||||
class Editor extends React.Component<IEditorProps> {
|
||||
public state: IEditorState;
|
||||
|
||||
constructor(props: IEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
configuration: Object.assign({}, props.configuration),
|
||||
history: [...props.history],
|
||||
historyCurrentStep: props.historyCurrentStep
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener('keyup', (event) => this.onKeyDown(event));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener('keyup', (event) => this.onKeyDown(event));
|
||||
}
|
||||
|
||||
public onKeyDown(event: KeyboardEvent): void {
|
||||
event.preventDefault();
|
||||
if (event.isComposing || event.keyCode === 229) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'z' &&
|
||||
event.ctrlKey &&
|
||||
this.state.historyCurrentStep > 0) {
|
||||
this.LoadState(this.state.historyCurrentStep - 1);
|
||||
} else if (event.key === 'y' &&
|
||||
event.ctrlKey &&
|
||||
this.state.historyCurrentStep < this.state.history.length - 1) {
|
||||
this.LoadState(this.state.historyCurrentStep + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1);
|
||||
public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep];
|
||||
|
||||
/**
|
||||
* Select a container
|
||||
* @param container Selected container
|
||||
*/
|
||||
public SelectContainer(container: ContainerModel): void {
|
||||
const history = this.getCurrentHistory();
|
||||
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!');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
history: history.concat([{
|
||||
MainContainer: mainContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||
SelectedContainer,
|
||||
SelectedContainerId: SelectedContainer.properties.id
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteContainer(containerId: string): void {
|
||||
const history = this.getCurrentHistory();
|
||||
const current = history[this.state.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);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
history: history.concat([{
|
||||
SelectedContainer: null,
|
||||
SelectedContainerId: '',
|
||||
MainContainer: mainContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handled the property change event in the properties form
|
||||
* @param key Property name
|
||||
* @param value New value of the property
|
||||
* @returns void
|
||||
*/
|
||||
public OnPropertyChange(key: string, value: string | number): void {
|
||||
const history = this.getCurrentHistory();
|
||||
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;
|
||||
this.setState({
|
||||
history: history.concat([{
|
||||
SelectedContainer: selectedContainerClone,
|
||||
SelectedContainerId: selectedContainerClone.properties.id,
|
||||
MainContainer: selectedContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]),
|
||||
historyCurrentStep: 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;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
history: history.concat([{
|
||||
SelectedContainer: container,
|
||||
SelectedContainerId: container.properties.id,
|
||||
MainContainer: mainContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new container to a selected container
|
||||
* @param type The type of container
|
||||
* @returns void
|
||||
*/
|
||||
public AddContainerToSelectedContainer(type: string): void {
|
||||
const history = this.getCurrentHistory();
|
||||
const current = history[history.length - 1];
|
||||
|
||||
if (current.SelectedContainer === null ||
|
||||
current.SelectedContainer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = current.SelectedContainer;
|
||||
this.AddContainer(parent.children.length, type, parent.properties.id);
|
||||
}
|
||||
|
||||
public AddContainer(index: number, type: string, parentId: string): void {
|
||||
const history = this.getCurrentHistory();
|
||||
const current = history[history.length - 1];
|
||||
|
||||
if (current.MainContainer === null ||
|
||||
current.MainContainer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the preset properties from the API
|
||||
const properties = this.props.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
|
||||
this.setState({
|
||||
history: history.concat([{
|
||||
MainContainer: clone,
|
||||
TypeCounters: newCounters,
|
||||
SelectedContainer: parentClone,
|
||||
SelectedContainerId: parentClone.properties.id
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
});
|
||||
}
|
||||
|
||||
public LoadState(move: number): void {
|
||||
this.setState({
|
||||
historyCurrentStep: move
|
||||
});
|
||||
}
|
||||
|
||||
public SaveEditorAsJSON(): void {
|
||||
const exportName = 'state';
|
||||
const spaces = import.meta.env.DEV ? 4 : 0;
|
||||
const data = JSON.stringify(this.state, 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();
|
||||
}
|
||||
|
||||
public SaveEditorAsSVG(): void {
|
||||
const svgWrapper = document.getElementById(SVG.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the application
|
||||
* @returns {JSX.Element} Rendered JSX element
|
||||
*/
|
||||
render(): JSX.Element {
|
||||
const current = this.getCurrentHistoryState();
|
||||
return (
|
||||
<div className="App font-sans h-full">
|
||||
<UI
|
||||
current={current}
|
||||
history={this.state.history}
|
||||
historyCurrentStep={this.state.historyCurrentStep}
|
||||
AvailableContainers={this.state.configuration.AvailableContainers}
|
||||
SelectContainer={(container) => this.SelectContainer(container)}
|
||||
DeleteContainer={(containerId: string) => this.DeleteContainer(containerId)}
|
||||
OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)}
|
||||
AddContainerToSelectedContainer={(type) => this.AddContainerToSelectedContainer(type)}
|
||||
AddContainer={(index, type, parentId) => this.AddContainer(index, type, parentId)}
|
||||
SaveEditorAsJSON={() => this.SaveEditorAsJSON()}
|
||||
SaveEditorAsSVG={() => this.SaveEditorAsSVG()}
|
||||
LoadState={(move) => this.LoadState(move)}
|
||||
/>
|
||||
<SVG
|
||||
width={Number(current.MainContainer?.properties.width)}
|
||||
height={Number(current.MainContainer?.properties.height)}
|
||||
selected={current.SelectedContainer}
|
||||
>
|
||||
{ current.MainContainer }
|
||||
</SVG>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Editor;
|
8
src/Interfaces/HistoryState.ts
Normal file
8
src/Interfaces/HistoryState.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { IContainerModel } from './ContainerModel';
|
||||
|
||||
export interface HistoryState {
|
||||
MainContainer: IContainerModel
|
||||
SelectedContainer: IContainerModel | null
|
||||
SelectedContainerId: string
|
||||
TypeCounters: Record<string, number>
|
||||
}
|
4
src/Interfaces/Point.ts
Normal file
4
src/Interfaces/Point.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import { App } from './Components/App/App';
|
||||
import './index.scss';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
|
|
37
src/utils/default.ts
Normal file
37
src/utils/default.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Configuration } from '../Interfaces/Configuration';
|
||||
import Properties from '../Interfaces/Properties';
|
||||
|
||||
export const DEFAULT_CONFIG: Configuration = {
|
||||
AvailableContainers: [
|
||||
{
|
||||
Type: 'Container',
|
||||
Width: 75,
|
||||
Height: 100,
|
||||
Style: {
|
||||
fillOpacity: 0,
|
||||
stroke: 'green'
|
||||
}
|
||||
}
|
||||
],
|
||||
AvailableSymbols: [],
|
||||
MainContainer: {
|
||||
Type: 'Container',
|
||||
Width: 2000,
|
||||
Height: 100,
|
||||
Style: {
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const DEFAULT_MAINCONTAINER_PROPS: Properties = {
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: DEFAULT_CONFIG.MainContainer.Width,
|
||||
height: DEFAULT_CONFIG.MainContainer.Height,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import { findContainerById, MakeIterator } from './itertools';
|
||||
import { IEditorState } from '../Editor';
|
||||
import { IEditorState } from '../Components/Editor/Editor';
|
||||
|
||||
/**
|
||||
* Revive the Editor state
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue