diff --git a/.env.development b/.env.development index 9b4e331..1227bc1 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,2 @@ -VITE_API_URL=http://localhost:5000 \ No newline at end of file +VITE_API_FETCH_URL=http://localhost:5000 +VITE_API_POST_URL=http://localhost:5000/ApplicationState \ No newline at end of file diff --git a/.env.production b/.env.production index b2c8524..8b00be3 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,3 @@ -VITE_API_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration \ No newline at end of file +VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration +VITE_API_POST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/ApplicationState \ No newline at end of file diff --git a/.env.test b/.env.test index 9b4e331..71d0acc 100644 --- a/.env.test +++ b/.env.test @@ -1 +1,2 @@ -VITE_API_URL=http://localhost:5000 \ No newline at end of file +VITE_API_FETCH_URL=http://localhost:5000 +VITE_API_POST_URL=http://localhost:5000 \ No newline at end of file diff --git a/public/workers/worker.js b/public/workers/worker.js index 9260405..3faf744 100644 --- a/public/workers/worker.js +++ b/public/workers/worker.js @@ -9,7 +9,7 @@ const getCircularReplacer = () => { return; } - if (key === 'Symbols') { + if (key === 'symbols') { return Array.from(value.entries()); } diff --git a/src/Components/API/api.test.tsx b/src/Components/API/api.test.tsx index 1005d35..9c1f7e4 100644 --- a/src/Components/API/api.test.tsx +++ b/src/Components/API/api.test.tsx @@ -3,7 +3,7 @@ import { FetchConfiguration } from './api'; describe.concurrent('API test', () => { it('Load environment', () => { - const url = import.meta.env.VITE_API_URL; + const url = import.meta.env.VITE_API_FETCH_URL; expect(url).toBe('http://localhost:5000'); }); diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts index 635b01b..72a4947 100644 --- a/src/Components/API/api.ts +++ b/src/Components/API/api.ts @@ -1,11 +1,15 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest'; +import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse'; +import { GetCircularReplacer } from '../../utils/saveload'; /** * Fetch the configuration from the API * @returns {Configation} The model of the configuration for the application */ export async function FetchConfiguration(): Promise { - const url = `${import.meta.env.VITE_API_URL}`; + const url = import.meta.env.VITE_API_FETCH_URL; // The test library cannot use the Fetch API // @ts-expect-error // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions @@ -28,3 +32,30 @@ export async function FetchConfiguration(): Promise { xhr.send(); }); } + +export async function SetContainerList(request: ISetContainerListRequest): Promise { + const url = import.meta.env.VITE_API_POST_URL; + const dataParsed = JSON.stringify(request, GetCircularReplacer()); + // The test library cannot use the Fetch API + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (window.fetch) { + return await fetch(url, { + method: 'POST', + body: dataParsed + }) + .then(async(response) => + await response.json() + ) as ISetContainerListResponse; + } + return await new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.onreadystatechange = function() { // Call a function when the state changes. + if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { + resolve(JSON.parse(this.responseText)); + } + }; + xhr.send(dataParsed); + }); +} diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index bd25f7f..9b2b460 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -1,4 +1,3 @@ -import { Dispatch, SetStateAction } from 'react'; import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel'; @@ -16,14 +15,13 @@ import { PropertyType } from '../../../Enums/PropertyType'; /** * Select a container * @param container Selected container + * @returns New history */ export function SelectContainer( containerId: string, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -35,8 +33,7 @@ export function SelectContainer( symbols: structuredClone(current.symbols), selectedSymbolId: current.selectedSymbolId }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } /** @@ -44,16 +41,13 @@ export function SelectContainer( * @param containerId containerId of the container to delete * @param fullHistory History of the editor * @param historyCurrentStep Current step - * @param setHistory State setter for History - * @param setHistoryCurrentStep State setter for current step + * @returns New history */ export function DeleteContainer( containerId: string, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -80,7 +74,7 @@ export function DeleteContainer( } const newSymbols = structuredClone(current.symbols); - UnlinkSymbol(newSymbols, container); + UnlinkContainerFromSymbols(newSymbols, container); const index = container.parent.children.indexOf(container); if (index > -1) { @@ -108,10 +102,21 @@ export function DeleteContainer( symbols: newSymbols, selectedSymbolId: current.selectedSymbolId }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } +/** + * Returns the next container that will be selected + * after the selectedContainer is removed. + * If the selected container is removed, select the sibling before, + * If there is no sibling, select the parent, + * + * @param mainContainerClone Main container + * @param selectedContainerId Current selected container + * @param parent Parent of the selected/deleted container + * @param index Index of the selected/deleted container + * @returns {IContainerModel} Next selected container + */ function GetSelectedContainerOnDelete( mainContainerClone: IContainerModel, selectedContainerId: string, @@ -125,7 +130,13 @@ function GetSelectedContainerOnDelete( return newSelectedContainerId; } -function UnlinkSymbol(symbols: Map, container: IContainerModel): void { +/** + * Unlink a container and its children to symbols + * (used when deleting a container) + * @param symbols Symbols to update + * @param container Container to unlink + */ +function UnlinkContainerFromSymbols(symbols: Map, container: IContainerModel): void { const it = MakeIterator(container); for (const child of it) { const symbol = symbols.get(child.properties.linkedSymbolId); @@ -142,8 +153,6 @@ function UnlinkSymbol(symbols: Map, container: IContainerM * @param configuration Configuration of the App * @param fullHistory History of the editor * @param historyCurrentStep Current step - * @param setHistory State setter for History - * @param setHistoryCurrentStep State setter for current step * @returns void */ export function AddContainerToSelectedContainer( @@ -151,28 +160,153 @@ export function AddContainerToSelectedContainer( selected: IContainerModel | undefined, configuration: IConfiguration, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] | null { if (selected === null || selected === undefined) { - return; + return null; } const parent = selected; - AddContainer( + return AddContainer( parent.children.length, type, parent.properties.id, configuration, fullHistory, - historyCurrentStep, - setHistory, - setHistoryCurrentStep + historyCurrentStep ); } +/** + * Create and add a new container at `index` in children of parent of `parentId` + * @param index Index where to insert to the new container + * @param type Type of container + * @param parentId Parent in which to insert the new container + * @param configuration Configuration of the app + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @returns void + */ +export function AddContainers( + index: number, + types: string[], + parentId: string, + configuration: IConfiguration, + fullHistory: IHistoryState[], + historyCurrentStep: number +): IHistoryState[] { + const history = GetCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + // Deep clone the main container for the history + const clone: IContainerModel = structuredClone(current.mainContainer); + + // Find the parent in the clone + const parentClone: IContainerModel | undefined = FindContainerById( + clone, parentId + ); + + if (parentClone === null || parentClone === undefined) { + throw new Error('[AddContainer] Container model was not found among children of the main container!'); + } + + // Deep clone the counters + const newCounters = Object.assign({}, current.typeCounters); + + // containerIds is used for logging purpose (see setHistory below) + const containerIds: string[] = []; + + // Iterate over the containers + types.forEach((type, typeIndex) => { + // Get the preset properties from the API + const containerConfig = configuration.AvailableContainers + .find(option => option.Type === type); + + if (containerConfig === undefined) { + throw new Error(`[AddContainer] Object type not found. Found: ${type}`); + } + + // Default margin + const left: number = containerConfig.Margin?.left ?? 0; + const bottom: number = containerConfig.Margin?.bottom ?? 0; + const top: number = containerConfig.Margin?.top ?? 0; + const right: number = containerConfig.Margin?.right ?? 0; + + // Default coordinates + let x = containerConfig.DefaultX ?? 0; + let y = containerConfig.DefaultY ?? 0; + let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width; + let height = containerConfig.Height ?? parentClone.properties.height; + + ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); + + // Apply an add method (append or insert/replace) + x = ApplyAddMethod(index + typeIndex, containerConfig, parentClone, x); + + // Set the counter of the object type in order to assign an unique id + UpdateCounters(newCounters, type); + const count = newCounters[type]; + + const defaultProperties = GetDefaultContainerProps( + type, + count, + parentClone, + x, + y, + width, + height, + containerConfig + ); + + // Create the container + const newContainer = new ContainerModel( + parentClone, + defaultProperties, + [], + { + type + } + ); + + // Add it to the parent + if (index === parentClone.children.length) { + parentClone.children.push(newContainer); + } else { + parentClone.children.splice(index, 0, newContainer); + } + + /// Handle behaviors here /// + + // Initialize default children of the container + InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters); + + // Apply the behaviors (flex, rigid, anchor) + ApplyBehaviors(newContainer, current.symbols); + + // Then, apply the behaviors on its siblings (mostly for flex) + ApplyBehaviorsOnSiblings(newContainer, current.symbols); + + // Sort the parent children by x + UpdateParentChildrenList(parentClone); + + // Add to the list of container id for logging purpose + containerIds.push(newContainer.properties.id); + }); + + // Update the state + history.push({ + lastAction: `Add [${containerIds.join(', ')}] in ${parentClone.properties.id}`, + mainContainer: clone, + selectedContainerId: parentClone.properties.id, + typeCounters: newCounters, + symbols: structuredClone(current.symbols), + selectedSymbolId: current.selectedSymbolId + }); + + return history; +} + /** * Create and add a new container at `index` in children of parent of `parentId` * @param index Index where to insert to the new container @@ -183,7 +317,7 @@ export function AddContainerToSelectedContainer( * @param historyCurrentStep Current step * @param setHistory State setter of History * @param setHistoryCurrentStep State setter of the current step - * @returns void + * @returns new history */ export function AddContainer( index: number, @@ -191,104 +325,55 @@ export function AddContainer( parentId: string, configuration: IConfiguration, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { - const history = GetCurrentHistory(fullHistory, historyCurrentStep); - const current = history[history.length - 1]; - - // Get the preset properties from the API - const containerConfig = configuration.AvailableContainers - .find(option => option.Type === type); - - if (containerConfig === undefined) { - throw new Error(`[AddContainer] Object type not found. Found: ${type}`); - } - - // Set the counter of the object type in order to assign an unique id - const newCounters = Object.assign({}, current.typeCounters); - UpdateCounters(newCounters, type); - const count = newCounters[type]; - - // Create maincontainer model - const clone: IContainerModel = structuredClone(current.mainContainer); - - // Find the parent - const parentClone: IContainerModel | undefined = FindContainerById( - clone, parentId + historyCurrentStep: number +): IHistoryState[] { + // just call AddContainers with an array on a single element + return AddContainers( + index, + [type], + parentId, + configuration, + fullHistory, + historyCurrentStep ); - - if (parentClone === null || parentClone === undefined) { - throw new Error('[AddContainer] Container model was not found among children of the main container!'); - } - const left: number = containerConfig.Margin?.left ?? 0; - const bottom: number = containerConfig.Margin?.bottom ?? 0; - const top: number = containerConfig.Margin?.top ?? 0; - const right: number = containerConfig.Margin?.right ?? 0; - - let x = containerConfig.DefaultX ?? 0; - let y = containerConfig.DefaultY ?? 0; - let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width; - let height = containerConfig.Height ?? parentClone.properties.height; - - ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); - - x = ApplyAddMethod(index, containerConfig, parentClone, x); - - const defaultProperties = GetDefaultContainerProps( - type, - count, - parentClone, - x, - y, - width, - height, - containerConfig - ); - - // Create the container - const newContainer = new ContainerModel( - parentClone, - defaultProperties, - [], - { - type - } - ); - - parentClone.children.push(newContainer); - UpdateParentChildrenList(parentClone); - - InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters); - - ApplyBehaviors(newContainer, current.symbols); - - ApplyBehaviorsOnSiblings(newContainer, current.symbols); - - // Update the state - history.push({ - lastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`, - mainContainer: clone, - selectedContainerId: parentClone.properties.id, - typeCounters: newCounters, - symbols: structuredClone(current.symbols), - selectedSymbolId: current.selectedSymbolId - }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); } +/** + * Sort the parent children by x + * @param parentClone The clone used for the sort + * @returns void + */ function UpdateParentChildrenList(parentClone: IContainerModel | null | undefined): void { if (parentClone === null || parentClone === undefined) { return; } + const children = parentClone.children; parentClone.children.sort( - (a, b) => TransformX(a.properties.x, a.properties.width, a.properties.xPositionReference) - - TransformX(b.properties.x, b.properties.width, b.properties.xPositionReference) + (a, b) => { + const xA = TransformX(a.properties.x, a.properties.width, a.properties.xPositionReference); + const xB = TransformX(b.properties.x, b.properties.width, b.properties.xPositionReference); + if (xA < xB) { + return -1; + } + if (xB < xA) { + return 1; + } + // xA = xB + const indexA = children.indexOf(a); + const indexB = children.indexOf(b); + return indexA - indexB; + } ); } +/** + * Initialize the children of the container + * @param configuration Current configuration of the app + * @param containerConfig The new container config used to read the default child type + * @param newContainer The new container to initialize its default children + * @param newCounters Type counter used for unique ids + * @returns + */ function InitializeDefaultChild( configuration: IConfiguration, containerConfig: IAvailableContainer, @@ -368,12 +453,18 @@ function InitializeDefaultChild( * @param x Additionnal offset * @returns New offset */ -function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, parent: IContainerModel, x: number): number { +function ApplyAddMethod( + index: number, + containerConfig: IAvailableContainer, + parent: IContainerModel, + x: number +): number { if (index > 0 && ( containerConfig.AddMethod === undefined || containerConfig.AddMethod === AddMethod.Append)) { // Append method (default) - const lastChild: IContainerModel | undefined = parent.children.at(index - 1); + const lastChild: IContainerModel | undefined = parent.children + .at(index - 1); if (lastChild !== undefined) { x += (lastChild.properties.x + lastChild.properties.width); @@ -394,10 +485,8 @@ export function OnPropertyChange( type: PropertyType = PropertyType.Simple, selected: IContainerModel | undefined, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -423,8 +512,7 @@ export function OnPropertyChange( symbols: structuredClone(current.symbols), selectedSymbolId: current.selectedSymbolId }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } /** @@ -465,6 +553,13 @@ function SetContainer( UpdateParentChildrenList(container.parent); } +/** + * Assign the property to a container depending on the type + * @param container Container in which the property will be applied to + * @param key Key/Id of the property + * @param value Value of the property + * @param type Type of the property + */ function AssignProperty(container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType): void { switch (type) { case PropertyType.Style: @@ -477,7 +572,12 @@ function AssignProperty(container: ContainerModel, key: string, value: string | (container.properties as any)[key] = value; } + /** + * Set the margin property + */ function SetMargin(): void { + // We need to detect change in order to apply transformation to the width and height + // Knowing the current margin is not enough as we dont keep the original width and height const oldMarginValue: number = (container.properties.margin as any)[key]; const diff = Number(value) - oldMarginValue; switch (key) { @@ -500,6 +600,14 @@ function AssignProperty(container: ContainerModel, 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 + * @returns + */ function LinkSymbol( containerId: string, oldSymbolId: string, @@ -519,10 +627,18 @@ function LinkSymbol( newSymbol.linkedContainers.add(containerId); } +/** + * Iterate over the siblings of newContainer and apply the behaviors + * @param newContainer + * @param symbols + * @returns + */ function ApplyBehaviorsOnSiblings(newContainer: ContainerModel, symbols: Map): void { if (newContainer.parent === null || newContainer.parent === undefined) { return; } - newContainer.parent.children.filter(container => newContainer !== container).forEach(container => ApplyBehaviors(container, symbols)); + newContainer.parent.children + .filter(container => newContainer !== container) + .forEach(container => ApplyBehaviors(container, symbols)); } diff --git a/src/Components/Editor/Actions/ContextMenuActions.ts b/src/Components/Editor/Actions/ContextMenuActions.ts new file mode 100644 index 0000000..91659f8 --- /dev/null +++ b/src/Components/Editor/Actions/ContextMenuActions.ts @@ -0,0 +1,131 @@ +import Swal from 'sweetalert2'; +import { AddMethod } from '../../../Enums/AddMethod'; +import { IAction } from '../../../Interfaces/IAction'; +import { IConfiguration } from '../../../Interfaces/IConfiguration'; +import { IContainerModel } from '../../../Interfaces/IContainerModel'; +import { IHistoryState } from '../../../Interfaces/IHistoryState'; +import { ISetContainerListRequest } from '../../../Interfaces/ISetContainerListRequest'; +import { ISetContainerListResponse } from '../../../Interfaces/ISetContainerListResponse'; +import { FindContainerById } from '../../../utils/itertools'; +import { SetContainerList } from '../../API/api'; +import { AddContainers, DeleteContainer } from './ContainerOperations'; + +export function GetAction( + action: IAction, + currentState: IHistoryState, + configuration: IConfiguration, + history: IHistoryState[], + historyCurrentStep: number, + setNewHistory: (newHistory: IHistoryState[]) => void +): (target: HTMLElement) => void { + return (target: HTMLElement) => { + const id = target.id; + const container = FindContainerById(currentState.mainContainer, id); + + if (container === undefined) { + Swal.fire({ + title: 'Error', + text: 'No container was selected on right click', + icon: 'error' + }); + throw new Error(`[API:${action.Action}] No container was selected`); + } + + /* eslint-disable @typescript-eslint/naming-convention */ + const request: ISetContainerListRequest = { + Container: container, + Action: action.Action, + ApplicationState: currentState + }; + /* eslint-enable */ + + SetContainerList(request) + .then((response: ISetContainerListResponse) => { + HandleSetContainerList( + action, + container, + response, + configuration, + history, + historyCurrentStep, + setNewHistory + ); + }); + }; +} + +function HandleSetContainerList( + action: IAction, + selectedContainer: IContainerModel, + response: ISetContainerListResponse, + configuration: IConfiguration, + history: IHistoryState[], + historyCurrentStep: number, + setNewHistory: (newHistory: IHistoryState[]) => void +): void { + switch (action.AddingBehavior) { + case AddMethod.Append: + setNewHistory( + AddContainers( + selectedContainer.children.length, + response.Containers.map(container => container.properties.type), + selectedContainer.properties.id, + configuration, + history, + historyCurrentStep + )); + break; + case AddMethod.Insert: + break; + case AddMethod.Replace: + setNewHistory( + HandleReplace( + selectedContainer, + response, + configuration, + history, + historyCurrentStep + ) + ); + break; + } +} + +function HandleReplace( + selectedContainer: IContainerModel, + response: ISetContainerListResponse, + configuration: IConfiguration, + history: IHistoryState[], + historyCurrentStep: number +): IHistoryState[] { + if (selectedContainer.parent === undefined || selectedContainer.parent === null) { + throw new Error('[ReplaceContainer] Cannot replace a container that does not exists'); + } + + const index = selectedContainer.parent.children.indexOf(selectedContainer); + + const types = response.Containers.map(container => container.properties.type); + const newHistoryBeforeDelete = AddContainers( + index + 1, + types, + selectedContainer.properties.parentId, + configuration, + history, + historyCurrentStep + ); + + const newHistoryAfterDelete = DeleteContainer( + selectedContainer.properties.id, + newHistoryBeforeDelete, + newHistoryBeforeDelete.length - 1 + ); + + // Remove AddContainers from history + newHistoryAfterDelete.splice(newHistoryAfterDelete.length - 2, 1); + + // Rename the last action by Replace + newHistoryAfterDelete[newHistoryAfterDelete.length - 1].lastAction = + `Replace ${selectedContainer.properties.id} by [${types.join(', ')}]`; + + return newHistoryAfterDelete; +} diff --git a/src/Components/Editor/Actions/SymbolOperations.ts b/src/Components/Editor/Actions/SymbolOperations.ts index 7d41a41..e3c89fa 100644 --- a/src/Components/Editor/Actions/SymbolOperations.ts +++ b/src/Components/Editor/Actions/SymbolOperations.ts @@ -13,10 +13,8 @@ export function AddSymbol( name: string, configuration: IConfiguration, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -44,17 +42,14 @@ export function AddSymbol( symbols: newSymbols, selectedSymbolId: newSymbol.id }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } export function SelectSymbol( symbolId: string, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -66,17 +61,14 @@ export function SelectSymbol( symbols: structuredClone(current.symbols), selectedSymbolId: symbolId }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } export function DeleteSymbol( symbolId: string, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -89,7 +81,7 @@ export function DeleteSymbol( const newMainContainer = structuredClone(current.mainContainer); - UnlinkContainers(symbol, newMainContainer); + UnlinkSymbolFromContainers(symbol, newMainContainer); newSymbols.delete(symbolId); @@ -101,13 +93,17 @@ export function DeleteSymbol( symbols: newSymbols, selectedSymbolId: symbolId }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } -function UnlinkContainers(symbol: ISymbolModel, newMainContainer: IContainerModel): void { +/** + * Unlink a symbol to a container and its children + * @param symbol Symbol to remove + * @param root Container and its children to remove a symbol from + */ +function UnlinkSymbolFromContainers(symbol: ISymbolModel, root: IContainerModel): void { symbol.linkedContainers.forEach((containerId) => { - const container = FindContainerById(newMainContainer, containerId); + const container = FindContainerById(root, containerId); if (container === undefined) { return; @@ -127,10 +123,8 @@ export function OnPropertyChange( key: string, value: string | number | boolean, fullHistory: IHistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { + historyCurrentStep: number +): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; @@ -166,6 +160,5 @@ export function OnPropertyChange( symbols: newSymbols, selectedSymbolId: symbol.id }); - setHistory(history); - setHistoryCurrentStep(history.length - 1); + return history; } diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 8851537..618967f 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -4,15 +4,16 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; import { SVG } from '../SVG/SVG'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { UI } from '../UI/UI'; -import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange } from './Actions/ContainerOperations'; +import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange, AddContainers } from './Actions/ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save'; import { OnKey } from './Actions/Shortcuts'; -import EditorEvents from '../../Events/EditorEvents'; +import { events as EVENTS } from '../../Events/EditorEvents'; import { IEditorState } from '../../Interfaces/IEditorState'; import { MAX_HISTORY } from '../../utils/default'; import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations'; import { FindContainerById } from '../../utils/itertools'; -import { Menu } from '../Menu/Menu'; +import { IMenuAction, Menu } from '../Menu/Menu'; +import { GetAction } from './Actions/ContextMenuActions'; interface IEditorProps { root: Element | Document @@ -22,11 +23,11 @@ interface IEditorProps { } function InitActions( - menuActions: Map, + menuActions: Map, + configuration: IConfiguration, history: IHistoryState[], historyCurrentStep: number, - setHistory: React.Dispatch>, - setHistoryCurrentStep: React.Dispatch> + setNewHistory: (newHistory: IHistoryState[]) => void ): void { menuActions.set( 'elements-sidebar-row', @@ -34,13 +35,12 @@ function InitActions( text: 'Delete', action: (target: HTMLElement) => { const id = target.id; - DeleteContainer( + const newHistory = DeleteContainer( id, history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep + historyCurrentStep ); + setNewHistory(newHistory); } }] ); @@ -50,16 +50,43 @@ function InitActions( text: 'Delete', action: (target: HTMLElement) => { const id = target.id; - DeleteSymbol( + const newHistory = DeleteSymbol( id, history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep + historyCurrentStep ); + setNewHistory(newHistory); } }] ); + + // API Actions + for (const availableContainer of configuration.AvailableContainers) { + if (availableContainer.Actions === undefined) { + continue; + } + + for (const action of availableContainer.Actions) { + if (menuActions.get(availableContainer.Type) === undefined) { + menuActions.set(availableContainer.Type, []); + } + + const currentState = GetCurrentHistoryState(history, historyCurrentStep); + const newAction: IMenuAction = { + text: action.Label, + action: GetAction( + action, + currentState, + configuration, + history, + historyCurrentStep, + setNewHistory + ) + }; + + menuActions.get(availableContainer.Type)?.push(newAction); + } + } } function UseShortcuts( @@ -90,11 +117,9 @@ function UseWindowEvents( historyCurrentStep: number, configuration: IConfiguration, editorRef: React.RefObject, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> + setNewHistory: (newHistory: IHistoryState[]) => void ): void { useEffect(() => { - const events = EditorEvents; const editorState: IEditorState = { history, historyCurrentStep, @@ -102,13 +127,12 @@ function UseWindowEvents( }; const funcs = new Map void>(); - for (const event of events) { + for (const event of EVENTS) { function Func(eventInitDict?: CustomEventInit): void { return event.func( root, editorState, - setHistory, - setHistoryCurrentStep, + setNewHistory, eventInitDict ); } @@ -116,7 +140,7 @@ function UseWindowEvents( funcs.set(event.name, Func); } return () => { - for (const event of events) { + for (const event of EVENTS) { const func = funcs.get(event.name); if (func === undefined) { continue; @@ -127,11 +151,29 @@ function UseWindowEvents( }); } +/** + * Return a macro function to use both setHistory + * and setHistoryCurrentStep at the same time + * @param setHistory + * @param setHistoryCurrentStep + * @returns + */ +function UseNewHistoryState( + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): (newHistory: IHistoryState[]) => void { + return (newHistory) => { + setHistory(newHistory); + setHistoryCurrentStep(newHistory.length - 1); + }; +} + export function Editor(props: IEditorProps): JSX.Element { // States const [history, setHistory] = React.useState(structuredClone(props.history)); const [historyCurrentStep, setHistoryCurrentStep] = React.useState(props.historyCurrentStep); const editorRef = useRef(null); + const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep); // Events UseShortcuts(history, historyCurrentStep, setHistoryCurrentStep); @@ -141,13 +183,18 @@ export function Editor(props: IEditorProps): JSX.Element { historyCurrentStep, props.configuration, editorRef, - setHistory, - setHistoryCurrentStep + setNewHistory ); // Context Menu const menuActions = new Map(); - InitActions(menuActions, history, historyCurrentStep, setHistory, setHistoryCurrentStep); + InitActions( + menuActions, + props.configuration, + history, + historyCurrentStep, + setNewHistory + ); // Render const configuration = props.configuration; @@ -162,66 +209,62 @@ export function Editor(props: IEditorProps): JSX.Element { historyCurrentStep={historyCurrentStep} availableContainers={configuration.AvailableContainers} availableSymbols={configuration.AvailableSymbols} - selectContainer={(container) => SelectContainer( - container, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - deleteContainer={(containerId: string) => DeleteContainer( - containerId, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - onPropertyChange={(key, value, type) => OnPropertyChange( - key, value, type, - selected, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - addContainer={(type) => AddContainerToSelectedContainer( - type, - selected, - configuration, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - addSymbol={(type) => AddSymbol( - type, - configuration, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - onSymbolPropertyChange={(key, value) => OnSymbolPropertyChange( - key, value, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - selectSymbol={(symbolId) => SelectSymbol( - symbolId, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} - deleteSymbol={(symbolId) => DeleteSymbol( - symbolId, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} + selectContainer={(container) => setNewHistory( + SelectContainer( + container, + history, + historyCurrentStep + ))} + deleteContainer={(containerId: string) => setNewHistory( + DeleteContainer( + containerId, + history, + historyCurrentStep + ))} + onPropertyChange={(key, value, type) => setNewHistory( + OnPropertyChange( + key, value, type, + selected, + history, + historyCurrentStep + ))} + addContainer={(type) => { + const newHistory = AddContainerToSelectedContainer( + type, + selected, + configuration, + history, + historyCurrentStep + ); + if (newHistory !== null) { + setNewHistory(newHistory); + } + }} + addSymbol={(type) => setNewHistory( + AddSymbol( + type, + configuration, + history, + historyCurrentStep + ))} + onSymbolPropertyChange={(key, value) => setNewHistory( + OnSymbolPropertyChange( + key, value, + history, + historyCurrentStep + ))} + selectSymbol={(symbolId) => setNewHistory( + SelectSymbol( + symbolId, + history, + historyCurrentStep + ))} + deleteSymbol={(symbolId) => setNewHistory( + DeleteSymbol( + symbolId, + history, + historyCurrentStep + ))} saveEditorAsJSON={() => SaveEditorAsJSON( history, historyCurrentStep, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 49df647..f3a2371 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -50,7 +50,7 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { return (