diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 86a1866..49a98f8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,9 +21,7 @@ module.exports = { ], rules: { 'space-before-function-paren': ['error', 'never'], - indent: ['warn', 2, { SwitchCase: 1 }], - semi: ['warn', 'always'], - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'error' + indent: ['warn', 2], + semi: ['warn', 'always'] } }; diff --git a/package-lock.json b/package-lock.json index 83e5fcb..3b90ace 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "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", @@ -24,7 +23,6 @@ "@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", @@ -526,14 +524,6 @@ "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", @@ -731,12 +721,6 @@ "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", @@ -1298,15 +1282,6 @@ "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", @@ -4479,15 +4454,6 @@ "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", @@ -5367,20 +5333,6 @@ "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", @@ -5652,15 +5604,6 @@ "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", @@ -6487,12 +6430,6 @@ } } }, - "@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", @@ -6676,12 +6613,6 @@ "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", @@ -7082,15 +7013,6 @@ "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", @@ -9336,12 +9258,6 @@ "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", @@ -9943,17 +9859,6 @@ "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", @@ -10164,12 +10069,6 @@ "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 149d76f..941f17c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "coverage": "vitest run coverage" }, "dependencies": { - "@heroicons/react": "^1.0.6", "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -28,7 +27,6 @@ "@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", diff --git a/src/App.scss b/src/App.scss index 2cb61ee..a32c70f 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,6 +1,24 @@ html, body, -#root { - width: 100%; +#root, +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/App.tsx b/src/App.tsx index ee638f9..3db4ae4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,16 @@ -import * as React from 'react'; +import React from 'react'; import './App.scss'; -import { MainMenu } from './Components/MainMenu/MainMenu'; -import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Components/SVG/Elements/ContainerModel'; -import Editor, { IEditorState } from './Editor'; +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'; + +interface IAppProps { +} export interface IHistoryState { MainContainer: IContainerModel | null, @@ -12,34 +18,44 @@ export interface IHistoryState { TypeCounters: Record } -interface IAppProps { -} - interface IAppState { + isSidebarOpen: boolean, + isSVGSidebarOpen: boolean, + isHistoryOpen: boolean, configuration: Configuration, - history: IHistoryState[], - historyCurrentStep: number, - isLoaded: boolean + history: Array, + historyCurrentStep: 0 } -export class App extends React.Component { +class App extends React.Component { public state: IAppState; constructor(props: IAppProps) { super(props); this.state = { + isSidebarOpen: true, + isSVGSidebarOpen: false, + isHistoryOpen: false, configuration: { AvailableContainers: [], AvailableSymbols: [], MainContainer: {} as AvailableContainer }, - history: [], - historyCurrentStep: 0, - isLoaded: false - }; + history: [ + { + MainContainer: null, + SelectedContainer: null, + TypeCounters: {} + } + ], + historyCurrentStep: 0 + } as IAppState; } - public NewEditor() { + 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) => { // Set the main container from the given properties of the API @@ -47,7 +63,6 @@ export class App extends React.Component { null, { id: 'main', - parentId: 'null', x: 0, y: 0, width: configuration.MainContainer.Width, @@ -57,67 +72,279 @@ export 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({ + this.setState(prevState => ({ + ...prevState, configuration, - history: + history: history.concat( [ { MainContainer, SelectedContainer: MainContainer, - TypeCounters: {} + TypeCounters: current.TypeCounters } - ], - historyCurrentStep: 0, - isLoaded: true - } as IAppState); + ] + ), + historyCurrentStep: history.length + } as IAppState)); }); } - public LoadEditor(files: FileList | null) { - if (files === null) { + /** + * 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'); + } + + 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 file = files[0]; - const reader = new FileReader(); - reader.addEventListener('load', () => { - const result = reader.result as string; - const editorState: IEditorState = JSON.parse(result); - Revive(editorState); + 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; + } + } - this.setState({ - configuration: editorState.configuration, - history: editorState.history, - historyCurrentStep: editorState.historyCurrentStep, - isLoaded: true + 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); - }); - reader.readAsText(file); } - public render() { - if (this.state.isLoaded) { - return ( -
- -
- ); - } else { - return ( -
- this.NewEditor()} - loadEditor={(files: FileList | null) => this.LoadEditor(files)} - /> -
- ); + /** + * 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 } + +
+ ); } } @@ -127,6 +354,7 @@ 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) { @@ -137,7 +365,8 @@ export async function fetchConfiguration(): Promise { response.json() ) as Configuration; } - return new Promise((resolve) => { + + return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.onreadystatechange = function() { // Call a function when the state changes. @@ -149,31 +378,4 @@ export async function fetchConfiguration(): Promise { }); } -/** - * 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 +export default App; diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 4e599ef..e93f527 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -38,8 +38,7 @@ 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 !== undefined && - this.props.SelectedContainer !== null && + const selectedClass: string = 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 deleted file mode 100644 index 92b19d5..0000000 --- a/src/Components/MainMenu/MainMenu.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index e75e86b..09c156d 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' || key === 'parentId'; // hardcoded + const isDisabled = key === 'id'; // hardcoded groupInput.push(
diff --git a/src/Components/SVG/Elements/ContainerModel.ts b/src/Components/SVG/Elements/ContainerModel.ts index d66f6c8..4b35082 100644 --- a/src/Components/SVG/Elements/ContainerModel.ts +++ b/src/Components/SVG/Elements/ContainerModel.ts @@ -78,13 +78,3 @@ 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.scss b/src/Editor.scss deleted file mode 100644 index 32b3726..0000000 --- a/src/Editor.scss +++ /dev/null @@ -1,22 +0,0 @@ - -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 deleted file mode 100644 index ef82066..0000000 --- a/src/Editor.tsx +++ /dev/null @@ -1,334 +0,0 @@ -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'; -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 -} - -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 { - public state: IEditorState; - - constructor(props: IEditorProps) { - super(props); - this.state = { - isSidebarOpen: true, - isSVGSidebarOpen: false, - isHistoryOpen: false, - configuration: Object.assign({}, 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}`, - parentId: parent.properties.id, - 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); - } - - 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 - */ - 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 } - - -
- ); - } -} - -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/Interfaces/Properties.ts b/src/Interfaces/Properties.ts index 79f4f47..ce386aa 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/Properties.ts @@ -2,7 +2,6 @@ import * as React from 'react'; export default interface Properties extends React.CSSProperties { id: string, - parentId: string | null, x: number, - y: number + y: number, } diff --git a/src/index.scss b/src/index.scss index c7e0d50..7327b1a 100644 --- a/src/index.scss +++ b/src/index.scss @@ -12,7 +12,4 @@ .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 diff --git a/src/main.tsx b/src/main.tsx index 6893f34..95b3daf 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(