From ac56f8419687b14520bffb35da4b54330667e011 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 08:37:10 -0400 Subject: [PATCH] Add option for the properties form to only update on submit (#23) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/23 --- src/Components/Editor/ContainerOperations.ts | 74 ++++++++++++++- src/Components/Editor/Editor.tsx | 10 +- .../ElementsSidebar/ElementsSidebar.test.tsx | 21 +++-- .../ElementsSidebar/ElementsSidebar.tsx | 7 +- src/Components/MainMenu/MainMenu.tsx | 9 +- src/Components/Properties/Properties.test.tsx | 21 +++-- src/Components/Properties/Properties.tsx | 93 ++++++++++++++----- src/Components/ToggleButton/ToggleButton.scss | 8 ++ src/Components/ToggleButton/ToggleButton.tsx | 52 +++++++++++ src/Components/UI/UI.tsx | 2 + src/index.scss | 10 ++ src/utils/default.ts | 2 +- 12 files changed, 256 insertions(+), 53 deletions(-) create mode 100644 src/Components/ToggleButton/ToggleButton.scss create mode 100644 src/Components/ToggleButton/ToggleButton.tsx 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.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index c1372b1..fac312b 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={() => {}} @@ -70,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(''); @@ -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={() => {}} @@ -212,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(''); @@ -225,6 +229,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} 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.test.tsx b/src/Components/Properties/Properties.test.tsx index 9f0fe13..2afc364 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(); @@ -16,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', @@ -32,6 +33,7 @@ describe.concurrent('Properties', () => { const { container, rerender } = render( {}} />); expect(screen.queryByText('id')).toBeDefined(); @@ -39,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(); @@ -65,12 +67,13 @@ describe.concurrent('Properties', () => { rerender( {}} />); - 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 c1ac966..7477542 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,25 +1,50 @@ -import * as React from 'react'; +import React, { useState } from 'react'; import ContainerProperties from '../../Interfaces/Properties'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; 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) => { + const [isDynamicInput, setIsDynamicInput] = useState(true); + if (props.properties === undefined) { return
; } 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)} + /> + { form }
); }; @@ -27,6 +52,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,31 +69,49 @@ const handleProperties = ( type = INPUT_TYPES[key]; } - const isDisabled = ['id', 'parentId'].includes(key); - /// + const ref: React.RefObject = React.useRef(null); + refs.push(ref); - groupInput.push( -
- - { - if (type === 'checkbox') { - onChange(key, event.target.checked); - return; - } - onChange(key, event.target.value); - }} - disabled={isDisabled} - /> + 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 ( +
+
+ +
+
+ ); +}; 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' };