From 293af451448d486dc2cea67e75635aa1f962f5dc Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 5 Aug 2022 15:38:44 -0400 Subject: [PATCH] Refactor Editor and module functions (#15) Moved all module functions to separate utils modules Replaced standard with standard with typescript Extracted UI elements to separate component Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/15 --- .eslintrc.cjs | 11 +- package-lock.json | 29 +++ package.json | 1 + src/App.tsx | 136 ++++++-------- .../ElementsSidebar/ElementsSidebar.tsx | 19 +- .../FloatingButton/FloatingButton.tsx | 2 +- src/Components/History/History.tsx | 12 +- src/Components/MainMenu/MainMenu.tsx | 2 +- src/Components/Properties/Properties.tsx | 6 +- src/Components/SVG/Elements/Container.tsx | 7 +- src/Components/SVG/Elements/Dimension.tsx | 18 +- .../SVG/Elements/DimensionLayer.tsx | 7 +- src/Components/SVG/Elements/Selector.tsx | 7 +- src/Components/SVG/SVG.tsx | 49 ++--- src/Components/Sidebar/Sidebar.tsx | 8 +- src/Components/UI/UI.tsx | 139 +++++++++++++++ src/Editor.tsx | 168 ++++-------------- src/Enums/AddingBehavior.ts | 4 +- src/Enums/XPositionReference.ts | 6 +- src/Interfaces/Configuration.ts | 6 +- src/Interfaces/ContainerModel.ts | 70 +------- src/Interfaces/Image.ts | 8 +- src/Interfaces/Properties.ts | 6 +- src/utils/itertools.ts | 65 +++++++ src/utils/saveload.ts | 54 ++++++ src/utils/test-utils.tsx | 4 +- 26 files changed, 477 insertions(+), 367 deletions(-) create mode 100644 src/Components/UI/UI.tsx create mode 100644 src/utils/itertools.ts create mode 100644 src/utils/saveload.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 86a1866..b72be75 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { }, extends: [ 'plugin:react/recommended', - 'standard' + 'standard-with-typescript' ], parser: '@typescript-eslint/parser', parserOptions: { @@ -13,7 +13,8 @@ module.exports = { jsx: true }, ecmaVersion: 'latest', - sourceType: 'module' + sourceType: 'module', + project: './tsconfig.json' }, plugins: [ 'react', @@ -21,9 +22,11 @@ module.exports = { ], rules: { 'space-before-function-paren': ['error', 'never'], + '@typescript-eslint/space-before-function-paren': ['error', 'never'], indent: ['warn', 2, { SwitchCase: 1 }], - semi: ['warn', 'always'], + semi: 'off', + '@typescript-eslint/semi': ['warn', 'always'], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'error' + '@typescript-eslint/no-unused-vars': 'error', } }; diff --git a/package-lock.json b/package-lock.json index 83e5fcb..285432c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "autoprefixer": "^10.4.8", "eslint": "^8.20.0", "eslint-config-standard": "^17.0.0", + "eslint-config-standard-with-typescript": "^22.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.2.4", "eslint-plugin-promise": "^6.0.0", @@ -2697,6 +2698,24 @@ "eslint-plugin-promise": "^6.0.0" } }, + "node_modules/eslint-config-standard-with-typescript": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-22.0.0.tgz", + "integrity": "sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "typescript": "*" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", @@ -8093,6 +8112,16 @@ "dev": true, "requires": {} }, + "eslint-config-standard-with-typescript": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-22.0.0.tgz", + "integrity": "sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + } + }, "eslint-import-resolver-node": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", diff --git a/package.json b/package.json index 149d76f..b4e99f5 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "autoprefixer": "^10.4.8", "eslint": "^8.20.0", "eslint-config-standard": "^17.0.0", + "eslint-config-standard-with-typescript": "^22.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.2.4", "eslint-plugin-promise": "^6.0.0", diff --git a/src/App.tsx b/src/App.tsx index 2b8c7fd..705e40d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,27 @@ import * as React from 'react'; import './App.scss'; import { MainMenu } from './Components/MainMenu/MainMenu'; -import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; +import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; import Editor, { IEditorState } from './Editor'; -import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; +import { Revive } from './utils/saveload'; export interface IHistoryState { - MainContainer: IContainerModel | null, - SelectedContainer: IContainerModel | null, - SelectedContainerId: string, + MainContainer: IContainerModel | null + SelectedContainer: IContainerModel | null + SelectedContainerId: string TypeCounters: Record } +// App will never have props +// eslint-disable-next-line @typescript-eslint/no-empty-interface interface IAppProps { } interface IAppState { - configuration: Configuration, - history: IHistoryState[], - historyCurrentStep: number, + configuration: Configuration + history: IHistoryState[] + historyCurrentStep: number isLoaded: boolean } @@ -32,7 +34,12 @@ export class App extends React.Component { configuration: { AvailableContainers: [], AvailableSymbols: [], - MainContainer: {} as AvailableContainer + MainContainer: { + Type: 'EmptyContainer', + Width: 3000, + Height: 200, + Style: {} + } }, history: [], historyCurrentStep: 0, @@ -40,7 +47,7 @@ export class App extends React.Component { }; } - componentDidMount() { + componentDidMount(): void { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const state = urlParams.get('state'); @@ -50,35 +57,39 @@ export class App extends React.Component { } fetch(state) - .then((response) => response.json()) + .then( + async(response) => await response.json(), + (error) => { throw new Error(error); } + ) .then((data: IEditorState) => { this.LoadState(data); - }); + }, (error) => { throw new Error(error); }); } - public NewEditor() { + public NewEditor(): void { // 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 - const MainContainer = new ContainerModel( - null, - { - id: 'main', - parentId: 'null', - x: 0, - y: 0, - width: configuration.MainContainer.Width, - height: configuration.MainContainer.Height, - fillOpacity: 0, - stroke: 'black' - } - ); + const MainContainer = new ContainerModel( + null, + { + id: 'main', + parentId: 'null', + x: 0, + y: 0, + width: configuration.MainContainer.Width, + height: configuration.MainContainer.Height, + fillOpacity: 0, + stroke: 'black' + } + ); - // Save the configuration and the new MainContainer - // and default the selected container to it - this.setState({ - configuration, - history: + // Save the configuration and the new MainContainer + // and default the selected container to it + this.setState({ + configuration, + history: [ { MainContainer, @@ -86,13 +97,14 @@ export class App extends React.Component { TypeCounters: {} } ], - historyCurrentStep: 0, - isLoaded: true - } as IAppState); - }); + historyCurrentStep: 0, + isLoaded: true + }); + }, (error) => { throw new Error(error); } + ); } - public LoadEditor(files: FileList | null) { + public LoadEditor(files: FileList | null): void { if (files === null) { return; } @@ -107,7 +119,7 @@ export class App extends React.Component { reader.readAsText(file); } - private LoadState(editorState: IEditorState) { + private LoadState(editorState: IEditorState): void { Revive(editorState); this.setState({ @@ -115,10 +127,10 @@ export class App extends React.Component { history: editorState.history, historyCurrentStep: editorState.historyCurrentStep, isLoaded: true - } as IAppState); + }); } - public render() { + public render(): JSX.Element { if (this.state.isLoaded) { return (
@@ -149,16 +161,17 @@ 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 + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (window.fetch) { return await fetch(url, { method: 'POST' }) - .then((response) => - response.json() + .then(async(response) => + await response.json() ) as Configuration; } - return new Promise((resolve) => { + return await new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.onreadystatechange = function() { // Call a function when the state changes. @@ -169,38 +182,3 @@ export async function fetchConfiguration(): Promise { xhr.send(); }); } - -/** - * Revive the Editor state - * by setting the containers references to their parent - * @param editorState Editor state - */ -function Revive(editorState: IEditorState): void { - const history = editorState.history; - for (const state of history) { - if (state.MainContainer === null || state.MainContainer === undefined) { - continue; - } - - const it = MakeIterator(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; - } - - const selected = findContainerById(state.MainContainer, state.SelectedContainerId); - if (selected === undefined) { - state.SelectedContainer = null; - continue; - } - state.SelectedContainer = selected; - } -} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index c4ea51a..caa6081 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,31 +1,32 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; -import { IContainerModel, getDepth, MakeIterator } from '../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../Interfaces/ContainerModel'; +import { getDepth, MakeIterator } from '../../utils/itertools'; interface IElementsSidebarProps { - MainContainer: IContainerModel | null, - isOpen: boolean, + MainContainer: IContainerModel | null + isOpen: boolean isHistoryOpen: boolean - SelectedContainer: IContainerModel | null, - onClick: () => void, - onPropertyChange: (key: string, value: string) => void, + SelectedContainer: IContainerModel | null + onClick: () => void + onPropertyChange: (key: string, value: string) => void selectContainer: (container: IContainerModel) => void } export class ElementsSidebar extends React.PureComponent { public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { - if (!this.props.MainContainer) { + if (this.props.MainContainer == null) { return null; } const it = MakeIterator(this.props.MainContainer); for (const container of it) { - handleContainer(container as IContainerModel); + handleContainer(container); } } - public render() { + public render(): JSX.Element { let isOpenClasses = '-right-64'; if (this.props.isOpen) { isOpenClasses = this.props.isHistoryOpen diff --git a/src/Components/FloatingButton/FloatingButton.tsx b/src/Components/FloatingButton/FloatingButton.tsx index 544c2bd..3145f45 100644 --- a/src/Components/FloatingButton/FloatingButton.tsx +++ b/src/Components/FloatingButton/FloatingButton.tsx @@ -9,7 +9,7 @@ interface IFloatingButtonProps { const toggleState = ( isHidden: boolean, setHidden: React.Dispatch> -) => { +): void => { setHidden(!isHidden); }; diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index e77a8c7..7487de1 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -2,19 +2,19 @@ import * as React from 'react'; import { IHistoryState } from '../../App'; interface IHistoryProps { - history: IHistoryState[], - historyCurrentStep: number, - isOpen: boolean, - onClick: () => void, + history: IHistoryState[] + historyCurrentStep: number + isOpen: boolean + onClick: () => void jumpTo: (move: number) => void } export class History extends React.PureComponent { - public render() { + public render(): JSX.Element { const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; const states = this.props.history.map((step, move) => { - const desc = move + const desc = move > 0 ? `Go to modification n°${move}` : 'Go to the beginning'; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index 92b19d5..a9b3da2 100644 --- a/src/Components/MainMenu/MainMenu.tsx +++ b/src/Components/MainMenu/MainMenu.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; interface IMainMenuProps { - newEditor: () => void; + newEditor: () => void loadEditor: (files: FileList | null) => void } diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 65cfefd..0d30d3d 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import ContainerProperties from '../../Interfaces/Properties'; interface IPropertiesProps { - properties?: ContainerProperties, + properties?: ContainerProperties onChange: (key: string, value: string) => void } export class Properties extends React.PureComponent { - public render() { + public render(): JSX.Element { if (this.props.properties === undefined) { return
; } @@ -27,7 +27,7 @@ export class Properties extends React.PureComponent { public handleProperties = ( [key, value]: [string, string | number], groupInput: React.ReactNode[] - ) => { + ): void => { const id = `property-${key}`; const type = 'text'; const isDisabled = key === 'id' || key === 'parentId'; // hardcoded diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index a352bcd..e64739c 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { getDepth, IContainerModel } from '../../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../../Interfaces/ContainerModel'; +import { getDepth } from '../../../utils/itertools'; import { Dimension } from './Dimension'; export interface IContainerProps { @@ -20,11 +21,11 @@ export class Container extends React.PureComponent { const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`; // g style - const defaultStyle = { + const defaultStyle: React.CSSProperties = { transitionProperty: 'all', transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionDuration: '150ms' - } as React.CSSProperties; + }; // Rect style const style = Object.assign( diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 96ab2c9..612b3f7 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,19 +1,19 @@ import * as React from 'react'; interface IDimensionProps { - id: string; - xStart: number; - xEnd: number; - y: number; - text: string; - strokeWidth: number; + id: string + xStart: number + xEnd: number + y: number + text: string + strokeWidth: number } export class Dimension extends React.PureComponent { - public render() { - const style = { + public render(): JSX.Element { + const style: React.CSSProperties = { stroke: 'black' - } as React.CSSProperties; + }; return ( = (props) => { props.selected.properties.width, props.selected.properties.height ]; - const style = { + const style: React.CSSProperties = { stroke: '#3B82F6', // tw blue-500 strokeWidth: 4, fillOpacity: 0, @@ -26,7 +27,7 @@ export const Selector: React.FC = (props) => { transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionDuration: '150ms', animation: 'fadein 750ms ease-in alternate infinite' - } as React.CSSProperties; + }; return ( { - public state: ISVGState; public static ID = 'svg'; + public state: ISVGState; constructor(props: ISVGProps) { super(props); this.state = { - value: { - viewerWidth: window.innerWidth, - viewerHeight: window.innerHeight - } as Value, - tool: TOOL_PAN + viewerWidth: window.innerWidth, + viewerHeight: window.innerHeight }; } - render() { + resizeViewBox(): void { + this.setState({ + viewerWidth: window.innerWidth, + viewerHeight: window.innerHeight + }); + } + + componentDidMount(): void { + window.addEventListener('resize', this.resizeViewBox.bind(this)); + } + + componentWillUnmount(): void { + window.removeEventListener('resize', this.resizeViewBox.bind(this)); + } + + render(): JSX.Element { const xmlns = ''; const properties = { @@ -49,13 +61,11 @@ export class SVG extends React.PureComponent { return (
- this.setState({ value })} - tool={this.state.tool} onChangeTool={tool => this.setState({ tool })} miniatureProps={{ position: 'left', background: '#616264', @@ -67,9 +77,8 @@ export class SVG extends React.PureComponent { { children } - +
- ); }; } diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index dd34812..b474cc9 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -3,13 +3,13 @@ import { AvailableContainer } from '../../Interfaces/AvailableContainer'; interface ISidebarProps { componentOptions: AvailableContainer[] - isOpen: boolean; - onClick: () => void; - buttonOnClick: (type: string) => void; + isOpen: boolean + onClick: () => void + buttonOnClick: (type: string) => void } export default class Sidebar extends React.PureComponent { - public render() { + public render(): JSX.Element { const listElements = this.props.componentOptions.map(componentOption => + + this.ToggleElementsSidebar()} + onPropertyChange={this.props.OnPropertyChange} + selectContainer={this.props.SelectContainer} + /> + + + this.ToggleHistory()} + jumpTo={this.props.LoadState} + /> + + + + + + + + ); + } +} diff --git a/src/Editor.tsx b/src/Editor.tsx index 89c38aa..8cb75f2 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,28 +1,22 @@ import React from 'react'; -import { UploadIcon, PhotographIcon } 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, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; -import Properties from './Interfaces/Properties'; +import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; +import { findContainerById, MakeIterator } from './utils/itertools'; import { IHistoryState } from './App'; -import FloatingButton from './Components/FloatingButton/FloatingButton'; +import { getCircularReplacer } from './utils/saveload'; +import { UI } from './Components/UI/UI'; interface IEditorProps { - configuration: Configuration, - history: Array, + configuration: Configuration + history: IHistoryState[] historyCurrentStep: number } export interface IEditorState { - isSidebarOpen: boolean, - isElementsSidebarOpen: boolean, - isHistoryOpen: boolean, - history: Array, - historyCurrentStep: number, + history: IHistoryState[] + historyCurrentStep: number // do not use it, use props.configuration // only used for serialization purpose configuration: Configuration @@ -34,50 +28,20 @@ class Editor extends React.Component { constructor(props: IEditorProps) { super(props); this.state = { - isSidebarOpen: true, - isElementsSidebarOpen: 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({ - isElementsSidebarOpen: !this.state.isElementsSidebarOpen - } 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) { + public SelectContainer(container: ContainerModel): void { const history = this.getCurrentHistory(); const current = history[history.length - 1]; @@ -100,7 +64,7 @@ class Editor extends React.Component { SelectedContainerId: SelectedContainer.properties.id }]), historyCurrentStep: history.length - } as IEditorState); + }); } /** @@ -134,7 +98,7 @@ class Editor extends React.Component { TypeCounters: Object.assign({}, current.TypeCounters) }]), historyCurrentStep: history.length - } as IEditorState); + }); return; } @@ -156,7 +120,7 @@ class Editor extends React.Component { TypeCounters: Object.assign({}, current.TypeCounters) }]), historyCurrentStep: history.length - } as IEditorState); + }); } /** @@ -196,8 +160,7 @@ class Editor extends React.Component { const count = newCounters[type]; // Create maincontainer model - const structure: IContainerModel = structuredClone(current.MainContainer); - const clone = Object.assign(new ContainerModel(null, {} as Properties), structure); + const clone: IContainerModel = structuredClone(current.MainContainer); // Find the parent const it = MakeIterator(clone); @@ -230,7 +193,7 @@ class Editor extends React.Component { width: properties?.Width, height: parent.properties.height, ...properties.Style - } as Properties, + }, [], { type @@ -249,16 +212,16 @@ class Editor extends React.Component { SelectedContainerId: parent.properties.id }]), historyCurrentStep: history.length - } as IEditorState); + }); } - public jumpTo(move: number): void { + public LoadState(move: number): void { this.setState({ historyCurrentStep: move - } as IEditorState); + }); } - public SaveEditorAsJSON() { + public SaveEditorAsJSON(): void { const exportName = 'state'; const spaces = import.meta.env.DEV ? 4 : 0; const data = JSON.stringify(this.state, getCircularReplacer(), spaces); @@ -271,7 +234,7 @@ class Editor extends React.Component { downloadAnchorNode.remove(); } - public SaveEditorAsSVG() { + public SaveEditorAsSVG(): void { const svgWrapper = document.getElementById(SVG.ID) as HTMLElement; const svg = svgWrapper.querySelector('svg') as SVGSVGElement; const preface = '\r\n'; @@ -289,59 +252,22 @@ class Editor extends React.Component { * Render the application * @returns {JSX.Element} Rendered JSX element */ - render() { + render(): JSX.Element { const current = this.getCurrentHistoryState(); - let buttonRightOffsetClasses = 'right-12'; - if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) { - buttonRightOffsetClasses = 'right-72'; - } - if (this.state.isHistoryOpen && this.state.isElementsSidebarOpen) { - buttonRightOffsetClasses = 'right-[544px]'; - } 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); }} + AvailableContainers={this.state.configuration.AvailableContainers} + SelectContainer={(container) => this.SelectContainer(container)} + OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} + AddContainer={(type) => this.AddContainer(type)} + SaveEditorAsJSON={() => this.SaveEditorAsJSON()} + SaveEditorAsSVG={() => this.SaveEditorAsSVG()} + LoadState={(move) => this.LoadState(move)} /> - - { > { current.MainContainer } - - - - -
); } } -const getCircularReplacer = () => { - const seen = new WeakSet(); - return (key: any, value: object | null) => { - if (key === 'parent') { - return; - } - - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return; - } - seen.add(value); - } - return value; - }; -}; - export default Editor; diff --git a/src/Enums/AddingBehavior.ts b/src/Enums/AddingBehavior.ts index ddf722e..fb6ae67 100644 --- a/src/Enums/AddingBehavior.ts +++ b/src/Enums/AddingBehavior.ts @@ -1,4 +1,4 @@ export enum AddingBehavior { - InsertInto, - Replace + InsertInto, + Replace } diff --git a/src/Enums/XPositionReference.ts b/src/Enums/XPositionReference.ts index ec8108a..8571167 100644 --- a/src/Enums/XPositionReference.ts +++ b/src/Enums/XPositionReference.ts @@ -1,5 +1,5 @@ export enum XPositionReference { - Left, - Center, - Right + Left, + Center, + Right } diff --git a/src/Interfaces/Configuration.ts b/src/Interfaces/Configuration.ts index 2127299..f8d4854 100644 --- a/src/Interfaces/Configuration.ts +++ b/src/Interfaces/Configuration.ts @@ -3,7 +3,7 @@ import { AvailableSymbolModel } from './AvailableSymbol'; /** Model of configuration for the application to configure it */ export interface Configuration { - AvailableContainers: AvailableContainer[]; - AvailableSymbols: AvailableSymbolModel[]; - MainContainer: AvailableContainer; + AvailableContainers: AvailableContainer[] + AvailableSymbols: AvailableSymbolModel[] + MainContainer: AvailableContainer } diff --git a/src/Interfaces/ContainerModel.ts b/src/Interfaces/ContainerModel.ts index 787cf91..1c70ae3 100644 --- a/src/Interfaces/ContainerModel.ts +++ b/src/Interfaces/ContainerModel.ts @@ -1,9 +1,9 @@ import Properties from './Properties'; export interface IContainerModel { - children: IContainerModel[], - parent: IContainerModel | null, - properties: Properties, + children: IContainerModel[] + parent: IContainerModel | null + properties: Properties userData: Record } @@ -24,67 +24,3 @@ export class ContainerModel implements IContainerModel { this.userData = userData; } }; - -/** - * Returns a Generator iterating of over the children depth-first - */ -export function * MakeIterator(root: IContainerModel): Generator { - const queue: IContainerModel[] = [root]; - const visited = new Set(queue); - while (queue.length > 0) { - const container = queue.pop() as IContainerModel; - - yield container; - - // if this reverse() gets costly, replace it by a simple for - container.children.forEach((child) => { - if (visited.has(child)) { - return; - } - visited.add(child); - queue.push(child); - }); - } -} - -/** - * Returns the depth of the container - * @returns The depth of the container - */ -export function getDepth(parent: IContainerModel) { - let depth = 0; - - let current: IContainerModel | null = parent; - while (current != null) { - depth++; - current = current.parent; - } - - return depth; -} - -/** - * Returns the absolute position by iterating to the parent - * @returns The absolute position of the container - */ -export function getAbsolutePosition(container: IContainerModel): [number, number] { - let x = Number(container.properties.x); - let y = Number(container.properties.y); - let current = container.parent; - while (current != null) { - x += Number(current.properties.x); - y += Number(current.properties.y); - current = current.parent; - } - return [x, y]; -} - -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/Interfaces/Image.ts b/src/Interfaces/Image.ts index 723d04b..b839b09 100644 --- a/src/Interfaces/Image.ts +++ b/src/Interfaces/Image.ts @@ -1,7 +1,7 @@ /** Model of an image with multiple source */ export interface Image { - Name: string; - Url: string; - Base64Image: string; - Svg: string; + Name: string + Url: string + Base64Image: string + Svg: string } diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/Properties.ts index 79f4f47..ed185f2 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/Properties.ts @@ -1,8 +1,8 @@ import * as React from 'react'; export default interface Properties extends React.CSSProperties { - id: string, - parentId: string | null, - x: number, + id: string + parentId: string | null + x: number y: number } diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts new file mode 100644 index 0000000..031b497 --- /dev/null +++ b/src/utils/itertools.ts @@ -0,0 +1,65 @@ +import { IContainerModel } from '../Interfaces/ContainerModel'; + +/** + * Returns a Generator iterating of over the children depth-first + */ +export function * MakeIterator(root: IContainerModel): Generator { + const queue: IContainerModel[] = [root]; + const visited = new Set(queue); + while (queue.length > 0) { + const container = queue.pop() as IContainerModel; + + yield container; + + // if this reverse() gets costly, replace it by a simple for + container.children.forEach((child) => { + if (visited.has(child)) { + return; + } + visited.add(child); + queue.push(child); + }); + } +} + +/** + * Returns the depth of the container + * @returns The depth of the container + */ +export function getDepth(parent: IContainerModel): number { + 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; +} diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts new file mode 100644 index 0000000..8993ad4 --- /dev/null +++ b/src/utils/saveload.ts @@ -0,0 +1,54 @@ +import { findContainerById, MakeIterator } from './itertools'; +import { IEditorState } from '../Editor'; + +/** + * Revive the Editor state + * by setting the containers references to their parent + * @param editorState Editor state + */ +export function Revive(editorState: IEditorState): void { + const history = editorState.history; + for (const state of history) { + if (state.MainContainer === null || state.MainContainer === undefined) { + continue; + } + + const it = MakeIterator(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; + } + + const selected = findContainerById(state.MainContainer, state.SelectedContainerId); + if (selected === undefined) { + state.SelectedContainer = null; + continue; + } + state.SelectedContainer = selected; + } +} + +export const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => { + const seen = new WeakSet(); + return (key: any, value: object | null) => { + if (key === 'parent') { + return; + } + + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx index bb5dc8b..139850e 100644 --- a/src/utils/test-utils.tsx +++ b/src/utils/test-utils.tsx @@ -1,13 +1,13 @@ /* eslint-disable import/export */ import * as React from 'react'; -import { cleanup, render } from '@testing-library/react'; +import { cleanup, render, RenderResult } from '@testing-library/react'; import { afterEach } from 'vitest'; afterEach(() => { cleanup(); }); -const customRender = (ui: React.ReactElement, options = {}) => +const customRender = (ui: React.ReactElement, options = {}): RenderResult => render(ui, { // wrap provider(s) here if needed wrapper: ({ children }) => children,