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 { PropertyType } from '../../../Enums/PropertyType'; /** * Select a container * @param container Selected container * @returns New history */ 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: structuredClone(current.mainContainer), 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 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); UnlinkContainerFromSymbols(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 }); 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, parent: IContainerModel, index: number ): string { const newSelectedContainer = FindContainerById(mainContainerClone, selectedContainerId) ?? parent.children.at(index - 1) ?? parent; const newSelectedContainerId = newSelectedContainer.properties.id; return newSelectedContainerId; } /** * 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); 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 * @returns void */ export function AddContainerToSelectedContainer( type: string, selected: IContainerModel | undefined, configuration: IConfiguration, fullHistory: IHistoryState[], historyCurrentStep: number ): IHistoryState[] | null { if (selected === null || selected === undefined) { return null; } const parent = selected; return AddContainer( parent.children.length, type, parent.properties.id, configuration, fullHistory, 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 * @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 new history */ export function AddContainer( index: number, type: string, parentId: string, configuration: IConfiguration, fullHistory: IHistoryState[], historyCurrentStep: number ): IHistoryState[] { // just call AddContainers with an array on a single element return AddContainers( index, [type], parentId, configuration, fullHistory, historyCurrentStep ); } /** * 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) => { 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, 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 ): 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 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 }); return history; } /** * 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); } /** * 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: (container.properties.style as any)[key] = value; break; case PropertyType.Margin: SetMargin(); 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]; 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 oldSymbolId Old Symbol id * @param newSymbolId New Symbol id * @param symbols Current list of symbols * @returns */ 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); } /** * 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)); }