Merge pull request 'Update master with latest dev changes' (#7) from dev into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/7
This commit is contained in:
commit
e71e181fc4
19 changed files with 870 additions and 349 deletions
|
@ -21,7 +21,9 @@ module.exports = {
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'space-before-function-paren': ['error', 'never'],
|
'space-before-function-paren': ['error', 'never'],
|
||||||
indent: ['warn', 2],
|
indent: ['warn', 2, { SwitchCase: 1 }],
|
||||||
semi: ['warn', 'always']
|
semi: ['warn', 'always'],
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
101
package-lock.json
generated
101
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"name": "svg-layout-designer-react",
|
"name": "svg-layout-designer-react",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^1.0.6",
|
||||||
"framer-motion": "^6.5.1",
|
"framer-motion": "^6.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||||
"@typescript-eslint/parser": "^5.31.0",
|
"@typescript-eslint/parser": "^5.31.0",
|
||||||
"@vitejs/plugin-react": "^2.0.0",
|
"@vitejs/plugin-react": "^2.0.0",
|
||||||
|
"@vitest/ui": "^0.20.3",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
"eslint-config-standard": "^17.0.0",
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
@ -524,6 +526,14 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.9.5",
|
"version": "0.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
|
||||||
|
@ -721,6 +731,12 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.24.26",
|
"version": "0.24.26",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.26.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.26.tgz",
|
||||||
|
@ -1282,6 +1298,15 @@
|
||||||
"vite": "^3.0.0"
|
"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": {
|
"node_modules/abab": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||||
|
@ -4454,6 +4479,15 @@
|
||||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -5333,6 +5367,20 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
|
@ -5604,6 +5652,15 @@
|
||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
||||||
|
@ -6430,6 +6487,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": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.9.5",
|
"version": "0.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
|
||||||
|
@ -6613,6 +6676,12 @@
|
||||||
"fastq": "^1.6.0"
|
"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": {
|
"@sinclair/typebox": {
|
||||||
"version": "0.24.26",
|
"version": "0.24.26",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.26.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.26.tgz",
|
||||||
|
@ -7013,6 +7082,15 @@
|
||||||
"react-refresh": "^0.14.0"
|
"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": {
|
"abab": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||||
|
@ -9258,6 +9336,12 @@
|
||||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||||
"dev": true
|
"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": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -9859,6 +9943,17 @@
|
||||||
"object-inspect": "^1.9.0"
|
"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": {
|
"slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
|
@ -10069,6 +10164,12 @@
|
||||||
"is-number": "^7.0.0"
|
"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": {
|
"tough-cookie": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"coverage": "vitest run coverage"
|
"coverage": "vitest run coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^1.0.6",
|
||||||
"framer-motion": "^6.5.1",
|
"framer-motion": "^6.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||||
"@typescript-eslint/parser": "^5.31.0",
|
"@typescript-eslint/parser": "^5.31.0",
|
||||||
"@vitejs/plugin-react": "^2.0.0",
|
"@vitejs/plugin-react": "^2.0.0",
|
||||||
|
"@vitest/ui": "^0.20.3",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.8",
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.20.0",
|
||||||
"eslint-config-standard": "^17.0.0",
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
|
22
src/App.scss
22
src/App.scss
|
@ -1,24 +1,6 @@
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root,
|
#root {
|
||||||
svg {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
text {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 800;
|
|
||||||
fill: none;
|
|
||||||
fill-opacity: 0;
|
|
||||||
stroke: #000000;
|
|
||||||
stroke-width: 1px;
|
|
||||||
stroke-linecap: butt;
|
|
||||||
stroke-linejoin: miter;
|
|
||||||
stroke-opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadein {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
}
|
300
src/App.tsx
300
src/App.tsx
|
@ -1,238 +1,123 @@
|
||||||
import React from 'react';
|
import * as React from 'react';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
import Sidebar from './Components/Sidebar/Sidebar';
|
import { MainMenu } from './Components/MainMenu/MainMenu';
|
||||||
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
|
import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Components/SVG/Elements/ContainerModel';
|
||||||
|
import Editor, { IEditorState } from './Editor';
|
||||||
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
||||||
import { Configuration } from './Interfaces/Configuration';
|
import { Configuration } from './Interfaces/Configuration';
|
||||||
import { Container } from './Components/SVG/Elements/Container';
|
|
||||||
import { SVG } from './Components/SVG/SVG';
|
export interface IHistoryState {
|
||||||
|
MainContainer: IContainerModel | null,
|
||||||
|
SelectedContainer: IContainerModel | null,
|
||||||
|
TypeCounters: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
interface IAppProps {
|
interface IAppProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAppState {
|
interface IAppState {
|
||||||
isSidebarOpen: boolean,
|
|
||||||
isSVGSidebarOpen: boolean,
|
|
||||||
configuration: Configuration,
|
configuration: Configuration,
|
||||||
MainContainer: Container | null,
|
history: IHistoryState[],
|
||||||
SelectedContainer: Container | null
|
historyCurrentStep: number,
|
||||||
Counters: Record<string, number>
|
isLoaded: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends React.Component<IAppProps> {
|
export class App extends React.Component<IAppProps> {
|
||||||
public state: IAppState;
|
public state: IAppState;
|
||||||
|
|
||||||
constructor(props: IAppProps) {
|
constructor(props: IAppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isSidebarOpen: true,
|
|
||||||
isSVGSidebarOpen: false,
|
|
||||||
configuration: {
|
configuration: {
|
||||||
AvailableContainers: [],
|
AvailableContainers: [],
|
||||||
AvailableSymbols: [],
|
AvailableSymbols: [],
|
||||||
MainContainer: {} as AvailableContainer
|
MainContainer: {} as AvailableContainer
|
||||||
},
|
},
|
||||||
MainContainer: null,
|
history: [],
|
||||||
SelectedContainer: null,
|
historyCurrentStep: 0,
|
||||||
Counters: {}
|
isLoaded: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public NewEditor() {
|
||||||
// Fetch the configuration from the API
|
// Fetch the configuration from the API
|
||||||
fetchConfiguration().then((configuration: Configuration) => {
|
fetchConfiguration().then((configuration: Configuration) => {
|
||||||
// Set the main container from the given properties of the API
|
// Set the main container from the given properties of the API
|
||||||
const MainContainer = new Container(
|
const MainContainer = new ContainerModel(
|
||||||
|
null,
|
||||||
{
|
{
|
||||||
parent: null,
|
id: 'main',
|
||||||
properties: {
|
parentId: 'null',
|
||||||
id: 'main',
|
x: 0,
|
||||||
x: 0,
|
y: 0,
|
||||||
y: 0,
|
width: configuration.MainContainer.Width,
|
||||||
width: configuration.MainContainer.Width,
|
height: configuration.MainContainer.Height,
|
||||||
height: configuration.MainContainer.Height,
|
fillOpacity: 0,
|
||||||
fillOpacity: 0,
|
stroke: 'black'
|
||||||
stroke: 'black'
|
|
||||||
},
|
|
||||||
children: []
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save the configuration and the new MainContainer
|
// Save the configuration and the new MainContainer
|
||||||
// and default the selected container to it
|
// and default the selected container to it
|
||||||
this.setState(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
configuration,
|
|
||||||
MainContainer,
|
|
||||||
SelectedContainer: MainContainer
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a container
|
|
||||||
* @param container Selected container
|
|
||||||
*/
|
|
||||||
public SelectContainer(container: Container) {
|
|
||||||
this.setState({
|
|
||||||
SelectedContainer: container
|
|
||||||
} as IAppProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
if (this.state.SelectedContainer === null ||
|
|
||||||
this.state.SelectedContainer === undefined) {
|
|
||||||
throw new Error('Property was changed before selecting a Container');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.MainContainer === null ||
|
|
||||||
this.state.MainContainer === undefined) {
|
|
||||||
throw new Error('Property was changed before the main container was added');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pair = {} as Record<string, string | number>;
|
|
||||||
pair[key] = value;
|
|
||||||
const properties = Object.assign(this.state.SelectedContainer.props.properties, pair);
|
|
||||||
const props = {
|
|
||||||
...this.state.SelectedContainer.props,
|
|
||||||
properties
|
|
||||||
};
|
|
||||||
|
|
||||||
const newSelectedContainer = new Container(props);
|
|
||||||
|
|
||||||
const parent = this.state.SelectedContainer.props.parent;
|
|
||||||
if (parent === null) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
SelectedContainer: newSelectedContainer,
|
configuration,
|
||||||
MainContainer: newSelectedContainer
|
history:
|
||||||
});
|
[
|
||||||
return;
|
{
|
||||||
}
|
MainContainer,
|
||||||
|
SelectedContainer: MainContainer,
|
||||||
const index = parent.props.children.indexOf(this.state.SelectedContainer);
|
TypeCounters: {}
|
||||||
parent.props.children[index] = newSelectedContainer;
|
}
|
||||||
|
],
|
||||||
const newMainContainer = new Container(Object.assign({}, this.state.MainContainer.props));
|
historyCurrentStep: 0,
|
||||||
this.setState({
|
isLoaded: true
|
||||||
SelectedContainer: newSelectedContainer,
|
} as IAppState);
|
||||||
MainContainer: newMainContainer
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public LoadEditor(files: FileList | null) {
|
||||||
* Add a new container to a selected container
|
if (files === null) {
|
||||||
* @param type The type of container
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
public AddContainer(type: string): void {
|
|
||||||
if (this.state.SelectedContainer === null ||
|
|
||||||
this.state.SelectedContainer === undefined) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
const editorState: IEditorState = JSON.parse(result);
|
||||||
|
|
||||||
if (this.state.MainContainer === null ||
|
Revive(editorState);
|
||||||
this.state.MainContainer === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the preset properties from the API
|
this.setState({
|
||||||
const properties = this.state.configuration.AvailableContainers.find(option => option.Type === type);
|
configuration: editorState.configuration,
|
||||||
|
history: editorState.history,
|
||||||
|
historyCurrentStep: editorState.historyCurrentStep,
|
||||||
|
isLoaded: true
|
||||||
|
} as IAppState);
|
||||||
|
});
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
if (properties === undefined) {
|
public render() {
|
||||||
throw new Error(`[AddContainer] Object type not found. Found: ${type}`);
|
if (this.state.isLoaded) {
|
||||||
}
|
return (
|
||||||
|
<div>
|
||||||
// Set the counter of the object type in order to assign an unique id
|
<Editor
|
||||||
const newCounters = Object.assign({}, this.state.Counters);
|
configuration={this.state.configuration}
|
||||||
if (newCounters[type] === null ||
|
history={this.state.history}
|
||||||
newCounters[type] === undefined) {
|
historyCurrentStep={this.state.historyCurrentStep}
|
||||||
newCounters[type] = 0;
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
newCounters[type]++;
|
return (
|
||||||
|
<div className='bg-blue-100 h-full w-full'>
|
||||||
|
<MainMenu
|
||||||
|
newEditor={() => this.NewEditor()}
|
||||||
|
loadEditor={(files: FileList | null) => this.LoadEditor(files)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the container
|
|
||||||
const parent = this.state.SelectedContainer;
|
|
||||||
const count = newCounters[type];
|
|
||||||
const container = new Container({
|
|
||||||
parent,
|
|
||||||
properties: {
|
|
||||||
id: `${type}-${count}`,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: properties?.Width,
|
|
||||||
height: parent.props.properties.height,
|
|
||||||
...properties.Style
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
userData: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// And push it the the parent children
|
|
||||||
parent.props.children.push(container);
|
|
||||||
|
|
||||||
// Update the state
|
|
||||||
const newMainContainer = new Container(Object.assign({}, this.state.MainContainer.props));
|
|
||||||
this.setState({
|
|
||||||
MainContainer: newMainContainer,
|
|
||||||
Counters: newCounters
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the application
|
|
||||||
* @returns {JSX.Element} Rendered JSX element
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="App font-sans h-full">
|
|
||||||
<Sidebar
|
|
||||||
componentOptions={this.state.configuration.AvailableContainers}
|
|
||||||
isOpen={this.state.isSidebarOpen}
|
|
||||||
onClick={() => this.ToggleSidebar()}
|
|
||||||
buttonOnClick={(type: string) => this.AddContainer(type)}
|
|
||||||
/>
|
|
||||||
<button className='fixed z-10 top-4 left-4 text-lg bg-blue-200 hover:bg-blue-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg' onClick={() => this.ToggleSidebar()}>☰ Components</button>
|
|
||||||
<ElementsSidebar
|
|
||||||
MainContainer={this.state.MainContainer}
|
|
||||||
SelectedContainer={this.state.SelectedContainer}
|
|
||||||
isOpen={this.state.isSVGSidebarOpen}
|
|
||||||
onClick={() => this.ToggleElementsSidebar()}
|
|
||||||
onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)}
|
|
||||||
selectContainer={(container: Container) => this.SelectContainer(container)}
|
|
||||||
/>
|
|
||||||
<button className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg' onClick={() => this.ToggleElementsSidebar()}>☰ Elements</button>
|
|
||||||
<SVG selected={this.state.SelectedContainer}>
|
|
||||||
{ this.state.MainContainer }
|
|
||||||
</SVG>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +127,6 @@ class App extends React.Component<IAppProps> {
|
||||||
*/
|
*/
|
||||||
export async function fetchConfiguration(): Promise<Configuration> {
|
export async function fetchConfiguration(): Promise<Configuration> {
|
||||||
const url = `${import.meta.env.VITE_API_URL}`;
|
const url = `${import.meta.env.VITE_API_URL}`;
|
||||||
|
|
||||||
// The test library cannot use the Fetch API
|
// The test library cannot use the Fetch API
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (window.fetch) {
|
if (window.fetch) {
|
||||||
|
@ -253,8 +137,7 @@ export async function fetchConfiguration(): Promise<Configuration> {
|
||||||
response.json()
|
response.json()
|
||||||
) as Configuration;
|
) as Configuration;
|
||||||
}
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', url, true);
|
xhr.open('POST', url, true);
|
||||||
xhr.onreadystatechange = function() { // Call a function when the state changes.
|
xhr.onreadystatechange = function() { // Call a function when the state changes.
|
||||||
|
@ -266,4 +149,31 @@ export async function fetchConfiguration(): Promise<Configuration> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,56 +1,63 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Properties } from '../Properties/Properties';
|
import { Properties } from '../Properties/Properties';
|
||||||
import { Container } from '../SVG/Elements/Container';
|
import { IContainerModel, getDepth, MakeIterator } from '../SVG/Elements/ContainerModel';
|
||||||
|
|
||||||
interface IElementsSidebarProps {
|
interface IElementsSidebarProps {
|
||||||
MainContainer: Container | null,
|
MainContainer: IContainerModel | null,
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
SelectedContainer: Container | null,
|
isHistoryOpen: boolean
|
||||||
|
SelectedContainer: IContainerModel | null,
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
onPropertyChange: (key: string, value: string) => void,
|
onPropertyChange: (key: string, value: string) => void,
|
||||||
selectContainer: (container: Container) => void
|
selectContainer: (container: IContainerModel) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
||||||
public iterateChilds(handleContainer: (container: Container) => void): React.ReactNode {
|
public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode {
|
||||||
if (!this.props.MainContainer) {
|
if (!this.props.MainContainer) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = this.props.MainContainer.MakeIterator();
|
const it = MakeIterator(this.props.MainContainer);
|
||||||
for (const container of it) {
|
for (const container of it) {
|
||||||
handleContainer(container);
|
handleContainer(container as IContainerModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
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[] = [];
|
const containerRows: React.ReactNode[] = [];
|
||||||
this.iterateChilds((container: Container) => {
|
this.iterateChilds((container: IContainerModel) => {
|
||||||
const depth: number = container.getDepth();
|
const depth: number = getDepth(container);
|
||||||
const key = container.props.properties.id.toString();
|
const key = container.properties.id.toString();
|
||||||
const text = '|\t'.repeat(depth) + key;
|
const text = '|\t'.repeat(depth) + key;
|
||||||
const selectedClass: string = this.props.SelectedContainer !== null &&
|
const selectedClass: string = this.props.SelectedContainer !== undefined &&
|
||||||
this.props.SelectedContainer.props.properties.id === container.props.properties.id
|
this.props.SelectedContainer !== null &&
|
||||||
|
this.props.SelectedContainer.properties.id === container.properties.id
|
||||||
? 'bg-blue-500 hover:bg-blue-600'
|
? 'bg-blue-500 hover:bg-blue-600'
|
||||||
: 'bg-slate-400 hover:bg-slate-600';
|
: 'bg-slate-400 hover:bg-slate-600';
|
||||||
containerRows.push(
|
containerRows.push(
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 1.2 }}
|
whileTap={{ scale: 1.2 }}
|
||||||
initial={{ opacity: 0, scale: 0 }}
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.150
|
duration: 0.150
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
`w-full elements-sidebar-row whitespace-pre
|
`w-full elements-sidebar-row whitespace-pre
|
||||||
text-left text-sm font-medium transition-all ${selectedClass}`
|
text-left text-sm font-medium transition-all ${selectedClass}`
|
||||||
}
|
}
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() => this.props.selectContainer(container)}>
|
onClick={() => this.props.selectContainer(container)}>
|
||||||
{ text }
|
{ text }
|
||||||
</motion.button>
|
</motion.button>
|
||||||
);
|
);
|
||||||
|
@ -67,7 +74,7 @@ export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
||||||
<div className='overflow-y-auto overflow-x-hidden text-slate-200 flex-grow divide-y divide-solid divide-slate-500'>
|
<div className='overflow-y-auto overflow-x-hidden text-slate-200 flex-grow divide-y divide-solid divide-slate-500'>
|
||||||
{ containerRows }
|
{ containerRows }
|
||||||
</div>
|
</div>
|
||||||
<Properties properties={this.props.SelectedContainer?.GetProperties()} onChange={this.props.onPropertyChange}></Properties>
|
<Properties properties={this.props.SelectedContainer?.properties} onChange={this.props.onPropertyChange}></Properties>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
62
src/Components/History/History.tsx
Normal file
62
src/Components/History/History.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IHistoryState } from '../../App';
|
||||||
|
|
||||||
|
interface IHistoryProps {
|
||||||
|
history: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
isOpen: boolean,
|
||||||
|
onClick: () => void,
|
||||||
|
jumpTo: (move: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class History extends React.Component<IHistoryProps> {
|
||||||
|
public render() {
|
||||||
|
const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64';
|
||||||
|
|
||||||
|
const states = this.props.history.map((step, move) => {
|
||||||
|
const desc = move
|
||||||
|
? `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 (
|
||||||
|
|
||||||
|
<button
|
||||||
|
key={move}
|
||||||
|
onClick={() => this.props.jumpTo(move)}
|
||||||
|
className={
|
||||||
|
`w-full elements-sidebar-row whitespace-pre
|
||||||
|
text-left text-sm font-medium transition-all ${selectedClass}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{desc}{isCurrentText}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// recent first
|
||||||
|
states.reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`fixed flex flex-col bg-slate-400 text-white transition-all h-screen w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
|
||||||
|
<button className='close-button bg-slate-500 hover:bg-slate-700 justify-start' onClick={this.props.onClick}>
|
||||||
|
× Close
|
||||||
|
</button>
|
||||||
|
<div className='bg-slate-600 sidebar-row'>
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
<div className='overflow-y-auto overflow-x-hidden text-slate-300 flex-grow divide-y divide-solid divide-slate-600'>
|
||||||
|
{ states }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
74
src/Components/MainMenu/MainMenu.tsx
Normal file
74
src/Components/MainMenu/MainMenu.tsx
Normal file
|
@ -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<IMainMenuProps> = (props) => {
|
||||||
|
const [windowState, setWindowState] = React.useState(WindowState.MAIN);
|
||||||
|
switch (windowState) {
|
||||||
|
case WindowState.LOAD:
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
|
<form className="flex items-center space-x-6">
|
||||||
|
<label className="block">
|
||||||
|
<span className="sr-only">Choose profile photo</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/json"
|
||||||
|
onChange={e => {
|
||||||
|
props.loadEditor(e.target.files);
|
||||||
|
}}
|
||||||
|
className="block w-full text-sm text-slate-500
|
||||||
|
file:mr-4 file:py-2 file:px-4
|
||||||
|
file:rounded-full file:border-0
|
||||||
|
file:text-sm file:font-semibold
|
||||||
|
file:transition-all
|
||||||
|
file:cursor-pointer
|
||||||
|
file:bg-blue-100 file:text-blue-700
|
||||||
|
hover:file:bg-blue-200
|
||||||
|
"/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
{/* <button
|
||||||
|
onClick={() => setWindowState(WindowState.MAIN)}
|
||||||
|
className='block text-sm
|
||||||
|
mt-8 py-4 px-4
|
||||||
|
rounded-full border-0
|
||||||
|
font-semibold
|
||||||
|
transition-all
|
||||||
|
bg-blue-100 text-blue-700
|
||||||
|
hover:bg-blue-200'
|
||||||
|
>
|
||||||
|
Load
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
onClick={() => setWindowState(WindowState.MAIN)}
|
||||||
|
className='block text-sm
|
||||||
|
mt-8 py-2 px-4
|
||||||
|
rounded-full border-0
|
||||||
|
font-semibold
|
||||||
|
transition-all
|
||||||
|
bg-blue-100 text-blue-700
|
||||||
|
hover:bg-blue-200'
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
|
<button className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button>
|
||||||
|
<button className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -43,7 +43,7 @@ export class Properties extends React.Component<IPropertiesProps, IPropertiesSta
|
||||||
) => {
|
) => {
|
||||||
const id = `property-${key}`;
|
const id = `property-${key}`;
|
||||||
const type = isNaN(Number(value)) ? 'text' : 'number';
|
const type = isNaN(Number(value)) ? 'text' : 'number';
|
||||||
const isDisabled = key === 'id'; // hardcoded
|
const isDisabled = key === 'id' || key === 'parentId'; // hardcoded
|
||||||
groupInput.push(
|
groupInput.push(
|
||||||
<div key={id} className='mt-4'>
|
<div key={id} className='mt-4'>
|
||||||
<label className='text-sm font-medium text-slate-200' htmlFor={id}>{key}</label>
|
<label className='text-sm font-medium text-slate-200' htmlFor={id}>{key}</label>
|
||||||
|
|
|
@ -1,92 +1,22 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Properties from '../../../Interfaces/Properties';
|
import { getDepth, IContainerModel } from './ContainerModel';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
|
|
||||||
interface IContainerProps {
|
export interface IContainerProps {
|
||||||
// eslint-disable-next-line no-use-before-define
|
model: IContainerModel
|
||||||
parent: Container | null,
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
children: Container[],
|
|
||||||
properties: Properties,
|
|
||||||
userData?: Record<string, string | number>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GAP = 50;
|
const GAP = 50;
|
||||||
|
|
||||||
export class Container extends React.Component<IContainerProps> {
|
export class Container extends React.Component<IContainerProps> {
|
||||||
/**
|
|
||||||
* 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<Container, void, unknown> {
|
|
||||||
const queue: Container[] = [this];
|
|
||||||
const visited = new Set<Container>(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
|
* Render the container
|
||||||
* @returns Render the container
|
* @returns Render the container
|
||||||
*/
|
*/
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const containersElements = this.props.children.map(child => child.render());
|
const containersElements = this.props.model.children.map(child => new Container({ model: child } as IContainerProps).render());
|
||||||
const xText = Number(this.props.properties.width) / 2;
|
const xText = Number(this.props.model.properties.width) / 2;
|
||||||
const yText = Number(this.props.properties.height) / 2;
|
const yText = Number(this.props.model.properties.height) / 2;
|
||||||
|
|
||||||
// g style
|
// g style
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
|
@ -98,7 +28,7 @@ export class Container extends React.Component<IContainerProps> {
|
||||||
// Rect style
|
// Rect style
|
||||||
const style = Object.assign(
|
const style = Object.assign(
|
||||||
JSON.parse(JSON.stringify(defaultStyle)),
|
JSON.parse(JSON.stringify(defaultStyle)),
|
||||||
this.props.properties
|
this.props.model.properties
|
||||||
);
|
);
|
||||||
style.x = 0;
|
style.x = 0;
|
||||||
style.y = 0;
|
style.y = 0;
|
||||||
|
@ -106,18 +36,18 @@ export class Container extends React.Component<IContainerProps> {
|
||||||
delete style.width;
|
delete style.width;
|
||||||
|
|
||||||
// Dimension props
|
// Dimension props
|
||||||
const id = `dim-${this.props.properties.id}`;
|
const id = `dim-${this.props.model.properties.id}`;
|
||||||
const xStart: number = 0;
|
const xStart: number = 0;
|
||||||
const xEnd = Number(this.props.properties.width);
|
const xEnd = Number(this.props.model.properties.width);
|
||||||
const y = -(GAP * (this.getDepth() + 1));
|
const y = -(GAP * (getDepth(this.props.model) + 1));
|
||||||
const strokeWidth = 1;
|
const strokeWidth = 1;
|
||||||
const text = (this.props.properties.width ?? 0).toString();
|
const text = (this.props.model.properties.width ?? 0).toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
style={defaultStyle}
|
style={defaultStyle}
|
||||||
transform={`translate(${this.props.properties.x}, ${this.props.properties.y})`}
|
transform={`translate(${this.props.model.properties.x}, ${this.props.model.properties.y})`}
|
||||||
key={`container-${this.props.properties.id}`}
|
key={`container-${this.props.model.properties.id}`}
|
||||||
>
|
>
|
||||||
<Dimension
|
<Dimension
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -128,8 +58,8 @@ export class Container extends React.Component<IContainerProps> {
|
||||||
text={text}
|
text={text}
|
||||||
/>
|
/>
|
||||||
<rect
|
<rect
|
||||||
width={this.props.properties.width}
|
width={this.props.model.properties.width}
|
||||||
height={this.props.properties.height}
|
height={this.props.model.properties.height}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
</rect>
|
</rect>
|
||||||
|
@ -137,7 +67,7 @@ export class Container extends React.Component<IContainerProps> {
|
||||||
x={xText}
|
x={xText}
|
||||||
y={yText}
|
y={yText}
|
||||||
>
|
>
|
||||||
{this.props.properties.id}
|
{this.props.model.properties.id}
|
||||||
</text>
|
</text>
|
||||||
{ containersElements }
|
{ containersElements }
|
||||||
</g>
|
</g>
|
||||||
|
|
90
src/Components/SVG/Elements/ContainerModel.ts
Normal file
90
src/Components/SVG/Elements/ContainerModel.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import Properties from '../../../Interfaces/Properties';
|
||||||
|
|
||||||
|
export interface IContainerModel {
|
||||||
|
children: IContainerModel[],
|
||||||
|
parent: IContainerModel | null,
|
||||||
|
properties: Properties,
|
||||||
|
userData: Record<string, string | number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContainerModel implements IContainerModel {
|
||||||
|
public children: IContainerModel[];
|
||||||
|
public parent: IContainerModel | null;
|
||||||
|
public properties: Properties;
|
||||||
|
public userData: Record<string, string | number>;
|
||||||
|
|
||||||
|
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<IContainerModel, void, unknown> {
|
||||||
|
const queue: IContainerModel[] = [root];
|
||||||
|
const visited = new Set<IContainerModel>(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];
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -1,25 +1,25 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Container } from './Container';
|
import { ContainerModel, getDepth, MakeIterator } from './ContainerModel';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
|
|
||||||
interface IDimensionLayerProps {
|
interface IDimensionLayerProps {
|
||||||
isHidden: boolean,
|
isHidden: boolean,
|
||||||
roots: Container | Container[] | null,
|
roots: ContainerModel | ContainerModel[] | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const GAP: number = 50;
|
const GAP: number = 50;
|
||||||
|
|
||||||
const getDimensionsNodes = (root: Container): React.ReactNode[] => {
|
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
||||||
const it = root.MakeIterator();
|
const it = MakeIterator(root);
|
||||||
const dimensions: React.ReactNode[] = [];
|
const dimensions: React.ReactNode[] = [];
|
||||||
for (const container of it) {
|
for (const container of it) {
|
||||||
// WARN: this might be dangerous later when using other units/rules
|
// 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 id = `dim-${container.properties.id}`;
|
||||||
const xStart: number = container.props.properties.x;
|
const xStart: number = container.properties.x;
|
||||||
const xEnd = xStart + width;
|
const xEnd = xStart + width;
|
||||||
const y = -(GAP * (container.getDepth() + 1));
|
const y = -(GAP * (getDepth(container) + 1));
|
||||||
const strokeWidth = 1;
|
const strokeWidth = 1;
|
||||||
const text = width.toString();
|
const text = width.toString();
|
||||||
const dimension = new Dimension({
|
const dimension = new Dimension({
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Container } from './Container';
|
import { IContainerModel, getAbsolutePosition } from './ContainerModel';
|
||||||
|
|
||||||
interface ISelectorProps {
|
interface ISelectorProps {
|
||||||
selected: Container | null
|
selected: IContainerModel | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Selector: React.FC<ISelectorProps> = (props) => {
|
export const Selector: React.FC<ISelectorProps> = (props) => {
|
||||||
|
@ -13,10 +13,10 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [x, y] = props.selected.getAbsolutePosition();
|
const [x, y] = getAbsolutePosition(props.selected);
|
||||||
const [width, height] = [
|
const [width, height] = [
|
||||||
props.selected.props.properties.width,
|
props.selected.properties.width,
|
||||||
props.selected.props.properties.height
|
props.selected.properties.height
|
||||||
];
|
];
|
||||||
const style = {
|
const style = {
|
||||||
stroke: '#3B82F6', // tw blue-500
|
stroke: '#3B82F6', // tw blue-500
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom';
|
import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom';
|
||||||
import { Container } from './Elements/Container';
|
import { Container } from './Elements/Container';
|
||||||
|
import { ContainerModel } from './Elements/ContainerModel';
|
||||||
import { Selector } from './Elements/Selector';
|
import { Selector } from './Elements/Selector';
|
||||||
|
|
||||||
interface ISVGProps {
|
interface ISVGProps {
|
||||||
children: Container | Container[] | null,
|
children: ContainerModel | ContainerModel[] | null,
|
||||||
selected: Container | null
|
selected: ContainerModel | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISVGState {
|
interface ISVGState {
|
||||||
|
@ -65,9 +66,9 @@ export class SVG extends React.Component<ISVGProps> {
|
||||||
|
|
||||||
let children: React.ReactNode | React.ReactNode[] = [];
|
let children: React.ReactNode | React.ReactNode[] = [];
|
||||||
if (Array.isArray(this.props.children)) {
|
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) {
|
} else if (this.props.children !== null) {
|
||||||
children = this.props.children.render();
|
children = new Container({ model: this.props.children }).render();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
22
src/Editor.scss
Normal file
22
src/Editor.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
fill: none;
|
||||||
|
fill-opacity: 0;
|
||||||
|
stroke: #000000;
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke-linecap: butt;
|
||||||
|
stroke-linejoin: miter;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
334
src/Editor.tsx
Normal file
334
src/Editor.tsx
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
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<IHistoryState>,
|
||||||
|
historyCurrentStep: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEditorState {
|
||||||
|
isSidebarOpen: boolean,
|
||||||
|
isSVGSidebarOpen: boolean,
|
||||||
|
isHistoryOpen: boolean,
|
||||||
|
history: Array<IHistoryState>,
|
||||||
|
historyCurrentStep: number,
|
||||||
|
// do not use it, use props.configuration
|
||||||
|
// only used for serialization purpose
|
||||||
|
configuration: Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
class Editor extends React.Component<IEditorProps> {
|
||||||
|
public state: IEditorState;
|
||||||
|
|
||||||
|
constructor(props: IEditorProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
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 (
|
||||||
|
<div className="App font-sans h-full">
|
||||||
|
<Sidebar
|
||||||
|
componentOptions={this.props.configuration.AvailableContainers}
|
||||||
|
isOpen={this.state.isSidebarOpen}
|
||||||
|
onClick={() => this.ToggleSidebar()}
|
||||||
|
buttonOnClick={(type: string) => this.AddContainer(type)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='fixed z-10 top-4 left-4 text-lg bg-blue-200 hover:bg-blue-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||||
|
onClick={() => this.ToggleSidebar()}
|
||||||
|
>
|
||||||
|
☰ Components
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ElementsSidebar
|
||||||
|
MainContainer={current.MainContainer}
|
||||||
|
SelectedContainer={current.SelectedContainer}
|
||||||
|
isOpen={this.state.isSVGSidebarOpen}
|
||||||
|
isHistoryOpen={this.state.isHistoryOpen}
|
||||||
|
onClick={() => this.ToggleElementsSidebar()}
|
||||||
|
onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)}
|
||||||
|
selectContainer={(container: ContainerModel) => this.SelectContainer(container)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||||
|
onClick={() => this.ToggleElementsSidebar()}
|
||||||
|
>
|
||||||
|
☰ Elements
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<History
|
||||||
|
history={this.state.history}
|
||||||
|
historyCurrentStep={this.state.historyCurrentStep}
|
||||||
|
isOpen={this.state.isHistoryOpen}
|
||||||
|
onClick={() => this.ToggleHistory()}
|
||||||
|
jumpTo={(move) => { this.jumpTo(move); }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='fixed z-10 top-4 right-72 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||||
|
onClick={() => this.ToggleHistory()}>
|
||||||
|
☰ History
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<SVG selected={current.SelectedContainer}>
|
||||||
|
{ current.MainContainer }
|
||||||
|
</SVG>
|
||||||
|
<button
|
||||||
|
className='fixed transition-all right-12 bottom-10 w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'
|
||||||
|
title='Export as JSON'
|
||||||
|
onClick={() => this.SaveEditor()}
|
||||||
|
>
|
||||||
|
<UploadIcon className="h-full w-full text-white align-middle items-center justify-center" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
||||||
|
|
||||||
export default interface Properties extends React.CSSProperties {
|
export default interface Properties extends React.CSSProperties {
|
||||||
id: string,
|
id: string,
|
||||||
|
parentId: string | null,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,7 @@
|
||||||
.close-button {
|
.close-button {
|
||||||
@apply transition-all w-full h-auto p-4 flex
|
@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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import { App } from './App';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue