Extract Editor from App
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Siklos 2022-08-04 14:53:49 +02:00
parent 72dfb4f9bb
commit 340cc86aa9
5 changed files with 373 additions and 314 deletions

View file

@ -1,24 +1,6 @@
html, html,
body, body,
#root, #root {
svg {
height: 100%;
width: 100%; width: 100%;
} height: 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;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
} }

View file

@ -1,16 +1,8 @@
import React from 'react'; import * as React from 'react';
import './App.scss'; import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel';
import Sidebar from './Components/Sidebar/Sidebar'; import Editor from './Editor';
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
import { AvailableContainer } from './Interfaces/AvailableContainer'; import { AvailableContainer } from './Interfaces/AvailableContainer';
import { Configuration } from './Interfaces/Configuration'; import { Configuration } from './Interfaces/Configuration';
import { SVG } from './Components/SVG/SVG';
import { History } from './Components/History/History';
import { ContainerModel, IContainerModel, MakeIterator } from './Components/SVG/Elements/ContainerModel';
import Properties from './Interfaces/Properties';
interface IAppProps {
}
export interface IHistoryState { export interface IHistoryState {
MainContainer: IContainerModel | null, MainContainer: IContainerModel | null,
@ -18,24 +10,35 @@ export interface IHistoryState {
TypeCounters: Record<string, number> TypeCounters: Record<string, number>
} }
interface IAppState { interface IAppProps {
isSidebarOpen: boolean,
isSVGSidebarOpen: boolean,
isHistoryOpen: boolean,
configuration: Configuration,
history: Array<IHistoryState>,
historyCurrentStep: 0
} }
class App extends React.Component<IAppProps> { interface IAppState {
configuration: Configuration,
history: IHistoryState[],
historyCurrentStep: number,
isLoaded: boolean
}
export class App extends React.Component<IAppProps> {
public state: IAppState; public state: IAppState;
constructor(props: IAppProps) { public constructor(props: IAppProps) {
super(props); super(props);
const MainContainer = new ContainerModel(
null,
{
id: 'main',
x: 0,
y: 0,
width: 1000,
height: 1000,
fillOpacity: 0,
stroke: 'black'
}
);
this.state = { this.state = {
isSidebarOpen: true,
isSVGSidebarOpen: false,
isHistoryOpen: false,
configuration: { configuration: {
AvailableContainers: [], AvailableContainers: [],
AvailableSymbols: [], AvailableSymbols: [],
@ -43,18 +46,16 @@ class App extends React.Component<IAppProps> {
}, },
history: [ history: [
{ {
MainContainer: null, MainContainer,
SelectedContainer: null, SelectedContainer: MainContainer,
TypeCounters: {} TypeCounters: {}
} }
], ],
historyCurrentStep: 0 historyCurrentStep: 0,
} as IAppState; isLoaded: false
};
} }
public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1);
public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep];
componentDidMount() { componentDidMount() {
// Fetch the configuration from the API // Fetch the configuration from the API
fetchConfiguration().then((configuration: Configuration) => { fetchConfiguration().then((configuration: Configuration) => {
@ -72,281 +73,38 @@ class App extends React.Component<IAppProps> {
} }
); );
const history = this.getCurrentHistory();
const current = history[history.length - 1];
// Save the configuration and the new MainContainer // Save the configuration and the new MainContainer
// and default the selected container to it // and default the selected container to it
this.setState(prevState => ({ this.setState({
...prevState,
configuration, configuration,
history: history.concat( history:
[ [
{ {
MainContainer, MainContainer,
SelectedContainer: MainContainer, SelectedContainer: MainContainer,
TypeCounters: current.TypeCounters TypeCounters: {}
} }
] ],
), historyCurrentStep: 0,
historyCurrentStep: history.length isLoaded: true
} as IAppState)); } as IAppState);
}); });
} }
/** public render() {
* Toggle the components sidebar if (this.state.isLoaded) {
*/
public ToggleSidebar() {
this.setState({
isSidebarOpen: !this.state.isSidebarOpen
} as IAppState);
}
/**
* Toggle the elements
*/
public ToggleElementsSidebar() {
this.setState({
isSVGSidebarOpen: !this.state.isSVGSidebarOpen
} as IAppState);
}
/**
* Toggle the elements
*/
public ToggleHistory() {
this.setState({
isHistoryOpen: !this.state.isHistoryOpen
} as IAppState);
}
/**
* Select a container
* @param container Selected container
*/
public SelectContainer(container: ContainerModel) {
const history = this.getCurrentHistory();
const current = history[history.length - 1];
this.setState({
history: history.concat([{
MainContainer: current.MainContainer,
TypeCounters: current.TypeCounters,
SelectedContainer: container
}]),
historyCurrentStep: history.length
} as IAppState);
}
/**
* 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 clone: IContainerModel = structuredClone(current.SelectedContainer);
(clone.properties as any)[key] = value;
this.setState({
history: history.concat([{
SelectedContainer: clone,
MainContainer: clone,
TypeCounters: current.TypeCounters
}]),
historyCurrentStep: history.length
} as IAppState);
return;
}
const clone: IContainerModel = structuredClone(current.MainContainer);
const it = MakeIterator(clone);
let container: ContainerModel | null = null;
for (const child of it) {
if (child.properties.id === current.SelectedContainer.properties.id) {
container = child as ContainerModel;
break;
}
}
if (container === null) {
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,
MainContainer: clone,
TypeCounters: current.TypeCounters
}]),
historyCurrentStep: history.length
} as IAppState);
}
/**
* Add a new container to a selected container
* @param type The type of container
* @returns void
*/
public AddContainer(type: string): void {
const history = this.getCurrentHistory();
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
return;
}
if (current.MainContainer === null ||
current.MainContainer === undefined) {
return;
}
// Get the preset properties from the API
const properties = this.state.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 structure: IContainerModel = structuredClone(current.MainContainer);
const clone = Object.assign(new ContainerModel(null, {} as Properties), structure);
// Find the parent
const it = MakeIterator(clone);
let parent: ContainerModel | null = null;
for (const child of it) {
if (child.properties.id === current.SelectedContainer.properties.id) {
parent = child as ContainerModel;
break;
}
}
if (parent === null) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
// Create the container
const newContainer = new ContainerModel(
parent,
{
id: `${type}-${count}`,
x: 0,
y: 0,
width: properties?.Width,
height: parent.properties.height,
...properties.Style
} as Properties,
[],
{
type
}
);
// And push it the the parent children
parent.children.push(newContainer);
// Update the state
this.setState({
history: history.concat([{
MainContainer: clone,
TypeCounters: newCounters,
SelectedContainer: parent
}]),
historyCurrentStep: history.length
} as IAppState);
}
public jumpTo(move: number): void {
this.setState({
historyCurrentStep: move
} as IAppState);
}
/**
* Render the application
* @returns {JSX.Element} Rendered JSX element
*/
render() {
const current = this.getCurrentHistoryState();
return ( return (
<div className="App font-sans h-full"> <div>
<Sidebar <Editor
componentOptions={this.state.configuration.AvailableContainers} configuration={this.state.configuration}
isOpen={this.state.isSidebarOpen}
onClick={() => this.ToggleSidebar()}
buttonOnClick={(type: string) => this.AddContainer(type)}
/>
<button
className='fixed z-10 top-4 left-4 text-lg bg-blue-200 hover:bg-blue-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
onClick={() => this.ToggleSidebar()}
>
&#9776; Components
</button>
<ElementsSidebar
MainContainer={current.MainContainer}
SelectedContainer={current.SelectedContainer}
isOpen={this.state.isSVGSidebarOpen}
isHistoryOpen={this.state.isHistoryOpen}
onClick={() => this.ToggleElementsSidebar()}
onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)}
selectContainer={(container: ContainerModel) => this.SelectContainer(container)}
/>
<button
className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
onClick={() => this.ToggleElementsSidebar()}
>
&#9776; Elements
</button>
<History
history={this.state.history} history={this.state.history}
historyCurrentStep={this.state.historyCurrentStep} historyCurrentStep={this.state.historyCurrentStep}
isOpen={this.state.isHistoryOpen}
onClick={() => this.ToggleHistory()}
jumpTo={(move) => { this.jumpTo(move); }}
/> />
<button
className='fixed z-10 top-4 right-72 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
onClick={() => this.ToggleHistory()}>
&#9776; History
</button>
<SVG selected={current.SelectedContainer}>
{ current.MainContainer }
</SVG>
</div> </div>
); );
} }
} }
}
/** /**
* Fetch the configuration from the API * Fetch the configuration from the API
@ -377,5 +135,3 @@ export async function fetchConfiguration(): Promise<Configuration> {
xhr.send(); xhr.send();
}); });
} }
export default App;

22
src/Editor.scss Normal file
View file

@ -0,0 +1,22 @@
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;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}

299
src/Editor.tsx Normal file
View file

@ -0,0 +1,299 @@
import React from 'react';
import './Editor.scss';
import Sidebar from './Components/Sidebar/Sidebar';
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
import { AvailableContainer } from './Interfaces/AvailableContainer';
import { Configuration } from './Interfaces/Configuration';
import { SVG } from './Components/SVG/SVG';
import { History } from './Components/History/History';
import { ContainerModel, IContainerModel, MakeIterator } from './Components/SVG/Elements/ContainerModel';
import Properties from './Interfaces/Properties';
import { IHistoryState } from './App';
interface IEditorProps {
configuration: Configuration,
history: Array<IHistoryState>,
historyCurrentStep: number
}
interface IEditorState {
isSidebarOpen: boolean,
isSVGSidebarOpen: boolean,
isHistoryOpen: boolean,
history: Array<IHistoryState>,
historyCurrentStep: number,
}
class Editor extends React.Component<IEditorProps> {
public state: IEditorState;
constructor(props: IEditorProps) {
super(props);
this.state = {
isSidebarOpen: true,
isSVGSidebarOpen: false,
isHistoryOpen: false,
configuration: props.configuration,
history: props.history,
historyCurrentStep: props.historyCurrentStep
} as IEditorState;
}
public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1);
public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep];
/**
* Toggle the components sidebar
*/
public ToggleSidebar() {
this.setState({
isSidebarOpen: !this.state.isSidebarOpen
} as IEditorState);
}
/**
* Toggle the elements
*/
public ToggleElementsSidebar() {
this.setState({
isSVGSidebarOpen: !this.state.isSVGSidebarOpen
} as IEditorState);
}
/**
* Toggle the elements
*/
public ToggleHistory() {
this.setState({
isHistoryOpen: !this.state.isHistoryOpen
} as IEditorState);
}
/**
* Select a container
* @param container Selected container
*/
public SelectContainer(container: ContainerModel) {
const history = this.getCurrentHistory();
const current = history[history.length - 1];
this.setState({
history: history.concat([{
MainContainer: current.MainContainer,
TypeCounters: current.TypeCounters,
SelectedContainer: container
}]),
historyCurrentStep: history.length
} as IEditorState);
}
/**
* 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 clone: IContainerModel = structuredClone(current.SelectedContainer);
(clone.properties as any)[key] = value;
this.setState({
history: history.concat([{
SelectedContainer: clone,
MainContainer: clone,
TypeCounters: current.TypeCounters
}]),
historyCurrentStep: history.length
} as IEditorState);
return;
}
const clone: IContainerModel = structuredClone(current.MainContainer);
const it = MakeIterator(clone);
let container: ContainerModel | null = null;
for (const child of it) {
if (child.properties.id === current.SelectedContainer.properties.id) {
container = child as ContainerModel;
break;
}
}
if (container === null) {
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,
MainContainer: clone,
TypeCounters: current.TypeCounters
}]),
historyCurrentStep: history.length
} as IEditorState);
}
/**
* Add a new container to a selected container
* @param type The type of container
* @returns void
*/
public AddContainer(type: string): void {
const history = this.getCurrentHistory();
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
return;
}
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 structure: IContainerModel = structuredClone(current.MainContainer);
const clone = Object.assign(new ContainerModel(null, {} as Properties), structure);
// Find the parent
const it = MakeIterator(clone);
let parent: ContainerModel | null = null;
for (const child of it) {
if (child.properties.id === current.SelectedContainer.properties.id) {
parent = child as ContainerModel;
break;
}
}
if (parent === null) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
// Create the container
const newContainer = new ContainerModel(
parent,
{
id: `${type}-${count}`,
x: 0,
y: 0,
width: properties?.Width,
height: parent.properties.height,
...properties.Style
} as Properties,
[],
{
type
}
);
// And push it the the parent children
parent.children.push(newContainer);
// Update the state
this.setState({
history: history.concat([{
MainContainer: clone,
TypeCounters: newCounters,
SelectedContainer: parent
}]),
historyCurrentStep: history.length
} as IEditorState);
}
public jumpTo(move: number): void {
this.setState({
historyCurrentStep: move
} as IEditorState);
}
/**
* Render the application
* @returns {JSX.Element} Rendered JSX element
*/
render() {
const current = this.getCurrentHistoryState();
return (
<div className="App font-sans h-full">
<Sidebar
componentOptions={this.props.configuration.AvailableContainers}
isOpen={this.state.isSidebarOpen}
onClick={() => this.ToggleSidebar()}
buttonOnClick={(type: string) => this.AddContainer(type)}
/>
<button
className='fixed z-10 top-4 left-4 text-lg bg-blue-200 hover:bg-blue-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
onClick={() => this.ToggleSidebar()}
>
&#9776; Components
</button>
<ElementsSidebar
MainContainer={current.MainContainer}
SelectedContainer={current.SelectedContainer}
isOpen={this.state.isSVGSidebarOpen}
isHistoryOpen={this.state.isHistoryOpen}
onClick={() => this.ToggleElementsSidebar()}
onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)}
selectContainer={(container: ContainerModel) => this.SelectContainer(container)}
/>
<button
className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
onClick={() => this.ToggleElementsSidebar()}
>
&#9776; Elements
</button>
<History
history={this.state.history}
historyCurrentStep={this.state.historyCurrentStep}
isOpen={this.state.isHistoryOpen}
onClick={() => this.ToggleHistory()}
jumpTo={(move) => { this.jumpTo(move); }}
/>
<button
className='fixed z-10 top-4 right-72 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
onClick={() => this.ToggleHistory()}>
&#9776; History
</button>
<SVG selected={current.SelectedContainer}>
{ current.MainContainer }
</SVG>
</div>
);
}
}
export default Editor;

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import { App } from './App';
import './index.scss'; import './index.scss';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(