svg-layout-designer-react/src/Components/Editor/Actions/ContainerOperations.ts
Eric Nguyen 3f58c5ba5e Merged PR 169: Fix bugs with flex + Disable obnoxious swals + Add selector text + Sort SVG scss to different files
- Disable PushBehavior and set it in a different file
- Fix behviors when parent === undefined
- Fix flex behavrios when using anchors
- Fix siblings not applying flex to theirs children on container delete
- Fix flex behavior when using anchors
- Enable flex by default
- Disable obnoxious swals
- Add selector text
- Sort SVG scss to different files

Others: Add some todos
2022-08-26 13:59:03 +00:00

523 lines
17 KiB
TypeScript

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<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): 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<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): 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<string, ISymbolModel>, 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<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): 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<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): 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<string, number>
): 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<string>([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<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): 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<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
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<string, ISymbolModel>
): 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<string, ISymbolModel>): void {
if (newContainer.parent === null || newContainer.parent === undefined) {
return;
}
newContainer.parent.children.filter(container => newContainer !== container).forEach(container => ApplyBehaviors(container, Symbols));
}