Merge branch 'dev'
This commit is contained in:
commit
4d20745e03
106 changed files with 3894 additions and 2042 deletions
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
VITE_API_URL=https://localhost/SmartMenuiserieTemplate
|
VITE_API_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
|
|
@ -4,6 +4,7 @@ module.exports = {
|
||||||
es2021: true
|
es2021: true
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
|
'only-warn',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'standard-with-typescript'
|
'standard-with-typescript'
|
||||||
],
|
],
|
||||||
|
@ -17,22 +18,67 @@ module.exports = {
|
||||||
project: './tsconfig.json'
|
project: './tsconfig.json'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
'only-warn',
|
|
||||||
'react',
|
'react',
|
||||||
'react-hooks',
|
'react-hooks',
|
||||||
'@typescript-eslint'
|
'@typescript-eslint'
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
'prefer-arrow-callback': 'error',
|
||||||
|
'func-style': ['error', 'declaration'],
|
||||||
'space-before-function-paren': ['error', 'never'],
|
'space-before-function-paren': ['error', 'never'],
|
||||||
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
|
|
||||||
|
// Import/export
|
||||||
|
'import/no-default-export': 'error',
|
||||||
|
|
||||||
|
// Typescript overload
|
||||||
indent: 'off',
|
indent: 'off',
|
||||||
'@typescript-eslint/indent': ['warn', 2, {SwitchCase: 1}],
|
|
||||||
semi: 'off',
|
semi: 'off',
|
||||||
'@typescript-eslint/semi': ['warn', 'always'],
|
"camelcase": "off",
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
|
||||||
|
// Typescript
|
||||||
|
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
|
||||||
|
'@typescript-eslint/indent': ['warn', 2, {SwitchCase: 1}],
|
||||||
|
'@typescript-eslint/semi': ['warn', 'always'],
|
||||||
'@typescript-eslint/no-unused-vars': 'error',
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
'@typescript-eslint/ban-types': ['error'],
|
'@typescript-eslint/ban-types': ['error'],
|
||||||
'@typescript-eslint/no-floating-promises': 'off', // disabled cuz troublesome for SweetAlert since they never reject
|
'@typescript-eslint/no-floating-promises': 'off', // disabled cuz troublesome for SweetAlert since they never reject
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "default",
|
||||||
|
"format": ["camelCase"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'selector': 'function',
|
||||||
|
'format': ['PascalCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "variable",
|
||||||
|
"format": ["camelCase", "UPPER_CASE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "parameter",
|
||||||
|
"format": ["camelCase"],
|
||||||
|
"leadingUnderscore": "allow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'selector': ['enumMember', 'enum'],
|
||||||
|
'format': ['PascalCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "memberLike",
|
||||||
|
"modifiers": ["private"],
|
||||||
|
"format": ["camelCase"],
|
||||||
|
"leadingUnderscore": "require"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": ['typeLike'],
|
||||||
|
"format": ["PascalCase"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// React
|
||||||
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
|
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
|
||||||
'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies
|
'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies
|
||||||
}
|
}
|
||||||
|
|
3
.gitattributes
vendored
3
.gitattributes
vendored
|
@ -1 +1,4 @@
|
||||||
*.drawio filter=lfs diff=lfs merge=lfs -text
|
*.drawio filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.dwg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
|
@ -14,6 +14,7 @@ Requierements :
|
||||||
- NodeJS
|
- NodeJS
|
||||||
- npm
|
- npm
|
||||||
- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
|
- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
|
||||||
|
- Chrome > 98
|
||||||
|
|
||||||
# Developping
|
# Developping
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ steps:
|
||||||
path: $(pnpm_config_cache)
|
path: $(pnpm_config_cache)
|
||||||
displayName: Cache pnpm
|
displayName: Cache pnpm
|
||||||
|
|
||||||
- script: |
|
- bash: |
|
||||||
curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
||||||
pnpm config set store-dir $(pnpm_config_cache)
|
pnpm config set store-dir $(pnpm_config_cache)
|
||||||
displayName: "Setup pnpm"
|
displayName: "Setup pnpm"
|
||||||
|
@ -31,7 +31,8 @@ steps:
|
||||||
versionSpec: '16.x'
|
versionSpec: '16.x'
|
||||||
displayName: 'Install Node.js 16.x LTS'
|
displayName: 'Install Node.js 16.x LTS'
|
||||||
|
|
||||||
- script: |
|
- bash: |
|
||||||
|
set -euo pipefail
|
||||||
node --version
|
node --version
|
||||||
node ./test-server/node-http.js &
|
node ./test-server/node-http.js &
|
||||||
jobs
|
jobs
|
||||||
|
@ -46,7 +47,8 @@ steps:
|
||||||
versionSpec: '>=18.7.0'
|
versionSpec: '>=18.7.0'
|
||||||
displayName: 'Install Node.js Latest'
|
displayName: 'Install Node.js Latest'
|
||||||
|
|
||||||
- script: |
|
- bash: |
|
||||||
|
set -euo pipefail
|
||||||
node --version
|
node --version
|
||||||
node ./test-server/node-http.js &
|
node ./test-server/node-http.js &
|
||||||
jobs
|
jobs
|
||||||
|
|
368
docs/Behaviors.md
Normal file
368
docs/Behaviors.md
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
# Container behaviors
|
||||||
|
|
||||||
|
This document is about the special and unique behaviors that a container can have.
|
||||||
|
|
||||||
|
Each behavior is documented into a section with 3 subsections :
|
||||||
|
- Rules
|
||||||
|
- Applications
|
||||||
|
- Code reference and algorithms
|
||||||
|
|
||||||
|
|
||||||
|
# Default behavior
|
||||||
|
|
||||||
|
The default behavior is the floating panel. It can move and resize itself but not its siblings.
|
||||||
|
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
The default behavior does not have any particular rules that applies to itself.
|
||||||
|
|
||||||
|
However it does have a common rule for any behavior that applies to its children.
|
||||||
|
|
||||||
|
Which is to apply the specials behaviors of its children (rigid or anchor).
|
||||||
|
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
The default behavior is important to have a good user experience when adding object.
|
||||||
|
|
||||||
|
The golden rule is to never oppose the user which is why we don't want to applies rigid body by default
|
||||||
|
as it can block the addition of container.
|
||||||
|
Allowing freedom of movement can help for better precision if not the same as the rigid property.
|
||||||
|
|
||||||
|
An example would be trying to overlap an element in order to use it as a layer.
|
||||||
|
|
||||||
|
## Code references and algorithms
|
||||||
|
|
||||||
|
In the module `PropertiesOperations.ts` in the following functions:
|
||||||
|
- `OnPropertyChange()`
|
||||||
|
- `OnPropertiesSubmit()`
|
||||||
|
|
||||||
|
and in `ContainerOperation.ts` in `AddContainer()`,
|
||||||
|
|
||||||
|
it uses the `ApplyBehaviors` function of the `Behaviors.ts` module to apply the specials behaviors of its children.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Rigid body behavior
|
||||||
|
|
||||||
|
The rigid body behavior is a special behavior
|
||||||
|
that allows a container to be restricted into a space.
|
||||||
|
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
The main rules are :
|
||||||
|
- The rigid container must be kept inside its parent container
|
||||||
|
- The rigid container must be inside an unallocated space of its parent. Meaning, that it cannot overlap with another sibling.
|
||||||
|
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
This behavior has many applications to it. Mainly about recalculations.
|
||||||
|
|
||||||
|
You may want to resize/move quickly and be certain that it does not overflow its parent.
|
||||||
|
|
||||||
|
You may want to resize its parent and makes its resize its children.
|
||||||
|
|
||||||
|
You may want siblings to interact with each other.
|
||||||
|
|
||||||
|
|
||||||
|
## Code references and algorithms
|
||||||
|
|
||||||
|
Its algorithm can be a little complicated due to the numerous uses cases.
|
||||||
|
|
||||||
|
|
||||||
|
### First rule
|
||||||
|
|
||||||
|
Lets start with the first rule : *The rigid container must be kept inside its parent container*
|
||||||
|
|
||||||
|
Inside the `RigidBodyBehaviors.ts`, see `constraintBodyInsideParent()` and `constraintBodyInsideSpace()`.
|
||||||
|
|
||||||
|
As you can see `constraintBodyInsideParent()` is just a wrapper for `constraintBodyInsideSpace()`, so lets just study the last function.
|
||||||
|
|
||||||
|
This is a simple problem of two rectangle.
|
||||||
|
|
||||||
|
In order to restrict the child to its parent,
|
||||||
|
we need to know firstly, if the children is not bigger than its parent.
|
||||||
|
|
||||||
|
If it is, we just need to set the child at the beginning and makes it takes the full size of its parent.
|
||||||
|
|
||||||
|
If it is not, we need to check if the children is out of bound (outside its parent). And if it is out of bound, we need to move it back inside.
|
||||||
|
|
||||||
|
To check if it bigger than its parent we just need to compare their sizes : `childWidth > parentWidth` and vertically `childHeight > parentHeight`.
|
||||||
|
|
||||||
|
If false we need to check out of bound, check for x (and y): `child.x < parent.x` for the left side or `child.x + child.width > parent.x + parent.width` for the right side. We don't want the overlap either which is why we uses `child.width`.
|
||||||
|
|
||||||
|
The condition is also equivalent to `child.x > parent.x + parent.width - child.width` which could makes more sense as the required space must be smaller because of the child size.
|
||||||
|
|
||||||
|
|
||||||
|
In my algorithm, I decided to put them near the edge where they went out of bound :
|
||||||
|
|
||||||
|
```
|
||||||
|
left oob: child.x = parent.x
|
||||||
|
right oob: child.x = parent.x + parent.width - child.width
|
||||||
|
```
|
||||||
|
|
||||||
|
Pseudo-code :
|
||||||
|
|
||||||
|
```c
|
||||||
|
constraintBodyInsideSpace(child, parent) {
|
||||||
|
if (child is bigger than parent) {
|
||||||
|
if (child is larger) {
|
||||||
|
set child x and width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is taller) {
|
||||||
|
set child y and height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (child is to the left of parent) {
|
||||||
|
set child x at the left side of parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is to the right of parent) {
|
||||||
|
set child x at the right side of parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is higher than parent) {
|
||||||
|
set child y at the top of parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is lower than parent) {
|
||||||
|
set child y at the bottom of parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Second rule
|
||||||
|
|
||||||
|
The second rule is the most important and complicated as it must interact with its siblings.
|
||||||
|
|
||||||
|
*The rigid container must be inside an unallocated space of its parent. Meaning, that it cannot overlap with another sibling.*
|
||||||
|
|
||||||
|
Let's first define what is a *space* : a *space* is the width of a container. Which consequently means that the rule only applies on the horizontal view. To simply the matter that also means that we only need to work on one dimension.
|
||||||
|
|
||||||
|
To solve this problem, like the parent, we could use collision detection between its siblings. However this could be very slow as the worst case scenario is a cartesian product: O(n2). Because for each container we need to search for other container that collide with use. When it collide we need to move it and search again.
|
||||||
|
|
||||||
|
Remember, this rule is applied every time you change a property of container, this is *lag*. We cannot afford to inefficient loops.
|
||||||
|
|
||||||
|
Let us use a "system of space" that has "containers" that cannot "overlap".
|
||||||
|
|
||||||
|
Memory.
|
||||||
|
|
||||||
|
Memory, RAM, Hard drive space, handles their space through a system of adresses and chunks of spaces (words, bytes...). In our case we don't have chunk of spaces but floating numbers (which can be a pain the work with because of the edge cases).
|
||||||
|
|
||||||
|
This system is particularly useful as it remember the space used after every iteration of allocation, meaning that we can know exactly when there is no more space inside parent and when a container must resized itself in order to fit inside.
|
||||||
|
|
||||||
|
Alright let us start the algorithm. See `constraintBodyInsideUnallocatedWidth()`, `getAvailableWidths()` and `getAvailableWidthsTwoLines()` in `RigidBodyBehaviors.ts` for implementation reference.
|
||||||
|
|
||||||
|
We have initially the whole space available: let `space` be this available space in the parent.
|
||||||
|
|
||||||
|
`space` is a pointer, thus at the beginning it has `0` to its pointer address and `parent.width` as it space.
|
||||||
|
|
||||||
|
To simplify the algorithm when adding a container, let us compare it to eating a bûche de Noël (yule log).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Like eating the cake, we need to cut it and take a part.
|
||||||
|
|
||||||
|
There is 5 possible way to cut it :
|
||||||
|
- Not eating the cake (maybe we prefer to eat a different cake/part)
|
||||||
|
- Eating the whole cake
|
||||||
|
- Cutting the cake at the left side
|
||||||
|
- Cutting the cake at the right right
|
||||||
|
- Cutting the cake in the middle
|
||||||
|
|
||||||
|
Not cutting the cake means returning the whole cake as is.
|
||||||
|
|
||||||
|
Eating the whole cake is to not returns anything.
|
||||||
|
|
||||||
|
Cutting at the left or the right side means leave 1 part.
|
||||||
|
|
||||||
|
Cutting the middle means leaving two part.
|
||||||
|
|
||||||
|
After cutting the cake, *while* there is still some left, we can continue the operation. (it is a *for* loop in the code though for syntax reasons)
|
||||||
|
|
||||||
|
However after serving for the siblings we *may* notice that there is no more left for us. We get angry, we *throw* a tantrum.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// if you did not understood the joke
|
||||||
|
if (there no more cake) {
|
||||||
|
throw tantrum
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait, there is actually cake!
|
||||||
|
|
||||||
|
But it is left in multiple parts, we will just takes the *closest* one that *fits* our hunger.
|
||||||
|
|
||||||
|
If there is one that fits our hunger let's take it!
|
||||||
|
|
||||||
|
Yet! There is cake but none fits our hunger. But we do have a *minimum* acceptance, let us be humble, we will still take the small bit. By the way, taking multiple part would look bad for us. Nonetheless, if my *minimum* acceptance were to be higher than what is left, I would *throw* a *warning* for next time.
|
||||||
|
|
||||||
|
|
||||||
|
Alright lets translate this in pseudo-code.
|
||||||
|
|
||||||
|
Let us start with getting the available spaces :
|
||||||
|
|
||||||
|
```c#
|
||||||
|
getAvailableSpaces(parent, me) {
|
||||||
|
spaces = [{ x: 0, size: parent.width }]
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
while (spaces.length > 0 and i < parent.length) {
|
||||||
|
let sibling = parent.children[i];
|
||||||
|
|
||||||
|
if (sibling is me or is neither rigid nor is anchor) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spacesLeft be an array
|
||||||
|
|
||||||
|
foreach(space in spaces) {
|
||||||
|
spacesLeftOfSpace = allocate(sibling, space);
|
||||||
|
spacesLeft.concat(spacesLeftOfSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces = spacesLeft
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return spaces;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To allocate:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
allocate(sibling, space) {
|
||||||
|
if (sibling is not overlapping the space) {
|
||||||
|
return [space]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling overlap the space entirely) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling overlap at the left side) {
|
||||||
|
return [{
|
||||||
|
x: right side of sibling
|
||||||
|
size: right side of space - right side of sibling // "cut the left part"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling overlap at the right side) {
|
||||||
|
return [{
|
||||||
|
x: left side of space
|
||||||
|
size: leftSide of sibling - leftSide of space
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (sibling overlap in the middle)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
x: left side of space
|
||||||
|
size: left side of sibling - left side of space
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: right side of sibling
|
||||||
|
size: right side of space - right side of sibling
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally the top part:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
constraintBodyInsideUnallocatedWidth(parent, container) {
|
||||||
|
spaces = getAvailableSpaces(parent, container)
|
||||||
|
if (there is no more spaces) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces = sort spaces by closest from the middle of container
|
||||||
|
|
||||||
|
spaceFound = spaces.find(space that fit container.space)
|
||||||
|
|
||||||
|
if (no spaceFound) {
|
||||||
|
|
||||||
|
spaceFound = spaces.find(space that fit container.minimumSpace)
|
||||||
|
|
||||||
|
if (no spaceFound) {
|
||||||
|
show warning
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
set container x and width to make it fit
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintBodyInsideSpace(container, spaceFound)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This algorithm is great but, some problems remains:
|
||||||
|
- Finding the closest takes O(nlogn) with n being the number of spaces. This is usually not bad since the objective of the rigid body is to **fill** space. But it still does have a very bad worst case.
|
||||||
|
- There is 2 searches for space, same problem but the previous sort helps to make it faster for the best cases
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Anchor behavior
|
||||||
|
|
||||||
|
The anchor behavior allows a container to gain priority over its siblings.
|
||||||
|
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
It has the following rules:
|
||||||
|
- The container cannot be moved by other rigid siblings container
|
||||||
|
- The container cannot be resized by any other siblings container
|
||||||
|
- The container cannot overlap any other siblings rigid container :
|
||||||
|
- overlapping containers are shifted to the nearest available space/width
|
||||||
|
- or resized when there is no available space left other than theirs
|
||||||
|
- or lose their rigid body properties when there is absolutely no available space left (not even theirs)
|
||||||
|
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
Gaining priority can helps makes sure that a rigid object won't move no matter what and will absolutly move no matter what is under it.
|
||||||
|
|
||||||
|
|
||||||
|
## Code references and algorithms
|
||||||
|
|
||||||
|
While there is a more rules applied to this behavior, most of them are just conditions.
|
||||||
|
|
||||||
|
These three rules:
|
||||||
|
- The container cannot be moved by other rigid siblings container
|
||||||
|
- The container cannot be resized by any other siblings container
|
||||||
|
- It cannot overlap any other siblings rigid container
|
||||||
|
|
||||||
|
Can be translate into a single one: "The container is an allocated space so any container is in contact will move or be resized"
|
||||||
|
|
||||||
|
Meaning that applying the rigid body properties of the sibling will also apply this rule. The difference with the default behavior and the anchor behavior is that the anchor container will be taken into account during the calculation of available space.
|
||||||
|
|
||||||
|
You can think of the default container as a floating panel and the anchor container as a wall. You can go under the floating panel but cannot go over the wall.
|
||||||
|
|
||||||
|
To optimize the algorithm, we just need to find the overlapping siblings since the anchor is not applied to those who are not in collision.
|
||||||
|
|
||||||
|
Pseudo-code :
|
||||||
|
|
||||||
|
```c#
|
||||||
|
ImposePosition(container) {
|
||||||
|
let rigidBodies be the rigid siblings that are not anchor
|
||||||
|
let overlappingRigidBodies be the overlapping rigid siblings of rigidBodies
|
||||||
|
|
||||||
|
foreach(overlappingRigidBody of overlappingRigidBodies) {
|
||||||
|
constraintBodyInsideUnallocatedWidth(overlappingRigidBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, modify `getAvailableSpaces()` so it takes into account anchor containers.
|
||||||
|
|
||||||
|
That's it.
|
BIN
docs/ComponentStructure.drawio
(Stored with Git LFS)
BIN
docs/ComponentStructure.drawio
(Stored with Git LFS)
Binary file not shown.
BIN
docs/Eric BF/01_141017-WG-11328-SYME-VERNUCCI-DET BF ind A.pdf
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/01_141017-WG-11328-SYME-VERNUCCI-DET BF ind A.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/02_141017-WG-11328-SYME-VERNUCCI-DET BF ind B.pdf
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/02_141017-WG-11328-SYME-VERNUCCI-DET BF ind B.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/03_SYME KLINE cde BV.pdf
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/03_SYME KLINE cde BV.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/04_ARC KL_K1485985-ARC-K1485985.pdf
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/04_ARC KL_K1485985-ARC-K1485985.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/05_DT_K1485985.pdf
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/05_DT_K1485985.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/06_ Photo IMG_1406.jpg
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/06_ Photo IMG_1406.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.dwg
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.dwg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.pdf
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.pdf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/Eric BF/image0000001.jpg
(Stored with Git LFS)
Normal file
BIN
docs/Eric BF/image0000001.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
docs/assets/yule-log-cake.jpg
(Stored with Git LFS)
Normal file
BIN
docs/assets/yule-log-cake.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
2
public/Interfaces.d.ts
vendored
2
public/Interfaces.d.ts
vendored
|
@ -38,7 +38,7 @@ declare interface IProperties extends React.CSSProperties {
|
||||||
parentId: string | null
|
parentId: string | null
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
isRigidBody: boolean
|
|
||||||
XPositionReference?: XPositionReference
|
XPositionReference?: XPositionReference
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
public/smartcomponent/svg-layout-designer.html
Normal file
2
public/smartcomponent/svg-layout-designer.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<div id="root">
|
||||||
|
</div>
|
49
public/smartcomponent/svg-layout-designer.ts
Normal file
49
public/smartcomponent/svg-layout-designer.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
namespace SmartBusiness.Web.Components {
|
||||||
|
export class SVGLayoutDesigner extends Components.ComponentBase {
|
||||||
|
|
||||||
|
public constructor(componentInfo: KnockoutComponentTypes.ComponentInfo, params: any) {
|
||||||
|
super(componentInfo, params);
|
||||||
|
setTimeout(() => (window as any).SVGLayoutDesigner.Render(this.$component[0]));
|
||||||
|
this.InitEventsListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetEditorComponent() {
|
||||||
|
return this.$component[0].querySelector('.Editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetCurrentHistoryState() {
|
||||||
|
this.GetEditorComponent().dispatchEvent(new CustomEvent('getCurrentHistoryState'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetEditorState() {
|
||||||
|
this.GetEditorComponent().dispatchEvent(new CustomEvent('getEditorState'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SetEditorState(editorState: IEditorState) {
|
||||||
|
this.GetEditorComponent().dispatchEvent(new CustomEvent('SetEditorState', { detail: editorState }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppendNewHistoryState(historyState: IHistoryState) {
|
||||||
|
this.GetEditorComponent().dispatchEvent(new CustomEvent('appendNewState', { detail: historyState }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public OHistoryState: KnockoutObservable<any>;
|
||||||
|
|
||||||
|
private InitEventsListener() {
|
||||||
|
this.$component[0].addEventListener('getCurrentHistoryState', (e: CustomEvent) => {
|
||||||
|
this.OHistoryState(e.detail);
|
||||||
|
console.log(this.OHistoryState());
|
||||||
|
});
|
||||||
|
this.$component[0].addEventListener('getEditorState', (e) => console.log((e as any).detail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ko.components.register('svg-layout-designer', {
|
||||||
|
viewModel: {
|
||||||
|
createViewModel: function (params, componentInfo) {
|
||||||
|
return new SmartBusiness.Web.Components.SVGLayoutDesigner(componentInfo, params);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: { element: 'svg-layout-designer' }
|
||||||
|
});
|
||||||
|
}
|
14
public/smartcomponent/svg-layout-designer.xcomponent
Normal file
14
public/smartcomponent/svg-layout-designer.xcomponent
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ComponentModel xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.Techform.com/SmartExpert/2009/05">
|
||||||
|
<HasContent>false</HasContent>
|
||||||
|
<Id>0A61000D-FC2D-4490-BB3E-0FAED2AF3FDC</Id>
|
||||||
|
<ImageUrl />
|
||||||
|
<ItemName>svg-layout-designer</ItemName>
|
||||||
|
<Parameters>
|
||||||
|
<ParameterModel>
|
||||||
|
<ItemName>viewModel</ItemName>
|
||||||
|
<Text>ViewModel</Text>
|
||||||
|
</ParameterModel>
|
||||||
|
</Parameters>
|
||||||
|
<Text>svg-layout-designer</Text>
|
||||||
|
</ComponentModel>
|
|
@ -4,22 +4,19 @@ onmessage = (e) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCircularReplacer = () => {
|
const getCircularReplacer = () => {
|
||||||
const seen = new WeakSet();
|
|
||||||
return (key, value) => {
|
return (key, value) => {
|
||||||
if (key === 'parent') {
|
if (key === 'parent') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'SelectedContainer') {
|
if (key === 'Symbols') {
|
||||||
return;
|
return Array.from(value.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (key === 'linkedContainers') {
|
||||||
if (seen.has(value)) {
|
return Array.from(value);
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { fetchConfiguration } from './api';
|
import { FetchConfiguration } from './api';
|
||||||
|
|
||||||
describe.concurrent('API test', () => {
|
describe.concurrent('API test', () => {
|
||||||
it('Load environment', () => {
|
it('Load environment', () => {
|
||||||
|
@ -8,7 +8,7 @@ describe.concurrent('API test', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Fetch configuration', async() => {
|
it('Fetch configuration', async() => {
|
||||||
const configuration = await fetchConfiguration();
|
const configuration = await FetchConfiguration();
|
||||||
expect(configuration.MainContainer).toBeDefined();
|
expect(configuration.MainContainer).toBeDefined();
|
||||||
expect(configuration.MainContainer.Height).toBeGreaterThan(0);
|
expect(configuration.MainContainer.Height).toBeGreaterThan(0);
|
||||||
expect(configuration.MainContainer.Width).toBeGreaterThan(0);
|
expect(configuration.MainContainer.Width).toBeGreaterThan(0);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||||
* Fetch the configuration from the API
|
* Fetch the configuration from the API
|
||||||
* @returns {Configation} The model of the configuration for the application
|
* @returns {Configation} The model of the configuration for the application
|
||||||
*/
|
*/
|
||||||
export async function fetchConfiguration(): Promise<IConfiguration> {
|
export async function FetchConfiguration(): Promise<IConfiguration> {
|
||||||
const url = `${import.meta.env.VITE_API_URL}`;
|
const url = `${import.meta.env.VITE_API_URL}`;
|
||||||
// The test library cannot use the Fetch API
|
// The test library cannot use the Fetch API
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { Revive } from '../../utils/saveload';
|
import { IEditorState } from '../../../Interfaces/IEditorState';
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
import { Revive } from '../../../utils/saveload';
|
||||||
|
|
||||||
export function LoadState(
|
export function LoadState(
|
||||||
editorState: IEditorState,
|
editorState: IEditorState,
|
43
src/Components/App/Actions/MenuActions.ts
Normal file
43
src/Components/App/Actions/MenuActions.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { IConfiguration } from '../../../Interfaces/IConfiguration';
|
||||||
|
import { FetchConfiguration } from '../../API/api';
|
||||||
|
import { IEditorState } from '../../../Interfaces/IEditorState';
|
||||||
|
import { LoadState } from './Load';
|
||||||
|
import { GetDefaultEditorState } from '../../../utils/default';
|
||||||
|
|
||||||
|
export function NewEditor(
|
||||||
|
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||||
|
setLoaded: Dispatch<SetStateAction<boolean>>
|
||||||
|
): void {
|
||||||
|
// Fetch the configuration from the API
|
||||||
|
FetchConfiguration()
|
||||||
|
.then((configuration: IConfiguration) => {
|
||||||
|
// Set the editor from the given properties of the API
|
||||||
|
const editorState: IEditorState = GetDefaultEditorState(configuration);
|
||||||
|
|
||||||
|
setEditorState(editorState);
|
||||||
|
setLoaded(true);
|
||||||
|
}, (error) => {
|
||||||
|
console.warn('[NewEditor] Could not fetch resource from API. Using default.', error);
|
||||||
|
setLoaded(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadEditor(
|
||||||
|
files: FileList | null,
|
||||||
|
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||||
|
setLoaded: Dispatch<SetStateAction<boolean>>
|
||||||
|
): void {
|
||||||
|
if (files === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
const editorState: IEditorState = JSON.parse(result);
|
||||||
|
|
||||||
|
LoadState(editorState, setEditorState, setLoaded);
|
||||||
|
});
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
|
@ -1,38 +1,24 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
import { MainMenu } from '../MainMenu/MainMenu';
|
import { MainMenu } from '../MainMenu/MainMenu';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import Editor from '../Editor/Editor';
|
import { Editor } from '../Editor/Editor';
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||||
import { LoadState } from './Load';
|
import { LoadState } from './Actions/Load';
|
||||||
import { LoadEditor, NewEditor } from './MenuActions';
|
import { LoadEditor, NewEditor } from './Actions/MenuActions';
|
||||||
import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
|
import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
|
||||||
|
|
||||||
// App will never have props
|
// App will never have props
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
interface IAppProps {
|
interface IAppProps {
|
||||||
|
root: Element | Document
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App: React.FunctionComponent<IAppProps> = (props) => {
|
function UseHTTPGETStatePreloading(
|
||||||
const [isLoaded, setLoaded] = useState<boolean>(false);
|
isLoaded: boolean,
|
||||||
|
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
||||||
const defaultMainContainer = new ContainerModel(
|
setLoaded: Dispatch<SetStateAction<boolean>>
|
||||||
null,
|
): void {
|
||||||
DEFAULT_MAINCONTAINER_PROPS
|
|
||||||
);
|
|
||||||
|
|
||||||
const [editorState, setEditorState] = useState<IEditorState>({
|
|
||||||
configuration: DEFAULT_CONFIG,
|
|
||||||
history: [{
|
|
||||||
LastAction: '',
|
|
||||||
MainContainer: defaultMainContainer,
|
|
||||||
SelectedContainer: defaultMainContainer,
|
|
||||||
SelectedContainerId: defaultMainContainer.properties.id,
|
|
||||||
TypeCounters: {}
|
|
||||||
}],
|
|
||||||
historyCurrentStep: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const queryString = window.location.search;
|
const queryString = window.location.search;
|
||||||
const urlParams = new URLSearchParams(queryString);
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
@ -53,11 +39,36 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
|
||||||
}, (error) => { throw new Error(error); });
|
}, (error) => { throw new Error(error); });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function App(props: IAppProps): JSX.Element {
|
||||||
|
const [isLoaded, setLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const defaultMainContainer = new ContainerModel(
|
||||||
|
null,
|
||||||
|
DEFAULT_MAINCONTAINER_PROPS
|
||||||
|
);
|
||||||
|
|
||||||
|
const [editorState, setEditorState] = useState<IEditorState>({
|
||||||
|
configuration: DEFAULT_CONFIG,
|
||||||
|
history: [{
|
||||||
|
lastAction: '',
|
||||||
|
mainContainer: defaultMainContainer,
|
||||||
|
selectedContainerId: defaultMainContainer.properties.id,
|
||||||
|
typeCounters: {},
|
||||||
|
symbols: new Map(),
|
||||||
|
selectedSymbolId: ''
|
||||||
|
}],
|
||||||
|
historyCurrentStep: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
UseHTTPGETStatePreloading(isLoaded, setEditorState, setLoaded);
|
||||||
|
|
||||||
if (isLoaded) {
|
if (isLoaded) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Editor
|
<Editor
|
||||||
|
root={props.root}
|
||||||
configuration={editorState.configuration}
|
configuration={editorState.configuration}
|
||||||
history={editorState.history}
|
history={editorState.history}
|
||||||
historyCurrentStep={editorState.historyCurrentStep}
|
historyCurrentStep={editorState.historyCurrentStep}
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
|
||||||
import { fetchConfiguration } from '../API/api';
|
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
|
||||||
import { LoadState } from './Load';
|
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
|
||||||
import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
|
|
||||||
|
|
||||||
export function NewEditor(
|
|
||||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
|
||||||
setLoaded: Dispatch<SetStateAction<boolean>>
|
|
||||||
): void {
|
|
||||||
// Fetch the configuration from the API
|
|
||||||
fetchConfiguration()
|
|
||||||
.then((configuration: IConfiguration) => {
|
|
||||||
// Set the main container from the given properties of the API
|
|
||||||
const MainContainer = new ContainerModel(
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
...DEFAULT_MAINCONTAINER_PROPS,
|
|
||||||
width: Number(configuration.MainContainer.Width),
|
|
||||||
height: Number(configuration.MainContainer.Height)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save the configuration and the new MainContainer
|
|
||||||
// and default the selected container to it
|
|
||||||
const editorState: IEditorState = {
|
|
||||||
configuration,
|
|
||||||
history:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
LastAction: '',
|
|
||||||
MainContainer,
|
|
||||||
SelectedContainer: MainContainer,
|
|
||||||
SelectedContainerId: MainContainer.properties.id,
|
|
||||||
TypeCounters: {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
historyCurrentStep: 0
|
|
||||||
};
|
|
||||||
setEditorState(editorState);
|
|
||||||
setLoaded(true);
|
|
||||||
}, (error) => {
|
|
||||||
// TODO: Implement an alert component
|
|
||||||
console.warn('[NewEditor] Could not fetch resource from API. Using default.', error);
|
|
||||||
setLoaded(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoadEditor(
|
|
||||||
files: FileList | null,
|
|
||||||
setEditorState: Dispatch<SetStateAction<IEditorState>>,
|
|
||||||
setLoaded: Dispatch<SetStateAction<boolean>>
|
|
||||||
): void {
|
|
||||||
if (files === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file = files[0];
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.addEventListener('load', () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
const editorState: IEditorState = JSON.parse(result);
|
|
||||||
|
|
||||||
LoadState(editorState, setEditorState, setLoaded);
|
|
||||||
});
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
|
@ -1,39 +1,40 @@
|
||||||
import { ClockIcon, CubeIcon, MapIcon } from '@heroicons/react/outline';
|
import { ClockIcon, CubeIcon, LinkIcon } from '@heroicons/react/outline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { BarIcon } from './BarIcon';
|
import { BarIcon } from './BarIcon';
|
||||||
|
|
||||||
interface IBarProps {
|
interface IBarProps {
|
||||||
isSidebarOpen: boolean
|
isSidebarOpen: boolean
|
||||||
|
isSymbolsOpen: boolean
|
||||||
isElementsSidebarOpen: boolean
|
isElementsSidebarOpen: boolean
|
||||||
isHistoryOpen: boolean
|
isHistoryOpen: boolean
|
||||||
ToggleSidebar: () => void
|
toggleSidebar: () => void
|
||||||
ToggleElementsSidebar: () => void
|
toggleSymbols: () => void
|
||||||
ToggleTimeline: () => void
|
toggleTimeline: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BAR_WIDTH = 64; // 4rem
|
export const BAR_WIDTH = 64; // 4rem
|
||||||
|
|
||||||
export const Bar: React.FC<IBarProps> = (props) => {
|
export function Bar(props: IBarProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'>
|
<div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'>
|
||||||
<BarIcon
|
<BarIcon
|
||||||
isActive={props.isSidebarOpen}
|
isActive={props.isSidebarOpen}
|
||||||
title='Components'
|
title='Components'
|
||||||
onClick={() => props.ToggleSidebar()}>
|
onClick={() => props.toggleSidebar()}>
|
||||||
<CubeIcon className='heroicon' />
|
<CubeIcon className='heroicon' />
|
||||||
</BarIcon>
|
</BarIcon>
|
||||||
<BarIcon
|
<BarIcon
|
||||||
isActive={props.isElementsSidebarOpen}
|
isActive={props.isSymbolsOpen}
|
||||||
title='Map'
|
title='Symbols'
|
||||||
onClick={() => props.ToggleElementsSidebar()}>
|
onClick={() => props.toggleSymbols()}>
|
||||||
<MapIcon className='heroicon'/>
|
<LinkIcon className='heroicon' />
|
||||||
</BarIcon>
|
</BarIcon>
|
||||||
<BarIcon
|
<BarIcon
|
||||||
isActive={props.isHistoryOpen}
|
isActive={props.isHistoryOpen}
|
||||||
title='Timeline'
|
title='Timeline'
|
||||||
onClick={() => props.ToggleTimeline()}>
|
onClick={() => props.toggleTimeline()}>
|
||||||
<ClockIcon className='heroicon' />
|
<ClockIcon className='heroicon' />
|
||||||
</BarIcon>
|
</BarIcon>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ interface IBarIconProps {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BarIcon: React.FC<IBarIconProps> = (props) => {
|
export function BarIcon(props: IBarIconProps): JSX.Element {
|
||||||
const isActiveClasses = props.isActive ? 'border-l-4 border-blue-500 bg-slate-200' : '';
|
const isActiveClasses = props.isActive ? 'border-l-4 border-blue-500 bg-slate-200' : '';
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button"
|
||||||
className={`bar-btn group ${isActiveClasses}`}
|
className={`bar-btn group ${isActiveClasses}`}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
onClick={() => props.onClick()}
|
onClick={() => props.onClick()}
|
||||||
|
@ -19,4 +19,4 @@ export const BarIcon: React.FC<IBarIconProps> = (props) => {
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
232
src/Components/ContainerProperties/ContainerForm.tsx
Normal file
232
src/Components/ContainerProperties/ContainerForm.tsx
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
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 { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, TransformX } from '../../utils/svg';
|
||||||
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
|
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||||
|
import { Select } from '../Select/Select';
|
||||||
|
|
||||||
|
interface IContainerFormProps {
|
||||||
|
properties: IContainerProperties
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetCSSInputs(properties: IContainerProperties,
|
||||||
|
onChange: (key: string, value: string | number | boolean, type: PropertyType) => 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, PropertyType.Style)} />);
|
||||||
|
}
|
||||||
|
return groupInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContainerForm(props: IContainerFormProps): JSX.Element {
|
||||||
|
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}
|
||||||
|
isDisabled={true} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='Type'
|
||||||
|
inputKey='type'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={props.properties.type}
|
||||||
|
isDisabled={true} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='Displayed text'
|
||||||
|
inputKey='displayedText'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={props.properties.displayedText?.toString()}
|
||||||
|
onChange={(event) => props.onChange('displayedText', event.target.value)} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='x'
|
||||||
|
inputKey='x'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
isDisabled={props.properties.linkedSymbolId !== ''}
|
||||||
|
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'
|
||||||
|
inputKey='y'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
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'
|
||||||
|
inputKey='minWidth'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
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'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={props.properties.minWidth}
|
||||||
|
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'
|
||||||
|
inputKey='height'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
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='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.isFlex}
|
||||||
|
onChange={(event) => props.onChange('isFlex', 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))} />
|
||||||
|
<Select
|
||||||
|
inputKey='linkedSymbolId'
|
||||||
|
labelText='Align with symbol'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
inputs={[...props.symbols.values()].map(symbol => ({
|
||||||
|
text: symbol.id,
|
||||||
|
value: symbol.id
|
||||||
|
}))}
|
||||||
|
value={props.properties.linkedSymbolId ?? ''}
|
||||||
|
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} />
|
||||||
|
{GetCSSInputs(props.properties, props.onChange)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,15 +2,15 @@ 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 { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import { IContainerProperties } from '../../Interfaces/IContainerProperties';
|
||||||
import { Properties } from './Properties';
|
import { Properties } from './ContainerProperties';
|
||||||
|
|
||||||
describe.concurrent('Properties', () => {
|
describe.concurrent('Properties', () => {
|
||||||
it('No properties', () => {
|
it('No properties', () => {
|
||||||
render(<Properties
|
render(<Properties
|
||||||
properties={undefined}
|
properties={undefined}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
onSubmit={() => {}}
|
symbols={new Map()}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.queryByText('id')).toBeNull();
|
expect(screen.queryByText('id')).toBeNull();
|
||||||
|
@ -20,17 +20,21 @@ describe.concurrent('Properties', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Some properties, change values with dynamic input', () => {
|
it('Some properties, change values with dynamic input', () => {
|
||||||
const prop: IProperties = {
|
const prop: IContainerProperties = {
|
||||||
id: 'stuff',
|
id: 'stuff',
|
||||||
|
type: 'type',
|
||||||
parentId: 'parentId',
|
parentId: 'parentId',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'stuff',
|
displayedText: 'stuff',
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 1,
|
y: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
XPositionReference: XPositionReference.Left,
|
maxWidth: Infinity,
|
||||||
isRigidBody: false,
|
margin: {},
|
||||||
|
xPositionReference: XPositionReference.Left,
|
||||||
|
isFlex: false,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ describe.concurrent('Properties', () => {
|
||||||
const { container, rerender } = render(<Properties
|
const { container, rerender } = render(<Properties
|
||||||
properties={prop}
|
properties={prop}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onSubmit={() => {}}
|
symbols={new Map()}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.queryByText('id')).toBeDefined();
|
expect(screen.queryByText('id')).toBeDefined();
|
||||||
|
@ -75,7 +79,7 @@ describe.concurrent('Properties', () => {
|
||||||
rerender(<Properties
|
rerender(<Properties
|
||||||
properties={Object.assign({}, prop)}
|
properties={Object.assign({}, prop)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onSubmit={() => {}}
|
symbols={new Map()}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
propertyId = container.querySelector('#id');
|
propertyId = container.querySelector('#id');
|
26
src/Components/ContainerProperties/ContainerProperties.tsx
Normal file
26
src/Components/ContainerProperties/ContainerProperties.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { PropertyType } from '../../Enums/PropertyType';
|
||||||
|
import { IContainerProperties } from '../../Interfaces/IContainerProperties';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { ContainerForm } from './ContainerForm';
|
||||||
|
|
||||||
|
interface IPropertiesProps {
|
||||||
|
properties?: IContainerProperties
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Properties(props: IPropertiesProps): JSX.Element {
|
||||||
|
if (props.properties === undefined) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
||||||
|
<ContainerForm
|
||||||
|
properties={props.properties}
|
||||||
|
symbols={props.symbols}
|
||||||
|
onChange={props.onChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
528
src/Components/Editor/Actions/ContainerOperations.ts
Normal file
528
src/Components/Editor/Actions/ContainerOperations.ts
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { IHistoryState } from '../../../Interfaces/IHistoryState';
|
||||||
|
import { IConfiguration } from '../../../Interfaces/IConfiguration';
|
||||||
|
import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { FindContainerById, MakeIterator } from '../../../utils/itertools';
|
||||||
|
import { GetCurrentHistory, UpdateCounters } from '../Editor';
|
||||||
|
import { AddMethod } from '../../../Enums/AddMethod';
|
||||||
|
import { IAvailableContainer } from '../../../Interfaces/IAvailableContainer';
|
||||||
|
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../../utils/default';
|
||||||
|
import { ApplyBehaviors } from '../Behaviors/Behaviors';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
import { ApplyMargin, TransformX } from '../../../utils/svg';
|
||||||
|
import { PropertyType } from '../../../Enums/PropertyType';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a container
|
||||||
|
* @param container Selected container
|
||||||
|
*/
|
||||||
|
export function SelectContainer(
|
||||||
|
containerId: string,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Select ${containerId}`,
|
||||||
|
mainContainer: structuredClone(current.mainContainer),
|
||||||
|
selectedContainerId: containerId,
|
||||||
|
typeCounters: Object.assign({}, current.typeCounters),
|
||||||
|
symbols: structuredClone(current.symbols),
|
||||||
|
selectedSymbolId: current.selectedSymbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a container
|
||||||
|
* @param containerId containerId of the container to delete
|
||||||
|
* @param fullHistory History of the editor
|
||||||
|
* @param historyCurrentStep Current step
|
||||||
|
* @param setHistory State setter for History
|
||||||
|
* @param setHistoryCurrentStep State setter for current step
|
||||||
|
*/
|
||||||
|
export function DeleteContainer(
|
||||||
|
containerId: string,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
const mainContainerClone: IContainerModel = structuredClone(current.mainContainer);
|
||||||
|
const container = FindContainerById(mainContainerClone, containerId);
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container === mainContainerClone ||
|
||||||
|
container.parent === undefined ||
|
||||||
|
container.parent === null) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Oops...',
|
||||||
|
text: 'Deleting the main container is not allowed!',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container === null || container === undefined) {
|
||||||
|
throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSymbols = structuredClone(current.symbols);
|
||||||
|
UnlinkSymbol(newSymbols, container);
|
||||||
|
|
||||||
|
const index = container.parent.children.indexOf(container);
|
||||||
|
if (index > -1) {
|
||||||
|
container.parent.children.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyBehaviorsOnSiblings(container, current.symbols);
|
||||||
|
|
||||||
|
// Select the previous container
|
||||||
|
// or select the one above
|
||||||
|
const selectedContainerId = GetSelectedContainerOnDelete(
|
||||||
|
mainContainerClone,
|
||||||
|
current.selectedContainerId,
|
||||||
|
container.parent,
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Delete ${containerId}`,
|
||||||
|
mainContainer: mainContainerClone,
|
||||||
|
selectedContainerId,
|
||||||
|
typeCounters: Object.assign({}, current.typeCounters),
|
||||||
|
symbols: newSymbols,
|
||||||
|
selectedSymbolId: current.selectedSymbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetSelectedContainerOnDelete(
|
||||||
|
mainContainerClone: IContainerModel,
|
||||||
|
selectedContainerId: string,
|
||||||
|
parent: IContainerModel,
|
||||||
|
index: number
|
||||||
|
): string {
|
||||||
|
const newSelectedContainer = FindContainerById(mainContainerClone, selectedContainerId) ??
|
||||||
|
parent.children.at(index - 1) ??
|
||||||
|
parent;
|
||||||
|
const newSelectedContainerId = newSelectedContainer.properties.id;
|
||||||
|
return newSelectedContainerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnlinkSymbol(symbols: Map<string, ISymbolModel>, container: IContainerModel): void {
|
||||||
|
const it = MakeIterator(container);
|
||||||
|
for (const child of it) {
|
||||||
|
const symbol = symbols.get(child.properties.linkedSymbolId);
|
||||||
|
if (symbol === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
symbol.linkedContainers.delete(child.properties.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new container to a selected container
|
||||||
|
* @param type The type of container
|
||||||
|
* @param configuration Configuration of the App
|
||||||
|
* @param fullHistory History of the editor
|
||||||
|
* @param historyCurrentStep Current step
|
||||||
|
* @param setHistory State setter for History
|
||||||
|
* @param setHistoryCurrentStep State setter for current step
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function AddContainerToSelectedContainer(
|
||||||
|
type: string,
|
||||||
|
selected: IContainerModel | undefined,
|
||||||
|
configuration: IConfiguration,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
if (selected === null ||
|
||||||
|
selected === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = selected;
|
||||||
|
AddContainer(
|
||||||
|
parent.children.length,
|
||||||
|
type,
|
||||||
|
parent.properties.id,
|
||||||
|
configuration,
|
||||||
|
fullHistory,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and add a new container at `index` in children of parent of `parentId`
|
||||||
|
* @param index Index where to insert to the new container
|
||||||
|
* @param type Type of container
|
||||||
|
* @param parentId Parent in which to insert the new container
|
||||||
|
* @param configuration Configuration of the app
|
||||||
|
* @param fullHistory History of the editor
|
||||||
|
* @param historyCurrentStep Current step
|
||||||
|
* @param setHistory State setter of History
|
||||||
|
* @param setHistoryCurrentStep State setter of the current step
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function AddContainer(
|
||||||
|
index: number,
|
||||||
|
type: string,
|
||||||
|
parentId: string,
|
||||||
|
configuration: IConfiguration,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
// Get the preset properties from the API
|
||||||
|
const containerConfig = configuration.AvailableContainers
|
||||||
|
.find(option => option.Type === type);
|
||||||
|
|
||||||
|
if (containerConfig === undefined) {
|
||||||
|
throw new Error(`[AddContainer] Object type not found. Found: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the counter of the object type in order to assign an unique id
|
||||||
|
const newCounters = Object.assign({}, current.typeCounters);
|
||||||
|
UpdateCounters(newCounters, type);
|
||||||
|
const count = newCounters[type];
|
||||||
|
|
||||||
|
// Create maincontainer model
|
||||||
|
const clone: IContainerModel = structuredClone(current.mainContainer);
|
||||||
|
|
||||||
|
// Find the parent
|
||||||
|
const parentClone: IContainerModel | undefined = FindContainerById(
|
||||||
|
clone, parentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parentClone === null || parentClone === undefined) {
|
||||||
|
throw new Error('[AddContainer] Container model was not found among children of the main container!');
|
||||||
|
}
|
||||||
|
const left: number = containerConfig.Margin?.left ?? 0;
|
||||||
|
const bottom: number = containerConfig.Margin?.bottom ?? 0;
|
||||||
|
const top: number = containerConfig.Margin?.top ?? 0;
|
||||||
|
const right: number = containerConfig.Margin?.right ?? 0;
|
||||||
|
|
||||||
|
let x = containerConfig.DefaultX ?? 0;
|
||||||
|
let y = containerConfig.DefaultY ?? 0;
|
||||||
|
let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width;
|
||||||
|
let height = containerConfig.Height ?? parentClone.properties.height;
|
||||||
|
|
||||||
|
({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right));
|
||||||
|
|
||||||
|
x = ApplyAddMethod(index, containerConfig, parentClone, x);
|
||||||
|
|
||||||
|
const defaultProperties = GetDefaultContainerProps(
|
||||||
|
type,
|
||||||
|
count,
|
||||||
|
parentClone,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
containerConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the container
|
||||||
|
const newContainer = new ContainerModel(
|
||||||
|
parentClone,
|
||||||
|
defaultProperties,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
type
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
parentClone.children.push(newContainer);
|
||||||
|
UpdateParentChildrenList(parentClone);
|
||||||
|
|
||||||
|
InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters);
|
||||||
|
|
||||||
|
ApplyBehaviors(newContainer, current.symbols);
|
||||||
|
|
||||||
|
ApplyBehaviorsOnSiblings(newContainer, current.symbols);
|
||||||
|
|
||||||
|
// Update the state
|
||||||
|
history.push({
|
||||||
|
lastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
|
||||||
|
mainContainer: clone,
|
||||||
|
selectedContainerId: parentClone.properties.id,
|
||||||
|
typeCounters: newCounters,
|
||||||
|
symbols: structuredClone(current.symbols),
|
||||||
|
selectedSymbolId: current.selectedSymbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateParentChildrenList(parentClone: IContainerModel | null | undefined): void {
|
||||||
|
if (parentClone === null || parentClone === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parentClone.children.sort(
|
||||||
|
(a, b) => TransformX(a.properties.x, a.properties.width, a.properties.xPositionReference) -
|
||||||
|
TransformX(b.properties.x, b.properties.width, b.properties.xPositionReference)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InitializeDefaultChild(
|
||||||
|
configuration: IConfiguration,
|
||||||
|
containerConfig: IAvailableContainer,
|
||||||
|
newContainer: ContainerModel,
|
||||||
|
newCounters: Record<string, number>
|
||||||
|
): void {
|
||||||
|
if (containerConfig.DefaultChildType === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentConfig = configuration.AvailableContainers
|
||||||
|
.find(option => option.Type === containerConfig.DefaultChildType);
|
||||||
|
let parent = newContainer;
|
||||||
|
let depth = 0;
|
||||||
|
const seen = new Set<string>([containerConfig.Type]);
|
||||||
|
|
||||||
|
while (currentConfig !== undefined &&
|
||||||
|
depth <= DEFAULTCHILDTYPE_MAX_DEPTH
|
||||||
|
) {
|
||||||
|
if (!DEFAULTCHILDTYPE_ALLOW_CYCLIC && seen.has(currentConfig.Type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(currentConfig.Type);
|
||||||
|
|
||||||
|
const left: number = currentConfig.Margin?.left ?? 0;
|
||||||
|
const bottom: number = currentConfig.Margin?.bottom ?? 0;
|
||||||
|
const top: number = currentConfig.Margin?.top ?? 0;
|
||||||
|
const right: number = currentConfig.Margin?.right ?? 0;
|
||||||
|
let x = currentConfig.DefaultX ?? 0;
|
||||||
|
let y = currentConfig.DefaultY ?? 0;
|
||||||
|
let width = currentConfig.Width ?? currentConfig.MaxWidth ?? currentConfig.MinWidth ?? parent.properties.width;
|
||||||
|
let height = currentConfig.Height ?? parent.properties.height;
|
||||||
|
|
||||||
|
({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right));
|
||||||
|
|
||||||
|
UpdateCounters(newCounters, currentConfig.Type);
|
||||||
|
const count = newCounters[currentConfig.Type];
|
||||||
|
const defaultChildProperties = GetDefaultContainerProps(
|
||||||
|
currentConfig.Type,
|
||||||
|
count,
|
||||||
|
parent,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
currentConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the container
|
||||||
|
const newChildContainer = new ContainerModel(
|
||||||
|
parent,
|
||||||
|
defaultChildProperties,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
type: currentConfig.Type
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// And push it the the parent children
|
||||||
|
parent.children.push(newChildContainer);
|
||||||
|
|
||||||
|
// iterate
|
||||||
|
depth++;
|
||||||
|
parent = newChildContainer;
|
||||||
|
currentConfig = configuration.AvailableContainers
|
||||||
|
.find(option => option.Type === (currentConfig as IAvailableContainer).DefaultChildType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new offset by applying an Add method (append, insert etc.)
|
||||||
|
* See AddMethod
|
||||||
|
* @param index Index of the container
|
||||||
|
* @param containerConfig Configuration of a container
|
||||||
|
* @param parent Parent container
|
||||||
|
* @param x Additionnal offset
|
||||||
|
* @returns New offset
|
||||||
|
*/
|
||||||
|
function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, parent: IContainerModel, x: number): number {
|
||||||
|
if (index > 0 && (
|
||||||
|
containerConfig.AddMethod === undefined ||
|
||||||
|
containerConfig.AddMethod === AddMethod.Append)) {
|
||||||
|
// Append method (default)
|
||||||
|
const lastChild: IContainerModel | undefined = parent.children.at(index - 1);
|
||||||
|
|
||||||
|
if (lastChild !== undefined) {
|
||||||
|
x += (lastChild.properties.x + lastChild.properties.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handled the property change event in the properties form
|
||||||
|
* @param key Property name
|
||||||
|
* @param value New value of the property
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function OnPropertyChange(
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean,
|
||||||
|
type: PropertyType = PropertyType.Simple,
|
||||||
|
selected: IContainerModel | undefined,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
if (selected === null ||
|
||||||
|
selected === undefined) {
|
||||||
|
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainContainerClone: IContainerModel = structuredClone(current.mainContainer);
|
||||||
|
const container: ContainerModel | undefined = FindContainerById(mainContainerClone, selected.properties.id);
|
||||||
|
|
||||||
|
if (container === null || container === undefined) {
|
||||||
|
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
||||||
|
}
|
||||||
|
|
||||||
|
SetContainer(container, key, value, type, current.symbols);
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Change ${key} of ${container.properties.id}`,
|
||||||
|
mainContainer: mainContainerClone,
|
||||||
|
selectedContainerId: container.properties.id,
|
||||||
|
typeCounters: Object.assign({}, current.typeCounters),
|
||||||
|
symbols: structuredClone(current.symbols),
|
||||||
|
selectedSymbolId: current.selectedSymbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the container with properties and behaviors (mutate)
|
||||||
|
* @param container Container to update
|
||||||
|
* @param key Key of the property to update
|
||||||
|
* @param value Value of the property to update
|
||||||
|
* @param type Type of the property to update
|
||||||
|
* @param symbols Current list of symbols
|
||||||
|
*/
|
||||||
|
function SetContainer(
|
||||||
|
container: ContainerModel,
|
||||||
|
key: string, value: string | number | boolean,
|
||||||
|
type: PropertyType,
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
): void {
|
||||||
|
// get the old symbol to detect unlink
|
||||||
|
const oldSymbolId = container.properties.linkedSymbolId;
|
||||||
|
|
||||||
|
// update the property
|
||||||
|
AssignProperty(container, key, value, type);
|
||||||
|
|
||||||
|
// link the symbol if it exists
|
||||||
|
LinkSymbol(
|
||||||
|
container.properties.id,
|
||||||
|
oldSymbolId,
|
||||||
|
container.properties.linkedSymbolId,
|
||||||
|
symbols
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply special behaviors: rigid, flex, symbol, anchor
|
||||||
|
ApplyBehaviors(container, symbols);
|
||||||
|
|
||||||
|
// Apply special behaviors on siblings
|
||||||
|
ApplyBehaviorsOnSiblings(container, symbols);
|
||||||
|
|
||||||
|
// sort the children list by their position
|
||||||
|
UpdateParentChildrenList(container.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AssignProperty(container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType): void {
|
||||||
|
switch (type) {
|
||||||
|
case PropertyType.Style:
|
||||||
|
(container.properties.style as any)[key] = value;
|
||||||
|
break;
|
||||||
|
case PropertyType.Margin:
|
||||||
|
SetMargin();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
(container.properties as any)[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetMargin(): void {
|
||||||
|
const oldMarginValue: number = (container.properties.margin as any)[key];
|
||||||
|
const diff = Number(value) - oldMarginValue;
|
||||||
|
switch (key) {
|
||||||
|
case 'left':
|
||||||
|
container.properties.x += diff;
|
||||||
|
container.properties.width -= diff;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
container.properties.width -= diff;
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
container.properties.height -= diff;
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
container.properties.y += diff;
|
||||||
|
container.properties.height -= diff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(container.properties.margin as any)[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkSymbol(
|
||||||
|
containerId: string,
|
||||||
|
oldSymbolId: string,
|
||||||
|
newSymbolId: string,
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
): void {
|
||||||
|
const oldSymbol = symbols.get(oldSymbolId);
|
||||||
|
const newSymbol = symbols.get(newSymbolId);
|
||||||
|
|
||||||
|
if (newSymbol === undefined) {
|
||||||
|
if (oldSymbol !== undefined) {
|
||||||
|
oldSymbol.linkedContainers.delete(containerId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSymbol.linkedContainers.add(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyBehaviorsOnSiblings(newContainer: ContainerModel, symbols: Map<string, ISymbolModel>): void {
|
||||||
|
if (newContainer.parent === null || newContainer.parent === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newContainer.parent.children.filter(container => newContainer !== container).forEach(container => ApplyBehaviors(container, symbols));
|
||||||
|
}
|
89
src/Components/Editor/Actions/Save.ts
Normal file
89
src/Components/Editor/Actions/Save.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { IHistoryState } from '../../../Interfaces/IHistoryState';
|
||||||
|
import { IConfiguration } from '../../../Interfaces/IConfiguration';
|
||||||
|
import { GetCircularReplacer } from '../../../utils/saveload';
|
||||||
|
import { ID } from '../../SVG/SVG';
|
||||||
|
import { IEditorState } from '../../../Interfaces/IEditorState';
|
||||||
|
import { SHOW_SELECTOR_TEXT } from '../../../utils/default';
|
||||||
|
|
||||||
|
export function SaveEditorAsJSON(
|
||||||
|
history: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
configuration: IConfiguration
|
||||||
|
): void {
|
||||||
|
const exportName = 'state.json';
|
||||||
|
const spaces = import.meta.env.DEV ? 4 : 0;
|
||||||
|
const editorState: IEditorState = {
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
configuration
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (window.Worker) {
|
||||||
|
// use webworker for the stringify to avoid freezing
|
||||||
|
const myWorker = new Worker('workers/worker.js');
|
||||||
|
myWorker.postMessage({ editorState, spaces });
|
||||||
|
myWorker.onmessage = (event) => {
|
||||||
|
const data = event.data;
|
||||||
|
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
|
||||||
|
CreateDownloadNode(exportName, dataStr);
|
||||||
|
myWorker.terminate();
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.stringify(editorState, GetCircularReplacer(), spaces);
|
||||||
|
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
|
||||||
|
CreateDownloadNode(exportName, dataStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveEditorAsSVG(): void {
|
||||||
|
const svgWrapper = document.getElementById(ID) as HTMLElement;
|
||||||
|
let svg = svgWrapper.querySelector('svg') as SVGSVGElement;
|
||||||
|
|
||||||
|
if (svg === undefined) {
|
||||||
|
throw new Error('[SaveEditorAsSVG] Missing <svg> element');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover svg from SVG Viewer
|
||||||
|
svg = svg.cloneNode(true) as SVGSVGElement;
|
||||||
|
svg.removeAttribute('height');
|
||||||
|
svg.removeAttribute('width');
|
||||||
|
const mainSvg = svg.children[1].children;
|
||||||
|
svg.replaceChildren(...mainSvg);
|
||||||
|
|
||||||
|
// remove the selector
|
||||||
|
const group = svg.children[svg.children.length - 1];
|
||||||
|
group.removeChild(group.children[group.children.length - 1]);
|
||||||
|
if (SHOW_SELECTOR_TEXT) {
|
||||||
|
group.removeChild(group.children[group.children.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get svg source.
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
let source = serializer.serializeToString(svg);
|
||||||
|
|
||||||
|
// add name spaces.
|
||||||
|
if (source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/) == null) {
|
||||||
|
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||||
|
}
|
||||||
|
if (source.match(/^<svg[^>]+"http:\/\/www\.w3\.org\/1999\/xlink"/) == null) {
|
||||||
|
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// add xml declaration
|
||||||
|
source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
|
||||||
|
|
||||||
|
// convert svg source to URI data scheme.
|
||||||
|
const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source);
|
||||||
|
CreateDownloadNode('state.svg', url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateDownloadNode(filename: string, datastring: string): void {
|
||||||
|
const downloadAnchorNode = document.createElement('a');
|
||||||
|
downloadAnchorNode.href = datastring;
|
||||||
|
downloadAnchorNode.download = filename;
|
||||||
|
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||||
|
downloadAnchorNode.click();
|
||||||
|
downloadAnchorNode.remove();
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../../Interfaces/IHistoryState';
|
||||||
import { ENABLE_SHORTCUTS } from '../../utils/default';
|
import { ENABLE_SHORTCUTS } from '../../../utils/default';
|
||||||
|
|
||||||
export function onKeyDown(
|
export function OnKey(
|
||||||
event: KeyboardEvent,
|
event: KeyboardEvent,
|
||||||
history: IHistoryState[],
|
history: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
171
src/Components/Editor/Actions/SymbolOperations.ts
Normal file
171
src/Components/Editor/Actions/SymbolOperations.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { IConfiguration } from '../../../Interfaces/IConfiguration';
|
||||||
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { IHistoryState } from '../../../Interfaces/IHistoryState';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import { GetDefaultSymbolModel } from '../../../utils/default';
|
||||||
|
import { FindContainerById } from '../../../utils/itertools';
|
||||||
|
import { RestoreX } from '../../../utils/svg';
|
||||||
|
import { ApplyBehaviors } from '../Behaviors/Behaviors';
|
||||||
|
import { GetCurrentHistory, UpdateCounters } from '../Editor';
|
||||||
|
|
||||||
|
export function AddSymbol(
|
||||||
|
name: string,
|
||||||
|
configuration: IConfiguration,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
const symbolConfig = configuration.AvailableSymbols
|
||||||
|
.find(option => option.Name === name);
|
||||||
|
|
||||||
|
if (symbolConfig === undefined) {
|
||||||
|
throw new Error('[AddSymbol] Symbol could not be found in the config');
|
||||||
|
}
|
||||||
|
const type = `symbol-${name}`;
|
||||||
|
const newCounters = structuredClone(current.typeCounters);
|
||||||
|
UpdateCounters(newCounters, type);
|
||||||
|
|
||||||
|
const newSymbols = structuredClone(current.symbols);
|
||||||
|
const newSymbol: ISymbolModel = GetDefaultSymbolModel(name, newCounters, type, symbolConfig);
|
||||||
|
newSymbol.x = RestoreX(newSymbol.x, newSymbol.width, newSymbol.config.XPositionReference);
|
||||||
|
|
||||||
|
newSymbols.set(newSymbol.id, newSymbol);
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Add ${name}`,
|
||||||
|
mainContainer: structuredClone(current.mainContainer),
|
||||||
|
selectedContainerId: current.selectedContainerId,
|
||||||
|
typeCounters: newCounters,
|
||||||
|
symbols: newSymbols,
|
||||||
|
selectedSymbolId: newSymbol.id
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectSymbol(
|
||||||
|
symbolId: string,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Select ${symbolId}`,
|
||||||
|
mainContainer: structuredClone(current.mainContainer),
|
||||||
|
selectedContainerId: current.selectedContainerId,
|
||||||
|
typeCounters: structuredClone(current.typeCounters),
|
||||||
|
symbols: structuredClone(current.symbols),
|
||||||
|
selectedSymbolId: symbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteSymbol(
|
||||||
|
symbolId: string,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
const newSymbols = structuredClone(current.symbols);
|
||||||
|
const symbol = newSymbols.get(symbolId);
|
||||||
|
|
||||||
|
if (symbol === undefined) {
|
||||||
|
throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMainContainer = structuredClone(current.mainContainer);
|
||||||
|
|
||||||
|
UnlinkContainers(symbol, newMainContainer);
|
||||||
|
|
||||||
|
newSymbols.delete(symbolId);
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Select ${symbolId}`,
|
||||||
|
mainContainer: newMainContainer,
|
||||||
|
selectedContainerId: current.selectedContainerId,
|
||||||
|
typeCounters: structuredClone(current.typeCounters),
|
||||||
|
symbols: newSymbols,
|
||||||
|
selectedSymbolId: symbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnlinkContainers(symbol: ISymbolModel, newMainContainer: IContainerModel): void {
|
||||||
|
symbol.linkedContainers.forEach((containerId) => {
|
||||||
|
const container = FindContainerById(newMainContainer, containerId);
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.properties.linkedSymbolId = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handled the property change event in the properties form
|
||||||
|
* @param key Property name
|
||||||
|
* @param value New value of the property
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function OnPropertyChange(
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
if (current.selectedSymbolId === '') {
|
||||||
|
throw new Error('[OnSymbolPropertyChange] Property was changed before selecting a symbol');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSymbols: Map<string, ISymbolModel> = structuredClone(current.symbols);
|
||||||
|
const symbol = newSymbols.get(current.selectedSymbolId);
|
||||||
|
|
||||||
|
if (symbol === null || symbol === undefined) {
|
||||||
|
throw new Error('[OnSymbolPropertyChange] Symbol model was not found in state!');
|
||||||
|
}
|
||||||
|
|
||||||
|
(symbol as any)[key] = value;
|
||||||
|
|
||||||
|
const newMainContainer = structuredClone(current.mainContainer);
|
||||||
|
symbol.linkedContainers.forEach((containerId) => {
|
||||||
|
const container = FindContainerById(newMainContainer, containerId);
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyBehaviors(container, newSymbols);
|
||||||
|
});
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
lastAction: `Change ${key} of ${symbol.id}`,
|
||||||
|
mainContainer: newMainContainer,
|
||||||
|
selectedContainerId: current.selectedContainerId,
|
||||||
|
typeCounters: Object.assign({}, current.typeCounters),
|
||||||
|
symbols: newSymbols,
|
||||||
|
selectedSymbolId: symbol.id
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
|
@ -14,26 +14,26 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { constraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
|
import { ConstraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Impose the container position to its siblings
|
* Impose the container position to its siblings
|
||||||
* Apply the following modification to the overlapping rigid body container :
|
* Apply the following modification to the overlapping rigid body container :
|
||||||
* @param container Container to impose its position
|
* @param container Container to impose its position
|
||||||
*/
|
*/
|
||||||
export function ImposePosition(container: IContainerModel): IContainerModel {
|
export function ApplyAnchor(container: IContainerModel): IContainerModel {
|
||||||
if (container.parent === undefined ||
|
if (container.parent === undefined ||
|
||||||
container.parent === null) {
|
container.parent === null) {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rigidBodies = container.parent.children.filter(
|
const rigidBodies = container.parent.children.filter(
|
||||||
child => child.properties.isRigidBody && !child.properties.isAnchor
|
child => !child.properties.isAnchor
|
||||||
);
|
);
|
||||||
|
|
||||||
const overlappingContainers = getOverlappingContainers(container, rigidBodies);
|
const overlappingContainers = GetOverlappingContainers(container, rigidBodies);
|
||||||
for (const overlappingContainer of overlappingContainers) {
|
for (const overlappingContainer of overlappingContainers) {
|
||||||
constraintBodyInsideUnallocatedWidth(overlappingContainer);
|
ConstraintBodyInsideUnallocatedWidth(overlappingContainer);
|
||||||
}
|
}
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export function ImposePosition(container: IContainerModel): IContainerModel {
|
||||||
* @param containers A list of containers
|
* @param containers A list of containers
|
||||||
* @returns A list of overlapping containers
|
* @returns A list of overlapping containers
|
||||||
*/
|
*/
|
||||||
function getOverlappingContainers(
|
function GetOverlappingContainers(
|
||||||
container: IContainerModel,
|
container: IContainerModel,
|
||||||
containers: IContainerModel[]
|
containers: IContainerModel[]
|
||||||
): IContainerModel[] {
|
): IContainerModel[] {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { ImposePosition } from './AnchorBehaviors';
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
import { RecalculatePhysics } from './RigidBodyBehaviors';
|
import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default';
|
||||||
|
import { ApplyAnchor } from './AnchorBehaviors';
|
||||||
|
import { Flex } from './FlexBehaviors';
|
||||||
|
import { ApplyRigidBody } from './RigidBodyBehaviors';
|
||||||
|
import { ApplySymbol } from './SymbolBehaviors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculate the position of the container and its neighbors
|
* Recalculate the position of the container and its neighbors
|
||||||
|
@ -8,13 +12,25 @@ import { RecalculatePhysics } from './RigidBodyBehaviors';
|
||||||
* @param container Container to recalculate its positions
|
* @param container Container to recalculate its positions
|
||||||
* @returns Updated container
|
* @returns Updated container
|
||||||
*/
|
*/
|
||||||
export function ApplyBehaviors(container: IContainerModel): IContainerModel {
|
export function ApplyBehaviors(container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel {
|
||||||
if (container.properties.isAnchor) {
|
if (container.properties.isAnchor) {
|
||||||
ImposePosition(container);
|
ApplyAnchor(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.properties.isRigidBody) {
|
Flex(container);
|
||||||
RecalculatePhysics(container);
|
|
||||||
|
ApplyRigidBody(container);
|
||||||
|
|
||||||
|
const symbol = symbols.get(container.properties.linkedSymbolId);
|
||||||
|
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
|
||||||
|
ApplySymbol(container, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (APPLY_BEHAVIORS_ON_CHILDREN) {
|
||||||
|
// Apply DFS by recursion
|
||||||
|
for (const child of container.children) {
|
||||||
|
ApplyBehaviors(child, symbols);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
|
|
122
src/Components/Editor/Behaviors/FlexBehaviors.ts
Normal file
122
src/Components/Editor/Behaviors/FlexBehaviors.ts
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { Simplex } from '../../../utils/simplex';
|
||||||
|
import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg';
|
||||||
|
|
||||||
|
interface IFlexibleGroup {
|
||||||
|
group: IContainerModel[]
|
||||||
|
offset: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 flexibleGroups = GetFlexibleGroups(container.parent);
|
||||||
|
|
||||||
|
for (const flexibleGroup of flexibleGroups) {
|
||||||
|
FlexGroup(flexibleGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlexGroup(flexibleGroup: IFlexibleGroup): void {
|
||||||
|
const children = flexibleGroup.group;
|
||||||
|
const flexibleContainers = children
|
||||||
|
.filter(sibling => sibling.properties.isFlex);
|
||||||
|
|
||||||
|
const minWidths = flexibleContainers
|
||||||
|
.map(sibling => sibling.properties.minWidth);
|
||||||
|
|
||||||
|
const fixedWidth = children
|
||||||
|
.filter(sibling => !sibling.properties.isFlex)
|
||||||
|
.map(sibling => sibling.properties.width)
|
||||||
|
.reduce((partialSum, a) => partialSum + a, 0);
|
||||||
|
|
||||||
|
const requiredMaxWidth = flexibleGroup.size - 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.'
|
||||||
|
// });
|
||||||
|
console.error('[FlexBehavior] 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 = flexibleGroup.offset;
|
||||||
|
for (const sibling of 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 = flexibleGroup.offset;
|
||||||
|
for (const sibling of children) {
|
||||||
|
sibling.properties.x = ApplyXMargin(right, sibling.properties.margin.left);
|
||||||
|
right += sibling.properties.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] {
|
||||||
|
const flexibleGroups: IFlexibleGroup[] = [];
|
||||||
|
let group: IContainerModel[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
let size = 0;
|
||||||
|
for (const child of parent.children) {
|
||||||
|
if (child.properties.isAnchor) {
|
||||||
|
size = child.properties.x - offset;
|
||||||
|
const flexibleGroup: IFlexibleGroup = {
|
||||||
|
group,
|
||||||
|
offset,
|
||||||
|
size
|
||||||
|
};
|
||||||
|
|
||||||
|
flexibleGroups.push(flexibleGroup);
|
||||||
|
offset = child.properties.x + child.properties.width;
|
||||||
|
group = [];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.push(child);
|
||||||
|
}
|
||||||
|
size = parent.properties.width - offset;
|
||||||
|
const flexibleGroup: IFlexibleGroup = {
|
||||||
|
group,
|
||||||
|
offset,
|
||||||
|
size
|
||||||
|
};
|
||||||
|
|
||||||
|
flexibleGroups.push(flexibleGroup);
|
||||||
|
return flexibleGroups;
|
||||||
|
}
|
73
src/Components/Editor/Behaviors/PushBehaviors.ts
Normal file
73
src/Components/Editor/Behaviors/PushBehaviors.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { ReversePairwise } from '../../../utils/itertools';
|
||||||
|
import { Flex } from './FlexBehaviors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
|
@ -19,11 +19,11 @@ import { ISizePointer } from '../../../Interfaces/ISizePointer';
|
||||||
* @param container Container to apply its rigid body properties
|
* @param container Container to apply its rigid body properties
|
||||||
* @returns A rigid body container
|
* @returns A rigid body container
|
||||||
*/
|
*/
|
||||||
export function RecalculatePhysics(
|
export function ApplyRigidBody(
|
||||||
container: IContainerModel
|
container: IContainerModel
|
||||||
): IContainerModel {
|
): IContainerModel {
|
||||||
container = constraintBodyInsideParent(container);
|
container = ConstraintBodyInsideParent(container);
|
||||||
container = constraintBodyInsideUnallocatedWidth(container);
|
container = ConstraintBodyInsideUnallocatedWidth(container);
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ export function RecalculatePhysics(
|
||||||
* @param container
|
* @param container
|
||||||
* @returns Updated container
|
* @returns Updated container
|
||||||
*/
|
*/
|
||||||
function constraintBodyInsideParent(
|
function ConstraintBodyInsideParent(
|
||||||
container: IContainerModel
|
container: IContainerModel
|
||||||
): IContainerModel {
|
): IContainerModel {
|
||||||
if (container.parent === null || container.parent === undefined) {
|
if (container.parent === null || container.parent === undefined) {
|
||||||
|
@ -46,7 +46,7 @@ function constraintBodyInsideParent(
|
||||||
const parentWidth = parentProperties.width;
|
const parentWidth = parentProperties.width;
|
||||||
const parentHeight = parentProperties.height;
|
const parentHeight = parentProperties.height;
|
||||||
|
|
||||||
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
|
return ConstraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,7 +59,7 @@ function constraintBodyInsideParent(
|
||||||
* @param height height of the rectangle
|
* @param height height of the rectangle
|
||||||
* @returns Updated container
|
* @returns Updated container
|
||||||
*/
|
*/
|
||||||
function constraintBodyInsideSpace(
|
function ConstraintBodyInsideSpace(
|
||||||
container: IContainerModel,
|
container: IContainerModel,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -113,15 +113,15 @@ function constraintBodyInsideSpace(
|
||||||
* @param container
|
* @param container
|
||||||
* @returns Updated container
|
* @returns Updated container
|
||||||
*/
|
*/
|
||||||
export function constraintBodyInsideUnallocatedWidth(
|
export function ConstraintBodyInsideUnallocatedWidth(
|
||||||
container: IContainerModel
|
container: IContainerModel
|
||||||
): IContainerModel {
|
): IContainerModel {
|
||||||
if (container.parent === null) {
|
if (container.parent === null || container.parent === undefined) {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = container.properties.x;
|
const containerX = container.properties.x;
|
||||||
const containerWidth = container.properties.width;
|
const containerWidth = container.properties.width;
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ export function constraintBodyInsideUnallocatedWidth(
|
||||||
// Check if the container actually fit inside
|
// Check if the container actually fit inside
|
||||||
// It will usually fit if it was alrady fitting
|
// It will usually fit if it was alrady fitting
|
||||||
const availableWidthFound = availableWidths.find((width) =>
|
const availableWidthFound = availableWidths.find((width) =>
|
||||||
isFitting(container.properties.width, width)
|
IsFitting(container.properties.width, width)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (availableWidthFound === undefined) {
|
if (availableWidthFound === undefined) {
|
||||||
|
@ -170,20 +170,19 @@ export function constraintBodyInsideUnallocatedWidth(
|
||||||
// We want the container to fit automatically inside the available space
|
// We want the container to fit automatically inside the available space
|
||||||
// even if it means to resize the container
|
// even if it means to resize the container
|
||||||
const availableWidth: ISizePointer | undefined = availableWidths.find((width) => {
|
const availableWidth: ISizePointer | undefined = availableWidths.find((width) => {
|
||||||
return isFitting(container.properties.minWidth, width);
|
return IsFitting(container.properties.minWidth, width);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (availableWidth === undefined) {
|
if (availableWidth === undefined) {
|
||||||
console.warn(`Container ${container.properties.id} cannot fit in any space due to its minimum width being to large. Consequently, its rigid body property is disabled.`);
|
console.warn(`Container ${container.properties.id} cannot fit in any space due to its minimum width being to large. Consequently, its rigid body property is disabled.`);
|
||||||
Swal.fire({
|
// Swal.fire({
|
||||||
position: 'top-end',
|
// position: 'top-end',
|
||||||
title: `Container ${container.properties.id} cannot fit!`,
|
// title: `Container ${container.properties.id} cannot fit!`,
|
||||||
text: 'Its rigid body property is now disabled. Change its the minimum width or free the parent container.',
|
// text: 'Its rigid body property is now disabled. Change its the minimum width or free the parent container.',
|
||||||
timerProgressBar: true,
|
// timerProgressBar: true,
|
||||||
showConfirmButton: false,
|
// showConfirmButton: false,
|
||||||
timer: 5000
|
// timer: 5000
|
||||||
});
|
// });
|
||||||
container.properties.isRigidBody = false;
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +191,7 @@ export function constraintBodyInsideUnallocatedWidth(
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
return constraintBodyInsideSpace(
|
return ConstraintBodyInsideSpace(
|
||||||
container,
|
container,
|
||||||
availableWidthFound.x,
|
availableWidthFound.x,
|
||||||
0,
|
0,
|
||||||
|
@ -207,10 +206,10 @@ export function constraintBodyInsideUnallocatedWidth(
|
||||||
* @param sizePointer Size space to check
|
* @param sizePointer Size space to check
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const isFitting = (
|
function IsFitting(containerWidth: number,
|
||||||
containerWidth: number,
|
sizePointer: ISizePointer): boolean {
|
||||||
sizePointer: ISizePointer
|
return containerWidth <= sizePointer.width;
|
||||||
): boolean => containerWidth <= sizePointer.width;
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the unallocated widths inside a container
|
* Get the unallocated widths inside a container
|
||||||
|
@ -221,7 +220,7 @@ const isFitting = (
|
||||||
* @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded)
|
* @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded)
|
||||||
* @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
|
* @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
|
||||||
*/
|
*/
|
||||||
function getAvailableWidths(
|
function GetAvailableWidths(
|
||||||
container: IContainerModel,
|
container: IContainerModel,
|
||||||
exception: IContainerModel
|
exception: IContainerModel
|
||||||
): ISizePointer[] {
|
): ISizePointer[] {
|
||||||
|
@ -231,13 +230,13 @@ function getAvailableWidths(
|
||||||
const width = 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
|
for (const child of container.children) {
|
||||||
const solidBodies = container.children.filter(
|
if (unallocatedSpaces.length < 1) {
|
||||||
(child) => child.properties.isRigidBody || child.properties.isAnchor
|
return unallocatedSpaces;
|
||||||
);
|
}
|
||||||
|
|
||||||
for (const child of solidBodies) {
|
|
||||||
// Ignore the exception
|
// Ignore the exception
|
||||||
|
// And we will also only uses containers that also are rigid or are anchors
|
||||||
if (child === exception) {
|
if (child === exception) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -252,7 +251,7 @@ function getAvailableWidths(
|
||||||
// In order to find unallocated space,
|
// In order to find unallocated space,
|
||||||
// We need to calculate the overlap between the two containers
|
// We need to calculate the overlap between the two containers
|
||||||
// We only works with widths meaning in 1D (with lines)
|
// We only works with widths meaning in 1D (with lines)
|
||||||
const newUnallocatedWidths = getAvailableWidthsTwoLines(
|
const newUnallocatedWidths = GetAvailableWidthsTwoLines(
|
||||||
unallocatedSpace,
|
unallocatedSpace,
|
||||||
childX,
|
childX,
|
||||||
childWidth
|
childWidth
|
||||||
|
@ -275,7 +274,7 @@ function getAvailableWidths(
|
||||||
* @param rectWidth width of the second line
|
* @param rectWidth width of the second line
|
||||||
* @returns Available widths
|
* @returns Available widths
|
||||||
*/
|
*/
|
||||||
function getAvailableWidthsTwoLines(
|
function GetAvailableWidthsTwoLines(
|
||||||
unallocatedSpace: ISizePointer,
|
unallocatedSpace: ISizePointer,
|
||||||
rectX: number,
|
rectX: number,
|
||||||
rectWidth: number
|
rectWidth: number
|
||||||
|
@ -296,18 +295,18 @@ function getAvailableWidthsTwoLines(
|
||||||
|
|
||||||
const isOverlappingOnTheLeft = unallocatedSpace.x >= rectX;
|
const isOverlappingOnTheLeft = unallocatedSpace.x >= rectX;
|
||||||
if (isOverlappingOnTheLeft) {
|
if (isOverlappingOnTheLeft) {
|
||||||
return getAvailableWidthsLeft(unallocatedSpaceRight, rectRight);
|
return GetAvailableWidthsLeft(unallocatedSpaceRight, rectRight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOverlappingOnTheRight = rectRight >= unallocatedSpaceRight;
|
const isOverlappingOnTheRight = rectRight >= unallocatedSpaceRight;
|
||||||
if (isOverlappingOnTheRight) {
|
if (isOverlappingOnTheRight) {
|
||||||
return getAvailableWidthsRight(unallocatedSpace.x, rectX);
|
return GetAvailableWidthsRight(unallocatedSpace.x, rectX);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getAvailableWidthsMiddle(unallocatedSpace.x, unallocatedSpaceRight, rectX, rectRight);
|
return GetAvailableWidthsMiddle(unallocatedSpace.x, unallocatedSpaceRight, rectX, rectRight);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number): ISizePointer[] {
|
function GetAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number): ISizePointer[] {
|
||||||
if (unallocatedSpaceRight - rectRight <= 0) {
|
if (unallocatedSpaceRight - rectRight <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -320,7 +319,7 @@ function getAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISizePointer[] {
|
function GetAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISizePointer[] {
|
||||||
if (rectX - unallocatedSpaceX <= 0) {
|
if (rectX - unallocatedSpaceX <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -333,7 +332,7 @@ function getAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISiz
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableWidthsMiddle(
|
function GetAvailableWidthsMiddle(
|
||||||
unallocatedSpaceX: number,
|
unallocatedSpaceX: number,
|
||||||
unallocatedSpaceRight: number,
|
unallocatedSpaceRight: number,
|
||||||
rectX: number,
|
rectX: number,
|
||||||
|
|
12
src/Components/Editor/Behaviors/SymbolBehaviors.ts
Normal file
12
src/Components/Editor/Behaviors/SymbolBehaviors.ts
Normal file
|
@ -0,0 +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;
|
||||||
|
}
|
|
@ -1,335 +0,0 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
|
||||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
|
||||||
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
|
|
||||||
import { findContainerById } from '../../utils/itertools';
|
|
||||||
import { getCurrentHistory } from './Editor';
|
|
||||||
import { AddMethod } from '../../Enums/AddMethod';
|
|
||||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
|
||||||
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../utils/default';
|
|
||||||
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a container
|
|
||||||
* @param container Selected container
|
|
||||||
*/
|
|
||||||
export function SelectContainer(
|
|
||||||
container: ContainerModel,
|
|
||||||
fullHistory: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
||||||
): void {
|
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
|
||||||
const current = history[history.length - 1];
|
|
||||||
|
|
||||||
const mainContainerClone = structuredClone(current.MainContainer);
|
|
||||||
const selectedContainer = findContainerById(mainContainerClone, container.properties.id);
|
|
||||||
|
|
||||||
if (selectedContainer === undefined) {
|
|
||||||
throw new Error('[SelectContainer] Cannot find container among children of main container!');
|
|
||||||
}
|
|
||||||
|
|
||||||
history.push({
|
|
||||||
LastAction: `Select ${selectedContainer.properties.id}`,
|
|
||||||
MainContainer: mainContainerClone,
|
|
||||||
SelectedContainer: selectedContainer,
|
|
||||||
SelectedContainerId: selectedContainer.properties.id,
|
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
|
||||||
});
|
|
||||||
setHistory(history);
|
|
||||||
setHistoryCurrentStep(history.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a container
|
|
||||||
* @param containerId containerId of the container to delete
|
|
||||||
* @param fullHistory History of the editor
|
|
||||||
* @param historyCurrentStep Current step
|
|
||||||
* @param setHistory State setter for History
|
|
||||||
* @param setHistoryCurrentStep State setter for current step
|
|
||||||
*/
|
|
||||||
export function DeleteContainer(
|
|
||||||
containerId: string,
|
|
||||||
fullHistory: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
||||||
): void {
|
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
|
||||||
const current = history[history.length - 1];
|
|
||||||
|
|
||||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
|
||||||
const container = findContainerById(mainContainerClone, containerId);
|
|
||||||
|
|
||||||
if (container === undefined) {
|
|
||||||
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (container === mainContainerClone ||
|
|
||||||
container.parent === undefined ||
|
|
||||||
container.parent === null) {
|
|
||||||
// TODO: Implement alert
|
|
||||||
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (container === null || container === undefined) {
|
|
||||||
throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = container.parent.children.indexOf(container);
|
|
||||||
if (index > -1) {
|
|
||||||
container.parent.children.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the previous container
|
|
||||||
// or select the one above
|
|
||||||
const SelectedContainer = findContainerById(mainContainerClone, current.SelectedContainerId) ??
|
|
||||||
container.parent.children.at(index - 1) ??
|
|
||||||
container.parent;
|
|
||||||
const SelectedContainerId = SelectedContainer.properties.id;
|
|
||||||
|
|
||||||
history.push({
|
|
||||||
LastAction: `Delete ${containerId}`,
|
|
||||||
MainContainer: mainContainerClone,
|
|
||||||
SelectedContainer,
|
|
||||||
SelectedContainerId,
|
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
|
||||||
});
|
|
||||||
setHistory(history);
|
|
||||||
setHistoryCurrentStep(history.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new container to a selected container
|
|
||||||
* @param type The type of container
|
|
||||||
* @param configuration Configuration of the App
|
|
||||||
* @param fullHistory History of the editor
|
|
||||||
* @param historyCurrentStep Current step
|
|
||||||
* @param setHistory State setter for History
|
|
||||||
* @param setHistoryCurrentStep State setter for current step
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
export function AddContainerToSelectedContainer(
|
|
||||||
type: string,
|
|
||||||
configuration: IConfiguration,
|
|
||||||
fullHistory: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
||||||
): void {
|
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
|
||||||
const current = history[history.length - 1];
|
|
||||||
|
|
||||||
if (current.SelectedContainer === null ||
|
|
||||||
current.SelectedContainer === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = current.SelectedContainer;
|
|
||||||
AddContainer(
|
|
||||||
parent.children.length,
|
|
||||||
type,
|
|
||||||
parent.properties.id,
|
|
||||||
configuration,
|
|
||||||
fullHistory,
|
|
||||||
historyCurrentStep,
|
|
||||||
setHistory,
|
|
||||||
setHistoryCurrentStep
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and add a new container at `index` in children of parent of `parentId`
|
|
||||||
* @param index Index where to insert to the new container
|
|
||||||
* @param type Type of container
|
|
||||||
* @param parentId Parent in which to insert the new container
|
|
||||||
* @param configuration Configuration of the app
|
|
||||||
* @param fullHistory History of the editor
|
|
||||||
* @param historyCurrentStep Current step
|
|
||||||
* @param setHistory State setter of History
|
|
||||||
* @param setHistoryCurrentStep State setter of the current step
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
export function AddContainer(
|
|
||||||
index: number,
|
|
||||||
type: string,
|
|
||||||
parentId: string,
|
|
||||||
configuration: IConfiguration,
|
|
||||||
fullHistory: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
||||||
): void {
|
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
|
||||||
const current = history[history.length - 1];
|
|
||||||
|
|
||||||
if (current.MainContainer === null ||
|
|
||||||
current.MainContainer === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the preset properties from the API
|
|
||||||
const containerConfig = configuration.AvailableContainers
|
|
||||||
.find(option => option.Type === type);
|
|
||||||
|
|
||||||
if (containerConfig === undefined) {
|
|
||||||
throw new Error(`[AddContainer] Object type not found. Found: ${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the counter of the object type in order to assign an unique id
|
|
||||||
const newCounters = Object.assign({}, current.TypeCounters);
|
|
||||||
UpdateCounters(newCounters, type);
|
|
||||||
const count = newCounters[type];
|
|
||||||
|
|
||||||
// Create maincontainer model
|
|
||||||
const clone: IContainerModel = structuredClone(current.MainContainer);
|
|
||||||
|
|
||||||
// Find the parent
|
|
||||||
const parentClone: IContainerModel | undefined = findContainerById(
|
|
||||||
clone, parentId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (parentClone === null || parentClone === undefined) {
|
|
||||||
throw new Error('[AddContainer] Container model was not found among children of the main container!');
|
|
||||||
}
|
|
||||||
|
|
||||||
let x = containerConfig.DefaultX ?? 0;
|
|
||||||
const y = containerConfig.DefaultY ?? 0;
|
|
||||||
|
|
||||||
x = ApplyAddMethod(index, containerConfig, parentClone, x);
|
|
||||||
|
|
||||||
const defaultProperties = GetDefaultContainerProps(
|
|
||||||
type,
|
|
||||||
count,
|
|
||||||
parentClone,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
containerConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the container
|
|
||||||
const newContainer = new ContainerModel(
|
|
||||||
parentClone,
|
|
||||||
defaultProperties,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
type
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
ApplyBehaviors(newContainer);
|
|
||||||
|
|
||||||
// And push it the the parent children
|
|
||||||
if (index === parentClone.children.length) {
|
|
||||||
parentClone.children.push(newContainer);
|
|
||||||
} else {
|
|
||||||
parentClone.children.splice(index, 0, newContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters);
|
|
||||||
|
|
||||||
// Update the state
|
|
||||||
history.push({
|
|
||||||
LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
|
|
||||||
MainContainer: clone,
|
|
||||||
SelectedContainer: parentClone,
|
|
||||||
SelectedContainerId: parentClone.properties.id,
|
|
||||||
TypeCounters: newCounters
|
|
||||||
});
|
|
||||||
setHistory(history);
|
|
||||||
setHistoryCurrentStep(history.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateCounters(counters: Record<string, number>, type: string): void {
|
|
||||||
if (counters[type] === null ||
|
|
||||||
counters[type] === undefined) {
|
|
||||||
counters[type] = 0;
|
|
||||||
} else {
|
|
||||||
counters[type]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function InitializeDefaultChild(
|
|
||||||
configuration: IConfiguration,
|
|
||||||
containerConfig: IAvailableContainer,
|
|
||||||
newContainer: ContainerModel,
|
|
||||||
newCounters: Record<string, number>
|
|
||||||
): void {
|
|
||||||
if (containerConfig.DefaultChildType === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentConfig = configuration.AvailableContainers
|
|
||||||
.find(option => option.Type === containerConfig.DefaultChildType);
|
|
||||||
let parent = newContainer;
|
|
||||||
let depth = 0;
|
|
||||||
const seen = new Set<string>([containerConfig.Type]);
|
|
||||||
|
|
||||||
while (currentConfig !== undefined &&
|
|
||||||
depth <= DEFAULTCHILDTYPE_MAX_DEPTH
|
|
||||||
) {
|
|
||||||
if (!DEFAULTCHILDTYPE_ALLOW_CYCLIC && seen.has(currentConfig.Type)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
seen.add(currentConfig.Type);
|
|
||||||
const x = currentConfig.DefaultX ?? 0;
|
|
||||||
const y = currentConfig.DefaultY ?? 0;
|
|
||||||
|
|
||||||
UpdateCounters(newCounters, currentConfig.Type);
|
|
||||||
const count = newCounters[currentConfig.Type];
|
|
||||||
const defaultChildProperties = GetDefaultContainerProps(
|
|
||||||
currentConfig.Type,
|
|
||||||
count,
|
|
||||||
parent,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
currentConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create the container
|
|
||||||
const newChildContainer = new ContainerModel(
|
|
||||||
parent,
|
|
||||||
defaultChildProperties,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
type: currentConfig.Type
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// And push it the the parent children
|
|
||||||
parent.children.push(newChildContainer);
|
|
||||||
|
|
||||||
// iterate
|
|
||||||
depth++;
|
|
||||||
parent = newChildContainer;
|
|
||||||
currentConfig = configuration.AvailableContainers
|
|
||||||
.find(option => option.Type === (currentConfig as IAvailableContainer).DefaultChildType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new offset by applying an Add method (append, insert etc.)
|
|
||||||
* See AddMethod
|
|
||||||
* @param index Index of the container
|
|
||||||
* @param containerConfig Configuration of a container
|
|
||||||
* @param parent Parent container
|
|
||||||
* @param x Additionnal offset
|
|
||||||
* @returns New offset
|
|
||||||
*/
|
|
||||||
function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, parent: IContainerModel, x: number): number {
|
|
||||||
if (index > 0 && (
|
|
||||||
containerConfig.AddMethod === undefined ||
|
|
||||||
containerConfig.AddMethod === AddMethod.Append)) {
|
|
||||||
// Append method (default)
|
|
||||||
const lastChild: IContainerModel | undefined = parent.children.at(index - 1);
|
|
||||||
|
|
||||||
if (lastChild !== undefined) {
|
|
||||||
x += (lastChild.properties.x + lastChild.properties.width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
|
@ -4,21 +4,4 @@ svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
text {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 800;
|
|
||||||
fill: none;
|
|
||||||
fill-opacity: 0;
|
|
||||||
stroke: #000000;
|
|
||||||
stroke-width: 1px;
|
|
||||||
stroke-linecap: butt;
|
|
||||||
stroke-linejoin: miter;
|
|
||||||
stroke-opacity: 1;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
transform-box: fill-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadein {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
|
@ -1,63 +1,99 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react';
|
||||||
import './Editor.scss';
|
import './Editor.scss';
|
||||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||||
import { SVG } from '../SVG/SVG';
|
import { SVG } from '../SVG/SVG';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import { UI } from '../UI/UI';
|
import { UI } from '../UI/UI';
|
||||||
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations';
|
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange } from './Actions/ContainerOperations';
|
||||||
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save';
|
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
|
||||||
import { onKeyDown } from './Shortcuts';
|
import { OnKey } from './Actions/Shortcuts';
|
||||||
import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations';
|
|
||||||
import EditorEvents from '../../Events/EditorEvents';
|
import EditorEvents from '../../Events/EditorEvents';
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||||
import { MAX_HISTORY } from '../../utils/default';
|
import { MAX_HISTORY } from '../../utils/default';
|
||||||
|
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations';
|
||||||
|
import { FindContainerById } from '../../utils/itertools';
|
||||||
|
|
||||||
interface IEditorProps {
|
interface IEditorProps {
|
||||||
|
root: Element | Document
|
||||||
configuration: IConfiguration
|
configuration: IConfiguration
|
||||||
history: IHistoryState[]
|
history: IHistoryState[]
|
||||||
historyCurrentStep: number
|
historyCurrentStep: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] =>
|
export function UpdateCounters(counters: Record<string, number>, type: string): void {
|
||||||
history.slice(
|
if (counters[type] === null ||
|
||||||
Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow)
|
counters[type] === undefined) {
|
||||||
|
counters[type] = 0;
|
||||||
|
} else {
|
||||||
|
counters[type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetCurrentHistory(history: IHistoryState[], historyCurrentStep: number): IHistoryState[] {
|
||||||
|
return history.slice(
|
||||||
|
Math.max(0, history.length - MAX_HISTORY),
|
||||||
historyCurrentStep + 1
|
historyCurrentStep + 1
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const getCurrentHistoryState = (history: IHistoryState[], historyCurrentStep: number): IHistoryState => history[historyCurrentStep];
|
export function GetCurrentHistoryState(history: IHistoryState[], historyCurrentStep: number): IHistoryState {
|
||||||
|
return history[historyCurrentStep];
|
||||||
|
}
|
||||||
|
|
||||||
const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
function UseShortcuts(
|
||||||
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
|
history: IHistoryState[],
|
||||||
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
|
historyCurrentStep: number,
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyUp = (event: KeyboardEvent): void => onKeyDown(
|
function OnKeyUp(event: KeyboardEvent): void {
|
||||||
|
return OnKey(
|
||||||
event,
|
event,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('keyup', onKeyUp);
|
window.addEventListener('keyup', OnKeyUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keyup', OnKeyUp);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function UseWindowEvents(
|
||||||
|
root: Element | Document,
|
||||||
|
history: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
configuration: IConfiguration,
|
||||||
|
editorRef: React.RefObject<HTMLDivElement>,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
const events = EditorEvents;
|
const events = EditorEvents;
|
||||||
const editorState: IEditorState = {
|
const editorState: IEditorState = {
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
configuration: props.configuration
|
configuration
|
||||||
};
|
};
|
||||||
|
|
||||||
const funcs = new Map<string, () => void>();
|
const funcs = new Map<string, () => void>();
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const func = (): void => event.func(editorState);
|
function Func(eventInitDict?: CustomEventInit): void {
|
||||||
editorRef.current?.addEventListener(event.name, func);
|
return event.func(
|
||||||
funcs.set(event.name, func);
|
root,
|
||||||
|
editorState,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep,
|
||||||
|
eventInitDict
|
||||||
|
);
|
||||||
|
}
|
||||||
|
editorRef.current?.addEventListener(event.name, Func);
|
||||||
|
funcs.set(event.name, Func);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keyup', onKeyUp);
|
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const func = funcs.get(event.name);
|
const func = funcs.get(event.name);
|
||||||
if (func === undefined) {
|
if (func === undefined) {
|
||||||
|
@ -67,45 +103,68 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Editor(props: IEditorProps): JSX.Element {
|
||||||
|
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
|
||||||
|
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
UseShortcuts(history, historyCurrentStep, setHistoryCurrentStep);
|
||||||
|
UseWindowEvents(
|
||||||
|
props.root,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
props.configuration,
|
||||||
|
editorRef,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
);
|
||||||
|
|
||||||
const configuration = props.configuration;
|
const configuration = props.configuration;
|
||||||
const current = getCurrentHistoryState(history, historyCurrentStep);
|
const current = GetCurrentHistoryState(history, historyCurrentStep);
|
||||||
|
const selected = FindContainerById(current.mainContainer, current.selectedContainerId);
|
||||||
return (
|
return (
|
||||||
<div ref={editorRef} className="Editor font-sans h-full">
|
<div ref={editorRef} className="Editor font-sans h-full">
|
||||||
<UI
|
<UI
|
||||||
|
selectedContainer={selected}
|
||||||
current={current}
|
current={current}
|
||||||
history={history}
|
history={history}
|
||||||
historyCurrentStep={historyCurrentStep}
|
historyCurrentStep={historyCurrentStep}
|
||||||
AvailableContainers={configuration.AvailableContainers}
|
availableContainers={configuration.AvailableContainers}
|
||||||
SelectContainer={(container) => SelectContainer(
|
availableSymbols={configuration.AvailableSymbols}
|
||||||
|
selectContainer={(container) => SelectContainer(
|
||||||
container,
|
container,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
DeleteContainer={(containerId: string) => DeleteContainer(
|
deleteContainer={(containerId: string) => DeleteContainer(
|
||||||
containerId,
|
containerId,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
|
onPropertyChange={(key, value, type) => OnPropertyChange(
|
||||||
key, value, isStyle,
|
key, value, type,
|
||||||
|
selected,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
OnPropertiesSubmit={(event) => OnPropertiesSubmit(
|
addContainer={(type) => AddContainerToSelectedContainer(
|
||||||
event,
|
type,
|
||||||
|
selected,
|
||||||
|
configuration,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
|
addSymbol={(type) => AddSymbol(
|
||||||
type,
|
type,
|
||||||
configuration,
|
configuration,
|
||||||
history,
|
history,
|
||||||
|
@ -113,33 +172,42 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
AddContainer={(index, type, parentId) => AddContainer(
|
onSymbolPropertyChange={(key, value) => OnSymbolPropertyChange(
|
||||||
index,
|
key, value,
|
||||||
type,
|
|
||||||
parentId,
|
|
||||||
configuration,
|
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
SaveEditorAsJSON={() => SaveEditorAsJSON(
|
selectSymbol={(symbolId) => SelectSymbol(
|
||||||
|
symbolId,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
)}
|
||||||
|
deleteSymbol={(symbolId) => DeleteSymbol(
|
||||||
|
symbolId,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
)}
|
||||||
|
saveEditorAsJSON={() => SaveEditorAsJSON(
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
configuration
|
configuration
|
||||||
)}
|
)}
|
||||||
SaveEditorAsSVG={() => SaveEditorAsSVG()}
|
saveEditorAsSVG={() => SaveEditorAsSVG()}
|
||||||
LoadState={(move) => setHistoryCurrentStep(move)}
|
loadState={(move) => setHistoryCurrentStep(move)} />
|
||||||
/>
|
|
||||||
<SVG
|
<SVG
|
||||||
width={current.MainContainer?.properties.width}
|
width={current.mainContainer?.properties.width}
|
||||||
height={current.MainContainer?.properties.height}
|
height={current.mainContainer?.properties.height}
|
||||||
selected={current.SelectedContainer}
|
selected={selected}
|
||||||
|
symbols={current.symbols}
|
||||||
>
|
>
|
||||||
{ current.MainContainer }
|
{current.mainContainer}
|
||||||
</SVG>
|
</SVG>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Editor;
|
|
||||||
|
|
|
@ -1,189 +0,0 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
|
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
|
||||||
import { findContainerById } from '../../utils/itertools';
|
|
||||||
import { getCurrentHistory } from './Editor';
|
|
||||||
import { restoreX } from '../SVG/Elements/Container';
|
|
||||||
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handled the property change event in the properties form
|
|
||||||
* @param key Property name
|
|
||||||
* @param value New value of the property
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
export function OnPropertyChange(
|
|
||||||
key: string,
|
|
||||||
value: string | number | boolean,
|
|
||||||
isStyle: boolean = false,
|
|
||||||
fullHistory: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
||||||
): void {
|
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
|
||||||
const current = history[history.length - 1];
|
|
||||||
|
|
||||||
if (current.SelectedContainer === null ||
|
|
||||||
current.SelectedContainer === undefined) {
|
|
||||||
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
|
||||||
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
|
|
||||||
|
|
||||||
if (container === null || container === undefined) {
|
|
||||||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStyle) {
|
|
||||||
(container.properties.style as any)[key] = value;
|
|
||||||
} else {
|
|
||||||
(container.properties as any)[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyBehaviors(container);
|
|
||||||
|
|
||||||
history.push({
|
|
||||||
LastAction: `Change ${key} of ${container.properties.id}`,
|
|
||||||
MainContainer: mainContainerClone,
|
|
||||||
SelectedContainer: container,
|
|
||||||
SelectedContainerId: container.properties.id,
|
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
|
||||||
});
|
|
||||||
setHistory(history);
|
|
||||||
setHistoryCurrentStep(history.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handled the property change event in the properties form
|
|
||||||
* @param key Property name
|
|
||||||
* @param properties Properties of the selected container
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
export function OnPropertiesSubmit(
|
|
||||||
event: React.SyntheticEvent<HTMLFormElement>,
|
|
||||||
fullHistory: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
||||||
): void {
|
|
||||||
event.preventDefault();
|
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
|
||||||
const current = history[history.length - 1];
|
|
||||||
|
|
||||||
if (current.SelectedContainer === null ||
|
|
||||||
current.SelectedContainer === undefined) {
|
|
||||||
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
|
||||||
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
|
|
||||||
|
|
||||||
if (container === null || container === undefined) {
|
|
||||||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign container properties
|
|
||||||
const form: HTMLFormElement = event.target as HTMLFormElement;
|
|
||||||
for (const property in container.properties) {
|
|
||||||
const input: HTMLInputElement | HTMLDivElement | null = form.querySelector(`#${property}`);
|
|
||||||
|
|
||||||
if (input === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the behaviors
|
|
||||||
ApplyBehaviors(container);
|
|
||||||
|
|
||||||
history.push({
|
|
||||||
LastAction: `Change properties of ${container.properties.id}`,
|
|
||||||
MainContainer: mainContainerClone,
|
|
||||||
SelectedContainer: container,
|
|
||||||
SelectedContainerId: container.properties.id,
|
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
|
||||||
});
|
|
||||||
setHistory(history);
|
|
||||||
setHistoryCurrentStep(history.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitHTMLInput = (
|
|
||||||
input: HTMLInputElement,
|
|
||||||
container: IContainerModel,
|
|
||||||
property: string,
|
|
||||||
form: HTMLFormElement
|
|
||||||
): void => {
|
|
||||||
if (input.type !== 'number') {
|
|
||||||
(container.properties as any)[property] = input.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (radiobutton.type === 'radio') {
|
|
||||||
(container.properties as any)[property] = Number(radiobutton.value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(container.properties as any)[property] = radiobutton.value;
|
|
||||||
};
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
|
||||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
|
||||||
import { getCircularReplacer } from '../../utils/saveload';
|
|
||||||
import { ID } from '../SVG/SVG';
|
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
|
||||||
|
|
||||||
export function SaveEditorAsJSON(
|
|
||||||
history: IHistoryState[],
|
|
||||||
historyCurrentStep: number,
|
|
||||||
configuration: IConfiguration
|
|
||||||
): void {
|
|
||||||
const exportName = 'state.json';
|
|
||||||
const spaces = import.meta.env.DEV ? 4 : 0;
|
|
||||||
const editorState: IEditorState = {
|
|
||||||
history,
|
|
||||||
historyCurrentStep,
|
|
||||||
configuration
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
||||||
if (window.Worker) {
|
|
||||||
// use webworker for the stringify to avoid freezing
|
|
||||||
const myWorker = new Worker('workers/worker.js');
|
|
||||||
myWorker.postMessage({ editorState, spaces });
|
|
||||||
myWorker.onmessage = (event) => {
|
|
||||||
const data = event.data;
|
|
||||||
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
|
|
||||||
createDownloadNode(exportName, dataStr);
|
|
||||||
myWorker.terminate();
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.stringify(editorState, getCircularReplacer(), spaces);
|
|
||||||
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`;
|
|
||||||
createDownloadNode(exportName, dataStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SaveEditorAsSVG(): void {
|
|
||||||
const svgWrapper = document.getElementById(ID) as HTMLElement;
|
|
||||||
const svg = svgWrapper.querySelector('svg') as SVGSVGElement;
|
|
||||||
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
|
|
||||||
const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
|
|
||||||
const svgUrl = URL.createObjectURL(svgBlob);
|
|
||||||
createDownloadNode('state.svg', svgUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDownloadNode(filename: string, datastring: string) {
|
|
||||||
const downloadAnchorNode = document.createElement('a');
|
|
||||||
downloadAnchorNode.href = datastring;
|
|
||||||
downloadAnchorNode.download = filename;
|
|
||||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
|
||||||
downloadAnchorNode.click();
|
|
||||||
downloadAnchorNode.remove();
|
|
||||||
}
|
|
|
@ -4,36 +4,40 @@ 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';
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
import { FindContainerById } from '../../utils/itertools';
|
||||||
|
|
||||||
describe.concurrent('Elements sidebar', () => {
|
describe.concurrent('Elements sidebar', () => {
|
||||||
it('With a MainContainer', () => {
|
it('With a MainContainer', () => {
|
||||||
render(<ElementsSidebar
|
render(<ElementsSidebar
|
||||||
MainContainer={{
|
symbols={new Map()}
|
||||||
|
mainContainer={{
|
||||||
children: [],
|
children: [],
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: null,
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
margin: {},
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
XPositionReference: XPositionReference.Left,
|
type: 'type',
|
||||||
isRigidBody: false,
|
maxWidth: Infinity,
|
||||||
|
isFlex: false,
|
||||||
|
xPositionReference: XPositionReference.Left,
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
}}
|
}}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
SelectedContainer={null}
|
selectedContainer={undefined}
|
||||||
OnPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
OnPropertiesSubmit={() => {}}
|
selectContainer={() => {}}
|
||||||
SelectContainer={() => {}}
|
deleteContainer={() => {}}
|
||||||
DeleteContainer={() => {}}
|
|
||||||
AddContainer={() => {}}
|
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -42,35 +46,38 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('With a selected MainContainer', () => {
|
it('With a selected MainContainer', () => {
|
||||||
const MainContainer = {
|
const mainContainer: IContainerModel = {
|
||||||
children: [],
|
children: [],
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
|
margin: {},
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
isRigidBody: false,
|
isFlex: false,
|
||||||
|
maxWidth: Infinity,
|
||||||
|
type: 'type',
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
XPositionReference: XPositionReference.Left
|
xPositionReference: XPositionReference.Left
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<ElementsSidebar
|
const { container } = render(<ElementsSidebar
|
||||||
MainContainer={MainContainer}
|
symbols={new Map()}
|
||||||
|
mainContainer={mainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
SelectedContainer={MainContainer}
|
selectedContainer={mainContainer}
|
||||||
OnPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
OnPropertiesSubmit={() => {}}
|
selectContainer={() => {}}
|
||||||
SelectContainer={() => {}}
|
deleteContainer={() => {}}
|
||||||
DeleteContainer={() => {}}
|
|
||||||
AddContainer={() => {}}
|
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -87,35 +94,39 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
const propertyY = container.querySelector('#y');
|
const propertyY = container.querySelector('#y');
|
||||||
const propertyWidth = container.querySelector('#width');
|
const propertyWidth = container.querySelector('#width');
|
||||||
const propertyHeight = container.querySelector('#height');
|
const propertyHeight = container.querySelector('#height');
|
||||||
expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString());
|
expect((propertyId as HTMLInputElement).value).toBe(mainContainer.properties.id.toString());
|
||||||
expect(propertyParentId).toBeDefined();
|
expect(propertyParentId).toBeDefined();
|
||||||
expect((propertyParentId as HTMLInputElement).value).toBe('');
|
expect((propertyParentId as HTMLInputElement).value).toBe('');
|
||||||
expect(propertyX).toBeDefined();
|
expect(propertyX).toBeDefined();
|
||||||
expect((propertyX as HTMLInputElement).value).toBe(MainContainer.properties.x.toString());
|
expect((propertyX as HTMLInputElement).value).toBe(mainContainer.properties.x.toString());
|
||||||
expect(propertyY).toBeDefined();
|
expect(propertyY).toBeDefined();
|
||||||
expect((propertyY as HTMLInputElement).value).toBe(MainContainer.properties.y.toString());
|
expect((propertyY as HTMLInputElement).value).toBe(mainContainer.properties.y.toString());
|
||||||
expect(propertyWidth).toBeDefined();
|
expect(propertyWidth).toBeDefined();
|
||||||
expect((propertyWidth as HTMLInputElement).value).toBe(MainContainer.properties.width.toString());
|
expect((propertyWidth as HTMLInputElement).value).toBe(mainContainer.properties.width.toString());
|
||||||
expect(propertyHeight).toBeDefined();
|
expect(propertyHeight).toBeDefined();
|
||||||
expect((propertyHeight as HTMLInputElement).value).toBe(MainContainer.properties.height.toString());
|
expect((propertyHeight as HTMLInputElement).value).toBe(mainContainer.properties.height.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('With multiple containers', () => {
|
it('With multiple containers', () => {
|
||||||
const children: IContainerModel[] = [];
|
const children: IContainerModel[] = [];
|
||||||
const MainContainer = {
|
const mainContainer: IContainerModel = {
|
||||||
children,
|
children,
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
XPositionReference: XPositionReference.Left,
|
xPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
margin: {},
|
||||||
|
isFlex: false,
|
||||||
|
maxWidth: Infinity,
|
||||||
|
type: 'type',
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
|
@ -124,19 +135,23 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
children.push(
|
children.push(
|
||||||
{
|
{
|
||||||
children: [],
|
children: [],
|
||||||
parent: MainContainer,
|
parent: mainContainer,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'child-1',
|
id: 'child-1',
|
||||||
parentId: 'main',
|
parentId: 'main',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'child-1',
|
displayedText: 'child-1',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
isRigidBody: false,
|
margin: {},
|
||||||
|
isFlex: false,
|
||||||
|
maxWidth: Infinity,
|
||||||
|
type: 'type',
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
XPositionReference: XPositionReference.Left
|
xPositionReference: XPositionReference.Left
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
}
|
}
|
||||||
|
@ -145,18 +160,22 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
children.push(
|
children.push(
|
||||||
{
|
{
|
||||||
children: [],
|
children: [],
|
||||||
parent: MainContainer,
|
parent: mainContainer,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'child-2',
|
id: 'child-2',
|
||||||
parentId: 'main',
|
parentId: 'main',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'child-2',
|
displayedText: 'child-2',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
margin: {},
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
XPositionReference: XPositionReference.Left,
|
xPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
isFlex: false,
|
||||||
|
maxWidth: Infinity,
|
||||||
|
type: 'type',
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
|
@ -164,15 +183,14 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
render(<ElementsSidebar
|
render(<ElementsSidebar
|
||||||
MainContainer={MainContainer}
|
symbols={new Map()}
|
||||||
|
mainContainer={mainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
SelectedContainer={MainContainer}
|
selectedContainer={mainContainer}
|
||||||
OnPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
OnPropertiesSubmit={() => {}}
|
selectContainer={() => {}}
|
||||||
SelectContainer={() => {}}
|
deleteContainer={() => {}}
|
||||||
DeleteContainer={() => {}}
|
|
||||||
AddContainer={() => {}}
|
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -184,20 +202,24 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
|
|
||||||
it('With multiple containers, change selection', () => {
|
it('With multiple containers, change selection', () => {
|
||||||
const children: IContainerModel[] = [];
|
const children: IContainerModel[] = [];
|
||||||
const MainContainer: IContainerModel = {
|
const mainContainer: IContainerModel = {
|
||||||
children,
|
children,
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
width: 2000,
|
width: 2000,
|
||||||
height: 100,
|
height: 100,
|
||||||
XPositionReference: XPositionReference.Left,
|
xPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
margin: {},
|
||||||
|
isFlex: false,
|
||||||
|
maxWidth: Infinity,
|
||||||
|
type: 'type',
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
|
@ -205,39 +227,42 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
|
|
||||||
const child1Model: IContainerModel = {
|
const child1Model: IContainerModel = {
|
||||||
children: [],
|
children: [],
|
||||||
parent: MainContainer,
|
parent: mainContainer,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'child-1',
|
id: 'child-1',
|
||||||
parentId: 'main',
|
parentId: 'main',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'child-1',
|
displayedText: 'child-1',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
XPositionReference: XPositionReference.Left,
|
xPositionReference: XPositionReference.Left,
|
||||||
isRigidBody: false,
|
margin: {},
|
||||||
|
isFlex: false,
|
||||||
|
maxWidth: Infinity,
|
||||||
|
type: 'type',
|
||||||
isAnchor: false
|
isAnchor: false
|
||||||
},
|
},
|
||||||
userData: {}
|
userData: {}
|
||||||
};
|
};
|
||||||
children.push(child1Model);
|
children.push(child1Model);
|
||||||
|
|
||||||
let SelectedContainer = MainContainer;
|
let selectedContainer: IContainerModel | undefined = mainContainer;
|
||||||
const selectContainer = vi.fn((container: IContainerModel) => {
|
const selectContainer = vi.fn((containerId: string) => {
|
||||||
SelectedContainer = container;
|
selectedContainer = FindContainerById(mainContainer, containerId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container, rerender } = render(<ElementsSidebar
|
const { container, rerender } = render(<ElementsSidebar
|
||||||
MainContainer={MainContainer}
|
symbols={new Map()}
|
||||||
|
mainContainer={mainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
SelectedContainer={SelectedContainer}
|
selectedContainer={selectedContainer}
|
||||||
OnPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
OnPropertiesSubmit={() => {}}
|
selectContainer={selectContainer}
|
||||||
SelectContainer={selectContainer}
|
deleteContainer={() => {}}
|
||||||
DeleteContainer={() => {}}
|
|
||||||
AddContainer={() => {}}
|
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -247,21 +272,20 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
expect(child1);
|
expect(child1);
|
||||||
const propertyId = container.querySelector('#id');
|
const propertyId = container.querySelector('#id');
|
||||||
const propertyParentId = container.querySelector('#parentId');
|
const propertyParentId = container.querySelector('#parentId');
|
||||||
expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString());
|
expect((propertyId as HTMLInputElement).value).toBe(mainContainer.properties.id.toString());
|
||||||
expect((propertyParentId as HTMLInputElement).value).toBe('');
|
expect((propertyParentId as HTMLInputElement).value).toBe('');
|
||||||
|
|
||||||
fireEvent.click(child1);
|
fireEvent.click(child1);
|
||||||
|
|
||||||
rerender(<ElementsSidebar
|
rerender(<ElementsSidebar
|
||||||
MainContainer={MainContainer}
|
symbols={new Map()}
|
||||||
|
mainContainer={mainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
SelectedContainer={SelectedContainer}
|
selectedContainer={selectedContainer}
|
||||||
OnPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
OnPropertiesSubmit={() => {}}
|
selectContainer={selectContainer}
|
||||||
SelectContainer={selectContainer}
|
deleteContainer={() => {}}
|
||||||
DeleteContainer={() => {}}
|
|
||||||
AddContainer={() => {}}
|
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
|
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
|
||||||
|
|
|
@ -1,26 +1,31 @@
|
||||||
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 '../ContainerProperties/ContainerProperties';
|
||||||
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';
|
||||||
import { MenuItem } from '../Menu/MenuItem';
|
import { MenuItem } from '../Menu/MenuItem';
|
||||||
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
|
import { UseMouseEvents } from './MouseEventHandlers';
|
||||||
import { IPoint } from '../../Interfaces/IPoint';
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { PropertyType } from '../../Enums/PropertyType';
|
||||||
|
|
||||||
interface IElementsSidebarProps {
|
interface IElementsSidebarProps {
|
||||||
MainContainer: IContainerModel
|
mainContainer: IContainerModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
isHistoryOpen: boolean
|
isHistoryOpen: boolean
|
||||||
SelectedContainer: IContainerModel | null
|
selectedContainer: IContainerModel | undefined
|
||||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
onPropertyChange: (
|
||||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
key: string,
|
||||||
SelectContainer: (container: IContainerModel) => void
|
value: string | number | boolean,
|
||||||
DeleteContainer: (containerid: string) => void
|
type?: PropertyType
|
||||||
AddContainer: (index: number, type: string, parent: string) => void
|
) => void
|
||||||
|
selectContainer: (containerId: string) => void
|
||||||
|
deleteContainer: (containerid: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElementsSidebarProps): JSX.Element => {
|
export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
||||||
// States
|
// States
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
||||||
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
|
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
|
||||||
|
@ -32,93 +37,62 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
||||||
const elementRef = React.useRef<HTMLDivElement>(null);
|
const elementRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
React.useEffect(() => {
|
UseMouseEvents(
|
||||||
const onContextMenu = (event: MouseEvent): void => handleRightClick(
|
isContextMenuOpen,
|
||||||
event,
|
elementRef,
|
||||||
setIsContextMenuOpen,
|
setIsContextMenuOpen,
|
||||||
setOnClickContainerId,
|
setOnClickContainerId,
|
||||||
setContextMenuPosition
|
setContextMenuPosition
|
||||||
);
|
);
|
||||||
|
|
||||||
const onLeftClick = (): void => handleLeftClick(
|
|
||||||
isContextMenuOpen,
|
|
||||||
setIsContextMenuOpen,
|
|
||||||
setOnClickContainerId
|
|
||||||
);
|
|
||||||
|
|
||||||
elementRef.current?.addEventListener(
|
|
||||||
'contextmenu',
|
|
||||||
onContextMenu
|
|
||||||
);
|
|
||||||
|
|
||||||
window.addEventListener(
|
|
||||||
'click',
|
|
||||||
onLeftClick
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
elementRef.current?.removeEventListener(
|
|
||||||
'contextmenu',
|
|
||||||
onContextMenu
|
|
||||||
);
|
|
||||||
|
|
||||||
window.removeEventListener(
|
|
||||||
'click',
|
|
||||||
onLeftClick
|
|
||||||
);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
let isOpenClasses = '-right-64';
|
let isOpenClasses = '-right-64';
|
||||||
if (props.isOpen) {
|
if (props.isOpen) {
|
||||||
isOpenClasses = props.isHistoryOpen
|
isOpenClasses = props.isHistoryOpen ? 'right-64' : 'right-0';
|
||||||
? 'right-64'
|
|
||||||
: 'right-0';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = MakeIterator(props.MainContainer);
|
const it = MakeIterator(props.mainContainer);
|
||||||
const containers = [...it];
|
const containers = [...it];
|
||||||
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => {
|
function Row({
|
||||||
|
index, style
|
||||||
|
}: {
|
||||||
|
index: number
|
||||||
|
style: React.CSSProperties
|
||||||
|
}): JSX.Element {
|
||||||
const container = containers[index];
|
const container = containers[index];
|
||||||
const depth: number = getDepth(container);
|
const depth: number = GetDepth(container);
|
||||||
const key = container.properties.id.toString();
|
const key = container.properties.id.toString();
|
||||||
const text = container.properties.displayedText === key
|
const text = container.properties.displayedText === key
|
||||||
? `${'|\t'.repeat(depth)} ${key}`
|
? `${'|\t'.repeat(depth)} ${key}`
|
||||||
: `${'|\t'.repeat(depth)} ${container.properties.displayedText} (${key})`;
|
: `${'|\t'.repeat(depth)} ${container.properties.displayedText} (${key})`;
|
||||||
const selectedClass: string = props.SelectedContainer !== undefined &&
|
const selectedClass: string = props.selectedContainer !== undefined &&
|
||||||
props.SelectedContainer !== null &&
|
props.selectedContainer !== null &&
|
||||||
props.SelectedContainer.properties.id === container.properties.id
|
props.selectedContainer.properties.id === container.properties.id
|
||||||
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
|
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
|
||||||
: 'bg-slate-300/60 hover:bg-slate-300';
|
: 'bg-slate-300/60 hover:bg-slate-300';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button"
|
||||||
className={
|
className={`w-full border-blue-500 elements-sidebar-row whitespace-pre
|
||||||
`w-full border-blue-500 elements-sidebar-row whitespace-pre
|
text-left text-sm font-medium transition-all ${selectedClass}`}
|
||||||
text-left text-sm font-medium transition-all ${selectedClass}`
|
|
||||||
}
|
|
||||||
id={key}
|
id={key}
|
||||||
key={key}
|
key={key}
|
||||||
style={style}
|
style={style}
|
||||||
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
|
onClick={() => props.selectContainer(container.properties.id)}
|
||||||
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
|
|
||||||
onDragLeave={(event) => handleDragLeave(event)}
|
|
||||||
onClick={() => props.SelectContainer(container)}
|
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
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
|
||||||
<div className='bg-slate-100 font-bold sidebar-title'>
|
className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}
|
||||||
Elements
|
>
|
||||||
</div>
|
<div className="bg-slate-100 font-bold sidebar-title">Elements</div>
|
||||||
<div ref={elementRef} className='h-96 text-gray-800'>
|
<div ref={elementRef} className="h-96 text-gray-800">
|
||||||
<List
|
<List
|
||||||
className='List divide-y divide-black'
|
className="List divide-y divide-black"
|
||||||
itemCount={containers.length}
|
itemCount={containers.length}
|
||||||
itemSize={35}
|
itemSize={35}
|
||||||
height={384}
|
height={384}
|
||||||
|
@ -128,21 +102,23 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<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}
|
x={contextMenuPosition.x}
|
||||||
y={contextMenuPosition.y}
|
y={contextMenuPosition.y}
|
||||||
isOpen={isContextMenuOpen}
|
isOpen={isContextMenuOpen}
|
||||||
>
|
>
|
||||||
<MenuItem className='contextmenu-item' text='Delete' onClick={() => {
|
<MenuItem
|
||||||
|
className="contextmenu-item"
|
||||||
|
text="Delete"
|
||||||
|
onClick={() => {
|
||||||
setIsContextMenuOpen(false);
|
setIsContextMenuOpen(false);
|
||||||
props.DeleteContainer(onClickContainerId);
|
props.deleteContainer(onClickContainerId);
|
||||||
} } />
|
} } />
|
||||||
</Menu>
|
</Menu>
|
||||||
<Properties
|
<Properties
|
||||||
properties={props.SelectedContainer?.properties}
|
properties={props.selectedContainer?.properties}
|
||||||
onChange={props.OnPropertyChange}
|
symbols={props.symbols}
|
||||||
onSubmit={props.OnPropertiesSubmit}
|
onChange={props.onPropertyChange} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,8 +1,56 @@
|
||||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
import React, { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
|
||||||
import { IPoint } from '../../Interfaces/IPoint';
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
import { findContainerById } from '../../utils/itertools';
|
|
||||||
|
|
||||||
export function handleRightClick(
|
export function UseMouseEvents(
|
||||||
|
isContextMenuOpen: boolean,
|
||||||
|
elementRef: RefObject<HTMLDivElement>,
|
||||||
|
setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>,
|
||||||
|
setOnClickContainerId: Dispatch<SetStateAction<string>>,
|
||||||
|
setContextMenuPosition: Dispatch<SetStateAction<IPoint>>
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
|
function OnContextMenu(event: MouseEvent): void {
|
||||||
|
return HandleRightClick(
|
||||||
|
event,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickContainerId,
|
||||||
|
setContextMenuPosition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnLeftClick(): void {
|
||||||
|
return HandleLeftClick(
|
||||||
|
isContextMenuOpen,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickContainerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
elementRef.current?.addEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
OnContextMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'click',
|
||||||
|
OnLeftClick
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
elementRef.current?.removeEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
OnContextMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
window.removeEventListener(
|
||||||
|
'click',
|
||||||
|
OnLeftClick
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HandleRightClick(
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
|
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
@ -22,7 +70,7 @@ export function handleRightClick(
|
||||||
setContextMenuPosition(contextMenuPosition);
|
setContextMenuPosition(contextMenuPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleLeftClick(
|
export function HandleLeftClick(
|
||||||
isContextMenuOpen: boolean,
|
isContextMenuOpen: boolean,
|
||||||
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
|
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
@ -34,96 +82,3 @@ export function handleLeftClick(
|
||||||
setIsContextMenuOpen(false);
|
setIsContextMenuOpen(false);
|
||||||
setOnClickContainerId('');
|
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,14 +6,12 @@ interface IFloatingButtonProps {
|
||||||
className: string
|
className: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleState = (
|
function ToggleState(isHidden: boolean,
|
||||||
isHidden: boolean,
|
setHidden: React.Dispatch<React.SetStateAction<boolean>>): void {
|
||||||
setHidden: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
): void => {
|
|
||||||
setHidden(!isHidden);
|
setHidden(!isHidden);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingButtonProps) => {
|
export function FloatingButton(props: IFloatingButtonProps): JSX.Element {
|
||||||
const [isHidden, setHidden] = React.useState(true);
|
const [isHidden, setHidden] = React.useState(true);
|
||||||
const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100';
|
const buttonListClasses = isHidden ? 'invisible opacity-0' : 'visible opacity-100';
|
||||||
const icon = isHidden
|
const icon = isHidden
|
||||||
|
@ -25,12 +23,12 @@ export const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingB
|
||||||
<div className={`transition-all flex flex-col gap-2 items-center ${buttonListClasses}`}>
|
<div className={`transition-all flex flex-col gap-2 items-center ${buttonListClasses}`}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button"
|
||||||
className={'transition-all w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
className={'transition-all w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||||
title='Open menu'
|
title='Open menu'
|
||||||
onClick={() => toggleState(isHidden, setHidden)}
|
onClick={() => ToggleState(isHidden, setHidden)}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</button>
|
</button>
|
||||||
</div>);
|
</div>);
|
||||||
};
|
}
|
||||||
|
|
|
@ -9,32 +9,30 @@ interface IHistoryProps {
|
||||||
jumpTo: (move: number) => void
|
jumpTo: (move: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
|
export function History(props: IHistoryProps): JSX.Element {
|
||||||
const isOpenClasses = props.isOpen ? 'right-0' : '-right-64';
|
const isOpenClasses = props.isOpen ? 'right-0' : '-right-64';
|
||||||
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => {
|
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
|
||||||
const reversedIndex = (props.history.length - 1) - index;
|
const reversedIndex = (props.history.length - 1) - index;
|
||||||
const step = props.history[reversedIndex];
|
const step = props.history[reversedIndex];
|
||||||
const desc = step.LastAction;
|
const desc = step.lastAction;
|
||||||
|
|
||||||
const selectedClass = reversedIndex === props.historyCurrentStep
|
const selectedClass = reversedIndex === props.historyCurrentStep
|
||||||
? 'bg-blue-500 hover:bg-blue-600'
|
? 'bg-blue-500 hover:bg-blue-600'
|
||||||
: 'bg-slate-500 hover:bg-slate-700';
|
: 'bg-slate-500 hover:bg-slate-700';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button"
|
||||||
key={reversedIndex}
|
key={reversedIndex}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={() => props.jumpTo(reversedIndex)}
|
onClick={() => props.jumpTo(reversedIndex)}
|
||||||
title={step.LastAction}
|
title={step.lastAction}
|
||||||
className={
|
className={`w-full elements-sidebar-row whitespace-pre overflow-hidden
|
||||||
`w-full elements-sidebar-row whitespace-pre overflow-hidden
|
text-left text-sm font-medium transition-all ${selectedClass}`}
|
||||||
text-left text-sm font-medium transition-all ${selectedClass}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{desc}
|
{desc}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
|
<div className={`fixed flex flex-col bg-slate-300 text-white transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
|
||||||
|
@ -52,4 +50,4 @@ export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const className = `
|
||||||
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
|
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`;
|
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
|
||||||
|
|
||||||
export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) => {
|
export function InputGroup(props: IInputGroupProps): JSX.Element {
|
||||||
return <>
|
return <>
|
||||||
<label
|
<label
|
||||||
key={props.labelKey}
|
key={props.labelKey}
|
||||||
|
@ -44,7 +44,6 @@ export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) =>
|
||||||
defaultChecked={props.defaultChecked}
|
defaultChecked={props.defaultChecked}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
min={props.min}
|
min={props.min}
|
||||||
disabled={props.isDisabled}
|
disabled={props.isDisabled} />
|
||||||
/>
|
|
||||||
</>;
|
</>;
|
||||||
};
|
}
|
||||||
|
|
|
@ -6,14 +6,14 @@ interface IMainMenuProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WindowState {
|
enum WindowState {
|
||||||
MAIN,
|
Main,
|
||||||
LOAD,
|
Load,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MainMenu: React.FC<IMainMenuProps> = (props) => {
|
export function MainMenu(props: IMainMenuProps): JSX.Element {
|
||||||
const [windowState, setWindowState] = React.useState(WindowState.MAIN);
|
const [windowState, setWindowState] = React.useState(WindowState.Main);
|
||||||
switch (windowState) {
|
switch (windowState) {
|
||||||
case WindowState.LOAD:
|
case WindowState.Load:
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
<div className='flex flex-col drop-shadow-lg bg-blue-50 p-12 rounded-lg absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
<form className="flex items-center space-x-6">
|
<form className="flex items-center space-x-6">
|
||||||
|
@ -36,8 +36,8 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
|
||||||
"/>
|
"/>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
<button
|
<button type="button"
|
||||||
onClick={() => setWindowState(WindowState.MAIN)}
|
onClick={() => setWindowState(WindowState.Main)}
|
||||||
className='normal-btn block
|
className='normal-btn block
|
||||||
mt-8 '
|
mt-8 '
|
||||||
>
|
>
|
||||||
|
@ -49,8 +49,8 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
|
||||||
<button className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button>
|
<button type="button" className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button>
|
||||||
<button className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button>
|
<button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.Load)}>Load a configuration file</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface IMenuProps {
|
||||||
children: React.ReactNode[] | React.ReactNode
|
children: React.ReactNode[] | React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Menu: React.FC<IMenuProps> = (props) => {
|
export function Menu(props: IMenuProps): JSX.Element {
|
||||||
const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0';
|
const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -20,4 +20,4 @@ export const Menu: React.FC<IMenuProps> = (props) => {
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -6,11 +6,11 @@ interface IMenuItemProps {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuItem: React.FC<IMenuItemProps> = (props) => {
|
export function MenuItem(props: IMenuItemProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button"
|
||||||
className={props.className}
|
className={props.className}
|
||||||
onClick={() => props.onClick()}>{props.text}
|
onClick={() => props.onClick()}>{props.text}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,168 +0,0 @@
|
||||||
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='Displayed text'
|
|
||||||
inputKey='displayedText'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='string'
|
|
||||||
value={props.properties.displayedText?.toString()}
|
|
||||||
onChange={(event) => props.onChange('displayedText', event.target.value)}
|
|
||||||
/>
|
|
||||||
<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='Minimum width'
|
|
||||||
inputKey='minWidth'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='number'
|
|
||||||
min={1}
|
|
||||||
value={props.properties.minWidth.toString()}
|
|
||||||
onChange={(event) => props.onChange('minWidth', Number(event.target.value))}
|
|
||||||
/>
|
|
||||||
<InputGroup
|
|
||||||
labelText='Width'
|
|
||||||
inputKey='width'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='number'
|
|
||||||
min={props.properties.minWidth}
|
|
||||||
value={props.properties.width.toString()}
|
|
||||||
onChange={(event) => props.onChange('width', Number(event.target.value))}
|
|
||||||
/>
|
|
||||||
<InputGroup
|
|
||||||
labelText='Height'
|
|
||||||
inputKey='height'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
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;
|
|
|
@ -1,24 +0,0 @@
|
||||||
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,36 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
|
||||||
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
|
||||||
import { Form } from './Form';
|
|
||||||
|
|
||||||
interface IPropertiesProps {
|
|
||||||
properties?: IProperties
|
|
||||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
|
|
||||||
const [isDynamicInput, setIsDynamicInput] = useState<boolean>(true);
|
|
||||||
|
|
||||||
if (props.properties === undefined) {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
|
||||||
<ToggleButton
|
|
||||||
id='isDynamic'
|
|
||||||
text='Dynamic update'
|
|
||||||
title='Enable dynamic svg update'
|
|
||||||
checked={isDynamicInput}
|
|
||||||
onChange={() => setIsDynamicInput(!isDynamicInput)}
|
|
||||||
/>
|
|
||||||
<Form
|
|
||||||
properties={props.properties}
|
|
||||||
isDynamicInput={isDynamicInput}
|
|
||||||
onChange={props.onChange}
|
|
||||||
onSubmit={props.onSubmit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,159 +0,0 @@
|
||||||
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='Displayed text'
|
|
||||||
inputKey='displayedText'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='string'
|
|
||||||
defaultValue={props.properties.displayedText?.toString()}
|
|
||||||
/>
|
|
||||||
<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='Minimum width'
|
|
||||||
inputKey='minWidth'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='number'
|
|
||||||
min={0}
|
|
||||||
defaultValue={props.properties.minWidth.toString()}
|
|
||||||
/>
|
|
||||||
<InputGroup
|
|
||||||
labelText='Width'
|
|
||||||
inputKey='width'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='number'
|
|
||||||
min={props.properties.minWidth}
|
|
||||||
defaultValue={props.properties.width.toString()}
|
|
||||||
/>
|
|
||||||
<InputGroup
|
|
||||||
labelText='Height'
|
|
||||||
inputKey='height'
|
|
||||||
labelClassName=''
|
|
||||||
inputClassName=''
|
|
||||||
type='number'
|
|
||||||
min={1}
|
|
||||||
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;
|
|
|
@ -11,7 +11,7 @@ interface IRadioGroupButtonsProps {
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps> = (props) => {
|
export function RadioGroupButtons(props: IRadioGroupButtonsProps): JSX.Element {
|
||||||
let inputGroups;
|
let inputGroups;
|
||||||
if (props.value !== undefined) {
|
if (props.value !== undefined) {
|
||||||
// dynamic
|
// dynamic
|
||||||
|
@ -24,8 +24,7 @@ export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps>
|
||||||
className={`peer m-2 ${props.inputClassName}`}
|
className={`peer m-2 ${props.inputClassName}`}
|
||||||
value={inputGroup.value}
|
value={inputGroup.value}
|
||||||
checked={props.value === inputGroup.value}
|
checked={props.value === inputGroup.value}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange} />
|
||||||
/>
|
|
||||||
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
|
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
|
||||||
{inputGroup.text}
|
{inputGroup.text}
|
||||||
</label>
|
</label>
|
||||||
|
@ -42,8 +41,7 @@ export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps>
|
||||||
name={props.name}
|
name={props.name}
|
||||||
className={`peer m-2 ${props.inputClassName}`}
|
className={`peer m-2 ${props.inputClassName}`}
|
||||||
value={inputGroup.value}
|
value={inputGroup.value}
|
||||||
defaultChecked={props.defaultValue === inputGroup.value}
|
defaultChecked={props.defaultValue === inputGroup.value} />
|
||||||
/>
|
|
||||||
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
|
<label htmlFor={inputGroup.value} className='text-gray-400 peer-checked:text-blue-500'>
|
||||||
{inputGroup.text}
|
{inputGroup.text}
|
||||||
</label>
|
</label>
|
||||||
|
@ -63,4 +61,4 @@ export const RadioGroupButtons: React.FunctionComponent<IRadioGroupButtonsProps>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Interweave, Node } from 'interweave';
|
import { Interweave, Node } from 'interweave';
|
||||||
import { XPositionReference } from '../../../Enums/XPositionReference';
|
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
|
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
|
||||||
import { getDepth } from '../../../utils/itertools';
|
import { GetDepth } from '../../../utils/itertools';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
import IProperties from '../../../Interfaces/IProperties';
|
import { IContainerProperties } from '../../../Interfaces/IContainerProperties';
|
||||||
|
import { TransformX } from '../../../utils/svg';
|
||||||
|
import { Camelize } from '../../../utils/stringtools';
|
||||||
|
|
||||||
interface IContainerProps {
|
interface IContainerProps {
|
||||||
model: IContainerModel
|
model: IContainerModel
|
||||||
|
@ -15,12 +16,19 @@ interface IContainerProps {
|
||||||
* Render the container
|
* Render the container
|
||||||
* @returns Render the container
|
* @returns Render the container
|
||||||
*/
|
*/
|
||||||
export const Container: React.FC<IContainerProps> = (props: IContainerProps) => {
|
export function Container(props: IContainerProps): JSX.Element {
|
||||||
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 = 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
|
// g style
|
||||||
const defaultStyle: React.CSSProperties = {
|
const defaultStyle: React.CSSProperties = {
|
||||||
|
@ -38,29 +46,25 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
const svg = (props.model.properties.customSVG != null)
|
const svg = (props.model.properties.customSVG != null)
|
||||||
? CreateReactCustomSVG(props.model.properties.customSVG, props.model.properties)
|
? CreateReactCustomSVG(props.model.properties.customSVG, props.model.properties)
|
||||||
: (<rect
|
: (<rect
|
||||||
width={props.model.properties.width}
|
width={width}
|
||||||
height={props.model.properties.height}
|
height={height}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
</rect>);
|
</rect>);
|
||||||
// Dimension props
|
// Dimension props
|
||||||
const depth = getDepth(props.model);
|
const depth = GetDepth(props.model);
|
||||||
const dimensionMargin = DIMENSION_MARGIN * (depth + 1);
|
const dimensionMargin = DIMENSION_MARGIN * depth;
|
||||||
const id = `dim-${props.model.properties.id}`;
|
const id = `dim-${props.model.properties.id}`;
|
||||||
const xStart: number = 0;
|
const xStart: number = 0;
|
||||||
const xEnd = props.model.properties.width;
|
const xEnd = width;
|
||||||
const y = -dimensionMargin;
|
const yDim = -dimensionMargin;
|
||||||
const strokeWidth = 1;
|
const strokeWidth = 1;
|
||||||
const text = (props.model.properties.width ?? 0).toString();
|
const text = (width ?? 0).toString();
|
||||||
|
|
||||||
let dimensionChildren: JSX.Element | null = null;
|
let dimensionChildren: JSX.Element | null = null;
|
||||||
if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) {
|
if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) {
|
||||||
const {
|
const {
|
||||||
childrenId,
|
childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren
|
||||||
xChildrenStart,
|
|
||||||
xChildrenEnd,
|
|
||||||
yChildren,
|
|
||||||
textChildren
|
|
||||||
} = GetChildrenDimensionProps(props, dimensionMargin);
|
} = GetChildrenDimensionProps(props, dimensionMargin);
|
||||||
dimensionChildren = <Dimension
|
dimensionChildren = <Dimension
|
||||||
id={childrenId}
|
id={childrenId}
|
||||||
|
@ -69,8 +73,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
yStart={yChildren}
|
yStart={yChildren}
|
||||||
yEnd={yChildren}
|
yEnd={yChildren}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
text={textChildren}
|
text={textChildren} />;
|
||||||
/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -84,13 +87,11 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
id={id}
|
id={id}
|
||||||
xStart={xStart}
|
xStart={xStart}
|
||||||
xEnd={xEnd}
|
xEnd={xEnd}
|
||||||
yStart={y}
|
yStart={yDim}
|
||||||
yEnd={y}
|
yEnd={yDim}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
text={text}
|
text={text} />
|
||||||
/>
|
: null}
|
||||||
: null
|
|
||||||
}
|
|
||||||
{dimensionChildren}
|
{dimensionChildren}
|
||||||
{svg}
|
{svg}
|
||||||
{SHOW_TEXT
|
{SHOW_TEXT
|
||||||
|
@ -104,24 +105,23 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
{containersElements}
|
{containersElements}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number):
|
function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): { childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } {
|
||||||
{ childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } {
|
|
||||||
const childrenId = `dim-children-${props.model.properties.id}`;
|
const childrenId = `dim-children-${props.model.properties.id}`;
|
||||||
|
|
||||||
const lastChild = props.model.children[props.model.children.length - 1];
|
const lastChild = props.model.children[props.model.children.length - 1];
|
||||||
let xChildrenStart = transformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.XPositionReference);
|
let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.xPositionReference);
|
||||||
let xChildrenEnd = 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
|
// Find the min and max
|
||||||
for (let i = props.model.children.length - 2; i >= 0; i--) {
|
for (let i = props.model.children.length - 2; i >= 0; i--) {
|
||||||
const child = props.model.children[i];
|
const child = props.model.children[i];
|
||||||
const left = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference);
|
const left = TransformX(child.properties.x, child.properties.width, child.properties.xPositionReference);
|
||||||
if (left < xChildrenStart) {
|
if (left < xChildrenStart) {
|
||||||
xChildrenStart = left;
|
xChildrenStart = left;
|
||||||
}
|
}
|
||||||
const right = transformX(child.properties.x, child.properties.width, child.properties.XPositionReference);
|
const right = TransformX(child.properties.x, child.properties.width, child.properties.xPositionReference);
|
||||||
if (right > xChildrenEnd) {
|
if (right > xChildrenEnd) {
|
||||||
xChildrenEnd = right;
|
xChildrenEnd = right;
|
||||||
}
|
}
|
||||||
|
@ -132,37 +132,17 @@ function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: numb
|
||||||
return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren };
|
return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
function CreateReactCustomSVG(customSVG: string, props: IContainerProperties): React.ReactNode {
|
||||||
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;
|
|
||||||
if (xPositionReference === XPositionReference.Center) {
|
|
||||||
transformedX -= width / 2;
|
|
||||||
} else if (xPositionReference === XPositionReference.Right) {
|
|
||||||
transformedX -= width;
|
|
||||||
}
|
|
||||||
return transformedX;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateReactCustomSVG(customSVG: string, props: IProperties): React.ReactNode {
|
|
||||||
return <Interweave
|
return <Interweave
|
||||||
tagName='g'
|
tagName='g'
|
||||||
disableLineBreaks={true}
|
disableLineBreaks={true}
|
||||||
content={customSVG}
|
content={customSVG}
|
||||||
allowElements={true}
|
allowElements={true}
|
||||||
transform={(node, children) => transform(node, children, props)}
|
transform={(node, children) => Transform(node, children, props)}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function transform(node: HTMLElement, children: Node[], props: IProperties): React.ReactNode {
|
function Transform(node: HTMLElement, children: Node[], props: IContainerProperties): React.ReactNode {
|
||||||
const supportedTags = ['line', 'path', 'rect'];
|
const supportedTags = ['line', 'path', 'rect'];
|
||||||
if (supportedTags.includes(node.tagName.toLowerCase())) {
|
if (supportedTags.includes(node.tagName.toLowerCase())) {
|
||||||
const attributes: { [att: string]: string | object | null } = {};
|
const attributes: { [att: string]: string | object | null } = {};
|
||||||
|
@ -183,7 +163,7 @@ function transform(node: HTMLElement, children: Node[], props: IProperties): Rea
|
||||||
|
|
||||||
const prop = Object.entries(props.userData).find(([key]) => `{${key}}` === userDataKey);
|
const prop = Object.entries(props.userData).find(([key]) => `{${key}}` === userDataKey);
|
||||||
if (prop !== undefined) {
|
if (prop !== undefined) {
|
||||||
attributes[camelize(attName)] = prop[1];
|
attributes[Camelize(attName)] = prop[1];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,22 +172,18 @@ function transform(node: HTMLElement, children: Node[], props: IProperties): Rea
|
||||||
// support for object
|
// support for object
|
||||||
const stringObject = attributeValue.slice(1, -1);
|
const stringObject = attributeValue.slice(1, -1);
|
||||||
const object: JSON = JSON.parse(stringObject);
|
const object: JSON = JSON.parse(stringObject);
|
||||||
attributes[camelize(attName)] = object;
|
attributes[Camelize(attName)] = object;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prop = Object.entries(props).find(([key]) => `{${key}}` === attributeValue);
|
const prop = Object.entries(props).find(([key]) => `{${key}}` === attributeValue);
|
||||||
if (prop !== undefined) {
|
if (prop !== undefined) {
|
||||||
attributes[camelize(attName)] = prop[1];
|
attributes[Camelize(attName)] = prop[1];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
attributes[camelize(attName)] = attributeValue;
|
attributes[Camelize(attName)] = attributeValue;
|
||||||
});
|
});
|
||||||
return React.createElement(node.tagName.toLowerCase(), attributes, children);
|
return React.createElement(node.tagName.toLowerCase(), attributes, children);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function camelize(str: string): any {
|
|
||||||
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { DIMENSION_MARGIN } from '../../../utils/default';
|
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||||
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
||||||
import { transformX } from './Container';
|
import { TransformX } from '../../../utils/svg';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
|
|
||||||
interface IDimensionLayerProps {
|
interface IDimensionLayerProps {
|
||||||
roots: ContainerModel | ContainerModel[] | null
|
roots: ContainerModel | ContainerModel[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
function GetDimensionsNodes(root: ContainerModel): React.ReactNode[] {
|
||||||
const it = MakeBFSIterator(root);
|
const it = MakeBFSIterator(root);
|
||||||
const dimensions: React.ReactNode[] = [];
|
const dimensions: React.ReactNode[] = [];
|
||||||
let currentDepth = 0;
|
let currentDepth = 0;
|
||||||
|
@ -25,8 +25,8 @@ const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
||||||
max = -Infinity;
|
max = -Infinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
const absoluteX = getAbsolutePosition(container)[0];
|
const absoluteX = GetAbsolutePosition(container)[0];
|
||||||
const x = transformX(absoluteX, container.properties.width, container.properties.XPositionReference);
|
const x = TransformX(absoluteX, container.properties.width, container.properties.xPositionReference);
|
||||||
lastY = container.properties.y + container.properties.height;
|
lastY = container.properties.y + container.properties.height;
|
||||||
if (x < min) {
|
if (x < min) {
|
||||||
min = x;
|
min = x;
|
||||||
|
@ -40,28 +40,28 @@ const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
||||||
AddNewDimension(currentDepth, min, max, lastY, dimensions);
|
AddNewDimension(currentDepth, min, max, lastY, dimensions);
|
||||||
|
|
||||||
return dimensions;
|
return dimensions;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A layer containing all dimension
|
* A layer containing all dimension
|
||||||
* @param props
|
* @param props
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const DepthDimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => {
|
export function DepthDimensionLayer(props: IDimensionLayerProps): JSX.Element {
|
||||||
let dimensions: React.ReactNode[] = [];
|
let dimensions: React.ReactNode[] = [];
|
||||||
if (Array.isArray(props.roots)) {
|
if (Array.isArray(props.roots)) {
|
||||||
props.roots.forEach(child => {
|
props.roots.forEach(child => {
|
||||||
dimensions.concat(getDimensionsNodes(child));
|
dimensions.concat(GetDimensionsNodes(child));
|
||||||
});
|
});
|
||||||
} else if (props.roots !== null) {
|
} else if (props.roots !== null) {
|
||||||
dimensions = getDimensionsNodes(props.roots);
|
dimensions = GetDimensionsNodes(props.roots);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
{dimensions}
|
{dimensions}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, dimensions: React.ReactNode[]): void {
|
function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, dimensions: React.ReactNode[]): void {
|
||||||
const id = `dim-depth-${currentDepth}`;
|
const id = `dim-depth-${currentDepth}`;
|
||||||
|
|
|
@ -20,9 +20,11 @@ interface IDimensionProps {
|
||||||
* @param vx Transform vector
|
* @param vx Transform vector
|
||||||
* @returns Returns a new coordinate from the origin coordinate
|
* @returns Returns a new coordinate from the origin coordinate
|
||||||
*/
|
*/
|
||||||
const applyParametric = (x0: number, t: number, vx: number): number => x0 + t * vx;
|
function ApplyParametric(x0: number, t: number, vx: number): number {
|
||||||
|
return x0 + t * vx;
|
||||||
|
}
|
||||||
|
|
||||||
export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) => {
|
export function Dimension(props: IDimensionProps): JSX.Element {
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
stroke: 'black'
|
stroke: 'black'
|
||||||
};
|
};
|
||||||
|
@ -39,15 +41,15 @@ export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) =>
|
||||||
const [perpVecX, perpVecY] = [unitY, -unitX];
|
const [perpVecX, perpVecY] = [unitY, -unitX];
|
||||||
|
|
||||||
// Use the parametric function to get the coordinates (x = x0 + t * v.x)
|
// Use the parametric function to get the coordinates (x = x0 + t * v.x)
|
||||||
const startTopX = applyParametric(props.xStart, NOTCHES_LENGTH, perpVecX);
|
const startTopX = ApplyParametric(props.xStart, NOTCHES_LENGTH, perpVecX);
|
||||||
const startTopY = applyParametric(props.yStart, NOTCHES_LENGTH, perpVecY);
|
const startTopY = ApplyParametric(props.yStart, NOTCHES_LENGTH, perpVecY);
|
||||||
const startBottomX = applyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX);
|
const startBottomX = ApplyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX);
|
||||||
const startBottomY = applyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY);
|
const startBottomY = ApplyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY);
|
||||||
|
|
||||||
const endTopX = applyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX);
|
const endTopX = ApplyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX);
|
||||||
const endTopY = applyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY);
|
const endTopY = ApplyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY);
|
||||||
const endBottomX = applyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX);
|
const endBottomX = ApplyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX);
|
||||||
const endBottomY = applyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY);
|
const endBottomY = ApplyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={props.id}>
|
<g key={props.id}>
|
||||||
|
@ -57,24 +59,21 @@ export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) =>
|
||||||
x2={startBottomX}
|
x2={startBottomX}
|
||||||
y2={startBottomY}
|
y2={startBottomY}
|
||||||
strokeWidth={props.strokeWidth}
|
strokeWidth={props.strokeWidth}
|
||||||
style={style}
|
style={style} />
|
||||||
/>
|
|
||||||
<line
|
<line
|
||||||
x1={props.xStart}
|
x1={props.xStart}
|
||||||
y1={props.yStart}
|
y1={props.yStart}
|
||||||
x2={props.xEnd}
|
x2={props.xEnd}
|
||||||
y2={props.yEnd}
|
y2={props.yEnd}
|
||||||
strokeWidth={props.strokeWidth}
|
strokeWidth={props.strokeWidth}
|
||||||
style={style}
|
style={style} />
|
||||||
/>
|
|
||||||
<line
|
<line
|
||||||
x1={endTopX}
|
x1={endTopX}
|
||||||
y1={endTopY}
|
y1={endTopY}
|
||||||
x2={endBottomX}
|
x2={endBottomX}
|
||||||
y2={endBottomY}
|
y2={endBottomY}
|
||||||
strokeWidth={props.strokeWidth}
|
strokeWidth={props.strokeWidth}
|
||||||
style={style}
|
style={style} />
|
||||||
/>
|
|
||||||
<text
|
<text
|
||||||
x={(props.xStart + props.xEnd) / 2}
|
x={(props.xStart + props.xEnd) / 2}
|
||||||
y={props.yStart}
|
y={props.yStart}
|
||||||
|
@ -83,4 +82,4 @@ export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) =>
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { DIMENSION_MARGIN } from '../../../utils/default';
|
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||||
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
|
|
||||||
interface IDimensionLayerProps {
|
interface IDimensionLayerProps {
|
||||||
roots: ContainerModel | ContainerModel[] | null
|
roots: ContainerModel | ContainerModel[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
function GetDimensionsNodes(root: ContainerModel): React.ReactNode[] {
|
||||||
const it = MakeBFSIterator(root);
|
const it = MakeBFSIterator(root);
|
||||||
const dimensions: React.ReactNode[] = [];
|
const dimensions: React.ReactNode[] = [];
|
||||||
for (const { container, depth } of it) {
|
for (const { container, depth } of it) {
|
||||||
const width = container.properties.width;
|
const width = container.properties.width;
|
||||||
const id = `dim-${container.properties.id}`;
|
const id = `dim-${container.properties.id}`;
|
||||||
const xStart = getAbsolutePosition(container)[0];
|
const xStart = GetAbsolutePosition(container)[0];
|
||||||
const xEnd = xStart + width;
|
const xEnd = xStart + width;
|
||||||
const y = (container.properties.y + container.properties.height) + (DIMENSION_MARGIN * (depth + 1));
|
const y = (container.properties.y + container.properties.height) + (DIMENSION_MARGIN * (depth + 1));
|
||||||
const strokeWidth = 1;
|
const strokeWidth = 1;
|
||||||
|
@ -28,30 +28,29 @@ const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
||||||
xEnd={xEnd}
|
xEnd={xEnd}
|
||||||
yEnd={y}
|
yEnd={y}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
text={text}
|
text={text} />
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return dimensions;
|
return dimensions;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A layer containing all dimension
|
* A layer containing all dimension
|
||||||
* @param props
|
* @param props
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const DimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => {
|
export function DimensionLayer(props: IDimensionLayerProps): JSX.Element {
|
||||||
let dimensions: React.ReactNode[] = [];
|
let dimensions: React.ReactNode[] = [];
|
||||||
if (Array.isArray(props.roots)) {
|
if (Array.isArray(props.roots)) {
|
||||||
props.roots.forEach(child => {
|
props.roots.forEach(child => {
|
||||||
dimensions.concat(getDimensionsNodes(child));
|
dimensions.concat(GetDimensionsNodes(child));
|
||||||
});
|
});
|
||||||
} else if (props.roots !== null) {
|
} else if (props.roots !== null) {
|
||||||
dimensions = getDimensionsNodes(props.roots);
|
dimensions = GetDimensionsNodes(props.roots);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
{dimensions}
|
{dimensions}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
|
||||||
import { getAbsolutePosition } from '../../../utils/itertools';
|
|
||||||
|
|
||||||
interface ISelectorProps {
|
|
||||||
selected: IContainerModel | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Selector: React.FC<ISelectorProps> = (props) => {
|
|
||||||
if (props.selected === undefined || props.selected === null) {
|
|
||||||
return (
|
|
||||||
<rect visibility={'hidden'}>
|
|
||||||
</rect>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [x, y] = getAbsolutePosition(props.selected);
|
|
||||||
const [width, height] = [
|
|
||||||
props.selected.properties.width,
|
|
||||||
props.selected.properties.height
|
|
||||||
];
|
|
||||||
const style: React.CSSProperties = {
|
|
||||||
stroke: '#3B82F6', // tw blue-500
|
|
||||||
strokeWidth: 4,
|
|
||||||
fillOpacity: 0,
|
|
||||||
transitionProperty: 'all',
|
|
||||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
transitionDuration: '150ms',
|
|
||||||
animation: 'fadein 750ms ease-in alternate infinite'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<rect
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
</rect>
|
|
||||||
);
|
|
||||||
};
|
|
4
src/Components/SVG/Elements/Selector/Selector.scss
Normal file
4
src/Components/SVG/Elements/Selector/Selector.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
@keyframes fadein {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
66
src/Components/SVG/Elements/Selector/Selector.tsx
Normal file
66
src/Components/SVG/Elements/Selector/Selector.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import './Selector.scss';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IContainerModel } from '../../../../Interfaces/IContainerModel';
|
||||||
|
import { SHOW_SELECTOR_TEXT } from '../../../../utils/default';
|
||||||
|
import { GetAbsolutePosition } from '../../../../utils/itertools';
|
||||||
|
import { RemoveMargin } from '../../../../utils/svg';
|
||||||
|
|
||||||
|
interface ISelectorProps {
|
||||||
|
selected?: IContainerModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Selector(props: ISelectorProps): JSX.Element {
|
||||||
|
if (props.selected === undefined || props.selected === null) {
|
||||||
|
return (
|
||||||
|
<rect visibility={'hidden'}>
|
||||||
|
</rect>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 xText = x + width / 2;
|
||||||
|
const yText = y + height / 2;
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
stroke: '#3B82F6',
|
||||||
|
strokeWidth: 4,
|
||||||
|
fillOpacity: 0,
|
||||||
|
transitionProperty: 'all',
|
||||||
|
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
transitionDuration: '150ms',
|
||||||
|
animation: 'fadein 750ms ease-in alternate infinite'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
</rect>
|
||||||
|
{SHOW_SELECTOR_TEXT
|
||||||
|
? <text
|
||||||
|
x={xText}
|
||||||
|
y={yText}
|
||||||
|
>
|
||||||
|
{props.selected.properties.displayedText}
|
||||||
|
</text>
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
37
src/Components/SVG/Elements/Symbol.tsx
Normal file
37
src/Components/SVG/Elements/Symbol.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Interweave } from 'interweave';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||||
|
|
||||||
|
interface ISymbolProps {
|
||||||
|
model: ISymbolModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Symbol(props: ISymbolProps): JSX.Element {
|
||||||
|
const href = props.model.config.Image.Base64Image ?? props.model.config.Image.Url;
|
||||||
|
const hasSVG = props.model.config.Image.Svg !== undefined &&
|
||||||
|
props.model.config.Image.Svg !== null;
|
||||||
|
if (hasSVG) {
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
x={props.model.x}
|
||||||
|
y={-DIMENSION_MARGIN}
|
||||||
|
>
|
||||||
|
<Interweave
|
||||||
|
noWrap={true}
|
||||||
|
disableLineBreaks={true}
|
||||||
|
content={props.model.config.Image.Svg}
|
||||||
|
allowElements={true} />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<image
|
||||||
|
href={href}
|
||||||
|
x={props.model.x}
|
||||||
|
y={-DIMENSION_MARGIN}
|
||||||
|
height={props.model.height}
|
||||||
|
width={props.model.width} />
|
||||||
|
);
|
||||||
|
}
|
21
src/Components/SVG/Elements/SymbolLayer.tsx
Normal file
21
src/Components/SVG/Elements/SymbolLayer.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import { Symbol } from './Symbol';
|
||||||
|
|
||||||
|
interface ISymbolLayerProps {
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SymbolLayer(props: ISymbolLayerProps): JSX.Element {
|
||||||
|
const symbols: JSX.Element[] = [];
|
||||||
|
props.symbols.forEach((symbol) => {
|
||||||
|
symbols.push(
|
||||||
|
<Symbol key={`symbol-${symbol.id}`} model={symbol} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
{symbols}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
14
src/Components/SVG/SVG.scss
Normal file
14
src/Components/SVG/SVG.scss
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
text {
|
||||||
|
font-family: 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
fill: #fff;
|
||||||
|
fill-opacity: 1;
|
||||||
|
stroke: #000000;
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke-linecap: butt;
|
||||||
|
stroke-linejoin: miter;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
transform-box: fill-box;
|
||||||
|
}
|
|
@ -1,18 +1,21 @@
|
||||||
|
import './SVG.scss';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
|
import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
|
||||||
import { Container } from './Elements/Container';
|
import { Container } from './Elements/Container';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { Selector } from './Elements/Selector';
|
import { Selector } from './Elements/Selector/Selector';
|
||||||
import { BAR_WIDTH } from '../Bar/Bar';
|
import { BAR_WIDTH } from '../Bar/Bar';
|
||||||
import { DimensionLayer } from './Elements/DimensionLayer';
|
|
||||||
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
||||||
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
||||||
|
import { SymbolLayer } from './Elements/SymbolLayer';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
interface ISVGProps {
|
interface ISVGProps {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
children: ContainerModel | ContainerModel[] | null
|
children: ContainerModel | ContainerModel[] | null
|
||||||
selected: ContainerModel | null
|
selected?: ContainerModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Viewer {
|
interface Viewer {
|
||||||
|
@ -22,7 +25,7 @@ interface Viewer {
|
||||||
|
|
||||||
export const ID = 'svg';
|
export const ID = 'svg';
|
||||||
|
|
||||||
function resizeViewBox(
|
function ResizeViewBox(
|
||||||
setViewer: React.Dispatch<React.SetStateAction<Viewer>>
|
setViewer: React.Dispatch<React.SetStateAction<Viewer>>
|
||||||
): void {
|
): void {
|
||||||
setViewer({
|
setViewer({
|
||||||
|
@ -31,20 +34,28 @@ function resizeViewBox(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
|
function UseSVGAutoResizer(
|
||||||
|
setViewer: React.Dispatch<React.SetStateAction<Viewer>>
|
||||||
|
): void {
|
||||||
|
React.useEffect(() => {
|
||||||
|
function OnResize(): void {
|
||||||
|
return ResizeViewBox(setViewer);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', OnResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', OnResize);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SVG(props: ISVGProps): JSX.Element {
|
||||||
const [viewer, setViewer] = React.useState<Viewer>({
|
const [viewer, setViewer] = React.useState<Viewer>({
|
||||||
viewerWidth: window.innerWidth - BAR_WIDTH,
|
viewerWidth: window.innerWidth - BAR_WIDTH,
|
||||||
viewerHeight: window.innerHeight
|
viewerHeight: window.innerHeight
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
UseSVGAutoResizer(setViewer);
|
||||||
const onResize = (): void => resizeViewBox(setViewer);
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', onResize);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const xmlns = '<http://www.w3.org/2000/svg>';
|
const xmlns = '<http://www.w3.org/2000/svg>';
|
||||||
const properties = {
|
const properties = {
|
||||||
|
@ -76,14 +87,13 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
|
||||||
>
|
>
|
||||||
<svg {...properties}>
|
<svg {...properties}>
|
||||||
{children}
|
{children}
|
||||||
<Selector selected={props.selected} />
|
{SHOW_DIMENSIONS_PER_DEPTH
|
||||||
{
|
|
||||||
SHOW_DIMENSIONS_PER_DEPTH
|
|
||||||
? <DepthDimensionLayer roots={props.children} />
|
? <DepthDimensionLayer roots={props.children} />
|
||||||
: null
|
: null}
|
||||||
}
|
<SymbolLayer symbols={props.symbols} />
|
||||||
|
<Selector selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
|
||||||
</svg>
|
</svg>
|
||||||
</UncontrolledReactSVGPanZoom>
|
</UncontrolledReactSVGPanZoom>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
55
src/Components/Select/Select.tsx
Normal file
55
src/Components/Select/Select.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IInputGroup } from '../../Interfaces/IInputGroup';
|
||||||
|
|
||||||
|
interface ISelectProps {
|
||||||
|
labelKey?: string
|
||||||
|
labelText: string
|
||||||
|
inputKey: string
|
||||||
|
labelClassName: string
|
||||||
|
inputClassName: string
|
||||||
|
inputs: IInputGroup[]
|
||||||
|
value?: string
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => 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 function Select(props: ISelectProps): JSX.Element {
|
||||||
|
const options = [(
|
||||||
|
<option key='symbol-none' value=''>None</option>
|
||||||
|
)];
|
||||||
|
|
||||||
|
props.inputs.forEach(input => {
|
||||||
|
options.push(<option
|
||||||
|
key={input.value}
|
||||||
|
value={input.value}
|
||||||
|
>
|
||||||
|
{input.text}
|
||||||
|
</option>);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
key={props.labelKey}
|
||||||
|
className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`}
|
||||||
|
htmlFor={props.inputKey}
|
||||||
|
>
|
||||||
|
{props.labelText}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id={props.inputKey}
|
||||||
|
value={props.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -31,15 +31,17 @@ describe.concurrent('Sidebar', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('With stuff', () => {
|
it('With stuff', () => {
|
||||||
const Type = 'stuff';
|
const type = 'stuff';
|
||||||
const handleButtonClick = vi.fn();
|
const handleButtonClick = vi.fn();
|
||||||
render(<Sidebar
|
render(<Sidebar
|
||||||
componentOptions={[
|
componentOptions={[
|
||||||
{
|
{
|
||||||
Type,
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
Type: type,
|
||||||
Width: 30,
|
Width: 30,
|
||||||
Height: 30,
|
Height: 30,
|
||||||
Style: {}
|
Style: {}
|
||||||
|
/* eslint-enable */
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||||
import { truncateString } from '../../utils/stringtools';
|
import { TruncateString } from '../../utils/stringtools';
|
||||||
|
|
||||||
interface ISidebarProps {
|
interface ISidebarProps {
|
||||||
componentOptions: IAvailableContainer[]
|
componentOptions: IAvailableContainer[]
|
||||||
|
@ -8,22 +8,22 @@ interface ISidebarProps {
|
||||||
buttonOnClick: (type: string) => void
|
buttonOnClick: (type: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
|
function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
|
||||||
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
|
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => {
|
export function Sidebar(props: ISidebarProps): JSX.Element {
|
||||||
const listElements = props.componentOptions.map(componentOption =>
|
const listElements = props.componentOptions.map(componentOption =>
|
||||||
<button
|
<button type="button"
|
||||||
className='justify-center transition-all sidebar-component'
|
className='justify-center transition-all sidebar-component'
|
||||||
key={componentOption.Type}
|
key={componentOption.Type}
|
||||||
id={componentOption.Type}
|
id={componentOption.Type}
|
||||||
title={componentOption.Type}
|
title={componentOption.Type}
|
||||||
onClick={() => props.buttonOnClick(componentOption.Type)}
|
onClick={() => props.buttonOnClick(componentOption.Type)}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragStart={(event) => handleDragStart(event)}
|
onDragStart={(event) => HandleDragStart(event)}
|
||||||
>
|
>
|
||||||
{truncateString(componentOption.Type, 5)}
|
{TruncateString(componentOption.Type, 5)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
51
src/Components/SymbolProperties/SymbolForm.tsx
Normal file
51
src/Components/SymbolProperties/SymbolForm.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { RestoreX, TransformX } from '../../utils/svg';
|
||||||
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
|
|
||||||
|
interface ISymbolFormProps {
|
||||||
|
symbol: ISymbolModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SymbolForm(props: ISymbolFormProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-2 gap-y-4'>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Name'
|
||||||
|
inputKey='id'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={props.symbol.id.toString()}
|
||||||
|
isDisabled={true} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='x'
|
||||||
|
inputKey='x'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
value={TransformX(props.symbol.x, props.symbol.width, props.symbol.config.XPositionReference).toString()}
|
||||||
|
onChange={(event) => props.onChange('x', RestoreX(Number(event.target.value), props.symbol.width, props.symbol.config.XPositionReference))} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='Height'
|
||||||
|
inputKey='height'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={props.symbol.height.toString()}
|
||||||
|
onChange={(event) => props.onChange('height', Number(event.target.value))} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='Width'
|
||||||
|
inputKey='width'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={props.symbol.width.toString()}
|
||||||
|
onChange={(event) => props.onChange('width', Number(event.target.value))} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
24
src/Components/SymbolProperties/SymbolProperties.tsx
Normal file
24
src/Components/SymbolProperties/SymbolProperties.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { SymbolForm } from './SymbolForm';
|
||||||
|
|
||||||
|
interface ISymbolPropertiesProps {
|
||||||
|
symbol?: ISymbolModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SymbolProperties(props: ISymbolPropertiesProps): JSX.Element {
|
||||||
|
if (props.symbol === undefined) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
||||||
|
<SymbolForm
|
||||||
|
symbol={props.symbol}
|
||||||
|
symbols={props.symbols}
|
||||||
|
onChange={props.onChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
68
src/Components/Symbols/Symbols.tsx
Normal file
68
src/Components/Symbols/Symbols.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||||
|
import { TruncateString } from '../../utils/stringtools';
|
||||||
|
|
||||||
|
interface ISymbolsProps {
|
||||||
|
componentOptions: IAvailableSymbol[]
|
||||||
|
isOpen: boolean
|
||||||
|
buttonOnClick: (type: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function HandleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
|
||||||
|
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Symbols(props: ISymbolsProps): JSX.Element {
|
||||||
|
const listElements = props.componentOptions.map(componentOption => {
|
||||||
|
if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) {
|
||||||
|
const url = componentOption.Image.Base64Image ?? componentOption.Image.Url;
|
||||||
|
return (<button type="button"
|
||||||
|
className='justify-center sidebar-component-card hover:h-full'
|
||||||
|
key={componentOption.Name}
|
||||||
|
id={componentOption.Name}
|
||||||
|
title={componentOption.Name}
|
||||||
|
onClick={() => props.buttonOnClick(componentOption.Name)}
|
||||||
|
draggable={true}
|
||||||
|
onDragStart={(event) => HandleDragStart(event)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
className='transition-all h-12 w-full object-cover'
|
||||||
|
src={url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{TruncateString(componentOption.Name, 5)}
|
||||||
|
</div>
|
||||||
|
</button>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<button type="button"
|
||||||
|
className='group justify-center sidebar-component hover:h-full'
|
||||||
|
key={componentOption.Name}
|
||||||
|
id={componentOption.Name}
|
||||||
|
title={componentOption.Name}
|
||||||
|
onClick={() => props.buttonOnClick(componentOption.Name)}
|
||||||
|
draggable={true}
|
||||||
|
onDragStart={(event) => HandleDragStart(event)}
|
||||||
|
>
|
||||||
|
|
||||||
|
{TruncateString(componentOption.Name, 5)}
|
||||||
|
</button>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
|
||||||
|
return (
|
||||||
|
<div className={`fixed z-10 bg-slate-200
|
||||||
|
text-gray-700 transition-all h-full w-64
|
||||||
|
overflow-y-auto ${isOpenClasses}`}>
|
||||||
|
<div className='bg-slate-100 sidebar-title'>
|
||||||
|
Symbols
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-3 gap-2
|
||||||
|
m-2 md:text-xs font-bold'>
|
||||||
|
{listElements}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
84
src/Components/SymbolsSidebar/MouseEventHandlers.ts
Normal file
84
src/Components/SymbolsSidebar/MouseEventHandlers.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
|
||||||
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
|
|
||||||
|
export function UseMouseEvents(
|
||||||
|
isContextMenuOpen: boolean,
|
||||||
|
elementRef: RefObject<HTMLDivElement>,
|
||||||
|
setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>,
|
||||||
|
setOnClickSymbolId: Dispatch<SetStateAction<string>>,
|
||||||
|
setContextMenuPosition: Dispatch<SetStateAction<IPoint>>
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
|
function OnContextMenu(event: MouseEvent): void {
|
||||||
|
return HandleRightClick(
|
||||||
|
event,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickSymbolId,
|
||||||
|
setContextMenuPosition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnLeftClick(): void {
|
||||||
|
return HandleLeftClick(
|
||||||
|
isContextMenuOpen,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickSymbolId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
elementRef.current?.addEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
OnContextMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'click',
|
||||||
|
OnLeftClick
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
elementRef.current?.removeEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
OnContextMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
window.removeEventListener(
|
||||||
|
'click',
|
||||||
|
OnLeftClick
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HandleRightClick(
|
||||||
|
event: MouseEvent,
|
||||||
|
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setOnClickSymbolId: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>
|
||||||
|
): void {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
|
setIsContextMenuOpen(false);
|
||||||
|
setOnClickSymbolId('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY };
|
||||||
|
setIsContextMenuOpen(true);
|
||||||
|
setOnClickSymbolId(event.target.id);
|
||||||
|
setContextMenuPosition(contextMenuPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HandleLeftClick(
|
||||||
|
isContextMenuOpen: boolean,
|
||||||
|
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
): void {
|
||||||
|
if (!isContextMenuOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsContextMenuOpen(false);
|
||||||
|
setOnClickContainerId('');
|
||||||
|
}
|
105
src/Components/SymbolsSidebar/SymbolsSidebar.tsx
Normal file
105
src/Components/SymbolsSidebar/SymbolsSidebar.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
import { Menu } from '../Menu/Menu';
|
||||||
|
import { MenuItem } from '../Menu/MenuItem';
|
||||||
|
import { UseMouseEvents } from './MouseEventHandlers';
|
||||||
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
|
||||||
|
|
||||||
|
interface ISymbolsSidebarProps {
|
||||||
|
selectedSymbolId: string
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
isOpen: boolean
|
||||||
|
isHistoryOpen: boolean
|
||||||
|
onPropertyChange: (key: string, value: string | number | boolean) => void
|
||||||
|
selectSymbol: (symbolId: string) => void
|
||||||
|
deleteSymbol: (containerid: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
||||||
|
// States
|
||||||
|
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
||||||
|
const [onClickSymbolId, setOnClickSymbolId] = React.useState<string>('');
|
||||||
|
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
UseMouseEvents(
|
||||||
|
isContextMenuOpen,
|
||||||
|
elementRef,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickSymbolId,
|
||||||
|
setContextMenuPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render
|
||||||
|
let isOpenClasses = '-right-64';
|
||||||
|
if (props.isOpen) {
|
||||||
|
isOpenClasses = props.isHistoryOpen
|
||||||
|
? 'right-64'
|
||||||
|
: 'right-0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = [...props.symbols.values()];
|
||||||
|
function Row({ index, style }: { index: number, style: React.CSSProperties }): JSX.Element {
|
||||||
|
const container = containers[index];
|
||||||
|
const key = container.id.toString();
|
||||||
|
const text = key;
|
||||||
|
const selectedClass: string = props.selectedSymbolId !== '' &&
|
||||||
|
props.selectedSymbolId === container.id
|
||||||
|
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
|
||||||
|
: 'bg-slate-300/60 hover:bg-slate-300';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button"
|
||||||
|
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}
|
||||||
|
onClick={() => props.selectSymbol(key)}
|
||||||
|
>
|
||||||
|
{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'>
|
||||||
|
<List
|
||||||
|
className='List divide-y divide-black'
|
||||||
|
itemCount={containers.length}
|
||||||
|
itemSize={35}
|
||||||
|
height={384}
|
||||||
|
width={256}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
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.deleteSymbol(onClickSymbolId);
|
||||||
|
} } />
|
||||||
|
</Menu>
|
||||||
|
<SymbolProperties
|
||||||
|
symbol={props.symbols.get(props.selectedSymbolId)}
|
||||||
|
symbols={props.symbols}
|
||||||
|
onChange={props.onPropertyChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,26 +1,26 @@
|
||||||
import React, { FC } from 'react';
|
import React from 'react';
|
||||||
import './ToggleButton.scss';
|
import './ToggleButton.scss';
|
||||||
|
|
||||||
interface IToggleButtonProps {
|
interface IToggleButtonProps {
|
||||||
id: string
|
id: string
|
||||||
text: string
|
text: string
|
||||||
type?: TOGGLE_TYPE
|
type?: ToggleType
|
||||||
title: string
|
title: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TOGGLE_TYPE {
|
export enum ToggleType {
|
||||||
MATERIAL,
|
Material,
|
||||||
IOS
|
IOS
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToggleButton: FC<IToggleButtonProps> = (props) => {
|
export function ToggleButton(props: IToggleButtonProps): JSX.Element {
|
||||||
const id = `toggle-${props.id}`;
|
const id = `toggle-${props.id}`;
|
||||||
const type = props.type ?? TOGGLE_TYPE.MATERIAL;
|
const type = props.type ?? ToggleType.Material;
|
||||||
let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner';
|
let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner';
|
||||||
let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition';
|
let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition';
|
||||||
if (type === TOGGLE_TYPE.IOS) {
|
if (type === ToggleType.IOS) {
|
||||||
classLine = 'line block bg-gray-600 w-14 h-8 rounded-full';
|
classLine = 'line block bg-gray-600 w-14 h-8 rounded-full';
|
||||||
classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition';
|
classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition';
|
||||||
}
|
}
|
||||||
|
@ -49,4 +49,4 @@ export const ToggleButton: FC<IToggleButtonProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -3,38 +3,54 @@ import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
|
||||||
import { Sidebar } from '../Sidebar/Sidebar';
|
import { Sidebar } from '../Sidebar/Sidebar';
|
||||||
import { History } from '../History/History';
|
import { History } from '../History/History';
|
||||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
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 { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||||
|
import { Symbols } from '../Symbols/Symbols';
|
||||||
|
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
|
||||||
|
import { PropertyType } from '../../Enums/PropertyType';
|
||||||
|
|
||||||
interface IUIProps {
|
interface IUIProps {
|
||||||
|
selectedContainer: IContainerModel | undefined
|
||||||
current: IHistoryState
|
current: IHistoryState
|
||||||
history: IHistoryState[]
|
history: IHistoryState[]
|
||||||
historyCurrentStep: number
|
historyCurrentStep: number
|
||||||
AvailableContainers: IAvailableContainer[]
|
availableContainers: IAvailableContainer[]
|
||||||
SelectContainer: (container: ContainerModel) => void
|
availableSymbols: IAvailableSymbol[]
|
||||||
DeleteContainer: (containerId: string) => void
|
selectContainer: (containerId: string) => void
|
||||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
deleteContainer: (containerId: string) => void
|
||||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
onPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
|
||||||
AddContainerToSelectedContainer: (type: string) => void
|
addContainer: (type: string) => void
|
||||||
AddContainer: (index: number, type: string, parentId: string) => void
|
addSymbol: (type: string) => void
|
||||||
SaveEditorAsJSON: () => void
|
onSymbolPropertyChange: (key: string, value: string | number | boolean) => void
|
||||||
SaveEditorAsSVG: () => void
|
selectSymbol: (symbolId: string) => void
|
||||||
LoadState: (move: number) => void
|
deleteSymbol: (symbolId: string) => void
|
||||||
|
saveEditorAsJSON: () => void
|
||||||
|
saveEditorAsSVG: () => void
|
||||||
|
loadState: (move: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
function CloseOtherSidebars(
|
||||||
|
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
): void {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
setIsSymbolsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UI(props: IUIProps): JSX.Element {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||||
const [isElementsSidebarOpen, setIsElementsSidebarOpen] = React.useState(false);
|
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
|
||||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||||
|
|
||||||
let buttonRightOffsetClasses = 'right-12';
|
let buttonRightOffsetClasses = 'right-12';
|
||||||
if (isElementsSidebarOpen || isHistoryOpen) {
|
if (isSidebarOpen || isHistoryOpen) {
|
||||||
buttonRightOffsetClasses = 'right-72';
|
buttonRightOffsetClasses = 'right-72';
|
||||||
}
|
}
|
||||||
if (isHistoryOpen && isElementsSidebarOpen) {
|
if (isHistoryOpen && isSidebarOpen) {
|
||||||
buttonRightOffsetClasses = 'right-[544px]';
|
buttonRightOffsetClasses = 'right-[544px]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,54 +58,66 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
||||||
<>
|
<>
|
||||||
<Bar
|
<Bar
|
||||||
isSidebarOpen={isSidebarOpen}
|
isSidebarOpen={isSidebarOpen}
|
||||||
isElementsSidebarOpen={isElementsSidebarOpen}
|
isSymbolsOpen={isSymbolsOpen}
|
||||||
|
isElementsSidebarOpen={isSidebarOpen}
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
ToggleElementsSidebar={() => setIsElementsSidebarOpen(!isElementsSidebarOpen)}
|
toggleSidebar={() => {
|
||||||
ToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
||||||
ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
setIsSidebarOpen(!isSidebarOpen);
|
||||||
/>
|
} }
|
||||||
|
toggleSymbols={() => {
|
||||||
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
||||||
|
setIsSymbolsOpen(!isSymbolsOpen);
|
||||||
|
} }
|
||||||
|
toggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} />
|
||||||
|
|
||||||
<Sidebar
|
<Sidebar
|
||||||
componentOptions={props.AvailableContainers}
|
componentOptions={props.availableContainers}
|
||||||
isOpen={isSidebarOpen}
|
isOpen={isSidebarOpen}
|
||||||
buttonOnClick={(type: string) => props.AddContainerToSelectedContainer(type)}
|
buttonOnClick={props.addContainer} />
|
||||||
/>
|
<Symbols
|
||||||
|
componentOptions={props.availableSymbols}
|
||||||
|
isOpen={isSymbolsOpen}
|
||||||
|
buttonOnClick={props.addSymbol} />
|
||||||
<ElementsSidebar
|
<ElementsSidebar
|
||||||
MainContainer={props.current.MainContainer}
|
mainContainer={props.current.mainContainer}
|
||||||
SelectedContainer={props.current.SelectedContainer}
|
symbols={props.current.symbols}
|
||||||
isOpen={isElementsSidebarOpen}
|
selectedContainer={props.selectedContainer}
|
||||||
|
isOpen={isSidebarOpen}
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
OnPropertyChange={props.OnPropertyChange}
|
onPropertyChange={props.onPropertyChange}
|
||||||
OnPropertiesSubmit={props.OnPropertiesSubmit}
|
selectContainer={props.selectContainer}
|
||||||
SelectContainer={props.SelectContainer}
|
deleteContainer={props.deleteContainer} />
|
||||||
DeleteContainer={props.DeleteContainer}
|
<SymbolsSidebar
|
||||||
AddContainer={props.AddContainer}
|
selectedSymbolId={props.current.selectedSymbolId}
|
||||||
/>
|
symbols={props.current.symbols}
|
||||||
|
isOpen={isSymbolsOpen}
|
||||||
|
isHistoryOpen={isHistoryOpen}
|
||||||
|
onPropertyChange={props.onSymbolPropertyChange}
|
||||||
|
selectSymbol={props.selectSymbol}
|
||||||
|
deleteSymbol={props.deleteSymbol} />
|
||||||
<History
|
<History
|
||||||
history={props.history}
|
history={props.history}
|
||||||
historyCurrentStep={props.historyCurrentStep}
|
historyCurrentStep={props.historyCurrentStep}
|
||||||
isOpen={isHistoryOpen}
|
isOpen={isHistoryOpen}
|
||||||
jumpTo={props.LoadState}
|
jumpTo={props.loadState} />
|
||||||
/>
|
|
||||||
|
|
||||||
<FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
|
<FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
|
||||||
<button
|
<button type="button"
|
||||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||||
title='Export as JSON'
|
title='Export as JSON'
|
||||||
onClick={props.SaveEditorAsJSON}
|
onClick={props.saveEditorAsJSON}
|
||||||
>
|
>
|
||||||
<UploadIcon className="heroicon text-white" />
|
<UploadIcon className="heroicon text-white" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||||
title='Export as SVG'
|
title='Export as SVG'
|
||||||
onClick={props.SaveEditorAsSVG}
|
onClick={props.saveEditorAsSVG}
|
||||||
>
|
>
|
||||||
<PhotographIcon className="heroicon text-white" />
|
<PhotographIcon className="heroicon text-white" />
|
||||||
</button>
|
</button>
|
||||||
</FloatingButton>
|
</FloatingButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default UI;
|
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
* Add method when creating a container
|
* Add method when creating a container
|
||||||
* - Append will append to the last children in list
|
* - Append will append to the last children in list
|
||||||
* - Insert will always place it at the begining
|
* - Insert will always place it at the begining
|
||||||
|
* - Replace will remove the selected container and insert a new one
|
||||||
* (default: Append)
|
* (default: Append)
|
||||||
*/
|
*/
|
||||||
export enum AddMethod {
|
export enum AddMethod {
|
||||||
Append,
|
Append,
|
||||||
Insert
|
Insert,
|
||||||
|
Replace // TODO: Implement this
|
||||||
}
|
}
|
||||||
|
|
22
src/Enums/PropertyType.ts
Normal file
22
src/Enums/PropertyType.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe the type of the property.
|
||||||
|
* Used for the assignation in the OnPropertyChange function
|
||||||
|
* See ContainerOperations.ts's OnPropertyChange
|
||||||
|
*/
|
||||||
|
export enum PropertyType {
|
||||||
|
/**
|
||||||
|
* Simple property: is not inside any object: id, x, width... (default)
|
||||||
|
*/
|
||||||
|
Simple,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style property: is inside the style object: stroke, fillOpacity...
|
||||||
|
*/
|
||||||
|
Style,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Margin property: is inside the margin property: left, bottom, top, right...
|
||||||
|
*/
|
||||||
|
Margin,
|
||||||
|
}
|
|
@ -1,26 +1,65 @@
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { GetCurrentHistory } from '../Components/Editor/Editor';
|
||||||
|
import { IConfiguration } from '../Interfaces/IConfiguration';
|
||||||
import { IEditorState } from '../Interfaces/IEditorState';
|
import { IEditorState } from '../Interfaces/IEditorState';
|
||||||
import { IHistoryState } from '../Interfaces/IHistoryState';
|
import { IHistoryState } from '../Interfaces/IHistoryState';
|
||||||
|
import { ReviveState } from '../utils/saveload';
|
||||||
|
|
||||||
const getEditorState = (editorState: IEditorState): void => {
|
|
||||||
|
const initEditor = (configuration: IConfiguration): void => {
|
||||||
|
// faire comme la callback de fetch
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEditorState = (
|
||||||
|
root: Element | Document,
|
||||||
|
editorState: IEditorState
|
||||||
|
): void => {
|
||||||
const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: editorState });
|
const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: editorState });
|
||||||
document.dispatchEvent(customEvent);
|
root.dispatchEvent(customEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentHistoryState = (editorState: IEditorState): void => {
|
const getCurrentHistoryState = (
|
||||||
|
root: Element | Document,
|
||||||
|
editorState: IEditorState
|
||||||
|
): void => {
|
||||||
const customEvent = new CustomEvent<IHistoryState>(
|
const customEvent = new CustomEvent<IHistoryState>(
|
||||||
'getCurrentHistoryState',
|
'getCurrentHistoryState',
|
||||||
{ detail: editorState.history[editorState.historyCurrentStep] });
|
{ detail: editorState.history[editorState.historyCurrentStep] });
|
||||||
document.dispatchEvent(customEvent);
|
root.dispatchEvent(customEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendNewState = (
|
||||||
|
root: Element | Document,
|
||||||
|
editorState: IEditorState,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
|
||||||
|
eventInitDict?: CustomEventInit
|
||||||
|
): void => {
|
||||||
|
const state: IHistoryState = JSON.parse(eventInitDict?.detail.state);
|
||||||
|
ReviveState(state);
|
||||||
|
const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep);
|
||||||
|
|
||||||
|
history.push(state);
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IEditorEvent {
|
export interface IEditorEvent {
|
||||||
name: string
|
name: string
|
||||||
func: (editorState: IEditorState) => void
|
func: (
|
||||||
|
root: Element | Document,
|
||||||
|
editorState: IEditorState,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
|
||||||
|
eventInitDict?: CustomEventInit
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const events: IEditorEvent[] = [
|
const events: IEditorEvent[] = [
|
||||||
{ name: 'getEditorState', func: getEditorState },
|
{ name: 'getEditorState', func: getEditorState },
|
||||||
{ name: 'getCurrentHistoryState', func: getCurrentHistoryState }
|
{ name: 'getCurrentHistoryState', func: getCurrentHistoryState },
|
||||||
|
{ name: 'appendNewState', func: appendNewState }
|
||||||
];
|
];
|
||||||
|
|
||||||
export default events;
|
export default events;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AddMethod } from '../Enums/AddMethod';
|
import { AddMethod } from '../Enums/AddMethod';
|
||||||
import { XPositionReference } from '../Enums/XPositionReference';
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
|
import { IMargin } from './IMargin';
|
||||||
|
|
||||||
/** Model of available container used in application configuration */
|
/** Model of available container used in application configuration */
|
||||||
export interface IAvailableContainer {
|
export interface IAvailableContainer {
|
||||||
|
@ -10,6 +12,9 @@ export interface IAvailableContainer {
|
||||||
Width?: number
|
Width?: number
|
||||||
Height?: number
|
Height?: number
|
||||||
MinWidth?: number
|
MinWidth?: number
|
||||||
|
MaxWidth?: number
|
||||||
|
Margin?: IMargin
|
||||||
|
IsFlex?: boolean
|
||||||
AddMethod?: AddMethod
|
AddMethod?: AddMethod
|
||||||
XPositionReference?: XPositionReference
|
XPositionReference?: XPositionReference
|
||||||
CustomSVG?: string
|
CustomSVG?: string
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { XPositionReference } from '../Enums/XPositionReference';
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
import { IImage } from './IImage';
|
import { IImage } from './IImage';
|
||||||
|
|
||||||
|
@ -5,8 +6,8 @@ import { IImage } from './IImage';
|
||||||
* Model of available symbol to configure the application */
|
* Model of available symbol to configure the application */
|
||||||
export interface IAvailableSymbol {
|
export interface IAvailableSymbol {
|
||||||
Name: string
|
Name: string
|
||||||
XPositionReference: XPositionReference
|
|
||||||
Image: IImage
|
Image: IImage
|
||||||
Width: number
|
Width?: number
|
||||||
Height: number
|
Height?: number
|
||||||
|
XPositionReference?: XPositionReference
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { IAvailableContainer } from './IAvailableContainer';
|
import { IAvailableContainer } from './IAvailableContainer';
|
||||||
import { IAvailableSymbol } from './IAvailableSymbol';
|
import { IAvailableSymbol } from './IAvailableSymbol';
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import IProperties from './IProperties';
|
import { IContainerProperties } from './IContainerProperties';
|
||||||
|
|
||||||
export interface IContainerModel {
|
export interface IContainerModel {
|
||||||
children: IContainerModel[]
|
children: IContainerModel[]
|
||||||
parent: IContainerModel | null
|
parent: IContainerModel | null
|
||||||
properties: IProperties
|
properties: IContainerProperties
|
||||||
userData: Record<string, string | number>
|
userData: Record<string, string | number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macro for creating the interface
|
||||||
|
* Do not add methods since they will be lost during serialization
|
||||||
|
*/
|
||||||
export class ContainerModel implements IContainerModel {
|
export class ContainerModel implements IContainerModel {
|
||||||
public children: IContainerModel[];
|
public children: IContainerModel[];
|
||||||
public parent: IContainerModel | null;
|
public parent: IContainerModel | null;
|
||||||
public properties: IProperties;
|
public properties: IContainerProperties;
|
||||||
public userData: Record<string, string | number>;
|
public userData: Record<string, string | number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
parent: IContainerModel | null,
|
parent: IContainerModel | null,
|
||||||
properties: IProperties,
|
properties: IContainerProperties,
|
||||||
children: IContainerModel[] = [],
|
children: IContainerModel[] = [],
|
||||||
userData = {}) {
|
userData = {}) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { XPositionReference } from '../Enums/XPositionReference';
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
|
import { IMargin } from './IMargin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties of a container
|
* Properties of a container
|
||||||
*/
|
*/
|
||||||
export default interface IProperties {
|
export interface IContainerProperties {
|
||||||
/** id of the container */
|
/** id of the container */
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
|
/** type matching the configuration on construction */
|
||||||
|
type: string
|
||||||
|
|
||||||
/** id of the parent container (null when there is no parent) */
|
/** id of the parent container (null when there is no parent) */
|
||||||
parentId: string | null
|
parentId: string
|
||||||
|
|
||||||
|
/** id of the linked symbol ('' when there is no parent) */
|
||||||
|
linkedSymbolId: string
|
||||||
|
|
||||||
/** Text displayed in the container */
|
/** Text displayed in the container */
|
||||||
displayedText: string
|
displayedText: string
|
||||||
|
@ -20,6 +27,9 @@ export default interface IProperties {
|
||||||
/** vertical offset */
|
/** vertical offset */
|
||||||
y: number
|
y: number
|
||||||
|
|
||||||
|
/** margin */
|
||||||
|
margin: IMargin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum width (min=1)
|
* Minimum width (min=1)
|
||||||
* Allows the container to set isRigidBody to false when it gets squeezed
|
* Allows the container to set isRigidBody to false when it gets squeezed
|
||||||
|
@ -27,20 +37,25 @@ export default interface IProperties {
|
||||||
*/
|
*/
|
||||||
minWidth: number
|
minWidth: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum width
|
||||||
|
*/
|
||||||
|
maxWidth: number
|
||||||
|
|
||||||
/** width */
|
/** width */
|
||||||
width: number
|
width: number
|
||||||
|
|
||||||
/** height */
|
/** height */
|
||||||
height: number
|
height: number
|
||||||
|
|
||||||
/** true if rigid, false otherwise */
|
|
||||||
isRigidBody: boolean
|
|
||||||
|
|
||||||
/** true if anchor, false otherwise */
|
/** true if anchor, false otherwise */
|
||||||
isAnchor: boolean
|
isAnchor: boolean
|
||||||
|
|
||||||
|
/** true if flex, false otherwise */
|
||||||
|
isFlex: boolean
|
||||||
|
|
||||||
/** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */
|
/** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */
|
||||||
XPositionReference: XPositionReference
|
xPositionReference: XPositionReference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional)
|
* (optional)
|
|
@ -1,9 +1,22 @@
|
||||||
import { IContainerModel } from './IContainerModel';
|
import { IContainerModel } from './IContainerModel';
|
||||||
|
import { ISymbolModel } from './ISymbolModel';
|
||||||
|
|
||||||
export interface IHistoryState {
|
export interface IHistoryState {
|
||||||
LastAction: string
|
/** Last editor action */
|
||||||
MainContainer: IContainerModel
|
lastAction: string
|
||||||
SelectedContainer: IContainerModel | null
|
|
||||||
SelectedContainerId: string
|
/** Reference to the main container */
|
||||||
TypeCounters: Record<string, number>
|
mainContainer: IContainerModel
|
||||||
|
|
||||||
|
/** Id of the selected container */
|
||||||
|
selectedContainerId: string
|
||||||
|
|
||||||
|
/** Counter of type of container. Used for ids. */
|
||||||
|
typeCounters: Record<string, number>
|
||||||
|
|
||||||
|
/** List of symbols */
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
|
||||||
|
/** Selected symbols id */
|
||||||
|
selectedSymbolId: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
/** Model of an image with multiple source */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
/**
|
||||||
|
* Model of an image with multiple source
|
||||||
|
* It must at least have one source.
|
||||||
|
*
|
||||||
|
* If Url/Base64Image and Svg are set,
|
||||||
|
* Url/Base64Image will be shown in the menu while SVG will be drawn
|
||||||
|
*/
|
||||||
export interface IImage {
|
export interface IImage {
|
||||||
|
/** Name of the image */
|
||||||
Name: string
|
Name: string
|
||||||
Url: string
|
|
||||||
Base64Image: string
|
/** (optional) Url of the image */
|
||||||
Svg: string
|
Url?: string
|
||||||
|
|
||||||
|
/** (optional) base64 data of the image */
|
||||||
|
Base64Image?: string
|
||||||
|
|
||||||
|
/** (optional) SVG string */
|
||||||
|
Svg?: string
|
||||||
}
|
}
|
||||||
|
|
6
src/Interfaces/IMargin.ts
Normal file
6
src/Interfaces/IMargin.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface IMargin {
|
||||||
|
left?: number
|
||||||
|
bottom?: number
|
||||||
|
top?: number
|
||||||
|
right?: number
|
||||||
|
}
|
24
src/Interfaces/ISymbolModel.ts
Normal file
24
src/Interfaces/ISymbolModel.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { IAvailableSymbol } from './IAvailableSymbol';
|
||||||
|
|
||||||
|
export interface ISymbolModel {
|
||||||
|
/** Identifier */
|
||||||
|
id: string
|
||||||
|
|
||||||
|
/** Type */
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/** Configuration of the symbol */
|
||||||
|
config: IAvailableSymbol
|
||||||
|
|
||||||
|
/** Horizontal offset */
|
||||||
|
x: number
|
||||||
|
|
||||||
|
/** Width */
|
||||||
|
width: number
|
||||||
|
|
||||||
|
/** Height */
|
||||||
|
height: number
|
||||||
|
|
||||||
|
/** List of linked container id */
|
||||||
|
linkedContainers: Set<string>
|
||||||
|
}
|
|
@ -11,6 +11,10 @@
|
||||||
@apply transition-all px-2 py-6 text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
@apply transition-all px-2 py-6 text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-component-card {
|
||||||
|
@apply transition-all overflow-hidden text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
||||||
|
}
|
||||||
|
|
||||||
.elements-sidebar-row {
|
.elements-sidebar-row {
|
||||||
@apply pl-6 pr-6 pt-2 pb-2 w-full
|
@apply pl-6 pr-6 pt-2 pb-2 w-full
|
||||||
}
|
}
|
||||||
|
|
14
src/main.tsx
14
src/main.tsx
|
@ -3,8 +3,18 @@ import ReactDOM from 'react-dom/client';
|
||||||
import { App } from './Components/App/App';
|
import { App } from './Components/App/App';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
function RenderRoot(root: Element | Document): void {
|
||||||
|
ReactDOM.createRoot(root.querySelector('#root') as HTMLDivElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App root={root}/>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace SVGLayoutDesigner {
|
||||||
|
export const Render = RenderRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).SVGLayoutDesigner = SVGLayoutDesigner;
|
||||||
|
|
||||||
|
RenderRoot(document);
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { XPositionReference } from '../Enums/XPositionReference';
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
|
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
|
||||||
|
import { IAvailableSymbol } from '../Interfaces/IAvailableSymbol';
|
||||||
import { IConfiguration } from '../Interfaces/IConfiguration';
|
import { IConfiguration } from '../Interfaces/IConfiguration';
|
||||||
import { IContainerModel } from '../Interfaces/IContainerModel';
|
import { ContainerModel, IContainerModel } from '../Interfaces/IContainerModel';
|
||||||
import IProperties from '../Interfaces/IProperties';
|
import { IContainerProperties } from '../Interfaces/IContainerProperties';
|
||||||
|
import { IEditorState } from '../Interfaces/IEditorState';
|
||||||
|
import { ISymbolModel } from '../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
/// CONTAINRE DEFAULTS ///
|
/// CONTAINER DEFAULTS ///
|
||||||
|
|
||||||
export const SHOW_TEXT = true;
|
export const SHOW_TEXT = false;
|
||||||
|
export const SHOW_SELECTOR_TEXT = true;
|
||||||
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
|
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
|
||||||
export const DEFAULTCHILDTYPE_MAX_DEPTH = 10;
|
export const DEFAULTCHILDTYPE_MAX_DEPTH = 10;
|
||||||
|
|
||||||
|
@ -18,16 +22,55 @@ export const SHOW_DIMENSIONS_PER_DEPTH = true;
|
||||||
export const DIMENSION_MARGIN = 50;
|
export const DIMENSION_MARGIN = 50;
|
||||||
export const NOTCHES_LENGTH = 4;
|
export const NOTCHES_LENGTH = 4;
|
||||||
|
|
||||||
|
/// SYMBOL DEFAULTS ///
|
||||||
|
|
||||||
|
export const DEFAULT_SYMBOL_WIDTH = 32;
|
||||||
|
export const DEFAULT_SYMBOL_HEIGHT = 32;
|
||||||
|
|
||||||
/// EDITOR DEFAULTS ///
|
/// EDITOR DEFAULTS ///
|
||||||
|
|
||||||
export const ENABLE_SHORTCUTS = true;
|
export const ENABLE_SHORTCUTS = true;
|
||||||
export const MAX_HISTORY = 200;
|
export const MAX_HISTORY = 200;
|
||||||
|
export const APPLY_BEHAVIORS_ON_CHILDREN = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default editor state given the configuration
|
||||||
|
*/
|
||||||
|
export function GetDefaultEditorState(configuration: IConfiguration): IEditorState {
|
||||||
|
const mainContainer = new ContainerModel(
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
...DEFAULT_MAINCONTAINER_PROPS,
|
||||||
|
width: Number(configuration.MainContainer.Width),
|
||||||
|
height: Number(configuration.MainContainer.Height)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
configuration,
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
lastAction: '',
|
||||||
|
mainContainer: mainContainer,
|
||||||
|
selectedContainerId: mainContainer.properties.id,
|
||||||
|
typeCounters: {},
|
||||||
|
symbols: new Map(),
|
||||||
|
selectedSymbolId: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
historyCurrentStep: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default config when the API is not available
|
||||||
|
*/
|
||||||
export const DEFAULT_CONFIG: IConfiguration = {
|
export const DEFAULT_CONFIG: IConfiguration = {
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
AvailableContainers: [
|
AvailableContainers: [
|
||||||
{
|
{
|
||||||
Type: 'Container',
|
Type: 'Container',
|
||||||
Width: 75,
|
MaxWidth: 200,
|
||||||
Height: 100,
|
Height: 100,
|
||||||
Style: {
|
Style: {
|
||||||
fillOpacity: 0,
|
fillOpacity: 0,
|
||||||
|
@ -38,53 +81,92 @@ export const DEFAULT_CONFIG: IConfiguration = {
|
||||||
AvailableSymbols: [],
|
AvailableSymbols: [],
|
||||||
MainContainer: {
|
MainContainer: {
|
||||||
Type: 'Container',
|
Type: 'Container',
|
||||||
Width: 2000,
|
Width: 800,
|
||||||
Height: 100,
|
Height: 100,
|
||||||
Style: {
|
Style: {
|
||||||
fillOpacity: 0,
|
fillOpacity: 0,
|
||||||
stroke: 'black'
|
stroke: 'black'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* eslint-enable */
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
|
/**
|
||||||
|
* Default Main container properties
|
||||||
|
*/
|
||||||
|
export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: 'null',
|
type: 'container',
|
||||||
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
margin: {},
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
|
maxWidth: Number.MAX_SAFE_INTEGER,
|
||||||
width: Number(DEFAULT_CONFIG.MainContainer.Width),
|
width: Number(DEFAULT_CONFIG.MainContainer.Width),
|
||||||
height: Number(DEFAULT_CONFIG.MainContainer.Height),
|
height: Number(DEFAULT_CONFIG.MainContainer.Height),
|
||||||
isRigidBody: false,
|
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
XPositionReference: XPositionReference.Left,
|
isFlex: false,
|
||||||
|
xPositionReference: XPositionReference.Left,
|
||||||
style: {
|
style: {
|
||||||
stroke: 'black',
|
stroke: 'black',
|
||||||
fillOpacity: 0
|
fillOpacity: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GetDefaultContainerProps = (
|
/**
|
||||||
type: string,
|
* Returns the default properties of a newly created container
|
||||||
|
* @param type Type of the container
|
||||||
|
* @param typeCount index of the container
|
||||||
|
* @param parent Parent of the container
|
||||||
|
* @param x horizontal offset
|
||||||
|
* @param y vertical offset
|
||||||
|
* @param containerConfig default config of the container sent by the API
|
||||||
|
* @returns {IContainerProperties} Default properties of a newly created container
|
||||||
|
*/
|
||||||
|
export function GetDefaultContainerProps(type: string,
|
||||||
typeCount: number,
|
typeCount: number,
|
||||||
parent: IContainerModel,
|
parent: IContainerModel,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
containerConfig: IAvailableContainer
|
width: number,
|
||||||
): IProperties => ({
|
height: number,
|
||||||
|
containerConfig: IAvailableContainer): IContainerProperties {
|
||||||
|
return ({
|
||||||
id: `${type}-${typeCount}`,
|
id: `${type}-${typeCount}`,
|
||||||
|
type,
|
||||||
parentId: parent.properties.id,
|
parentId: parent.properties.id,
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: `${type}-${typeCount}`,
|
displayedText: `${type}-${typeCount}`,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: containerConfig.Width ?? containerConfig.MinWidth ?? parent.properties.width,
|
margin: containerConfig.Margin ?? {},
|
||||||
height: containerConfig.Height ?? parent.properties.height,
|
width,
|
||||||
isRigidBody: false, // set this to true to replicate Florian's project
|
height,
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
|
isFlex: containerConfig.IsFlex ?? containerConfig.Width === undefined,
|
||||||
minWidth: containerConfig.MinWidth ?? 0,
|
xPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
|
||||||
|
minWidth: containerConfig.MinWidth ?? 1,
|
||||||
|
maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER,
|
||||||
customSVG: containerConfig.CustomSVG,
|
customSVG: containerConfig.CustomSVG,
|
||||||
style: structuredClone(containerConfig.Style),
|
style: structuredClone(containerConfig.Style),
|
||||||
userData: structuredClone(containerConfig.UserData)
|
userData: structuredClone(containerConfig.UserData)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetDefaultSymbolModel(name: string,
|
||||||
|
newCounters: Record<string, number>,
|
||||||
|
type: string,
|
||||||
|
symbolConfig: IAvailableSymbol): ISymbolModel {
|
||||||
|
return {
|
||||||
|
id: `${name}-${newCounters[type]}`,
|
||||||
|
type: name,
|
||||||
|
config: structuredClone(symbolConfig),
|
||||||
|
x: 0,
|
||||||
|
width: symbolConfig.Width ?? DEFAULT_SYMBOL_WIDTH,
|
||||||
|
height: symbolConfig.Height ?? DEFAULT_SYMBOL_HEIGHT,
|
||||||
|
linkedContainers: new Set()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function * MakeIterator(root: IContainerModel): Generator<IContainerModel
|
||||||
for (let i = container.children.length - 1; i >= 0; i--) {
|
for (let i = container.children.length - 1; i >= 0; i--) {
|
||||||
const child = container.children[i];
|
const child = container.children[i];
|
||||||
if (visited.has(child)) {
|
if (visited.has(child)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
visited.add(child);
|
visited.add(child);
|
||||||
queue.push(child);
|
queue.push(child);
|
||||||
|
@ -54,7 +54,7 @@ export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAnd
|
||||||
* Returns the depth of the container
|
* Returns the depth of the container
|
||||||
* @returns The depth of the container
|
* @returns The depth of the container
|
||||||
*/
|
*/
|
||||||
export function getDepth(parent: IContainerModel): number {
|
export function GetDepth(parent: IContainerModel): number {
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
|
||||||
let current: IContainerModel | null = parent;
|
let current: IContainerModel | null = parent;
|
||||||
|
@ -70,10 +70,21 @@ export function getDepth(parent: IContainerModel): number {
|
||||||
* Returns the absolute position by iterating to the parent
|
* Returns the absolute position by iterating to the parent
|
||||||
* @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 = container.properties.x;
|
const x = container.properties.x;
|
||||||
let y = container.properties.y;
|
const y = container.properties.y;
|
||||||
let current = container.parent;
|
return CancelParentTransform(container.parent, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the hierarchic transformations to the given x, y
|
||||||
|
* @param parent Parent of the container to remove its transform
|
||||||
|
* @param x value to be restored
|
||||||
|
* @param y value to be restored
|
||||||
|
* @returns x and y such that the transformations of the parent are cancelled
|
||||||
|
*/
|
||||||
|
export function CancelParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] {
|
||||||
|
let current = parent;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
x += current.properties.x;
|
x += current.properties.x;
|
||||||
y += current.properties.y;
|
y += current.properties.y;
|
||||||
|
@ -82,7 +93,24 @@ export function getAbsolutePosition(container: IContainerModel): [number, number
|
||||||
return [x, y];
|
return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
|
/**
|
||||||
|
* Cancel the hierarchic transformations to the given x, y
|
||||||
|
* @param parent Parent of the container to remove its transform
|
||||||
|
* @param x value to be restored
|
||||||
|
* @param y value to be restored
|
||||||
|
* @returns x and y such that the transformations of the parent are cancelled
|
||||||
|
*/
|
||||||
|
export function ApplyParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] {
|
||||||
|
let current = parent;
|
||||||
|
while (current != null) {
|
||||||
|
x -= current.properties.x;
|
||||||
|
y -= current.properties.y;
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FindContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
|
||||||
const it = MakeIterator(root);
|
const it = MakeIterator(root);
|
||||||
for (const container of it) {
|
for (const container of it) {
|
||||||
if (container.properties.id === id) {
|
if (container.properties.id === id) {
|
||||||
|
@ -91,3 +119,20 @@ export function findContainerById(root: IContainerModel, id: string): IContainer
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPair<T> {
|
||||||
|
cur: T
|
||||||
|
next: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export function * Pairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> {
|
||||||
|
for (let i = 0; i < arr.length - 1; i++) {
|
||||||
|
yield { cur: arr[i], next: arr[i + 1] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function * ReversePairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> {
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
yield { cur: arr[i], next: arr[i - 1] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { findContainerById, MakeIterator } from './itertools';
|
import { FindContainerById, MakeIterator } from './itertools';
|
||||||
import { IEditorState } from '../Interfaces/IEditorState';
|
import { IEditorState } from '../Interfaces/IEditorState';
|
||||||
|
import { IHistoryState } from '../Interfaces/IHistoryState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revive the Editor state
|
* Revive the Editor state
|
||||||
|
@ -14,43 +15,49 @@ export function Revive(editorState: IEditorState): void {
|
||||||
|
|
||||||
// restore the parents and the selected container
|
// restore the parents and the selected container
|
||||||
for (const state of history) {
|
for (const state of history) {
|
||||||
if (state.MainContainer === null || state.MainContainer === undefined) {
|
ReviveState(state);
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = MakeIterator(state.MainContainer);
|
export function ReviveState(state: IHistoryState): void {
|
||||||
|
if (state.mainContainer === null || state.mainContainer === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.symbols = new Map(state.symbols);
|
||||||
|
for (const symbol of state.symbols.values()) {
|
||||||
|
symbol.linkedContainers = new Set(symbol.linkedContainers);
|
||||||
|
}
|
||||||
|
|
||||||
|
const it = MakeIterator(state.mainContainer);
|
||||||
for (const container of it) {
|
for (const container of it) {
|
||||||
const parentId = container.properties.parentId;
|
const parentId = container.properties.parentId;
|
||||||
if (parentId === null) {
|
if (parentId === null) {
|
||||||
container.parent = null;
|
container.parent = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const parent = findContainerById(state.MainContainer, parentId);
|
const parent = FindContainerById(state.mainContainer, parentId);
|
||||||
if (parent === undefined) {
|
if (parent === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
container.parent = parent;
|
container.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = findContainerById(state.MainContainer, state.SelectedContainerId);
|
|
||||||
if (selected === undefined) {
|
|
||||||
state.SelectedContainer = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
state.SelectedContainer = selected;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => {
|
export function GetCircularReplacer(): (key: any, value: object | Map<string, any> | null) => object | null | undefined {
|
||||||
return (key: any, value: object | null) => {
|
return (key: any, value: object | null) => {
|
||||||
if (key === 'parent') {
|
if (key === 'parent') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'SelectedContainer') {
|
if (key === 'Symbols') {
|
||||||
return;
|
return Array.from((value as Map<string, any>).entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'linkedContainers') {
|
||||||
|
return Array.from(value as Set<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue