From fab40f5cf7bf0dc80457f7372fd572f8ced23736 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 10:23:48 +0200 Subject: [PATCH 01/11] Implement history --- src/App.tsx | 135 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 42 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f7f74a9..875dfa0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,13 +10,18 @@ import { SVG } from './Components/SVG/SVG'; interface IAppProps { } +export interface IHistoryState { + MainContainer: Container | null, + SelectedContainer: Container | null, + TypeCounters: Record +} + interface IAppState { isSidebarOpen: boolean, isSVGSidebarOpen: boolean, configuration: Configuration, - MainContainer: Container | null, - SelectedContainer: Container | null - Counters: Record + history: Array, + historyCurrentStep: 0 } class App extends React.Component { @@ -32,12 +37,20 @@ class App extends React.Component { AvailableSymbols: [], MainContainer: {} as AvailableContainer }, - MainContainer: null, - SelectedContainer: null, - Counters: {} - }; + history: [ + { + MainContainer: null, + SelectedContainer: null, + TypeCounters: {} + } + ], + historyCurrentStep: 0 + } as IAppState; } + public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1); + public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep]; + componentDidMount() { // Fetch the configuration from the API fetchConfiguration().then((configuration: Configuration) => { @@ -58,14 +71,25 @@ class App extends React.Component { } ); + const history = this.getCurrentHistory(); + const current = history[history.length - 1]; + // Save the configuration and the new MainContainer // and default the selected container to it this.setState(prevState => ({ ...prevState, configuration, - MainContainer, - SelectedContainer: MainContainer - })); + history: history.concat( + [ + { + MainContainer, + SelectedContainer: MainContainer, + TypeCounters: current.TypeCounters + } + ] + ), + historyCurrentStep: history.length + } as IAppState)); }); } @@ -92,9 +116,16 @@ class App extends React.Component { * @param container Selected container */ public SelectContainer(container: Container) { + const history = this.getCurrentHistory(); + const current = history[history.length - 1]; this.setState({ - SelectedContainer: container - } as IAppProps); + history: history.concat([{ + MainContainer: current.MainContainer, + TypeCounters: current.TypeCounters, + SelectedContainer: container + }]), + historyCurrentStep: history.length + } as IAppState); } /** @@ -104,43 +135,55 @@ class App extends React.Component { * @returns void */ public OnPropertyChange(key: string, value: string | number): void { - if (this.state.SelectedContainer === null || - this.state.SelectedContainer === undefined) { + const history = this.getCurrentHistory(); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { throw new Error('Property was changed before selecting a Container'); } - if (this.state.MainContainer === null || - this.state.MainContainer === undefined) { + if (current.MainContainer === null || + current.MainContainer === undefined) { throw new Error('Property was changed before the main container was added'); } const pair = {} as Record; pair[key] = value; - const properties = Object.assign(this.state.SelectedContainer.props.properties, pair); + const properties = Object.assign(current.SelectedContainer.props.properties, pair); const props = { - ...this.state.SelectedContainer.props, + ...current.SelectedContainer.props, properties }; const newSelectedContainer = new Container(props); - const parent = this.state.SelectedContainer.props.parent; + const parent = current.SelectedContainer.props.parent; if (parent === null) { this.setState({ - SelectedContainer: newSelectedContainer, - MainContainer: newSelectedContainer - }); + history: history.concat([{ + SelectedContainer: newSelectedContainer, + MainContainer: newSelectedContainer, + TypeCounters: current.TypeCounters + }]), + historyCurrentStep: history.length + } as IAppState); return; } - const index = parent.props.children.indexOf(this.state.SelectedContainer); + const index = parent.props.children.indexOf(current.SelectedContainer); parent.props.children[index] = newSelectedContainer; - const newMainContainer = new Container(Object.assign({}, this.state.MainContainer.props)); - this.setState({ - SelectedContainer: newSelectedContainer, - MainContainer: newMainContainer - }); + const newMainContainer = new Container(Object.assign({}, current.MainContainer.props)); + this.setState( + { + history: history.concat([{ + SelectedContainer: newSelectedContainer, + MainContainer: newMainContainer, + TypeCounters: current.TypeCounters + }]), + historyCurrentStep: history.length + } as IAppState); } /** @@ -149,13 +192,16 @@ class App extends React.Component { * @returns void */ public AddContainer(type: string): void { - if (this.state.SelectedContainer === null || - this.state.SelectedContainer === undefined) { + const history = this.getCurrentHistory(); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { return; } - if (this.state.MainContainer === null || - this.state.MainContainer === undefined) { + if (current.MainContainer === null || + current.MainContainer === undefined) { return; } @@ -167,7 +213,7 @@ class App extends React.Component { } // Set the counter of the object type in order to assign an unique id - const newCounters = Object.assign({}, this.state.Counters); + const newCounters = Object.assign({}, current.TypeCounters); if (newCounters[type] === null || newCounters[type] === undefined) { newCounters[type] = 0; @@ -176,7 +222,7 @@ class App extends React.Component { } // Create the container - const parent = this.state.SelectedContainer; + const parent = current.SelectedContainer; const count = newCounters[type]; const container = new Container({ parent, @@ -198,11 +244,15 @@ class App extends React.Component { parent.props.children.push(container); // Update the state - const newMainContainer = new Container(Object.assign({}, this.state.MainContainer.props)); + const newMainContainer = new Container(Object.assign({}, current.MainContainer.props)); this.setState({ - MainContainer: newMainContainer, - Counters: newCounters - }); + history: history.concat([{ + MainContainer: newMainContainer, + TypeCounters: newCounters, + SelectedContainer: current.SelectedContainer + }]), + historyCurrentStep: history.length + } as IAppState); } /** @@ -210,6 +260,7 @@ class App extends React.Component { * @returns {JSX.Element} Rendered JSX element */ render() { + const current = this.getCurrentHistoryState(); return (
{ /> this.ToggleElementsSidebar()} onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} selectContainer={(container: Container) => this.SelectContainer(container)} /> - - { this.state.MainContainer } + + { current.MainContainer }
); From 964d9a0e578cd8d010445e6fe107f8ab7c3f5c21 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 11:17:59 +0200 Subject: [PATCH 02/11] Implement history form --- src/App.tsx | 10 +++++++++- src/Components/History/History.tsx | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/Components/History/History.tsx diff --git a/src/App.tsx b/src/App.tsx index 875dfa0..ac29d48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Children } from 'react'; import './App.scss'; import Sidebar from './Components/Sidebar/Sidebar'; import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; @@ -6,6 +6,7 @@ import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; import { Container } from './Components/SVG/Elements/Container'; import { SVG } from './Components/SVG/SVG'; +import { History } from './Components/History/History'; interface IAppProps { } @@ -255,6 +256,12 @@ class App extends React.Component { } as IAppState); } + public jumpTo(move: number): void { + this.setState({ + historyCurrentStep: move + } as IAppState); + } + /** * Render the application * @returns {JSX.Element} Rendered JSX element @@ -282,6 +289,7 @@ class App extends React.Component { { current.MainContainer } + { this.jumpTo(move); }} /> ); } diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx new file mode 100644 index 0000000..6f6d49e --- /dev/null +++ b/src/Components/History/History.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { IHistoryState } from '../../App'; + +interface IHistoryProps { + history: IHistoryState[], + jumpTo: (move: number) => void +} + +export class History extends React.Component { + public render() { + const states = this.props.history.map((step, move) => { + const desc = move + ? `Go back at turn n°${move}` + : 'Go back at the beginning'; + + return ( +
  • + +
  • + ); + }); + + return ( +
    + { states } +
    + ); + } +} From e2a099457c5ec2888af83092e4b4e916bb52bf2c Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 12:57:34 +0200 Subject: [PATCH 03/11] Separated the model and the Container entity in order to remove any mutation operation --- src/App.tsx | 119 ++++++++++-------- .../ElementsSidebar/ElementsSidebar.tsx | 48 +++---- src/Components/SVG/Elements/Container.tsx | 102 +++------------ src/Components/SVG/Elements/ContainerModel.ts | 80 ++++++++++++ .../SVG/Elements/DimensionLayer.tsx | 16 +-- src/Components/SVG/Elements/Selector.tsx | 10 +- src/Components/SVG/SVG.tsx | 9 +- 7 files changed, 206 insertions(+), 178 deletions(-) create mode 100644 src/Components/SVG/Elements/ContainerModel.ts diff --git a/src/App.tsx b/src/App.tsx index ac29d48..ed36af1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,20 @@ -import React, { Children } from 'react'; +import React from 'react'; import './App.scss'; import Sidebar from './Components/Sidebar/Sidebar'; import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; -import { Container } from './Components/SVG/Elements/Container'; 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 { - MainContainer: Container | null, - SelectedContainer: Container | null, + MainContainer: IContainerModel | null, + SelectedContainer: IContainerModel | null, TypeCounters: Record } @@ -56,19 +57,16 @@ class App extends React.Component { // Fetch the configuration from the API fetchConfiguration().then((configuration: Configuration) => { // Set the main container from the given properties of the API - const MainContainer = new Container( + const MainContainer = new ContainerModel( + null, { - parent: null, - properties: { - id: 'main', - x: 0, - y: 0, - width: configuration.MainContainer.Width, - height: configuration.MainContainer.Height, - fillOpacity: 0, - stroke: 'black' - }, - children: [] + id: 'main', + x: 0, + y: 0, + width: configuration.MainContainer.Width, + height: configuration.MainContainer.Height, + fillOpacity: 0, + stroke: 'black' } ); @@ -116,7 +114,7 @@ class App extends React.Component { * Select a container * @param container Selected container */ - public SelectContainer(container: Container) { + public SelectContainer(container: ContainerModel) { const history = this.getCurrentHistory(); const current = history[history.length - 1]; this.setState({ @@ -141,30 +139,21 @@ class App extends React.Component { if (current.SelectedContainer === null || current.SelectedContainer === undefined) { - throw new Error('Property was changed before selecting a Container'); + throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } if (current.MainContainer === null || current.MainContainer === undefined) { - throw new Error('Property was changed before the main container was added'); + throw new Error('[OnPropertyChange] Property was changed before the main container was added'); } - const pair = {} as Record; - pair[key] = value; - const properties = Object.assign(current.SelectedContainer.props.properties, pair); - const props = { - ...current.SelectedContainer.props, - properties - }; - - const newSelectedContainer = new Container(props); - - const parent = current.SelectedContainer.props.parent; if (parent === null) { + const clone: IContainerModel = structuredClone(current.SelectedContainer); + (clone.properties as any)[key] = value; this.setState({ history: history.concat([{ - SelectedContainer: newSelectedContainer, - MainContainer: newSelectedContainer, + SelectedContainer: clone, + MainContainer: clone, TypeCounters: current.TypeCounters }]), historyCurrentStep: history.length @@ -172,15 +161,27 @@ class App extends React.Component { return; } - const index = parent.props.children.indexOf(current.SelectedContainer); - parent.props.children[index] = newSelectedContainer; + 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; - const newMainContainer = new Container(Object.assign({}, current.MainContainer.props)); this.setState( { history: history.concat([{ - SelectedContainer: newSelectedContainer, - MainContainer: newMainContainer, + SelectedContainer: container, + MainContainer: clone, TypeCounters: current.TypeCounters }]), historyCurrentStep: history.length @@ -221,36 +222,52 @@ class App extends React.Component { } 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 parent = current.SelectedContainer; - const count = newCounters[type]; - const container = new Container({ + const newContainer = new ContainerModel( parent, - properties: { + { id: `${type}-${count}`, x: 0, y: 0, width: properties?.Width, - height: parent.props.properties.height, + height: parent.properties.height, ...properties.Style - }, - children: [], - userData: { + } as Properties, + [], + { type } - }); + ); // And push it the the parent children - parent.props.children.push(container); + parent.children.push(newContainer); // Update the state - const newMainContainer = new Container(Object.assign({}, current.MainContainer.props)); this.setState({ history: history.concat([{ - MainContainer: newMainContainer, + MainContainer: clone, TypeCounters: newCounters, - SelectedContainer: current.SelectedContainer + SelectedContainer: parent }]), historyCurrentStep: history.length } as IAppState); @@ -283,7 +300,7 @@ class App extends React.Component { isOpen={this.state.isSVGSidebarOpen} onClick={() => this.ToggleElementsSidebar()} onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} - selectContainer={(container: Container) => this.SelectContainer(container)} + selectContainer={(container: ContainerModel) => this.SelectContainer(container)} /> diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index afec90a..3caea25 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,26 +1,26 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; -import { Container } from '../SVG/Elements/Container'; +import { IContainerModel, getDepth, MakeIterator } from '../SVG/Elements/ContainerModel'; interface IElementsSidebarProps { - MainContainer: Container | null, + MainContainer: IContainerModel | null, isOpen: boolean, - SelectedContainer: Container | null, + SelectedContainer: IContainerModel | null, onClick: () => void, onPropertyChange: (key: string, value: string) => void, - selectContainer: (container: Container) => void + selectContainer: (container: IContainerModel) => void } export class ElementsSidebar extends React.Component { - public iterateChilds(handleContainer: (container: Container) => void): React.ReactNode { + public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { if (!this.props.MainContainer) { return null; } - const it = this.props.MainContainer.MakeIterator(); + const it = MakeIterator(this.props.MainContainer); for (const container of it) { - handleContainer(container); + handleContainer(container as IContainerModel); } } @@ -28,29 +28,29 @@ export class ElementsSidebar extends React.Component { const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; const containerRows: React.ReactNode[] = []; - this.iterateChilds((container: Container) => { - const depth: number = container.getDepth(); - const key = container.props.properties.id.toString(); + this.iterateChilds((container: IContainerModel) => { + const depth: number = getDepth(container); + const key = container.properties.id.toString(); const text = '|\t'.repeat(depth) + key; const selectedClass: string = this.props.SelectedContainer !== null && - this.props.SelectedContainer.props.properties.id === container.props.properties.id + this.props.SelectedContainer.properties.id === container.properties.id ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-400 hover:bg-slate-600'; containerRows.push( this.props.selectContainer(container)}> + } + key={key} + onClick={() => this.props.selectContainer(container)}> { text } ); @@ -67,7 +67,7 @@ export class ElementsSidebar extends React.Component {
    { containerRows }
    - + ); } diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 1094588..ab8d92c 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,92 +1,22 @@ import * as React from 'react'; -import Properties from '../../../Interfaces/Properties'; +import { getDepth, IContainerModel } from './ContainerModel'; import { Dimension } from './Dimension'; -interface IContainerProps { - // eslint-disable-next-line no-use-before-define - parent: Container | null, - // eslint-disable-next-line no-use-before-define - children: Container[], - properties: Properties, - userData?: Record +export interface IContainerProps { + model: IContainerModel } const GAP = 50; export class Container extends React.Component { - /** - * Returns A copy of the properties of the Container - * @returns A copy of the properties of the Container - */ - public GetProperties(): Properties { - const properties : Properties = { - ...this.props.properties - }; - return properties; - } - - /** - * Returns a Generator iterating of over the children depth-first - */ - public * MakeIterator(): Generator { - const queue: Container[] = [this]; - const visited = new Set(queue); - while (queue.length > 0) { - const container = queue.pop() as Container; - - yield container; - - // if this reverse() gets costly, replace it by a simple for - container.props.children.reverse().forEach((child) => { - if (visited.has(child)) { - return; - } - visited.add(child); - queue.push(child); - }); - } - } - - /** - * Returns the depth of the container - * @returns The depth of the container - */ - public getDepth() { - let depth = 0; - - let current: Container | null = this.props.parent; - while (current != null) { - depth++; - current = current.props.parent; - } - - return depth; - } - - /** - * Returns the absolute position by iterating to the parent - * @returns The absolute position of the container - */ - public getAbsolutePosition(): [number, number] { - let x = Number(this.props.properties.x); - let y = Number(this.props.properties.y); - let current = this.props.parent; - while (current != null) { - x += Number(current.props.properties.x); - y += Number(current.props.properties.y); - current = current.props.parent; - } - return [x, y]; - } - /** * Render the container * @returns Render the container */ public render(): React.ReactNode { - const containersElements = this.props.children.map(child => child.render()); - const xText = Number(this.props.properties.width) / 2; - const yText = Number(this.props.properties.height) / 2; + const containersElements = this.props.model.children.map(child => new Container({ model: child } as IContainerProps).render()); + const xText = Number(this.props.model.properties.width) / 2; + const yText = Number(this.props.model.properties.height) / 2; // g style const defaultStyle = { @@ -98,7 +28,7 @@ export class Container extends React.Component { // Rect style const style = Object.assign( JSON.parse(JSON.stringify(defaultStyle)), - this.props.properties + this.props.model.properties ); style.x = 0; style.y = 0; @@ -106,18 +36,18 @@ export class Container extends React.Component { delete style.width; // Dimension props - const id = `dim-${this.props.properties.id}`; + const id = `dim-${this.props.model.properties.id}`; const xStart: number = 0; - const xEnd = Number(this.props.properties.width); - const y = -(GAP * (this.getDepth() + 1)); + const xEnd = Number(this.props.model.properties.width); + const y = -(GAP * (getDepth(this.props.model) + 1)); const strokeWidth = 1; - const text = (this.props.properties.width ?? 0).toString(); + const text = (this.props.model.properties.width ?? 0).toString(); return ( { text={text} /> @@ -137,7 +67,7 @@ export class Container extends React.Component { x={xText} y={yText} > - {this.props.properties.id} + {this.props.model.properties.id} { containersElements } diff --git a/src/Components/SVG/Elements/ContainerModel.ts b/src/Components/SVG/Elements/ContainerModel.ts new file mode 100644 index 0000000..4b35082 --- /dev/null +++ b/src/Components/SVG/Elements/ContainerModel.ts @@ -0,0 +1,80 @@ +import Properties from '../../../Interfaces/Properties'; + +export interface IContainerModel { + children: IContainerModel[], + parent: IContainerModel | null, + properties: Properties, + userData: Record +} + +export class ContainerModel implements IContainerModel { + public children: IContainerModel[]; + public parent: IContainerModel | null; + public properties: Properties; + public userData: Record; + + constructor( + parent: IContainerModel | null, + properties: Properties, + children: IContainerModel[] = [], + userData = {}) { + this.parent = parent; + this.properties = properties; + this.children = children; + this.userData = userData; + } +}; + +/** + * Returns a Generator iterating of over the children depth-first + */ +export function * MakeIterator(root: IContainerModel): Generator { + const queue: IContainerModel[] = [root]; + const visited = new Set(queue); + while (queue.length > 0) { + const container = queue.pop() as IContainerModel; + + yield container; + + // if this reverse() gets costly, replace it by a simple for + container.children.forEach((child) => { + if (visited.has(child)) { + return; + } + visited.add(child); + queue.push(child); + }); + } +} + +/** + * Returns the depth of the container + * @returns The depth of the container + */ +export function getDepth(parent: IContainerModel) { + let depth = 0; + + let current: IContainerModel | null = parent; + while (current != null) { + depth++; + current = current.parent; + } + + return depth; +} + +/** + * Returns the absolute position by iterating to the parent + * @returns The absolute position of the container + */ +export function getAbsolutePosition(container: IContainerModel): [number, number] { + let x = Number(container.properties.x); + let y = Number(container.properties.y); + let current = container.parent; + while (current != null) { + x += Number(current.properties.x); + y += Number(current.properties.y); + current = current.parent; + } + return [x, y]; +} diff --git a/src/Components/SVG/Elements/DimensionLayer.tsx b/src/Components/SVG/Elements/DimensionLayer.tsx index 601fedd..610177b 100644 --- a/src/Components/SVG/Elements/DimensionLayer.tsx +++ b/src/Components/SVG/Elements/DimensionLayer.tsx @@ -1,25 +1,25 @@ import * as React from 'react'; -import { Container } from './Container'; +import { ContainerModel, getDepth, MakeIterator } from './ContainerModel'; import { Dimension } from './Dimension'; interface IDimensionLayerProps { isHidden: boolean, - roots: Container | Container[] | null, + roots: ContainerModel | ContainerModel[] | null, } const GAP: number = 50; -const getDimensionsNodes = (root: Container): React.ReactNode[] => { - const it = root.MakeIterator(); +const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => { + const it = MakeIterator(root); const dimensions: React.ReactNode[] = []; for (const container of it) { // WARN: this might be dangerous later when using other units/rules - const width = Number(container.props.properties.width); + const width = Number(container.properties.width); - const id = `dim-${container.props.properties.id}`; - const xStart: number = container.props.properties.x; + const id = `dim-${container.properties.id}`; + const xStart: number = container.properties.x; const xEnd = xStart + width; - const y = -(GAP * (container.getDepth() + 1)); + const y = -(GAP * (getDepth(container) + 1)); const strokeWidth = 1; const text = width.toString(); const dimension = new Dimension({ diff --git a/src/Components/SVG/Elements/Selector.tsx b/src/Components/SVG/Elements/Selector.tsx index 6268d2f..6fc1b9b 100644 --- a/src/Components/SVG/Elements/Selector.tsx +++ b/src/Components/SVG/Elements/Selector.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { Container } from './Container'; +import { IContainerModel, getAbsolutePosition } from './ContainerModel'; interface ISelectorProps { - selected: Container | null + selected: IContainerModel | null } export const Selector: React.FC = (props) => { @@ -13,10 +13,10 @@ export const Selector: React.FC = (props) => { ); } - const [x, y] = props.selected.getAbsolutePosition(); + const [x, y] = getAbsolutePosition(props.selected); const [width, height] = [ - props.selected.props.properties.width, - props.selected.props.properties.height + props.selected.properties.width, + props.selected.properties.height ]; const style = { stroke: '#3B82F6', // tw blue-500 diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 64a3246..9583571 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; +import { ContainerModel } from './Elements/ContainerModel'; import { Selector } from './Elements/Selector'; interface ISVGProps { - children: Container | Container[] | null, - selected: Container | null + children: ContainerModel | ContainerModel[] | null, + selected: ContainerModel | null } interface ISVGState { @@ -65,9 +66,9 @@ export class SVG extends React.Component { let children: React.ReactNode | React.ReactNode[] = []; if (Array.isArray(this.props.children)) { - children = this.props.children.map(child => child.render()); + children = this.props.children.map(child => new Container({ model: child }).render()); } else if (this.props.children !== null) { - children = this.props.children.render(); + children = new Container({ model: this.props.children }).render(); } return ( From 72dfb4f9bb8f0b3edf8bf9296e29d52fe6866649 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 08:12:22 -0400 Subject: [PATCH 04/11] Improve history style (#5) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/5 --- src/App.tsx | 42 +++++++++++++++-- .../ElementsSidebar/ElementsSidebar.tsx | 8 +++- src/Components/History/History.tsx | 47 ++++++++++++++++--- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ed36af1..3db4ae4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ export interface IHistoryState { interface IAppState { isSidebarOpen: boolean, isSVGSidebarOpen: boolean, + isHistoryOpen: boolean, configuration: Configuration, history: Array, historyCurrentStep: 0 @@ -34,6 +35,7 @@ class App extends React.Component { this.state = { isSidebarOpen: true, isSVGSidebarOpen: false, + isHistoryOpen: false, configuration: { AvailableContainers: [], AvailableSymbols: [], @@ -110,6 +112,15 @@ class App extends React.Component { } as IAppState); } + /** + * Toggle the elements + */ + public ToggleHistory() { + this.setState({ + isHistoryOpen: !this.state.isHistoryOpen + } as IAppState); + } + /** * Select a container * @param container Selected container @@ -293,20 +304,45 @@ class App extends React.Component { onClick={() => this.ToggleSidebar()} buttonOnClick={(type: string) => this.AddContainer(type)} /> - + + this.ToggleElementsSidebar()} onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} selectContainer={(container: ContainerModel) => this.SelectContainer(container)} /> - + + + this.ToggleHistory()} + jumpTo={(move) => { this.jumpTo(move); }} + /> + + { current.MainContainer } - { this.jumpTo(move); }} /> ); } diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 3caea25..e93f527 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -6,6 +6,7 @@ import { IContainerModel, getDepth, MakeIterator } from '../SVG/Elements/Contain interface IElementsSidebarProps { MainContainer: IContainerModel | null, isOpen: boolean, + isHistoryOpen: boolean SelectedContainer: IContainerModel | null, onClick: () => void, onPropertyChange: (key: string, value: string) => void, @@ -25,7 +26,12 @@ export class ElementsSidebar extends React.Component { } public render() { - const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; + let isOpenClasses = '-right-64'; + if (this.props.isOpen) { + isOpenClasses = this.props.isHistoryOpen + ? 'right-64' + : 'right-0'; + } const containerRows: React.ReactNode[] = []; this.iterateChilds((container: IContainerModel) => { diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index 6f6d49e..750fcc7 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -3,26 +3,59 @@ import { IHistoryState } from '../../App'; interface IHistoryProps { history: IHistoryState[], + historyCurrentStep: number, + isOpen: boolean, + onClick: () => void, jumpTo: (move: number) => void } export class History extends React.Component { public render() { + const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; + const states = this.props.history.map((step, move) => { const desc = move - ? `Go back at turn n°${move}` - : 'Go back at the beginning'; + ? `Go to modification n°${move}` + : 'Go to the beginning'; + const isCurrent = move === this.props.historyCurrentStep; + + const selectedClass = isCurrent + ? 'bg-blue-500 hover:bg-blue-600' + : 'bg-slate-500 hover:bg-slate-700'; + + const isCurrentText = isCurrent + ? ' (current)' + : ''; return ( -
  • - -
  • + + ); }); + // recent first + states.reverse(); + return ( -
    - { states } +
    + +
    + History +
    +
    + { states } +
    ); } From 340cc86aa9b0d3dc43373ee143b6044d557b19cd Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 14:53:49 +0200 Subject: [PATCH 05/11] Extract Editor from App --- src/App.scss | 22 +--- src/App.tsx | 342 +++++++----------------------------------------- src/Editor.scss | 22 ++++ src/Editor.tsx | 299 ++++++++++++++++++++++++++++++++++++++++++ src/main.tsx | 2 +- 5 files changed, 373 insertions(+), 314 deletions(-) create mode 100644 src/Editor.scss create mode 100644 src/Editor.tsx diff --git a/src/App.scss b/src/App.scss index a32c70f..2cb61ee 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,24 +1,6 @@ html, body, -#root, -svg { - height: 100%; +#root { 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; } + height: 100%; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 3db4ae4..5d2c9ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,8 @@ -import React from 'react'; -import './App.scss'; -import Sidebar from './Components/Sidebar/Sidebar'; -import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; +import * as React from 'react'; +import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel'; +import Editor from './Editor'; 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'; - -interface IAppProps { -} export interface IHistoryState { MainContainer: IContainerModel | null, @@ -18,24 +10,35 @@ export interface IHistoryState { TypeCounters: Record } -interface IAppState { - isSidebarOpen: boolean, - isSVGSidebarOpen: boolean, - isHistoryOpen: boolean, - configuration: Configuration, - history: Array, - historyCurrentStep: 0 +interface IAppProps { } -class App extends React.Component { +interface IAppState { + configuration: Configuration, + history: IHistoryState[], + historyCurrentStep: number, + isLoaded: boolean +} + +export class App extends React.Component { public state: IAppState; - constructor(props: IAppProps) { + public constructor(props: IAppProps) { super(props); + + const MainContainer = new ContainerModel( + null, + { + id: 'main', + x: 0, + y: 0, + width: 1000, + height: 1000, + fillOpacity: 0, + stroke: 'black' + } + ); this.state = { - isSidebarOpen: true, - isSVGSidebarOpen: false, - isHistoryOpen: false, configuration: { AvailableContainers: [], AvailableSymbols: [], @@ -43,18 +46,16 @@ class App extends React.Component { }, history: [ { - MainContainer: null, - SelectedContainer: null, + MainContainer, + SelectedContainer: MainContainer, TypeCounters: {} } ], - historyCurrentStep: 0 - } as IAppState; + historyCurrentStep: 0, + isLoaded: false + }; } - public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1); - public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep]; - componentDidMount() { // Fetch the configuration from the API fetchConfiguration().then((configuration: Configuration) => { @@ -72,279 +73,36 @@ class App extends React.Component { } ); - const history = this.getCurrentHistory(); - const current = history[history.length - 1]; - // Save the configuration and the new MainContainer // and default the selected container to it - this.setState(prevState => ({ - ...prevState, + this.setState({ configuration, - history: history.concat( + history: [ { MainContainer, SelectedContainer: MainContainer, - TypeCounters: current.TypeCounters + TypeCounters: {} } - ] - ), - historyCurrentStep: history.length - } as IAppState)); + ], + historyCurrentStep: 0, + isLoaded: true + } as IAppState); }); } - /** - * Toggle the components sidebar - */ - 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'); + public render() { + if (this.state.isLoaded) { + return ( +
    + +
    + ); } - - 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 ( -
    - this.ToggleSidebar()} - buttonOnClick={(type: string) => this.AddContainer(type)} - /> - - - this.ToggleElementsSidebar()} - onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} - selectContainer={(container: ContainerModel) => this.SelectContainer(container)} - /> - - - this.ToggleHistory()} - jumpTo={(move) => { this.jumpTo(move); }} - /> - - - - { current.MainContainer } - -
    - ); } } @@ -377,5 +135,3 @@ export async function fetchConfiguration(): Promise { xhr.send(); }); } - -export default App; diff --git a/src/Editor.scss b/src/Editor.scss new file mode 100644 index 0000000..32b3726 --- /dev/null +++ b/src/Editor.scss @@ -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; } +} \ No newline at end of file diff --git a/src/Editor.tsx b/src/Editor.tsx new file mode 100644 index 0000000..81db229 --- /dev/null +++ b/src/Editor.tsx @@ -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, + historyCurrentStep: number +} + +interface IEditorState { + isSidebarOpen: boolean, + isSVGSidebarOpen: boolean, + isHistoryOpen: boolean, + history: Array, + historyCurrentStep: number, +} + +class Editor extends React.Component { + 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 ( +
    + this.ToggleSidebar()} + buttonOnClick={(type: string) => this.AddContainer(type)} + /> + + + this.ToggleElementsSidebar()} + onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} + selectContainer={(container: ContainerModel) => this.SelectContainer(container)} + /> + + + this.ToggleHistory()} + jumpTo={(move) => { this.jumpTo(move); }} + /> + + + + { current.MainContainer } + +
    + ); + } +} + +export default Editor; diff --git a/src/main.tsx b/src/main.tsx index 95b3daf..6893f34 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import { App } from './App'; import './index.scss'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( From e2ad8d6ebd3f47a7ef30774a18fc4e1f2ad3a265 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 17:09:42 +0200 Subject: [PATCH 06/11] Implement basic save/load (still needs some fixes) --- .eslintrc.cjs | 6 +- src/App.tsx | 58 +++++++++------ .../ElementsSidebar/ElementsSidebar.tsx | 3 +- src/Components/MainMenu/MainMenu.tsx | 74 +++++++++++++++++++ src/Editor.tsx | 35 ++++++++- src/index.scss | 3 + 6 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 src/Components/MainMenu/MainMenu.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 49a98f8..86a1866 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,7 +21,9 @@ module.exports = { ], rules: { 'space-before-function-paren': ['error', 'never'], - indent: ['warn', 2], - semi: ['warn', 'always'] + indent: ['warn', 2, { SwitchCase: 1 }], + semi: ['warn', 'always'], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error' } }; diff --git a/src/App.tsx b/src/App.tsx index 5d2c9ae..97a10d0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; +import './App.scss'; +import { MainMenu } from './Components/MainMenu/MainMenu'; import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel'; -import Editor from './Editor'; +import Editor, { IEditorState } from './Editor'; import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; @@ -23,40 +25,21 @@ interface IAppState { export class App extends React.Component { public state: IAppState; - public constructor(props: IAppProps) { + constructor(props: IAppProps) { super(props); - - const MainContainer = new ContainerModel( - null, - { - id: 'main', - x: 0, - y: 0, - width: 1000, - height: 1000, - fillOpacity: 0, - stroke: 'black' - } - ); this.state = { configuration: { AvailableContainers: [], AvailableSymbols: [], MainContainer: {} as AvailableContainer }, - history: [ - { - MainContainer, - SelectedContainer: MainContainer, - TypeCounters: {} - } - ], + history: [], historyCurrentStep: 0, isLoaded: false }; } - componentDidMount() { + public NewEditor() { // Fetch the configuration from the API fetchConfiguration().then((configuration: Configuration) => { // Set the main container from the given properties of the API @@ -91,6 +74,25 @@ export class App extends React.Component { }); } + public LoadEditor(files: FileList | null) { + 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.setState({ + configuration: editorState.configuration, + history: editorState.history, + historyCurrentStep: editorState.historyCurrentStep, + isLoaded: true + } as IAppState); + }); + reader.readAsText(file); + } + public render() { if (this.state.isLoaded) { return ( @@ -102,6 +104,15 @@ export class App extends React.Component { />
    ); + } else { + return ( +
    + this.NewEditor()} + loadEditor={(files: FileList | null) => this.LoadEditor(files)} + /> +
    + ); } } } @@ -131,6 +142,7 @@ export async function fetchConfiguration(): Promise { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { resolve(JSON.parse(this.responseText)); } + reject(xhr.responseText); }; xhr.send(); }); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index e93f527..4e599ef 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -38,7 +38,8 @@ export class ElementsSidebar extends React.Component { const depth: number = getDepth(container); const key = container.properties.id.toString(); const text = '|\t'.repeat(depth) + key; - const selectedClass: string = this.props.SelectedContainer !== null && + const selectedClass: string = this.props.SelectedContainer !== undefined && + this.props.SelectedContainer !== null && this.props.SelectedContainer.properties.id === container.properties.id ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-400 hover:bg-slate-600'; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx new file mode 100644 index 0000000..92b19d5 --- /dev/null +++ b/src/Components/MainMenu/MainMenu.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; + +interface IMainMenuProps { + newEditor: () => void; + loadEditor: (files: FileList | null) => void +} + +enum WindowState { + MAIN, + LOAD, +} + +export const MainMenu: React.FC = (props) => { + const [windowState, setWindowState] = React.useState(WindowState.MAIN); + switch (windowState) { + case WindowState.LOAD: + return ( +
    +
    + +
    + {/* */} + +
    + + ); + default: + return ( +
    + + +
    + ); + } +}; diff --git a/src/Editor.tsx b/src/Editor.tsx index 81db229..5358df5 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -2,7 +2,6 @@ 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'; @@ -16,12 +15,15 @@ interface IEditorProps { historyCurrentStep: number } -interface IEditorState { +export interface IEditorState { isSidebarOpen: boolean, isSVGSidebarOpen: boolean, isHistoryOpen: boolean, history: Array, historyCurrentStep: number, + // do not use it, use props.configuration + // only used for serialization purpose + configuration: Configuration } class Editor extends React.Component { @@ -33,8 +35,8 @@ class Editor extends React.Component { isSidebarOpen: true, isSVGSidebarOpen: false, isHistoryOpen: false, - configuration: props.configuration, - history: props.history, + configuration: Object.assign({}, props.configuration), + history: [...props.history], historyCurrentStep: props.historyCurrentStep } as IEditorState; } @@ -238,6 +240,17 @@ class Editor extends React.Component { } as IEditorState); } + public SaveEditor() { + const exportName = 'state'; + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(this.state, getCircularReplacer(), 4))}`; + 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(); + } + /** * Render the application * @returns {JSX.Element} Rendered JSX element @@ -291,9 +304,23 @@ class Editor extends React.Component { { current.MainContainer } + ); } } +const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key: any, value: object | null) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; + export default Editor; diff --git a/src/index.scss b/src/index.scss index 7327b1a..c7e0d50 100644 --- a/src/index.scss +++ b/src/index.scss @@ -12,4 +12,7 @@ .close-button { @apply transition-all w-full h-auto p-4 flex } + .mainmenu-btn { + @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg + } } \ No newline at end of file From cf49ad644a5683e76d1a902ae1c3058db3bc181c Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 18:02:27 +0200 Subject: [PATCH 07/11] Implement revive of json after load + Added parentId --- src/App.tsx | 35 ++++++++++++++++++- src/Components/Properties/Properties.tsx | 2 +- src/Components/SVG/Elements/ContainerModel.ts | 10 ++++++ src/Editor.tsx | 1 + src/Interfaces/Properties.ts | 3 +- 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 97a10d0..3f5541a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import './App.scss'; import { MainMenu } from './Components/MainMenu/MainMenu'; -import { ContainerModel, IContainerModel } from './Components/SVG/Elements/ContainerModel'; +import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Components/SVG/Elements/ContainerModel'; import Editor, { IEditorState } from './Editor'; import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; @@ -47,6 +47,7 @@ export class App extends React.Component { null, { id: 'main', + parentId: 'null', x: 0, y: 0, width: configuration.MainContainer.Width, @@ -83,6 +84,9 @@ export class App extends React.Component { reader.addEventListener('load', () => { const result = reader.result as string; const editorState: IEditorState = JSON.parse(result); + + Revive(editorState); + this.setState({ configuration: editorState.configuration, history: editorState.history, @@ -147,3 +151,32 @@ export async function fetchConfiguration(): Promise { xhr.send(); }); } + +/** + * Revive the Editor state + * by setting the containers references to their parent + * @param editorState Editor state + */ +function Revive(editorState: IEditorState): void { + const history = editorState.history; + for (const state of history) { + if (state.MainContainer === null) { + continue; + } + + const it = MakeIterator(state.MainContainer); + state.SelectedContainer = state.MainContainer; + for (const container of it) { + const parentId = container.properties.parentId; + if (parentId === null) { + container.parent = null; + continue; + } + const parent = findContainerById(state.MainContainer, parentId); + if (parent === undefined) { + continue; + } + container.parent = parent; + } + } +} \ No newline at end of file diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 09c156d..e75e86b 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -43,7 +43,7 @@ export class Properties extends React.Component { const id = `property-${key}`; const type = isNaN(Number(value)) ? 'text' : 'number'; - const isDisabled = key === 'id'; // hardcoded + const isDisabled = key === 'id' || key === 'parentId'; // hardcoded groupInput.push(
    diff --git a/src/Components/SVG/Elements/ContainerModel.ts b/src/Components/SVG/Elements/ContainerModel.ts index 4b35082..d66f6c8 100644 --- a/src/Components/SVG/Elements/ContainerModel.ts +++ b/src/Components/SVG/Elements/ContainerModel.ts @@ -78,3 +78,13 @@ export function getAbsolutePosition(container: IContainerModel): [number, number } return [x, y]; } + +export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined { + const it = MakeIterator(root); + for (const container of it) { + if (container.properties.id === id) { + return container; + } + } + return undefined; +} diff --git a/src/Editor.tsx b/src/Editor.tsx index 5358df5..c9dc2fa 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -208,6 +208,7 @@ class Editor extends React.Component { parent, { id: `${type}-${count}`, + parentId: parent.properties.id, x: 0, y: 0, width: properties?.Width, diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/Properties.ts index ce386aa..79f4f47 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/Properties.ts @@ -2,6 +2,7 @@ import * as React from 'react'; export default interface Properties extends React.CSSProperties { id: string, + parentId: string | null, x: number, - y: number, + y: number } From 70e1031245abff8490f5f6c7159825de7b8461a3 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 18:06:11 +0200 Subject: [PATCH 08/11] Add react Heroicon --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3b90ace..7706f70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "svg-layout-designer-react", "version": "0.0.0", "dependencies": { + "@heroicons/react": "^1.0.6", "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -524,6 +525,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@heroicons/react": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.6.tgz", + "integrity": "sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -6430,6 +6439,12 @@ } } }, + "@heroicons/react": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.6.tgz", + "integrity": "sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", diff --git a/package.json b/package.json index 941f17c..e8b097f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "coverage": "vitest run coverage" }, "dependencies": { + "@heroicons/react": "^1.0.6", "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", From 8cee073f395bff2deead8dfcb97c16fa89d16bd9 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 18:17:45 +0200 Subject: [PATCH 09/11] Add Style to save button --- src/Editor.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Editor.tsx b/src/Editor.tsx index c9dc2fa..ef82066 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { UploadIcon } from '@heroicons/react/outline'; import './Editor.scss'; import Sidebar from './Components/Sidebar/Sidebar'; import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; @@ -305,7 +306,13 @@ class Editor extends React.Component { { current.MainContainer } - +
    ); } From 8b110020e1168a888349d5d283b5d0b89950f140 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 18:32:24 +0200 Subject: [PATCH 10/11] Add vitest/ui --- package-lock.json | 86 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 87 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7706f70..83e5fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vitejs/plugin-react": "^2.0.0", + "@vitest/ui": "^0.20.3", "autoprefixer": "^10.4.8", "eslint": "^8.20.0", "eslint-config-standard": "^17.0.0", @@ -730,6 +731,12 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.24.26", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.26.tgz", @@ -1291,6 +1298,15 @@ "vite": "^3.0.0" } }, + "node_modules/@vitest/ui": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.20.3.tgz", + "integrity": "sha512-Rlg+y3PtE5IcGPVmViF/BXM7euY7LG0yjfIvXKlF0L3OnNSVS8+esgLlAhaYftSJXtcunqa/cYXiQ+qFVTaBGw==", + "dev": true, + "dependencies": { + "sirv": "^2.0.2" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -4463,6 +4479,15 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5342,6 +5367,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sirv": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz", + "integrity": "sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5613,6 +5652,15 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", + "integrity": "sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -6628,6 +6676,12 @@ "fastq": "^1.6.0" } }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, "@sinclair/typebox": { "version": "0.24.26", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.26.tgz", @@ -7028,6 +7082,15 @@ "react-refresh": "^0.14.0" } }, + "@vitest/ui": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.20.3.tgz", + "integrity": "sha512-Rlg+y3PtE5IcGPVmViF/BXM7euY7LG0yjfIvXKlF0L3OnNSVS8+esgLlAhaYftSJXtcunqa/cYXiQ+qFVTaBGw==", + "dev": true, + "requires": { + "sirv": "^2.0.2" + } + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -9273,6 +9336,12 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9874,6 +9943,17 @@ "object-inspect": "^1.9.0" } }, + "sirv": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.2.tgz", + "integrity": "sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10084,6 +10164,12 @@ "is-number": "^7.0.0" } }, + "totalist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", + "integrity": "sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==", + "dev": true + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", diff --git a/package.json b/package.json index e8b097f..149d76f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vitejs/plugin-react": "^2.0.0", + "@vitest/ui": "^0.20.3", "autoprefixer": "^10.4.8", "eslint": "^8.20.0", "eslint-config-standard": "^17.0.0", From 8081e7fee9590f3632b31610bcf45cc206fbe3cc Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 18:32:40 +0200 Subject: [PATCH 11/11] Fix regression in fetchConfiguration --- src/App.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3f5541a..ee638f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -127,7 +127,6 @@ export class App extends React.Component { */ export async function fetchConfiguration(): Promise { const url = `${import.meta.env.VITE_API_URL}`; - // The test library cannot use the Fetch API // @ts-ignore if (window.fetch) { @@ -138,15 +137,13 @@ export async function fetchConfiguration(): Promise { response.json() ) as Configuration; } - - return new Promise((resolve, reject) => { + return 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)); } - reject(xhr.responseText); }; xhr.send(); });