import { IHistoryState } from '../../../Interfaces/IHistoryState'; import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { FindContainerById, MakeDFSIterator } from '../../../utils/itertools'; import { GetCurrentHistory } from '../Editor'; import { ApplyBehaviors, ApplyBehaviorsOnSiblings, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import Swal from 'sweetalert2'; import { PropertyType } from '../../../Enums/PropertyType'; import { TransformX, TransformY } from '../../../utils/svg'; import { Orientation } from '../../../Enums/Orientation'; import { AddContainerToSelectedContainer } from './AddContainer'; import { IConfiguration } from '../../../Interfaces/IConfiguration'; /** * Select a container * @returns New history * @param containerId * @param fullHistory * @param historyCurrentStep */ export function SelectContainer( containerId: string, fullHistory: IHistoryState[], historyCurrentStep: number ): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; history.push({ lastAction: `Select ${containerId}`, mainContainer: current.mainContainer, containers: structuredClone(current.containers), selectedContainerId: containerId, 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 * @param fullHistory History of the editor * @param historyCurrentStep Current step * @returns New history */ export function DeleteContainer( containerId: string, fullHistory: IHistoryState[], historyCurrentStep: number ): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; const containers = structuredClone(current.containers); const mainContainerClone: IContainerModel | undefined = FindContainerById(containers, current.mainContainer); const container = FindContainerById(containers, containerId); if (container === undefined) { throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); } const parent = FindContainerById(containers, container.properties.parentId); if (container === mainContainerClone || parent === undefined || 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); UnlinkContainerFromSymbols(containers, newSymbols, container); const index = parent.children.indexOf(container.properties.id); const success = containers.delete(container.properties.id); if (index > -1 && success) { parent.children.splice(index, 1); } else { throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } ApplyBehaviorsOnSiblings(containers, container, current.symbols); // Select the previous container // or select the one above const selectedContainerId = GetSelectedContainerOnDelete( containers, current.selectedContainerId, parent, index ); history.push({ lastAction: `Delete ${containerId}`, mainContainer: current.mainContainer, containers, selectedContainerId, typeCounters: Object.assign({}, current.typeCounters), symbols: newSymbols, selectedSymbolId: current.selectedSymbolId }); return history; } /** * Replace a container * @param containerId containerId of the container to delete * @param newContainerId * @param configuration * @param fullHistory History of the editor * @param historyCurrentStep Current step * @returns New history */ export function ReplaceByContainer( containerId: string, newContainerId: string, configuration: IConfiguration, fullHistory: IHistoryState[], historyCurrentStep: number ): IHistoryState[] { const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; const historyDelete = DeleteContainer(containerId, fullHistory, historyCurrentStep); const currentDelete = historyDelete[historyDelete.length - 1]; const selectedContainer = FindContainerById(currentDelete.containers, currentDelete.selectedContainerId); if (selectedContainer != null) { const historyAdd = AddContainerToSelectedContainer(newContainerId, selectedContainer, configuration, fullHistory, historyCurrentStep); const currentAdd = historyAdd[historyAdd.length - 1]; fullHistory.push({ lastAction: `Replace ${containerId} by ${newContainerId}`, mainContainer: currentAdd.mainContainer, containers: currentAdd.containers, selectedContainerId: currentAdd.selectedContainerId, typeCounters: Object.assign({}, currentAdd.typeCounters), symbols: current.symbols, selectedSymbolId: current.selectedSymbolId }); return fullHistory; } return history; } /** * Returns the next container that will be selected * after the selectedContainer is removed. * If the selected container is removed, select the sibling after, * If there is no sibling, select the parent, * * @param containers * @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( containers: Map, selectedContainerId: string, parent: IContainerModel, index: number ): string { return FindContainerById(containers, selectedContainerId)?.properties.id ?? parent.children.at(index) ?? parent.children.at(index - 1) ?? parent.properties.id; } /** * Unlink a container and its children to symbols * (used when deleting a container) * @param containers * @param symbols Symbols to update * @param container Container to unlink */ function UnlinkContainerFromSymbols( containers: Map, symbols: Map, container: IContainerModel ): void { const it = MakeDFSIterator(container, containers); for (const child of it) { const symbol = symbols.get(child.properties.linkedSymbolId); if (symbol === undefined) { continue; } symbol.linkedContainers.delete(child.properties.id); } } /** * 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[], type: PropertyType = PropertyType.Simple, selected: IContainerModel | undefined, fullHistory: IHistoryState[], historyCurrentStep: number ): IHistoryState[] { 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 containers = structuredClone(current.containers); const container: IContainerModel | undefined = FindContainerById(containers, selected.properties.id); if (container === null || container === undefined) { throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } SetContainer(containers, container, key, value, type, current.symbols); history.push({ lastAction: `Change ${key} of ${container.properties.id}`, mainContainer: current.mainContainer, containers, selectedContainerId: container.properties.id, typeCounters: Object.assign({}, current.typeCounters), symbols: structuredClone(current.symbols), selectedSymbolId: current.selectedSymbolId }); return history; } /** * Sort the parent children by x * @param containers * @param parent The clone used for the sort * @returns void */ export function SortChildren( containers: Map, parent: IContainerModel ): void { const isHorizontal = parent.properties.orientation === Orientation.Horizontal; const children = parent.children; if (!isHorizontal) { parent.children.sort( (aId, bId) => { const a = FindContainerById(containers, aId); const b = FindContainerById(containers, bId); if (a === undefined || b === undefined) { return 0; } const yA = TransformY(a.properties.y, a.properties.height, a.properties.positionReference); const yB = TransformY(b.properties.y, b.properties.height, b.properties.positionReference); if (yA < yB) { return -1; } if (yB < yA) { return 1; } // xA = xB const indexA = children.indexOf(aId); const indexB = children.indexOf(bId); return indexA - indexB; } ); return; } parent.children.sort( (aId, bId) => { const a = FindContainerById(containers, aId); const b = FindContainerById(containers, bId); if (a === undefined || b === undefined) { return 0; } const xA = TransformX(a.properties.x, a.properties.width, a.properties.positionReference); const xB = TransformX(b.properties.x, b.properties.width, b.properties.positionReference); if (xA < xB) { return -1; } if (xB < xA) { return 1; } // xA = xB const indexA = children.indexOf(aId); const indexB = children.indexOf(bId); return indexA - indexB; } ); } /** * 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 * @param type Type of the property to update * @param symbols Current list of symbols */ function SetContainer( containers: Map, container: IContainerModel, key: string, value: string | number | boolean | number[], 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 const oldSymbol = symbols.get(oldSymbolId); const newSymbol = symbols.get(container.properties.linkedSymbolId); LinkSymbol( container.properties.id, oldSymbol, newSymbol ); // Apply special behaviors: rigid, flex, symbol, anchor ApplyBehaviors(containers, container, symbols); // Apply special behaviors on siblings ApplyBehaviorsOnSiblingsChildren(containers, container, symbols); // sort the children list by their position const parent = FindContainerById(containers, container.properties.parentId); if (parent !== null && parent !== undefined) { SortChildren(containers, 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: IContainerModel, key: string, value: string | number | boolean | number[], type: PropertyType): void { switch (type) { case PropertyType.Style: (container.properties.style as any)[key] = value; break; case PropertyType.Margin: SetMargin(); break; case PropertyType.SelfDimension: (container.properties.dimensionOptions.selfDimensions as any)[key] = value; break; case PropertyType.SelfMarginDimension: (container.properties.dimensionOptions.selfMarginsDimensions as any)[key] = value; break; case PropertyType.ChildrenDimensions: (container.properties.dimensionOptions.childrenDimensions as any)[key] = value; break; case PropertyType.DimensionWithMarks: (container.properties.dimensionOptions.dimensionWithMarks as any)[key] = value; break; case PropertyType.DimensionOptions: (container.properties.dimensionOptions as any)[key] = value; break; default: (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] ?? 0; 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; } } /** * Link a symbol to a container * @param containerId Container id * @param oldSymbol * @param newSymbol * @returns */ export function LinkSymbol( containerId: string, oldSymbol: ISymbolModel | undefined, newSymbol: ISymbolModel | undefined ): void { if (newSymbol === undefined) { if (oldSymbol !== undefined) { oldSymbol.linkedContainers.delete(containerId); } return; } newSymbol.linkedContainers.add(containerId); }