- Implements API methods through right click - Refactor events - Refactor usage of setHistory and setHistoryCurrentStep into a single function - Update ContainerOperations documentations - Added AddContainers in order to add multiple containers + refactor AddContainer to use it - Fix regression - Fix AddContainer at index
644 lines
20 KiB
TypeScript
644 lines
20 KiB
TypeScript
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<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
|
|
* @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<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
|
|
): 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<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);
|
|
}
|
|
|
|
/**
|
|
* 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<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);
|
|
}
|
|
|
|
/**
|
|
* Iterate over the siblings of newContainer and apply the behaviors
|
|
* @param newContainer
|
|
* @param symbols
|
|
* @returns
|
|
*/
|
|
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));
|
|
}
|