diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b72be75..86a1866 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { }, extends: [ 'plugin:react/recommended', - 'standard-with-typescript' + 'standard' ], parser: '@typescript-eslint/parser', parserOptions: { @@ -13,8 +13,7 @@ module.exports = { jsx: true }, ecmaVersion: 'latest', - sourceType: 'module', - project: './tsconfig.json' + sourceType: 'module' }, plugins: [ 'react', @@ -22,11 +21,9 @@ module.exports = { ], rules: { 'space-before-function-paren': ['error', 'never'], - '@typescript-eslint/space-before-function-paren': ['error', 'never'], indent: ['warn', 2, { SwitchCase: 1 }], - semi: 'off', - '@typescript-eslint/semi': ['warn', 'always'], + 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 285432c..83e5fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "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", @@ -2698,24 +2697,6 @@ "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", @@ -8112,16 +8093,6 @@ "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 b4e99f5..149d76f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "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 705e40d..2b8c7fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,27 +1,25 @@ import * as React from 'react'; import './App.scss'; import { MainMenu } from './Components/MainMenu/MainMenu'; -import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; +import { ContainerModel, findContainerById, IContainerModel, MakeIterator } 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 } @@ -34,12 +32,7 @@ export class App extends React.Component { configuration: { AvailableContainers: [], AvailableSymbols: [], - MainContainer: { - Type: 'EmptyContainer', - Width: 3000, - Height: 200, - Style: {} - } + MainContainer: {} as AvailableContainer }, history: [], historyCurrentStep: 0, @@ -47,7 +40,7 @@ export class App extends React.Component { }; } - componentDidMount(): void { + componentDidMount() { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const state = urlParams.get('state'); @@ -57,39 +50,35 @@ export class App extends React.Component { } fetch(state) - .then( - async(response) => await response.json(), - (error) => { throw new Error(error); } - ) + .then((response) => response.json()) .then((data: IEditorState) => { this.LoadState(data); - }, (error) => { throw new Error(error); }); + }); } - public NewEditor(): void { + public NewEditor() { // 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, @@ -97,14 +86,13 @@ export class App extends React.Component { TypeCounters: {} } ], - historyCurrentStep: 0, - isLoaded: true - }); - }, (error) => { throw new Error(error); } - ); + historyCurrentStep: 0, + isLoaded: true + } as IAppState); + }); } - public LoadEditor(files: FileList | null): void { + public LoadEditor(files: FileList | null) { if (files === null) { return; } @@ -119,7 +107,7 @@ export class App extends React.Component { reader.readAsText(file); } - private LoadState(editorState: IEditorState): void { + private LoadState(editorState: IEditorState) { Revive(editorState); this.setState({ @@ -127,10 +115,10 @@ export class App extends React.Component { history: editorState.history, historyCurrentStep: editorState.historyCurrentStep, isLoaded: true - }); + } as IAppState); } - public render(): JSX.Element { + public render() { if (this.state.isLoaded) { return (
@@ -161,17 +149,16 @@ 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-expect-error - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + // @ts-ignore if (window.fetch) { return await fetch(url, { method: 'POST' }) - .then(async(response) => - await response.json() + .then((response) => + response.json() ) as Configuration; } - return await new Promise((resolve) => { + return new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.onreadystatechange = function() { // Call a function when the state changes. @@ -182,3 +169,38 @@ 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 caa6081..c4ea51a 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,32 +1,31 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; -import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { getDepth, MakeIterator } from '../../utils/itertools'; +import { IContainerModel, getDepth, MakeIterator } from '../../Interfaces/ContainerModel'; 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 == null) { + if (!this.props.MainContainer) { return null; } const it = MakeIterator(this.props.MainContainer); for (const container of it) { - handleContainer(container); + handleContainer(container as IContainerModel); } } - public render(): JSX.Element { + public render() { 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 3145f45..544c2bd 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 7487de1..e77a8c7 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(): JSX.Element { + public render() { const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; const states = this.props.history.map((step, move) => { - const desc = move > 0 + const desc = move ? `Go to modification n°${move}` : 'Go to the beginning'; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index a9b3da2..92b19d5 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 0d30d3d..65cfefd 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(): JSX.Element { + public render() { 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 e64739c..a352bcd 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -import { IContainerModel } from '../../../Interfaces/ContainerModel'; -import { getDepth } from '../../../utils/itertools'; +import { getDepth, IContainerModel } from '../../../Interfaces/ContainerModel'; import { Dimension } from './Dimension'; export interface IContainerProps { @@ -21,11 +20,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: React.CSSProperties = { + const defaultStyle = { 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 612b3f7..96ab2c9 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(): JSX.Element { - const style: React.CSSProperties = { + public render() { + const style = { stroke: 'black' - }; + } as React.CSSProperties; return ( = (props) => { props.selected.properties.width, props.selected.properties.height ]; - const style: React.CSSProperties = { + const style = { stroke: '#3B82F6', // tw blue-500 strokeWidth: 4, fillOpacity: 0, @@ -27,7 +26,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 static ID = 'svg'; public state: ISVGState; + public static ID = 'svg'; constructor(props: ISVGProps) { super(props); this.state = { - viewerWidth: window.innerWidth, - viewerHeight: window.innerHeight + value: { + viewerWidth: window.innerWidth, + viewerHeight: window.innerHeight + } as Value, + tool: TOOL_PAN }; } - resizeViewBox(): void { - this.setState({ - viewerWidth: window.innerWidth, - viewerHeight: window.innerHeight - }); - } - - componentDidMount(): void { - window.addEventListener('resize', () => this.resizeViewBox()); - } - - componentWillUnmount(): void { - window.removeEventListener('resize', () => this.resizeViewBox()); - } - - render(): JSX.Element { + render() { const xmlns = ''; const properties = { @@ -61,11 +49,13 @@ export class SVG extends React.PureComponent { return (
- this.setState({ value })} + tool={this.state.tool} onChangeTool={tool => this.setState({ tool })} miniatureProps={{ position: 'left', background: '#616264', @@ -77,8 +67,9 @@ export class SVG extends React.PureComponent { { children } - +
+ ); }; } diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index b474cc9..dd34812 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(): JSX.Element { + public render() { 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 5bdca49..89c38aa 100644 --- a/src/Editor.tsx +++ b/src/Editor.tsx @@ -1,22 +1,28 @@ 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 { ContainerModel, IContainerModel } from './Interfaces/ContainerModel'; -import { findContainerById, MakeIterator } from './utils/itertools'; +import { History } from './Components/History/History'; +import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; +import Properties from './Interfaces/Properties'; import { IHistoryState } from './App'; -import { getCircularReplacer } from './utils/saveload'; -import { UI } from './Components/UI/UI'; +import FloatingButton from './Components/FloatingButton/FloatingButton'; interface IEditorProps { - configuration: Configuration - history: IHistoryState[] + configuration: Configuration, + history: Array, historyCurrentStep: number } export interface IEditorState { - history: IHistoryState[] - historyCurrentStep: number + isSidebarOpen: boolean, + isElementsSidebarOpen: boolean, + isHistoryOpen: boolean, + history: Array, + historyCurrentStep: number, // do not use it, use props.configuration // only used for serialization purpose configuration: Configuration @@ -28,44 +34,50 @@ 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 - }; - } - - componentDidMount(): void { - window.addEventListener('keyup', (event) => this.onKeyDown(event)); - } - - componentWillUnmount(): void { - window.removeEventListener('keyup', (event) => this.onKeyDown(event)); - } - - public onKeyDown(event: KeyboardEvent): void { - event.preventDefault(); - if (event.isComposing || event.keyCode === 229) { - return; - } - if (event.key === 'z' && - event.ctrlKey && - this.state.historyCurrentStep > 0) { - this.LoadState(this.state.historyCurrentStep - 1); - } else if (event.key === 'y' && - event.ctrlKey && - this.state.historyCurrentStep < this.state.history.length - 1) { - this.LoadState(this.state.historyCurrentStep + 1); - } + } 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): void { + public SelectContainer(container: ContainerModel) { const history = this.getCurrentHistory(); const current = history[history.length - 1]; @@ -88,7 +100,7 @@ class Editor extends React.Component { SelectedContainerId: SelectedContainer.properties.id }]), historyCurrentStep: history.length - }); + } as IEditorState); } /** @@ -122,7 +134,7 @@ class Editor extends React.Component { TypeCounters: Object.assign({}, current.TypeCounters) }]), historyCurrentStep: history.length - }); + } as IEditorState); return; } @@ -144,7 +156,7 @@ class Editor extends React.Component { TypeCounters: Object.assign({}, current.TypeCounters) }]), historyCurrentStep: history.length - }); + } as IEditorState); } /** @@ -184,7 +196,8 @@ class Editor extends React.Component { const count = newCounters[type]; // Create maincontainer model - const clone: IContainerModel = structuredClone(current.MainContainer); + const structure: IContainerModel = structuredClone(current.MainContainer); + const clone = Object.assign(new ContainerModel(null, {} as Properties), structure); // Find the parent const it = MakeIterator(clone); @@ -217,7 +230,7 @@ class Editor extends React.Component { width: properties?.Width, height: parent.properties.height, ...properties.Style - }, + } as Properties, [], { type @@ -236,16 +249,16 @@ class Editor extends React.Component { SelectedContainerId: parent.properties.id }]), historyCurrentStep: history.length - }); + } as IEditorState); } - public LoadState(move: number): void { + public jumpTo(move: number): void { this.setState({ historyCurrentStep: move - }); + } as IEditorState); } - public SaveEditorAsJSON(): void { + public SaveEditorAsJSON() { const exportName = 'state'; const spaces = import.meta.env.DEV ? 4 : 0; const data = JSON.stringify(this.state, getCircularReplacer(), spaces); @@ -258,7 +271,7 @@ class Editor extends React.Component { downloadAnchorNode.remove(); } - public SaveEditorAsSVG(): void { + public SaveEditorAsSVG() { const svgWrapper = document.getElementById(SVG.ID) as HTMLElement; const svg = svgWrapper.querySelector('svg') as SVGSVGElement; const preface = '\r\n'; @@ -276,22 +289,59 @@ class Editor extends React.Component { * Render the application * @returns {JSX.Element} Rendered JSX element */ - render(): JSX.Element { + render() { 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.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)} + isOpen={this.state.isHistoryOpen} + onClick={() => this.ToggleHistory()} + jumpTo={(move) => { this.jumpTo(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 fb6ae67..ddf722e 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 8571167..ec8108a 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 f8d4854..2127299 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 1c70ae3..787cf91 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,3 +24,67 @@ 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 b839b09..723d04b 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 ed185f2..79f4f47 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 deleted file mode 100644 index 031b497..0000000 --- a/src/utils/itertools.ts +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 8993ad4..0000000 --- a/src/utils/saveload.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 139850e..bb5dc8b 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, RenderResult } from '@testing-library/react'; +import { cleanup, render } from '@testing-library/react'; import { afterEach } from 'vitest'; afterEach(() => { cleanup(); }); -const customRender = (ui: React.ReactElement, options = {}): RenderResult => +const customRender = (ui: React.ReactElement, options = {}) => render(ui, { // wrap provider(s) here if needed wrapper: ({ children }) => children,