diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index 8dec2a1..1e37328 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -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>, @@ -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' + } } ); diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index 0ab670d..95a0889 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -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 diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index fcd62f9..75bf90c 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -91,16 +91,15 @@ const Editor: React.FunctionComponent = (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, diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts index 4bd4c2d..9f35e63 100644 --- a/src/Components/Editor/PropertiesOperations.ts +++ b/src/Components/Editor/PropertiesOperations.ts @@ -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>, @@ -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, - properties: IProperties, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -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); } diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 8d34480..f207a46 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -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 }, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index ab3efdd..98eb071 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -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, properties: ContainerProperties) => void + OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + OnPropertiesSubmit: (event: React.FormEvent) => void SelectContainer: (container: IContainerModel) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void diff --git a/src/Components/InputGroup/InputGroup.tsx b/src/Components/InputGroup/InputGroup.tsx new file mode 100644 index 0000000..bc794d3 --- /dev/null +++ b/src/Components/InputGroup/InputGroup.tsx @@ -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) => 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 = (props) => { + return <> + + + ; +}; diff --git a/src/Components/Properties/DynamicForm.tsx b/src/Components/Properties/DynamicForm.tsx new file mode 100644 index 0000000..0be155d --- /dev/null +++ b/src/Components/Properties/DynamicForm.tsx @@ -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( onChange(key, event.target.value, true)} + />); + } + return groupInput; +}; + +const DynamicForm: React.FunctionComponent = (props) => { + return ( +
+ + + props.onChange('x', event.target.value)} + /> + props.onChange('y', event.target.value)} + /> + props.onChange('width', event.target.value)} + /> + props.onChange('height', event.target.value)} + /> + props.onChange('isRigidBody', event.target.checked)} + /> + props.onChange('isAnchor', event.target.checked)} + /> + + +
+ ), + value: XPositionReference.Left.toString() + }, + { + text: ( +
+ +
+ ), + value: XPositionReference.Center.toString() + }, + { + text: ( +
+ +
+ ), + value: XPositionReference.Right.toString() + } + ]} + onChange={(event) => props.onChange('XPositionReference', event.target.value)} + /> + { getCSSInputs(props.properties, props.onChange) } + + ); +}; + +export default DynamicForm; diff --git a/src/Components/Properties/Form.tsx b/src/Components/Properties/Form.tsx new file mode 100644 index 0000000..bc0f508 --- /dev/null +++ b/src/Components/Properties/Form.tsx @@ -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) => void +} + +export const Form: React.FunctionComponent = (props) => { + if (props.isDynamicInput) { + return ; + } + return ; +}; diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 8b8db31..61441f8 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -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( { 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(); diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 516f23d..cb38a60 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -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, properties: ContainerProperties) => void + properties?: IProperties + onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + onSubmit: (event: React.FormEvent) => void } export const Properties: React.FC = (props: IPropertiesProps) => { @@ -16,26 +16,6 @@ export const Properties: React.FC = (props: IPropertiesProps) return
; } - const groupInput: React.ReactNode[] = []; - Object - .entries(props.properties) - .forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange)); - - const form = isDynamicInput - ?
- { groupInput } -
- :
props.onSubmit(event, props.properties as ContainerProperties)} - > - -
- { groupInput } -
-
- ; - return (
= (props: IPropertiesProps) checked={isDynamicInput} onChange={() => setIsDynamicInput(!isDynamicInput)} /> - { form } +
); }; - -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 - ? { - if (type === 'checkbox') { - onChange(key, event.target.checked); - return; - } - onChange(key, event.target.value); - }} - disabled={isDisabled} - /> - : ; - - groupInput.push( - - ); - groupInput.push(input); -}; diff --git a/src/Components/Properties/StaticForm.tsx b/src/Components/Properties/StaticForm.tsx new file mode 100644 index 0000000..f16d196 --- /dev/null +++ b/src/Components/Properties/StaticForm.tsx @@ -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) => void +} + +const getCSSInputs = (properties: IProperties): JSX.Element[] => { + const groupInput: JSX.Element[] = []; + for (const key in properties.style) { + groupInput.push(); + } + return groupInput; +}; + +const StaticForm: React.FunctionComponent = (props) => { + return ( props.onSubmit(event)} + > + +
+ + + + + + + + + + +
+ ), + value: XPositionReference.Left.toString() + }, + { + text: ( +
+ +
+ ), + value: XPositionReference.Center.toString() + }, + { + text: ( +
+ +
+ ), + value: XPositionReference.Right.toString() + } + ]} + /> + { getCSSInputs(props.properties) } + + ); +}; + +export default StaticForm; diff --git a/src/Components/RadioGroupButtons/RadioGroupButtons.tsx b/src/Components/RadioGroupButtons/RadioGroupButtons.tsx new file mode 100644 index 0000000..8fb620b --- /dev/null +++ b/src/Components/RadioGroupButtons/RadioGroupButtons.tsx @@ -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) => void +} + +export const RadioGroupButtons: React.FunctionComponent = (props) => { + let inputGroups; + if (props.value !== undefined) { + // dynamic + inputGroups = props.inputGroups.map((inputGroup) => ( +
+ + +
+ + )); + } else { + // static + inputGroups = props.inputGroups.map((inputGroup) => ( +
+ + +
+ )); + } + + return ( + <> + +
+ { inputGroups } +
+ + ); +}; diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 56d4797..80ae56c 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -36,12 +36,8 @@ export const Container: React.FC = (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 = (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; } diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 4fa36e6..c57daee 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -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, properties: IProperties) => void + OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + OnPropertiesSubmit: (event: React.FormEvent) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void diff --git a/src/Interfaces/IInputGroup.ts b/src/Interfaces/IInputGroup.ts new file mode 100644 index 0000000..dfc9942 --- /dev/null +++ b/src/Interfaces/IInputGroup.ts @@ -0,0 +1,6 @@ +import React from 'react'; + +export interface IInputGroup { + text: React.ReactNode + value: string +} diff --git a/src/Interfaces/IProperties.ts b/src/Interfaces/IProperties.ts index ad23442..2513d74 100644 --- a/src/Interfaces/IProperties.ts +++ b/src/Interfaces/IProperties.ts @@ -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 { +export default interface IProperties { id: string parentId: string | null x: number @@ -19,5 +19,6 @@ export default interface IProperties extends Omit