This commit is contained in:
commit
0452a17454
32 changed files with 784 additions and 234 deletions
14
.drone.yml
14
.drone.yml
|
@ -6,11 +6,10 @@ steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: node:16
|
image: node:16
|
||||||
commands:
|
commands:
|
||||||
- curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
|
||||||
- node ./test-server/node-http.js &
|
- node ./test-server/node-http.js &
|
||||||
- pnpm install
|
- npm ci
|
||||||
- pnpm run test:nowatch
|
- npm run test:nowatch
|
||||||
- pnpm run build
|
- npm run build
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
|
@ -20,8 +19,7 @@ steps:
|
||||||
- name: test
|
- name: test
|
||||||
image: node
|
image: node
|
||||||
commands:
|
commands:
|
||||||
- curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
|
||||||
- node ./test-server/node-http.js &
|
- node ./test-server/node-http.js &
|
||||||
- pnpm install
|
- npm ci
|
||||||
- pnpm run test:nowatch
|
- npm run test:nowatch
|
||||||
- pnpm run build
|
- npm run build
|
|
@ -25,7 +25,8 @@ module.exports = {
|
||||||
rules: {
|
rules: {
|
||||||
'space-before-function-paren': ['error', 'never'],
|
'space-before-function-paren': ['error', 'never'],
|
||||||
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
|
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
|
||||||
indent: ['warn', 2, { SwitchCase: 1 }],
|
indent: 'off',
|
||||||
|
'@typescript-eslint/indent': ['warn', 2, {SwitchCase: 1}],
|
||||||
semi: 'off',
|
semi: 'off',
|
||||||
'@typescript-eslint/semi': ['warn', 'always'],
|
'@typescript-eslint/semi': ['warn', 'always'],
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
|
BIN
docs/ComponentStructure.drawio
(Stored with Git LFS)
BIN
docs/ComponentStructure.drawio
(Stored with Git LFS)
Binary file not shown.
|
@ -13,6 +13,7 @@ It depends on Vite in order to build the project.
|
||||||
|
|
||||||
Others dependencies:
|
Others dependencies:
|
||||||
- [react-dom](https://reactjs.org/docs/react-dom.html): library used to inject the app to `#root` html element.
|
- [react-dom](https://reactjs.org/docs/react-dom.html): library used to inject the app to `#root` html element.
|
||||||
|
- [react-window](https://www.npmjs.com/package/react-windows): component that offers component dynamic loading over scroll (very useful++)
|
||||||
- [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element
|
- [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,24 @@ The project is structured this way
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── docs Documentation folder
|
├── docs/ Documentation folder
|
||||||
├── public Public folder in which the index.html
|
├── public/ Public folder in which the index.html
|
||||||
│ import its resources
|
│ import its resources
|
||||||
├── src Source folder for the react app
|
├── src/ Source folder for the react app
|
||||||
│ ├── assets Assets folder in which the react app
|
│ ├── assets/ Assets folder in which the react app
|
||||||
│ │ import its resources
|
│ │ import its resources
|
||||||
│ ├── Components Components folder
|
│ ├── Components/ Components folder
|
||||||
│ ├── Enums Enums folder
|
│ ├── Enums/ Enums folder
|
||||||
│ ├── Interfaces Interface (+ types folder)
|
│ ├── Events/ API Events folder
|
||||||
│ ├── test Setup folder for the tests
|
│ ├── Interfaces/ Interface (+ types folder)
|
||||||
│ ├── tests Other tests + resources
|
│ ├── test/ Setup folder for the tests
|
||||||
│ ├── utils Utilities folder
|
│ ├── tests/ Other tests + resources
|
||||||
|
│ ├── utils/ Utilities folder
|
||||||
|
│ ├── workers/ Webworkers folder
|
||||||
│ ├── index.scss Tailwind CSS extends
|
│ ├── index.scss Tailwind CSS extends
|
||||||
│ ├── main.tsx Entrypoint for App injection
|
│ ├── main.tsx Entrypoint for App injection
|
||||||
│ └── vite-env.d.ts Types for .env files
|
│ └── vite-env.d.ts Types for .env files
|
||||||
├── test-server Tests servers to test the API
|
├── test-server/ Tests servers to test the API
|
||||||
│ ├── http.js Test server for bun.sh
|
│ ├── http.js Test server for bun.sh
|
||||||
│ └── node-http.js Test server for Node.js
|
│ └── node-http.js Test server for Node.js
|
||||||
├── azure-pipelines.yml Azure Pipelines YAML config file
|
├── azure-pipelines.yml Azure Pipelines YAML config file
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { fetchConfiguration } from '../API/api';
|
import { fetchConfiguration } from '../API/api';
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||||
import { LoadState } from './Load';
|
import { LoadState } from './Load';
|
||||||
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
|
||||||
export function NewEditor(
|
export function NewEditor(
|
||||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||||
|
@ -20,12 +21,15 @@ export function NewEditor(
|
||||||
parentId: 'null',
|
parentId: 'null',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: configuration.MainContainer.Width,
|
width: Number(configuration.MainContainer.Width),
|
||||||
height: configuration.MainContainer.Height,
|
height: Number(configuration.MainContainer.Height),
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
fillOpacity: 0,
|
XPositionReference: XPositionReference.Left,
|
||||||
stroke: 'black'
|
style: {
|
||||||
|
fillOpacity: 0,
|
||||||
|
stroke: 'black'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ function getOverlappingContainers(
|
||||||
containers: IContainerModel[]
|
containers: IContainerModel[]
|
||||||
): IContainerModel[] {
|
): IContainerModel[] {
|
||||||
const min1 = container.properties.x;
|
const min1 = container.properties.x;
|
||||||
const max1 = container.properties.x + Number(container.properties.width);
|
const max1 = container.properties.x + container.properties.width;
|
||||||
const overlappingContainers: IContainerModel[] = [];
|
const overlappingContainers: IContainerModel[] = [];
|
||||||
for (const other of containers) {
|
for (const other of containers) {
|
||||||
if (other === container) {
|
if (other === container) {
|
||||||
|
@ -57,7 +57,7 @@ function getOverlappingContainers(
|
||||||
}
|
}
|
||||||
|
|
||||||
const min2 = other.properties.x;
|
const min2 = other.properties.x;
|
||||||
const max2 = other.properties.x + Number(other.properties.width);
|
const max2 = other.properties.x + other.properties.width;
|
||||||
const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0;
|
const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0;
|
||||||
|
|
||||||
if (!isOverlapping) {
|
if (!isOverlapping) {
|
||||||
|
|
|
@ -42,8 +42,8 @@ function constraintBodyInsideParent(
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentProperties = container.parent.properties;
|
const parentProperties = container.parent.properties;
|
||||||
const parentWidth = Number(parentProperties.width);
|
const parentWidth = parentProperties.width;
|
||||||
const parentHeight = Number(parentProperties.height);
|
const parentHeight = parentProperties.height;
|
||||||
|
|
||||||
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
|
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
|
||||||
}
|
}
|
||||||
|
@ -66,10 +66,10 @@ function constraintBodyInsideSpace(
|
||||||
height: number
|
height: number
|
||||||
): IContainerModel {
|
): IContainerModel {
|
||||||
const containerProperties = container.properties;
|
const containerProperties = container.properties;
|
||||||
const containerX = Number(containerProperties.x);
|
const containerX = containerProperties.x;
|
||||||
const containerY = Number(containerProperties.y);
|
const containerY = containerProperties.y;
|
||||||
const containerWidth = Number(containerProperties.width);
|
const containerWidth = containerProperties.width;
|
||||||
const containerHeight = Number(containerProperties.height);
|
const containerHeight = containerProperties.height;
|
||||||
|
|
||||||
// Check size bigger than parent
|
// Check size bigger than parent
|
||||||
const isBodyLargerThanParent = containerWidth > width;
|
const isBodyLargerThanParent = containerWidth > width;
|
||||||
|
@ -121,8 +121,8 @@ export function constraintBodyInsideUnallocatedWidth(
|
||||||
|
|
||||||
// Get the available spaces of the parent
|
// Get the available spaces of the parent
|
||||||
const availableWidths = getAvailableWidths(container.parent, container);
|
const availableWidths = getAvailableWidths(container.parent, container);
|
||||||
const containerX = Number(container.properties.x);
|
const containerX = container.properties.x;
|
||||||
const containerWidth = Number(container.properties.width);
|
const containerWidth = container.properties.width;
|
||||||
|
|
||||||
// Check if there is still some space
|
// Check if there is still some space
|
||||||
if (availableWidths.length === 0) {
|
if (availableWidths.length === 0) {
|
||||||
|
@ -177,7 +177,7 @@ export function constraintBodyInsideUnallocatedWidth(
|
||||||
availableWidthFound.x,
|
availableWidthFound.x,
|
||||||
0,
|
0,
|
||||||
availableWidthFound.width,
|
availableWidthFound.width,
|
||||||
Number(container.parent.properties.height)
|
container.parent.properties.height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +197,7 @@ function getAvailableWidths(
|
||||||
// Initialize the first size pointer
|
// Initialize the first size pointer
|
||||||
// which takes full width of the available space
|
// which takes full width of the available space
|
||||||
const x = 0;
|
const x = 0;
|
||||||
const width = Number(container.properties.width);
|
const width = container.properties.width;
|
||||||
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
|
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
|
||||||
|
|
||||||
// We will only uses containers that also are rigid or are anchors
|
// We will only uses containers that also are rigid or are anchors
|
||||||
|
@ -211,7 +211,7 @@ function getAvailableWidths(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const childX = child.properties.x;
|
const childX = child.properties.x;
|
||||||
const childWidth = Number(child.properties.width);
|
const childWidth = child.properties.width;
|
||||||
|
|
||||||
// get the space of the child that is inside the parent
|
// get the space of the child that is inside the parent
|
||||||
let newUnallocatedSpace: ISizePointer[] = [];
|
let newUnallocatedSpace: ISizePointer[] = [];
|
||||||
|
@ -309,4 +309,4 @@ function getAvailableWidthsTwoLines(
|
||||||
const isFitting = (
|
const isFitting = (
|
||||||
container: IContainerModel,
|
container: IContainerModel,
|
||||||
sizePointer: ISizePointer
|
sizePointer: ISizePointer
|
||||||
): boolean => Number(container.properties.width) <= sizePointer.width;
|
): boolean => container.properties.width <= sizePointer.width;
|
||||||
|
|
|
@ -5,6 +5,9 @@ import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerMode
|
||||||
import { findContainerById } from '../../utils/itertools';
|
import { findContainerById } from '../../utils/itertools';
|
||||||
import { getCurrentHistory } from './Editor';
|
import { getCurrentHistory } from './Editor';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import IProperties from '../../Interfaces/IProperties';
|
||||||
|
import { AddMethod } from '../../Enums/AddMethod';
|
||||||
|
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||||
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a container
|
* Select a container
|
||||||
|
@ -54,7 +57,7 @@ export function DeleteContainer(
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
): void {
|
): void {
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
const current = history[historyCurrentStep];
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
||||||
const container = findContainerById(mainContainerClone, containerId);
|
const container = findContainerById(mainContainerClone, containerId);
|
||||||
|
@ -169,10 +172,10 @@ export function AddContainer(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the preset properties from the API
|
// Get the preset properties from the API
|
||||||
const properties = configuration.AvailableContainers
|
const containerConfig = configuration.AvailableContainers
|
||||||
.find(option => option.Type === type);
|
.find(option => option.Type === type);
|
||||||
|
|
||||||
if (properties === undefined) {
|
if (containerConfig === undefined) {
|
||||||
throw new Error(`[AddContainer] Object type not found. Found: ${type}`);
|
throw new Error(`[AddContainer] Object type not found. Found: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,25 +201,24 @@ export function AddContainer(
|
||||||
throw new Error('[AddContainer] Container model was not found among children of the main container!');
|
throw new Error('[AddContainer] Container model was not found among children of the main container!');
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = 0;
|
let x = containerConfig.DefaultX ?? 0;
|
||||||
if (index > 0) {
|
const y = containerConfig.DefaultY ?? 0;
|
||||||
const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1);
|
const width = containerConfig.Width ?? parentClone.properties.width;
|
||||||
if (lastChild !== undefined) {
|
const height = containerConfig.Height ?? parentClone.properties.height;
|
||||||
x = lastChild.properties.x + Number(lastChild.properties.width);
|
|
||||||
}
|
x = ApplyAddMethod(index, containerConfig, parentClone, x);
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProperties: IProperties = {
|
const defaultProperties: IProperties = {
|
||||||
id: `${type}-${count}`,
|
id: `${type}-${count}`,
|
||||||
parentId: parentClone.properties.id,
|
parentId: parentClone.properties.id,
|
||||||
x,
|
x,
|
||||||
y: 0,
|
y,
|
||||||
width: properties.Width,
|
width,
|
||||||
height: parentClone.properties.height,
|
height,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
XPositionReference: properties.XPositionReference,
|
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
|
||||||
...properties.Style
|
style: containerConfig.Style
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the container
|
// Create the container
|
||||||
|
@ -247,3 +249,25 @@ export function AddContainer(
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)) {
|
||||||
|
const lastChild: IContainerModel | undefined = parent.children.at(index - 1);
|
||||||
|
|
||||||
|
if (lastChild !== undefined) {
|
||||||
|
x += (lastChild.properties.x + lastChild.properties.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
|
@ -91,16 +91,15 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
OnPropertyChange={(key, value) => OnPropertyChange(
|
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
|
||||||
key, value,
|
key, value, isStyle,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit(
|
OnPropertiesSubmit={(event) => OnPropertiesSubmit(
|
||||||
event,
|
event,
|
||||||
properties,
|
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
|
@ -133,8 +132,8 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
LoadState={(move) => setHistoryCurrentStep(move)}
|
LoadState={(move) => setHistoryCurrentStep(move)}
|
||||||
/>
|
/>
|
||||||
<SVG
|
<SVG
|
||||||
width={Number(current.MainContainer?.properties.width)}
|
width={current.MainContainer?.properties.width}
|
||||||
height={Number(current.MainContainer?.properties.height)}
|
height={current.MainContainer?.properties.height}
|
||||||
selected={current.SelectedContainer}
|
selected={current.SelectedContainer}
|
||||||
>
|
>
|
||||||
{ current.MainContainer }
|
{ current.MainContainer }
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
|
||||||
import { findContainerById } from '../../utils/itertools';
|
import { findContainerById } from '../../utils/itertools';
|
||||||
import { getCurrentHistory } from './Editor';
|
import { getCurrentHistory } from './Editor';
|
||||||
import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors';
|
import { constraintBodyInsideUnallocatedWidth, RecalculatePhysics } from './Behaviors/RigidBodyBehaviors';
|
||||||
import { INPUT_TYPES } from '../Properties/PropertiesInputTypes';
|
import { INPUT_TYPES } from '../Properties/PropertiesInputTypes';
|
||||||
import { ImposePosition } from './Behaviors/AnchorBehaviors';
|
import { ImposePosition } from './Behaviors/AnchorBehaviors';
|
||||||
|
import { restoreX } from '../SVG/Elements/Container';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handled the property change event in the properties form
|
* Handled the property change event in the properties form
|
||||||
|
@ -17,6 +17,7 @@ import { ImposePosition } from './Behaviors/AnchorBehaviors';
|
||||||
export function OnPropertyChange(
|
export function OnPropertyChange(
|
||||||
key: string,
|
key: string,
|
||||||
value: string | number | boolean,
|
value: string | number | boolean,
|
||||||
|
isStyle: boolean = false,
|
||||||
fullHistory: IHistoryState[],
|
fullHistory: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
@ -37,8 +38,8 @@ export function OnPropertyChange(
|
||||||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (INPUT_TYPES[key] === 'number') {
|
if (isStyle) {
|
||||||
(container.properties as any)[key] = Number(value);
|
(container.properties.style as any)[key] = value;
|
||||||
} else {
|
} else {
|
||||||
(container.properties as any)[key] = value;
|
(container.properties as any)[key] = value;
|
||||||
}
|
}
|
||||||
|
@ -70,7 +71,6 @@ export function OnPropertyChange(
|
||||||
*/
|
*/
|
||||||
export function OnPropertiesSubmit(
|
export function OnPropertiesSubmit(
|
||||||
event: React.SyntheticEvent<HTMLFormElement>,
|
event: React.SyntheticEvent<HTMLFormElement>,
|
||||||
properties: IProperties,
|
|
||||||
fullHistory: IHistoryState[],
|
fullHistory: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
@ -92,16 +92,29 @@ export function OnPropertiesSubmit(
|
||||||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const property in properties) {
|
// Assign container properties
|
||||||
const input = (event.target as HTMLFormElement).querySelector(`#${property}`);
|
const form: HTMLFormElement = event.target as HTMLFormElement;
|
||||||
if (input instanceof HTMLInputElement) {
|
for (const property in container.properties) {
|
||||||
(container.properties as any)[property] = input.value;
|
const input: HTMLInputElement | HTMLDivElement | null = form.querySelector(`#${property}`);
|
||||||
if (INPUT_TYPES[property] === 'number') {
|
|
||||||
(container.properties as any)[property] = Number(input.value);
|
if (input === null) {
|
||||||
} else {
|
continue;
|
||||||
(container.properties as any)[property] = input.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input instanceof HTMLInputElement) {
|
||||||
|
submitHTMLInput(input, container, property, form);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input instanceof HTMLDivElement) {
|
||||||
|
submitRadioButtons(input, container, property);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign cssproperties
|
||||||
|
for (const styleProperty in container.properties.style) {
|
||||||
|
submitCSSForm(form, styleProperty, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.properties.isRigidBody) {
|
if (container.properties.isRigidBody) {
|
||||||
|
@ -118,3 +131,67 @@ export function OnPropertiesSubmit(
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitHTMLInput = (
|
||||||
|
input: HTMLInputElement,
|
||||||
|
container: IContainerModel,
|
||||||
|
property: string,
|
||||||
|
form: HTMLFormElement
|
||||||
|
): void => {
|
||||||
|
if (INPUT_TYPES[property] !== 'number') {
|
||||||
|
(container.properties as any)[property] = input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property === 'x') {
|
||||||
|
// Hardcoded fix for XPositionReference
|
||||||
|
const x = RestoreX(form, input);
|
||||||
|
(container.properties as any)[property] = x;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(container.properties as any)[property] = Number(input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitCSSForm = (form: HTMLFormElement, styleProperty: string, container: ContainerModel): void => {
|
||||||
|
const input: HTMLInputElement | null = form.querySelector(`#${styleProperty}`);
|
||||||
|
if (input === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(container.properties.style as any)[styleProperty] = input.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RestoreX = (
|
||||||
|
form: HTMLFormElement,
|
||||||
|
input: HTMLInputElement
|
||||||
|
): number => {
|
||||||
|
const inputWidth: HTMLInputElement | null = form.querySelector('#width');
|
||||||
|
const inputRadio: HTMLDivElement | null = form.querySelector('#XPositionReference');
|
||||||
|
if (inputWidth === null || inputRadio === null) {
|
||||||
|
throw new Error('[OnPropertiesSubmit] Missing inputs for width or XPositionReference');
|
||||||
|
}
|
||||||
|
|
||||||
|
const radiobutton: HTMLInputElement | null = inputRadio.querySelector('input[name="XPositionReference"]:checked');
|
||||||
|
if (radiobutton === null) {
|
||||||
|
throw new Error('[OnPropertiesSubmit] Missing inputs for XPositionReference');
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoreX(Number(input.value), Number(inputWidth.value), Number(radiobutton.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitRadioButtons = (
|
||||||
|
div: HTMLDivElement,
|
||||||
|
container: IContainerModel,
|
||||||
|
property: string
|
||||||
|
): void => {
|
||||||
|
const radiobutton: HTMLInputElement | null = div.querySelector(`input[name="${property}"]:checked`);
|
||||||
|
if (radiobutton === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (INPUT_TYPES[property] === 'number') {
|
||||||
|
(container.properties as any)[property] = Number(radiobutton.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(container.properties as any)[property] = radiobutton.value;
|
||||||
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
||||||
import { fireEvent, render, screen } from '../../utils/test-utils';
|
import { fireEvent, render, screen } from '../../utils/test-utils';
|
||||||
import { ElementsSidebar } from './ElementsSidebar';
|
import { ElementsSidebar } from './ElementsSidebar';
|
||||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
|
||||||
describe.concurrent('Elements sidebar', () => {
|
describe.concurrent('Elements sidebar', () => {
|
||||||
it('With a MainContainer', () => {
|
it('With a MainContainer', () => {
|
||||||
|
@ -17,6 +18,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
XPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
|
@ -49,7 +51,8 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false,
|
||||||
|
XPositionReference: XPositionReference.Left
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
};
|
};
|
||||||
|
@ -105,6 +108,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
XPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
|
@ -123,7 +127,8 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false,
|
||||||
|
XPositionReference: XPositionReference.Left
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
}
|
}
|
||||||
|
@ -140,6 +145,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
XPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
|
@ -178,6 +184,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
XPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
|
@ -194,6 +201,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
XPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List } from 'react-window';
|
||||||
import { Properties } from '../Properties/Properties';
|
import { Properties } from '../Properties/Properties';
|
||||||
import ContainerProperties from '../../Interfaces/IProperties';
|
|
||||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { getDepth, MakeIterator } from '../../utils/itertools';
|
import { getDepth, MakeIterator } from '../../utils/itertools';
|
||||||
import { Menu } from '../Menu/Menu';
|
import { Menu } from '../Menu/Menu';
|
||||||
|
@ -14,8 +13,8 @@ interface IElementsSidebarProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
isHistoryOpen: boolean
|
isHistoryOpen: boolean
|
||||||
SelectedContainer: IContainerModel | null
|
SelectedContainer: IContainerModel | null
|
||||||
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void
|
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
SelectContainer: (container: IContainerModel) => void
|
SelectContainer: (container: IContainerModel) => void
|
||||||
DeleteContainer: (containerid: string) => void
|
DeleteContainer: (containerid: string) => void
|
||||||
AddContainer: (index: number, type: string, parent: string) => void
|
AddContainer: (index: number, type: string, parent: string) => void
|
||||||
|
@ -110,8 +109,6 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROW_HEIGHT = 35;
|
|
||||||
const NUMBERS_OF_ROWS = 10;
|
|
||||||
return (
|
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={`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'>
|
<div className='bg-slate-100 font-bold sidebar-title'>
|
||||||
|
|
|
@ -36,9 +36,8 @@ export function handleLeftClick(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeBorderClasses(target: HTMLButtonElement): void {
|
export function removeBorderClasses(target: HTMLButtonElement): void {
|
||||||
target.classList.remove('border-t-8');
|
const bordersClasses = ['border-t-8', 'border-8', 'border-b-8'];
|
||||||
target.classList.remove('border-8');
|
target.classList.remove(...bordersClasses);
|
||||||
target.classList.remove('border-b-8');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDragLeave(event: React.DragEvent): void {
|
export function handleDragLeave(event: React.DragEvent): void {
|
||||||
|
@ -54,26 +53,19 @@ export function handleDragOver(
|
||||||
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
const y = event.clientY - rect.top; // y position within the element.
|
const y = event.clientY - rect.top; // y position within the element.
|
||||||
|
removeBorderClasses(target);
|
||||||
|
|
||||||
if (target.id === mainContainer.properties.id) {
|
if (target.id === mainContainer.properties.id) {
|
||||||
target.classList.add('border-8');
|
target.classList.add('border-8');
|
||||||
target.classList.remove('border-t-8');
|
|
||||||
target.classList.remove('border-b-8');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (y < 12) {
|
if (y < 12) {
|
||||||
target.classList.add('border-t-8');
|
target.classList.add('border-t-8');
|
||||||
target.classList.remove('border-b-8');
|
|
||||||
target.classList.remove('border-8');
|
|
||||||
} else if (y < 24) {
|
} else if (y < 24) {
|
||||||
target.classList.add('border-8');
|
target.classList.add('border-8');
|
||||||
target.classList.remove('border-t-8');
|
|
||||||
target.classList.remove('border-b-8');
|
|
||||||
} else {
|
} else {
|
||||||
target.classList.add('border-b-8');
|
target.classList.add('border-b-8');
|
||||||
target.classList.remove('border-8');
|
|
||||||
target.classList.remove('border-t-8');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
48
src/Components/InputGroup/InputGroup.tsx
Normal file
48
src/Components/InputGroup/InputGroup.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface IInputGroupProps {
|
||||||
|
labelKey?: string
|
||||||
|
labelText: string
|
||||||
|
inputKey: string
|
||||||
|
labelClassName: string
|
||||||
|
inputClassName: string
|
||||||
|
type: string
|
||||||
|
value?: string
|
||||||
|
checked?: boolean
|
||||||
|
defaultValue?: string
|
||||||
|
defaultChecked?: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = `
|
||||||
|
w-full
|
||||||
|
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
|
||||||
|
bg-white border-2 border-white rounded-lg placeholder-gray-800
|
||||||
|
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
|
||||||
|
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
|
||||||
|
|
||||||
|
export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) => {
|
||||||
|
return <>
|
||||||
|
<label
|
||||||
|
key={props.labelKey}
|
||||||
|
className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`}
|
||||||
|
htmlFor={props.inputKey}
|
||||||
|
>
|
||||||
|
{ props.labelText }
|
||||||
|
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
key={props.inputKey}
|
||||||
|
id={props.inputKey}
|
||||||
|
className={`${className} ${props.inputClassName}`}
|
||||||
|
type={props.type}
|
||||||
|
value={props.value}
|
||||||
|
defaultValue={props.defaultValue}
|
||||||
|
checked={props.checked}
|
||||||
|
defaultChecked={props.defaultChecked}
|
||||||
|
onChange={props.onChange}
|
||||||
|
disabled={props.isDisabled}
|
||||||
|
/>
|
||||||
|
</>;
|
||||||
|
};
|
147
src/Components/Properties/DynamicForm.tsx
Normal file
147
src/Components/Properties/DynamicForm.tsx
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
import IProperties from '../../Interfaces/IProperties';
|
||||||
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
|
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||||
|
import { restoreX, transformX } from '../SVG/Elements/Container';
|
||||||
|
|
||||||
|
interface IDynamicFormProps {
|
||||||
|
properties: IProperties
|
||||||
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCSSInputs = (
|
||||||
|
properties: IProperties,
|
||||||
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
|
): JSX.Element[] => {
|
||||||
|
const groupInput: JSX.Element[] = [];
|
||||||
|
for (const key in properties.style) {
|
||||||
|
groupInput.push(<InputGroup
|
||||||
|
key={key}
|
||||||
|
labelText={key}
|
||||||
|
inputKey={key}
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={(properties.style as any)[key]}
|
||||||
|
onChange={(event) => onChange(key, event.target.value, true)}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
return groupInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-2 gap-y-4'>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Name'
|
||||||
|
inputKey='id'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={props.properties.id.toString()}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Parent name'
|
||||||
|
inputKey='parentId'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={props.properties.parentId?.toString()}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='x'
|
||||||
|
inputKey='x'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
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))}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='y'
|
||||||
|
inputKey='y'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
value={props.properties.y.toString()}
|
||||||
|
onChange={(event) => props.onChange('y', Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Width'
|
||||||
|
inputKey='width'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
value={props.properties.width.toString()}
|
||||||
|
onChange={(event) => props.onChange('width', Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Height'
|
||||||
|
inputKey='height'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
value={props.properties.height.toString()}
|
||||||
|
onChange={(event) => props.onChange('height', Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Rigid'
|
||||||
|
inputKey='isRigidBody'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
checked={props.properties.isRigidBody}
|
||||||
|
onChange={(event) => props.onChange('isRigidBody', event.target.checked)}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Anchor'
|
||||||
|
inputKey='isAnchor'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
checked={props.properties.isAnchor}
|
||||||
|
onChange={(event) => props.onChange('isAnchor', event.target.checked)}
|
||||||
|
/>
|
||||||
|
<RadioGroupButtons
|
||||||
|
name='XPositionReference'
|
||||||
|
value={props.properties.XPositionReference.toString()}
|
||||||
|
inputClassName='hidden'
|
||||||
|
labelText='Horizontal alignment'
|
||||||
|
inputGroups={[
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div title='Left' aria-label='left' className='radio-button-icon'>
|
||||||
|
<MenuAlt2Icon className='heroicon' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: XPositionReference.Left.toString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div title='Center' aria-label='center' className='radio-button-icon'>
|
||||||
|
<MenuIcon className='heroicon' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: XPositionReference.Center.toString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div title='Right' aria-label='right' className='radio-button-icon'>
|
||||||
|
<MenuAlt3Icon className='heroicon' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: XPositionReference.Right.toString()
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
onChange={(event) => props.onChange('XPositionReference', Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
{ getCSSInputs(props.properties, props.onChange) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicForm;
|
24
src/Components/Properties/Form.tsx
Normal file
24
src/Components/Properties/Form.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import IProperties from '../../Interfaces/IProperties';
|
||||||
|
import DynamicForm from './DynamicForm';
|
||||||
|
import StaticForm from './StaticForm';
|
||||||
|
|
||||||
|
interface IFormProps {
|
||||||
|
properties: IProperties
|
||||||
|
isDynamicInput: boolean
|
||||||
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Form: React.FunctionComponent<IFormProps> = (props) => {
|
||||||
|
if (props.isDynamicInput) {
|
||||||
|
return <DynamicForm
|
||||||
|
properties={props.properties}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
return <StaticForm
|
||||||
|
properties={props.properties}
|
||||||
|
onSubmit={props.onSubmit}
|
||||||
|
/>;
|
||||||
|
};
|
|
@ -1,6 +1,8 @@
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { expect, describe, it, vi } from 'vitest';
|
import { expect, describe, it, vi } from 'vitest';
|
||||||
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
import IProperties from '../../Interfaces/IProperties';
|
||||||
import { Properties } from './Properties';
|
import { Properties } from './Properties';
|
||||||
|
|
||||||
describe.concurrent('Properties', () => {
|
describe.concurrent('Properties', () => {
|
||||||
|
@ -18,11 +20,14 @@ describe.concurrent('Properties', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Some properties, change values with dynamic input', () => {
|
it('Some properties, change values with dynamic input', () => {
|
||||||
const prop = {
|
const prop: IProperties = {
|
||||||
id: 'stuff',
|
id: 'stuff',
|
||||||
parentId: 'parentId',
|
parentId: 'parentId',
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 1,
|
y: 1,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
XPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
};
|
};
|
||||||
|
@ -59,12 +64,12 @@ describe.concurrent('Properties', () => {
|
||||||
fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } });
|
fireEvent.change(propertyParentId as Element, { target: { value: 'parentedId' } });
|
||||||
fireEvent.change(propertyX as Element, { target: { value: '2' } });
|
fireEvent.change(propertyX as Element, { target: { value: '2' } });
|
||||||
fireEvent.change(propertyY as Element, { target: { value: '2' } });
|
fireEvent.change(propertyY as Element, { target: { value: '2' } });
|
||||||
expect(handleChange).toBeCalledTimes(4);
|
expect(handleChange).toBeCalledTimes(2);
|
||||||
|
|
||||||
expect(prop.id).toBe('stuffed');
|
expect(prop.id).toBe('stuff');
|
||||||
expect(prop.parentId).toBe('parentedId');
|
expect(prop.parentId).toBe('parentId');
|
||||||
expect(prop.x).toBe('2');
|
expect(prop.x).toBe(2);
|
||||||
expect(prop.y).toBe('2');
|
expect(prop.y).toBe(2);
|
||||||
rerender(<Properties
|
rerender(<Properties
|
||||||
properties={Object.assign({}, prop)}
|
properties={Object.assign({}, prop)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
@ -76,9 +81,9 @@ describe.concurrent('Properties', () => {
|
||||||
propertyX = container.querySelector('#x');
|
propertyX = container.querySelector('#x');
|
||||||
propertyY = container.querySelector('#y');
|
propertyY = container.querySelector('#y');
|
||||||
expect(propertyId).toBeDefined();
|
expect(propertyId).toBeDefined();
|
||||||
expect((propertyId as HTMLInputElement).value).toBe('stuffed');
|
expect((propertyId as HTMLInputElement).value).toBe('stuff');
|
||||||
expect(propertyParentId).toBeDefined();
|
expect(propertyParentId).toBeDefined();
|
||||||
expect((propertyParentId as HTMLInputElement).value).toBe('parentedId');
|
expect((propertyParentId as HTMLInputElement).value).toBe('parentId');
|
||||||
expect(propertyX).toBeDefined();
|
expect(propertyX).toBeDefined();
|
||||||
expect((propertyX as HTMLInputElement).value).toBe('2');
|
expect((propertyX as HTMLInputElement).value).toBe('2');
|
||||||
expect(propertyY).toBeDefined();
|
expect(propertyY).toBeDefined();
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import ContainerProperties from '../../Interfaces/IProperties';
|
import IProperties from '../../Interfaces/IProperties';
|
||||||
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
||||||
import { INPUT_TYPES } from './PropertiesInputTypes';
|
import { Form } from './Form';
|
||||||
|
|
||||||
interface IPropertiesProps {
|
interface IPropertiesProps {
|
||||||
properties?: ContainerProperties
|
properties?: IProperties
|
||||||
onChange: (key: string, value: string | number | boolean) => void
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>, properties: ContainerProperties) => void
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
|
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
|
||||||
|
@ -16,26 +16,6 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupInput: React.ReactNode[] = [];
|
|
||||||
Object
|
|
||||||
.entries(props.properties)
|
|
||||||
.forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange));
|
|
||||||
|
|
||||||
const form = isDynamicInput
|
|
||||||
? <div className='grid grid-cols-2 gap-4'>
|
|
||||||
{ groupInput }
|
|
||||||
</div>
|
|
||||||
: <form
|
|
||||||
key={props.properties.id}
|
|
||||||
onSubmit={(event) => props.onSubmit(event, props.properties as ContainerProperties)}
|
|
||||||
>
|
|
||||||
<input type='submit' className='normal-btn block mx-auto mb-4 border-2 border-blue-400 cursor-pointer' value='Submit'/>
|
|
||||||
<div className='grid grid-cols-2 gap-y-4'>
|
|
||||||
{ groupInput }
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
|
@ -45,72 +25,12 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
||||||
checked={isDynamicInput}
|
checked={isDynamicInput}
|
||||||
onChange={() => setIsDynamicInput(!isDynamicInput)}
|
onChange={() => setIsDynamicInput(!isDynamicInput)}
|
||||||
/>
|
/>
|
||||||
{ form }
|
<Form
|
||||||
|
properties={props.properties}
|
||||||
|
isDynamicInput={isDynamicInput}
|
||||||
|
onChange={props.onChange}
|
||||||
|
onSubmit={props.onSubmit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProperties = (
|
|
||||||
[key, value]: [string, string | number],
|
|
||||||
groupInput: React.ReactNode[],
|
|
||||||
isDynamicInput: boolean,
|
|
||||||
onChange: (key: string, value: string | number | boolean) => void
|
|
||||||
): void => {
|
|
||||||
const id = `property-${key}`;
|
|
||||||
let type = 'text';
|
|
||||||
let checked;
|
|
||||||
|
|
||||||
/// hardcoded stuff for ergonomy ///
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
checked = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key in INPUT_TYPES) {
|
|
||||||
type = INPUT_TYPES[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = `
|
|
||||||
w-full
|
|
||||||
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
|
|
||||||
bg-white border-2 border-white rounded-lg placeholder-gray-800
|
|
||||||
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
|
|
||||||
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
|
|
||||||
const isDisabled = ['id', 'parentId'].includes(key);
|
|
||||||
const input = isDynamicInput
|
|
||||||
? <input
|
|
||||||
key={key}
|
|
||||||
id={key}
|
|
||||||
className={className}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
checked={checked}
|
|
||||||
onChange={(event) => {
|
|
||||||
if (type === 'checkbox') {
|
|
||||||
onChange(key, event.target.checked);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onChange(key, event.target.value);
|
|
||||||
}}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
: <input
|
|
||||||
key={key}
|
|
||||||
id={key}
|
|
||||||
className={className}
|
|
||||||
type={type}
|
|
||||||
defaultValue={value}
|
|
||||||
defaultChecked={checked}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
groupInput.push(
|
|
||||||
<label
|
|
||||||
key={id}
|
|
||||||
className='mt-4 text-xs font-medium text-gray-800'
|
|
||||||
htmlFor={key}
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
groupInput.push(input);
|
|
||||||
};
|
|
||||||
|
|
|
@ -4,5 +4,6 @@ export const INPUT_TYPES: Record<string, string> = {
|
||||||
width: 'number',
|
width: 'number',
|
||||||
height: 'number',
|
height: 'number',
|
||||||
isRigidBody: 'checkbox',
|
isRigidBody: 'checkbox',
|
||||||
isAnchor: 'checkbox'
|
isAnchor: 'checkbox',
|
||||||
|
XPositionReference: 'number'
|
||||||
};
|
};
|
||||||
|
|
140
src/Components/Properties/StaticForm.tsx
Normal file
140
src/Components/Properties/StaticForm.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { MenuAlt2Icon, MenuIcon, MenuAlt3Icon } from '@heroicons/react/outline';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
import IProperties from '../../Interfaces/IProperties';
|
||||||
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
|
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||||
|
import { transformX } from '../SVG/Elements/Container';
|
||||||
|
|
||||||
|
interface IStaticFormProps {
|
||||||
|
properties: IProperties
|
||||||
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCSSInputs = (properties: IProperties): JSX.Element[] => {
|
||||||
|
const groupInput: JSX.Element[] = [];
|
||||||
|
for (const key in properties.style) {
|
||||||
|
groupInput.push(<InputGroup
|
||||||
|
key={key}
|
||||||
|
labelText={key}
|
||||||
|
inputKey={key}
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
defaultValue={(properties.style as any)[key]}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
return groupInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
|
||||||
|
return (<form
|
||||||
|
key={props.properties.id}
|
||||||
|
onSubmit={(event) => props.onSubmit(event)}
|
||||||
|
>
|
||||||
|
<input type='submit' className='normal-btn block mx-auto mb-4 border-2 border-blue-400 cursor-pointer' value='Submit'/>
|
||||||
|
<div className='grid grid-cols-2 gap-y-4'>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Name'
|
||||||
|
inputKey='id'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
defaultValue={props.properties.id.toString()}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Parent name'
|
||||||
|
inputKey='parentId'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
defaultValue={props.properties.parentId?.toString()}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='x'
|
||||||
|
inputKey='x'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
defaultValue={transformX(props.properties.x, props.properties.width, props.properties.XPositionReference).toString()}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='y'
|
||||||
|
inputKey='y'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
defaultValue={props.properties.y.toString()}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Width'
|
||||||
|
inputKey='width'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
defaultValue={props.properties.width.toString()}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Height'
|
||||||
|
inputKey='height'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
defaultValue={props.properties.height.toString()}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Rigid'
|
||||||
|
inputKey='isRigidBody'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
defaultChecked={props.properties.isRigidBody}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Anchor'
|
||||||
|
inputKey='isAnchor'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
defaultChecked={props.properties.isAnchor}
|
||||||
|
/>
|
||||||
|
<RadioGroupButtons
|
||||||
|
name='XPositionReference'
|
||||||
|
defaultValue={props.properties.XPositionReference.toString()}
|
||||||
|
inputClassName='hidden'
|
||||||
|
labelText='Horizontal alignment'
|
||||||
|
inputGroups={[
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div title='Left' aria-label='left' className='radio-button-icon'>
|
||||||
|
<MenuAlt2Icon className='heroicon' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: XPositionReference.Left.toString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div title='Center' aria-label='center' className='radio-button-icon'>
|
||||||
|
<MenuIcon className='heroicon' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: XPositionReference.Center.toString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: (
|
||||||
|
<div title='Right' aria-label='right' className='radio-button-icon'>
|
||||||
|
<MenuAlt3Icon className='heroicon' />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: XPositionReference.Right.toString()
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{ getCSSInputs(props.properties) }
|
||||||
|
</div>
|
||||||
|
</form>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaticForm;
|
66
src/Components/RadioGroupButtons/RadioGroupButtons.tsx
Normal file
66
src/Components/RadioGroupButtons/RadioGroupButtons.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IInputGroup } from '../../Interfaces/IInputGroup';
|
||||||
|
|
||||||
|
interface IRadioGroupButtonsProps {
|
||||||
|
name: string
|
||||||
|
value?: string
|
||||||
|
defaultValue?: string
|
||||||
|
inputClassName: string
|
||||||
|
labelText: string
|
||||||
|
inputGroups: IInputGroup[]
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps> = (props) => {
|
||||||
|
let inputGroups;
|
||||||
|
if (props.value !== undefined) {
|
||||||
|
// dynamic
|
||||||
|
inputGroups = props.inputGroups.map((inputGroup) => (
|
||||||
|
<div key={inputGroup.value}>
|
||||||
|
<input
|
||||||
|
id={inputGroup.value}
|
||||||
|
type='radio'
|
||||||
|
name={props.name}
|
||||||
|
className={`peer m-2 ${props.inputClassName}`}
|
||||||
|
value={inputGroup.value}
|
||||||
|
checked={props.value === inputGroup.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
|
||||||
|
{inputGroup.text}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// static
|
||||||
|
inputGroups = props.inputGroups.map((inputGroup) => (
|
||||||
|
<div key={inputGroup.value}>
|
||||||
|
<input
|
||||||
|
id={inputGroup.value}
|
||||||
|
type='radio'
|
||||||
|
name={props.name}
|
||||||
|
className={`peer m-2 ${props.inputClassName}`}
|
||||||
|
value={inputGroup.value}
|
||||||
|
defaultChecked={props.defaultValue === inputGroup.value}
|
||||||
|
/>
|
||||||
|
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
|
||||||
|
{inputGroup.text}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className='mt-4 text-xs font-medium text-gray-800'>
|
||||||
|
{props.labelText}
|
||||||
|
</label>
|
||||||
|
<div id='XPositionReference'
|
||||||
|
className='flex flex-col'
|
||||||
|
>
|
||||||
|
{ inputGroups }
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { XPositionReference } from '../../../Enums/XPositionReference';
|
import { XPositionReference } from '../../../Enums/XPositionReference';
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||||
import { getDepth } from '../../../utils/itertools';
|
import { getDepth } from '../../../utils/itertools';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
|
|
||||||
|
@ -8,24 +9,16 @@ interface IContainerProps {
|
||||||
model: IContainerModel
|
model: IContainerModel
|
||||||
}
|
}
|
||||||
|
|
||||||
const GAP = 50;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the container
|
* Render the container
|
||||||
* @returns Render the container
|
* @returns Render the container
|
||||||
*/
|
*/
|
||||||
export const Container: React.FC<IContainerProps> = (props: 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 containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
|
||||||
const xText = Number(props.model.properties.width) / 2;
|
const xText = props.model.properties.width / 2;
|
||||||
const yText = Number(props.model.properties.height) / 2;
|
const yText = props.model.properties.height / 2;
|
||||||
|
|
||||||
const [transformedX, transformedY] = transformPosition(
|
const transform = `translate(${props.model.properties.x}, ${props.model.properties.y})`;
|
||||||
Number(props.model.properties.x),
|
|
||||||
Number(props.model.properties.y),
|
|
||||||
Number(props.model.properties.width),
|
|
||||||
props.model.properties.XPositionReference
|
|
||||||
);
|
|
||||||
const transform = `translate(${transformedX}, ${transformedY})`;
|
|
||||||
|
|
||||||
// g style
|
// g style
|
||||||
const defaultStyle: React.CSSProperties = {
|
const defaultStyle: React.CSSProperties = {
|
||||||
|
@ -37,21 +30,39 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
// Rect style
|
// Rect style
|
||||||
const style = Object.assign(
|
const style = Object.assign(
|
||||||
JSON.parse(JSON.stringify(defaultStyle)),
|
JSON.parse(JSON.stringify(defaultStyle)),
|
||||||
props.model.properties
|
props.model.properties.style
|
||||||
);
|
);
|
||||||
style.x = 0;
|
|
||||||
style.y = 0;
|
|
||||||
delete style.height;
|
|
||||||
delete style.width;
|
|
||||||
|
|
||||||
// Dimension props
|
// Dimension props
|
||||||
|
const depth = getDepth(props.model);
|
||||||
|
const dimensionMargin = DIMENSION_MARGIN * (depth + 1);
|
||||||
const id = `dim-${props.model.properties.id}`;
|
const id = `dim-${props.model.properties.id}`;
|
||||||
const xStart: number = 0;
|
const xStart: number = 0;
|
||||||
const xEnd = Number(props.model.properties.width);
|
const xEnd = props.model.properties.width;
|
||||||
const y = -(GAP * (getDepth(props.model) + 1));
|
const y = -dimensionMargin;
|
||||||
const strokeWidth = 1;
|
const strokeWidth = 1;
|
||||||
const text = (props.model.properties.width ?? 0).toString();
|
const text = (props.model.properties.width ?? 0).toString();
|
||||||
|
|
||||||
|
let dimensionChildren: JSX.Element | null = null;
|
||||||
|
if (props.model.children.length > 1) {
|
||||||
|
const {
|
||||||
|
childrenId,
|
||||||
|
xChildrenStart,
|
||||||
|
xChildrenEnd,
|
||||||
|
yChildren,
|
||||||
|
textChildren
|
||||||
|
} = GetChildrenDimensionProps(props, dimensionMargin);
|
||||||
|
dimensionChildren = <Dimension
|
||||||
|
id={childrenId}
|
||||||
|
xStart={xChildrenStart}
|
||||||
|
xEnd={xChildrenEnd}
|
||||||
|
yStart={yChildren}
|
||||||
|
yEnd={yChildren}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
text={textChildren}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
style={defaultStyle}
|
style={defaultStyle}
|
||||||
|
@ -67,6 +78,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
text={text}
|
text={text}
|
||||||
/>
|
/>
|
||||||
|
{ dimensionChildren }
|
||||||
<rect
|
<rect
|
||||||
width={props.model.properties.width}
|
width={props.model.properties.width}
|
||||||
height={props.model.properties.height}
|
height={props.model.properties.height}
|
||||||
|
@ -84,12 +96,48 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function transformPosition(x: number, y: number, width: number, xPositionReference = XPositionReference.Left): [number, number] {
|
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];
|
||||||
|
let xChildrenStart = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference);
|
||||||
|
let xChildrenEnd = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference);
|
||||||
|
|
||||||
|
// Find the min and max
|
||||||
|
for (let i = props.model.children.length - 2; i >= 0; i--) {
|
||||||
|
const child = props.model.children[i];
|
||||||
|
const left = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference);
|
||||||
|
if (left < xChildrenStart) {
|
||||||
|
xChildrenStart = left;
|
||||||
|
}
|
||||||
|
const right = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference);
|
||||||
|
if (right > xChildrenEnd) {
|
||||||
|
xChildrenEnd = right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const yChildren = props.model.properties.height + dimensionMargin;
|
||||||
|
const textChildren = (xChildrenEnd - xChildrenStart).toString();
|
||||||
|
return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||||
|
let transformedX = x;
|
||||||
|
if (xPositionReference === XPositionReference.Center) {
|
||||||
|
transformedX += width / 2;
|
||||||
|
} else if (xPositionReference === XPositionReference.Right) {
|
||||||
|
transformedX += width;
|
||||||
|
}
|
||||||
|
return transformedX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||||
let transformedX = x;
|
let transformedX = x;
|
||||||
if (xPositionReference === XPositionReference.Center) {
|
if (xPositionReference === XPositionReference.Center) {
|
||||||
transformedX -= width / 2;
|
transformedX -= width / 2;
|
||||||
} else if (xPositionReference === XPositionReference.Right) {
|
} else if (xPositionReference === XPositionReference.Right) {
|
||||||
transformedX -= width;
|
transformedX -= width;
|
||||||
}
|
}
|
||||||
return [transformedX, y];
|
return transformedX;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||||
import { Bar } from '../Bar/Bar';
|
import { Bar } from '../Bar/Bar';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
|
||||||
|
|
||||||
interface IUIProps {
|
interface IUIProps {
|
||||||
current: IHistoryState
|
current: IHistoryState
|
||||||
|
@ -17,8 +16,8 @@ interface IUIProps {
|
||||||
AvailableContainers: IAvailableContainer[]
|
AvailableContainers: IAvailableContainer[]
|
||||||
SelectContainer: (container: ContainerModel) => void
|
SelectContainer: (container: ContainerModel) => void
|
||||||
DeleteContainer: (containerId: string) => void
|
DeleteContainer: (containerId: string) => void
|
||||||
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>, properties: IProperties) => void
|
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
AddContainerToSelectedContainer: (type: string) => void
|
AddContainerToSelectedContainer: (type: string) => void
|
||||||
AddContainer: (index: number, type: string, parentId: string) => void
|
AddContainer: (index: number, type: string, parentId: string) => void
|
||||||
SaveEditorAsJSON: () => void
|
SaveEditorAsJSON: () => void
|
||||||
|
|
10
src/Enums/AddMethod.ts
Normal file
10
src/Enums/AddMethod.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Add method when creating a container
|
||||||
|
* - Append will append to the last children in list
|
||||||
|
* - Insert will always place it at the begining
|
||||||
|
* (default: Append)
|
||||||
|
*/
|
||||||
|
export enum AddMethod {
|
||||||
|
Append,
|
||||||
|
Insert
|
||||||
|
}
|
|
@ -1,11 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { AddMethod } from '../Enums/AddMethod';
|
||||||
import { XPositionReference } from '../Enums/XPositionReference';
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
|
|
||||||
/** Model of available container used in application configuration */
|
/** Model of available container used in application configuration */
|
||||||
export interface IAvailableContainer {
|
export interface IAvailableContainer {
|
||||||
Type: string
|
Type: string
|
||||||
Width: number
|
Width?: number
|
||||||
Height: number
|
Height?: number
|
||||||
|
DefaultX?: number
|
||||||
|
DefaultY?: number
|
||||||
|
AddMethod?: AddMethod
|
||||||
XPositionReference?: XPositionReference
|
XPositionReference?: XPositionReference
|
||||||
Style: React.CSSProperties
|
Style: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
6
src/Interfaces/IInputGroup.ts
Normal file
6
src/Interfaces/IInputGroup.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface IInputGroup {
|
||||||
|
text: React.ReactNode
|
||||||
|
value: string
|
||||||
|
}
|
|
@ -10,12 +10,15 @@ import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
* @property isRigidBody if true apply rigid body behaviors
|
* @property isRigidBody if true apply rigid body behaviors
|
||||||
* @property isAnchor if true apply anchor behaviors
|
* @property isAnchor if true apply anchor behaviors
|
||||||
*/
|
*/
|
||||||
export default interface IProperties extends React.CSSProperties {
|
export default interface IProperties {
|
||||||
id: string
|
id: string
|
||||||
parentId: string | null
|
parentId: string | null
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
isRigidBody: boolean
|
isRigidBody: boolean
|
||||||
isAnchor: boolean
|
isAnchor: boolean
|
||||||
XPositionReference?: XPositionReference
|
XPositionReference: XPositionReference
|
||||||
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,10 @@
|
||||||
@apply h-full w-full align-middle items-center justify-center
|
@apply h-full w-full align-middle items-center justify-center
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radio-button-icon {
|
||||||
|
@apply rounded-md shadow-sm bg-white w-8 cursor-pointer inline-block
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-tooltip {
|
.sidebar-tooltip {
|
||||||
@apply absolute w-auto p-2 m-2 min-w-max left-14
|
@apply absolute w-auto p-2 m-2 min-w-max left-14
|
||||||
rounded-md shadow-md
|
rounded-md shadow-md
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
import { IConfiguration } from '../Interfaces/IConfiguration';
|
import { IConfiguration } from '../Interfaces/IConfiguration';
|
||||||
import IProperties from '../Interfaces/IProperties';
|
import IProperties from '../Interfaces/IProperties';
|
||||||
|
|
||||||
|
@ -30,14 +31,18 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
|
||||||
parentId: 'null',
|
parentId: 'null',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: DEFAULT_CONFIG.MainContainer.Width,
|
width: Number(DEFAULT_CONFIG.MainContainer.Width),
|
||||||
height: DEFAULT_CONFIG.MainContainer.Height,
|
height: Number(DEFAULT_CONFIG.MainContainer.Height),
|
||||||
isRigidBody: false,
|
isRigidBody: false,
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
fillOpacity: 0,
|
XPositionReference: XPositionReference.Left,
|
||||||
stroke: 'black'
|
style: {
|
||||||
|
stroke: 'black',
|
||||||
|
fillOpacity: 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DIMENSION_MARGIN = 50;
|
||||||
export const NOTCHES_LENGTH = 4;
|
export const NOTCHES_LENGTH = 4;
|
||||||
|
|
||||||
export const MAX_HISTORY = 200;
|
export const MAX_HISTORY = 200;
|
||||||
|
|
|
@ -43,12 +43,12 @@ export function getDepth(parent: IContainerModel): number {
|
||||||
* @returns The absolute position of the container
|
* @returns The absolute position of the container
|
||||||
*/
|
*/
|
||||||
export function getAbsolutePosition(container: IContainerModel): [number, number] {
|
export function getAbsolutePosition(container: IContainerModel): [number, number] {
|
||||||
let x = Number(container.properties.x);
|
let x = container.properties.x;
|
||||||
let y = Number(container.properties.y);
|
let y = container.properties.y;
|
||||||
let current = container.parent;
|
let current = container.parent;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
x += Number(current.properties.x);
|
x += current.properties.x;
|
||||||
y += Number(current.properties.y);
|
y += current.properties.y;
|
||||||
current = current.parent;
|
current = current.parent;
|
||||||
}
|
}
|
||||||
return [x, y];
|
return [x, y];
|
||||||
|
|
|
@ -55,27 +55,44 @@ const GetSVGLayoutConfiguration = () => {
|
||||||
Type: 'Chassis',
|
Type: 'Chassis',
|
||||||
Width: 500,
|
Width: 500,
|
||||||
Style: {
|
Style: {
|
||||||
fillOpacity: 0,
|
fillOpacity: 1,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
stroke: 'red',
|
||||||
|
fill: '#78350F',
|
||||||
stroke: 'red'
|
stroke: 'red'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: 'Trou',
|
Type: 'Trou',
|
||||||
Width: 300,
|
DefaultX: 10,
|
||||||
|
DefaultY: 10,
|
||||||
|
Width: 480,
|
||||||
|
Height: 180,
|
||||||
Style: {
|
Style: {
|
||||||
fillOpacity: 0,
|
fillOpacity: 1,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
stroke: 'green'
|
stroke: 'green',
|
||||||
|
fill: 'white'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: 'Remplissage',
|
||||||
|
Style: {
|
||||||
|
fillOpacity: 1,
|
||||||
|
borderWidth: 2,
|
||||||
|
stroke: '#bfdbfe',
|
||||||
|
fill: '#bfdbfe'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: 'Montant',
|
Type: 'Montant',
|
||||||
Width: 100,
|
Width: 10,
|
||||||
|
XPositionReference: 1,
|
||||||
Style: {
|
Style: {
|
||||||
fillOpacity: 0,
|
fillOpacity: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
stroke: 'blue',
|
stroke: '#713f12',
|
||||||
|
fill: '#713f12',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue