440 lines
14 KiB
TypeScript
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);
|
|
}
|