svg-layout-designer-react/src/Components/Editor/Actions/ContainerOperations.ts

440 lines
14 KiB
TypeScript

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<string, IContainerModel>,
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<string, IContainerModel>,
symbols: Map<string, ISymbolModel>,
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<string, IContainerModel>,
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<string, IContainerModel>,
container: IContainerModel,
key: string, value: string | number | boolean | number[],
type: PropertyType,
symbols: Map<string, ISymbolModel>
): 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);
}