svg-layout-designer-react/src/Editor.tsx
Siklos 340cc86aa9
All checks were successful
continuous-integration/drone/push Build is passing
Extract Editor from App
2022-08-04 14:53:49 +02:00

299 lines
9 KiB
TypeScript

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;