Unrefactor Properties form to allow more freedom on the input types and form (#32)
All checks were successful
continuous-integration/drone/push Build is passing

- The css style is now in IProperties.Style again.
- Forms are divided in DynamicForm and StaticForm
- Faster because less logic
- Add RadioGroupButton
- Add InputGroup
- Fix Children Dimensions not using x for their origin

Co-authored-by: Eric NGUYEN <enguyen@techform.fr>
Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/32
This commit is contained in:
Siklos 2022-08-16 08:57:54 -04:00
parent 3d7baafc17
commit 5f8e011bc6
19 changed files with 529 additions and 134 deletions

View file

@ -4,6 +4,7 @@ import { ContainerModel } from '../../Interfaces/IContainerModel';
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 { XPositionReference } from '../../Enums/XPositionReference';
export function NewEditor( export function NewEditor(
setEditorState: Dispatch<SetStateAction<IEditorState>>, setEditorState: Dispatch<SetStateAction<IEditorState>>,
@ -24,9 +25,12 @@ export function NewEditor(
height: Number(configuration.MainContainer.Height), height: Number(configuration.MainContainer.Height),
isRigidBody: false, isRigidBody: false,
isAnchor: false, isAnchor: false,
XPositionReference: XPositionReference.Left,
style: {
fillOpacity: 0, fillOpacity: 0,
stroke: 'black' stroke: 'black'
} }
}
); );
// Save the configuration and the new MainContainer // Save the configuration and the new MainContainer

View file

@ -1,4 +1,4 @@
import React, { Dispatch, SetStateAction } from 'react'; 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';
@ -8,6 +8,7 @@ import IProperties from '../../Interfaces/IProperties';
import { AddMethod } from '../../Enums/AddMethod'; import { AddMethod } from '../../Enums/AddMethod';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { transformPosition } from '../SVG/Elements/Container'; import { transformPosition } from '../SVG/Elements/Container';
import { XPositionReference } from '../../Enums/XPositionReference';
/** /**
* Select a container * Select a container
@ -209,7 +210,6 @@ export function AddContainer(
x = ApplyAddMethod(index, containerConfig, parentClone, x); x = ApplyAddMethod(index, containerConfig, parentClone, x);
const defaultProperties: IProperties = { const defaultProperties: IProperties = {
...containerConfig.Style,
id: `${type}-${count}`, id: `${type}-${count}`,
parentId: parentClone.properties.id, parentId: parentClone.properties.id,
x, x,
@ -218,7 +218,8 @@ export function AddContainer(
height, height,
isRigidBody: false, isRigidBody: false,
isAnchor: false, isAnchor: false,
XPositionReference: containerConfig.XPositionReference XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
style: containerConfig.Style
}; };
// Create the container // Create the container

View file

@ -91,16 +91,15 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
OnPropertyChange={(key, value) => OnPropertyChange( OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
key, value, key, value, isStyle,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit( OnPropertiesSubmit={(event) => OnPropertiesSubmit(
event, event,
properties,
history, history,
historyCurrentStep, historyCurrentStep,
setHistory, setHistory,

View file

@ -1,7 +1,6 @@
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import IProperties from '../../Interfaces/IProperties';
import { findContainerById } from '../../utils/itertools'; import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor'; import { getCurrentHistory } from './Editor';
import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors'; import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors';
@ -17,6 +16,7 @@ import { ImposePosition } from './Behaviors/AnchorBehaviors';
export function OnPropertyChange( export function OnPropertyChange(
key: string, key: string,
value: string | number | boolean, value: string | number | boolean,
isStyle: boolean = false,
fullHistory: IHistoryState[], fullHistory: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
@ -37,11 +37,15 @@ export function OnPropertyChange(
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!');
} }
if (isStyle) {
(container.properties.style as any)[key] = value;
} else {
if (INPUT_TYPES[key] === 'number') { if (INPUT_TYPES[key] === 'number') {
(container.properties as any)[key] = Number(value); (container.properties as any)[key] = Number(value);
} else { } else {
(container.properties as any)[key] = value; (container.properties as any)[key] = value;
} }
}
if (container.properties.isAnchor) { if (container.properties.isAnchor) {
ImposePosition(container); ImposePosition(container);
@ -70,7 +74,6 @@ export function OnPropertyChange(
*/ */
export function OnPropertiesSubmit( export function OnPropertiesSubmit(
event: React.SyntheticEvent<HTMLFormElement>, event: React.SyntheticEvent<HTMLFormElement>,
properties: IProperties,
fullHistory: IHistoryState[], fullHistory: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
@ -92,18 +95,42 @@ export function OnPropertiesSubmit(
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!');
} }
for (const property in properties) { // Assign container properties
const input = (event.target as HTMLFormElement).querySelector(`#${property}`); for (const property in container.properties) {
const input: HTMLInputElement | HTMLDivElement | null = (event.target as HTMLFormElement).querySelector(`#${property}`);
if (input === null) {
continue;
}
if (input instanceof HTMLInputElement) { if (input instanceof HTMLInputElement) {
(container.properties as any)[property] = input.value; (container.properties as any)[property] = input.value;
if (INPUT_TYPES[property] === 'number') { if (INPUT_TYPES[property] === 'number') {
(container.properties as any)[property] = Number(input.value); (container.properties as any)[property] = Number(input.value);
} else { }
(container.properties as any)[property] = input.value; } else if (input instanceof HTMLDivElement) {
const radiobutton: HTMLInputElement | null = input.querySelector(`input[name="${property}"]:checked`);
if (radiobutton === null) {
continue;
}
(container.properties as any)[property] = radiobutton.value;
if (INPUT_TYPES[property] === 'number') {
(container.properties as any)[property] = Number(radiobutton.value);
} }
} }
} }
// Assign cssproperties
for (const styleProperty in container.properties.style) {
const input: HTMLInputElement | null = (event.target as HTMLFormElement).querySelector(`#${styleProperty}`);
if (input === null) {
continue;
}
(container.properties.style as any)[styleProperty] = input.value;
}
if (container.properties.isRigidBody) { if (container.properties.isRigidBody) {
RecalculatePhysics(container); RecalculatePhysics(container);
} }

View file

@ -3,6 +3,7 @@ import * as React from 'react';
import { fireEvent, render, screen } from '../../utils/test-utils'; 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';
describe.concurrent('Elements sidebar', () => { describe.concurrent('Elements sidebar', () => {
it('With a MainContainer', () => { it('With a MainContainer', () => {
@ -17,6 +18,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
}, },
@ -49,7 +51,8 @@ describe.concurrent('Elements sidebar', () => {
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false,
XPositionReference: XPositionReference.Left
}, },
userData: {} userData: {}
}; };
@ -105,6 +108,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
}, },
@ -123,7 +127,8 @@ describe.concurrent('Elements sidebar', () => {
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false,
XPositionReference: XPositionReference.Left
}, },
userData: {} userData: {}
} }
@ -140,6 +145,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
}, },
@ -178,6 +184,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
}, },
@ -194,6 +201,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
}, },

View file

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { Properties } from '../Properties/Properties'; import { Properties } from '../Properties/Properties';
import ContainerProperties from '../../Interfaces/IProperties';
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';
@ -14,8 +13,8 @@ interface IElementsSidebarProps {
isOpen: boolean isOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
SelectedContainer: IContainerModel | null SelectedContainer: IContainerModel | null
OnPropertyChange: (key: string, value: string | number | boolean) => void OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
SelectContainer: (container: IContainerModel) => void SelectContainer: (container: IContainerModel) => void
DeleteContainer: (containerid: string) => void DeleteContainer: (containerid: string) => void
AddContainer: (index: number, type: string, parent: string) => void AddContainer: (index: number, type: string, parent: string) => void

View file

@ -0,0 +1,48 @@
import * as React from 'react';
interface IInputGroupProps {
labelKey?: string
labelText: string
inputKey: string
labelClassName: string
inputClassName: string
type: string
value?: string
checked?: boolean
defaultValue?: string
defaultChecked?: boolean
isDisabled?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const className = `
w-full
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
bg-white border-2 border-white rounded-lg placeholder-gray-800
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`;
export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) => {
return <>
<label
key={props.labelKey}
className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`}
htmlFor={props.inputKey}
>
{ props.labelText }
</label>
<input
key={props.inputKey}
id={props.inputKey}
className={`${className} ${props.inputClassName}`}
type={props.type}
value={props.value}
defaultValue={props.defaultValue}
checked={props.checked}
defaultChecked={props.defaultChecked}
onChange={props.onChange}
disabled={props.isDisabled}
/>
</>;
};

View file

@ -0,0 +1,146 @@
import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { XPositionReference } from '../../Enums/XPositionReference';
import IProperties from '../../Interfaces/IProperties';
import { InputGroup } from '../InputGroup/InputGroup';
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
interface IDynamicFormProps {
properties: IProperties
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
}
const getCSSInputs = (
properties: IProperties,
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
): JSX.Element[] => {
const groupInput: JSX.Element[] = [];
for (const key in properties.style) {
groupInput.push(<InputGroup
key={key}
labelText={key}
inputKey={key}
labelClassName=''
inputClassName=''
type='string'
value={(properties.style as any)[key]}
onChange={(event) => onChange(key, event.target.value, true)}
/>);
}
return groupInput;
};
const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
return (
<div className='grid grid-cols-2 gap-y-4'>
<InputGroup
labelText='Name'
inputKey='id'
labelClassName=''
inputClassName=''
type='string'
value={props.properties.id.toString()}
isDisabled={true}
/>
<InputGroup
labelText='Parent name'
inputKey='parentId'
labelClassName=''
inputClassName=''
type='string'
value={props.properties.parentId?.toString()}
isDisabled={true}
/>
<InputGroup
labelText='x'
inputKey='x'
labelClassName=''
inputClassName=''
type='number'
value={props.properties.x.toString()}
onChange={(event) => props.onChange('x', event.target.value)}
/>
<InputGroup
labelText='y'
inputKey='y'
labelClassName=''
inputClassName=''
type='number'
value={props.properties.y.toString()}
onChange={(event) => props.onChange('y', event.target.value)}
/>
<InputGroup
labelText='Width'
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
value={props.properties.width.toString()}
onChange={(event) => props.onChange('width', event.target.value)}
/>
<InputGroup
labelText='Height'
inputKey='height'
labelClassName=''
inputClassName=''
type='number'
value={props.properties.height.toString()}
onChange={(event) => props.onChange('height', event.target.value)}
/>
<InputGroup
labelText='Rigid'
inputKey='isRigidBody'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.isRigidBody}
onChange={(event) => props.onChange('isRigidBody', event.target.checked)}
/>
<InputGroup
labelText='Anchor'
inputKey='isAnchor'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.isAnchor}
onChange={(event) => props.onChange('isAnchor', event.target.checked)}
/>
<RadioGroupButtons
name='XPositionReference'
value={props.properties.XPositionReference.toString()}
inputClassName='hidden'
labelText='Horizontal alignment'
inputGroups={[
{
text: (
<div title='Left' aria-label='left' className='radio-button-icon'>
<MenuAlt2Icon className='heroicon' />
</div>
),
value: XPositionReference.Left.toString()
},
{
text: (
<div title='Center' aria-label='center' className='radio-button-icon'>
<MenuIcon className='heroicon' />
</div>
),
value: XPositionReference.Center.toString()
},
{
text: (
<div title='Right' aria-label='right' className='radio-button-icon'>
<MenuAlt3Icon className='heroicon' />
</div>
),
value: XPositionReference.Right.toString()
}
]}
onChange={(event) => props.onChange('XPositionReference', event.target.value)}
/>
{ getCSSInputs(props.properties, props.onChange) }
</div>
);
};
export default DynamicForm;

View file

@ -0,0 +1,24 @@
import * as React from 'react';
import IProperties from '../../Interfaces/IProperties';
import DynamicForm from './DynamicForm';
import StaticForm from './StaticForm';
interface IFormProps {
properties: IProperties
isDynamicInput: boolean
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
}
export const Form: React.FunctionComponent<IFormProps> = (props) => {
if (props.isDynamicInput) {
return <DynamicForm
properties={props.properties}
onChange={props.onChange}
/>;
}
return <StaticForm
properties={props.properties}
onSubmit={props.onSubmit}
/>;
};

View file

@ -1,6 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'; 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 IProperties from '../../Interfaces/IProperties'; import IProperties from '../../Interfaces/IProperties';
import { Properties } from './Properties'; import { Properties } from './Properties';
@ -26,6 +27,7 @@ describe.concurrent('Properties', () => {
y: 1, y: 1,
width: 1, width: 1,
height: 1, height: 1,
XPositionReference: XPositionReference.Left,
isRigidBody: false, isRigidBody: false,
isAnchor: false isAnchor: false
}; };
@ -62,10 +64,10 @@ describe.concurrent('Properties', () => {
fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } }); fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } });
fireEvent.change(propertyX as Element, { target: { value: '2' } }); fireEvent.change(propertyX as Element, { target: { value: '2' } });
fireEvent.change(propertyY as Element, { target: { value: '2' } }); fireEvent.change(propertyY as Element, { target: { value: '2' } });
expect(handleChange).toBeCalledTimes(4); expect(handleChange).toBeCalledTimes(2);
expect(prop.id).toBe('stuffed'); expect(prop.id).toBe('stuff');
expect(prop.parentId).toBe('parentedId'); expect(prop.parentId).toBe('parentId');
expect(prop.x).toBe('2'); expect(prop.x).toBe('2');
expect(prop.y).toBe('2'); expect(prop.y).toBe('2');
rerender(<Properties rerender(<Properties
@ -79,9 +81,9 @@ describe.concurrent('Properties', () => {
propertyX = container.querySelector('#x'); propertyX = container.querySelector('#x');
propertyY = container.querySelector('#y'); propertyY = container.querySelector('#y');
expect(propertyId).toBeDefined(); expect(propertyId).toBeDefined();
expect((propertyId as HTMLInputElement).value).toBe('stuffed'); expect((propertyId as HTMLInputElement).value).toBe('stuff');
expect(propertyParentId).toBeDefined(); expect(propertyParentId).toBeDefined();
expect((propertyParentId as HTMLInputElement).value).toBe('parentedId'); expect((propertyParentId as HTMLInputElement).value).toBe('parentId');
expect(propertyX).toBeDefined(); expect(propertyX).toBeDefined();
expect((propertyX as HTMLInputElement).value).toBe('2'); expect((propertyX as HTMLInputElement).value).toBe('2');
expect(propertyY).toBeDefined(); expect(propertyY).toBeDefined();

View file

@ -1,12 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ContainerProperties from '../../Interfaces/IProperties'; import IProperties from '../../Interfaces/IProperties';
import { ToggleButton } from '../ToggleButton/ToggleButton'; import { ToggleButton } from '../ToggleButton/ToggleButton';
import { INPUT_TYPES } from './PropertiesInputTypes'; import { Form } from './Form';
interface IPropertiesProps { interface IPropertiesProps {
properties?: ContainerProperties properties?: IProperties
onChange: (key: string, value: string | number | boolean) => void onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
} }
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => { export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
@ -16,26 +16,6 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
return <div></div>; return <div></div>;
} }
const groupInput: React.ReactNode[] = [];
Object
.entries(props.properties)
.forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange));
const form = isDynamicInput
? <div className='grid grid-cols-2 gap-4'>
{ groupInput }
</div>
: <form
key={props.properties.id}
onSubmit={(event) => props.onSubmit(event, props.properties as ContainerProperties)}
>
<input type='submit' className='normal-btn block mx-auto mb-4 border-2 border-blue-400 cursor-pointer' value='Submit'/>
<div className='grid grid-cols-2 gap-y-4'>
{ groupInput }
</div>
</form>
;
return ( return (
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'> <div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
<ToggleButton <ToggleButton
@ -45,72 +25,12 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
checked={isDynamicInput} checked={isDynamicInput}
onChange={() => setIsDynamicInput(!isDynamicInput)} onChange={() => setIsDynamicInput(!isDynamicInput)}
/> />
{ form } <Form
properties={props.properties}
isDynamicInput={isDynamicInput}
onChange={props.onChange}
onSubmit={props.onSubmit}
/>
</div> </div>
); );
}; };
const handleProperties = (
[key, value]: [string, string | number],
groupInput: React.ReactNode[],
isDynamicInput: boolean,
onChange: (key: string, value: string | number | boolean) => void
): void => {
const id = `property-${key}`;
let type = 'text';
let checked;
/// hardcoded stuff for ergonomy ///
if (typeof value === 'boolean') {
checked = value;
}
if (key in INPUT_TYPES) {
type = INPUT_TYPES[key];
}
const className = `
w-full
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
bg-white border-2 border-white rounded-lg placeholder-gray-800
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`;
const isDisabled = ['id', 'parentId'].includes(key);
const input = isDynamicInput
? <input
key={key}
id={key}
className={className}
type={type}
value={value}
checked={checked}
onChange={(event) => {
if (type === 'checkbox') {
onChange(key, event.target.checked);
return;
}
onChange(key, event.target.value);
}}
disabled={isDisabled}
/>
: <input
key={key}
id={key}
className={className}
type={type}
defaultValue={value}
defaultChecked={checked}
disabled={isDisabled}
/>;
groupInput.push(
<label
key={id}
className='mt-4 text-xs font-medium text-gray-800'
htmlFor={key}
>
{key}
</label>
);
groupInput.push(input);
};

View file

@ -0,0 +1,139 @@
import { MenuAlt2Icon, MenuIcon, MenuAlt3Icon } from '@heroicons/react/outline';
import * as React from 'react';
import { XPositionReference } from '../../Enums/XPositionReference';
import IProperties from '../../Interfaces/IProperties';
import { InputGroup } from '../InputGroup/InputGroup';
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
interface IStaticFormProps {
properties: IProperties
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
}
const getCSSInputs = (properties: IProperties): JSX.Element[] => {
const groupInput: JSX.Element[] = [];
for (const key in properties.style) {
groupInput.push(<InputGroup
key={key}
labelText={key}
inputKey={key}
labelClassName=''
inputClassName=''
type='string'
defaultValue={(properties.style as any)[key]}
/>);
}
return groupInput;
};
const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
return (<form
key={props.properties.id}
onSubmit={(event) => props.onSubmit(event)}
>
<input type='submit' className='normal-btn block mx-auto mb-4 border-2 border-blue-400 cursor-pointer' value='Submit'/>
<div className='grid grid-cols-2 gap-y-4'>
<InputGroup
labelText='Name'
inputKey='id'
labelClassName=''
inputClassName=''
type='string'
defaultValue={props.properties.id.toString()}
isDisabled={true}
/>
<InputGroup
labelText='Parent name'
inputKey='parentId'
labelClassName=''
inputClassName=''
type='string'
defaultValue={props.properties.parentId?.toString()}
isDisabled={true}
/>
<InputGroup
labelText='x'
inputKey='x'
labelClassName=''
inputClassName=''
type='number'
defaultValue={props.properties.x.toString()}
/>
<InputGroup
labelText='y'
inputKey='y'
labelClassName=''
inputClassName=''
type='number'
defaultValue={props.properties.y.toString()}
/>
<InputGroup
labelText='Width'
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
defaultValue={props.properties.width.toString()}
/>
<InputGroup
labelText='Height'
inputKey='height'
labelClassName=''
inputClassName=''
type='number'
defaultValue={props.properties.height.toString()}
/>
<InputGroup
labelText='Rigid'
inputKey='isRigidBody'
labelClassName=''
inputClassName=''
type='checkbox'
defaultChecked={props.properties.isRigidBody}
/>
<InputGroup
labelText='Anchor'
inputKey='isAnchor'
labelClassName=''
inputClassName=''
type='checkbox'
defaultChecked={props.properties.isAnchor}
/>
<RadioGroupButtons
name='XPositionReference'
defaultValue={props.properties.XPositionReference.toString()}
inputClassName='hidden'
labelText='Horizontal alignment'
inputGroups={[
{
text: (
<div title='Left' aria-label='left' className='radio-button-icon'>
<MenuAlt2Icon className='heroicon' />
</div>
),
value: XPositionReference.Left.toString()
},
{
text: (
<div title='Center' aria-label='center' className='radio-button-icon'>
<MenuIcon className='heroicon' />
</div>
),
value: XPositionReference.Center.toString()
},
{
text: (
<div title='Right' aria-label='right' className='radio-button-icon'>
<MenuAlt3Icon className='heroicon' />
</div>
),
value: XPositionReference.Right.toString()
}
]}
/>
{ getCSSInputs(props.properties) }
</div>
</form>);
};
export default StaticForm;

View file

@ -0,0 +1,66 @@
import * as React from 'react';
import { IInputGroup } from '../../Interfaces/IInputGroup';
interface IRadioGroupButtonsProps {
name: string
value?: string
defaultValue?: string
inputClassName: string
labelText: string
inputGroups: IInputGroup[]
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps> = (props) => {
let inputGroups;
if (props.value !== undefined) {
// dynamic
inputGroups = props.inputGroups.map((inputGroup) => (
<div key={inputGroup.value}>
<input
id={inputGroup.value}
type='radio'
name={props.name}
className={`peer m-2 ${props.inputClassName}`}
value={inputGroup.value}
checked={props.value === inputGroup.value}
onChange={props.onChange}
/>
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
{inputGroup.text}
</label>
</div>
));
} else {
// static
inputGroups = props.inputGroups.map((inputGroup) => (
<div key={inputGroup.value}>
<input
id={inputGroup.value}
type='radio'
name={props.name}
className={`peer m-2 ${props.inputClassName}`}
value={inputGroup.value}
defaultChecked={props.defaultValue === inputGroup.value}
/>
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
{inputGroup.text}
</label>
</div>
));
}
return (
<>
<label className='mt-4 text-xs font-medium text-gray-800'>
{props.labelText}
</label>
<div id='XPositionReference'
className='flex flex-col'
>
{ inputGroups }
</div>
</>
);
};

View file

@ -36,12 +36,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
// Rect style // Rect style
const style = Object.assign( const style = Object.assign(
JSON.parse(JSON.stringify(defaultStyle)), JSON.parse(JSON.stringify(defaultStyle)),
props.model.properties props.model.properties.style
); );
style.x = 0;
style.y = 0;
delete style.height;
delete style.width;
// Dimension props // Dimension props
const depth = getDepth(props.model); const depth = getDepth(props.model);
@ -54,7 +50,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
const text = (props.model.properties.width ?? 0).toString(); const text = (props.model.properties.width ?? 0).toString();
let dimensionChildren: JSX.Element | null = null; let dimensionChildren: JSX.Element | null = null;
if (props.model.children.length > 0) { if (props.model.children.length > 1) {
const { const {
childrenId, childrenId,
xChildrenStart, xChildrenStart,
@ -112,14 +108,16 @@ function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: numb
const lastChild = props.model.children[props.model.children.length - 1]; const lastChild = props.model.children[props.model.children.length - 1];
let xChildrenStart = lastChild.properties.x; let xChildrenStart = lastChild.properties.x;
let xChildrenEnd = lastChild.properties.x + lastChild.properties.width; let xChildrenEnd = lastChild.properties.x;
// 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 = child.properties.x; const left = child.properties.x;
if (left < xChildrenStart) { if (left < xChildrenStart) {
xChildrenStart = left; xChildrenStart = left;
} }
const right = child.properties.x + child.properties.width; const right = child.properties.x;
if (right > xChildrenEnd) { if (right > xChildrenEnd) {
xChildrenEnd = right; xChildrenEnd = right;
} }

View file

@ -8,7 +8,6 @@ import { IHistoryState } from '../../Interfaces/IHistoryState';
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
import { FloatingButton } from '../FloatingButton/FloatingButton'; import { FloatingButton } from '../FloatingButton/FloatingButton';
import { Bar } from '../Bar/Bar'; import { Bar } from '../Bar/Bar';
import IProperties from '../../Interfaces/IProperties';
interface IUIProps { interface IUIProps {
current: IHistoryState current: IHistoryState
@ -17,8 +16,8 @@ interface IUIProps {
AvailableContainers: IAvailableContainer[] AvailableContainers: IAvailableContainer[]
SelectContainer: (container: ContainerModel) => void SelectContainer: (container: ContainerModel) => void
DeleteContainer: (containerId: string) => void DeleteContainer: (containerId: string) => void
OnPropertyChange: (key: string, value: string | number | boolean) => void OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: IProperties) => void OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
AddContainerToSelectedContainer: (type: string) => void AddContainerToSelectedContainer: (type: string) => void
AddContainer: (index: number, type: string, parentId: string) => void AddContainer: (index: number, type: string, parentId: string) => void
SaveEditorAsJSON: () => void SaveEditorAsJSON: () => void

View file

@ -0,0 +1,6 @@
import React from 'react';
export interface IInputGroup {
text: React.ReactNode
value: string
}

View file

@ -10,7 +10,7 @@ import { XPositionReference } from '../Enums/XPositionReference';
* @property isRigidBody if true apply rigid body behaviors * @property isRigidBody if true apply rigid body behaviors
* @property isAnchor if true apply anchor behaviors * @property isAnchor if true apply anchor behaviors
*/ */
export default interface IProperties extends Omit<React.CSSProperties, 'width' | 'height'> { export default interface IProperties {
id: string id: string
parentId: string | null parentId: string | null
x: number x: number
@ -19,5 +19,6 @@ export default interface IProperties extends Omit<React.CSSProperties, 'width' |
height: number height: number
isRigidBody: boolean isRigidBody: boolean
isAnchor: boolean isAnchor: boolean
XPositionReference?: XPositionReference XPositionReference: XPositionReference
style?: React.CSSProperties
} }

View file

@ -46,6 +46,10 @@
@apply h-full w-full align-middle items-center justify-center @apply h-full w-full align-middle items-center justify-center
} }
.radio-button-icon {
@apply rounded-md shadow-sm bg-white w-8 cursor-pointer inline-block
}
.sidebar-tooltip { .sidebar-tooltip {
@apply absolute w-auto p-2 m-2 min-w-max left-14 @apply absolute w-auto p-2 m-2 min-w-max left-14
rounded-md shadow-md rounded-md shadow-md

View file

@ -1,3 +1,4 @@
import { XPositionReference } from '../Enums/XPositionReference';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { IConfiguration } from '../Interfaces/IConfiguration';
import IProperties from '../Interfaces/IProperties'; import IProperties from '../Interfaces/IProperties';
@ -34,8 +35,11 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
height: Number(DEFAULT_CONFIG.MainContainer.Height), height: Number(DEFAULT_CONFIG.MainContainer.Height),
isRigidBody: false, isRigidBody: false,
isAnchor: false, isAnchor: false,
fillOpacity: 0, XPositionReference: XPositionReference.Left,
stroke: 'black' style: {
stroke: 'black',
fillOpacity: 0
}
}; };
export const DIMENSION_MARGIN = 50; export const DIMENSION_MARGIN = 50;