From af73fa60832d878f2eec0281a8ebcb8ac91e7820 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 12:39:42 +0200 Subject: [PATCH 1/5] Implement basic form for properties to avoid history pollution --- src/Components/Editor/ContainerOperations.ts | 74 ++++++++++++++++- src/Components/Editor/Editor.tsx | 10 ++- .../ElementsSidebar/ElementsSidebar.tsx | 7 +- src/Components/MainMenu/MainMenu.tsx | 9 +-- src/Components/Properties/Properties.tsx | 81 +++++++++++++++---- src/Components/UI/UI.tsx | 2 + src/index.scss | 10 +++ src/utils/default.ts | 2 +- 8 files changed, 167 insertions(+), 28 deletions(-) diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index ea4db42..8ad34ec 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import { HistoryState } from '../../Interfaces/HistoryState'; import { Configuration } from '../../Interfaces/Configuration'; import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; @@ -60,7 +60,7 @@ export function DeleteContainer( } 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) { @@ -266,6 +266,76 @@ export function OnPropertyChange( 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, + refs: Array>, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): 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 export function RecalculatePhysics(container: IContainerModel): IContainerModel { diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index ad27c22..b008607 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -4,7 +4,7 @@ import { Configuration } from '../../Interfaces/Configuration'; import { SVG } from '../SVG/SVG'; import { HistoryState } from '../../Interfaces/HistoryState'; 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 { onKeyDown } from './Shortcuts'; @@ -72,6 +72,14 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} + OnPropertiesSubmit={(event, refs) => OnPropertiesSubmit( + event, + refs, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( type, configuration, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 873161d..96be3fe 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -14,6 +14,7 @@ interface IElementsSidebarProps { isHistoryOpen: boolean SelectedContainer: IContainerModel | null OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, refs: Array>) => void SelectContainer: (container: IContainerModel) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void @@ -145,7 +146,11 @@ export const ElementsSidebar: React.FC = (props: IElement props.DeleteContainer(onClickContainerId); }} /> - + ); }; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index 1d84f9a..9c8b27f 100644 --- a/src/Components/MainMenu/MainMenu.tsx +++ b/src/Components/MainMenu/MainMenu.tsx @@ -38,13 +38,8 @@ export const MainMenu: React.FC = (props) => { diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index c1ac966..9c90705 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,10 +1,11 @@ -import * as React from 'react'; +import React, { useState } from 'react'; import ContainerProperties from '../../Interfaces/Properties'; import { INPUT_TYPES } from './PropertiesInputTypes'; interface IPropertiesProps { properties?: ContainerProperties onChange: (key: string, value: string | number | boolean) => void + onSubmit: (event: React.FormEvent, refs: Array>) => void } export const Properties: React.FC = (props: IPropertiesProps) => { @@ -12,14 +13,35 @@ export const Properties: React.FC = (props: IPropertiesProps) return
; } + const [isDynamicInput, setIsDynamicInput] = useState(true); + const groupInput: React.ReactNode[] = []; + const refs: Array> = []; Object .entries(props.properties) - .forEach((pair) => handleProperties(pair, groupInput, props.onChange)); + .forEach((pair) => handleProperties(pair, groupInput, refs, isDynamicInput, props.onChange)); + + const form = isDynamicInput + ?
+ { groupInput } +
+ :
props.onSubmit(event, refs)} + > + + { groupInput } +
+ ; return ( -
- { groupInput } +
+ { setIsDynamicInput(!isDynamicInput); }} + checked={isDynamicInput} + /> + { form }
); }; @@ -27,6 +49,8 @@ export const Properties: React.FC = (props: IPropertiesProps) const handleProperties = ( [key, value]: [string, string | number], groupInput: React.ReactNode[], + refs: Array>, + isDynamicInput: boolean, onChange: (key: string, value: string | number | boolean) => void ): void => { const id = `property-${key}`; @@ -42,9 +66,40 @@ const handleProperties = ( type = INPUT_TYPES[key]; } - const isDisabled = ['id', 'parentId'].includes(key); - /// + const ref: React.RefObject = React.useRef(null); + refs.push(ref); + const isDisabled = ['id', 'parentId'].includes(key); + if (isDynamicInput) { + groupInput.push( +
+ + { + if (type === 'checkbox') { + onChange(key, event.target.checked); + return; + } + onChange(key, event.target.value); + }} + disabled={isDisabled} + /> +
+ ); + return; + } + + /// groupInput.push(
@@ -55,16 +110,10 @@ const handleProperties = ( disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none ' type={type} - id={id} - value={value} - checked={checked} - onChange={(event) => { - if (type === 'checkbox') { - onChange(key, event.target.checked); - return; - } - onChange(key, event.target.value); - }} + id={key} + ref={ref} + defaultValue={value} + defaultChecked={checked} disabled={isDisabled} />
diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 6d6adbe..5c7012e 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -17,6 +17,7 @@ interface IUIProps { SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, refs: Array>) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void @@ -59,6 +60,7 @@ export const UI: React.FunctionComponent = (props: IUIProps) => { isOpen={isElementsSidebarOpen} isHistoryOpen={isHistoryOpen} OnPropertyChange={props.OnPropertyChange} + OnPropertiesSubmit={props.OnPropertiesSubmit} SelectContainer={props.SelectContainer} DeleteContainer={props.DeleteContainer} AddContainer={props.AddContainer} diff --git a/src/index.scss b/src/index.scss index 1bc3361..f653990 100644 --- a/src/index.scss +++ b/src/index.scss @@ -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 } + .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 { @apply h-full w-full text-white align-middle items-center justify-center } diff --git a/src/utils/default.ts b/src/utils/default.ts index 552b8a0..a117ef0 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -30,9 +30,9 @@ export const DEFAULT_MAINCONTAINER_PROPS: Properties = { parentId: 'null', x: 0, y: 0, - isRigidBody: false, width: DEFAULT_CONFIG.MainContainer.Width, height: DEFAULT_CONFIG.MainContainer.Height, + isRigidBody: false, fillOpacity: 0, stroke: 'black' }; -- 2.47.2 From 6ae8322144baba12aea6f2433991cfe8565f68e1 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 13:20:57 +0200 Subject: [PATCH 2/5] Add toggle button to disable / enable dynamic update --- src/Components/Properties/Properties.tsx | 84 +++++++++---------- src/Components/ToggleButton/ToggleButton.scss | 8 ++ src/Components/ToggleButton/ToggleButton.tsx | 52 ++++++++++++ 3 files changed, 100 insertions(+), 44 deletions(-) create mode 100644 src/Components/ToggleButton/ToggleButton.scss create mode 100644 src/Components/ToggleButton/ToggleButton.tsx diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 9c90705..cf87e5b 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import ContainerProperties from '../../Interfaces/Properties'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; import { INPUT_TYPES } from './PropertiesInputTypes'; interface IPropertiesProps { @@ -9,12 +10,12 @@ interface IPropertiesProps { } export const Properties: React.FC = (props: IPropertiesProps) => { + const [isDynamicInput, setIsDynamicInput] = useState(false); + if (props.properties === undefined) { return
; } - const [isDynamicInput, setIsDynamicInput] = useState(true); - const groupInput: React.ReactNode[] = []; const refs: Array> = []; Object @@ -36,10 +37,12 @@ export const Properties: React.FC = (props: IPropertiesProps) return (
- { setIsDynamicInput(!isDynamicInput); }} + setIsDynamicInput(!isDynamicInput)} /> { form }
@@ -70,52 +73,45 @@ const handleProperties = ( refs.push(ref); const isDisabled = ['id', 'parentId'].includes(key); - if (isDynamicInput) { - groupInput.push( -
- - { - if (type === 'checkbox') { - onChange(key, event.target.checked); - return; - } - onChange(key, event.target.value); - }} - disabled={isDisabled} - /> -
- ); - return; - } + type={type} + id={key} + ref={ref} + value={value} + checked={checked} + onChange={(event) => { + if (type === 'checkbox') { + onChange(key, event.target.checked); + return; + } + onChange(key, event.target.value); + }} + disabled={isDisabled} + /> + : ; - /// groupInput.push(
- - + + {input}
); }; diff --git a/src/Components/ToggleButton/ToggleButton.scss b/src/Components/ToggleButton/ToggleButton.scss new file mode 100644 index 0000000..0948f52 --- /dev/null +++ b/src/Components/ToggleButton/ToggleButton.scss @@ -0,0 +1,8 @@ +input:checked ~ .dot { + transform: translateX(100%); +} +input:checked ~ .line { + background-color: #3B82F6; +} + + diff --git a/src/Components/ToggleButton/ToggleButton.tsx b/src/Components/ToggleButton/ToggleButton.tsx new file mode 100644 index 0000000..198bf99 --- /dev/null +++ b/src/Components/ToggleButton/ToggleButton.tsx @@ -0,0 +1,52 @@ +import React, { FC } from 'react'; +import './ToggleButton.scss'; + +interface IToggleButtonProps { + id: string + text: string + type?: TOGGLE_TYPE + title: string + checked: boolean + onChange: React.ChangeEventHandler +} + +export enum TOGGLE_TYPE { + MATERIAL, + IOS +} + +export const ToggleButton: FC = (props) => { + const id = `toggle-${props.id}`; + const type = props.type ?? TOGGLE_TYPE.MATERIAL; + let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner'; + let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition'; + if (type === TOGGLE_TYPE.IOS) { + classLine = 'line block bg-gray-600 w-14 h-8 rounded-full'; + classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition'; + } + + return ( +
+
+ +
+
+ ); +}; -- 2.47.2 From 6e2d7299616dfe33030b19cae22248f08bf0467a Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 14:26:41 +0200 Subject: [PATCH 3/5] Center submit button --- src/Components/Properties/Properties.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index cf87e5b..39a6411 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -30,7 +30,7 @@ export const Properties: React.FC = (props: IPropertiesProps) key={props.properties.id} onSubmit={(event) => props.onSubmit(event, refs)} > - + { groupInput } ; -- 2.47.2 From 3d710f6f7ea17353cfacb3f13f43ff301de4812a Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 14:29:15 +0200 Subject: [PATCH 4/5] Fix tests not updated --- src/Components/ElementsSidebar/ElementsSidebar.test.tsx | 5 +++++ src/Components/Properties/Properties.test.tsx | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index c1372b1..927e70c 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -25,6 +25,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -57,6 +58,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -146,6 +148,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -202,6 +205,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} @@ -225,6 +229,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 9f0fe13..55fa718 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -8,6 +8,7 @@ describe.concurrent('Properties', () => { render( {}} + onSubmit={() => {}} />); expect(screen.queryByText('id')).toBeNull(); @@ -32,6 +33,7 @@ describe.concurrent('Properties', () => { const { container, rerender } = render( {}} />); expect(screen.queryByText('id')).toBeDefined(); @@ -65,6 +67,7 @@ describe.concurrent('Properties', () => { rerender( {}} />); propertyId = container.querySelector('#property-id'); -- 2.47.2 From d07353d02e95e542b7075ab4d9fbf595d4f2ba07 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 14:36:25 +0200 Subject: [PATCH 5/5] Fix regression + fix tests --- .../ElementsSidebar/ElementsSidebar.test.tsx | 16 ++++++++-------- src/Components/Properties/Properties.test.tsx | 18 +++++++++--------- src/Components/Properties/Properties.tsx | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 927e70c..fac312b 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -72,12 +72,12 @@ describe.concurrent('Elements sidebar', () => { expect(screen.queryByText('y')).toBeDefined(); expect(screen.queryByText('width')).toBeDefined(); expect(screen.queryByText('height')).toBeDefined(); - const propertyId = container.querySelector('#property-id'); - const propertyParentId = container.querySelector('#property-parentId'); - const propertyX = container.querySelector('#property-x'); - const propertyY = container.querySelector('#property-y'); - const propertyWidth = container.querySelector('#property-width'); - const propertyHeight = container.querySelector('#property-height'); + const propertyId = container.querySelector('#id'); + const propertyParentId = container.querySelector('#parentId'); + const propertyX = container.querySelector('#x'); + const propertyY = container.querySelector('#y'); + const propertyWidth = container.querySelector('#width'); + const propertyHeight = container.querySelector('#height'); expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect(propertyParentId).toBeDefined(); expect((propertyParentId as HTMLInputElement).value).toBe(''); @@ -216,8 +216,8 @@ describe.concurrent('Elements sidebar', () => { expect(screen.getByText(/main/i)); const child1 = screen.getByText(/child-1/i); expect(child1); - const propertyId = container.querySelector('#property-id'); - const propertyParentId = container.querySelector('#property-parentId'); + const propertyId = container.querySelector('#id'); + const propertyParentId = container.querySelector('#parentId'); expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect((propertyParentId as HTMLInputElement).value).toBe(''); diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 55fa718..2afc364 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -17,7 +17,7 @@ describe.concurrent('Properties', () => { expect(screen.queryByText('y')).toBeNull(); }); - it('Some properties', () => { + it('Some properties, change values with dynamic input', () => { const prop = { id: 'stuff', parentId: 'parentId', @@ -41,10 +41,10 @@ describe.concurrent('Properties', () => { expect(screen.queryByText('x')).toBeDefined(); expect(screen.queryByText('y')).toBeDefined(); - let propertyId = container.querySelector('#property-id'); - let propertyParentId = container.querySelector('#property-parentId'); - let propertyX = container.querySelector('#property-x'); - let propertyY = container.querySelector('#property-y'); + let propertyId = container.querySelector('#id'); + let propertyParentId = container.querySelector('#parentId'); + let propertyX = container.querySelector('#x'); + let propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); expect((propertyId as HTMLInputElement).value).toBe('stuff'); expect(propertyParentId).toBeDefined(); @@ -70,10 +70,10 @@ describe.concurrent('Properties', () => { onSubmit={() => {}} />); - propertyId = container.querySelector('#property-id'); - propertyParentId = container.querySelector('#property-parentId'); - propertyX = container.querySelector('#property-x'); - propertyY = container.querySelector('#property-y'); + propertyId = container.querySelector('#id'); + propertyParentId = container.querySelector('#parentId'); + propertyX = container.querySelector('#x'); + propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); expect((propertyId as HTMLInputElement).value).toBe('stuffed'); expect(propertyParentId).toBeDefined(); diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 39a6411..7477542 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -10,7 +10,7 @@ interface IPropertiesProps { } export const Properties: React.FC = (props: IPropertiesProps) => { - const [isDynamicInput, setIsDynamicInput] = useState(false); + const [isDynamicInput, setIsDynamicInput] = useState(true); if (props.properties === undefined) { return
; -- 2.47.2