diff --git a/src/Components/ContainerProperties/ContainerProperties.test.tsx b/src/Components/ContainerProperties/ContainerProperties.test.tsx index d02686c..cd2da15 100644 --- a/src/Components/ContainerProperties/ContainerProperties.test.tsx +++ b/src/Components/ContainerProperties/ContainerProperties.test.tsx @@ -4,11 +4,11 @@ import { expect, describe, it, vi } from 'vitest'; import { PositionReference } from '../../Enums/PositionReference'; import { IContainerProperties } from '../../Interfaces/IContainerProperties'; import { Orientation } from '../../Enums/Orientation'; -import { Properties } from './ContainerProperties'; +import { ContainerProperties } from './ContainerProperties'; describe.concurrent('Properties', () => { it('No properties', () => { - render( {}} symbols={new Map()} @@ -67,7 +67,7 @@ describe.concurrent('Properties', () => { (prop as any)[key] = value; }); - const { container, rerender } = render( { expect(prop.parentId).toBe('parentId'); expect(prop.x).toBe(2); expect(prop.y).toBe(2); - rerender( void } -export function Properties(props: IPropertiesProps): JSX.Element { +export function ContainerProperties(props: IPropertiesProps): JSX.Element { if (props.properties === undefined) { return
; } diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index 131df82..82162e3 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -36,6 +36,30 @@ export function SelectContainer( return history; } +/** + * Deselect a container + * @returns New history + */ +export function DeselectContainer( + containerId: string, + fullHistory: IHistoryState[], + historyCurrentStep: number +): IHistoryState[] { + const history = GetCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + history.push({ + lastAction: `Deselect ${containerId}`, + mainContainer: current.mainContainer, + containers: structuredClone(current.containers), + selectedContainerId: 'undefined', + typeCounters: Object.assign({}, current.typeCounters), + symbols: structuredClone(current.symbols), + selectedSymbolId: current.selectedSymbolId + }); + return history; +} + /** * Delete a container * @param containerId containerId of the container to delete @@ -115,7 +139,7 @@ export function DeleteContainer( * If the selected container is removed, select the sibling after, * If there is no sibling, select the parent, * - * @param mainContainerClone Main container + * @param containers * @param selectedContainerId Current selected container * @param parent Parent of the selected/deleted container * @param index Index of the selected/deleted container @@ -127,11 +151,10 @@ function GetSelectedContainerOnDelete( parent: IContainerModel, index: number ): string { - const newSelectedContainerId = FindContainerById(containers, selectedContainerId)?.properties.id ?? - parent.children.at(index) ?? - parent.children.at(index - 1) ?? - parent.properties.id; - return newSelectedContainerId; + return FindContainerById(containers, selectedContainerId)?.properties.id ?? + parent.children.at(index) ?? + parent.children.at(index - 1) ?? + parent.properties.id; } /** @@ -157,11 +180,15 @@ function UnlinkContainerFromSymbols( } /** - * Handled the property change event in the properties form - * @param key Property name - * @param value New value of the property - * @returns void - */ + * Handled the property change event in the properties form + * @param key Property name + * @param value New value of the property + * @param type + * @param selected + * @param fullHistory + * @param historyCurrentStep + * @returns void + */ export function OnPropertyChange( key: string, value: string | number | boolean | number[], @@ -201,6 +228,7 @@ export function OnPropertyChange( /** * Sort the parent children by x + * @param containers * @param parent The clone used for the sort * @returns void */ @@ -265,6 +293,7 @@ export function SortChildren( /** * Set the container with properties and behaviors (mutate) + * @param containers * @param container Container to update * @param key Key of the property to update * @param value Value of the property to update @@ -371,9 +400,8 @@ function AssignProperty(container: IContainerModel, key: string, value: string | /** * Link a symbol to a container * @param containerId Container id - * @param oldSymbolId Old Symbol id - * @param newSymbolId New Symbol id - * @param symbols Current list of symbols + * @param oldSymbol + * @param newSymbol * @returns */ export function LinkSymbol( diff --git a/src/Components/ElementsList/ElementsList.tsx b/src/Components/ElementsList/ElementsSideBar.tsx similarity index 87% rename from src/Components/ElementsList/ElementsList.tsx rename to src/Components/ElementsList/ElementsSideBar.tsx index f068a2c..78d9b6b 100644 --- a/src/Components/ElementsList/ElementsList.tsx +++ b/src/Components/ElementsList/ElementsSideBar.tsx @@ -1,14 +1,17 @@ import * as React from 'react'; +import { useState } from 'react'; import useSize from '@react-hook/size'; import { FixedSizeList as List } from 'react-window'; -import { Properties } from '../ContainerProperties/ContainerProperties'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { ContainerProperties } from '../ContainerProperties/ContainerProperties'; import { IContainerModel } from '../../Interfaces/IContainerModel'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { PropertyType } from '../../Enums/PropertyType'; -import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar'; +import { Text } from '../Text/Text'; -interface IElementsListProps { +interface IElementsSideBarProps { containers: Map mainContainer: IContainerModel symbols: Map @@ -20,6 +23,8 @@ interface IElementsListProps { ) => void selectContainer: (containerId: string) => void addContainer: (index: number, type: string, parent: string) => void + isExpanded: boolean + onExpandChange: () => void } function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void { @@ -119,10 +124,11 @@ function HandleOnDrop( } } -export function ElementsList(props: IElementsListProps): JSX.Element { +export function ElementsSideBar(props: IElementsSideBarProps): JSX.Element { // States const divRef = React.useRef(null); - const [, height] = useSize(divRef); + const [,height] = useSize(divRef); + const [showProperties, setShowProperties] = useState(props.isExpanded); // Render const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true); @@ -160,10 +166,21 @@ export function ElementsList(props: IElementsListProps): JSX.Element { } return ( -
-
+
+ {showProperties && +
+ +
+ } +
+
+ { setShowProperties(newValue); props.onExpandChange(); }} /> +
-
- -
); } diff --git a/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.scss b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.scss new file mode 100644 index 0000000..50ba0e3 --- /dev/null +++ b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.scss @@ -0,0 +1,4 @@ +.text-vertical{ + text-align: right; + writing-mode: vertical-rl; +} \ No newline at end of file diff --git a/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx new file mode 100644 index 0000000..61a35d9 --- /dev/null +++ b/src/Components/Sidebar/ToggleSideBar/ToggleSideBar.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import './ToggleSideBar.scss'; + +interface IToggleSidebarProps { + title: string + checked: boolean + onChange: (newValue: boolean) => void +} + +export function ToggleSideBar({ title, checked, onChange }: IToggleSidebarProps): JSX.Element { + return ( +
+ +
+ ); +} diff --git a/src/Components/SymbolProperties/SymbolProperties.tsx b/src/Components/SymbolProperties/SymbolProperties.tsx index 419e235..02ff5ab 100644 --- a/src/Components/SymbolProperties/SymbolProperties.tsx +++ b/src/Components/SymbolProperties/SymbolProperties.tsx @@ -14,7 +14,7 @@ export function SymbolProperties(props: ISymbolPropertiesProps): JSX.Element { } return ( -
+
onPropertyChange: (key: string, value: string | number | boolean) => void selectSymbol: (symbolId: string) => void + isExpanded: boolean + onExpandChange: (isExpanded: boolean) => void } export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element { // States const divRef = React.useRef(null); const height = useSize(divRef)[1]; + const [showProperties, setShowProperties] = useState(props.isExpanded); // Render const symbols = [...props.symbols.values()]; function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element { @@ -39,12 +45,22 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element { ); } - + const selectedSymbol = props.symbols.get(props.selectedSymbolId); return ( -
-
+
+ {showProperties &&
+ {(selectedSymbol == null) &&

{Text({ textId: '@NoSymbolSelected' })}

} + +
} +
+
+ { setShowProperties(newValue); props.onExpandChange(newValue); }} /> +
-
- -
); } diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index d80e869..f85b63c 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { ElementsList } from '../ElementsList/ElementsList'; +import { ElementsSideBar } from '../ElementsList/ElementsSideBar'; import { History } from '../History/History'; -import { Bar } from '../Bar/Bar'; +import { Bar, BAR_WIDTH } from '../Bar/Bar'; import { Symbols } from '../Symbols/Symbols'; -import { SymbolsSidebar } from '../SymbolsList/SymbolsList'; +import { SymbolsSidebar } from '../SymbolsList/SymbolsSidebar'; import { PropertyType } from '../../Enums/PropertyType'; import { Messages } from '../Messages/Messages'; import { Sidebar } from '../Sidebar/Sidebar'; @@ -37,7 +37,9 @@ export interface IUIProps { export enum SidebarType { None, Components, + ComponentsExpanded, Symbols, + SymbolsExpanded, History, Messages, Settings @@ -59,6 +61,7 @@ function UseSetOrToggleSidebar( export function UI({ editorState, ...methods }: IUIProps): JSX.Element { const [selectedSidebar, setSelectedSidebar] = React.useState(SidebarType.Components); + const [messages, setMessages] = React.useState([]); const current = GetCurrentHistoryState(editorState.history, editorState.historyCurrentStep); const configuration = editorState.configuration; @@ -78,7 +81,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element { ); } - // Please use setOrToggleSidebar rather than setSelectedSidebar so we can close the sidebar + // Please use setOrToggleSidebar rather than setSelectedSidebar, so we can close the sidebar const setOrToggleSidebar = UseSetOrToggleSidebar(selectedSidebar, setSelectedSidebar); let leftSidebarTitle = ''; @@ -104,7 +107,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element { buttonOnClick={methods.addContainer} />; rightSidebarTitle = Text({ textId: '@Elements' }); - rightChildren = setOrToggleSidebar(SidebarType.ComponentsExpanded) } + />; + break; + case SidebarType.ComponentsExpanded: + leftSidebarTitle = Text({ textId: '@Components' }); + leftChildren = ; + rightSidebarTitle = Text({ textId: '@Elements' }); + rightChildren = setOrToggleSidebar(SidebarType.Components) } />; break; - case SidebarType.Symbols: leftSidebarTitle = Text({ textId: '@SymbolsLeft' }); leftChildren = setOrToggleSidebar(SidebarType.SymbolsExpanded) } + />; + break; + case SidebarType.SymbolsExpanded: + leftSidebarTitle = Text({ textId: '@SymbolsLeft' }); + leftChildren = ; + rightSidebarTitle = Text({ textId: '@SymbolsRight' }); + rightChildren = setOrToggleSidebar(SidebarType.Symbols)} />; break; @@ -159,6 +202,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element { const isLeftSidebarOpen = selectedSidebar !== SidebarType.None; const isRightSidebarOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.Symbols; + const isRightSidebarOpenExpanded = selectedSidebar === SidebarType.ComponentsExpanded || selectedSidebar === SidebarType.SymbolsExpanded; const isLeftSidebarOpenClasses = new Set([ 'left-sidebar', @@ -170,14 +214,29 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element { let isRightSidebarOpenClasses = 'right-0 -bottom-full md:-right-80 md:bottom-0'; + let marginSidebar = BAR_WIDTH; + const viewerMarginClasses = new Set([ + 'ml-16' + ]); + if (isLeftSidebarOpen) { isLeftSidebarOpenClasses.delete('-bottom-full'); isLeftSidebarOpenClasses.delete('md:-left-64'); isLeftSidebarOpenClasses.delete('md:bottom-0'); + marginSidebar += 256; + viewerMarginClasses.add(' md:ml-80'); } - if (isRightSidebarOpen) { + if (isRightSidebarOpen || isRightSidebarOpenExpanded) { isRightSidebarOpenClasses = 'right-0'; + + if (isRightSidebarOpenExpanded) { + viewerMarginClasses.add(' md:mr-[32rem]'); + marginSidebar += 512; + } else { + viewerMarginClasses.add(' md:mr-64'); + marginSidebar += 256; + } } else { isLeftSidebarOpenClasses.delete('left-sidebar'); isLeftSidebarOpenClasses.add('left-sidebar-single'); @@ -186,16 +245,24 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element { return ( <> { - setOrToggleSidebar(SidebarType.Components); + if (selectedSidebar === SidebarType.ComponentsExpanded) { + setOrToggleSidebar(SidebarType.ComponentsExpanded); + } else { + setOrToggleSidebar(SidebarType.Components); + } } } toggleSymbols={() => { - setOrToggleSidebar(SidebarType.Symbols); + if (selectedSidebar === SidebarType.SymbolsExpanded) { + setOrToggleSidebar(SidebarType.SymbolsExpanded); + } else { + setOrToggleSidebar(SidebarType.Symbols); + } } } toggleTimeline={() => { setOrToggleSidebar(SidebarType.History); @@ -214,11 +281,11 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element { { leftChildren } void + margin: number } -interface IViewer { - viewerWidth: number - viewerHeight: number -} - -function OnResize( - isLeftSidebarOpen: boolean, - isRightSidebarOpen: boolean, - setViewer: React.Dispatch> -): void { - let marginSidebar = BAR_WIDTH; - if (isLeftSidebarOpen) { - marginSidebar += 256; - } - if (isRightSidebarOpen) { - marginSidebar += 256; +export function Viewer({ + className, + current, + selectedContainer, + selectContainer, + margin +}: IViewerProps): JSX.Element { + function computeWidth(margin: number): number { + return window.innerWidth - (window.innerWidth < 768 ? BAR_WIDTH : margin); } - const margin = window.innerWidth < 768 ? BAR_WIDTH : marginSidebar; - setViewer({ - viewerWidth: window.innerWidth - margin, - viewerHeight: window.innerHeight - }); -} + const [windowSize, setWindowSize] = useState([ + computeWidth(margin), + window.innerHeight + ]); -function UseSVGAutoResizerOnWindowResize( - isLeftSidebarOpen: boolean, - isRightSidebarOpen: boolean, - setViewer: React.Dispatch> -): void { React.useEffect(() => { function SVGAutoResizer(): void { - OnResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer); + setWindowSize([ + computeWidth(margin), + window.innerHeight + ]); } window.addEventListener('resize', SVGAutoResizer); @@ -60,41 +50,13 @@ function UseSVGAutoResizerOnWindowResize( window.removeEventListener('resize', SVGAutoResizer); }; }); -} -function UseSVGAutoResizerOnSidebar( - isLeftSidebarOpen: boolean, - isRightSidebarOpen: boolean, - setViewer: React.Dispatch> -): void { React.useEffect(() => { - OnResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer); - }, [isLeftSidebarOpen, isRightSidebarOpen, setViewer]); -} - -export function Viewer({ - isLeftSidebarOpen, isRightSidebarOpen, - current, - selectedContainer, - selectContainer -}: IViewerProps): JSX.Element { - let marginClasses = 'ml-16'; - let marginSidebar = BAR_WIDTH; - if (isLeftSidebarOpen) { - marginClasses += ' md:ml-80'; - marginSidebar += 256; - } - - if (isRightSidebarOpen) { - marginClasses += ' md:mr-64'; - marginSidebar += 256; - } - - const margin = window.innerWidth < 768 ? BAR_WIDTH : marginSidebar; - const [viewer, setViewer] = React.useState({ - viewerWidth: window.innerWidth - margin, - viewerHeight: window.innerHeight - }); + setWindowSize([ + computeWidth(margin), + window.innerHeight + ]); + }, [margin]); const mainContainer = FindContainerById(current.containers, current.mainContainer); @@ -102,9 +64,6 @@ export function Viewer({ return <>; } - UseSVGAutoResizerOnWindowResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer); - UseSVGAutoResizerOnSidebar(isLeftSidebarOpen, isRightSidebarOpen, setViewer); - if (USE_EXPERIMENTAL_CANVAS_API) { function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void { if (mainContainer === undefined) { @@ -169,9 +128,9 @@ export function Viewer({ return ( + {mainContainer} ); @@ -206,10 +166,11 @@ function RenderDimensions( currentTransform: [number, number] ): void { ctx.save(); - const containerLeftDim = leftDim - (DIMENSION_MARGIN * (depth + 1)) / scale; - const containerTopDim = topDim - (DIMENSION_MARGIN * (depth + 1)) / scale; - const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale; - const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale; + const depthOffset = (DIMENSION_MARGIN * (depth + 1)) / scale; + const containerLeftDim = leftDim - depthOffset; + const containerTopDim = topDim - depthOffset; + const containerBottomDim = bottomDim + depthOffset; + const containerRightDim = rightDim + depthOffset; const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; AddDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth); ctx.restore(); diff --git a/src/Translations/translation.en.json b/src/Translations/translation.en.json index c5709b7..b9d78ed 100644 --- a/src/Translations/translation.en.json +++ b/src/Translations/translation.en.json @@ -2,12 +2,14 @@ "@StartFromScratch": "Start from scratch", "@LoadConfigFile": "Load a configuration file", "@GoBack": "Go back", + "@Properties" : "Properties", "@Components": "Components", "@Elements": "Elements", "@Symbols": "Symbols", "@SymbolsLeft": "Symbols", "@SymbolsRight": "Symbols", + "@NoSymbolSelected": "No symbol selected", "@Timeline": "Timeline", "@Messages": "Messages", "@Settings": "Settings", diff --git a/src/Translations/translation.fr.json b/src/Translations/translation.fr.json index 2741fb3..3d71f2c 100644 --- a/src/Translations/translation.fr.json +++ b/src/Translations/translation.fr.json @@ -2,12 +2,14 @@ "@StartFromScratch": "Partir de zéro", "@LoadConfigFile": "Charger un fichier de configuration", "@GoBack": "Revenir", + "@Properties" : "Propriétés", "@Components": "Composants", "@Elements": "Éléments", "@Symbols": "Symboles", "@SymbolsLeft": "Symboles", "@SymbolsRight": "Symboles", + "@NoSymbolSelected": "Pas de symbol sélectionné", "@Timeline": "Chronologie", "@Messages": "Messages", "@Settings": "Paramètres", diff --git a/src/index.scss b/src/index.scss index 0122e22..aba1712 100644 --- a/src/index.scss +++ b/src/index.scss @@ -21,10 +21,11 @@ .right-sidebar { @apply fixed shadow-lg z-20 - w-[calc(100%_-_4rem)] md:w-64 h-1/2 md:h-full bottom-0 md:bottom-0 } + + .sidebar-title { @apply p-3 md:p-5 font-bold h-12 md:h-16 }