Implement basic form for properties to avoid history pollution

This commit is contained in:
Siklos 2022-08-11 12:39:42 +02:00
parent 7c16d6c97d
commit af73fa6083
8 changed files with 167 additions and 28 deletions

View file

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction } from 'react'; import React, { Dispatch, SetStateAction } from 'react';
import { HistoryState } from '../../Interfaces/HistoryState'; import { HistoryState } from '../../Interfaces/HistoryState';
import { Configuration } from '../../Interfaces/Configuration'; import { Configuration } from '../../Interfaces/Configuration';
import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel';
@ -60,7 +60,7 @@ export function DeleteContainer(
} }
if (container === null || container === undefined) { if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
} }
if (container.parent != null) { if (container.parent != null) {
@ -266,6 +266,76 @@ export function OnPropertyChange(
setHistoryCurrentStep(history.length); setHistoryCurrentStep(history.length);
} }
/**
* Handled the property change event in the properties form
* @param key Property name
* @param value New value of the property
* @returns void
*/
export function OnPropertiesSubmit(
event: React.SyntheticEvent<HTMLFormElement>,
refs: Array<React.RefObject<HTMLInputElement>>,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
event.preventDefault();
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.SelectedContainer === null ||
current.SelectedContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
if (parent === null) {
const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer);
for (const ref of refs) {
const input = ref.current;
if (input instanceof HTMLInputElement) {
(selectedContainerClone.properties as any)[input.id] = input.value;
}
}
setHistory(history.concat([{
LastAction: 'Change property of main',
MainContainer: selectedContainerClone,
SelectedContainer: selectedContainerClone,
SelectedContainerId: selectedContainerClone.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
return;
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
for (const ref of refs) {
const input = ref.current;
if (input instanceof HTMLInputElement) {
(container.properties as any)[input.id] = input.value;
}
}
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
setHistory(history.concat([{
LastAction: `Change property of container ${container.properties.id}`,
MainContainer: mainContainerClone,
SelectedContainer: container,
SelectedContainerId: container.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
}
// TODO put this in a different file // TODO put this in a different file
export function RecalculatePhysics(container: IContainerModel): IContainerModel { export function RecalculatePhysics(container: IContainerModel): IContainerModel {

View file

@ -4,7 +4,7 @@ import { Configuration } from '../../Interfaces/Configuration';
import { SVG } from '../SVG/SVG'; import { SVG } from '../SVG/SVG';
import { HistoryState } from '../../Interfaces/HistoryState'; import { HistoryState } from '../../Interfaces/HistoryState';
import { UI } from '../UI/UI'; import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer, OnPropertiesSubmit } from './ContainerOperations';
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save';
import { onKeyDown } from './Shortcuts'; import { onKeyDown } from './Shortcuts';
@ -72,6 +72,14 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory, setHistory,
setHistoryCurrentStep setHistoryCurrentStep
)} )}
OnPropertiesSubmit={(event, refs) => OnPropertiesSubmit(
event,
refs,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
type, type,
configuration, configuration,

View file

@ -14,6 +14,7 @@ interface IElementsSidebarProps {
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) => void
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, refs: Array<React.RefObject<HTMLInputElement>>) => 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
@ -145,7 +146,11 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
props.DeleteContainer(onClickContainerId); props.DeleteContainer(onClickContainerId);
}} /> }} />
</Menu> </Menu>
<Properties properties={props.SelectedContainer?.properties} onChange={props.OnPropertyChange}></Properties> <Properties
properties={props.SelectedContainer?.properties}
onChange={props.OnPropertyChange}
onSubmit={props.OnPropertiesSubmit}
/>
</div> </div>
); );
}; };

View file

@ -38,13 +38,8 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
</form> </form>
<button <button
onClick={() => setWindowState(WindowState.MAIN)} onClick={() => setWindowState(WindowState.MAIN)}
className='block text-sm className='normal-btn block
mt-8 py-2 px-4 mt-8 '
rounded-full border-0
font-semibold
transition-all
bg-blue-100 text-blue-700
hover:bg-blue-200'
> >
Go back Go back
</button> </button>

View file

@ -1,10 +1,11 @@
import * as React from 'react'; import React, { useState } from 'react';
import ContainerProperties from '../../Interfaces/Properties'; import ContainerProperties from '../../Interfaces/Properties';
import { INPUT_TYPES } from './PropertiesInputTypes'; import { INPUT_TYPES } from './PropertiesInputTypes';
interface IPropertiesProps { interface IPropertiesProps {
properties?: ContainerProperties properties?: ContainerProperties
onChange: (key: string, value: string | number | boolean) => void onChange: (key: string, value: string | number | boolean) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>, refs: Array<React.RefObject<HTMLInputElement>>) => void
} }
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => { export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
@ -12,14 +13,35 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
return <div></div>; return <div></div>;
} }
const [isDynamicInput, setIsDynamicInput] = useState<boolean>(true);
const groupInput: React.ReactNode[] = []; const groupInput: React.ReactNode[] = [];
const refs: Array<React.RefObject<HTMLInputElement>> = [];
Object Object
.entries(props.properties) .entries(props.properties)
.forEach((pair) => handleProperties(pair, groupInput, props.onChange)); .forEach((pair) => handleProperties(pair, groupInput, refs, isDynamicInput, props.onChange));
const form = isDynamicInput
? <div>
{ groupInput }
</div>
: <form
key={props.properties.id}
onSubmit={(event) => props.onSubmit(event, refs)}
>
<input type='submit' className='normal-btn' value='Submit'/>
{ groupInput }
</form>
;
return ( return (
<div className='p-3 bg-slate-200 h-3/5 overflow-y-auto'> <div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
{ groupInput } <input
type='checkbox'
onChange={() => { setIsDynamicInput(!isDynamicInput); }}
checked={isDynamicInput}
/>
{ form }
</div> </div>
); );
}; };
@ -27,6 +49,8 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
const handleProperties = ( const handleProperties = (
[key, value]: [string, string | number], [key, value]: [string, string | number],
groupInput: React.ReactNode[], groupInput: React.ReactNode[],
refs: Array<React.RefObject<HTMLInputElement>>,
isDynamicInput: boolean,
onChange: (key: string, value: string | number | boolean) => void onChange: (key: string, value: string | number | boolean) => void
): void => { ): void => {
const id = `property-${key}`; const id = `property-${key}`;
@ -42,9 +66,40 @@ const handleProperties = (
type = INPUT_TYPES[key]; type = INPUT_TYPES[key];
} }
const isDisabled = ['id', 'parentId'].includes(key); const ref: React.RefObject<HTMLInputElement> = React.useRef<HTMLInputElement>(null);
/// refs.push(ref);
const isDisabled = ['id', 'parentId'].includes(key);
if (isDynamicInput) {
groupInput.push(
<div key={id} className='mt-4'>
<label className='text-sm font-medium text-gray-800' htmlFor={id}>{key}</label>
<input
className='text-base font-medium transition-all text-gray-800 mt-1 block w-full 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
'
type={type}
id={id}
ref={ref}
value={value}
checked={checked}
onChange={(event) => {
if (type === 'checkbox') {
onChange(key, event.target.checked);
return;
}
onChange(key, event.target.value);
}}
disabled={isDisabled}
/>
</div>
);
return;
}
///
groupInput.push( groupInput.push(
<div key={id} className='mt-4'> <div key={id} className='mt-4'>
<label className='text-sm font-medium text-gray-800' htmlFor={id}>{key}</label> <label className='text-sm font-medium text-gray-800' htmlFor={id}>{key}</label>
@ -55,16 +110,10 @@ const handleProperties = (
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none
' '
type={type} type={type}
id={id} id={key}
value={value} ref={ref}
checked={checked} defaultValue={value}
onChange={(event) => { defaultChecked={checked}
if (type === 'checkbox') {
onChange(key, event.target.checked);
return;
}
onChange(key, event.target.value);
}}
disabled={isDisabled} disabled={isDisabled}
/> />
</div> </div>

View file

@ -17,6 +17,7 @@ interface IUIProps {
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) => void
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, refs: Array<React.RefObject<HTMLInputElement>>) => 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
@ -59,6 +60,7 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
isOpen={isElementsSidebarOpen} isOpen={isElementsSidebarOpen}
isHistoryOpen={isHistoryOpen} isHistoryOpen={isHistoryOpen}
OnPropertyChange={props.OnPropertyChange} OnPropertyChange={props.OnPropertyChange}
OnPropertiesSubmit={props.OnPropertiesSubmit}
SelectContainer={props.SelectContainer} SelectContainer={props.SelectContainer}
DeleteContainer={props.DeleteContainer} DeleteContainer={props.DeleteContainer}
AddContainer={props.AddContainer} AddContainer={props.AddContainer}

View file

@ -23,6 +23,16 @@
@apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg
} }
.normal-btn {
@apply text-sm
py-2 px-4
rounded-full border-0
font-semibold
transition-all
bg-blue-100 text-blue-700
hover:bg-blue-200
}
.floating-btn { .floating-btn {
@apply h-full w-full text-white align-middle items-center justify-center @apply h-full w-full text-white align-middle items-center justify-center
} }

View file

@ -30,9 +30,9 @@ export const DEFAULT_MAINCONTAINER_PROPS: Properties = {
parentId: 'null', parentId: 'null',
x: 0, x: 0,
y: 0, y: 0,
isRigidBody: false,
width: DEFAULT_CONFIG.MainContainer.Width, width: DEFAULT_CONFIG.MainContainer.Width,
height: DEFAULT_CONFIG.MainContainer.Height, height: DEFAULT_CONFIG.MainContainer.Height,
isRigidBody: false,
fillOpacity: 0, fillOpacity: 0,
stroke: 'black' stroke: 'black'
}; };