import { Dispatch, SetStateAction } from 'react'; import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel'; import { findContainerById, MakeIterator } from '../../../utils/itertools'; import { getCurrentHistory, UpdateCounters } from '../Editor'; import { AddMethod } from '../../../Enums/AddMethod'; import { IAvailableContainer } from '../../../Interfaces/IAvailableContainer'; import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../../utils/default'; import { ApplyBehaviors } from '../Behaviors/Behaviors'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import Swal from 'sweetalert2'; import { ApplyMargin, transformX } from '../../../utils/svg'; import { Flex } from '../Behaviors/FlexBehaviors'; import { PropertyType } from '../../../Enums/PropertyType'; /** * Select a container * @param container Selected container */ export function SelectContainer( containerId: string, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; history.push({ LastAction: `Select ${containerId}`, MainContainer: structuredClone(current.MainContainer), SelectedContainerId: containerId, TypeCounters: Object.assign({}, current.TypeCounters), Symbols: structuredClone(current.Symbols), SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); } /** * Delete a container * @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 */ export function DeleteContainer( containerId: string, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const container = findContainerById(mainContainerClone, containerId); if (container === undefined) { throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); } if (container === mainContainerClone || container.parent === undefined || container.parent === null) { Swal.fire({ title: 'Oops...', text: 'Deleting the main container is not allowed!', icon: 'error' }); throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!'); } if (container === null || container === undefined) { throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); } const newSymbols = structuredClone(current.Symbols); UnlinkSymbol(newSymbols, container); const index = container.parent.children.indexOf(container); if (index > -1) { container.parent.children.splice(index, 1); } else { throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } ApplyBehaviorsOnSiblings(container, current.Symbols); // Select the previous container // or select the one above const SelectedContainerId = GetSelectedContainerOnDelete( mainContainerClone, current.SelectedContainerId, container.parent, index ); history.push({ LastAction: `Delete ${containerId}`, MainContainer: mainContainerClone, SelectedContainerId, TypeCounters: Object.assign({}, current.TypeCounters), Symbols: newSymbols, SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); } function GetSelectedContainerOnDelete(mainContainerClone: IContainerModel, selectedContainerId: string, parent: IContainerModel, index: number): string { const SelectedContainer = findContainerById(mainContainerClone, selectedContainerId) ?? parent.children.at(index - 1) ?? parent; const SelectedContainerId = SelectedContainer.properties.id; return SelectedContainerId; } function UnlinkSymbol(symbols: Map, container: IContainerModel): void { const it = MakeIterator(container); for (const child of it) { const symbol = symbols.get(child.properties.linkedSymbolId); if (symbol === undefined) { continue; } symbol.linkedContainers.delete(child.properties.id); } } /** * Add a new container to a selected container * @param type The type of container * @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( type: string, selected: IContainerModel | undefined, configuration: IConfiguration, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { if (selected === null || selected === undefined) { return; } const parent = selected; AddContainer( parent.children.length, type, parent.properties.id, configuration, fullHistory, historyCurrentStep, setHistory, setHistoryCurrentStep ); } /** * 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 * @param setHistory State setter of History * @param setHistoryCurrentStep State setter of the current step * @returns void */ export function AddContainer( index: number, type: string, 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 ); 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); } function UpdateParentChildrenList(parentClone: IContainerModel | null | undefined): void { if (parentClone === null || parentClone === undefined) { return; } 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) ); } function InitializeDefaultChild( configuration: IConfiguration, containerConfig: IAvailableContainer, newContainer: ContainerModel, newCounters: Record ): void { if (containerConfig.DefaultChildType === undefined) { return; } let currentConfig = configuration.AvailableContainers .find(option => option.Type === containerConfig.DefaultChildType); let parent = newContainer; let depth = 0; const seen = new Set([containerConfig.Type]); while (currentConfig !== undefined && depth <= DEFAULTCHILDTYPE_MAX_DEPTH ) { if (!DEFAULTCHILDTYPE_ALLOW_CYCLIC && seen.has(currentConfig.Type)) { return; } seen.add(currentConfig.Type); const left: number = currentConfig.Margin?.left ?? 0; const bottom: number = currentConfig.Margin?.bottom ?? 0; const top: number = currentConfig.Margin?.top ?? 0; const right: number = currentConfig.Margin?.right ?? 0; let x = currentConfig.DefaultX ?? 0; let y = currentConfig.DefaultY ?? 0; let width = currentConfig.Width ?? currentConfig.MaxWidth ?? currentConfig.MinWidth ?? parent.properties.width; let height = currentConfig.Height ?? parent.properties.height; ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); UpdateCounters(newCounters, currentConfig.Type); const count = newCounters[currentConfig.Type]; const defaultChildProperties = GetDefaultContainerProps( currentConfig.Type, count, parent, x, y, width, height, currentConfig ); // Create the container const newChildContainer = new ContainerModel( parent, defaultChildProperties, [], { type: currentConfig.Type } ); // And push it the the parent children parent.children.push(newChildContainer); // iterate depth++; parent = newChildContainer; currentConfig = configuration.AvailableContainers .find(option => option.Type === (currentConfig as IAvailableContainer).DefaultChildType); } } /** * Returns a new offset by applying an Add method (append, insert etc.) * See AddMethod * @param index Index of the container * @param containerConfig Configuration of a container * @param parent Parent container * @param x Additionnal offset * @returns New offset */ 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); if (lastChild !== undefined) { x += (lastChild.properties.x + lastChild.properties.width); } } return x; } /** * Handled the property change event in the properties form * @param key Property name * @param value New value of the property * @returns void */ export function OnPropertyChange( key: string, value: string | number | boolean, type: PropertyType = PropertyType.SIMPLE, selected: IContainerModel | undefined, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; if (selected === null || selected === undefined) { throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const container: ContainerModel | undefined = findContainerById(mainContainerClone, selected.properties.id); if (container === null || container === undefined) { throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } SetContainer(container, key, value, type, current.Symbols); history.push({ LastAction: `Change ${key} of ${container.properties.id}`, MainContainer: mainContainerClone, SelectedContainerId: container.properties.id, TypeCounters: Object.assign({}, current.TypeCounters), Symbols: structuredClone(current.Symbols), SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); } /** * Set the container with properties and behaviors (mutate) * @param container Container to update * @param key Key of the property to update * @param value Value of the property to update * @param type Type of the property to update * @param symbols Current list of symbols */ function SetContainer( container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType, symbols: Map ): void { // get the old symbol to detect unlink const oldSymbolId = container.properties.linkedSymbolId; // update the property AssignProperty(container, key, value, type); // link the symbol if it exists LinkSymbol( container.properties.id, oldSymbolId, container.properties.linkedSymbolId, symbols ); // Apply special behaviors: rigid, flex, symbol, anchor ApplyBehaviors(container, symbols); // Apply special behaviors on siblings ApplyBehaviorsOnSiblings(container, symbols); // sort the children list by their position UpdateParentChildrenList(container.parent); } function AssignProperty(container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType): void { switch (type) { case PropertyType.STYLE: (container.properties.style as any)[key] = value; break; case PropertyType.MARGIN: SetMargin(); break; default: (container.properties as any)[key] = value; } function SetMargin(): void { const oldMarginValue: number = (container.properties.margin as any)[key]; const diff = Number(value) - oldMarginValue; switch (key) { case 'left': container.properties.x += diff; container.properties.width -= diff; break; case 'right': container.properties.width -= diff; break; case 'bottom': container.properties.height -= diff; break; case 'top': container.properties.y += diff; container.properties.height -= diff; break; } (container.properties.margin as any)[key] = value; } } function LinkSymbol( containerId: string, oldSymbolId: string, newSymbolId: string, symbols: Map ): void { const oldSymbol = symbols.get(oldSymbolId); const newSymbol = symbols.get(newSymbolId); if (newSymbol === undefined) { if (oldSymbol !== undefined) { oldSymbol.linkedContainers.delete(containerId); } return; } newSymbol.linkedContainers.add(containerId); } 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)); }