Merge branch 'dev' of https://techformsa.visualstudio.com/DefaultCollection/SmartConfigurator/_git/SVGLayoutDesignerReact into dev
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
7e3ccdee99
46 changed files with 1068 additions and 181 deletions
|
@ -21,7 +21,7 @@ steps:
|
|||
path: $(pnpm_config_cache)
|
||||
displayName: Cache pnpm
|
||||
|
||||
- script: |
|
||||
- bash: |
|
||||
curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
||||
pnpm config set store-dir $(pnpm_config_cache)
|
||||
displayName: "Setup pnpm"
|
||||
|
@ -31,7 +31,8 @@ steps:
|
|||
versionSpec: '16.x'
|
||||
displayName: 'Install Node.js 16.x LTS'
|
||||
|
||||
- script: |
|
||||
- bash: |
|
||||
set -euo pipefail
|
||||
node --version
|
||||
node ./test-server/node-http.js &
|
||||
jobs
|
||||
|
@ -46,7 +47,8 @@ steps:
|
|||
versionSpec: '>=18.7.0'
|
||||
displayName: 'Install Node.js Latest'
|
||||
|
||||
- script: |
|
||||
- bash: |
|
||||
set -euo pipefail
|
||||
node --version
|
||||
node ./test-server/node-http.js &
|
||||
jobs
|
||||
|
|
|
@ -4,22 +4,19 @@ onmessage = (e) => {
|
|||
};
|
||||
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key, value) => {
|
||||
if (key === 'parent') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'SelectedContainer') {
|
||||
return;
|
||||
if (key === 'Symbols') {
|
||||
return Array.from(value.entries());
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
if (key === 'linkedContainers') {
|
||||
return Array.from(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,9 +26,10 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
|
|||
history: [{
|
||||
LastAction: '',
|
||||
MainContainer: defaultMainContainer,
|
||||
SelectedContainer: defaultMainContainer,
|
||||
SelectedContainerId: defaultMainContainer.properties.id,
|
||||
TypeCounters: {}
|
||||
TypeCounters: {},
|
||||
Symbols: new Map(),
|
||||
SelectedSymbolId: ''
|
||||
}],
|
||||
historyCurrentStep: 0
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@ 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(
|
||||
|
@ -26,6 +25,7 @@ export function NewEditor(
|
|||
|
||||
// Save the configuration and the new MainContainer
|
||||
// and default the selected container to it
|
||||
// TODO: Put this in default.ts
|
||||
const editorState: IEditorState = {
|
||||
configuration,
|
||||
history:
|
||||
|
@ -33,9 +33,10 @@ export function NewEditor(
|
|||
{
|
||||
LastAction: '',
|
||||
MainContainer,
|
||||
SelectedContainer: MainContainer,
|
||||
SelectedContainerId: MainContainer.properties.id,
|
||||
TypeCounters: {}
|
||||
TypeCounters: {},
|
||||
Symbols: new Map(),
|
||||
SelectedSymbolId: ''
|
||||
}
|
||||
],
|
||||
historyCurrentStep: 0
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { ClockIcon, CubeIcon, MapIcon } from '@heroicons/react/outline';
|
||||
import { ClockIcon, CubeIcon, LinkIcon, MapIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { BarIcon } from './BarIcon';
|
||||
|
||||
interface IBarProps {
|
||||
isSidebarOpen: boolean
|
||||
isSymbolsOpen: boolean
|
||||
isElementsSidebarOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
ToggleSidebar: () => void
|
||||
ToggleElementsSidebar: () => void
|
||||
ToggleSymbols: () => void
|
||||
ToggleTimeline: () => void
|
||||
}
|
||||
|
||||
|
@ -23,10 +24,10 @@ export const Bar: React.FC<IBarProps> = (props) => {
|
|||
<CubeIcon className='heroicon'/>
|
||||
</BarIcon>
|
||||
<BarIcon
|
||||
isActive={props.isElementsSidebarOpen}
|
||||
title='Map'
|
||||
onClick={() => props.ToggleElementsSidebar()}>
|
||||
<MapIcon className='heroicon'/>
|
||||
isActive={props.isSymbolsOpen}
|
||||
title='Symbols'
|
||||
onClick={() => props.ToggleSymbols()}>
|
||||
<LinkIcon className='heroicon'/>
|
||||
</BarIcon>
|
||||
<BarIcon
|
||||
isActive={props.isHistoryOpen}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { constraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
|
|||
* Apply the following modification to the overlapping rigid body container :
|
||||
* @param container Container to impose its position
|
||||
*/
|
||||
export function ImposePosition(container: IContainerModel): IContainerModel {
|
||||
export function ApplyAnchor(container: IContainerModel): IContainerModel {
|
||||
if (container.parent === undefined ||
|
||||
container.parent === null) {
|
||||
return container;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||
import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default';
|
||||
import { ImposePosition } from './AnchorBehaviors';
|
||||
import { RecalculatePhysics } from './RigidBodyBehaviors';
|
||||
import { ApplyAnchor } from './AnchorBehaviors';
|
||||
import { ApplyRigidBody } from './RigidBodyBehaviors';
|
||||
import { ApplySymbol } from './SymbolBehaviors';
|
||||
|
||||
/**
|
||||
* Recalculate the position of the container and its neighbors
|
||||
|
@ -9,19 +11,24 @@ import { RecalculatePhysics } from './RigidBodyBehaviors';
|
|||
* @param container Container to recalculate its positions
|
||||
* @returns Updated container
|
||||
*/
|
||||
export function ApplyBehaviors(container: IContainerModel): IContainerModel {
|
||||
export function ApplyBehaviors(container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel {
|
||||
if (container.properties.isAnchor) {
|
||||
ImposePosition(container);
|
||||
ApplyAnchor(container);
|
||||
}
|
||||
|
||||
if (container.properties.isRigidBody) {
|
||||
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);
|
||||
ApplyBehaviors(child, symbols);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ISizePointer } from '../../../Interfaces/ISizePointer';
|
|||
* @param container Container to apply its rigid body properties
|
||||
* @returns A rigid body container
|
||||
*/
|
||||
export function RecalculatePhysics(
|
||||
export function ApplyRigidBody(
|
||||
container: IContainerModel
|
||||
): IContainerModel {
|
||||
container = constraintBodyInsideParent(container);
|
||||
|
|
9
src/Components/Editor/Behaviors/SymbolBehaviors.ts
Normal file
9
src/Components/Editor/Behaviors/SymbolBehaviors.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||
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);
|
||||
return container;
|
||||
}
|
|
@ -2,19 +2,20 @@ 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 { 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';
|
||||
|
||||
/**
|
||||
* Select a container
|
||||
* @param container Selected container
|
||||
*/
|
||||
export function SelectContainer(
|
||||
container: ContainerModel,
|
||||
containerId: string,
|
||||
fullHistory: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||
|
@ -23,19 +24,13 @@ export function SelectContainer(
|
|||
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)
|
||||
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);
|
||||
|
@ -76,6 +71,8 @@ export function DeleteContainer(
|
|||
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) {
|
||||
|
@ -94,14 +91,26 @@ export function DeleteContainer(
|
|||
history.push({
|
||||
LastAction: `Delete ${containerId}`,
|
||||
MainContainer: mainContainerClone,
|
||||
SelectedContainer,
|
||||
SelectedContainerId,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||
Symbols: newSymbols,
|
||||
SelectedSymbolId: current.SelectedSymbolId
|
||||
});
|
||||
setHistory(history);
|
||||
setHistoryCurrentStep(history.length - 1);
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -114,21 +123,19 @@ export function DeleteContainer(
|
|||
*/
|
||||
export function AddContainerToSelectedContainer(
|
||||
type: string,
|
||||
selected: IContainerModel | undefined,
|
||||
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) {
|
||||
if (selected === null ||
|
||||
selected === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = current.SelectedContainer;
|
||||
const parent = selected;
|
||||
AddContainer(
|
||||
parent.children.length,
|
||||
type,
|
||||
|
@ -166,11 +173,6 @@ export function AddContainer(
|
|||
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);
|
||||
|
@ -220,7 +222,7 @@ export function AddContainer(
|
|||
}
|
||||
);
|
||||
|
||||
ApplyBehaviors(newContainer);
|
||||
ApplyBehaviors(newContainer, current.Symbols);
|
||||
|
||||
// And push it the the parent children
|
||||
if (index === parentClone.children.length) {
|
||||
|
@ -235,23 +237,15 @@ export function AddContainer(
|
|||
history.push({
|
||||
LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
|
||||
MainContainer: clone,
|
||||
SelectedContainer: parentClone,
|
||||
SelectedContainerId: parentClone.properties.id,
|
||||
TypeCounters: newCounters
|
||||
TypeCounters: newCounters,
|
||||
Symbols: structuredClone(current.Symbols),
|
||||
SelectedSymbolId: current.SelectedSymbolId
|
||||
});
|
||||
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,
|
||||
|
|
|
@ -11,6 +11,8 @@ import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations';
|
|||
import EditorEvents from '../../Events/EditorEvents';
|
||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||
import { MAX_HISTORY } from '../../utils/default';
|
||||
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './SymbolOperations';
|
||||
import { findContainerById } from '../../utils/itertools';
|
||||
|
||||
interface IEditorProps {
|
||||
configuration: IConfiguration
|
||||
|
@ -18,6 +20,15 @@ interface IEditorProps {
|
|||
historyCurrentStep: number
|
||||
}
|
||||
|
||||
export function UpdateCounters(counters: Record<string, number>, type: string): void {
|
||||
if (counters[type] === null ||
|
||||
counters[type] === undefined) {
|
||||
counters[type] = 0;
|
||||
} else {
|
||||
counters[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] =>
|
||||
history.slice(
|
||||
Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow)
|
||||
|
@ -70,13 +81,16 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
|
||||
const configuration = props.configuration;
|
||||
const current = getCurrentHistoryState(history, historyCurrentStep);
|
||||
const selected = findContainerById(current.MainContainer, current.SelectedContainerId);
|
||||
return (
|
||||
<div ref={editorRef} className="Editor font-sans h-full">
|
||||
<UI
|
||||
SelectedContainer={selected}
|
||||
current={current}
|
||||
history={history}
|
||||
historyCurrentStep={historyCurrentStep}
|
||||
AvailableContainers={configuration.AvailableContainers}
|
||||
AvailableSymbols={configuration.AvailableSymbols}
|
||||
SelectContainer={(container) => SelectContainer(
|
||||
container,
|
||||
history,
|
||||
|
@ -93,6 +107,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
)}
|
||||
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
|
||||
key, value, isStyle,
|
||||
selected,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
|
@ -100,6 +115,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
)}
|
||||
OnPropertiesSubmit={(event) => OnPropertiesSubmit(
|
||||
event,
|
||||
selected,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
|
@ -107,6 +123,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
)}
|
||||
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
|
||||
type,
|
||||
selected,
|
||||
configuration,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
|
@ -123,6 +140,35 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
AddSymbol={(type) => AddSymbol(
|
||||
type,
|
||||
configuration,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
OnSymbolPropertyChange={(key, value) => OnSymbolPropertyChange(
|
||||
key, value,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
SelectSymbol={(symbolId) => SelectSymbol(
|
||||
symbolId,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
DeleteSymbol={(symbolId) => DeleteSymbol(
|
||||
symbolId,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
)}
|
||||
SaveEditorAsJSON={() => SaveEditorAsJSON(
|
||||
history,
|
||||
historyCurrentStep,
|
||||
|
@ -134,7 +180,8 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
|||
<SVG
|
||||
width={current.MainContainer?.properties.width}
|
||||
height={current.MainContainer?.properties.height}
|
||||
selected={current.SelectedContainer}
|
||||
selected={selected}
|
||||
symbols={current.Symbols}
|
||||
>
|
||||
{ current.MainContainer }
|
||||
</SVG>
|
||||
|
|
|
@ -3,8 +3,9 @@ import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerMode
|
|||
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';
|
||||
import { restoreX } from '../../utils/svg';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
|
||||
/**
|
||||
* Handled the property change event in the properties form
|
||||
|
@ -16,6 +17,7 @@ export function OnPropertyChange(
|
|||
key: string,
|
||||
value: string | number | boolean,
|
||||
isStyle: boolean = false,
|
||||
selected: IContainerModel | undefined,
|
||||
fullHistory: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||
|
@ -24,37 +26,66 @@ export function OnPropertyChange(
|
|||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||
const current = history[history.length - 1];
|
||||
|
||||
if (current.SelectedContainer === null ||
|
||||
current.SelectedContainer === undefined) {
|
||||
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, current.SelectedContainer.properties.id);
|
||||
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!');
|
||||
}
|
||||
|
||||
const oldSymbolId = container.properties.linkedSymbolId;
|
||||
|
||||
if (isStyle) {
|
||||
(container.properties.style as any)[key] = value;
|
||||
} else {
|
||||
(container.properties as any)[key] = value;
|
||||
}
|
||||
|
||||
ApplyBehaviors(container);
|
||||
LinkSymbol(
|
||||
container.properties.id,
|
||||
oldSymbolId,
|
||||
container.properties.linkedSymbolId,
|
||||
current.Symbols
|
||||
);
|
||||
|
||||
ApplyBehaviors(container, current.Symbols);
|
||||
|
||||
history.push({
|
||||
LastAction: `Change ${key} of ${container.properties.id}`,
|
||||
MainContainer: mainContainerClone,
|
||||
SelectedContainer: container,
|
||||
SelectedContainerId: container.properties.id,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||
Symbols: structuredClone(current.Symbols),
|
||||
SelectedSymbolId: current.SelectedSymbolId
|
||||
});
|
||||
setHistory(history);
|
||||
setHistoryCurrentStep(history.length - 1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handled the property change event in the properties form
|
||||
* @param key Property name
|
||||
|
@ -63,6 +94,7 @@ export function OnPropertyChange(
|
|||
*/
|
||||
export function OnPropertiesSubmit(
|
||||
event: React.SyntheticEvent<HTMLFormElement>,
|
||||
selected: IContainerModel | undefined,
|
||||
fullHistory: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||
|
@ -72,13 +104,13 @@ export function OnPropertiesSubmit(
|
|||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||
const current = history[history.length - 1];
|
||||
|
||||
if (current.SelectedContainer === null ||
|
||||
current.SelectedContainer === undefined) {
|
||||
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, current.SelectedContainer.properties.id);
|
||||
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!');
|
||||
|
@ -110,14 +142,15 @@ export function OnPropertiesSubmit(
|
|||
}
|
||||
|
||||
// Apply the behaviors
|
||||
ApplyBehaviors(container);
|
||||
ApplyBehaviors(container, current.Symbols);
|
||||
|
||||
history.push({
|
||||
LastAction: `Change properties of ${container.properties.id}`,
|
||||
MainContainer: mainContainerClone,
|
||||
SelectedContainer: container,
|
||||
SelectedContainerId: container.properties.id,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||
Symbols: structuredClone(current.Symbols),
|
||||
SelectedSymbolId: current.SelectedSymbolId
|
||||
});
|
||||
setHistory(history);
|
||||
setHistoryCurrentStep(history.length - 1);
|
||||
|
@ -187,3 +220,4 @@ const submitRadioButtons = (
|
|||
|
||||
(container.properties as any)[property] = radiobutton.value;
|
||||
};
|
||||
|
||||
|
|
180
src/Components/Editor/SymbolOperations.ts
Normal file
180
src/Components/Editor/SymbolOperations.ts
Normal file
|
@ -0,0 +1,180 @@
|
|||
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 { DEFAULT_SYMBOL_HEIGHT, DEFAULT_SYMBOL_WIDTH } 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);
|
||||
// TODO: Put this in default.ts as GetDefaultConfig
|
||||
const newSymbol: ISymbolModel = {
|
||||
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()
|
||||
};
|
||||
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) {
|
||||
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);
|
||||
}
|
|
@ -4,16 +4,19 @@ import { fireEvent, render, screen } from '../../utils/test-utils';
|
|||
import { ElementsSidebar } from './ElementsSidebar';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
import { findContainerById } from '../../utils/itertools';
|
||||
|
||||
describe.concurrent('Elements sidebar', () => {
|
||||
it('With a MainContainer', () => {
|
||||
render(<ElementsSidebar
|
||||
symbols={new Map()}
|
||||
MainContainer={{
|
||||
children: [],
|
||||
parent: null,
|
||||
properties: {
|
||||
id: 'main',
|
||||
parentId: null,
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'main',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -28,7 +31,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
}}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={null}
|
||||
SelectedContainer={undefined}
|
||||
OnPropertyChange={() => {}}
|
||||
OnPropertiesSubmit={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
|
@ -42,12 +45,13 @@ describe.concurrent('Elements sidebar', () => {
|
|||
});
|
||||
|
||||
it('With a selected MainContainer', () => {
|
||||
const MainContainer = {
|
||||
const MainContainer: IContainerModel = {
|
||||
children: [],
|
||||
parent: null,
|
||||
properties: {
|
||||
id: 'main',
|
||||
parentId: '',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'main',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -62,6 +66,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
};
|
||||
|
||||
const { container } = render(<ElementsSidebar
|
||||
symbols={new Map()}
|
||||
MainContainer={MainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
|
@ -102,12 +107,13 @@ describe.concurrent('Elements sidebar', () => {
|
|||
|
||||
it('With multiple containers', () => {
|
||||
const children: IContainerModel[] = [];
|
||||
const MainContainer = {
|
||||
const MainContainer: IContainerModel = {
|
||||
children,
|
||||
parent: null,
|
||||
properties: {
|
||||
id: 'main',
|
||||
parentId: '',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'main',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -128,6 +134,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
properties: {
|
||||
id: 'child-1',
|
||||
parentId: 'main',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'child-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -149,6 +156,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
properties: {
|
||||
id: 'child-2',
|
||||
parentId: 'main',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'child-2',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -164,6 +172,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
);
|
||||
|
||||
render(<ElementsSidebar
|
||||
symbols={new Map()}
|
||||
MainContainer={MainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
|
@ -190,6 +199,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
properties: {
|
||||
id: 'main',
|
||||
parentId: '',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'main',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -209,6 +219,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
properties: {
|
||||
id: 'child-1',
|
||||
parentId: 'main',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'child-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -223,12 +234,13 @@ describe.concurrent('Elements sidebar', () => {
|
|||
};
|
||||
children.push(child1Model);
|
||||
|
||||
let SelectedContainer = MainContainer;
|
||||
const selectContainer = vi.fn((container: IContainerModel) => {
|
||||
SelectedContainer = container;
|
||||
let SelectedContainer: IContainerModel | undefined = MainContainer;
|
||||
const selectContainer = vi.fn((containerId: string) => {
|
||||
SelectedContainer = findContainerById(MainContainer, containerId);
|
||||
});
|
||||
|
||||
const { container, rerender } = render(<ElementsSidebar
|
||||
symbols={new Map()}
|
||||
MainContainer={MainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
|
@ -253,6 +265,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
fireEvent.click(child1);
|
||||
|
||||
rerender(<ElementsSidebar
|
||||
symbols={new Map()}
|
||||
MainContainer={MainContainer}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
|
|
|
@ -7,15 +7,17 @@ import { Menu } from '../Menu/Menu';
|
|||
import { MenuItem } from '../Menu/MenuItem';
|
||||
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
|
||||
interface IElementsSidebarProps {
|
||||
MainContainer: IContainerModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
SelectedContainer: IContainerModel | null
|
||||
SelectedContainer: IContainerModel | undefined
|
||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
SelectContainer: (container: IContainerModel) => void
|
||||
SelectContainer: (containerId: string) => void
|
||||
DeleteContainer: (containerid: string) => void
|
||||
AddContainer: (index: number, type: string, parent: string) => void
|
||||
}
|
||||
|
@ -104,7 +106,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
|||
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
|
||||
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
|
||||
onDragLeave={(event) => handleDragLeave(event)}
|
||||
onClick={() => props.SelectContainer(container)}
|
||||
onClick={() => props.SelectContainer(container.properties.id)}
|
||||
>
|
||||
{ text }
|
||||
</button>
|
||||
|
@ -140,6 +142,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
|||
</Menu>
|
||||
<Properties
|
||||
properties={props.SelectedContainer?.properties}
|
||||
symbols={props.symbols}
|
||||
onChange={props.OnPropertyChange}
|
||||
onSubmit={props.OnPropertiesSubmit}
|
||||
/>
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import { restoreX, transformX } from '../../utils/svg';
|
||||
import { InputGroup } from '../InputGroup/InputGroup';
|
||||
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||
import { restoreX, transformX } from '../SVG/Elements/Container';
|
||||
import { Select } from '../Select/Select';
|
||||
|
||||
interface IDynamicFormProps {
|
||||
properties: IProperties
|
||||
properties: IContainerProperties
|
||||
symbols: Map<string, ISymbolModel>
|
||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
}
|
||||
|
||||
const getCSSInputs = (
|
||||
properties: IProperties,
|
||||
properties: IContainerProperties,
|
||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
): JSX.Element[] => {
|
||||
const groupInput: JSX.Element[] = [];
|
||||
|
@ -67,6 +70,7 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
|
|||
labelClassName=''
|
||||
inputClassName=''
|
||||
type='number'
|
||||
isDisabled={props.properties.linkedSymbolId !== ''}
|
||||
value={transformX(props.properties.x, props.properties.width, props.properties.XPositionReference).toString()}
|
||||
onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.properties.width, props.properties.XPositionReference))}
|
||||
/>
|
||||
|
@ -160,9 +164,22 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
|
|||
]}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default DynamicForm;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import DynamicForm from './DynamicForm';
|
||||
import StaticForm from './StaticForm';
|
||||
|
||||
interface IFormProps {
|
||||
properties: IProperties
|
||||
properties: IContainerProperties
|
||||
symbols: Map<string, ISymbolModel>
|
||||
isDynamicInput: boolean
|
||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
|
@ -14,6 +16,7 @@ export const Form: React.FunctionComponent<IFormProps> = (props) => {
|
|||
if (props.isDynamicInput) {
|
||||
return <DynamicForm
|
||||
properties={props.properties}
|
||||
symbols={props.symbols}
|
||||
onChange={props.onChange}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
|||
import * as React from 'react';
|
||||
import { expect, describe, it, vi } from 'vitest';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||
import { Properties } from './Properties';
|
||||
|
||||
describe.concurrent('Properties', () => {
|
||||
|
@ -11,6 +11,7 @@ describe.concurrent('Properties', () => {
|
|||
properties={undefined}
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
symbols={new Map()}
|
||||
/>);
|
||||
|
||||
expect(screen.queryByText('id')).toBeNull();
|
||||
|
@ -20,9 +21,10 @@ describe.concurrent('Properties', () => {
|
|||
});
|
||||
|
||||
it('Some properties, change values with dynamic input', () => {
|
||||
const prop: IProperties = {
|
||||
const prop: IContainerProperties = {
|
||||
id: 'stuff',
|
||||
parentId: 'parentId',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'stuff',
|
||||
x: 1,
|
||||
y: 1,
|
||||
|
@ -42,6 +44,7 @@ describe.concurrent('Properties', () => {
|
|||
properties={prop}
|
||||
onChange={handleChange}
|
||||
onSubmit={() => {}}
|
||||
symbols={new Map()}
|
||||
/>);
|
||||
|
||||
expect(screen.queryByText('id')).toBeDefined();
|
||||
|
@ -76,6 +79,7 @@ describe.concurrent('Properties', () => {
|
|||
properties={Object.assign({}, prop)}
|
||||
onChange={handleChange}
|
||||
onSubmit={() => {}}
|
||||
symbols={new Map()}
|
||||
/>);
|
||||
|
||||
propertyId = container.querySelector('#id');
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
||||
import { Form } from './Form';
|
||||
|
||||
interface IPropertiesProps {
|
||||
properties?: IProperties
|
||||
properties?: IContainerProperties
|
||||
symbols: Map<string, ISymbolModel>
|
||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
}
|
||||
|
@ -27,6 +29,7 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
|||
/>
|
||||
<Form
|
||||
properties={props.properties}
|
||||
symbols={props.symbols}
|
||||
isDynamicInput={isDynamicInput}
|
||||
onChange={props.onChange}
|
||||
onSubmit={props.onSubmit}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { MenuAlt2Icon, MenuIcon, MenuAlt3Icon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||
import IProperties from '../../Interfaces/IProperties';
|
||||
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||
import { transformX } from '../../utils/svg';
|
||||
import { InputGroup } from '../InputGroup/InputGroup';
|
||||
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||
import { transformX } from '../SVG/Elements/Container';
|
||||
|
||||
interface IStaticFormProps {
|
||||
properties: IProperties
|
||||
properties: IContainerProperties
|
||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
}
|
||||
|
||||
const getCSSInputs = (properties: IProperties): JSX.Element[] => {
|
||||
const getCSSInputs = (properties: IContainerProperties): JSX.Element[] => {
|
||||
const groupInput: JSX.Element[] = [];
|
||||
for (const key in properties.style) {
|
||||
groupInput.push(<InputGroup
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import { Interweave, Node } from 'interweave';
|
||||
import { XPositionReference } from '../../../Enums/XPositionReference';
|
||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
|
||||
import { getDepth } from '../../../utils/itertools';
|
||||
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 {
|
||||
model: IContainerModel
|
||||
|
@ -45,7 +46,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
|||
</rect>);
|
||||
// Dimension props
|
||||
const depth = getDepth(props.model);
|
||||
const dimensionMargin = DIMENSION_MARGIN * (depth + 1);
|
||||
const dimensionMargin = DIMENSION_MARGIN * depth;
|
||||
const id = `dim-${props.model.properties.id}`;
|
||||
const xStart: number = 0;
|
||||
const xEnd = props.model.properties.width;
|
||||
|
@ -132,27 +133,7 @@ function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: numb
|
|||
return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren };
|
||||
}
|
||||
|
||||
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||
let transformedX = x;
|
||||
if (xPositionReference === XPositionReference.Center) {
|
||||
transformedX += width / 2;
|
||||
} else if (xPositionReference === XPositionReference.Right) {
|
||||
transformedX += width;
|
||||
}
|
||||
return transformedX;
|
||||
}
|
||||
|
||||
export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||
let transformedX = x;
|
||||
if (xPositionReference === XPositionReference.Center) {
|
||||
transformedX -= width / 2;
|
||||
} else if (xPositionReference === XPositionReference.Right) {
|
||||
transformedX -= width;
|
||||
}
|
||||
return transformedX;
|
||||
}
|
||||
|
||||
function CreateReactCustomSVG(customSVG: string, props: IProperties): React.ReactNode {
|
||||
function CreateReactCustomSVG(customSVG: string, props: IContainerProperties): React.ReactNode {
|
||||
return <Interweave
|
||||
tagName='g'
|
||||
disableLineBreaks={true}
|
||||
|
@ -162,7 +143,7 @@ function CreateReactCustomSVG(customSVG: string, props: IProperties): React.Reac
|
|||
/>;
|
||||
}
|
||||
|
||||
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'];
|
||||
if (supportedTags.includes(node.tagName.toLowerCase())) {
|
||||
const attributes: {[att: string]: string | object | null} = {};
|
||||
|
@ -207,7 +188,3 @@ function transform(node: HTMLElement, children: Node[], props: IProperties): Rea
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function camelize(str: string): any {
|
||||
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
||||
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
||||
import { transformX } from './Container';
|
||||
import { transformX } from '../../../utils/svg';
|
||||
import { Dimension } from './Dimension';
|
||||
|
||||
interface IDimensionLayerProps {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
|||
import { getAbsolutePosition } from '../../../utils/itertools';
|
||||
|
||||
interface ISelectorProps {
|
||||
selected: IContainerModel | null
|
||||
selected?: IContainerModel
|
||||
}
|
||||
|
||||
export const Selector: React.FC<ISelectorProps> = (props) => {
|
||||
|
|
39
src/Components/SVG/Elements/Symbol.tsx
Normal file
39
src/Components/SVG/Elements/Symbol.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
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 const Symbol: React.FC<ISymbolProps> = (props) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
23
src/Components/SVG/Elements/SymbolLayer.tsx
Normal file
23
src/Components/SVG/Elements/SymbolLayer.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react';
|
||||
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||
import { Symbol } from './Symbol';
|
||||
|
||||
interface ISymbolLayerProps {
|
||||
symbols: Map<string, ISymbolModel>
|
||||
}
|
||||
|
||||
export const SymbolLayer: React.FC<ISymbolLayerProps> = (props) => {
|
||||
const symbols: JSX.Element[] = [];
|
||||
props.symbols.forEach((symbol) => {
|
||||
symbols.push(
|
||||
<Symbol key={`symbol-${symbol.id}`} model={symbol} />
|
||||
);
|
||||
});
|
||||
return (
|
||||
<g>
|
||||
{
|
||||
symbols
|
||||
}
|
||||
</g>
|
||||
);
|
||||
};
|
|
@ -4,15 +4,17 @@ import { Container } from './Elements/Container';
|
|||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { Selector } from './Elements/Selector';
|
||||
import { BAR_WIDTH } from '../Bar/Bar';
|
||||
import { DimensionLayer } from './Elements/DimensionLayer';
|
||||
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
||||
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
||||
import { SymbolLayer } from './Elements/SymbolLayer';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
|
||||
interface ISVGProps {
|
||||
width: number
|
||||
height: number
|
||||
children: ContainerModel | ContainerModel[] | null
|
||||
selected: ContainerModel | null
|
||||
selected?: ContainerModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
}
|
||||
|
||||
interface Viewer {
|
||||
|
@ -81,6 +83,7 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
|
|||
? <DepthDimensionLayer roots={props.children}/>
|
||||
: null
|
||||
}
|
||||
<SymbolLayer symbols={props.symbols} />
|
||||
<Selector selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
|
||||
</svg>
|
||||
</UncontrolledReactSVGPanZoom>
|
||||
|
|
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 const Select: React.FC<ISelectProps> = (props) => {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
56
src/Components/SymbolProperties/DynamicForm.tsx
Normal file
56
src/Components/SymbolProperties/DynamicForm.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import * as React from 'react';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import { restoreX, transformX } from '../../utils/svg';
|
||||
import { InputGroup } from '../InputGroup/InputGroup';
|
||||
|
||||
interface IDynamicFormProps {
|
||||
symbol: ISymbolModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
onChange: (key: string, value: string | number | boolean) => void
|
||||
}
|
||||
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.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>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicForm;
|
17
src/Components/SymbolProperties/Form.tsx
Normal file
17
src/Components/SymbolProperties/Form.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import DynamicForm from './DynamicForm';
|
||||
|
||||
interface IFormProps {
|
||||
symbol: ISymbolModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
}
|
||||
|
||||
export const Form: React.FunctionComponent<IFormProps> = (props) => {
|
||||
return <DynamicForm
|
||||
symbol={props.symbol}
|
||||
symbols={props.symbols}
|
||||
onChange={props.onChange}
|
||||
/>;
|
||||
};
|
27
src/Components/SymbolProperties/SymbolProperties.tsx
Normal file
27
src/Components/SymbolProperties/SymbolProperties.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
||||
import { Form } from './Form';
|
||||
|
||||
interface ISymbolPropertiesProps {
|
||||
symbol?: ISymbolModel
|
||||
symbols: Map<string, ISymbolModel>
|
||||
onChange: (key: string, value: string | number | boolean) => void
|
||||
}
|
||||
|
||||
export const SymbolProperties: React.FC<ISymbolPropertiesProps> = (props: ISymbolPropertiesProps) => {
|
||||
if (props.symbol === undefined) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
||||
<Form
|
||||
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 const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
|
||||
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
|
||||
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
|
||||
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>
|
||||
);
|
||||
};
|
34
src/Components/SymbolsSidebar/MouseEventHandlers.ts
Normal file
34
src/Components/SymbolsSidebar/MouseEventHandlers.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
|
||||
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('');
|
||||
}
|
137
src/Components/SymbolsSidebar/SymbolsSidebar.tsx
Normal file
137
src/Components/SymbolsSidebar/SymbolsSidebar.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
import * as React from 'react';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
import { handleLeftClick, handleRightClick } 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 const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (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
|
||||
React.useEffect(() => {
|
||||
const onContextMenu = (event: MouseEvent): void => handleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickSymbolId,
|
||||
setContextMenuPosition
|
||||
);
|
||||
|
||||
const onLeftClick = (): void => handleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickSymbolId
|
||||
);
|
||||
|
||||
elementRef.current?.addEventListener(
|
||||
'contextmenu',
|
||||
onContextMenu
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'click',
|
||||
onLeftClick
|
||||
);
|
||||
|
||||
return () => {
|
||||
elementRef.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
onContextMenu
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
'click',
|
||||
onLeftClick
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
// Render
|
||||
let isOpenClasses = '-right-64';
|
||||
if (props.isOpen) {
|
||||
isOpenClasses = props.isHistoryOpen
|
||||
? 'right-64'
|
||||
: 'right-0';
|
||||
}
|
||||
|
||||
const containers = [...props.symbols.values()];
|
||||
const 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
|
||||
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>
|
||||
);
|
||||
};
|
|
@ -3,38 +3,55 @@ import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
|
|||
import { Sidebar } from '../Sidebar/Sidebar';
|
||||
import { History } from '../History/History';
|
||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||
import { Bar } from '../Bar/Bar';
|
||||
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||
import { Symbols } from '../Symbols/Symbols';
|
||||
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
|
||||
|
||||
interface IUIProps {
|
||||
SelectedContainer: IContainerModel | undefined
|
||||
current: IHistoryState
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
AvailableContainers: IAvailableContainer[]
|
||||
SelectContainer: (container: ContainerModel) => void
|
||||
AvailableSymbols: IAvailableSymbol[]
|
||||
SelectContainer: (containerId: string) => void
|
||||
DeleteContainer: (containerId: string) => void
|
||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||
AddContainerToSelectedContainer: (type: string) => void
|
||||
AddContainer: (index: number, type: string, parentId: string) => void
|
||||
AddSymbol: (type: string) => void
|
||||
OnSymbolPropertyChange: (key: string, value: string | number | boolean) => void
|
||||
SelectSymbol: (symbolId: string) => void
|
||||
DeleteSymbol: (symbolId: string) => void
|
||||
SaveEditorAsJSON: () => void
|
||||
SaveEditorAsSVG: () => void
|
||||
LoadState: (move: number) => void
|
||||
}
|
||||
|
||||
function CloseOtherSidebars(
|
||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
): void {
|
||||
setIsSidebarOpen(false);
|
||||
setIsSymbolsOpen(false);
|
||||
}
|
||||
|
||||
export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||
const [isElementsSidebarOpen, setIsElementsSidebarOpen] = React.useState(false);
|
||||
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||
|
||||
let buttonRightOffsetClasses = 'right-12';
|
||||
if (isElementsSidebarOpen || isHistoryOpen) {
|
||||
if (isSidebarOpen || isHistoryOpen) {
|
||||
buttonRightOffsetClasses = 'right-72';
|
||||
}
|
||||
if (isHistoryOpen && isElementsSidebarOpen) {
|
||||
if (isHistoryOpen && isSidebarOpen) {
|
||||
buttonRightOffsetClasses = 'right-[544px]';
|
||||
}
|
||||
|
||||
|
@ -42,22 +59,35 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
|||
<>
|
||||
<Bar
|
||||
isSidebarOpen={isSidebarOpen}
|
||||
isElementsSidebarOpen={isElementsSidebarOpen}
|
||||
isSymbolsOpen={isSymbolsOpen}
|
||||
isElementsSidebarOpen={isSidebarOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
ToggleElementsSidebar={() => setIsElementsSidebarOpen(!isElementsSidebarOpen)}
|
||||
ToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
ToggleSidebar={() => {
|
||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
}}
|
||||
ToggleSymbols={() => {
|
||||
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
||||
setIsSymbolsOpen(!isSymbolsOpen);
|
||||
}}
|
||||
ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||
/>
|
||||
|
||||
<Sidebar
|
||||
componentOptions={props.AvailableContainers}
|
||||
isOpen={isSidebarOpen}
|
||||
buttonOnClick={(type: string) => props.AddContainerToSelectedContainer(type)}
|
||||
buttonOnClick={props.AddContainerToSelectedContainer}
|
||||
/>
|
||||
<Symbols
|
||||
componentOptions={props.AvailableSymbols}
|
||||
isOpen={isSymbolsOpen}
|
||||
buttonOnClick={props.AddSymbol}
|
||||
/>
|
||||
<ElementsSidebar
|
||||
MainContainer={props.current.MainContainer}
|
||||
SelectedContainer={props.current.SelectedContainer}
|
||||
isOpen={isElementsSidebarOpen}
|
||||
symbols={props.current.Symbols}
|
||||
SelectedContainer={props.SelectedContainer}
|
||||
isOpen={isSidebarOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
OnPropertyChange={props.OnPropertyChange}
|
||||
OnPropertiesSubmit={props.OnPropertiesSubmit}
|
||||
|
@ -65,6 +95,15 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
|||
DeleteContainer={props.DeleteContainer}
|
||||
AddContainer={props.AddContainer}
|
||||
/>
|
||||
<SymbolsSidebar
|
||||
SelectedSymbolId={props.current.SelectedSymbolId}
|
||||
symbols={props.current.Symbols}
|
||||
isOpen={isSymbolsOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
OnPropertyChange={props.OnSymbolPropertyChange}
|
||||
SelectSymbol={props.SelectSymbol}
|
||||
DeleteSymbol={props.DeleteSymbol}
|
||||
/>
|
||||
<History
|
||||
history={props.history}
|
||||
historyCurrentStep={props.historyCurrentStep}
|
||||
|
|
|
@ -5,8 +5,8 @@ import { IImage } from './IImage';
|
|||
* Model of available symbol to configure the application */
|
||||
export interface IAvailableSymbol {
|
||||
Name: string
|
||||
XPositionReference: XPositionReference
|
||||
Image: IImage
|
||||
Width: number
|
||||
Height: number
|
||||
Width?: number
|
||||
Height?: number
|
||||
XPositionReference?: XPositionReference
|
||||
}
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
import IProperties from './IProperties';
|
||||
import IContainerProperties from './IContainerProperties';
|
||||
|
||||
export interface IContainerModel {
|
||||
children: IContainerModel[]
|
||||
parent: IContainerModel | null
|
||||
properties: IProperties
|
||||
properties: IContainerProperties
|
||||
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 {
|
||||
public children: IContainerModel[];
|
||||
public parent: IContainerModel | null;
|
||||
public properties: IProperties;
|
||||
public properties: IContainerProperties;
|
||||
public userData: Record<string, string | number>;
|
||||
|
||||
constructor(
|
||||
parent: IContainerModel | null,
|
||||
properties: IProperties,
|
||||
properties: IContainerProperties,
|
||||
children: IContainerModel[] = [],
|
||||
userData = {}) {
|
||||
this.parent = parent;
|
||||
|
|
|
@ -4,13 +4,17 @@ import { XPositionReference } from '../Enums/XPositionReference';
|
|||
/**
|
||||
* Properties of a container
|
||||
*/
|
||||
export default interface IProperties {
|
||||
export default interface IContainerProperties {
|
||||
/** id of the container */
|
||||
id: string
|
||||
|
||||
// TODO: replace null by empty string
|
||||
/** id of the parent container (null when there is no parent) */
|
||||
parentId: string | null
|
||||
|
||||
/** id of the linked symbol ('' when there is no parent) */
|
||||
linkedSymbolId: string
|
||||
|
||||
/** Text displayed in the container */
|
||||
displayedText: string
|
||||
|
|
@ -1,9 +1,22 @@
|
|||
import { IContainerModel } from './IContainerModel';
|
||||
import { ISymbolModel } from './ISymbolModel';
|
||||
|
||||
export interface IHistoryState {
|
||||
/** Last editor action */
|
||||
LastAction: string
|
||||
|
||||
/** Reference to the main container */
|
||||
MainContainer: IContainerModel
|
||||
SelectedContainer: IContainerModel | null
|
||||
|
||||
/** 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,20 @@
|
|||
/** Model of an image with multiple source */
|
||||
/**
|
||||
* 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 {
|
||||
/** Name of the image */
|
||||
Name: string
|
||||
Url: string
|
||||
Base64Image: string
|
||||
Svg: string
|
||||
|
||||
/** (optional) Url of the image */
|
||||
Url?: string
|
||||
|
||||
/** (optional) base64 data of the image */
|
||||
Base64Image?: string
|
||||
|
||||
/** (optional) SVG string */
|
||||
Svg?: string
|
||||
}
|
||||
|
|
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
|
||||
}
|
||||
|
||||
.sidebar-component-card {
|
||||
@apply transition-all overflow-hidden text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
||||
}
|
||||
|
||||
.elements-sidebar-row {
|
||||
@apply pl-6 pr-6 pt-2 pb-2 w-full
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { XPositionReference } from '../Enums/XPositionReference';
|
|||
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
|
||||
import { IConfiguration } from '../Interfaces/IConfiguration';
|
||||
import { IContainerModel } from '../Interfaces/IContainerModel';
|
||||
import IProperties from '../Interfaces/IProperties';
|
||||
import IContainerProperties from '../Interfaces/IContainerProperties';
|
||||
|
||||
/// CONTAINER DEFAULTS ///
|
||||
|
||||
|
@ -18,6 +18,11 @@ export const SHOW_DIMENSIONS_PER_DEPTH = true;
|
|||
export const DIMENSION_MARGIN = 50;
|
||||
export const NOTCHES_LENGTH = 4;
|
||||
|
||||
/// SYMBOL DEFAULTS ///
|
||||
|
||||
export const DEFAULT_SYMBOL_WIDTH = 32;
|
||||
export const DEFAULT_SYMBOL_HEIGHT = 32;
|
||||
|
||||
/// EDITOR DEFAULTS ///
|
||||
|
||||
export const ENABLE_SHORTCUTS = true;
|
||||
|
@ -48,9 +53,10 @@ export const DEFAULT_CONFIG: IConfiguration = {
|
|||
}
|
||||
};
|
||||
|
||||
export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
|
||||
export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
linkedSymbolId: '',
|
||||
displayedText: 'main',
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -73,9 +79,10 @@ export const GetDefaultContainerProps = (
|
|||
x: number,
|
||||
y: number,
|
||||
containerConfig: IAvailableContainer
|
||||
): IProperties => ({
|
||||
): IContainerProperties => ({
|
||||
id: `${type}-${typeCount}`,
|
||||
parentId: parent.properties.id,
|
||||
linkedSymbolId: '',
|
||||
displayedText: `${type}-${typeCount}`,
|
||||
x,
|
||||
y,
|
||||
|
|
|
@ -18,6 +18,11 @@ export function Revive(editorState: IEditorState): void {
|
|||
continue;
|
||||
}
|
||||
|
||||
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) {
|
||||
const parentId = container.properties.parentId;
|
||||
|
@ -31,24 +36,21 @@ export function Revive(editorState: IEditorState): void {
|
|||
}
|
||||
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 const getCircularReplacer = (): (key: any, value: object | Map<string, any> | null) => object | null | undefined => {
|
||||
return (key: any, value: object | null) => {
|
||||
if (key === 'parent') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'SelectedContainer') {
|
||||
return;
|
||||
if (key === 'Symbols') {
|
||||
return Array.from((value as Map<string, any>).entries());
|
||||
}
|
||||
|
||||
if (key === 'linkedContainers') {
|
||||
return Array.from(value as Set<string>);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
|
|
@ -4,3 +4,7 @@ export function truncateString(str: string, num: number): string {
|
|||
}
|
||||
return `${str.slice(0, num)}...`;
|
||||
}
|
||||
|
||||
export function camelize(str: string): any {
|
||||
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
|
||||
}
|
||||
|
|
21
src/utils/svg.ts
Normal file
21
src/utils/svg.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { XPositionReference } from '../Enums/XPositionReference';
|
||||
|
||||
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||
let transformedX = x;
|
||||
if (xPositionReference === XPositionReference.Center) {
|
||||
transformedX += width / 2;
|
||||
} else if (xPositionReference === XPositionReference.Right) {
|
||||
transformedX += width;
|
||||
}
|
||||
return transformedX;
|
||||
}
|
||||
|
||||
export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||
let transformedX = x;
|
||||
if (xPositionReference === XPositionReference.Center) {
|
||||
transformedX -= width / 2;
|
||||
} else if (xPositionReference === XPositionReference.Right) {
|
||||
transformedX -= width;
|
||||
}
|
||||
return transformedX;
|
||||
}
|
|
@ -112,7 +112,8 @@ const GetSVGLayoutConfiguration = () => {
|
|||
],
|
||||
AvailableSymbols: [
|
||||
{
|
||||
Height: 0,
|
||||
Width: 32,
|
||||
Height: 32,
|
||||
Image: {
|
||||
Base64Image: null,
|
||||
Name: null,
|
||||
|
@ -120,11 +121,11 @@ const GetSVGLayoutConfiguration = () => {
|
|||
Url: 'https://www.manutan.fr/img/S/GRP/ST/AIG3930272.jpg'
|
||||
},
|
||||
Name: 'Poteau structure',
|
||||
Width: 0,
|
||||
XPositionReference: 1
|
||||
},
|
||||
{
|
||||
Height: 0,
|
||||
Width: 32,
|
||||
Height: 32,
|
||||
Image: {
|
||||
Base64Image: null,
|
||||
Name: null,
|
||||
|
@ -132,7 +133,6 @@ const GetSVGLayoutConfiguration = () => {
|
|||
Url: 'https://e7.pngegg.com/pngimages/647/127/png-clipart-svg-working-group-information-world-wide-web-internet-structure.png'
|
||||
},
|
||||
Name: 'Joint de structure',
|
||||
Width: 0,
|
||||
XPositionReference: 0
|
||||
}
|
||||
],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue