Merged PR 167: Add Flex and fix bugs (read desc)

Note: The branch name does not fit the new features.

- Implement Flex with simplex
- Enable rigid body by default (removed IsRigidBody property) <=== possibly a bad idea
- Sort children in add and update properties
- Implement MaxWidth
- Add more docs

Fixes :
- .env.production url
- Symbols: not blocking the linked container when the parent is moving
This commit is contained in:
Eric Nguyen 2022-08-25 13:28:32 +00:00
parent ec3fddec9d
commit 7f3f6a489a
43 changed files with 1127 additions and 453 deletions

View file

@ -1,9 +1,10 @@
import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { PropertyType } from '../../Enums/PropertyType';
import { XPositionReference } from '../../Enums/XPositionReference';
import IContainerProperties from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { restoreX, transformX } from '../../utils/svg';
import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, restoreX, transformX } from '../../utils/svg';
import { InputGroup } from '../InputGroup/InputGroup';
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
import { Select } from '../Select/Select';
@ -11,12 +12,12 @@ import { Select } from '../Select/Select';
interface IContainerFormProps {
properties: IContainerProperties
symbols: Map<string, ISymbolModel>
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
}
const getCSSInputs = (
properties: IContainerProperties,
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
onChange: (key: string, value: string | number | boolean, type: PropertyType) => void
): JSX.Element[] => {
const groupInput: JSX.Element[] = [];
for (const key in properties.style) {
@ -28,7 +29,7 @@ const getCSSInputs = (
inputClassName=''
type='string'
value={(properties.style as any)[key]}
onChange={(event) => onChange(key, event.target.value, true)}
onChange={(event) => onChange(key, event.target.value, PropertyType.STYLE)}
/>);
}
return groupInput;
@ -52,7 +53,16 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
labelClassName=''
inputClassName=''
type='string'
value={props.properties.parentId?.toString()}
value={props.properties.parentId}
isDisabled={true}
/>
<InputGroup
labelText='Type'
inputKey='type'
labelClassName=''
inputClassName=''
type='string'
value={props.properties.type}
isDisabled={true}
/>
<InputGroup
@ -71,8 +81,18 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName=''
type='number'
isDisabled={props.properties.linkedSymbolId !== ''}
value={transformX(props.properties.x, props.properties.width, props.properties.XPositionReference).toString()}
onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.properties.width, props.properties.XPositionReference))}
value={transformX(RemoveXMargin(props.properties.x, props.properties.margin.left), props.properties.width, props.properties.XPositionReference).toString()}
onChange={(event) => props.onChange(
'x',
ApplyXMargin(
restoreX(
Number(event.target.value),
props.properties.width,
props.properties.XPositionReference
),
props.properties.margin.left
)
)}
/>
<InputGroup
labelText='y'
@ -80,8 +100,8 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
labelClassName=''
inputClassName=''
type='number'
value={props.properties.y.toString()}
onChange={(event) => props.onChange('y', Number(event.target.value))}
value={(props.properties.y - (props.properties.margin?.top ?? 0)).toString()}
onChange={(event) => props.onChange('y', Number(event.target.value) + (props.properties.margin?.top ?? 0))}
/>
<InputGroup
labelText='Minimum width'
@ -93,6 +113,16 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
value={props.properties.minWidth.toString()}
onChange={(event) => props.onChange('minWidth', Number(event.target.value))}
/>
<InputGroup
labelText='Maximum width'
inputKey='maxWidth'
labelClassName=''
inputClassName=''
type='number'
min={1}
value={props.properties.maxWidth.toString()}
onChange={(event) => props.onChange('maxWidth', Number(event.target.value))}
/>
<InputGroup
labelText='Width'
inputKey='width'
@ -100,8 +130,9 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName=''
type='number'
min={props.properties.minWidth}
value={props.properties.width.toString()}
onChange={(event) => props.onChange('width', Number(event.target.value))}
value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()}
onChange={(event) => props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))}
isDisabled={props.properties.isFlex}
/>
<InputGroup
labelText='Height'
@ -110,17 +141,57 @@ const ContainerForm: React.FunctionComponent<IContainerFormProps> = (props) => {
inputClassName=''
type='number'
min={0}
value={props.properties.height.toString()}
onChange={(event) => props.onChange('height', Number(event.target.value))}
value={(props.properties.height + (props.properties.margin?.top ?? 0) + (props.properties.margin?.bottom ?? 0)).toString()}
onChange={(event) => props.onChange('height', Number(event.target.value) - (props.properties.margin?.top ?? 0) - (props.properties.margin?.bottom ?? 0))}
/>
<InputGroup
labelText='Rigid'
inputKey='isRigidBody'
labelText='Margin left'
inputKey='left'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.margin.left ?? 0).toString()}
onChange={(event) => props.onChange('left', Number(event.target.value), PropertyType.MARGIN)}
/>
<InputGroup
labelText='Margin bottom'
inputKey='bottom'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.margin.bottom ?? 0).toString()}
onChange={(event) => props.onChange('bottom', Number(event.target.value), PropertyType.MARGIN)}
/>
<InputGroup
labelText='Margin top'
inputKey='top'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.margin.top ?? 0).toString()}
onChange={(event) => props.onChange('top', Number(event.target.value), PropertyType.MARGIN)}
/>
<InputGroup
labelText='Margin right'
inputKey='right'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.margin.right ?? 0).toString()}
onChange={(event) => props.onChange('right', Number(event.target.value), PropertyType.MARGIN)}
/>
<InputGroup
labelText='Flex'
inputKey='isFlex'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.isRigidBody}
onChange={(event) => props.onChange('isRigidBody', event.target.checked)}
checked={props.properties.isFlex}
onChange={(event) => props.onChange('isFlex', event.target.checked)}
/>
<InputGroup
labelText='Anchor'

View file

@ -22,6 +22,7 @@ describe.concurrent('Properties', () => {
it('Some properties, change values with dynamic input', () => {
const prop: IContainerProperties = {
id: 'stuff',
type: 'type',
parentId: 'parentId',
linkedSymbolId: '',
displayedText: 'stuff',
@ -30,8 +31,10 @@ describe.concurrent('Properties', () => {
width: 1,
height: 1,
minWidth: 1,
maxWidth: Infinity,
margin: {},
XPositionReference: XPositionReference.Left,
isRigidBody: false,
isFlex: false,
isAnchor: false
};

View file

@ -1,4 +1,5 @@
import React from 'react';
import { PropertyType } from '../../Enums/PropertyType';
import IContainerProperties from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import ContainerForm from './ContainerForm';
@ -6,7 +7,7 @@ import ContainerForm from './ContainerForm';
interface IPropertiesProps {
properties?: IContainerProperties
symbols: Map<string, ISymbolModel>
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
}
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {

View file

@ -10,6 +10,9 @@ import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTY
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
@ -87,6 +90,8 @@ export function DeleteContainer(
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
}
Flex(container);
// Select the previous container
// or select the one above
const SelectedContainerId = GetSelectedContainerOnDelete(
@ -147,7 +152,7 @@ export function AddContainerToSelectedContainer(
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
if (selected === null ||
selected === undefined) {
selected === undefined) {
return;
}
@ -213,9 +218,17 @@ export function AddContainer(
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;
const y = containerConfig.DefaultY ?? 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);
@ -225,6 +238,8 @@ export function AddContainer(
parentClone,
x,
y,
width,
height,
containerConfig
);
@ -238,17 +253,15 @@ export function AddContainer(
}
);
ApplyBehaviors(newContainer, current.Symbols);
// And push it the the parent children
if (index === parentClone.children.length) {
parentClone.children.push(newContainer);
} else {
parentClone.children.splice(index, 0, newContainer);
}
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}`,
@ -262,6 +275,16 @@ export function AddContainer(
setHistoryCurrentStep(history.length - 1);
}
function UpdateParentChildrenList(parentClone: IContainerModel | null): void {
if (parentClone === null) {
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,
@ -286,8 +309,17 @@ function InitializeDefaultChild(
}
seen.add(currentConfig.Type);
const x = currentConfig.DefaultX ?? 0;
const y = currentConfig.DefaultY ?? 0;
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];
@ -297,6 +329,8 @@ function InitializeDefaultChild(
parent,
x,
y,
width,
height,
currentConfig
);
@ -353,7 +387,7 @@ function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, par
export function OnPropertyChange(
key: string,
value: string | number | boolean,
isStyle: boolean = false,
type: PropertyType = PropertyType.SIMPLE,
selected: IContainerModel | undefined,
fullHistory: IHistoryState[],
historyCurrentStep: number,
@ -364,7 +398,7 @@ export function OnPropertyChange(
const current = history[history.length - 1];
if (selected === null ||
selected === undefined) {
selected === undefined) {
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
@ -375,22 +409,7 @@ export function OnPropertyChange(
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
const oldSymbolId = container.properties.linkedSymbolId;
if (isStyle) {
(container.properties.style as any)[key] = value;
} else {
(container.properties as any)[key] = value;
}
LinkSymbol(
container.properties.id,
oldSymbolId,
container.properties.linkedSymbolId,
current.Symbols
);
ApplyBehaviors(container, current.Symbols);
SetContainer(container, key, value, type, current.Symbols);
history.push({
LastAction: `Change ${key} of ${container.properties.id}`,
@ -404,6 +423,79 @@ export function OnPropertyChange(
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,
@ -422,3 +514,10 @@ function LinkSymbol(
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));
}

View file

@ -28,7 +28,7 @@ export function ApplyAnchor(container: IContainerModel): IContainerModel {
}
const rigidBodies = container.parent.children.filter(
child => child.properties.isRigidBody && !child.properties.isAnchor
child => !child.properties.isAnchor
);
const overlappingContainers = getOverlappingContainers(container, rigidBodies);

View file

@ -2,6 +2,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default';
import { ApplyAnchor } from './AnchorBehaviors';
import { Flex } from './FlexBehaviors';
import { ApplyRigidBody } from './RigidBodyBehaviors';
import { ApplySymbol } from './SymbolBehaviors';
@ -16,10 +17,12 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map<string,
ApplyAnchor(container);
}
if (container.properties.isRigidBody) {
ApplyRigidBody(container);
if (container.properties.isFlex) {
Flex(container);
}
ApplyRigidBody(container);
const symbol = symbols.get(container.properties.linkedSymbolId);
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
ApplySymbol(container, symbol);

View file

@ -0,0 +1,144 @@
import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { reversePairwise } from '../../../utils/itertools';
import { Simplex } from '../../../utils/simplex';
import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg';
/**
* Try to push the siblings
* @param container
* @returns
*/
export function PushContainers(container: IContainerModel): IContainerModel {
if (container.parent === null) {
return container;
}
if (container.parent.children.length <= 1) {
return container;
}
const prevIndex = container.parent.children.length - 2;
const prev: IContainerModel = container.parent.children[prevIndex];
const isOverlapping = prev.properties.x + prev.properties.width > container.properties.x;
if (!isOverlapping) {
return container;
}
// find hole
let lastContainer: IContainerModel | null = null;
let space: number = 0;
while (space.toFixed(2) < container.properties.width.toFixed(2)) {
// FIXME: possible infinite loop due to floating point
// FIXME: A fix was applied using toFixed(2).
// FIXME: A coverture check must be done to ensure that all scenarios are covered
const it = reversePairwise<IContainerModel>(container.parent.children.filter(child => child !== container));
for (const { cur, next } of it) {
const hasSpaceBetween = next.properties.x + next.properties.width < cur.properties.x;
if (hasSpaceBetween) {
lastContainer = cur;
space = cur.properties.x - (next.properties.x + next.properties.width);
break;
}
}
if (lastContainer === null) {
// no space between
break;
}
const indexLastContainer = container.parent.children.indexOf(lastContainer);
for (let i = indexLastContainer; i <= container.parent.children.length - 2; i++) {
const sibling = container.parent.children[i];
sibling.properties.x -= space;
}
}
const hasNoSpaceBetween = lastContainer === null;
if (hasNoSpaceBetween) {
// test gap between the left of the parent and the first container
space = container.parent.children[0].properties.x;
if (space > 0) {
for (let i = 0; i <= container.parent.children.length - 2; i++) {
const sibling = container.parent.children[i];
sibling.properties.x -= space;
}
return container;
}
}
Flex(container);
return container;
}
/**
* Flex the container and its siblings (mutate)
* @param container Container to flex
* @returns Flexed container
*/
export function Flex(container: IContainerModel): void {
if (container.parent === null || container.parent === undefined) {
return;
}
const flexibleContainers = container.parent.children
.filter(sibling => sibling.properties.isFlex);
const minWidths = flexibleContainers
.map(sibling => sibling.properties.minWidth);
const fixedWidth = container.parent.children
.filter(sibling => !sibling.properties.isFlex)
.map(sibling => sibling.properties.width)
.reduce((partialSum, a) => partialSum + a, 0);
const requiredMaxWidth = container.parent.properties.width - fixedWidth;
const minimumPossibleWidth = minWidths.reduce((partialSum, a) => partialSum + a, 0);
if (minimumPossibleWidth > requiredMaxWidth) {
Swal.fire({
icon: 'error',
title: 'Cannot fit!',
text: 'Cannot fit at all even when squeezing all flex containers to the minimum.'
});
return;
}
const maxMinWidths = Math.max(...minWidths);
if (maxMinWidths * minWidths.length < requiredMaxWidth) {
const wantedWidth = requiredMaxWidth / minWidths.length;
// it fits, flex with maxMinWidths and fixed width
let right = 0;
for (const sibling of container.parent.children) {
if (!sibling.properties.isFlex) {
sibling.properties.x = right;
right += sibling.properties.width;
continue;
}
sibling.properties.x = ApplyXMargin(right, sibling.properties.margin.left);
sibling.properties.width = ApplyWidthMargin(wantedWidth, sibling.properties.margin.left, sibling.properties.margin.right);
right += wantedWidth;
}
return;
}
// does not fit
/// SIMPLEX ///
const solutions: number[] = Simplex(minWidths, requiredMaxWidth);
// apply the solutions
for (let i = 0; i < flexibleContainers.length; i++) {
flexibleContainers[i].properties.width = ApplyWidthMargin(solutions[i], flexibleContainers[i].properties.margin.left, flexibleContainers[i].properties.margin.right)
}
// move the containers
let right = 0;
for (const sibling of container.parent.children) {
sibling.properties.x = ApplyXMargin(right, sibling.properties.margin.left);
right += sibling.properties.width;
}
}

View file

@ -9,6 +9,7 @@
import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer';
import { PushContainers } from './FlexBehaviors';
/**
* "Transform the container into a rigid body"
@ -23,6 +24,7 @@ export function ApplyRigidBody(
container: IContainerModel
): IContainerModel {
container = constraintBodyInsideParent(container);
container = PushContainers(container);
container = constraintBodyInsideUnallocatedWidth(container);
return container;
}
@ -183,7 +185,6 @@ export function constraintBodyInsideUnallocatedWidth(
showConfirmButton: false,
timer: 5000
});
container.properties.isRigidBody = false;
return container;
}
@ -238,8 +239,7 @@ function getAvailableWidths(
// Ignore the exception
// And we will also only uses containers that also are rigid or are anchors
if (child === exception ||
(!child.properties.isRigidBody && !child.properties.isAnchor)) {
if (child === exception) {
continue;
}
const childX = child.properties.x;

View file

@ -1,9 +1,12 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { applyParentTransform } from '../../../utils/itertools';
import { restoreX, transformX } from '../../../utils/svg';
export function ApplySymbol(container: IContainerModel, symbol: ISymbolModel): IContainerModel {
container.properties.x = transformX(symbol.x, symbol.width, symbol.config.XPositionReference);
container.properties.x = restoreX(container.properties.x, container.properties.width, container.properties.XPositionReference);
const [x] = applyParentTransform(container.parent, container.properties.x, 0);
container.properties.x = x;
return container;
}

View file

@ -4,7 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { SVG } from '../SVG/SVG';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer, OnPropertyChange } from './Actions/ContainerOperations';
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange } from './Actions/ContainerOperations';
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
import { onKeyDown } from './Actions/Shortcuts';
import EditorEvents from '../../Events/EditorEvents';
@ -60,7 +60,9 @@ function useWindowEvents(
history: IHistoryState[],
historyCurrentStep: number,
configuration: IConfiguration,
editorRef: React.RefObject<HTMLDivElement>
editorRef: React.RefObject<HTMLDivElement>,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
useEffect(() => {
const events = EditorEvents;
@ -72,7 +74,12 @@ function useWindowEvents(
const funcs = new Map<string, () => void>();
for (const event of events) {
const func = (): void => event.func(editorState);
const func = (eventInitDict?: CustomEventInit): void => event.func(
editorState,
setHistory,
setHistoryCurrentStep,
eventInitDict
);
editorRef.current?.addEventListener(event.name, func);
funcs.set(event.name, func);
}
@ -94,7 +101,14 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
const editorRef = useRef<HTMLDivElement>(null);
useShortcuts(history, historyCurrentStep, setHistoryCurrentStep);
useWindowEvents(history, historyCurrentStep, props.configuration, editorRef);
useWindowEvents(
history,
historyCurrentStep,
props.configuration,
editorRef,
setHistory,
setHistoryCurrentStep
);
const configuration = props.configuration;
const current = getCurrentHistoryState(history, historyCurrentStep);
@ -122,15 +136,15 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory,
setHistoryCurrentStep
)}
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
key, value, isStyle,
OnPropertyChange={(key, value, type) => OnPropertyChange(
key, value, type,
selected,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
AddContainer={(type) => AddContainerToSelectedContainer(
type,
selected,
configuration,
@ -139,16 +153,6 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
setHistory,
setHistoryCurrentStep
)}
AddContainer={(index, type, parentId) => AddContainer(
index,
type,
parentId,
configuration,
history,
historyCurrentStep,
setHistory,
setHistoryCurrentStep
)}
AddSymbol={(type) => AddSymbol(
type,
configuration,

View file

@ -22,9 +22,12 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 2000,
height: 100,
margin: {},
minWidth: 1,
type: 'type',
maxWidth: Infinity,
isFlex: false,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
isAnchor: false
},
userData: {}
@ -35,7 +38,6 @@ describe.concurrent('Elements sidebar', () => {
OnPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -56,8 +58,11 @@ describe.concurrent('Elements sidebar', () => {
y: 0,
width: 2000,
height: 100,
margin: {},
minWidth: 1,
isRigidBody: false,
isFlex: false,
maxWidth: Infinity,
type: 'type',
isAnchor: false,
XPositionReference: XPositionReference.Left
},
@ -73,7 +78,6 @@ describe.concurrent('Elements sidebar', () => {
OnPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -119,7 +123,10 @@ describe.concurrent('Elements sidebar', () => {
width: 2000,
height: 100,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
margin: {},
isFlex: false,
maxWidth: Infinity,
type: 'type',
isAnchor: false
},
userData: {}
@ -139,7 +146,10 @@ describe.concurrent('Elements sidebar', () => {
minWidth: 1,
width: 0,
height: 0,
isRigidBody: false,
margin: {},
isFlex: false,
maxWidth: Infinity,
type: 'type',
isAnchor: false,
XPositionReference: XPositionReference.Left
},
@ -158,11 +168,14 @@ describe.concurrent('Elements sidebar', () => {
displayedText: 'child-2',
x: 0,
y: 0,
margin: {},
minWidth: 1,
width: 0,
height: 0,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
isFlex: false,
maxWidth: Infinity,
type: 'type',
isAnchor: false
},
userData: {}
@ -178,7 +191,6 @@ describe.concurrent('Elements sidebar', () => {
OnPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -204,7 +216,10 @@ describe.concurrent('Elements sidebar', () => {
width: 2000,
height: 100,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
margin: {},
isFlex: false,
maxWidth: Infinity,
type: 'type',
isAnchor: false
},
userData: {}
@ -224,7 +239,10 @@ describe.concurrent('Elements sidebar', () => {
width: 0,
height: 0,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
margin: {},
isFlex: false,
maxWidth: Infinity,
type: 'type',
isAnchor: false
},
userData: {}
@ -245,7 +263,6 @@ describe.concurrent('Elements sidebar', () => {
OnPropertyChange={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -269,7 +286,6 @@ describe.concurrent('Elements sidebar', () => {
OnPropertyChange={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();

View file

@ -5,10 +5,10 @@ import { IContainerModel } from '../../Interfaces/IContainerModel';
import { getDepth, MakeIterator } from '../../utils/itertools';
import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem';
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick, useMouseEvents } from './MouseEventHandlers';
import { useMouseEvents } from './MouseEventHandlers';
import { IPoint } from '../../Interfaces/IPoint';
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { Dispatch, RefObject, SetStateAction } from 'react';
import { PropertyType } from '../../Enums/PropertyType';
interface IElementsSidebarProps {
MainContainer: IContainerModel
@ -16,16 +16,23 @@ interface IElementsSidebarProps {
isOpen: boolean
isHistoryOpen: boolean
SelectedContainer: IContainerModel | undefined
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
OnPropertyChange: (
key: string,
value: string | number | boolean,
type?: PropertyType
) => void
SelectContainer: (containerId: string) => void
DeleteContainer: (containerid: string) => void
AddContainer: (index: number, type: string, parent: string) => void
}
export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElementsSidebarProps): JSX.Element => {
export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
props: IElementsSidebarProps
): JSX.Element => {
// States
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
const [isContextMenuOpen, setIsContextMenuOpen] =
React.useState<boolean>(false);
const [onClickContainerId, setOnClickContainerId] =
React.useState<string>('');
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
x: 0,
y: 0
@ -45,71 +52,78 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
// Render
let isOpenClasses = '-right-64';
if (props.isOpen) {
isOpenClasses = props.isHistoryOpen
? 'right-64'
: 'right-0';
isOpenClasses = props.isHistoryOpen ? 'right-64' : 'right-0';
}
const it = MakeIterator(props.MainContainer);
const containers = [...it];
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => {
const Row = ({
index,
style
}: {
index: number
style: React.CSSProperties
}): JSX.Element => {
const container = containers[index];
const depth: number = getDepth(container);
const key = container.properties.id.toString();
const text = container.properties.displayedText === key
? `${'|\t'.repeat(depth)} ${key}`
: `${'|\t'.repeat(depth)} ${container.properties.displayedText} (${key})`;
const selectedClass: string = props.SelectedContainer !== undefined &&
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
const text =
container.properties.displayedText === key
? `${'|\t'.repeat(depth)} ${key}`
: `${'|\t'.repeat(depth)} ${
container.properties.displayedText
} (${key})`;
const selectedClass: string =
props.SelectedContainer !== undefined &&
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
return (
<button
className={
`w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`
}
className={`w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`}
id={key}
key={key}
style={style}
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
onDragLeave={(event) => handleDragLeave(event)}
onClick={() => props.SelectContainer(container.properties.id)}
>
{ text }
{text}
</button>
);
};
return (
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
<div className='bg-slate-100 font-bold sidebar-title'>
Elements
</div>
<div ref={elementRef} className='h-96 text-gray-800'>
<div
className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}
>
<div className="bg-slate-100 font-bold sidebar-title">Elements</div>
<div ref={elementRef} className="h-96 text-gray-800">
<List
className='List divide-y divide-black'
className="List divide-y divide-black"
itemCount={containers.length}
itemSize={35}
height={384}
width={256}
>
{ Row }
{Row}
</List>
</div>
<Menu
className='transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl'
className="transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl"
x={contextMenuPosition.x}
y={contextMenuPosition.y}
isOpen={isContextMenuOpen}
>
<MenuItem className='contextmenu-item' text='Delete' onClick={() => {
setIsContextMenuOpen(false);
props.DeleteContainer(onClickContainerId);
}} />
<MenuItem
className="contextmenu-item"
text="Delete"
onClick={() => {
setIsContextMenuOpen(false);
props.DeleteContainer(onClickContainerId);
}}
/>
</Menu>
<Properties
properties={props.SelectedContainer?.properties}

View file

@ -1,7 +1,5 @@
import React, { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { IPoint } from '../../Interfaces/IPoint';
import { findContainerById } from '../../utils/itertools';
export function useMouseEvents(
isContextMenuOpen: boolean,
@ -81,95 +79,3 @@ export function handleLeftClick(
setOnClickContainerId('');
}
export function removeBorderClasses(target: HTMLButtonElement): void {
const bordersClasses = ['border-t-8', 'border-8', 'border-b-8'];
target.classList.remove(...bordersClasses);
}
export function handleDragLeave(event: React.DragEvent): void {
const target: HTMLButtonElement = event.target as HTMLButtonElement;
removeBorderClasses(target);
}
export function handleDragOver(
event: React.DragEvent,
mainContainer: IContainerModel
): void {
event.preventDefault();
const target: HTMLButtonElement = event.target as HTMLButtonElement;
const rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element.
removeBorderClasses(target);
if (target.id === mainContainer.properties.id) {
target.classList.add('border-8');
return;
}
if (y < 12) {
target.classList.add('border-t-8');
} else if (y < 24) {
target.classList.add('border-8');
} else {
target.classList.add('border-b-8');
}
}
export function handleOnDrop(
event: React.DragEvent,
mainContainer: IContainerModel,
addContainer: (index: number, type: string, parent: string) => void
): void {
event.preventDefault();
const type = event.dataTransfer.getData('type');
const target: HTMLButtonElement = event.target as HTMLButtonElement;
removeBorderClasses(target);
const targetContainer: IContainerModel | undefined = findContainerById(
mainContainer,
target.id
);
if (targetContainer === undefined) {
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
}
if (targetContainer === mainContainer) {
// if the container is the root, only add type as child
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
return;
}
if (targetContainer.parent === null ||
targetContainer.parent === undefined) {
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
}
const rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element.
// locate the hitboxes
if (y < 12) {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index,
type,
targetContainer.parent.properties.id
);
} else if (y < 24) {
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
} else {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index + 1,
type,
targetContainer.parent.properties.id
);
}
}

View file

@ -18,10 +18,17 @@ interface IContainerProps {
*/
export const Container: React.FC<IContainerProps> = (props: IContainerProps) => {
const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
const xText = props.model.properties.width / 2;
const yText = props.model.properties.height / 2;
const transform = `translate(${props.model.properties.x}, ${props.model.properties.y})`;
const width: number = props.model.properties.width;
const height: number = props.model.properties.height;
const x = props.model.properties.x;
const y = props.model.properties.y;
const xText = width / 2;
const yText = height / 2;
const transform = `translate(${x}, ${y})`;
// g style
const defaultStyle: React.CSSProperties = {
@ -39,8 +46,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
const svg = (props.model.properties.customSVG != null)
? CreateReactCustomSVG(props.model.properties.customSVG, props.model.properties)
: (<rect
width={props.model.properties.width}
height={props.model.properties.height}
width={width}
height={height}
style={style}
>
</rect>);
@ -49,10 +56,10 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
const dimensionMargin = DIMENSION_MARGIN * depth;
const id = `dim-${props.model.properties.id}`;
const xStart: number = 0;
const xEnd = props.model.properties.width;
const y = -dimensionMargin;
const xEnd = width;
const yDim = -dimensionMargin;
const strokeWidth = 1;
const text = (props.model.properties.width ?? 0).toString();
const text = (width ?? 0).toString();
let dimensionChildren: JSX.Element | null = null;
if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) {
@ -80,35 +87,34 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
transform={transform}
key={`container-${props.model.properties.id}`}
>
{ SHOW_PARENT_DIMENSION
{SHOW_PARENT_DIMENSION
? <Dimension
id={id}
xStart={xStart}
xEnd={xEnd}
yStart={y}
yEnd={y}
yStart={yDim}
yEnd={yDim}
strokeWidth={strokeWidth}
text={text}
/>
: null
}
{ dimensionChildren }
{ svg }
{ SHOW_TEXT
{dimensionChildren}
{svg}
{SHOW_TEXT
? <text
x={xText}
y={yText}
>
{props.model.properties.displayedText}
</text>
: null }
{ containersElements }
: null}
{containersElements}
</g>
);
};
function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number):
{ childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } {
function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): { childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } {
const childrenId = `dim-children-${props.model.properties.id}`;
const lastChild = props.model.children[props.model.children.length - 1];
@ -146,7 +152,7 @@ function CreateReactCustomSVG(customSVG: string, props: IContainerProperties): R
function transform(node: HTMLElement, children: Node[], props: IContainerProperties): React.ReactNode {
const supportedTags = ['line', 'path', 'rect'];
if (supportedTags.includes(node.tagName.toLowerCase())) {
const attributes: {[att: string]: string | object | null} = {};
const attributes: { [att: string]: string | object | null } = {};
node.getAttributeNames().forEach(attName => {
const attributeValue = node.getAttribute(attName);
if (attributeValue === null) {

View file

@ -1,6 +1,7 @@
import * as React from 'react';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { getAbsolutePosition } from '../../../utils/itertools';
import { RemoveMargin } from '../../../utils/svg';
interface ISelectorProps {
selected?: IContainerModel
@ -14,11 +15,19 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
);
}
const [x, y] = getAbsolutePosition(props.selected);
const [width, height] = [
let [x, y] = getAbsolutePosition(props.selected);
let [width, height] = [
props.selected.properties.width,
props.selected.properties.height
];
({ x, y, width, height } = RemoveMargin(x, y, width, height,
props.selected.properties.margin.left,
props.selected.properties.margin.bottom,
props.selected.properties.margin.top,
props.selected.properties.margin.right
));
const style: React.CSSProperties = {
stroke: '#3B82F6', // tw blue-500
strokeWidth: 4,

View file

@ -11,6 +11,7 @@ import { Bar } from '../Bar/Bar';
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { Symbols } from '../Symbols/Symbols';
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
import { PropertyType } from '../../Enums/PropertyType';
interface IUIProps {
SelectedContainer: IContainerModel | undefined
@ -21,9 +22,8 @@ interface IUIProps {
AvailableSymbols: IAvailableSymbol[]
SelectContainer: (containerId: string) => void
DeleteContainer: (containerId: string) => void
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
AddContainerToSelectedContainer: (type: string) => void
AddContainer: (index: number, type: string, parentId: string) => void
OnPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
AddContainer: (type: string) => void
AddSymbol: (type: string) => void
OnSymbolPropertyChange: (key: string, value: string | number | boolean) => void
SelectSymbol: (symbolId: string) => void
@ -75,7 +75,7 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
<Sidebar
componentOptions={props.AvailableContainers}
isOpen={isSidebarOpen}
buttonOnClick={props.AddContainerToSelectedContainer}
buttonOnClick={props.AddContainer}
/>
<Symbols
componentOptions={props.AvailableSymbols}
@ -91,7 +91,6 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
OnPropertyChange={props.OnPropertyChange}
SelectContainer={props.SelectContainer}
DeleteContainer={props.DeleteContainer}
AddContainer={props.AddContainer}
/>
<SymbolsSidebar
SelectedSymbolId={props.current.SelectedSymbolId}