Added Standard with Typescript + Moved module methods to utils/itertools
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Siklos 2022-08-05 19:52:09 +02:00
parent 8e34d6b72a
commit 361a200d07
24 changed files with 258 additions and 223 deletions

View file

@ -5,7 +5,7 @@ module.exports = {
}, },
extends: [ extends: [
'plugin:react/recommended', 'plugin:react/recommended',
'standard' 'standard-with-typescript'
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
@ -13,7 +13,8 @@ module.exports = {
jsx: true jsx: true
}, },
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module' sourceType: 'module',
project: './tsconfig.json'
}, },
plugins: [ plugins: [
'react', 'react',
@ -21,9 +22,11 @@ module.exports = {
], ],
rules: { rules: {
'space-before-function-paren': ['error', 'never'], 'space-before-function-paren': ['error', 'never'],
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
indent: ['warn', 2, { SwitchCase: 1 }], indent: ['warn', 2, { SwitchCase: 1 }],
semi: ['warn', 'always'], semi: 'off',
'@typescript-eslint/semi': ['warn', 'always'],
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error' '@typescript-eslint/no-unused-vars': 'error',
} }
}; };

29
package-lock.json generated
View file

@ -28,6 +28,7 @@
"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",
"eslint-config-standard-with-typescript": "^22.0.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.4", "eslint-plugin-n": "^15.2.4",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",
@ -2697,6 +2698,24 @@
"eslint-plugin-promise": "^6.0.0" "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": { "node_modules/eslint-import-resolver-node": {
"version": "0.3.6", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
@ -8093,6 +8112,16 @@
"dev": true, "dev": true,
"requires": {} "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": { "eslint-import-resolver-node": {
"version": "0.3.6", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",

View file

@ -32,6 +32,7 @@
"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",
"eslint-config-standard-with-typescript": "^22.0.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.4", "eslint-plugin-n": "^15.2.4",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",

View file

@ -1,25 +1,27 @@
import * as React from 'react'; import * as React from 'react';
import './App.scss'; import './App.scss';
import { MainMenu } from './Components/MainMenu/MainMenu'; import { MainMenu } from './Components/MainMenu/MainMenu';
import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel';
import { findContainerById, MakeIterator } from './utils/itertools';
import Editor, { IEditorState } from './Editor'; import Editor, { IEditorState } from './Editor';
import { AvailableContainer } from './Interfaces/AvailableContainer';
import { Configuration } from './Interfaces/Configuration'; import { Configuration } from './Interfaces/Configuration';
export interface IHistoryState { export interface IHistoryState {
MainContainer: IContainerModel | null, MainContainer: IContainerModel | null
SelectedContainer: IContainerModel | null, SelectedContainer: IContainerModel | null
SelectedContainerId: string, SelectedContainerId: string
TypeCounters: Record<string, number> TypeCounters: Record<string, number>
} }
// App will never have props
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IAppProps { interface IAppProps {
} }
interface IAppState { interface IAppState {
configuration: Configuration, configuration: Configuration
history: IHistoryState[], history: IHistoryState[]
historyCurrentStep: number, historyCurrentStep: number
isLoaded: boolean isLoaded: boolean
} }
@ -32,7 +34,12 @@ export class App extends React.Component<IAppProps> {
configuration: { configuration: {
AvailableContainers: [], AvailableContainers: [],
AvailableSymbols: [], AvailableSymbols: [],
MainContainer: {} as AvailableContainer MainContainer: {
Type: 'EmptyContainer',
Width: 3000,
Height: 200,
Style: {}
}
}, },
history: [], history: [],
historyCurrentStep: 0, historyCurrentStep: 0,
@ -40,7 +47,7 @@ export class App extends React.Component<IAppProps> {
}; };
} }
componentDidMount() { componentDidMount(): void {
const queryString = window.location.search; const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString); const urlParams = new URLSearchParams(queryString);
const state = urlParams.get('state'); const state = urlParams.get('state');
@ -50,15 +57,19 @@ export class App extends React.Component<IAppProps> {
} }
fetch(state) fetch(state)
.then((response) => response.json()) .then(
async(response) => await response.json(),
(error) => { throw new Error(error); }
)
.then((data: IEditorState) => { .then((data: IEditorState) => {
this.LoadState(data); this.LoadState(data);
}); }, (error) => { throw new Error(error); });
} }
public NewEditor() { public NewEditor(): void {
// 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 ContainerModel( const MainContainer = new ContainerModel(
null, null,
@ -88,11 +99,12 @@ export class App extends React.Component<IAppProps> {
], ],
historyCurrentStep: 0, historyCurrentStep: 0,
isLoaded: true isLoaded: true
} as IAppState);
}); });
}, (error) => { throw new Error(error); }
);
} }
public LoadEditor(files: FileList | null) { public LoadEditor(files: FileList | null): void {
if (files === null) { if (files === null) {
return; return;
} }
@ -107,7 +119,7 @@ export class App extends React.Component<IAppProps> {
reader.readAsText(file); reader.readAsText(file);
} }
private LoadState(editorState: IEditorState) { private LoadState(editorState: IEditorState): void {
Revive(editorState); Revive(editorState);
this.setState({ this.setState({
@ -115,10 +127,10 @@ export class App extends React.Component<IAppProps> {
history: editorState.history, history: editorState.history,
historyCurrentStep: editorState.historyCurrentStep, historyCurrentStep: editorState.historyCurrentStep,
isLoaded: true isLoaded: true
} as IAppState); });
} }
public render() { public render(): JSX.Element {
if (this.state.isLoaded) { if (this.state.isLoaded) {
return ( return (
<div> <div>
@ -149,16 +161,17 @@ export 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-expect-error
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (window.fetch) { if (window.fetch) {
return await fetch(url, { return await fetch(url, {
method: 'POST' method: 'POST'
}) })
.then((response) => .then(async(response) =>
response.json() await response.json()
) as Configuration; ) as Configuration;
} }
return new Promise((resolve) => { return await new Promise((resolve) => {
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.

View file

@ -1,31 +1,32 @@
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 { IContainerModel, getDepth, MakeIterator } from '../../Interfaces/ContainerModel'; import { IContainerModel } from '../../Interfaces/ContainerModel';
import { getDepth, MakeIterator } from '../../utils/itertools';
interface IElementsSidebarProps { interface IElementsSidebarProps {
MainContainer: IContainerModel | null, MainContainer: IContainerModel | null
isOpen: boolean, isOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
SelectedContainer: IContainerModel | null, SelectedContainer: IContainerModel | null
onClick: () => void, onClick: () => void
onPropertyChange: (key: string, value: string) => void, onPropertyChange: (key: string, value: string) => void
selectContainer: (container: IContainerModel) => void selectContainer: (container: IContainerModel) => void
} }
export class ElementsSidebar extends React.PureComponent<IElementsSidebarProps> { export class ElementsSidebar extends React.PureComponent<IElementsSidebarProps> {
public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode {
if (!this.props.MainContainer) { if (this.props.MainContainer == null) {
return null; return null;
} }
const it = MakeIterator(this.props.MainContainer); const it = MakeIterator(this.props.MainContainer);
for (const container of it) { for (const container of it) {
handleContainer(container as IContainerModel); handleContainer(container);
} }
} }
public render() { public render(): JSX.Element {
let isOpenClasses = '-right-64'; let isOpenClasses = '-right-64';
if (this.props.isOpen) { if (this.props.isOpen) {
isOpenClasses = this.props.isHistoryOpen isOpenClasses = this.props.isHistoryOpen

View file

@ -9,7 +9,7 @@ interface IFloatingButtonProps {
const toggleState = ( const toggleState = (
isHidden: boolean, isHidden: boolean,
setHidden: React.Dispatch<React.SetStateAction<boolean>> setHidden: React.Dispatch<React.SetStateAction<boolean>>
) => { ): void => {
setHidden(!isHidden); setHidden(!isHidden);
}; };

View file

@ -2,19 +2,19 @@ import * as React from 'react';
import { IHistoryState } from '../../App'; import { IHistoryState } from '../../App';
interface IHistoryProps { interface IHistoryProps {
history: IHistoryState[], history: IHistoryState[]
historyCurrentStep: number, historyCurrentStep: number
isOpen: boolean, isOpen: boolean
onClick: () => void, onClick: () => void
jumpTo: (move: number) => void jumpTo: (move: number) => void
} }
export class History extends React.PureComponent<IHistoryProps> { export class History extends React.PureComponent<IHistoryProps> {
public render() { public render(): JSX.Element {
const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64';
const states = this.props.history.map((step, move) => { const states = this.props.history.map((step, move) => {
const desc = move const desc = move > 0
? `Go to modification n°${move}` ? `Go to modification n°${move}`
: 'Go to the beginning'; : 'Go to the beginning';

View file

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
interface IMainMenuProps { interface IMainMenuProps {
newEditor: () => void; newEditor: () => void
loadEditor: (files: FileList | null) => void loadEditor: (files: FileList | null) => void
} }

View file

@ -2,7 +2,7 @@ import * as React from 'react';
import ContainerProperties from '../../Interfaces/Properties'; import ContainerProperties from '../../Interfaces/Properties';
interface IPropertiesProps { interface IPropertiesProps {
properties?: ContainerProperties, properties?: ContainerProperties
onChange: (key: string, value: string) => void onChange: (key: string, value: string) => void
} }
@ -27,7 +27,7 @@ export class Properties extends React.PureComponent<IPropertiesProps> {
public handleProperties = ( public handleProperties = (
[key, value]: [string, string | number], [key, value]: [string, string | number],
groupInput: React.ReactNode[] groupInput: React.ReactNode[]
) => { ): void => {
const id = `property-${key}`; const id = `property-${key}`;
const type = 'text'; const type = 'text';
const isDisabled = key === 'id' || key === 'parentId'; // hardcoded const isDisabled = key === 'id' || key === 'parentId'; // hardcoded

View file

@ -1,5 +1,6 @@
import * as React from 'react'; 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'; import { Dimension } from './Dimension';
export interface IContainerProps { export interface IContainerProps {
@ -20,11 +21,11 @@ export class Container extends React.PureComponent<IContainerProps> {
const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`; const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`;
// g style // g style
const defaultStyle = { const defaultStyle: React.CSSProperties = {
transitionProperty: 'all', transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms' transitionDuration: '150ms'
} as React.CSSProperties; };
// Rect style // Rect style
const style = Object.assign( const style = Object.assign(

View file

@ -1,19 +1,19 @@
import * as React from 'react'; import * as React from 'react';
interface IDimensionProps { interface IDimensionProps {
id: string; id: string
xStart: number; xStart: number
xEnd: number; xEnd: number
y: number; y: number
text: string; text: string
strokeWidth: number; strokeWidth: number
} }
export class Dimension extends React.PureComponent<IDimensionProps> { export class Dimension extends React.PureComponent<IDimensionProps> {
public render() { public render(): JSX.Element {
const style = { const style: React.CSSProperties = {
stroke: 'black' stroke: 'black'
} as React.CSSProperties; };
return ( return (
<g key={this.props.id}> <g key={this.props.id}>
<line <line

View file

@ -1,10 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { ContainerModel, getDepth, MakeIterator } from '../../../Interfaces/ContainerModel'; import { ContainerModel } from '../../../Interfaces/ContainerModel';
import { getDepth, MakeIterator } from '../../../utils/itertools';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
interface IDimensionLayerProps { interface IDimensionLayerProps {
isHidden: boolean, isHidden: boolean
roots: ContainerModel | ContainerModel[] | null, roots: ContainerModel | ContainerModel[] | null
} }
const GAP: number = 50; const GAP: number = 50;

View file

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { IContainerModel, getAbsolutePosition } from '../../../Interfaces/ContainerModel'; import { IContainerModel } from '../../../Interfaces/ContainerModel';
import { getAbsolutePosition } from '../../../utils/itertools';
interface ISelectorProps { interface ISelectorProps {
selected: IContainerModel | null selected: IContainerModel | null
@ -18,7 +19,7 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
props.selected.properties.width, props.selected.properties.width,
props.selected.properties.height props.selected.properties.height
]; ];
const style = { const style: React.CSSProperties = {
stroke: '#3B82F6', // tw blue-500 stroke: '#3B82F6', // tw blue-500
strokeWidth: 4, strokeWidth: 4,
fillOpacity: 0, fillOpacity: 0,
@ -26,7 +27,7 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms', transitionDuration: '150ms',
animation: 'fadein 750ms ease-in alternate infinite' animation: 'fadein 750ms ease-in alternate infinite'
} as React.CSSProperties; };
return ( return (
<rect <rect

View file

@ -1,34 +1,21 @@
import * as React from 'react'; import * as React from 'react';
import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom'; import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
import { Container } from './Elements/Container'; import { Container } from './Elements/Container';
import { ContainerModel } from '../../Interfaces/ContainerModel'; import { ContainerModel } from '../../Interfaces/ContainerModel';
import { Selector } from './Elements/Selector'; import { Selector } from './Elements/Selector';
interface ISVGProps { interface ISVGProps {
width: number, width: number
height: number, height: number
children: ContainerModel | ContainerModel[] | null, children: ContainerModel | ContainerModel[] | null
selected: ContainerModel | null selected: ContainerModel | null
} }
interface ISVGState {
value: Value,
tool: Tool
}
export class SVG extends React.PureComponent<ISVGProps> { export class SVG extends React.PureComponent<ISVGProps> {
public state: ISVGState;
public static ID = 'svg'; public static ID = 'svg';
constructor(props: ISVGProps) { constructor(props: ISVGProps) {
super(props); super(props);
this.state = {
value: {
viewerWidth: window.innerWidth,
viewerHeight: window.innerHeight
} as Value,
tool: TOOL_PAN
};
} }
render() { render() {
@ -49,13 +36,11 @@ export class SVG extends React.PureComponent<ISVGProps> {
return ( return (
<div id={SVG.ID}> <div id={SVG.ID}>
<ReactSVGPanZoom <UncontrolledReactSVGPanZoom
width={window.innerWidth} width={window.innerWidth}
height={window.innerHeight} height={window.innerHeight}
background={'#ffffff'} background={'#ffffff'}
defaultTool='pan' defaultTool='pan'
value={this.state.value} onChangeValue={value => this.setState({ value })}
tool={this.state.tool} onChangeTool={tool => this.setState({ tool })}
miniatureProps={{ miniatureProps={{
position: 'left', position: 'left',
background: '#616264', background: '#616264',
@ -67,9 +52,8 @@ export class SVG extends React.PureComponent<ISVGProps> {
{ children } { children }
<Selector selected={this.props.selected} /> <Selector selected={this.props.selected} />
</svg> </svg>
</ReactSVGPanZoom> </UncontrolledReactSVGPanZoom>
</div> </div>
); );
}; };
} }

View file

@ -3,13 +3,13 @@ import { AvailableContainer } from '../../Interfaces/AvailableContainer';
interface ISidebarProps { interface ISidebarProps {
componentOptions: AvailableContainer[] componentOptions: AvailableContainer[]
isOpen: boolean; isOpen: boolean
onClick: () => void; onClick: () => void
buttonOnClick: (type: string) => void; buttonOnClick: (type: string) => void
} }
export default class Sidebar extends React.PureComponent<ISidebarProps> { export default class Sidebar extends React.PureComponent<ISidebarProps> {
public render() { public render(): JSX.Element {
const listElements = this.props.componentOptions.map(componentOption => const listElements = this.props.componentOptions.map(componentOption =>
<button className='hover:bg-blue-600 transition-all sidebar-row' key={componentOption.Type} onClick={() => this.props.buttonOnClick(componentOption.Type)}> <button className='hover:bg-blue-600 transition-all sidebar-row' key={componentOption.Type} onClick={() => this.props.buttonOnClick(componentOption.Type)}>
{componentOption.Type} {componentOption.Type}

View file

@ -6,23 +6,24 @@ import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
import { Configuration } from './Interfaces/Configuration'; import { Configuration } from './Interfaces/Configuration';
import { SVG } from './Components/SVG/SVG'; import { SVG } from './Components/SVG/SVG';
import { History } from './Components/History/History'; import { History } from './Components/History/History';
import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel'; import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel';
import { findContainerById, MakeIterator } from './utils/itertools';
import Properties from './Interfaces/Properties'; import Properties from './Interfaces/Properties';
import { IHistoryState } from './App'; import { IHistoryState } from './App';
import FloatingButton from './Components/FloatingButton/FloatingButton'; import FloatingButton from './Components/FloatingButton/FloatingButton';
interface IEditorProps { interface IEditorProps {
configuration: Configuration, configuration: Configuration
history: Array<IHistoryState>, history: IHistoryState[]
historyCurrentStep: number historyCurrentStep: number
} }
export interface IEditorState { export interface IEditorState {
isSidebarOpen: boolean, isSidebarOpen: boolean
isElementsSidebarOpen: boolean, isElementsSidebarOpen: boolean
isHistoryOpen: boolean, isHistoryOpen: boolean
history: Array<IHistoryState>, history: IHistoryState[]
historyCurrentStep: number, historyCurrentStep: number
// do not use it, use props.configuration // do not use it, use props.configuration
// only used for serialization purpose // only used for serialization purpose
configuration: Configuration configuration: Configuration
@ -40,7 +41,7 @@ class Editor extends React.Component<IEditorProps> {
configuration: Object.assign({}, props.configuration), configuration: Object.assign({}, props.configuration),
history: [...props.history], history: [...props.history],
historyCurrentStep: props.historyCurrentStep historyCurrentStep: props.historyCurrentStep
} as IEditorState; };
} }
public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1); public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1);
@ -49,35 +50,35 @@ class Editor extends React.Component<IEditorProps> {
/** /**
* Toggle the components sidebar * Toggle the components sidebar
*/ */
public ToggleSidebar() { public ToggleSidebar(): void {
this.setState({ this.setState({
isSidebarOpen: !this.state.isSidebarOpen isSidebarOpen: !this.state.isSidebarOpen
} as IEditorState); });
} }
/** /**
* Toggle the elements * Toggle the elements
*/ */
public ToggleElementsSidebar() { public ToggleElementsSidebar(): void {
this.setState({ this.setState({
isElementsSidebarOpen: !this.state.isElementsSidebarOpen isElementsSidebarOpen: !this.state.isElementsSidebarOpen
} as IEditorState); });
} }
/** /**
* Toggle the elements * Toggle the elements
*/ */
public ToggleHistory() { public ToggleHistory(): void {
this.setState({ this.setState({
isHistoryOpen: !this.state.isHistoryOpen isHistoryOpen: !this.state.isHistoryOpen
} as IEditorState); });
} }
/** /**
* Select a container * Select a container
* @param container Selected container * @param container Selected container
*/ */
public SelectContainer(container: ContainerModel) { public SelectContainer(container: ContainerModel): void {
const history = this.getCurrentHistory(); const history = this.getCurrentHistory();
const current = history[history.length - 1]; const current = history[history.length - 1];
@ -100,7 +101,7 @@ class Editor extends React.Component<IEditorProps> {
SelectedContainerId: SelectedContainer.properties.id SelectedContainerId: SelectedContainer.properties.id
}]), }]),
historyCurrentStep: history.length historyCurrentStep: history.length
} as IEditorState); });
} }
/** /**
@ -134,7 +135,7 @@ class Editor extends React.Component<IEditorProps> {
TypeCounters: Object.assign({}, current.TypeCounters) TypeCounters: Object.assign({}, current.TypeCounters)
}]), }]),
historyCurrentStep: history.length historyCurrentStep: history.length
} as IEditorState); });
return; return;
} }
@ -156,7 +157,7 @@ class Editor extends React.Component<IEditorProps> {
TypeCounters: Object.assign({}, current.TypeCounters) TypeCounters: Object.assign({}, current.TypeCounters)
}]), }]),
historyCurrentStep: history.length historyCurrentStep: history.length
} as IEditorState); });
} }
/** /**
@ -196,8 +197,7 @@ class Editor extends React.Component<IEditorProps> {
const count = newCounters[type]; const count = newCounters[type];
// Create maincontainer model // Create maincontainer model
const structure: IContainerModel = structuredClone(current.MainContainer); const clone: IContainerModel = structuredClone(current.MainContainer);
const clone = Object.assign(new ContainerModel(null, {} as Properties), structure);
// Find the parent // Find the parent
const it = MakeIterator(clone); const it = MakeIterator(clone);
@ -230,7 +230,7 @@ class Editor extends React.Component<IEditorProps> {
width: properties?.Width, width: properties?.Width,
height: parent.properties.height, height: parent.properties.height,
...properties.Style ...properties.Style
} as Properties, },
[], [],
{ {
type type
@ -249,16 +249,16 @@ class Editor extends React.Component<IEditorProps> {
SelectedContainerId: parent.properties.id SelectedContainerId: parent.properties.id
}]), }]),
historyCurrentStep: history.length historyCurrentStep: history.length
} as IEditorState); });
} }
public jumpTo(move: number): void { public jumpTo(move: number): void {
this.setState({ this.setState({
historyCurrentStep: move historyCurrentStep: move
} as IEditorState); });
} }
public SaveEditorAsJSON() { public SaveEditorAsJSON(): void {
const exportName = 'state'; const exportName = 'state';
const spaces = import.meta.env.DEV ? 4 : 0; const spaces = import.meta.env.DEV ? 4 : 0;
const data = JSON.stringify(this.state, getCircularReplacer(), spaces); const data = JSON.stringify(this.state, getCircularReplacer(), spaces);
@ -271,7 +271,7 @@ class Editor extends React.Component<IEditorProps> {
downloadAnchorNode.remove(); downloadAnchorNode.remove();
} }
public SaveEditorAsSVG() { public SaveEditorAsSVG(): void {
const svgWrapper = document.getElementById(SVG.ID) as HTMLElement; const svgWrapper = document.getElementById(SVG.ID) as HTMLElement;
const svg = svgWrapper.querySelector('svg') as SVGSVGElement; const svg = svgWrapper.querySelector('svg') as SVGSVGElement;
const preface = '<?xml version="1.0" standalone="no"?>\r\n'; const preface = '<?xml version="1.0" standalone="no"?>\r\n';
@ -289,7 +289,7 @@ class Editor extends React.Component<IEditorProps> {
* Render the application * Render the application
* @returns {JSX.Element} Rendered JSX element * @returns {JSX.Element} Rendered JSX element
*/ */
render() { render(): JSX.Element {
const current = this.getCurrentHistoryState(); const current = this.getCurrentHistoryState();
let buttonRightOffsetClasses = 'right-12'; let buttonRightOffsetClasses = 'right-12';
if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) { if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) {
@ -371,7 +371,7 @@ class Editor extends React.Component<IEditorProps> {
} }
} }
const getCircularReplacer = () => { const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => {
const seen = new WeakSet(); const seen = new WeakSet();
return (key: any, value: object | null) => { return (key: any, value: object | null) => {
if (key === 'parent') { if (key === 'parent') {

View file

@ -3,7 +3,7 @@ import { AvailableSymbolModel } from './AvailableSymbol';
/** Model of configuration for the application to configure it */ /** Model of configuration for the application to configure it */
export interface Configuration { export interface Configuration {
AvailableContainers: AvailableContainer[]; AvailableContainers: AvailableContainer[]
AvailableSymbols: AvailableSymbolModel[]; AvailableSymbols: AvailableSymbolModel[]
MainContainer: AvailableContainer; MainContainer: AvailableContainer
} }

View file

@ -1,9 +1,9 @@
import Properties from './Properties'; import Properties from './Properties';
export interface IContainerModel { export interface IContainerModel {
children: IContainerModel[], children: IContainerModel[]
parent: IContainerModel | null, parent: IContainerModel | null
properties: Properties, properties: Properties
userData: Record<string, string | number> userData: Record<string, string | number>
} }
@ -24,67 +24,3 @@ export class ContainerModel implements IContainerModel {
this.userData = userData; 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;
}

View file

@ -1,7 +1,7 @@
/** Model of an image with multiple source */ /** Model of an image with multiple source */
export interface Image { export interface Image {
Name: string; Name: string
Url: string; Url: string
Base64Image: string; Base64Image: string
Svg: string; Svg: string
} }

View file

@ -1,8 +1,8 @@
import * as React from 'react'; 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, parentId: string | null
x: number, x: number
y: number y: number
} }

65
src/utils/itertools.ts Normal file
View file

@ -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<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): 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;
}

View file

@ -1,13 +1,13 @@
/* eslint-disable import/export */ /* eslint-disable import/export */
import * as React from 'react'; import * as React from 'react';
import { cleanup, render } from '@testing-library/react'; import { cleanup, render, RenderResult } from '@testing-library/react';
import { afterEach } from 'vitest'; import { afterEach } from 'vitest';
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
}); });
const customRender = (ui: React.ReactElement, options = {}) => const customRender = (ui: React.ReactElement, options = {}): RenderResult =>
render(ui, { render(ui, {
// wrap provider(s) here if needed // wrap provider(s) here if needed
wrapper: ({ children }) => children, wrapper: ({ children }) => children,