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
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:
parent
3d7baafc17
commit
5f8e011bc6
19 changed files with 529 additions and 134 deletions
|
@ -4,6 +4,7 @@ import { ContainerModel } from '../../Interfaces/IContainerModel';
|
|||
import { fetchConfiguration } from '../API/api';
|
||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||
import { LoadState } from './Load';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
|
||||
export function NewEditor(
|
||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||
|
@ -24,8 +25,11 @@ export function NewEditor(
|
|||
height: Number(configuration.MainContainer.Height),
|
||||
isRigidBody: false,
|
||||
isAnchor: false,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
XPositionReference: XPositionReference.Left,
|
||||
style: {
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
|
@ -8,6 +8,7 @@ import IProperties from '../../Interfaces/IProperties';
|
|||
import { AddMethod } from '../../Enums/AddMethod';
|
||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||
import { transformPosition } from '../SVG/Elements/Container';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
|
||||
/**
|
||||
* Select a container
|
||||
|
@ -209,7 +210,6 @@ export function AddContainer(
|
|||
x = ApplyAddMethod(index, containerConfig, parentClone, x);
|
||||
|
||||
const defaultProperties: IProperties = {
|
||||
...containerConfig.Style,
|
||||
id: `${type}-${count}`,
|
||||
parentId: parentClone.properties.id,
|
||||
x,
|
||||
|
@ -218,7 +218,8 @@ export function AddContainer(
|
|||
height,
|
||||
isRigidBody: false,
|
||||
isAnchor: false,
|
||||
XPositionReference: containerConfig.XPositionReference
|
||||
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
|
||||
style: containerConfig.Style
|
||||
};
|
||||
|
||||
// Create the container
|
||||
|
|
|
@ -91,16 +91,15 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
OnPropertyChange={(key, value) => OnPropertyChange(
|
||||
key, value,
|
||||
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
|
||||
key, value, isStyle,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit(
|
||||
OnPropertiesSubmit={(event) => OnPropertiesSubmit(
|
||||
event,
|
||||
properties,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import { findContainerById } from '../../utils/itertools';
|
||||
import { getCurrentHistory } from './Editor';
|
||||
import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors';
|
||||
|
@ -17,6 +16,7 @@ import { ImposePosition } from './Behaviors/AnchorBehaviors';
|
|||
export function OnPropertyChange(
|
||||
key: string,
|
||||
value: string | number | boolean,
|
||||
isStyle: boolean = false,
|
||||
fullHistory: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||
|
@ -37,10 +37,14 @@ export function OnPropertyChange(
|
|||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
||||
}
|
||||
|
||||
if (INPUT_TYPES[key] === 'number') {
|
||||
(container.properties as any)[key] = Number(value);
|
||||
if (isStyle) {
|
||||
(container.properties.style as any)[key] = value;
|
||||
} else {
|
||||
(container.properties as any)[key] = value;
|
||||
if (INPUT_TYPES[key] === 'number') {
|
||||
(container.properties as any)[key] = Number(value);
|
||||
} else {
|
||||
(container.properties as any)[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (container.properties.isAnchor) {
|
||||
|
@ -70,7 +74,6 @@ export function OnPropertyChange(
|
|||
*/
|
||||
export function OnPropertiesSubmit(
|
||||
event: React.SyntheticEvent<HTMLFormElement>,
|
||||
properties: IProperties,
|
||||
fullHistory: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
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!');
|
||||
}
|
||||
|
||||
for (const property in properties) {
|
||||
const input = (event.target as HTMLFormElement).querySelector(`#${property}`);
|
||||
// Assign container properties
|
||||
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) {
|
||||
(container.properties as any)[property] = input.value;
|
||||
if (INPUT_TYPES[property] === 'number') {
|
||||
(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) {
|
||||
RecalculatePhysics(container);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
|||
import { fireEvent, render, screen } from '../../utils/test-utils';
|
||||
import { ElementsSidebar } from './ElementsSidebar';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
|
||||
describe.concurrent('Elements sidebar', () => {
|
||||
it('With a MainContainer', () => {
|
||||
|
@ -17,6 +18,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
y: 0,
|
||||
width: 2000,
|
||||
height: 100,
|
||||
XPositionReference: XPositionReference.Left,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
},
|
||||
|
@ -49,7 +51,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
width: 2000,
|
||||
height: 100,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
isAnchor: false,
|
||||
XPositionReference: XPositionReference.Left
|
||||
},
|
||||
userData: {}
|
||||
};
|
||||
|
@ -105,6 +108,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
y: 0,
|
||||
width: 2000,
|
||||
height: 100,
|
||||
XPositionReference: XPositionReference.Left,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
},
|
||||
|
@ -123,7 +127,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
width: 0,
|
||||
height: 0,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
isAnchor: false,
|
||||
XPositionReference: XPositionReference.Left
|
||||
},
|
||||
userData: {}
|
||||
}
|
||||
|
@ -140,6 +145,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
XPositionReference: XPositionReference.Left,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
},
|
||||
|
@ -178,6 +184,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
y: 0,
|
||||
width: 2000,
|
||||
height: 100,
|
||||
XPositionReference: XPositionReference.Left,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
},
|
||||
|
@ -194,6 +201,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
XPositionReference: XPositionReference.Left,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { Properties } from '../Properties/Properties';
|
||||
import ContainerProperties from '../../Interfaces/IProperties';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { getDepth, MakeIterator } from '../../utils/itertools';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
|
@ -14,8 +13,8 @@ interface IElementsSidebarProps {
|
|||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
SelectedContainer: IContainerModel | null
|
||||
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void
|
||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
SelectContainer: (container: IContainerModel) => void
|
||||
DeleteContainer: (containerid: string) => void
|
||||
AddContainer: (index: number, type: string, parent: string) => void
|
||||
|
|
48
src/Components/InputGroup/InputGroup.tsx
Normal file
48
src/Components/InputGroup/InputGroup.tsx
Normal 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}
|
||||
/>
|
||||
</>;
|
||||
};
|
146
src/Components/Properties/DynamicForm.tsx
Normal file
146
src/Components/Properties/DynamicForm.tsx
Normal 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;
|
24
src/Components/Properties/Form.tsx
Normal file
24
src/Components/Properties/Form.tsx
Normal 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}
|
||||
/>;
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { expect, describe, it, vi } from 'vitest';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import { Properties } from './Properties';
|
||||
|
||||
|
@ -26,6 +27,7 @@ describe.concurrent('Properties', () => {
|
|||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
XPositionReference: XPositionReference.Left,
|
||||
isRigidBody: false,
|
||||
isAnchor: false
|
||||
};
|
||||
|
@ -62,10 +64,10 @@ describe.concurrent('Properties', () => {
|
|||
fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } });
|
||||
fireEvent.change(propertyX 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.parentId).toBe('parentedId');
|
||||
expect(prop.id).toBe('stuff');
|
||||
expect(prop.parentId).toBe('parentId');
|
||||
expect(prop.x).toBe('2');
|
||||
expect(prop.y).toBe('2');
|
||||
rerender(<Properties
|
||||
|
@ -79,9 +81,9 @@ describe.concurrent('Properties', () => {
|
|||
propertyX = container.querySelector('#x');
|
||||
propertyY = container.querySelector('#y');
|
||||
expect(propertyId).toBeDefined();
|
||||
expect((propertyId as HTMLInputElement).value).toBe('stuffed');
|
||||
expect((propertyId as HTMLInputElement).value).toBe('stuff');
|
||||
expect(propertyParentId).toBeDefined();
|
||||
expect((propertyParentId as HTMLInputElement).value).toBe('parentedId');
|
||||
expect((propertyParentId as HTMLInputElement).value).toBe('parentId');
|
||||
expect(propertyX).toBeDefined();
|
||||
expect((propertyX as HTMLInputElement).value).toBe('2');
|
||||
expect(propertyY).toBeDefined();
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import ContainerProperties from '../../Interfaces/IProperties';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
||||
import { INPUT_TYPES } from './PropertiesInputTypes';
|
||||
import { Form } from './Form';
|
||||
|
||||
interface IPropertiesProps {
|
||||
properties?: ContainerProperties
|
||||
onChange: (key: string, value: string | number | boolean) => void
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void
|
||||
properties?: IProperties
|
||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
}
|
||||
|
||||
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
|
||||
|
@ -16,26 +16,6 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
|||
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 (
|
||||
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
||||
<ToggleButton
|
||||
|
@ -45,72 +25,12 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
|||
checked={isDynamicInput}
|
||||
onChange={() => setIsDynamicInput(!isDynamicInput)}
|
||||
/>
|
||||
{ form }
|
||||
<Form
|
||||
properties={props.properties}
|
||||
isDynamicInput={isDynamicInput}
|
||||
onChange={props.onChange}
|
||||
onSubmit={props.onSubmit}
|
||||
/>
|
||||
</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);
|
||||
};
|
||||
|
|
139
src/Components/Properties/StaticForm.tsx
Normal file
139
src/Components/Properties/StaticForm.tsx
Normal 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;
|
66
src/Components/RadioGroupButtons/RadioGroupButtons.tsx
Normal file
66
src/Components/RadioGroupButtons/RadioGroupButtons.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -36,12 +36,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
|||
// Rect style
|
||||
const style = Object.assign(
|
||||
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
|
||||
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();
|
||||
|
||||
let dimensionChildren: JSX.Element | null = null;
|
||||
if (props.model.children.length > 0) {
|
||||
if (props.model.children.length > 1) {
|
||||
const {
|
||||
childrenId,
|
||||
xChildrenStart,
|
||||
|
@ -112,14 +108,16 @@ function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: numb
|
|||
|
||||
const lastChild = props.model.children[props.model.children.length - 1];
|
||||
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--) {
|
||||
const child = props.model.children[i];
|
||||
const left = child.properties.x;
|
||||
if (left < xChildrenStart) {
|
||||
xChildrenStart = left;
|
||||
}
|
||||
const right = child.properties.x + child.properties.width;
|
||||
const right = child.properties.x;
|
||||
if (right > xChildrenEnd) {
|
||||
xChildrenEnd = right;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import { IHistoryState } from '../../Interfaces/IHistoryState';
|
|||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||
import { Bar } from '../Bar/Bar';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
|
||||
interface IUIProps {
|
||||
current: IHistoryState
|
||||
|
@ -17,8 +16,8 @@ interface IUIProps {
|
|||
AvailableContainers: IAvailableContainer[]
|
||||
SelectContainer: (container: ContainerModel) => void
|
||||
DeleteContainer: (containerId: string) => void
|
||||
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: IProperties) => void
|
||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
AddContainerToSelectedContainer: (type: string) => void
|
||||
AddContainer: (index: number, type: string, parentId: string) => void
|
||||
SaveEditorAsJSON: () => void
|
||||
|
|
6
src/Interfaces/IInputGroup.ts
Normal file
6
src/Interfaces/IInputGroup.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
export interface IInputGroup {
|
||||
text: React.ReactNode
|
||||
value: string
|
||||
}
|
|
@ -10,7 +10,7 @@ import { XPositionReference } from '../Enums/XPositionReference';
|
|||
* @property isRigidBody if true apply rigid body behaviors
|
||||
* @property isAnchor if true apply anchor behaviors
|
||||
*/
|
||||
export default interface IProperties extends Omit<React.CSSProperties, 'width' | 'height'> {
|
||||
export default interface IProperties {
|
||||
id: string
|
||||
parentId: string | null
|
||||
x: number
|
||||
|
@ -19,5 +19,6 @@ export default interface IProperties extends Omit<React.CSSProperties, 'width' |
|
|||
height: number
|
||||
isRigidBody: boolean
|
||||
isAnchor: boolean
|
||||
XPositionReference?: XPositionReference
|
||||
XPositionReference: XPositionReference
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
|
|
@ -46,6 +46,10 @@
|
|||
@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 {
|
||||
@apply absolute w-auto p-2 m-2 min-w-max left-14
|
||||
rounded-md shadow-md
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { XPositionReference } from '../Enums/XPositionReference';
|
||||
import { IConfiguration } from '../Interfaces/IConfiguration';
|
||||
import IProperties from '../Interfaces/IProperties';
|
||||
|
||||
|
@ -34,8 +35,11 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
|
|||
height: Number(DEFAULT_CONFIG.MainContainer.Height),
|
||||
isRigidBody: false,
|
||||
isAnchor: false,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
XPositionReference: XPositionReference.Left,
|
||||
style: {
|
||||
stroke: 'black',
|
||||
fillOpacity: 0
|
||||
}
|
||||
};
|
||||
|
||||
export const DIMENSION_MARGIN = 50;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue