Merged PR 170: Add new eslint rules

- naming-convention
- prefer-arrow-callback
- func-style
- import/no-default-export
This commit is contained in:
Eric Nguyen 2022-08-26 16:13:21 +00:00
parent 3f58c5ba5e
commit ad126c6c28
65 changed files with 781 additions and 784 deletions

View file

@ -1,12 +1,10 @@
// TODO: https://eslint.org/docs/latest/rules/func-names
// TODO: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/naming-convention.md
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
es2021: true es2021: true
}, },
extends: [ extends: [
'only-warn',
'plugin:react/recommended', 'plugin:react/recommended',
'standard-with-typescript' 'standard-with-typescript'
], ],
@ -20,22 +18,67 @@ module.exports = {
project: './tsconfig.json' project: './tsconfig.json'
}, },
plugins: [ plugins: [
'only-warn',
'react', 'react',
'react-hooks', 'react-hooks',
'@typescript-eslint' '@typescript-eslint'
], ],
rules: { rules: {
'prefer-arrow-callback': 'error',
'func-style': ['error', 'declaration'],
'space-before-function-paren': ['error', 'never'], 'space-before-function-paren': ['error', 'never'],
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
// Import/export
'import/no-default-export': 'error',
// Typescript overload
indent: 'off', indent: 'off',
'@typescript-eslint/indent': ['warn', 2, {SwitchCase: 1}],
semi: 'off', semi: 'off',
'@typescript-eslint/semi': ['warn', 'always'], "camelcase": "off",
'no-unused-vars': 'off', 'no-unused-vars': 'off',
// Typescript
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
'@typescript-eslint/indent': ['warn', 2, {SwitchCase: 1}],
'@typescript-eslint/semi': ['warn', 'always'],
'@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/ban-types': ['error'], '@typescript-eslint/ban-types': ['error'],
'@typescript-eslint/no-floating-promises': 'off', // disabled cuz troublesome for SweetAlert since they never reject '@typescript-eslint/no-floating-promises': 'off', // disabled cuz troublesome for SweetAlert since they never reject
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "default",
"format": ["camelCase"]
},
{
'selector': 'function',
'format': ['PascalCase']
},
{
"selector": "variable",
"format": ["camelCase", "UPPER_CASE"]
},
{
"selector": "parameter",
"format": ["camelCase"],
"leadingUnderscore": "allow"
},
{
'selector': ['enumMember', 'enum'],
'format': ['PascalCase']
},
{
"selector": "memberLike",
"modifiers": ["private"],
"format": ["camelCase"],
"leadingUnderscore": "require"
},
{
"selector": ['typeLike'],
"format": ["PascalCase"],
}
],
// React
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies 'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies
} }

View file

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { fetchConfiguration } from './api'; import { FetchConfiguration } from './api';
describe.concurrent('API test', () => { describe.concurrent('API test', () => {
it('Load environment', () => { it('Load environment', () => {
@ -8,7 +8,7 @@ describe.concurrent('API test', () => {
}); });
it('Fetch configuration', async() => { it('Fetch configuration', async() => {
const configuration = await fetchConfiguration(); const configuration = await FetchConfiguration();
expect(configuration.MainContainer).toBeDefined(); expect(configuration.MainContainer).toBeDefined();
expect(configuration.MainContainer.Height).toBeGreaterThan(0); expect(configuration.MainContainer.Height).toBeGreaterThan(0);
expect(configuration.MainContainer.Width).toBeGreaterThan(0); expect(configuration.MainContainer.Width).toBeGreaterThan(0);

View file

@ -4,7 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
* Fetch the configuration from the API * Fetch the configuration from the API
* @returns {Configation} The model of the configuration for the application * @returns {Configation} The model of the configuration for the application
*/ */
export async function fetchConfiguration(): Promise<IConfiguration> { export async function FetchConfiguration(): Promise<IConfiguration> {
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-expect-error // @ts-expect-error

View file

@ -1,6 +1,6 @@
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { IConfiguration } from '../../../Interfaces/IConfiguration';
import { fetchConfiguration } from '../../API/api'; import { FetchConfiguration } from '../../API/api';
import { IEditorState } from '../../../Interfaces/IEditorState'; import { IEditorState } from '../../../Interfaces/IEditorState';
import { LoadState } from './Load'; import { LoadState } from './Load';
import { GetDefaultEditorState } from '../../../utils/default'; import { GetDefaultEditorState } from '../../../utils/default';
@ -10,7 +10,7 @@ export function NewEditor(
setLoaded: Dispatch<SetStateAction<boolean>> setLoaded: Dispatch<SetStateAction<boolean>>
): void { ): void {
// Fetch the configuration from the API // Fetch the configuration from the API
fetchConfiguration() FetchConfiguration()
.then((configuration: IConfiguration) => { .then((configuration: IConfiguration) => {
// Set the editor from the given properties of the API // Set the editor from the given properties of the API
const editorState: IEditorState = GetDefaultEditorState(configuration); const editorState: IEditorState = GetDefaultEditorState(configuration);

View file

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import './App.scss'; import './App.scss';
import { MainMenu } from '../MainMenu/MainMenu'; import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel } from '../../Interfaces/IContainerModel';
import Editor from '../Editor/Editor'; import { Editor } from '../Editor/Editor';
import { IEditorState } from '../../Interfaces/IEditorState'; import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Actions/Load'; import { LoadState } from './Actions/Load';
import { LoadEditor, NewEditor } from './Actions/MenuActions'; import { LoadEditor, NewEditor } from './Actions/MenuActions';
@ -14,28 +14,11 @@ interface IAppProps {
root: Element | Document root: Element | Document
} }
export const App: React.FunctionComponent<IAppProps> = (props) => { function UseHTTPGETStatePreloading(
const [isLoaded, setLoaded] = useState<boolean>(false); isLoaded: boolean,
setEditorState: Dispatch<SetStateAction<IEditorState>>,
const defaultMainContainer = new ContainerModel( setLoaded: Dispatch<SetStateAction<boolean>>
null, ): void {
DEFAULT_MAINCONTAINER_PROPS
);
const [editorState, setEditorState] = useState<IEditorState>({
configuration: DEFAULT_CONFIG,
history: [{
LastAction: '',
MainContainer: defaultMainContainer,
SelectedContainerId: defaultMainContainer.properties.id,
TypeCounters: {},
Symbols: new Map(),
SelectedSymbolId: ''
}],
historyCurrentStep: 0
});
// TODO: move this into a variable
useEffect(() => { useEffect(() => {
const queryString = window.location.search; const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString); const urlParams = new URLSearchParams(queryString);
@ -56,6 +39,30 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
}, (error) => { throw new Error(error); }); }, (error) => { throw new Error(error); });
} }
}); });
};
export function App(props: IAppProps): JSX.Element {
const [isLoaded, setLoaded] = useState<boolean>(false);
const defaultMainContainer = new ContainerModel(
null,
DEFAULT_MAINCONTAINER_PROPS
);
const [editorState, setEditorState] = useState<IEditorState>({
configuration: DEFAULT_CONFIG,
history: [{
lastAction: '',
mainContainer: defaultMainContainer,
selectedContainerId: defaultMainContainer.properties.id,
typeCounters: {},
symbols: new Map(),
selectedSymbolId: ''
}],
historyCurrentStep: 0
});
UseHTTPGETStatePreloading(isLoaded, setEditorState, setLoaded);
if (isLoaded) { if (isLoaded) {
return ( return (

View file

@ -1,4 +1,4 @@
import { ClockIcon, CubeIcon, LinkIcon, MapIcon } from '@heroicons/react/outline'; import { ClockIcon, CubeIcon, LinkIcon } from '@heroicons/react/outline';
import * as React from 'react'; import * as React from 'react';
import { BarIcon } from './BarIcon'; import { BarIcon } from './BarIcon';
@ -7,34 +7,34 @@ interface IBarProps {
isSymbolsOpen: boolean isSymbolsOpen: boolean
isElementsSidebarOpen: boolean isElementsSidebarOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
ToggleSidebar: () => void toggleSidebar: () => void
ToggleSymbols: () => void toggleSymbols: () => void
ToggleTimeline: () => void toggleTimeline: () => void
} }
export const BAR_WIDTH = 64; // 4rem export const BAR_WIDTH = 64; // 4rem
export const Bar: React.FC<IBarProps> = (props) => { export function Bar(props: IBarProps): JSX.Element {
return ( return (
<div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'> <div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'>
<BarIcon <BarIcon
isActive={props.isSidebarOpen} isActive={props.isSidebarOpen}
title='Components' title='Components'
onClick={() => props.ToggleSidebar()}> onClick={() => props.toggleSidebar()}>
<CubeIcon className='heroicon'/> <CubeIcon className='heroicon' />
</BarIcon> </BarIcon>
<BarIcon <BarIcon
isActive={props.isSymbolsOpen} isActive={props.isSymbolsOpen}
title='Symbols' title='Symbols'
onClick={() => props.ToggleSymbols()}> onClick={() => props.toggleSymbols()}>
<LinkIcon className='heroicon'/> <LinkIcon className='heroicon' />
</BarIcon> </BarIcon>
<BarIcon <BarIcon
isActive={props.isHistoryOpen} isActive={props.isHistoryOpen}
title='Timeline' title='Timeline'
onClick={() => props.ToggleTimeline()}> onClick={() => props.toggleTimeline()}>
<ClockIcon className='heroicon'/> <ClockIcon className='heroicon' />
</BarIcon> </BarIcon>
</div> </div>
); );
}; }

View file

@ -7,7 +7,7 @@ interface IBarIconProps {
onClick: () => void onClick: () => void
} }
export const BarIcon: React.FC<IBarIconProps> = (props) => { export function BarIcon(props: IBarIconProps): JSX.Element {
const isActiveClasses = props.isActive ? 'border-l-4 border-blue-500 bg-slate-200' : ''; const isActiveClasses = props.isActive ? 'border-l-4 border-blue-500 bg-slate-200' : '';
return ( return (
<button type="button" <button type="button"
@ -16,7 +16,7 @@ export const BarIcon: React.FC<IBarIconProps> = (props) => {
onClick={() => props.onClick()} onClick={() => props.onClick()}
> >
<span className='sidebar-tooltip group-hover:scale-100'>{props.title}</span> <span className='sidebar-tooltip group-hover:scale-100'>{props.title}</span>
{ props.children } {props.children}
</button> </button>
); );
}; }

View file

@ -2,9 +2,9 @@ import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
import * as React from 'react'; import * as React from 'react';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
import { XPositionReference } from '../../Enums/XPositionReference'; import { XPositionReference } from '../../Enums/XPositionReference';
import IContainerProperties from '../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, restoreX, transformX } from '../../utils/svg'; import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, TransformX } from '../../utils/svg';
import { InputGroup } from '../InputGroup/InputGroup'; import { InputGroup } from '../InputGroup/InputGroup';
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons'; import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
import { Select } from '../Select/Select'; import { Select } from '../Select/Select';
@ -15,10 +15,8 @@ interface IContainerFormProps {
onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
} }
const getCSSInputs = ( function GetCSSInputs(properties: IContainerProperties,
properties: IContainerProperties, onChange: (key: string, value: string | number | boolean, type: PropertyType) => void): JSX.Element[] {
onChange: (key: string, value: string | number | boolean, type: PropertyType) => void
): JSX.Element[] => {
const groupInput: JSX.Element[] = []; const groupInput: JSX.Element[] = [];
for (const key in properties.style) { for (const key in properties.style) {
groupInput.push(<InputGroup groupInput.push(<InputGroup
@ -29,13 +27,12 @@ const getCSSInputs = (
inputClassName='' inputClassName=''
type='string' type='string'
value={(properties.style as any)[key]} value={(properties.style as any)[key]}
onChange={(event) => onChange(key, event.target.value, PropertyType.STYLE)} onChange={(event) => onChange(key, event.target.value, PropertyType.Style)} />);
/>);
} }
return groupInput; return groupInput;
}; }
const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => { export function ContainerForm(props: IContainerFormProps): JSX.Element {
return ( return (
<div className='grid grid-cols-2 gap-y-4'> <div className='grid grid-cols-2 gap-y-4'>
<InputGroup <InputGroup
@ -45,8 +42,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='string' type='string'
value={props.properties.id.toString()} value={props.properties.id.toString()}
isDisabled={true} isDisabled={true} />
/>
<InputGroup <InputGroup
labelText='Parent name' labelText='Parent name'
inputKey='parentId' inputKey='parentId'
@ -54,8 +50,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='string' type='string'
value={props.properties.parentId} value={props.properties.parentId}
isDisabled={true} isDisabled={true} />
/>
<InputGroup <InputGroup
labelText='Type' labelText='Type'
inputKey='type' inputKey='type'
@ -63,8 +58,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='string' type='string'
value={props.properties.type} value={props.properties.type}
isDisabled={true} isDisabled={true} />
/>
<InputGroup <InputGroup
labelText='Displayed text' labelText='Displayed text'
inputKey='displayedText' inputKey='displayedText'
@ -72,8 +66,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='string' type='string'
value={props.properties.displayedText?.toString()} value={props.properties.displayedText?.toString()}
onChange={(event) => props.onChange('displayedText', event.target.value)} onChange={(event) => props.onChange('displayedText', event.target.value)} />
/>
<InputGroup <InputGroup
labelText='x' labelText='x'
inputKey='x' inputKey='x'
@ -81,19 +74,18 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='number' type='number'
isDisabled={props.properties.linkedSymbolId !== ''} isDisabled={props.properties.linkedSymbolId !== ''}
value={transformX(RemoveXMargin(props.properties.x, props.properties.margin.left), props.properties.width, props.properties.XPositionReference).toString()} value={TransformX(RemoveXMargin(props.properties.x, props.properties.margin.left), props.properties.width, props.properties.xPositionReference).toString()}
onChange={(event) => props.onChange( onChange={(event) => props.onChange(
'x', 'x',
ApplyXMargin( ApplyXMargin(
restoreX( RestoreX(
Number(event.target.value), Number(event.target.value),
props.properties.width, props.properties.width,
props.properties.XPositionReference props.properties.xPositionReference
), ),
props.properties.margin.left props.properties.margin.left
) )
)} )} />
/>
<InputGroup <InputGroup
labelText='y' labelText='y'
inputKey='y' inputKey='y'
@ -101,8 +93,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='number' type='number'
value={(props.properties.y - (props.properties.margin?.top ?? 0)).toString()} value={(props.properties.y - (props.properties.margin?.top ?? 0)).toString()}
onChange={(event) => props.onChange('y', Number(event.target.value) + (props.properties.margin?.top ?? 0))} onChange={(event) => props.onChange('y', Number(event.target.value) + (props.properties.margin?.top ?? 0))} />
/>
<InputGroup <InputGroup
labelText='Minimum width' labelText='Minimum width'
inputKey='minWidth' inputKey='minWidth'
@ -111,8 +102,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={1} min={1}
value={props.properties.minWidth.toString()} value={props.properties.minWidth.toString()}
onChange={(event) => props.onChange('minWidth', Number(event.target.value))} onChange={(event) => props.onChange('minWidth', Number(event.target.value))} />
/>
<InputGroup <InputGroup
labelText='Maximum width' labelText='Maximum width'
inputKey='maxWidth' inputKey='maxWidth'
@ -121,8 +111,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={1} min={1}
value={props.properties.maxWidth.toString()} value={props.properties.maxWidth.toString()}
onChange={(event) => props.onChange('maxWidth', Number(event.target.value))} onChange={(event) => props.onChange('maxWidth', Number(event.target.value))} />
/>
<InputGroup <InputGroup
labelText='Width' labelText='Width'
inputKey='width' inputKey='width'
@ -132,8 +121,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
min={props.properties.minWidth} min={props.properties.minWidth}
value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()} value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()}
onChange={(event) => props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))} onChange={(event) => props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))}
isDisabled={props.properties.isFlex} isDisabled={props.properties.isFlex} />
/>
<InputGroup <InputGroup
labelText='Height' labelText='Height'
inputKey='height' inputKey='height'
@ -142,8 +130,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={(props.properties.height + (props.properties.margin?.top ?? 0) + (props.properties.margin?.bottom ?? 0)).toString()} value={(props.properties.height + (props.properties.margin?.top ?? 0) + (props.properties.margin?.bottom ?? 0)).toString()}
onChange={(event) => props.onChange('height', Number(event.target.value) - (props.properties.margin?.top ?? 0) - (props.properties.margin?.bottom ?? 0))} onChange={(event) => props.onChange('height', Number(event.target.value) - (props.properties.margin?.top ?? 0) - (props.properties.margin?.bottom ?? 0))} />
/>
<InputGroup <InputGroup
labelText='Margin left' labelText='Margin left'
inputKey='left' inputKey='left'
@ -152,8 +139,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.left ?? 0).toString()} value={(props.properties.margin.left ?? 0).toString()}
onChange={(event) => props.onChange('left', Number(event.target.value), PropertyType.MARGIN)} onChange={(event) => props.onChange('left', Number(event.target.value), PropertyType.Margin)} />
/>
<InputGroup <InputGroup
labelText='Margin bottom' labelText='Margin bottom'
inputKey='bottom' inputKey='bottom'
@ -162,8 +148,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.bottom ?? 0).toString()} value={(props.properties.margin.bottom ?? 0).toString()}
onChange={(event) => props.onChange('bottom', Number(event.target.value), PropertyType.MARGIN)} onChange={(event) => props.onChange('bottom', Number(event.target.value), PropertyType.Margin)} />
/>
<InputGroup <InputGroup
labelText='Margin top' labelText='Margin top'
inputKey='top' inputKey='top'
@ -172,8 +157,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.top ?? 0).toString()} value={(props.properties.margin.top ?? 0).toString()}
onChange={(event) => props.onChange('top', Number(event.target.value), PropertyType.MARGIN)} onChange={(event) => props.onChange('top', Number(event.target.value), PropertyType.Margin)} />
/>
<InputGroup <InputGroup
labelText='Margin right' labelText='Margin right'
inputKey='right' inputKey='right'
@ -182,8 +166,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.right ?? 0).toString()} value={(props.properties.margin.right ?? 0).toString()}
onChange={(event) => props.onChange('right', Number(event.target.value), PropertyType.MARGIN)} onChange={(event) => props.onChange('right', Number(event.target.value), PropertyType.Margin)} />
/>
<InputGroup <InputGroup
labelText='Flex' labelText='Flex'
inputKey='isFlex' inputKey='isFlex'
@ -191,8 +174,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='checkbox' type='checkbox'
checked={props.properties.isFlex} checked={props.properties.isFlex}
onChange={(event) => props.onChange('isFlex', event.target.checked)} onChange={(event) => props.onChange('isFlex', event.target.checked)} />
/>
<InputGroup <InputGroup
labelText='Anchor' labelText='Anchor'
inputKey='isAnchor' inputKey='isAnchor'
@ -200,11 +182,10 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName='' inputClassName=''
type='checkbox' type='checkbox'
checked={props.properties.isAnchor} checked={props.properties.isAnchor}
onChange={(event) => props.onChange('isAnchor', event.target.checked)} onChange={(event) => props.onChange('isAnchor', event.target.checked)} />
/>
<RadioGroupButtons <RadioGroupButtons
name='XPositionReference' name='XPositionReference'
value={props.properties.XPositionReference.toString()} value={props.properties.xPositionReference.toString()}
inputClassName='hidden' inputClassName='hidden'
labelText='Horizontal alignment' labelText='Horizontal alignment'
inputGroups={[ inputGroups={[
@ -233,8 +214,7 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
value: XPositionReference.Right.toString() value: XPositionReference.Right.toString()
} }
]} ]}
onChange={(event) => props.onChange('XPositionReference', Number(event.target.value))} onChange={(event) => props.onChange('xPositionReference', Number(event.target.value))} />
/>
<Select <Select
inputKey='linkedSymbolId' inputKey='linkedSymbolId'
labelText='Align with symbol' labelText='Align with symbol'
@ -245,11 +225,8 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
value: symbol.id value: symbol.id
}))} }))}
value={props.properties.linkedSymbolId ?? ''} value={props.properties.linkedSymbolId ?? ''}
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} />
/> {GetCSSInputs(props.properties, props.onChange)}
{ getCSSInputs(props.properties, props.onChange) }
</div> </div>
); );
}; }
export default ContainerForm;

View file

@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react'; import * as React from 'react';
import { expect, describe, it, vi } from 'vitest'; import { expect, describe, it, vi } from 'vitest';
import { XPositionReference } from '../../Enums/XPositionReference'; import { XPositionReference } from '../../Enums/XPositionReference';
import IContainerProperties from '../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../Interfaces/IContainerProperties';
import { Properties } from './ContainerProperties'; import { Properties } from './ContainerProperties';
describe.concurrent('Properties', () => { describe.concurrent('Properties', () => {
@ -33,7 +33,7 @@ describe.concurrent('Properties', () => {
minWidth: 1, minWidth: 1,
maxWidth: Infinity, maxWidth: Infinity,
margin: {}, margin: {},
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
isFlex: false, isFlex: false,
isAnchor: false isAnchor: false
}; };

View file

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
import IContainerProperties from '../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import ContainerForm from './ContainerForm'; import { ContainerForm } from './ContainerForm';
interface IPropertiesProps { interface IPropertiesProps {
properties?: IContainerProperties properties?: IContainerProperties
@ -10,7 +10,7 @@ interface IPropertiesProps {
onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
} }
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => { export function Properties(props: IPropertiesProps): JSX.Element {
if (props.properties === undefined) { if (props.properties === undefined) {
return <div></div>; return <div></div>;
} }
@ -20,8 +20,7 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
<ContainerForm <ContainerForm
properties={props.properties} properties={props.properties}
symbols={props.symbols} symbols={props.symbols}
onChange={props.onChange} onChange={props.onChange} />
/>
</div> </div>
); );
}; }

View file

@ -2,16 +2,15 @@ import { Dispatch, SetStateAction } from 'react';
import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { IConfiguration } from '../../../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel';
import { findContainerById, MakeIterator } from '../../../utils/itertools'; import { FindContainerById, MakeIterator } from '../../../utils/itertools';
import { getCurrentHistory, UpdateCounters } from '../Editor'; import { GetCurrentHistory, UpdateCounters } from '../Editor';
import { AddMethod } from '../../../Enums/AddMethod'; import { AddMethod } from '../../../Enums/AddMethod';
import { IAvailableContainer } from '../../../Interfaces/IAvailableContainer'; import { IAvailableContainer } from '../../../Interfaces/IAvailableContainer';
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../../utils/default'; import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../../utils/default';
import { ApplyBehaviors } from '../Behaviors/Behaviors'; import { ApplyBehaviors } from '../Behaviors/Behaviors';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import { ApplyMargin, transformX } from '../../../utils/svg'; import { ApplyMargin, TransformX } from '../../../utils/svg';
import { Flex } from '../Behaviors/FlexBehaviors';
import { PropertyType } from '../../../Enums/PropertyType'; import { PropertyType } from '../../../Enums/PropertyType';
/** /**
@ -25,16 +24,16 @@ export function SelectContainer(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
history.push({ history.push({
LastAction: `Select ${containerId}`, lastAction: `Select ${containerId}`,
MainContainer: structuredClone(current.MainContainer), mainContainer: structuredClone(current.mainContainer),
SelectedContainerId: containerId, selectedContainerId: containerId,
TypeCounters: Object.assign({}, current.TypeCounters), typeCounters: Object.assign({}, current.typeCounters),
Symbols: structuredClone(current.Symbols), symbols: structuredClone(current.symbols),
SelectedSymbolId: current.SelectedSymbolId selectedSymbolId: current.selectedSymbolId
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
@ -55,11 +54,11 @@ export function DeleteContainer(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const mainContainerClone: IContainerModel = structuredClone(current.mainContainer);
const container = findContainerById(mainContainerClone, containerId); const container = FindContainerById(mainContainerClone, containerId);
if (container === undefined) { if (container === undefined) {
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
@ -80,7 +79,7 @@ export function DeleteContainer(
throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
} }
const newSymbols = structuredClone(current.Symbols); const newSymbols = structuredClone(current.symbols);
UnlinkSymbol(newSymbols, container); UnlinkSymbol(newSymbols, container);
const index = container.parent.children.indexOf(container); const index = container.parent.children.indexOf(container);
@ -90,35 +89,40 @@ export function DeleteContainer(
throw new Error('[DeleteContainer] Could not find container among parent\'s children'); throw new Error('[DeleteContainer] Could not find container among parent\'s children');
} }
ApplyBehaviorsOnSiblings(container, current.Symbols); ApplyBehaviorsOnSiblings(container, current.symbols);
// Select the previous container // Select the previous container
// or select the one above // or select the one above
const SelectedContainerId = GetSelectedContainerOnDelete( const selectedContainerId = GetSelectedContainerOnDelete(
mainContainerClone, mainContainerClone,
current.SelectedContainerId, current.selectedContainerId,
container.parent, container.parent,
index index
); );
history.push({ history.push({
LastAction: `Delete ${containerId}`, lastAction: `Delete ${containerId}`,
MainContainer: mainContainerClone, mainContainer: mainContainerClone,
SelectedContainerId, selectedContainerId,
TypeCounters: Object.assign({}, current.TypeCounters), typeCounters: Object.assign({}, current.typeCounters),
Symbols: newSymbols, symbols: newSymbols,
SelectedSymbolId: current.SelectedSymbolId selectedSymbolId: current.selectedSymbolId
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
} }
function GetSelectedContainerOnDelete(mainContainerClone: IContainerModel, selectedContainerId: string, parent: IContainerModel, index: number): string { function GetSelectedContainerOnDelete(
const SelectedContainer = findContainerById(mainContainerClone, selectedContainerId) ?? mainContainerClone: IContainerModel,
selectedContainerId: string,
parent: IContainerModel,
index: number
): string {
const newSelectedContainer = FindContainerById(mainContainerClone, selectedContainerId) ??
parent.children.at(index - 1) ?? parent.children.at(index - 1) ??
parent; parent;
const SelectedContainerId = SelectedContainer.properties.id; const newSelectedContainerId = newSelectedContainer.properties.id;
return SelectedContainerId; return newSelectedContainerId;
} }
function UnlinkSymbol(symbols: Map<string, ISymbolModel>, container: IContainerModel): void { function UnlinkSymbol(symbols: Map<string, ISymbolModel>, container: IContainerModel): void {
@ -191,7 +195,7 @@ export function AddContainer(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
// Get the preset properties from the API // Get the preset properties from the API
@ -203,15 +207,15 @@ export function AddContainer(
} }
// Set the counter of the object type in order to assign an unique id // Set the counter of the object type in order to assign an unique id
const newCounters = Object.assign({}, current.TypeCounters); const newCounters = Object.assign({}, current.typeCounters);
UpdateCounters(newCounters, type); UpdateCounters(newCounters, type);
const count = newCounters[type]; const count = newCounters[type];
// Create maincontainer model // Create maincontainer model
const clone: IContainerModel = structuredClone(current.MainContainer); const clone: IContainerModel = structuredClone(current.mainContainer);
// Find the parent // Find the parent
const parentClone: IContainerModel | undefined = findContainerById( const parentClone: IContainerModel | undefined = FindContainerById(
clone, parentId clone, parentId
); );
@ -258,18 +262,18 @@ export function AddContainer(
InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters); InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters);
ApplyBehaviors(newContainer, current.Symbols); ApplyBehaviors(newContainer, current.symbols);
ApplyBehaviorsOnSiblings(newContainer, current.Symbols); ApplyBehaviorsOnSiblings(newContainer, current.symbols);
// Update the state // Update the state
history.push({ history.push({
LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`, lastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
MainContainer: clone, mainContainer: clone,
SelectedContainerId: parentClone.properties.id, selectedContainerId: parentClone.properties.id,
TypeCounters: newCounters, typeCounters: newCounters,
Symbols: structuredClone(current.Symbols), symbols: structuredClone(current.symbols),
SelectedSymbolId: current.SelectedSymbolId selectedSymbolId: current.selectedSymbolId
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
@ -280,8 +284,8 @@ function UpdateParentChildrenList(parentClone: IContainerModel | null | undefine
return; return;
} }
parentClone.children.sort( parentClone.children.sort(
(a, b) => transformX(a.properties.x, a.properties.width, a.properties.XPositionReference) - (a, b) => TransformX(a.properties.x, a.properties.width, a.properties.xPositionReference) -
transformX(b.properties.x, b.properties.width, b.properties.XPositionReference) TransformX(b.properties.x, b.properties.width, b.properties.xPositionReference)
); );
} }
@ -387,14 +391,14 @@ function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, par
export function OnPropertyChange( export function OnPropertyChange(
key: string, key: string,
value: string | number | boolean, value: string | number | boolean,
type: PropertyType = PropertyType.SIMPLE, type: PropertyType = PropertyType.Simple,
selected: IContainerModel | undefined, selected: IContainerModel | undefined,
fullHistory: IHistoryState[], fullHistory: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
if (selected === null || if (selected === null ||
@ -402,22 +406,22 @@ export function OnPropertyChange(
throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
} }
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const mainContainerClone: IContainerModel = structuredClone(current.mainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, selected.properties.id); const container: ContainerModel | undefined = FindContainerById(mainContainerClone, selected.properties.id);
if (container === null || container === undefined) { if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
} }
SetContainer(container, key, value, type, current.Symbols); SetContainer(container, key, value, type, current.symbols);
history.push({ history.push({
LastAction: `Change ${key} of ${container.properties.id}`, lastAction: `Change ${key} of ${container.properties.id}`,
MainContainer: mainContainerClone, mainContainer: mainContainerClone,
SelectedContainerId: container.properties.id, selectedContainerId: container.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters), typeCounters: Object.assign({}, current.typeCounters),
Symbols: structuredClone(current.Symbols), symbols: structuredClone(current.symbols),
SelectedSymbolId: current.SelectedSymbolId selectedSymbolId: current.selectedSymbolId
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
@ -463,10 +467,10 @@ function SetContainer(
function AssignProperty(container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType): void { function AssignProperty(container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType): void {
switch (type) { switch (type) {
case PropertyType.STYLE: case PropertyType.Style:
(container.properties.style as any)[key] = value; (container.properties.style as any)[key] = value;
break; break;
case PropertyType.MARGIN: case PropertyType.Margin:
SetMargin(); SetMargin();
break; break;
default: default:
@ -514,10 +518,11 @@ function LinkSymbol(
newSymbol.linkedContainers.add(containerId); newSymbol.linkedContainers.add(containerId);
} }
function ApplyBehaviorsOnSiblings(newContainer: ContainerModel, Symbols: Map<string, ISymbolModel>): void {
function ApplyBehaviorsOnSiblings(newContainer: ContainerModel, symbols: Map<string, ISymbolModel>): void {
if (newContainer.parent === null || newContainer.parent === undefined) { if (newContainer.parent === null || newContainer.parent === undefined) {
return; return;
} }
newContainer.parent.children.filter(container => newContainer !== container).forEach(container => ApplyBehaviors(container, Symbols)); newContainer.parent.children.filter(container => newContainer !== container).forEach(container => ApplyBehaviors(container, symbols));
} }

View file

@ -1,6 +1,6 @@
import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { IConfiguration } from '../../../Interfaces/IConfiguration';
import { getCircularReplacer } from '../../../utils/saveload'; import { GetCircularReplacer } from '../../../utils/saveload';
import { ID } from '../../SVG/SVG'; import { ID } from '../../SVG/SVG';
import { IEditorState } from '../../../Interfaces/IEditorState'; import { IEditorState } from '../../../Interfaces/IEditorState';
import { SHOW_SELECTOR_TEXT } from '../../../utils/default'; import { SHOW_SELECTOR_TEXT } from '../../../utils/default';
@ -26,15 +26,15 @@ export function SaveEditorAsJSON(
myWorker.onmessage = (event) => { myWorker.onmessage = (event) => {
const data = event.data; const data = event.data;
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
createDownloadNode(exportName, dataStr); CreateDownloadNode(exportName, dataStr);
myWorker.terminate(); myWorker.terminate();
}; };
return; return;
} }
const data = JSON.stringify(editorState, getCircularReplacer(), spaces); const data = JSON.stringify(editorState, GetCircularReplacer(), spaces);
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
createDownloadNode(exportName, dataStr); CreateDownloadNode(exportName, dataStr);
} }
export function SaveEditorAsSVG(): void { export function SaveEditorAsSVG(): void {
@ -64,10 +64,10 @@ export function SaveEditorAsSVG(): void {
let source = serializer.serializeToString(svg); let source = serializer.serializeToString(svg);
// add name spaces. // add name spaces.
if (source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/) == null) { if (source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/) == null) {
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"'); source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
} }
if (source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/) == null) { if (source.match(/^<svg[^>]+"http:\/\/www\.w3\.org\/1999\/xlink"/) == null) {
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"'); source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
} }
@ -76,10 +76,10 @@ export function SaveEditorAsSVG(): void {
// convert svg source to URI data scheme. // convert svg source to URI data scheme.
const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source); const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source);
createDownloadNode('state.svg', url); CreateDownloadNode('state.svg', url);
} }
function createDownloadNode(filename: string, datastring: string): void { function CreateDownloadNode(filename: string, datastring: string): void {
const downloadAnchorNode = document.createElement('a'); const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.href = datastring; downloadAnchorNode.href = datastring;
downloadAnchorNode.download = filename; downloadAnchorNode.download = filename;

View file

@ -2,7 +2,7 @@ import { Dispatch, SetStateAction } from 'react';
import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { ENABLE_SHORTCUTS } from '../../../utils/default'; import { ENABLE_SHORTCUTS } from '../../../utils/default';
export function onKeyDown( export function OnKey(
event: KeyboardEvent, event: KeyboardEvent,
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,

View file

@ -4,10 +4,10 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { GetDefaultSymbolModel } from '../../../utils/default'; import { GetDefaultSymbolModel } from '../../../utils/default';
import { findContainerById } from '../../../utils/itertools'; import { FindContainerById } from '../../../utils/itertools';
import { restoreX } from '../../../utils/svg'; import { RestoreX } from '../../../utils/svg';
import { ApplyBehaviors } from '../Behaviors/Behaviors'; import { ApplyBehaviors } from '../Behaviors/Behaviors';
import { getCurrentHistory, UpdateCounters } from '../Editor'; import { GetCurrentHistory, UpdateCounters } from '../Editor';
export function AddSymbol( export function AddSymbol(
name: string, name: string,
@ -17,7 +17,7 @@ export function AddSymbol(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
const symbolConfig = configuration.AvailableSymbols const symbolConfig = configuration.AvailableSymbols
@ -27,22 +27,22 @@ export function AddSymbol(
throw new Error('[AddSymbol] Symbol could not be found in the config'); throw new Error('[AddSymbol] Symbol could not be found in the config');
} }
const type = `symbol-${name}`; const type = `symbol-${name}`;
const newCounters = structuredClone(current.TypeCounters); const newCounters = structuredClone(current.typeCounters);
UpdateCounters(newCounters, type); UpdateCounters(newCounters, type);
const newSymbols = structuredClone(current.Symbols); const newSymbols = structuredClone(current.symbols);
const newSymbol: ISymbolModel = GetDefaultSymbolModel(name, newCounters, type, symbolConfig); const newSymbol: ISymbolModel = GetDefaultSymbolModel(name, newCounters, type, symbolConfig);
newSymbol.x = restoreX(newSymbol.x, newSymbol.width, newSymbol.config.XPositionReference); newSymbol.x = RestoreX(newSymbol.x, newSymbol.width, newSymbol.config.XPositionReference);
newSymbols.set(newSymbol.id, newSymbol); newSymbols.set(newSymbol.id, newSymbol);
history.push({ history.push({
LastAction: `Add ${name}`, lastAction: `Add ${name}`,
MainContainer: structuredClone(current.MainContainer), mainContainer: structuredClone(current.mainContainer),
SelectedContainerId: current.SelectedContainerId, selectedContainerId: current.selectedContainerId,
TypeCounters: newCounters, typeCounters: newCounters,
Symbols: newSymbols, symbols: newSymbols,
SelectedSymbolId: newSymbol.id selectedSymbolId: newSymbol.id
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
@ -55,16 +55,16 @@ export function SelectSymbol(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
history.push({ history.push({
LastAction: `Select ${symbolId}`, lastAction: `Select ${symbolId}`,
MainContainer: structuredClone(current.MainContainer), mainContainer: structuredClone(current.mainContainer),
SelectedContainerId: current.SelectedContainerId, selectedContainerId: current.selectedContainerId,
TypeCounters: structuredClone(current.TypeCounters), typeCounters: structuredClone(current.typeCounters),
Symbols: structuredClone(current.Symbols), symbols: structuredClone(current.symbols),
SelectedSymbolId: symbolId selectedSymbolId: symbolId
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
@ -77,29 +77,29 @@ export function DeleteSymbol(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
const newSymbols = structuredClone(current.Symbols); const newSymbols = structuredClone(current.symbols);
const symbol = newSymbols.get(symbolId); const symbol = newSymbols.get(symbolId);
if (symbol === undefined) { if (symbol === undefined) {
throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`); throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`);
} }
const newMainContainer = structuredClone(current.MainContainer); const newMainContainer = structuredClone(current.mainContainer);
UnlinkContainers(symbol, newMainContainer); UnlinkContainers(symbol, newMainContainer);
newSymbols.delete(symbolId); newSymbols.delete(symbolId);
history.push({ history.push({
LastAction: `Select ${symbolId}`, lastAction: `Select ${symbolId}`,
MainContainer: newMainContainer, mainContainer: newMainContainer,
SelectedContainerId: current.SelectedContainerId, selectedContainerId: current.selectedContainerId,
TypeCounters: structuredClone(current.TypeCounters), typeCounters: structuredClone(current.typeCounters),
Symbols: newSymbols, symbols: newSymbols,
SelectedSymbolId: symbolId selectedSymbolId: symbolId
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);
@ -107,7 +107,7 @@ export function DeleteSymbol(
function UnlinkContainers(symbol: ISymbolModel, newMainContainer: IContainerModel): void { function UnlinkContainers(symbol: ISymbolModel, newMainContainer: IContainerModel): void {
symbol.linkedContainers.forEach((containerId) => { symbol.linkedContainers.forEach((containerId) => {
const container = findContainerById(newMainContainer, containerId); const container = FindContainerById(newMainContainer, containerId);
if (container === undefined) { if (container === undefined) {
return; return;
@ -131,15 +131,15 @@ export function OnPropertyChange(
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
const history = getCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
if (current.SelectedSymbolId === '') { if (current.selectedSymbolId === '') {
throw new Error('[OnSymbolPropertyChange] Property was changed before selecting a symbol'); throw new Error('[OnSymbolPropertyChange] Property was changed before selecting a symbol');
} }
const newSymbols: Map<string, ISymbolModel> = structuredClone(current.Symbols); const newSymbols: Map<string, ISymbolModel> = structuredClone(current.symbols);
const symbol = newSymbols.get(current.SelectedSymbolId); const symbol = newSymbols.get(current.selectedSymbolId);
if (symbol === null || symbol === undefined) { if (symbol === null || symbol === undefined) {
throw new Error('[OnSymbolPropertyChange] Symbol model was not found in state!'); throw new Error('[OnSymbolPropertyChange] Symbol model was not found in state!');
@ -147,9 +147,9 @@ export function OnPropertyChange(
(symbol as any)[key] = value; (symbol as any)[key] = value;
const newMainContainer = structuredClone(current.MainContainer); const newMainContainer = structuredClone(current.mainContainer);
symbol.linkedContainers.forEach((containerId) => { symbol.linkedContainers.forEach((containerId) => {
const container = findContainerById(newMainContainer, containerId); const container = FindContainerById(newMainContainer, containerId);
if (container === undefined) { if (container === undefined) {
return; return;
@ -159,12 +159,12 @@ export function OnPropertyChange(
}); });
history.push({ history.push({
LastAction: `Change ${key} of ${symbol.id}`, lastAction: `Change ${key} of ${symbol.id}`,
MainContainer: newMainContainer, mainContainer: newMainContainer,
SelectedContainerId: current.SelectedContainerId, selectedContainerId: current.selectedContainerId,
TypeCounters: Object.assign({}, current.TypeCounters), typeCounters: Object.assign({}, current.typeCounters),
Symbols: newSymbols, symbols: newSymbols,
SelectedSymbolId: symbol.id selectedSymbolId: symbol.id
}); });
setHistory(history); setHistory(history);
setHistoryCurrentStep(history.length - 1); setHistoryCurrentStep(history.length - 1);

View file

@ -14,7 +14,7 @@
*/ */
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { constraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors'; import { ConstraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
/** /**
* Impose the container position to its siblings * Impose the container position to its siblings
@ -31,9 +31,9 @@ export function ApplyAnchor(container: IContainerModel): IContainerModel {
child => !child.properties.isAnchor child => !child.properties.isAnchor
); );
const overlappingContainers = getOverlappingContainers(container, rigidBodies); const overlappingContainers = GetOverlappingContainers(container, rigidBodies);
for (const overlappingContainer of overlappingContainers) { for (const overlappingContainer of overlappingContainers) {
constraintBodyInsideUnallocatedWidth(overlappingContainer); ConstraintBodyInsideUnallocatedWidth(overlappingContainer);
} }
return container; return container;
} }
@ -44,7 +44,7 @@ export function ApplyAnchor(container: IContainerModel): IContainerModel {
* @param containers A list of containers * @param containers A list of containers
* @returns A list of overlapping containers * @returns A list of overlapping containers
*/ */
function getOverlappingContainers( function GetOverlappingContainers(
container: IContainerModel, container: IContainerModel,
containers: IContainerModel[] containers: IContainerModel[]
): IContainerModel[] { ): IContainerModel[] {

View file

@ -1,4 +1,3 @@
import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { Simplex } from '../../../utils/simplex'; import { Simplex } from '../../../utils/simplex';
import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg'; import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg';

View file

@ -1,5 +1,5 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { reversePairwise } from '../../../utils/itertools'; import { ReversePairwise } from '../../../utils/itertools';
import { Flex } from './FlexBehaviors'; import { Flex } from './FlexBehaviors';
/** /**
@ -32,7 +32,7 @@ export function PushContainers(container: IContainerModel): IContainerModel {
// FIXME: A fix was applied using toFixed(2). // FIXME: A fix was applied using toFixed(2).
// FIXME: A coverture check must be done to ensure that all scenarios are covered // FIXME: A coverture check must be done to ensure that all scenarios are covered
const it = reversePairwise<IContainerModel>(container.parent.children.filter(child => child !== container)); const it = ReversePairwise<IContainerModel>(container.parent.children.filter(child => child !== container));
for (const { cur, next } of it) { for (const { cur, next } of it) {
const hasSpaceBetween = next.properties.x + next.properties.width < cur.properties.x; const hasSpaceBetween = next.properties.x + next.properties.width < cur.properties.x;

View file

@ -22,8 +22,8 @@ import { ISizePointer } from '../../../Interfaces/ISizePointer';
export function ApplyRigidBody( export function ApplyRigidBody(
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
container = constraintBodyInsideParent(container); container = ConstraintBodyInsideParent(container);
container = constraintBodyInsideUnallocatedWidth(container); container = ConstraintBodyInsideUnallocatedWidth(container);
return container; return container;
} }
@ -35,7 +35,7 @@ export function ApplyRigidBody(
* @param container * @param container
* @returns Updated container * @returns Updated container
*/ */
function constraintBodyInsideParent( function ConstraintBodyInsideParent(
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
if (container.parent === null || container.parent === undefined) { if (container.parent === null || container.parent === undefined) {
@ -46,7 +46,7 @@ function constraintBodyInsideParent(
const parentWidth = parentProperties.width; const parentWidth = parentProperties.width;
const parentHeight = parentProperties.height; const parentHeight = parentProperties.height;
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight); return ConstraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
} }
/** /**
@ -59,7 +59,7 @@ function constraintBodyInsideParent(
* @param height height of the rectangle * @param height height of the rectangle
* @returns Updated container * @returns Updated container
*/ */
function constraintBodyInsideSpace( function ConstraintBodyInsideSpace(
container: IContainerModel, container: IContainerModel,
x: number, x: number,
y: number, y: number,
@ -113,7 +113,7 @@ function constraintBodyInsideSpace(
* @param container * @param container
* @returns Updated container * @returns Updated container
*/ */
export function constraintBodyInsideUnallocatedWidth( export function ConstraintBodyInsideUnallocatedWidth(
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
if (container.parent === null || container.parent === undefined) { if (container.parent === null || container.parent === undefined) {
@ -121,7 +121,7 @@ export function constraintBodyInsideUnallocatedWidth(
} }
// Get the available spaces of the parent // Get the available spaces of the parent
const availableWidths = getAvailableWidths(container.parent, container); const availableWidths = GetAvailableWidths(container.parent, container);
const containerX = container.properties.x; const containerX = container.properties.x;
const containerWidth = container.properties.width; const containerWidth = container.properties.width;
@ -158,7 +158,7 @@ export function constraintBodyInsideUnallocatedWidth(
// Check if the container actually fit inside // Check if the container actually fit inside
// It will usually fit if it was alrady fitting // It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) => const availableWidthFound = availableWidths.find((width) =>
isFitting(container.properties.width, width) IsFitting(container.properties.width, width)
); );
if (availableWidthFound === undefined) { if (availableWidthFound === undefined) {
@ -170,7 +170,7 @@ export function constraintBodyInsideUnallocatedWidth(
// We want the container to fit automatically inside the available space // We want the container to fit automatically inside the available space
// even if it means to resize the container // even if it means to resize the container
const availableWidth: ISizePointer | undefined = availableWidths.find((width) => { const availableWidth: ISizePointer | undefined = availableWidths.find((width) => {
return isFitting(container.properties.minWidth, width); return IsFitting(container.properties.minWidth, width);
}); });
if (availableWidth === undefined) { if (availableWidth === undefined) {
@ -191,7 +191,7 @@ export function constraintBodyInsideUnallocatedWidth(
return container; return container;
} }
return constraintBodyInsideSpace( return ConstraintBodyInsideSpace(
container, container,
availableWidthFound.x, availableWidthFound.x,
0, 0,
@ -206,10 +206,10 @@ export function constraintBodyInsideUnallocatedWidth(
* @param sizePointer Size space to check * @param sizePointer Size space to check
* @returns * @returns
*/ */
const isFitting = ( function IsFitting(containerWidth: number,
containerWidth: number, sizePointer: ISizePointer): boolean {
sizePointer: ISizePointer return containerWidth <= sizePointer.width;
): boolean => containerWidth <= sizePointer.width; }
/** /**
* Get the unallocated widths inside a container * Get the unallocated widths inside a container
@ -220,7 +220,7 @@ const isFitting = (
* @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded) * @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded)
* @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space) * @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
*/ */
function getAvailableWidths( function GetAvailableWidths(
container: IContainerModel, container: IContainerModel,
exception: IContainerModel exception: IContainerModel
): ISizePointer[] { ): ISizePointer[] {
@ -251,7 +251,7 @@ function getAvailableWidths(
// In order to find unallocated space, // In order to find unallocated space,
// We need to calculate the overlap between the two containers // We need to calculate the overlap between the two containers
// We only works with widths meaning in 1D (with lines) // We only works with widths meaning in 1D (with lines)
const newUnallocatedWidths = getAvailableWidthsTwoLines( const newUnallocatedWidths = GetAvailableWidthsTwoLines(
unallocatedSpace, unallocatedSpace,
childX, childX,
childWidth childWidth
@ -274,7 +274,7 @@ function getAvailableWidths(
* @param rectWidth width of the second line * @param rectWidth width of the second line
* @returns Available widths * @returns Available widths
*/ */
function getAvailableWidthsTwoLines( function GetAvailableWidthsTwoLines(
unallocatedSpace: ISizePointer, unallocatedSpace: ISizePointer,
rectX: number, rectX: number,
rectWidth: number rectWidth: number
@ -295,18 +295,18 @@ function getAvailableWidthsTwoLines(
const isOverlappingOnTheLeft = unallocatedSpace.x >= rectX; const isOverlappingOnTheLeft = unallocatedSpace.x >= rectX;
if (isOverlappingOnTheLeft) { if (isOverlappingOnTheLeft) {
return getAvailableWidthsLeft(unallocatedSpaceRight, rectRight); return GetAvailableWidthsLeft(unallocatedSpaceRight, rectRight);
} }
const isOverlappingOnTheRight = rectRight >= unallocatedSpaceRight; const isOverlappingOnTheRight = rectRight >= unallocatedSpaceRight;
if (isOverlappingOnTheRight) { if (isOverlappingOnTheRight) {
return getAvailableWidthsRight(unallocatedSpace.x, rectX); return GetAvailableWidthsRight(unallocatedSpace.x, rectX);
} }
return getAvailableWidthsMiddle(unallocatedSpace.x, unallocatedSpaceRight, rectX, rectRight); return GetAvailableWidthsMiddle(unallocatedSpace.x, unallocatedSpaceRight, rectX, rectRight);
} }
function getAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number): ISizePointer[] { function GetAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number): ISizePointer[] {
if (unallocatedSpaceRight - rectRight <= 0) { if (unallocatedSpaceRight - rectRight <= 0) {
return []; return [];
} }
@ -319,7 +319,7 @@ function getAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number
]; ];
} }
function getAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISizePointer[] { function GetAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISizePointer[] {
if (rectX - unallocatedSpaceX <= 0) { if (rectX - unallocatedSpaceX <= 0) {
return []; return [];
} }
@ -332,7 +332,7 @@ function getAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISiz
]; ];
} }
function getAvailableWidthsMiddle( function GetAvailableWidthsMiddle(
unallocatedSpaceX: number, unallocatedSpaceX: number,
unallocatedSpaceRight: number, unallocatedSpaceRight: number,
rectX: number, rectX: number,

View file

@ -1,12 +1,12 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { applyParentTransform } from '../../../utils/itertools'; import { ApplyParentTransform } from '../../../utils/itertools';
import { restoreX, transformX } from '../../../utils/svg'; import { RestoreX, TransformX } from '../../../utils/svg';
export function ApplySymbol(container: IContainerModel, symbol: ISymbolModel): IContainerModel { export function ApplySymbol(container: IContainerModel, symbol: ISymbolModel): IContainerModel {
container.properties.x = transformX(symbol.x, symbol.width, symbol.config.XPositionReference); container.properties.x = TransformX(symbol.x, symbol.width, symbol.config.XPositionReference);
container.properties.x = restoreX(container.properties.x, container.properties.width, container.properties.XPositionReference); container.properties.x = RestoreX(container.properties.x, container.properties.width, container.properties.xPositionReference);
const [x] = applyParentTransform(container.parent, container.properties.x, 0); const [x] = ApplyParentTransform(container.parent, container.properties.x, 0);
container.properties.x = x; container.properties.x = x;
return container; return container;
} }

View file

@ -6,12 +6,12 @@ import { IHistoryState } from '../../Interfaces/IHistoryState';
import { UI } from '../UI/UI'; import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange } from './Actions/ContainerOperations'; import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange } from './Actions/ContainerOperations';
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
import { onKeyDown } from './Actions/Shortcuts'; import { OnKey } from './Actions/Shortcuts';
import EditorEvents from '../../Events/EditorEvents'; import EditorEvents from '../../Events/EditorEvents';
import { IEditorState } from '../../Interfaces/IEditorState'; import { IEditorState } from '../../Interfaces/IEditorState';
import { MAX_HISTORY } from '../../utils/default'; import { MAX_HISTORY } from '../../utils/default';
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations'; import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations';
import { findContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
interface IEditorProps { interface IEditorProps {
root: Element | Document root: Element | Document
@ -29,35 +29,40 @@ export function UpdateCounters(counters: Record<string, number>, type: string):
} }
} }
export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] => export function GetCurrentHistory(history: IHistoryState[], historyCurrentStep: number): IHistoryState[] {
history.slice( return history.slice(
Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow) Math.max(0, history.length - MAX_HISTORY),
historyCurrentStep + 1 historyCurrentStep + 1
); );
}
export const getCurrentHistoryState = (history: IHistoryState[], historyCurrentStep: number): IHistoryState => history[historyCurrentStep]; export function GetCurrentHistoryState(history: IHistoryState[], historyCurrentStep: number): IHistoryState {
return history[historyCurrentStep];
}
function useShortcuts( function UseShortcuts(
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void { ): void {
useEffect(() => { useEffect(() => {
const onKeyUp = (event: KeyboardEvent): void => onKeyDown( function OnKeyUp(event: KeyboardEvent): void {
event, return OnKey(
history, event,
historyCurrentStep, history,
setHistoryCurrentStep historyCurrentStep,
); setHistoryCurrentStep
);
}
window.addEventListener('keyup', onKeyUp); window.addEventListener('keyup', OnKeyUp);
return () => { return () => {
window.removeEventListener('keyup', onKeyUp); window.removeEventListener('keyup', OnKeyUp);
}; };
}); });
} }
function useWindowEvents( function UseWindowEvents(
root: Element | Document, root: Element | Document,
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
@ -76,15 +81,17 @@ function useWindowEvents(
const funcs = new Map<string, () => void>(); const funcs = new Map<string, () => void>();
for (const event of events) { for (const event of events) {
const func = (eventInitDict?: CustomEventInit): void => event.func( function Func(eventInitDict?: CustomEventInit): void {
root, return event.func(
editorState, root,
setHistory, editorState,
setHistoryCurrentStep, setHistory,
eventInitDict setHistoryCurrentStep,
); eventInitDict
editorRef.current?.addEventListener(event.name, func); );
funcs.set(event.name, func); }
editorRef.current?.addEventListener(event.name, Func);
funcs.set(event.name, Func);
} }
return () => { return () => {
for (const event of events) { for (const event of events) {
@ -98,13 +105,13 @@ function useWindowEvents(
}); });
} }
const Editor: React.FunctionComponent<IEditorProps> = (props) => { export function Editor(props: IEditorProps): JSX.Element {
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history)); const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep); const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
const editorRef = useRef<HTMLDivElement>(null); const editorRef = useRef<HTMLDivElement>(null);
useShortcuts(history, historyCurrentStep, setHistoryCurrentStep); UseShortcuts(history, historyCurrentStep, setHistoryCurrentStep);
useWindowEvents( UseWindowEvents(
props.root, props.root,
history, history,
historyCurrentStep, historyCurrentStep,
@ -115,32 +122,32 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
); );
const configuration = props.configuration; const configuration = props.configuration;
const current = getCurrentHistoryState(history, historyCurrentStep); const current = GetCurrentHistoryState(history, historyCurrentStep);
const selected = findContainerById(current.MainContainer, current.SelectedContainerId); const selected = FindContainerById(current.mainContainer, current.selectedContainerId);
return ( return (
<div ref={editorRef} className="Editor font-sans h-full"> <div ref={editorRef} className="Editor font-sans h-full">
<UI <UI
SelectedContainer={selected} selectedContainer={selected}
current={current} current={current}
history={history} history={history}
historyCurrentStep={historyCurrentStep} historyCurrentStep={historyCurrentStep}
AvailableContainers={configuration.AvailableContainers} availableContainers={configuration.AvailableContainers}
AvailableSymbols={configuration.AvailableSymbols} availableSymbols={configuration.AvailableSymbols}
SelectContainer={(container) => SelectContainer( selectContainer={(container) => SelectContainer(
container, container,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
DeleteContainer={(containerId: string) => DeleteContainer( deleteContainer={(containerId: string) => DeleteContainer(
containerId, containerId,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
OnPropertyChange={(key, value, type) => OnPropertyChange( onPropertyChange={(key, value, type) => OnPropertyChange(
key, value, type, key, value, type,
selected, selected,
history, history,
@ -148,7 +155,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
AddContainer={(type) => AddContainerToSelectedContainer( addContainer={(type) => AddContainerToSelectedContainer(
type, type,
selected, selected,
configuration, configuration,
@ -157,7 +164,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
AddSymbol={(type) => AddSymbol( addSymbol={(type) => AddSymbol(
type, type,
configuration, configuration,
history, history,
@ -165,45 +172,42 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
OnSymbolPropertyChange={(key, value) => OnSymbolPropertyChange( onSymbolPropertyChange={(key, value) => OnSymbolPropertyChange(
key, value, key, value,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
SelectSymbol={(symbolId) => SelectSymbol( selectSymbol={(symbolId) => SelectSymbol(
symbolId, symbolId,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
DeleteSymbol={(symbolId) => DeleteSymbol( deleteSymbol={(symbolId) => DeleteSymbol(
symbolId, symbolId,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
SaveEditorAsJSON={() => SaveEditorAsJSON( saveEditorAsJSON={() => SaveEditorAsJSON(
history, history,
historyCurrentStep, historyCurrentStep,
configuration configuration
)} )}
SaveEditorAsSVG={() => SaveEditorAsSVG()} saveEditorAsSVG={() => SaveEditorAsSVG()}
LoadState={(move) => setHistoryCurrentStep(move)} loadState={(move) => setHistoryCurrentStep(move)} />
/>
<SVG <SVG
width={current.MainContainer?.properties.width} width={current.mainContainer?.properties.width}
height={current.MainContainer?.properties.height} height={current.mainContainer?.properties.height}
selected={selected} selected={selected}
symbols={current.Symbols} symbols={current.symbols}
> >
{ current.MainContainer } {current.mainContainer}
</SVG> </SVG>
</div> </div>
); );
}; }
export default Editor;

View file

@ -4,13 +4,13 @@ import { fireEvent, render, screen } from '../../utils/test-utils';
import { ElementsSidebar } from './ElementsSidebar'; import { ElementsSidebar } from './ElementsSidebar';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { XPositionReference } from '../../Enums/XPositionReference'; import { XPositionReference } from '../../Enums/XPositionReference';
import { findContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
describe.concurrent('Elements sidebar', () => { describe.concurrent('Elements sidebar', () => {
it('With a MainContainer', () => { it('With a MainContainer', () => {
render(<ElementsSidebar render(<ElementsSidebar
symbols={new Map()} symbols={new Map()}
MainContainer={{ mainContainer={{
children: [], children: [],
parent: null, parent: null,
properties: { properties: {
@ -27,17 +27,17 @@ describe.concurrent('Elements sidebar', () => {
type: 'type', type: 'type',
maxWidth: Infinity, maxWidth: Infinity,
isFlex: false, isFlex: false,
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
isAnchor: false isAnchor: false
}, },
userData: {} userData: {}
}} }}
isOpen={true} isOpen={true}
isHistoryOpen={false} isHistoryOpen={false}
SelectedContainer={undefined} selectedContainer={undefined}
OnPropertyChange={() => {}} onPropertyChange={() => {}}
SelectContainer={() => {}} selectContainer={() => {}}
DeleteContainer={() => {}} deleteContainer={() => {}}
/>); />);
expect(screen.getByText(/Elements/i)); expect(screen.getByText(/Elements/i));
@ -46,7 +46,7 @@ describe.concurrent('Elements sidebar', () => {
}); });
it('With a selected MainContainer', () => { it('With a selected MainContainer', () => {
const MainContainer: IContainerModel = { const mainContainer: IContainerModel = {
children: [], children: [],
parent: null, parent: null,
properties: { properties: {
@ -64,20 +64,20 @@ describe.concurrent('Elements sidebar', () => {
maxWidth: Infinity, maxWidth: Infinity,
type: 'type', type: 'type',
isAnchor: false, isAnchor: false,
XPositionReference: XPositionReference.Left xPositionReference: XPositionReference.Left
}, },
userData: {} userData: {}
}; };
const { container } = render(<ElementsSidebar const { container } = render(<ElementsSidebar
symbols={new Map()} symbols={new Map()}
MainContainer={MainContainer} mainContainer={mainContainer}
isOpen={true} isOpen={true}
isHistoryOpen={false} isHistoryOpen={false}
SelectedContainer={MainContainer} selectedContainer={mainContainer}
OnPropertyChange={() => {}} onPropertyChange={() => {}}
SelectContainer={() => {}} selectContainer={() => {}}
DeleteContainer={() => {}} deleteContainer={() => {}}
/>); />);
expect(screen.getByText(/Elements/i)); expect(screen.getByText(/Elements/i));
@ -94,22 +94,22 @@ describe.concurrent('Elements sidebar', () => {
const propertyY = container.querySelector('#y'); const propertyY = container.querySelector('#y');
const propertyWidth = container.querySelector('#width'); const propertyWidth = container.querySelector('#width');
const propertyHeight = container.querySelector('#height'); const propertyHeight = container.querySelector('#height');
expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect((propertyId as HTMLInputElement).value).toBe(mainContainer.properties.id.toString());
expect(propertyParentId).toBeDefined(); expect(propertyParentId).toBeDefined();
expect((propertyParentId as HTMLInputElement).value).toBe(''); expect((propertyParentId as HTMLInputElement).value).toBe('');
expect(propertyX).toBeDefined(); expect(propertyX).toBeDefined();
expect((propertyX as HTMLInputElement).value).toBe(MainContainer.properties.x.toString()); expect((propertyX as HTMLInputElement).value).toBe(mainContainer.properties.x.toString());
expect(propertyY).toBeDefined(); expect(propertyY).toBeDefined();
expect((propertyY as HTMLInputElement).value).toBe(MainContainer.properties.y.toString()); expect((propertyY as HTMLInputElement).value).toBe(mainContainer.properties.y.toString());
expect(propertyWidth).toBeDefined(); expect(propertyWidth).toBeDefined();
expect((propertyWidth as HTMLInputElement).value).toBe(MainContainer.properties.width.toString()); expect((propertyWidth as HTMLInputElement).value).toBe(mainContainer.properties.width.toString());
expect(propertyHeight).toBeDefined(); expect(propertyHeight).toBeDefined();
expect((propertyHeight as HTMLInputElement).value).toBe(MainContainer.properties.height.toString()); expect((propertyHeight as HTMLInputElement).value).toBe(mainContainer.properties.height.toString());
}); });
it('With multiple containers', () => { it('With multiple containers', () => {
const children: IContainerModel[] = []; const children: IContainerModel[] = [];
const MainContainer: IContainerModel = { const mainContainer: IContainerModel = {
children, children,
parent: null, parent: null,
properties: { properties: {
@ -122,7 +122,7 @@ describe.concurrent('Elements sidebar', () => {
minWidth: 1, minWidth: 1,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
margin: {}, margin: {},
isFlex: false, isFlex: false,
maxWidth: Infinity, maxWidth: Infinity,
@ -135,7 +135,7 @@ describe.concurrent('Elements sidebar', () => {
children.push( children.push(
{ {
children: [], children: [],
parent: MainContainer, parent: mainContainer,
properties: { properties: {
id: 'child-1', id: 'child-1',
parentId: 'main', parentId: 'main',
@ -151,7 +151,7 @@ describe.concurrent('Elements sidebar', () => {
maxWidth: Infinity, maxWidth: Infinity,
type: 'type', type: 'type',
isAnchor: false, isAnchor: false,
XPositionReference: XPositionReference.Left xPositionReference: XPositionReference.Left
}, },
userData: {} userData: {}
} }
@ -160,7 +160,7 @@ describe.concurrent('Elements sidebar', () => {
children.push( children.push(
{ {
children: [], children: [],
parent: MainContainer, parent: mainContainer,
properties: { properties: {
id: 'child-2', id: 'child-2',
parentId: 'main', parentId: 'main',
@ -172,7 +172,7 @@ describe.concurrent('Elements sidebar', () => {
minWidth: 1, minWidth: 1,
width: 0, width: 0,
height: 0, height: 0,
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
isFlex: false, isFlex: false,
maxWidth: Infinity, maxWidth: Infinity,
type: 'type', type: 'type',
@ -184,13 +184,13 @@ describe.concurrent('Elements sidebar', () => {
render(<ElementsSidebar render(<ElementsSidebar
symbols={new Map()} symbols={new Map()}
MainContainer={MainContainer} mainContainer={mainContainer}
isOpen={true} isOpen={true}
isHistoryOpen={false} isHistoryOpen={false}
SelectedContainer={MainContainer} selectedContainer={mainContainer}
OnPropertyChange={() => {}} onPropertyChange={() => {}}
SelectContainer={() => {}} selectContainer={() => {}}
DeleteContainer={() => {}} deleteContainer={() => {}}
/>); />);
expect(screen.getByText(/Elements/i)); expect(screen.getByText(/Elements/i));
@ -202,7 +202,7 @@ describe.concurrent('Elements sidebar', () => {
it('With multiple containers, change selection', () => { it('With multiple containers, change selection', () => {
const children: IContainerModel[] = []; const children: IContainerModel[] = [];
const MainContainer: IContainerModel = { const mainContainer: IContainerModel = {
children, children,
parent: null, parent: null,
properties: { properties: {
@ -215,7 +215,7 @@ describe.concurrent('Elements sidebar', () => {
minWidth: 1, minWidth: 1,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
margin: {}, margin: {},
isFlex: false, isFlex: false,
maxWidth: Infinity, maxWidth: Infinity,
@ -227,7 +227,7 @@ describe.concurrent('Elements sidebar', () => {
const child1Model: IContainerModel = { const child1Model: IContainerModel = {
children: [], children: [],
parent: MainContainer, parent: mainContainer,
properties: { properties: {
id: 'child-1', id: 'child-1',
parentId: 'main', parentId: 'main',
@ -238,7 +238,7 @@ describe.concurrent('Elements sidebar', () => {
minWidth: 1, minWidth: 1,
width: 0, width: 0,
height: 0, height: 0,
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
margin: {}, margin: {},
isFlex: false, isFlex: false,
maxWidth: Infinity, maxWidth: Infinity,
@ -249,20 +249,20 @@ describe.concurrent('Elements sidebar', () => {
}; };
children.push(child1Model); children.push(child1Model);
let SelectedContainer: IContainerModel | undefined = MainContainer; let selectedContainer: IContainerModel | undefined = mainContainer;
const selectContainer = vi.fn((containerId: string) => { const selectContainer = vi.fn((containerId: string) => {
SelectedContainer = findContainerById(MainContainer, containerId); selectedContainer = FindContainerById(mainContainer, containerId);
}); });
const { container, rerender } = render(<ElementsSidebar const { container, rerender } = render(<ElementsSidebar
symbols={new Map()} symbols={new Map()}
MainContainer={MainContainer} mainContainer={mainContainer}
isOpen={true} isOpen={true}
isHistoryOpen={false} isHistoryOpen={false}
SelectedContainer={SelectedContainer} selectedContainer={selectedContainer}
OnPropertyChange={() => {}} onPropertyChange={() => {}}
SelectContainer={selectContainer} selectContainer={selectContainer}
DeleteContainer={() => {}} deleteContainer={() => {}}
/>); />);
expect(screen.getByText(/Elements/i)); expect(screen.getByText(/Elements/i));
@ -272,20 +272,20 @@ describe.concurrent('Elements sidebar', () => {
expect(child1); expect(child1);
const propertyId = container.querySelector('#id'); const propertyId = container.querySelector('#id');
const propertyParentId = container.querySelector('#parentId'); const propertyParentId = container.querySelector('#parentId');
expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect((propertyId as HTMLInputElement).value).toBe(mainContainer.properties.id.toString());
expect((propertyParentId as HTMLInputElement).value).toBe(''); expect((propertyParentId as HTMLInputElement).value).toBe('');
fireEvent.click(child1); fireEvent.click(child1);
rerender(<ElementsSidebar rerender(<ElementsSidebar
symbols={new Map()} symbols={new Map()}
MainContainer={MainContainer} mainContainer={mainContainer}
isOpen={true} isOpen={true}
isHistoryOpen={false} isHistoryOpen={false}
SelectedContainer={SelectedContainer} selectedContainer={selectedContainer}
OnPropertyChange={() => {}} onPropertyChange={() => {}}
SelectContainer={selectContainer} selectContainer={selectContainer}
DeleteContainer={() => {}} deleteContainer={() => {}}
/>); />);
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();

View file

@ -2,37 +2,33 @@ import * as React from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { Properties } from '../ContainerProperties/ContainerProperties'; import { Properties } from '../ContainerProperties/ContainerProperties';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { getDepth, MakeIterator } from '../../utils/itertools'; import { GetDepth, MakeIterator } from '../../utils/itertools';
import { Menu } from '../Menu/Menu'; import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem'; import { MenuItem } from '../Menu/MenuItem';
import { useMouseEvents } from './MouseEventHandlers'; import { UseMouseEvents } from './MouseEventHandlers';
import { IPoint } from '../../Interfaces/IPoint'; import { IPoint } from '../../Interfaces/IPoint';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
interface IElementsSidebarProps { interface IElementsSidebarProps {
MainContainer: IContainerModel mainContainer: IContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
isOpen: boolean isOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
SelectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
OnPropertyChange: ( onPropertyChange: (
key: string, key: string,
value: string | number | boolean, value: string | number | boolean,
type?: PropertyType type?: PropertyType
) => void ) => void
SelectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
DeleteContainer: (containerid: string) => void deleteContainer: (containerid: string) => void
} }
export const ElementsSidebar: React.FC<IElementsSidebarProps> = ( export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
props: IElementsSidebarProps
): JSX.Element => {
// States // States
const [isContextMenuOpen, setIsContextMenuOpen] = const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
React.useState<boolean>(false); const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
const [onClickContainerId, setOnClickContainerId] =
React.useState<string>('');
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({ const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
x: 0, x: 0,
y: 0 y: 0
@ -41,7 +37,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
const elementRef = React.useRef<HTMLDivElement>(null); const elementRef = React.useRef<HTMLDivElement>(null);
// Event listeners // Event listeners
useMouseEvents( UseMouseEvents(
isContextMenuOpen, isContextMenuOpen,
elementRef, elementRef,
setIsContextMenuOpen, setIsContextMenuOpen,
@ -55,30 +51,25 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
isOpenClasses = props.isHistoryOpen ? 'right-64' : 'right-0'; isOpenClasses = props.isHistoryOpen ? 'right-64' : 'right-0';
} }
const it = MakeIterator(props.MainContainer); const it = MakeIterator(props.mainContainer);
const containers = [...it]; const containers = [...it];
const Row = ({ function Row({
index, index, style
style
}: { }: {
index: number index: number
style: React.CSSProperties style: React.CSSProperties
}): JSX.Element => { }): JSX.Element {
const container = containers[index]; const container = containers[index];
const depth: number = getDepth(container); const depth: number = GetDepth(container);
const key = container.properties.id.toString(); const key = container.properties.id.toString();
const text = const text = container.properties.displayedText === key
container.properties.displayedText === key ? `${'|\t'.repeat(depth)} ${key}`
? `${'|\t'.repeat(depth)} ${key}` : `${'|\t'.repeat(depth)} ${container.properties.displayedText} (${key})`;
: `${'|\t'.repeat(depth)} ${ const selectedClass: string = props.selectedContainer !== undefined &&
container.properties.displayedText props.selectedContainer !== null &&
} (${key})`; props.selectedContainer.properties.id === container.properties.id
const selectedClass: string = ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
props.SelectedContainer !== undefined && : 'bg-slate-300/60 hover:bg-slate-300';
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
return ( return (
<button type="button" <button type="button"
@ -87,12 +78,12 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
id={key} id={key}
key={key} key={key}
style={style} style={style}
onClick={() => props.SelectContainer(container.properties.id)} onClick={() => props.selectContainer(container.properties.id)}
> >
{text} {text}
</button> </button>
); );
}; }
return ( return (
<div <div
@ -121,15 +112,13 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
text="Delete" text="Delete"
onClick={() => { onClick={() => {
setIsContextMenuOpen(false); setIsContextMenuOpen(false);
props.DeleteContainer(onClickContainerId); props.deleteContainer(onClickContainerId);
}} } } />
/>
</Menu> </Menu>
<Properties <Properties
properties={props.SelectedContainer?.properties} properties={props.selectedContainer?.properties}
symbols={props.symbols} symbols={props.symbols}
onChange={props.OnPropertyChange} onChange={props.onPropertyChange} />
/>
</div> </div>
); );
}; }

View file

@ -1,7 +1,7 @@
import React, { RefObject, Dispatch, SetStateAction, useEffect } from 'react'; import React, { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
import { IPoint } from '../../Interfaces/IPoint'; import { IPoint } from '../../Interfaces/IPoint';
export function useMouseEvents( export function UseMouseEvents(
isContextMenuOpen: boolean, isContextMenuOpen: boolean,
elementRef: RefObject<HTMLDivElement>, elementRef: RefObject<HTMLDivElement>,
setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>, setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>,
@ -9,44 +9,48 @@ export function useMouseEvents(
setContextMenuPosition: Dispatch<SetStateAction<IPoint>> setContextMenuPosition: Dispatch<SetStateAction<IPoint>>
): void { ): void {
useEffect(() => { useEffect(() => {
const onContextMenu = (event: MouseEvent): void => handleRightClick( function OnContextMenu(event: MouseEvent): void {
event, return HandleRightClick(
setIsContextMenuOpen, event,
setOnClickContainerId, setIsContextMenuOpen,
setContextMenuPosition setOnClickContainerId,
); setContextMenuPosition
);
}
const onLeftClick = (): void => handleLeftClick( function OnLeftClick(): void {
isContextMenuOpen, return HandleLeftClick(
setIsContextMenuOpen, isContextMenuOpen,
setOnClickContainerId setIsContextMenuOpen,
); setOnClickContainerId
);
}
elementRef.current?.addEventListener( elementRef.current?.addEventListener(
'contextmenu', 'contextmenu',
onContextMenu OnContextMenu
); );
window.addEventListener( window.addEventListener(
'click', 'click',
onLeftClick OnLeftClick
); );
return () => { return () => {
elementRef.current?.removeEventListener( elementRef.current?.removeEventListener(
'contextmenu', 'contextmenu',
onContextMenu OnContextMenu
); );
window.removeEventListener( window.removeEventListener(
'click', 'click',
onLeftClick OnLeftClick
); );
}; };
}); });
} }
export function handleRightClick( export function HandleRightClick(
event: MouseEvent, event: MouseEvent,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>, setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>, setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
@ -66,7 +70,7 @@ export function handleRightClick(
setContextMenuPosition(contextMenuPosition); setContextMenuPosition(contextMenuPosition);
} }
export function handleLeftClick( export function HandleLeftClick(
isContextMenuOpen: boolean, isContextMenuOpen: boolean,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>, setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>> setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
@ -78,4 +82,3 @@ export function handleLeftClick(
setIsContextMenuOpen(false); setIsContextMenuOpen(false);
setOnClickContainerId(''); setOnClickContainerId('');
} }

View file

@ -6,14 +6,12 @@ interface IFloatingButtonProps {
className: string className: string
} }
const toggleState = ( function ToggleState(isHidden: boolean,
isHidden: boolean, setHidden: React.Dispatch<React.SetStateAction<boolean>>): void {
setHidden: React.Dispatch<React.SetStateAction<boolean>>
): void => {
setHidden(!isHidden); setHidden(!isHidden);
}; }
export const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingButtonProps) => { export function FloatingButton(props: IFloatingButtonProps): JSX.Element {
const [isHidden, setHidden] = React.useState(true); const [isHidden, setHidden] = React.useState(true);
const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100'; const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100';
const icon = isHidden const icon = isHidden
@ -23,14 +21,14 @@ export const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingB
return ( return (
<div className={`transition-all ${props.className}`}> <div className={`transition-all ${props.className}`}>
<div className={`transition-all flex flex-col gap-2 items-center ${buttonListClasses}`}> <div className={`transition-all flex flex-col gap-2 items-center ${buttonListClasses}`}>
{ props.children } {props.children}
</div> </div>
<button type="button" <button type="button"
className={'transition-all w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'} className={'transition-all w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Open menu' title='Open menu'
onClick={() => toggleState(isHidden, setHidden)} onClick={() => ToggleState(isHidden, setHidden)}
> >
{ icon } {icon}
</button> </button>
</div>); </div>);
}; }

View file

@ -9,12 +9,12 @@ interface IHistoryProps {
jumpTo: (move: number) => void jumpTo: (move: number) => void
} }
export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => { export function History(props: IHistoryProps): JSX.Element {
const isOpenClasses = props.isOpen ? 'right-0' : '-right-64'; const isOpenClasses = props.isOpen ? 'right-0' : '-right-64';
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
const reversedIndex = (props.history.length - 1) - index; const reversedIndex = (props.history.length - 1) - index;
const step = props.history[reversedIndex]; const step = props.history[reversedIndex];
const desc = step.LastAction; const desc = step.lastAction;
const selectedClass = reversedIndex === props.historyCurrentStep const selectedClass = reversedIndex === props.historyCurrentStep
? 'bg-blue-500 hover:bg-blue-600' ? 'bg-blue-500 hover:bg-blue-600'
@ -25,21 +25,19 @@ export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
key={reversedIndex} key={reversedIndex}
style={style} style={style}
onClick={() => props.jumpTo(reversedIndex)} onClick={() => props.jumpTo(reversedIndex)}
title={step.LastAction} title={step.lastAction}
className={ className={`w-full elements-sidebar-row whitespace-pre overflow-hidden
`w-full elements-sidebar-row whitespace-pre overflow-hidden text-left text-sm font-medium transition-all ${selectedClass}`}
text-left text-sm font-medium transition-all ${selectedClass}`
}
> >
{desc} {desc}
</button> </button>
); );
}; }
return ( return (
<div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}> <div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className='bg-slate-600 font-bold sidebar-title'> <div className='bg-slate-600 font-bold sidebar-title'>
Timeline Timeline
</div> </div>
<List <List
className='List overflow-x-hidden' className='List overflow-x-hidden'
@ -48,8 +46,8 @@ export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
height={window.innerHeight} height={window.innerHeight}
width={256} width={256}
> >
{ Row } {Row}
</List> </List>
</div> </div>
); );
}; }

View file

@ -23,14 +23,14 @@ const className = `
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`; disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) => { export function InputGroup(props: IInputGroupProps): JSX.Element {
return <> return <>
<label <label
key={props.labelKey} key={props.labelKey}
className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`} className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`}
htmlFor={props.inputKey} htmlFor={props.inputKey}
> >
{ props.labelText } {props.labelText}
</label> </label>
<input <input
@ -44,7 +44,6 @@ export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) =>
defaultChecked={props.defaultChecked} defaultChecked={props.defaultChecked}
onChange={props.onChange} onChange={props.onChange}
min={props.min} min={props.min}
disabled={props.isDisabled} disabled={props.isDisabled} />
/>
</>; </>;
}; }

View file

@ -6,14 +6,14 @@ interface IMainMenuProps {
} }
enum WindowState { enum WindowState {
MAIN, Main,
LOAD, Load,
} }
export const MainMenu: React.FC<IMainMenuProps> = (props) => { export function MainMenu(props: IMainMenuProps): JSX.Element {
const [windowState, setWindowState] = React.useState(WindowState.MAIN); const [windowState, setWindowState] = React.useState(WindowState.Main);
switch (windowState) { switch (windowState) {
case WindowState.LOAD: case WindowState.Load:
return ( return (
<div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'> <div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<form className="flex items-center space-x-6"> <form className="flex items-center space-x-6">
@ -37,7 +37,7 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
</label> </label>
</form> </form>
<button type="button" <button type="button"
onClick={() => setWindowState(WindowState.MAIN)} onClick={() => setWindowState(WindowState.Main)}
className='normal-btn block className='normal-btn block
mt-8 ' mt-8 '
> >
@ -50,7 +50,7 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
return ( return (
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'> <div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<button type="button" className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button> <button type="button" className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button>
<button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button> <button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.Load)}>Load a configuration file</button>
</div> </div>
); );
} }

View file

@ -8,7 +8,7 @@ interface IMenuProps {
children: React.ReactNode[] | React.ReactNode children: React.ReactNode[] | React.ReactNode
} }
export const Menu: React.FC<IMenuProps> = (props) => { export function Menu(props: IMenuProps): JSX.Element {
const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0'; const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0';
return ( return (
<div <div
@ -17,7 +17,7 @@ export const Menu: React.FC<IMenuProps> = (props) => {
left: props.x, left: props.x,
top: props.y top: props.y
}}> }}>
{ props.children } {props.children}
</div> </div>
); );
}; }

View file

@ -6,11 +6,11 @@ interface IMenuItemProps {
onClick: () => void onClick: () => void
} }
export const MenuItem: React.FC<IMenuItemProps> = (props) => { export function MenuItem(props: IMenuItemProps): JSX.Element {
return ( return (
<button type="button" <button type="button"
className={props.className} className={props.className}
onClick={() => props.onClick()}>{props.text} onClick={() => props.onClick()}>{props.text}
</button> </button>
); );
}; }

View file

@ -11,7 +11,7 @@ interface IRadioGroupButtonsProps {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps> = (props) => { export function RadioGroupButtons(props: IRadioGroupButtonsProps): JSX.Element {
let inputGroups; let inputGroups;
if (props.value !== undefined) { if (props.value !== undefined) {
// dynamic // dynamic
@ -24,8 +24,7 @@ export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps>
className={`peer m-2 ${props.inputClassName}`} className={`peer m-2 ${props.inputClassName}`}
value={inputGroup.value} value={inputGroup.value}
checked={props.value === inputGroup.value} checked={props.value === inputGroup.value}
onChange={props.onChange} onChange={props.onChange} />
/>
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'> <label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
{inputGroup.text} {inputGroup.text}
</label> </label>
@ -42,8 +41,7 @@ export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps>
name={props.name} name={props.name}
className={`peer m-2 ${props.inputClassName}`} className={`peer m-2 ${props.inputClassName}`}
value={inputGroup.value} value={inputGroup.value}
defaultChecked={props.defaultValue === inputGroup.value} defaultChecked={props.defaultValue === inputGroup.value} />
/>
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'> <label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
{inputGroup.text} {inputGroup.text}
</label> </label>
@ -59,8 +57,8 @@ export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps>
<div id='XPositionReference' <div id='XPositionReference'
className='flex flex-col' className='flex flex-col'
> >
{ inputGroups } {inputGroups}
</div> </div>
</> </>
); );
}; }

View file

@ -2,11 +2,11 @@ import * as React from 'react';
import { Interweave, Node } from 'interweave'; import { Interweave, Node } from 'interweave';
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default'; import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
import { getDepth } from '../../../utils/itertools'; import { GetDepth } from '../../../utils/itertools';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
import IContainerProperties from '../../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../../Interfaces/IContainerProperties';
import { transformX } from '../../../utils/svg'; import { TransformX } from '../../../utils/svg';
import { camelize } from '../../../utils/stringtools'; import { Camelize } from '../../../utils/stringtools';
interface IContainerProps { interface IContainerProps {
model: IContainerModel model: IContainerModel
@ -16,7 +16,7 @@ interface IContainerProps {
* Render the container * Render the container
* @returns Render the container * @returns Render the container
*/ */
export const Container: React.FC<IContainerProps> = (props: IContainerProps) => { export function Container(props: IContainerProps): JSX.Element {
const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />); const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
const width: number = props.model.properties.width; const width: number = props.model.properties.width;
@ -52,7 +52,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
> >
</rect>); </rect>);
// Dimension props // Dimension props
const depth = getDepth(props.model); const depth = GetDepth(props.model);
const dimensionMargin = DIMENSION_MARGIN * depth; const dimensionMargin = DIMENSION_MARGIN * depth;
const id = `dim-${props.model.properties.id}`; const id = `dim-${props.model.properties.id}`;
const xStart: number = 0; const xStart: number = 0;
@ -64,11 +64,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
let dimensionChildren: JSX.Element | null = null; let dimensionChildren: JSX.Element | null = null;
if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) { if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) {
const { const {
childrenId, childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren
xChildrenStart,
xChildrenEnd,
yChildren,
textChildren
} = GetChildrenDimensionProps(props, dimensionMargin); } = GetChildrenDimensionProps(props, dimensionMargin);
dimensionChildren = <Dimension dimensionChildren = <Dimension
id={childrenId} id={childrenId}
@ -77,8 +73,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
yStart={yChildren} yStart={yChildren}
yEnd={yChildren} yEnd={yChildren}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
text={textChildren} text={textChildren} />;
/>;
} }
return ( return (
@ -95,10 +90,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
yStart={yDim} yStart={yDim}
yEnd={yDim} yEnd={yDim}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
text={text} text={text} />
/> : null}
: null
}
{dimensionChildren} {dimensionChildren}
{svg} {svg}
{SHOW_TEXT {SHOW_TEXT
@ -112,23 +105,23 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
{containersElements} {containersElements}
</g> </g>
); );
}; }
function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): { childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } { function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): { childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } {
const childrenId = `dim-children-${props.model.properties.id}`; const childrenId = `dim-children-${props.model.properties.id}`;
const lastChild = props.model.children[props.model.children.length - 1]; const lastChild = props.model.children[props.model.children.length - 1];
let xChildrenStart = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference); let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.xPositionReference);
let xChildrenEnd = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference); let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.xPositionReference);
// Find the min and max // Find the min and max
for (let i = props.model.children.length - 2; i >= 0; i--) { for (let i = props.model.children.length - 2; i >= 0; i--) {
const child = props.model.children[i]; const child = props.model.children[i];
const left = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference); const left = TransformX(child.properties.x, child.properties.width, child.properties.xPositionReference);
if (left < xChildrenStart) { if (left < xChildrenStart) {
xChildrenStart = left; xChildrenStart = left;
} }
const right = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference); const right = TransformX(child.properties.x, child.properties.width, child.properties.xPositionReference);
if (right > xChildrenEnd) { if (right > xChildrenEnd) {
xChildrenEnd = right; xChildrenEnd = right;
} }
@ -145,11 +138,11 @@ function CreateReactCustomSVG(customSVG: string, props: IContainerProperties): R
disableLineBreaks={true} disableLineBreaks={true}
content={customSVG} content={customSVG}
allowElements={true} allowElements={true}
transform={(node, children) => transform(node, children, props)} transform={(node, children) => Transform(node, children, props)}
/>; />;
} }
function transform(node: HTMLElement, children: Node[], props: IContainerProperties): React.ReactNode { function Transform(node: HTMLElement, children: Node[], props: IContainerProperties): React.ReactNode {
const supportedTags = ['line', 'path', 'rect']; const supportedTags = ['line', 'path', 'rect'];
if (supportedTags.includes(node.tagName.toLowerCase())) { if (supportedTags.includes(node.tagName.toLowerCase())) {
const attributes: { [att: string]: string | object | null } = {}; const attributes: { [att: string]: string | object | null } = {};
@ -170,7 +163,7 @@ function transform(node: HTMLElement, children: Node[], props: IContainerPropert
const prop = Object.entries(props.userData).find(([key]) => `{${key}}` === userDataKey); const prop = Object.entries(props.userData).find(([key]) => `{${key}}` === userDataKey);
if (prop !== undefined) { if (prop !== undefined) {
attributes[camelize(attName)] = prop[1]; attributes[Camelize(attName)] = prop[1];
return; return;
} }
} }
@ -179,16 +172,16 @@ function transform(node: HTMLElement, children: Node[], props: IContainerPropert
// support for object // support for object
const stringObject = attributeValue.slice(1, -1); const stringObject = attributeValue.slice(1, -1);
const object: JSON = JSON.parse(stringObject); const object: JSON = JSON.parse(stringObject);
attributes[camelize(attName)] = object; attributes[Camelize(attName)] = object;
return; return;
} }
const prop = Object.entries(props).find(([key]) => `{${key}}` === attributeValue); const prop = Object.entries(props).find(([key]) => `{${key}}` === attributeValue);
if (prop !== undefined) { if (prop !== undefined) {
attributes[camelize(attName)] = prop[1]; attributes[Camelize(attName)] = prop[1];
return; return;
} }
attributes[camelize(attName)] = attributeValue; attributes[Camelize(attName)] = attributeValue;
}); });
return React.createElement(node.tagName.toLowerCase(), attributes, children); return React.createElement(node.tagName.toLowerCase(), attributes, children);
} }

View file

@ -1,15 +1,15 @@
import * as React from 'react'; import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/IContainerModel'; import { ContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default'; import { DIMENSION_MARGIN } from '../../../utils/default';
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools'; import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
import { transformX } from '../../../utils/svg'; import { TransformX } from '../../../utils/svg';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
interface IDimensionLayerProps { interface IDimensionLayerProps {
roots: ContainerModel | ContainerModel[] | null roots: ContainerModel | ContainerModel[] | null
} }
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => { function GetDimensionsNodes(root: ContainerModel): React.ReactNode[] {
const it = MakeBFSIterator(root); const it = MakeBFSIterator(root);
const dimensions: React.ReactNode[] = []; const dimensions: React.ReactNode[] = [];
let currentDepth = 0; let currentDepth = 0;
@ -25,8 +25,8 @@ const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
max = -Infinity; max = -Infinity;
} }
const absoluteX = getAbsolutePosition(container)[0]; const absoluteX = GetAbsolutePosition(container)[0];
const x = transformX(absoluteX, container.properties.width, container.properties.XPositionReference); const x = TransformX(absoluteX, container.properties.width, container.properties.xPositionReference);
lastY = container.properties.y + container.properties.height; lastY = container.properties.y + container.properties.height;
if (x < min) { if (x < min) {
min = x; min = x;
@ -40,28 +40,28 @@ const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
AddNewDimension(currentDepth, min, max, lastY, dimensions); AddNewDimension(currentDepth, min, max, lastY, dimensions);
return dimensions; return dimensions;
}; }
/** /**
* A layer containing all dimension * A layer containing all dimension
* @param props * @param props
* @returns * @returns
*/ */
export const DepthDimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => { export function DepthDimensionLayer(props: IDimensionLayerProps): JSX.Element {
let dimensions: React.ReactNode[] = []; let dimensions: React.ReactNode[] = [];
if (Array.isArray(props.roots)) { if (Array.isArray(props.roots)) {
props.roots.forEach(child => { props.roots.forEach(child => {
dimensions.concat(getDimensionsNodes(child)); dimensions.concat(GetDimensionsNodes(child));
}); });
} else if (props.roots !== null) { } else if (props.roots !== null) {
dimensions = getDimensionsNodes(props.roots); dimensions = GetDimensionsNodes(props.roots);
} }
return ( return (
<g> <g>
{ dimensions } {dimensions}
</g> </g>
); );
}; }
function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, dimensions: React.ReactNode[]): void { function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, dimensions: React.ReactNode[]): void {
const id = `dim-depth-${currentDepth}`; const id = `dim-depth-${currentDepth}`;

View file

@ -20,9 +20,11 @@ interface IDimensionProps {
* @param vx Transform vector * @param vx Transform vector
* @returns Returns a new coordinate from the origin coordinate * @returns Returns a new coordinate from the origin coordinate
*/ */
const applyParametric = (x0: number, t: number, vx: number): number => x0 + t * vx; function ApplyParametric(x0: number, t: number, vx: number): number {
return x0 + t * vx;
}
export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) => { export function Dimension(props: IDimensionProps): JSX.Element {
const style: React.CSSProperties = { const style: React.CSSProperties = {
stroke: 'black' stroke: 'black'
}; };
@ -39,15 +41,15 @@ export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) =>
const [perpVecX, perpVecY] = [unitY, -unitX]; const [perpVecX, perpVecY] = [unitY, -unitX];
// Use the parametric function to get the coordinates (x = x0 + t * v.x) // Use the parametric function to get the coordinates (x = x0 + t * v.x)
const startTopX = applyParametric(props.xStart, NOTCHES_LENGTH, perpVecX); const startTopX = ApplyParametric(props.xStart, NOTCHES_LENGTH, perpVecX);
const startTopY = applyParametric(props.yStart, NOTCHES_LENGTH, perpVecY); const startTopY = ApplyParametric(props.yStart, NOTCHES_LENGTH, perpVecY);
const startBottomX = applyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX); const startBottomX = ApplyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX);
const startBottomY = applyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY); const startBottomY = ApplyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY);
const endTopX = applyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX); const endTopX = ApplyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX);
const endTopY = applyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY); const endTopY = ApplyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY);
const endBottomX = applyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX); const endBottomX = ApplyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX);
const endBottomY = applyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY); const endBottomY = ApplyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY);
return ( return (
<g key={props.id}> <g key={props.id}>
@ -57,24 +59,21 @@ export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) =>
x2={startBottomX} x2={startBottomX}
y2={startBottomY} y2={startBottomY}
strokeWidth={props.strokeWidth} strokeWidth={props.strokeWidth}
style={style} style={style} />
/>
<line <line
x1={props.xStart} x1={props.xStart}
y1={props.yStart} y1={props.yStart}
x2={props.xEnd} x2={props.xEnd}
y2={props.yEnd} y2={props.yEnd}
strokeWidth={props.strokeWidth} strokeWidth={props.strokeWidth}
style={style} style={style} />
/>
<line <line
x1={endTopX} x1={endTopX}
y1={endTopY} y1={endTopY}
x2={endBottomX} x2={endBottomX}
y2={endBottomY} y2={endBottomY}
strokeWidth={props.strokeWidth} strokeWidth={props.strokeWidth}
style={style} style={style} />
/>
<text <text
x={(props.xStart + props.xEnd) / 2} x={(props.xStart + props.xEnd) / 2}
y={props.yStart} y={props.yStart}
@ -83,4 +82,4 @@ export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) =>
</text> </text>
</g> </g>
); );
}; }

View file

@ -1,20 +1,20 @@
import * as React from 'react'; import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/IContainerModel'; import { ContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default'; import { DIMENSION_MARGIN } from '../../../utils/default';
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools'; import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
interface IDimensionLayerProps { interface IDimensionLayerProps {
roots: ContainerModel | ContainerModel[] | null roots: ContainerModel | ContainerModel[] | null
} }
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => { function GetDimensionsNodes(root: ContainerModel): React.ReactNode[] {
const it = MakeBFSIterator(root); const it = MakeBFSIterator(root);
const dimensions: React.ReactNode[] = []; const dimensions: React.ReactNode[] = [];
for (const { container, depth } of it) { for (const { container, depth } of it) {
const width = container.properties.width; const width = container.properties.width;
const id = `dim-${container.properties.id}`; const id = `dim-${container.properties.id}`;
const xStart = getAbsolutePosition(container)[0]; const xStart = GetAbsolutePosition(container)[0];
const xEnd = xStart + width; const xEnd = xStart + width;
const y = (container.properties.y + container.properties.height) + (DIMENSION_MARGIN * (depth + 1)); const y = (container.properties.y + container.properties.height) + (DIMENSION_MARGIN * (depth + 1));
const strokeWidth = 1; const strokeWidth = 1;
@ -28,30 +28,29 @@ const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
xEnd={xEnd} xEnd={xEnd}
yEnd={y} yEnd={y}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
text={text} text={text} />
/>
); );
} }
return dimensions; return dimensions;
}; }
/** /**
* A layer containing all dimension * A layer containing all dimension
* @param props * @param props
* @returns * @returns
*/ */
export const DimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => { export function DimensionLayer(props: IDimensionLayerProps): JSX.Element {
let dimensions: React.ReactNode[] = []; let dimensions: React.ReactNode[] = [];
if (Array.isArray(props.roots)) { if (Array.isArray(props.roots)) {
props.roots.forEach(child => { props.roots.forEach(child => {
dimensions.concat(getDimensionsNodes(child)); dimensions.concat(GetDimensionsNodes(child));
}); });
} else if (props.roots !== null) { } else if (props.roots !== null) {
dimensions = getDimensionsNodes(props.roots); dimensions = GetDimensionsNodes(props.roots);
} }
return ( return (
<g> <g>
{ dimensions } {dimensions}
</g> </g>
); );
}; }

View file

@ -2,14 +2,14 @@ import './Selector.scss';
import * as React from 'react'; import * as React from 'react';
import { IContainerModel } from '../../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../../Interfaces/IContainerModel';
import { SHOW_SELECTOR_TEXT } from '../../../../utils/default'; import { SHOW_SELECTOR_TEXT } from '../../../../utils/default';
import { getAbsolutePosition } from '../../../../utils/itertools'; import { GetAbsolutePosition } from '../../../../utils/itertools';
import { RemoveMargin } from '../../../../utils/svg'; import { RemoveMargin } from '../../../../utils/svg';
interface ISelectorProps { interface ISelectorProps {
selected?: IContainerModel selected?: IContainerModel
} }
export const Selector: React.FC<ISelectorProps> = (props) => { export function Selector(props: ISelectorProps): JSX.Element {
if (props.selected === undefined || props.selected === null) { if (props.selected === undefined || props.selected === null) {
return ( return (
<rect visibility={'hidden'}> <rect visibility={'hidden'}>
@ -17,7 +17,7 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
); );
} }
let [x, y] = getAbsolutePosition(props.selected); let [x, y] = GetAbsolutePosition(props.selected);
let [width, height] = [ let [width, height] = [
props.selected.properties.width, props.selected.properties.width,
props.selected.properties.height props.selected.properties.height
@ -34,7 +34,7 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
const yText = y + height / 2; const yText = y + height / 2;
const style: React.CSSProperties = { const style: React.CSSProperties = {
stroke: '#3B82F6', // tw blue-500 stroke: '#3B82F6',
strokeWidth: 4, strokeWidth: 4,
fillOpacity: 0, fillOpacity: 0,
transitionProperty: 'all', transitionProperty: 'all',
@ -63,4 +63,4 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
: null} : null}
</> </>
); );
}; }

View file

@ -7,7 +7,7 @@ interface ISymbolProps {
model: ISymbolModel model: ISymbolModel
} }
export const Symbol: React.FC<ISymbolProps> = (props) => { export function Symbol(props: ISymbolProps): JSX.Element {
const href = props.model.config.Image.Base64Image ?? props.model.config.Image.Url; const href = props.model.config.Image.Base64Image ?? props.model.config.Image.Url;
const hasSVG = props.model.config.Image.Svg !== undefined && const hasSVG = props.model.config.Image.Svg !== undefined &&
props.model.config.Image.Svg !== null; props.model.config.Image.Svg !== null;
@ -21,8 +21,7 @@ export const Symbol: React.FC<ISymbolProps> = (props) => {
noWrap={true} noWrap={true}
disableLineBreaks={true} disableLineBreaks={true}
content={props.model.config.Image.Svg} content={props.model.config.Image.Svg}
allowElements={true} allowElements={true} />
/>
</g> </g>
); );
} }
@ -33,7 +32,6 @@ export const Symbol: React.FC<ISymbolProps> = (props) => {
x={props.model.x} x={props.model.x}
y={-DIMENSION_MARGIN} y={-DIMENSION_MARGIN}
height={props.model.height} height={props.model.height}
width={props.model.width} width={props.model.width} />
/>
); );
}; }

View file

@ -6,7 +6,7 @@ interface ISymbolLayerProps {
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
} }
export const SymbolLayer: React.FC<ISymbolLayerProps> = (props) => { export function SymbolLayer(props: ISymbolLayerProps): JSX.Element {
const symbols: JSX.Element[] = []; const symbols: JSX.Element[] = [];
props.symbols.forEach((symbol) => { props.symbols.forEach((symbol) => {
symbols.push( symbols.push(
@ -15,9 +15,7 @@ export const SymbolLayer: React.FC<ISymbolLayerProps> = (props) => {
}); });
return ( return (
<g> <g>
{ {symbols}
symbols
}
</g> </g>
); );
}; }

View file

@ -25,7 +25,7 @@ interface Viewer {
export const ID = 'svg'; export const ID = 'svg';
function resizeViewBox( function ResizeViewBox(
setViewer: React.Dispatch<React.SetStateAction<Viewer>> setViewer: React.Dispatch<React.SetStateAction<Viewer>>
): void { ): void {
setViewer({ setViewer({
@ -34,26 +34,28 @@ function resizeViewBox(
}); });
} }
function useSVGAutoResizer( function UseSVGAutoResizer(
setViewer: React.Dispatch<React.SetStateAction<Viewer>> setViewer: React.Dispatch<React.SetStateAction<Viewer>>
): void { ): void {
React.useEffect(() => { React.useEffect(() => {
const onResize = (): void => resizeViewBox(setViewer); function OnResize(): void {
window.addEventListener('resize', onResize); return ResizeViewBox(setViewer);
}
window.addEventListener('resize', OnResize);
return () => { return () => {
window.removeEventListener('resize', onResize); window.removeEventListener('resize', OnResize);
}; };
}); });
} }
export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => { export function SVG(props: ISVGProps): JSX.Element {
const [viewer, setViewer] = React.useState<Viewer>({ const [viewer, setViewer] = React.useState<Viewer>({
viewerWidth: window.innerWidth - BAR_WIDTH, viewerWidth: window.innerWidth - BAR_WIDTH,
viewerHeight: window.innerHeight viewerHeight: window.innerHeight
}); });
useSVGAutoResizer(setViewer); UseSVGAutoResizer(setViewer);
const xmlns = '<http://www.w3.org/2000/svg>'; const xmlns = '<http://www.w3.org/2000/svg>';
const properties = { const properties = {
@ -64,9 +66,9 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
let children: React.ReactNode | React.ReactNode[] = []; let children: React.ReactNode | React.ReactNode[] = [];
if (Array.isArray(props.children)) { if (Array.isArray(props.children)) {
children = props.children.map(child => <Container key={`container-${child.properties.id}`} model={child}/>); children = props.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
} else if (props.children !== null) { } else if (props.children !== null) {
children = <Container key={`container-${props.children.properties.id}`} model={props.children}/>; children = <Container key={`container-${props.children.properties.id}`} model={props.children} />;
} }
return ( return (
@ -84,16 +86,14 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
}} }}
> >
<svg {...properties}> <svg {...properties}>
{ children } {children}
{ {SHOW_DIMENSIONS_PER_DEPTH
SHOW_DIMENSIONS_PER_DEPTH ? <DepthDimensionLayer roots={props.children} />
? <DepthDimensionLayer roots={props.children}/> : null}
: null
}
<SymbolLayer symbols={props.symbols} /> <SymbolLayer symbols={props.symbols} />
<Selector selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */} <Selector selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
</svg> </svg>
</UncontrolledReactSVGPanZoom> </UncontrolledReactSVGPanZoom>
</div> </div>
); );
}; }

View file

@ -19,7 +19,7 @@ const className = `
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`; disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
export const Select: React.FC<ISelectProps> = (props) => { export function Select(props: ISelectProps): JSX.Element {
const options = [( const options = [(
<option key='symbol-none' value=''>None</option> <option key='symbol-none' value=''>None</option>
)]; )];
@ -40,7 +40,7 @@ export const Select: React.FC<ISelectProps> = (props) => {
className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`} className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`}
htmlFor={props.inputKey} htmlFor={props.inputKey}
> >
{ props.labelText } {props.labelText}
</label> </label>
<select <select
id={props.inputKey} id={props.inputKey}
@ -48,8 +48,8 @@ export const Select: React.FC<ISelectProps> = (props) => {
onChange={props.onChange} onChange={props.onChange}
className={className} className={className}
> >
{ options } {options}
</select> </select>
</> </>
); );
}; }

View file

@ -31,15 +31,17 @@ describe.concurrent('Sidebar', () => {
}); });
it('With stuff', () => { it('With stuff', () => {
const Type = 'stuff'; const type = 'stuff';
const handleButtonClick = vi.fn(); const handleButtonClick = vi.fn();
render(<Sidebar render(<Sidebar
componentOptions={[ componentOptions={[
{ {
Type, /* eslint-disable @typescript-eslint/naming-convention */
Type: type,
Width: 30, Width: 30,
Height: 30, Height: 30,
Style: {} Style: {}
/* eslint-enable */
} }
]} ]}
isOpen={true} isOpen={true}

View file

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { truncateString } from '../../utils/stringtools'; import { TruncateString } from '../../utils/stringtools';
interface ISidebarProps { interface ISidebarProps {
componentOptions: IAvailableContainer[] componentOptions: IAvailableContainer[]
@ -8,11 +8,11 @@ interface ISidebarProps {
buttonOnClick: (type: string) => void buttonOnClick: (type: string) => void
} }
function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void { function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id); event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
} }
export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => { export function Sidebar(props: ISidebarProps): JSX.Element {
const listElements = props.componentOptions.map(componentOption => const listElements = props.componentOptions.map(componentOption =>
<button type="button" <button type="button"
className='justify-center transition-all sidebar-component' className='justify-center transition-all sidebar-component'
@ -21,9 +21,9 @@ export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => {
title={componentOption.Type} title={componentOption.Type}
onClick={() => props.buttonOnClick(componentOption.Type)} onClick={() => props.buttonOnClick(componentOption.Type)}
draggable={true} draggable={true}
onDragStart={(event) => handleDragStart(event)} onDragStart={(event) => HandleDragStart(event)}
> >
{truncateString(componentOption.Type, 5)} {TruncateString(componentOption.Type, 5)}
</button> </button>
); );

View file

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { restoreX, transformX } from '../../utils/svg'; import { RestoreX, TransformX } from '../../utils/svg';
import { InputGroup } from '../InputGroup/InputGroup'; import { InputGroup } from '../InputGroup/InputGroup';
interface ISymbolFormProps { interface ISymbolFormProps {
@ -8,7 +8,8 @@ interface ISymbolFormProps {
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
onChange: (key: string, value: string | number | boolean) => void onChange: (key: string, value: string | number | boolean) => void
} }
const SymbolForm: React.FunctionComponent<ISymbolFormProps> = (props) => {
export function SymbolForm(props: ISymbolFormProps): JSX.Element {
return ( return (
<div className='grid grid-cols-2 gap-y-4'> <div className='grid grid-cols-2 gap-y-4'>
<InputGroup <InputGroup
@ -18,17 +19,15 @@ const SymbolForm: React.FunctionComponent<ISymbolFormProps> = (props) => {
inputClassName='' inputClassName=''
type='string' type='string'
value={props.symbol.id.toString()} value={props.symbol.id.toString()}
isDisabled={true} isDisabled={true} />
/>
<InputGroup <InputGroup
labelText='x' labelText='x'
inputKey='x' inputKey='x'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='number' type='number'
value={transformX(props.symbol.x, props.symbol.width, props.symbol.config.XPositionReference).toString()} value={TransformX(props.symbol.x, props.symbol.width, props.symbol.config.XPositionReference).toString()}
onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.symbol.width, props.symbol.config.XPositionReference))} onChange={(event) => props.onChange('x', RestoreX(Number(event.target.value), props.symbol.width, props.symbol.config.XPositionReference))} />
/>
<InputGroup <InputGroup
labelText='Height' labelText='Height'
inputKey='height' inputKey='height'
@ -37,8 +36,7 @@ const SymbolForm: React.FunctionComponent<ISymbolFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={props.symbol.height.toString()} value={props.symbol.height.toString()}
onChange={(event) => props.onChange('height', Number(event.target.value))} onChange={(event) => props.onChange('height', Number(event.target.value))} />
/>
<InputGroup <InputGroup
labelText='Width' labelText='Width'
inputKey='width' inputKey='width'
@ -47,10 +45,7 @@ const SymbolForm: React.FunctionComponent<ISymbolFormProps> = (props) => {
type='number' type='number'
min={0} min={0}
value={props.symbol.width.toString()} value={props.symbol.width.toString()}
onChange={(event) => props.onChange('width', Number(event.target.value))} onChange={(event) => props.onChange('width', Number(event.target.value))} />
/>
</div> </div>
); );
}; }
export default SymbolForm;

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import SymbolForm from './SymbolForm'; import { SymbolForm } from './SymbolForm';
interface ISymbolPropertiesProps { interface ISymbolPropertiesProps {
symbol?: ISymbolModel symbol?: ISymbolModel
@ -8,7 +8,7 @@ interface ISymbolPropertiesProps {
onChange: (key: string, value: string | number | boolean) => void onChange: (key: string, value: string | number | boolean) => void
} }
export const SymbolProperties: React.FC<ISymbolPropertiesProps> = (props: ISymbolPropertiesProps) => { export function SymbolProperties(props: ISymbolPropertiesProps): JSX.Element {
if (props.symbol === undefined) { if (props.symbol === undefined) {
return <div></div>; return <div></div>;
} }
@ -18,8 +18,7 @@ export const SymbolProperties: React.FC<ISymbolPropertiesProps> = (props: ISymbo
<SymbolForm <SymbolForm
symbol={props.symbol} symbol={props.symbol}
symbols={props.symbols} symbols={props.symbols}
onChange={props.onChange} onChange={props.onChange} />
/>
</div> </div>
); );
}; }

View file

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol'; import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { truncateString } from '../../utils/stringtools'; import { TruncateString } from '../../utils/stringtools';
interface ISymbolsProps { interface ISymbolsProps {
componentOptions: IAvailableSymbol[] componentOptions: IAvailableSymbol[]
@ -8,11 +8,11 @@ interface ISymbolsProps {
buttonOnClick: (type: string) => void buttonOnClick: (type: string) => void
} }
function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void { function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id); event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
} }
export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => { export function Symbols(props: ISymbolsProps): JSX.Element {
const listElements = props.componentOptions.map(componentOption => { const listElements = props.componentOptions.map(componentOption => {
if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) { if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) {
const url = componentOption.Image.Base64Image ?? componentOption.Image.Url; const url = componentOption.Image.Base64Image ?? componentOption.Image.Url;
@ -23,7 +23,7 @@ export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
title={componentOption.Name} title={componentOption.Name}
onClick={() => props.buttonOnClick(componentOption.Name)} onClick={() => props.buttonOnClick(componentOption.Name)}
draggable={true} draggable={true}
onDragStart={(event) => handleDragStart(event)} onDragStart={(event) => HandleDragStart(event)}
> >
<div> <div>
<img <img
@ -32,7 +32,7 @@ export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
/> />
</div> </div>
<div> <div>
{truncateString(componentOption.Name, 5)} {TruncateString(componentOption.Name, 5)}
</div> </div>
</button>); </button>);
} }
@ -44,10 +44,10 @@ export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
title={componentOption.Name} title={componentOption.Name}
onClick={() => props.buttonOnClick(componentOption.Name)} onClick={() => props.buttonOnClick(componentOption.Name)}
draggable={true} draggable={true}
onDragStart={(event) => handleDragStart(event)} onDragStart={(event) => HandleDragStart(event)}
> >
{truncateString(componentOption.Name, 5)} {TruncateString(componentOption.Name, 5)}
</button>); </button>);
}); });

View file

@ -1,7 +1,7 @@
import { RefObject, Dispatch, SetStateAction, useEffect } from 'react'; import { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
import { IPoint } from '../../Interfaces/IPoint'; import { IPoint } from '../../Interfaces/IPoint';
export function useMouseEvents( export function UseMouseEvents(
isContextMenuOpen: boolean, isContextMenuOpen: boolean,
elementRef: RefObject<HTMLDivElement>, elementRef: RefObject<HTMLDivElement>,
setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>, setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>,
@ -9,44 +9,48 @@ export function useMouseEvents(
setContextMenuPosition: Dispatch<SetStateAction<IPoint>> setContextMenuPosition: Dispatch<SetStateAction<IPoint>>
): void { ): void {
useEffect(() => { useEffect(() => {
const onContextMenu = (event: MouseEvent): void => handleRightClick( function OnContextMenu(event: MouseEvent): void {
event, return HandleRightClick(
setIsContextMenuOpen, event,
setOnClickSymbolId, setIsContextMenuOpen,
setContextMenuPosition setOnClickSymbolId,
); setContextMenuPosition
);
}
const onLeftClick = (): void => handleLeftClick( function OnLeftClick(): void {
isContextMenuOpen, return HandleLeftClick(
setIsContextMenuOpen, isContextMenuOpen,
setOnClickSymbolId setIsContextMenuOpen,
); setOnClickSymbolId
);
}
elementRef.current?.addEventListener( elementRef.current?.addEventListener(
'contextmenu', 'contextmenu',
onContextMenu OnContextMenu
); );
window.addEventListener( window.addEventListener(
'click', 'click',
onLeftClick OnLeftClick
); );
return () => { return () => {
elementRef.current?.removeEventListener( elementRef.current?.removeEventListener(
'contextmenu', 'contextmenu',
onContextMenu OnContextMenu
); );
window.removeEventListener( window.removeEventListener(
'click', 'click',
onLeftClick OnLeftClick
); );
}; };
}); });
} }
export function handleRightClick( export function HandleRightClick(
event: MouseEvent, event: MouseEvent,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>, setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickSymbolId: React.Dispatch<React.SetStateAction<string>>, setOnClickSymbolId: React.Dispatch<React.SetStateAction<string>>,
@ -66,7 +70,7 @@ export function handleRightClick(
setContextMenuPosition(contextMenuPosition); setContextMenuPosition(contextMenuPosition);
} }
export function handleLeftClick( export function HandleLeftClick(
isContextMenuOpen: boolean, isContextMenuOpen: boolean,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>, setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>> setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>

View file

@ -2,22 +2,22 @@ import * as React from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { Menu } from '../Menu/Menu'; import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem'; import { MenuItem } from '../Menu/MenuItem';
import { handleLeftClick, handleRightClick, useMouseEvents } from './MouseEventHandlers'; import { UseMouseEvents } from './MouseEventHandlers';
import { IPoint } from '../../Interfaces/IPoint'; import { IPoint } from '../../Interfaces/IPoint';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { SymbolProperties } from '../SymbolProperties/SymbolProperties'; import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
interface ISymbolsSidebarProps { interface ISymbolsSidebarProps {
SelectedSymbolId: string selectedSymbolId: string
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
isOpen: boolean isOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
OnPropertyChange: (key: string, value: string | number | boolean) => void onPropertyChange: (key: string, value: string | number | boolean) => void
SelectSymbol: (symbolId: string) => void selectSymbol: (symbolId: string) => void
DeleteSymbol: (containerid: string) => void deleteSymbol: (containerid: string) => void
} }
export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSidebarProps): JSX.Element => { export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
// States // States
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false); const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
const [onClickSymbolId, setOnClickSymbolId] = React.useState<string>(''); const [onClickSymbolId, setOnClickSymbolId] = React.useState<string>('');
@ -29,7 +29,7 @@ export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSi
const elementRef = React.useRef<HTMLDivElement>(null); const elementRef = React.useRef<HTMLDivElement>(null);
// Event listeners // Event listeners
useMouseEvents( UseMouseEvents(
isContextMenuOpen, isContextMenuOpen,
elementRef, elementRef,
setIsContextMenuOpen, setIsContextMenuOpen,
@ -46,35 +46,33 @@ export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSi
} }
const containers = [...props.symbols.values()]; const containers = [...props.symbols.values()];
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
const container = containers[index]; const container = containers[index];
const key = container.id.toString(); const key = container.id.toString();
const text = key; const text = key;
const selectedClass: string = props.SelectedSymbolId !== '' && const selectedClass: string = props.selectedSymbolId !== '' &&
props.SelectedSymbolId === container.id props.selectedSymbolId === container.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300'; : 'bg-slate-300/60 hover:bg-slate-300';
return ( return (
<button type="button" <button type="button"
className={ className={`w-full border-blue-500 elements-sidebar-row whitespace-pre
`w-full border-blue-500 elements-sidebar-row whitespace-pre text-left text-sm font-medium transition-all ${selectedClass}`}
text-left text-sm font-medium transition-all ${selectedClass}`
}
id={key} id={key}
key={key} key={key}
style={style} style={style}
onClick={() => props.SelectSymbol(key)} onClick={() => props.selectSymbol(key)}
> >
{ text } {text}
</button> </button>
); );
}; }
return ( return (
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}> <div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className='bg-slate-100 font-bold sidebar-title'> <div className='bg-slate-100 font-bold sidebar-title'>
Elements Elements
</div> </div>
<div ref={elementRef} className='h-96 text-gray-800'> <div ref={elementRef} className='h-96 text-gray-800'>
<List <List
@ -84,7 +82,7 @@ export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSi
height={384} height={384}
width={256} width={256}
> >
{ Row } {Row}
</List> </List>
</div> </div>
<Menu <Menu
@ -95,14 +93,13 @@ export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSi
> >
<MenuItem className='contextmenu-item' text='Delete' onClick={() => { <MenuItem className='contextmenu-item' text='Delete' onClick={() => {
setIsContextMenuOpen(false); setIsContextMenuOpen(false);
props.DeleteSymbol(onClickSymbolId); props.deleteSymbol(onClickSymbolId);
}} /> } } />
</Menu> </Menu>
<SymbolProperties <SymbolProperties
symbol={props.symbols.get(props.SelectedSymbolId)} symbol={props.symbols.get(props.selectedSymbolId)}
symbols={props.symbols} symbols={props.symbols}
onChange={props.OnPropertyChange} onChange={props.onPropertyChange} />
/>
</div> </div>
); );
}; }

View file

@ -1,26 +1,26 @@
import React, { FC } from 'react'; import React from 'react';
import './ToggleButton.scss'; import './ToggleButton.scss';
interface IToggleButtonProps { interface IToggleButtonProps {
id: string id: string
text: string text: string
type?: TOGGLE_TYPE type?: ToggleType
title: string title: string
checked: boolean checked: boolean
onChange: React.ChangeEventHandler<HTMLInputElement> onChange: React.ChangeEventHandler<HTMLInputElement>
} }
export enum TOGGLE_TYPE { export enum ToggleType {
MATERIAL, Material,
IOS IOS
} }
export const ToggleButton: FC<IToggleButtonProps> = (props) => { export function ToggleButton(props: IToggleButtonProps): JSX.Element {
const id = `toggle-${props.id}`; const id = `toggle-${props.id}`;
const type = props.type ?? TOGGLE_TYPE.MATERIAL; const type = props.type ?? ToggleType.Material;
let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner'; let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner';
let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition'; let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition';
if (type === TOGGLE_TYPE.IOS) { if (type === ToggleType.IOS) {
classLine = 'line block bg-gray-600 w-14 h-8 rounded-full'; classLine = 'line block bg-gray-600 w-14 h-8 rounded-full';
classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition'; classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition';
} }
@ -43,10 +43,10 @@ export const ToggleButton: FC<IToggleButtonProps> = (props) => {
<div className={classDot}></div> <div className={classDot}></div>
</div> </div>
<div className="ml-3 text-gray-700 font-medium"> <div className="ml-3 text-gray-700 font-medium">
{ props.text } {props.text}
</div> </div>
</label> </label>
</div> </div>
</div> </div>
); );
}; }

View file

@ -14,23 +14,23 @@ import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
interface IUIProps { interface IUIProps {
SelectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
current: IHistoryState current: IHistoryState
history: IHistoryState[] history: IHistoryState[]
historyCurrentStep: number historyCurrentStep: number
AvailableContainers: IAvailableContainer[] availableContainers: IAvailableContainer[]
AvailableSymbols: IAvailableSymbol[] availableSymbols: IAvailableSymbol[]
SelectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
DeleteContainer: (containerId: string) => void deleteContainer: (containerId: string) => void
OnPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void onPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
AddContainer: (type: string) => void addContainer: (type: string) => void
AddSymbol: (type: string) => void addSymbol: (type: string) => void
OnSymbolPropertyChange: (key: string, value: string | number | boolean) => void onSymbolPropertyChange: (key: string, value: string | number | boolean) => void
SelectSymbol: (symbolId: string) => void selectSymbol: (symbolId: string) => void
DeleteSymbol: (symbolId: string) => void deleteSymbol: (symbolId: string) => void
SaveEditorAsJSON: () => void saveEditorAsJSON: () => void
SaveEditorAsSVG: () => void saveEditorAsSVG: () => void
LoadState: (move: number) => void loadState: (move: number) => void
} }
function CloseOtherSidebars( function CloseOtherSidebars(
@ -41,7 +41,7 @@ function CloseOtherSidebars(
setIsSymbolsOpen(false); setIsSymbolsOpen(false);
} }
export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => { export function UI(props: IUIProps): JSX.Element {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true); const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false); const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false); const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
@ -61,71 +61,63 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
isSymbolsOpen={isSymbolsOpen} isSymbolsOpen={isSymbolsOpen}
isElementsSidebarOpen={isSidebarOpen} isElementsSidebarOpen={isSidebarOpen}
isHistoryOpen={isHistoryOpen} isHistoryOpen={isHistoryOpen}
ToggleSidebar={() => { toggleSidebar={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen); CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
setIsSidebarOpen(!isSidebarOpen); setIsSidebarOpen(!isSidebarOpen);
}} } }
ToggleSymbols={() => { toggleSymbols={() => {
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen); CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
setIsSymbolsOpen(!isSymbolsOpen); setIsSymbolsOpen(!isSymbolsOpen);
}} } }
ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} />
/>
<Sidebar <Sidebar
componentOptions={props.AvailableContainers} componentOptions={props.availableContainers}
isOpen={isSidebarOpen} isOpen={isSidebarOpen}
buttonOnClick={props.AddContainer} buttonOnClick={props.addContainer} />
/>
<Symbols <Symbols
componentOptions={props.AvailableSymbols} componentOptions={props.availableSymbols}
isOpen={isSymbolsOpen} isOpen={isSymbolsOpen}
buttonOnClick={props.AddSymbol} buttonOnClick={props.addSymbol} />
/>
<ElementsSidebar <ElementsSidebar
MainContainer={props.current.MainContainer} mainContainer={props.current.mainContainer}
symbols={props.current.Symbols} symbols={props.current.symbols}
SelectedContainer={props.SelectedContainer} selectedContainer={props.selectedContainer}
isOpen={isSidebarOpen} isOpen={isSidebarOpen}
isHistoryOpen={isHistoryOpen} isHistoryOpen={isHistoryOpen}
OnPropertyChange={props.OnPropertyChange} onPropertyChange={props.onPropertyChange}
SelectContainer={props.SelectContainer} selectContainer={props.selectContainer}
DeleteContainer={props.DeleteContainer} deleteContainer={props.deleteContainer} />
/>
<SymbolsSidebar <SymbolsSidebar
SelectedSymbolId={props.current.SelectedSymbolId} selectedSymbolId={props.current.selectedSymbolId}
symbols={props.current.Symbols} symbols={props.current.symbols}
isOpen={isSymbolsOpen} isOpen={isSymbolsOpen}
isHistoryOpen={isHistoryOpen} isHistoryOpen={isHistoryOpen}
OnPropertyChange={props.OnSymbolPropertyChange} onPropertyChange={props.onSymbolPropertyChange}
SelectSymbol={props.SelectSymbol} selectSymbol={props.selectSymbol}
DeleteSymbol={props.DeleteSymbol} deleteSymbol={props.deleteSymbol} />
/>
<History <History
history={props.history} history={props.history}
historyCurrentStep={props.historyCurrentStep} historyCurrentStep={props.historyCurrentStep}
isOpen={isHistoryOpen} isOpen={isHistoryOpen}
jumpTo={props.LoadState} jumpTo={props.loadState} />
/>
<FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}> <FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
<button type="button" <button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'} className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as JSON' title='Export as JSON'
onClick={props.SaveEditorAsJSON} onClick={props.saveEditorAsJSON}
> >
<UploadIcon className="heroicon text-white" /> <UploadIcon className="heroicon text-white" />
</button> </button>
<button type="button" <button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'} className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as SVG' title='Export as SVG'
onClick={props.SaveEditorAsSVG} onClick={props.saveEditorAsSVG}
> >
<PhotographIcon className="heroicon text-white" /> <PhotographIcon className="heroicon text-white" />
</button> </button>
</FloatingButton> </FloatingButton>
</> </>
); );
}; }
export default UI;

View file

@ -8,15 +8,15 @@ export enum PropertyType {
/** /**
* Simple property: is not inside any object: id, x, width... (default) * Simple property: is not inside any object: id, x, width... (default)
*/ */
SIMPLE, Simple,
/** /**
* Style property: is inside the style object: stroke, fillOpacity... * Style property: is inside the style object: stroke, fillOpacity...
*/ */
STYLE, Style,
/** /**
* Margin property: is inside the margin property: left, bottom, top, right... * Margin property: is inside the margin property: left, bottom, top, right...
*/ */
MARGIN, Margin,
} }

View file

@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { getCurrentHistory } from '../Components/Editor/Editor'; import { GetCurrentHistory } from '../Components/Editor/Editor';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { IConfiguration } from '../Interfaces/IConfiguration';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
@ -38,7 +38,7 @@ const appendNewState = (
): void => { ): void => {
const state: IHistoryState = JSON.parse(eventInitDict?.detail.state); const state: IHistoryState = JSON.parse(eventInitDict?.detail.state);
ReviveState(state); ReviveState(state);
const history = getCurrentHistory(editorState.history, editorState.historyCurrentStep); const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep);
history.push(state); history.push(state);
setHistory(history); setHistory(history);

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React from 'react'; import React from 'react';
import { AddMethod } from '../Enums/AddMethod'; import { AddMethod } from '../Enums/AddMethod';
import { XPositionReference } from '../Enums/XPositionReference'; import { XPositionReference } from '../Enums/XPositionReference';

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { XPositionReference } from '../Enums/XPositionReference'; import { XPositionReference } from '../Enums/XPositionReference';
import { IImage } from './IImage'; import { IImage } from './IImage';

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { IAvailableContainer } from './IAvailableContainer'; import { IAvailableContainer } from './IAvailableContainer';
import { IAvailableSymbol } from './IAvailableSymbol'; import { IAvailableSymbol } from './IAvailableSymbol';

View file

@ -1,4 +1,4 @@
import IContainerProperties from './IContainerProperties'; import { IContainerProperties } from './IContainerProperties';
export interface IContainerModel { export interface IContainerModel {
children: IContainerModel[] children: IContainerModel[]

View file

@ -5,7 +5,7 @@ import { IMargin } from './IMargin';
/** /**
* Properties of a container * Properties of a container
*/ */
export default interface IContainerProperties { export interface IContainerProperties {
/** id of the container */ /** id of the container */
id: string id: string
@ -55,7 +55,7 @@ export default interface IContainerProperties {
isFlex: boolean isFlex: boolean
/** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */ /** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */
XPositionReference: XPositionReference xPositionReference: XPositionReference
/** /**
* (optional) * (optional)

View file

@ -3,20 +3,20 @@ import { ISymbolModel } from './ISymbolModel';
export interface IHistoryState { export interface IHistoryState {
/** Last editor action */ /** Last editor action */
LastAction: string lastAction: string
/** Reference to the main container */ /** Reference to the main container */
MainContainer: IContainerModel mainContainer: IContainerModel
/** Id of the selected container */ /** Id of the selected container */
SelectedContainerId: string selectedContainerId: string
/** Counter of type of container. Used for ids. */ /** Counter of type of container. Used for ids. */
TypeCounters: Record<string, number> typeCounters: Record<string, number>
/** List of symbols */ /** List of symbols */
Symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
/** Selected symbols id */ /** Selected symbols id */
SelectedSymbolId: string selectedSymbolId: string
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
/** /**
* Model of an image with multiple source * Model of an image with multiple source
* It must at least have one source. * It must at least have one source.

View file

@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
import { App } from './Components/App/App'; import { App } from './Components/App/App';
import './index.scss'; import './index.scss';
function render(root: Element | Document): void { function RenderRoot(root: Element | Document): void {
ReactDOM.createRoot(root.querySelector('#root') as HTMLDivElement).render( ReactDOM.createRoot(root.querySelector('#root') as HTMLDivElement).render(
<React.StrictMode> <React.StrictMode>
<App root={root}/> <App root={root}/>
@ -12,10 +12,9 @@ function render(root: Element | Document): void {
} }
namespace SVGLayoutDesigner { namespace SVGLayoutDesigner {
export const Render = render; export const Render = RenderRoot;
} }
(window as any).SVGLayoutDesigner = SVGLayoutDesigner; (window as any).SVGLayoutDesigner = SVGLayoutDesigner;
render(document); RenderRoot(document);

View file

@ -3,7 +3,7 @@ import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
import { IAvailableSymbol } from '../Interfaces/IAvailableSymbol'; import { IAvailableSymbol } from '../Interfaces/IAvailableSymbol';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { IConfiguration } from '../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../Interfaces/IContainerModel';
import IContainerProperties from '../Interfaces/IContainerProperties'; import { IContainerProperties } from '../Interfaces/IContainerProperties';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { ISymbolModel } from '../Interfaces/ISymbolModel'; import { ISymbolModel } from '../Interfaces/ISymbolModel';
@ -36,7 +36,7 @@ export const APPLY_BEHAVIORS_ON_CHILDREN = true;
/** /**
* Returns the default editor state given the configuration * Returns the default editor state given the configuration
*/ */
export const GetDefaultEditorState = (configuration: IConfiguration): IEditorState => { export function GetDefaultEditorState(configuration: IConfiguration): IEditorState {
const mainContainer = new ContainerModel( const mainContainer = new ContainerModel(
null, null,
{ {
@ -50,22 +50,23 @@ export const GetDefaultEditorState = (configuration: IConfiguration): IEditorSta
configuration, configuration,
history: [ history: [
{ {
LastAction: '', lastAction: '',
MainContainer: mainContainer, mainContainer: mainContainer,
SelectedContainerId: mainContainer.properties.id, selectedContainerId: mainContainer.properties.id,
TypeCounters: {}, typeCounters: {},
Symbols: new Map(), symbols: new Map(),
SelectedSymbolId: '' selectedSymbolId: ''
} }
], ],
historyCurrentStep: 0 historyCurrentStep: 0
}; };
}; }
/** /**
* Default config when the API is not available * Default config when the API is not available
*/ */
export const DEFAULT_CONFIG: IConfiguration = { export const DEFAULT_CONFIG: IConfiguration = {
/* eslint-disable @typescript-eslint/naming-convention */
AvailableContainers: [ AvailableContainers: [
{ {
Type: 'Container', Type: 'Container',
@ -87,6 +88,7 @@ export const DEFAULT_CONFIG: IConfiguration = {
stroke: 'black' stroke: 'black'
} }
} }
/* eslint-enable */
}; };
/** /**
@ -107,7 +109,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
height: Number(DEFAULT_CONFIG.MainContainer.Height), height: Number(DEFAULT_CONFIG.MainContainer.Height),
isAnchor: false, isAnchor: false,
isFlex: false, isFlex: false,
XPositionReference: XPositionReference.Left, xPositionReference: XPositionReference.Left,
style: { style: {
stroke: 'black', stroke: 'black',
fillOpacity: 0 fillOpacity: 0
@ -124,42 +126,40 @@ export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
* @param containerConfig default config of the container sent by the API * @param containerConfig default config of the container sent by the API
* @returns {IContainerProperties} Default properties of a newly created container * @returns {IContainerProperties} Default properties of a newly created container
*/ */
export const GetDefaultContainerProps = ( export function GetDefaultContainerProps(type: string,
type: string,
typeCount: number, typeCount: number,
parent: IContainerModel, parent: IContainerModel,
x: number, x: number,
y: number, y: number,
width: number, width: number,
height: number, height: number,
containerConfig: IAvailableContainer containerConfig: IAvailableContainer): IContainerProperties {
): IContainerProperties => ({ return ({
id: `${type}-${typeCount}`, id: `${type}-${typeCount}`,
type, type,
parentId: parent.properties.id, parentId: parent.properties.id,
linkedSymbolId: '', linkedSymbolId: '',
displayedText: `${type}-${typeCount}`, displayedText: `${type}-${typeCount}`,
x, x,
y, y,
margin: containerConfig.Margin ?? {}, margin: containerConfig.Margin ?? {},
width, width,
height, height,
isAnchor: false, isAnchor: false,
isFlex: containerConfig.IsFlex ?? containerConfig.Width === undefined, isFlex: containerConfig.IsFlex ?? containerConfig.Width === undefined,
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left, xPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
minWidth: containerConfig.MinWidth ?? 1, minWidth: containerConfig.MinWidth ?? 1,
maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER, maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER,
customSVG: containerConfig.CustomSVG, customSVG: containerConfig.CustomSVG,
style: structuredClone(containerConfig.Style), style: structuredClone(containerConfig.Style),
userData: structuredClone(containerConfig.UserData) userData: structuredClone(containerConfig.UserData)
}); });
}
export const GetDefaultSymbolModel = ( export function GetDefaultSymbolModel(name: string,
name: string,
newCounters: Record<string, number>, newCounters: Record<string, number>,
type: string, type: string,
symbolConfig: IAvailableSymbol symbolConfig: IAvailableSymbol): ISymbolModel {
): ISymbolModel => {
return { return {
id: `${name}-${newCounters[type]}`, id: `${name}-${newCounters[type]}`,
type: name, type: name,
@ -169,4 +169,4 @@ export const GetDefaultSymbolModel = (
height: symbolConfig.Height ?? DEFAULT_SYMBOL_HEIGHT, height: symbolConfig.Height ?? DEFAULT_SYMBOL_HEIGHT,
linkedContainers: new Set() linkedContainers: new Set()
}; };
}; }

View file

@ -54,7 +54,7 @@ export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAnd
* Returns the depth of the container * Returns the depth of the container
* @returns The depth of the container * @returns The depth of the container
*/ */
export function getDepth(parent: IContainerModel): number { export function GetDepth(parent: IContainerModel): number {
let depth = 0; let depth = 0;
let current: IContainerModel | null = parent; let current: IContainerModel | null = parent;
@ -70,10 +70,10 @@ export function getDepth(parent: IContainerModel): number {
* Returns the absolute position by iterating to the parent * Returns the absolute position by iterating to the parent
* @returns The absolute position of the container * @returns The absolute position of the container
*/ */
export function getAbsolutePosition(container: IContainerModel): [number, number] { export function GetAbsolutePosition(container: IContainerModel): [number, number] {
const x = container.properties.x; const x = container.properties.x;
const y = container.properties.y; const y = container.properties.y;
return cancelParentTransform(container.parent, x, y); return CancelParentTransform(container.parent, x, y);
} }
/** /**
@ -83,7 +83,7 @@ export function getAbsolutePosition(container: IContainerModel): [number, number
* @param y value to be restored * @param y value to be restored
* @returns x and y such that the transformations of the parent are cancelled * @returns x and y such that the transformations of the parent are cancelled
*/ */
export function cancelParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] { export function CancelParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] {
let current = parent; let current = parent;
while (current != null) { while (current != null) {
x += current.properties.x; x += current.properties.x;
@ -100,7 +100,7 @@ export function cancelParentTransform(parent: IContainerModel | null, x: number,
* @param y value to be restored * @param y value to be restored
* @returns x and y such that the transformations of the parent are cancelled * @returns x and y such that the transformations of the parent are cancelled
*/ */
export function applyParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] { export function ApplyParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] {
let current = parent; let current = parent;
while (current != null) { while (current != null) {
x -= current.properties.x; x -= current.properties.x;
@ -110,7 +110,7 @@ export function applyParentTransform(parent: IContainerModel | null, x: number,
return [x, y]; return [x, y];
} }
export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined { export function FindContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
const it = MakeIterator(root); const it = MakeIterator(root);
for (const container of it) { for (const container of it) {
if (container.properties.id === id) { if (container.properties.id === id) {
@ -125,13 +125,13 @@ export interface IPair<T> {
next: T next: T
} }
export function * pairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> { export function * Pairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> {
for (let i = 0; i < arr.length - 1; i++) { for (let i = 0; i < arr.length - 1; i++) {
yield { cur: arr[i], next: arr[i + 1] }; yield { cur: arr[i], next: arr[i + 1] };
} }
} }
export function * reversePairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> { export function * ReversePairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> {
for (let i = arr.length - 1; i > 0; i--) { for (let i = arr.length - 1; i > 0; i--) {
yield { cur: arr[i], next: arr[i - 1] }; yield { cur: arr[i], next: arr[i - 1] };
} }

View file

@ -1,4 +1,4 @@
import { findContainerById, MakeIterator } from './itertools'; import { FindContainerById, MakeIterator } from './itertools';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
@ -19,34 +19,32 @@ export function Revive(editorState: IEditorState): void {
} }
} }
export const ReviveState = ( export function ReviveState(state: IHistoryState): void {
state: IHistoryState if (state.mainContainer === null || state.mainContainer === undefined) {
): void => {
if (state.MainContainer === null || state.MainContainer === undefined) {
return; return;
} }
state.Symbols = new Map(state.Symbols); state.symbols = new Map(state.symbols);
for (const symbol of state.Symbols.values()) { for (const symbol of state.symbols.values()) {
symbol.linkedContainers = new Set(symbol.linkedContainers); symbol.linkedContainers = new Set(symbol.linkedContainers);
} }
const it = MakeIterator(state.MainContainer); const it = MakeIterator(state.mainContainer);
for (const container of it) { for (const container of it) {
const parentId = container.properties.parentId; const parentId = container.properties.parentId;
if (parentId === null) { if (parentId === null) {
container.parent = null; container.parent = null;
continue; continue;
} }
const parent = findContainerById(state.MainContainer, parentId); const parent = FindContainerById(state.mainContainer, parentId);
if (parent === undefined) { if (parent === undefined) {
continue; continue;
} }
container.parent = parent; container.parent = parent;
} }
}; }
export const getCircularReplacer = (): (key: any, value: object | Map<string, any> | null) => object | null | undefined => { export function GetCircularReplacer(): (key: any, value: object | Map<string, any> | null) => object | null | undefined {
return (key: any, value: object | null) => { return (key: any, value: object | null) => {
if (key === 'parent') { if (key === 'parent') {
return; return;
@ -62,4 +60,4 @@ export const getCircularReplacer = (): (key: any, value: object | Map<string, an
return value; return value;
}; };
}; }

View file

@ -89,7 +89,7 @@ function GetInitialMatrix(
return matrix; return matrix;
} }
function getAllIndexes(arr: number[], val: number): number[] { function GetAllIndexes(arr: number[], val: number): number[] {
const indexes = []; let i = -1; const indexes = []; let i = -1;
while ((i = arr.indexOf(val, i + 1)) !== -1) { while ((i = arr.indexOf(val, i + 1)) !== -1) {
indexes.push(i); indexes.push(i);
@ -125,9 +125,9 @@ function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
// 1) find the index with smallest coefficient (O(n)+) // 1) find the index with smallest coefficient (O(n)+)
const lastRow = matrix[matrix.length - 1]; const lastRow = matrix[matrix.length - 1];
const min = Math.min(...lastRow); const min = Math.min(...lastRow);
const indexes = getAllIndexes(lastRow, min); const indexes = GetAllIndexes(lastRow, min);
// to avoid infinite loop try to select the least used selected index // to avoid infinite loop try to select the least used selected index
const pivotColIndex = getLeastUsedIndex(indexes, indexesTried); const pivotColIndex = GetLeastUsedIndex(indexes, indexesTried);
// record the usage of index by incrementing // record the usage of index by incrementing
indexesTried[pivotColIndex] = indexesTried[pivotColIndex] !== undefined ? indexesTried[pivotColIndex] + 1 : 1; indexesTried[pivotColIndex] = indexesTried[pivotColIndex] !== undefined ? indexesTried[pivotColIndex] + 1 : 1;
@ -227,7 +227,7 @@ function GetSolutions(nCols: number, finalMatrix: number[][]): number[] {
* @param indexesTried Record of indexes. Count the number of times the index was used. * @param indexesTried Record of indexes. Count the number of times the index was used.
* @returns The least used index * @returns The least used index
*/ */
function getLeastUsedIndex(indexes: number[], indexesTried: Record<number, number>): number { function GetLeastUsedIndex(indexes: number[], indexesTried: Record<number, number>): number {
let minUsed = Infinity; let minUsed = Infinity;
let minIndex = -1; let minIndex = -1;
for (const index of indexes) { for (const index of indexes) {

View file

@ -1,10 +1,10 @@
export function truncateString(str: string, num: number): string { export function TruncateString(str: string, num: number): string {
if (str.length <= num) { if (str.length <= num) {
return str; return str;
} }
return `${str.slice(0, num)}...`; return `${str.slice(0, num)}...`;
} }
export function camelize(str: string): any { export function Camelize(str: string): any {
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join(''); return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
} }

View file

@ -10,7 +10,7 @@ import { XPositionReference } from '../Enums/XPositionReference';
* it is better to fix serialization with the reviver. * it is better to fix serialization with the reviver.
*/ */
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number { export function TransformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
let transformedX = x; let transformedX = x;
if (xPositionReference === XPositionReference.Center) { if (xPositionReference === XPositionReference.Center) {
transformedX += width / 2; transformedX += width / 2;
@ -20,7 +20,7 @@ export function transformX(x: number, width: number, xPositionReference = XPosit
return transformedX; return transformedX;
} }
export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number { export function RestoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
let transformedX = x; let transformedX = x;
if (xPositionReference === XPositionReference.Center) { if (xPositionReference === XPositionReference.Center) {
transformedX -= width / 2; transformedX -= width / 2;

View file

@ -7,14 +7,15 @@ afterEach(() => {
cleanup(); cleanup();
}); });
const customRender = (ui: React.ReactElement, options = {}): RenderResult => function CustomRender(ui: React.ReactElement, options = {}): RenderResult {
render(ui, { return render(ui, {
// wrap provider(s) here if needed // wrap provider(s) here if needed
wrapper: ({ children }) => children, wrapper: ({ children }) => children,
...options ...options
}); });
}
export * from '@testing-library/react'; export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event'; export { default as userEvent } from '@testing-library/user-event';
// override render export // override render export
export { customRender as render }; export { CustomRender as render };