Merged PR 173: Implements API methods through right click + more (read desc)

- 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
This commit is contained in:
Eric Nguyen 2022-08-30 14:45:29 +00:00
parent 79c6874240
commit 57e6c9a156
20 changed files with 652 additions and 291 deletions

View file

@ -1,4 +1,3 @@
import { Dispatch, SetStateAction } from 'react';
import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { IConfiguration } from '../../../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel';
@ -16,14 +15,13 @@ import { PropertyType } from '../../../Enums/PropertyType';
/**
* Select a container
* @param container Selected container
* @returns New history
*/
export function SelectContainer(
containerId: string,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
historyCurrentStep: number
): IHistoryState[] {
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
@ -35,8 +33,7 @@ export function SelectContainer(
symbols: structuredClone(current.symbols),
selectedSymbolId: current.selectedSymbolId
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
return history;
}
/**
@ -44,16 +41,13 @@ export function SelectContainer(
* @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
* @returns New history
*/
export function DeleteContainer(
containerId: string,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
historyCurrentStep: number
): IHistoryState[] {
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
@ -80,7 +74,7 @@ export function DeleteContainer(
}
const newSymbols = structuredClone(current.symbols);
UnlinkSymbol(newSymbols, container);
UnlinkContainerFromSymbols(newSymbols, container);
const index = container.parent.children.indexOf(container);
if (index > -1) {
@ -108,10 +102,21 @@ export function DeleteContainer(
symbols: newSymbols,
selectedSymbolId: current.selectedSymbolId
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
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,
@ -125,7 +130,13 @@ function GetSelectedContainerOnDelete(
return newSelectedContainerId;
}
function UnlinkSymbol(symbols: Map<string, ISymbolModel>, container: IContainerModel): void {
/**
* 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);
@ -142,8 +153,6 @@ function UnlinkSymbol(symbols: Map<string, ISymbolModel>, container: IContainerM
* @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(
@ -151,28 +160,153 @@ export function AddContainerToSelectedContainer(
selected: IContainerModel | undefined,
configuration: IConfiguration,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
historyCurrentStep: number
): IHistoryState[] | null {
if (selected === null ||
selected === undefined) {
return;
return null;
}
const parent = selected;
AddContainer(
return AddContainer(
parent.children.length,
type,
parent.properties.id,
configuration,
fullHistory,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
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
@ -183,7 +317,7 @@ export function AddContainerToSelectedContainer(
* @param historyCurrentStep Current step
* @param setHistory State setter of History
* @param setHistoryCurrentStep State setter of the current step
* @returns void
* @returns new history
*/
export function AddContainer(
index: number,
@ -191,104 +325,55 @@ export function AddContainer(
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
historyCurrentStep: number
): IHistoryState[] {
// just call AddContainers with an array on a single element
return AddContainers(
index,
[type],
parentId,
configuration,
fullHistory,
historyCurrentStep
);
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);
}
/**
* 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) => TransformX(a.properties.x, a.properties.width, a.properties.xPositionReference) -
TransformX(b.properties.x, b.properties.width, b.properties.xPositionReference)
(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,
@ -368,12 +453,18 @@ function InitializeDefaultChild(
* @param x Additionnal offset
* @returns New offset
*/
function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, parent: IContainerModel, x: number): number {
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);
const lastChild: IContainerModel | undefined = parent.children
.at(index - 1);
if (lastChild !== undefined) {
x += (lastChild.properties.x + lastChild.properties.width);
@ -394,10 +485,8 @@ export function OnPropertyChange(
type: PropertyType = PropertyType.Simple,
selected: IContainerModel | undefined,
fullHistory: IHistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
historyCurrentStep: number
): IHistoryState[] {
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
@ -423,8 +512,7 @@ export function OnPropertyChange(
symbols: structuredClone(current.symbols),
selectedSymbolId: current.selectedSymbolId
});
setHistory(history);
setHistoryCurrentStep(history.length - 1);
return history;
}
/**
@ -465,6 +553,13 @@ function SetContainer(
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:
@ -477,7 +572,12 @@ function AssignProperty(container: ContainerModel, key: string, value: string |
(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) {
@ -500,6 +600,14 @@ function AssignProperty(container: ContainerModel, key: string, value: string |
}
}
/**
* 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,
@ -519,10 +627,18 @@ function LinkSymbol(
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));
newContainer.parent.children
.filter(container => newContainer !== container)
.forEach(container => ApplyBehaviors(container, symbols));
}