Separated the model and the Container entity in order to remove any mutation operation
This commit is contained in:
parent
964d9a0e57
commit
e2a099457c
7 changed files with 206 additions and 178 deletions
|
@ -1,26 +1,26 @@
|
|||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Properties } from '../Properties/Properties';
|
||||
import { Container } from '../SVG/Elements/Container';
|
||||
import { IContainerModel, getDepth, MakeIterator } from '../SVG/Elements/ContainerModel';
|
||||
|
||||
interface IElementsSidebarProps {
|
||||
MainContainer: Container | null,
|
||||
MainContainer: IContainerModel | null,
|
||||
isOpen: boolean,
|
||||
SelectedContainer: Container | null,
|
||||
SelectedContainer: IContainerModel | null,
|
||||
onClick: () => void,
|
||||
onPropertyChange: (key: string, value: string) => void,
|
||||
selectContainer: (container: Container) => void
|
||||
selectContainer: (container: IContainerModel) => void
|
||||
}
|
||||
|
||||
export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
||||
public iterateChilds(handleContainer: (container: Container) => void): React.ReactNode {
|
||||
public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode {
|
||||
if (!this.props.MainContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const it = this.props.MainContainer.MakeIterator();
|
||||
const it = MakeIterator(this.props.MainContainer);
|
||||
for (const container of it) {
|
||||
handleContainer(container);
|
||||
handleContainer(container as IContainerModel);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,29 +28,29 @@ export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
|||
const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64';
|
||||
|
||||
const containerRows: React.ReactNode[] = [];
|
||||
this.iterateChilds((container: Container) => {
|
||||
const depth: number = container.getDepth();
|
||||
const key = container.props.properties.id.toString();
|
||||
this.iterateChilds((container: IContainerModel) => {
|
||||
const depth: number = getDepth(container);
|
||||
const key = container.properties.id.toString();
|
||||
const text = '|\t'.repeat(depth) + key;
|
||||
const selectedClass: string = this.props.SelectedContainer !== null &&
|
||||
this.props.SelectedContainer.props.properties.id === container.props.properties.id
|
||||
this.props.SelectedContainer.properties.id === container.properties.id
|
||||
? 'bg-blue-500 hover:bg-blue-600'
|
||||
: 'bg-slate-400 hover:bg-slate-600';
|
||||
containerRows.push(
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 1.2 }}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.150
|
||||
}}
|
||||
className={
|
||||
`w-full elements-sidebar-row whitespace-pre
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 1.2 }}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.150
|
||||
}}
|
||||
className={
|
||||
`w-full elements-sidebar-row whitespace-pre
|
||||
text-left text-sm font-medium transition-all ${selectedClass}`
|
||||
}
|
||||
key={key}
|
||||
onClick={() => this.props.selectContainer(container)}>
|
||||
}
|
||||
key={key}
|
||||
onClick={() => this.props.selectContainer(container)}>
|
||||
{ text }
|
||||
</motion.button>
|
||||
);
|
||||
|
@ -67,7 +67,7 @@ export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
|
|||
<div className='overflow-y-auto overflow-x-hidden text-slate-200 flex-grow divide-y divide-solid divide-slate-500'>
|
||||
{ containerRows }
|
||||
</div>
|
||||
<Properties properties={this.props.SelectedContainer?.GetProperties()} onChange={this.props.onPropertyChange}></Properties>
|
||||
<Properties properties={this.props.SelectedContainer?.properties} onChange={this.props.onPropertyChange}></Properties>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,92 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import Properties from '../../../Interfaces/Properties';
|
||||
import { getDepth, IContainerModel } from './ContainerModel';
|
||||
import { Dimension } from './Dimension';
|
||||
|
||||
interface IContainerProps {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
parent: Container | null,
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
children: Container[],
|
||||
properties: Properties,
|
||||
userData?: Record<string, string | number>
|
||||
export interface IContainerProps {
|
||||
model: IContainerModel
|
||||
}
|
||||
|
||||
const GAP = 50;
|
||||
|
||||
export class Container extends React.Component<IContainerProps> {
|
||||
/**
|
||||
* Returns A copy of the properties of the Container
|
||||
* @returns A copy of the properties of the Container
|
||||
*/
|
||||
public GetProperties(): Properties {
|
||||
const properties : Properties = {
|
||||
...this.props.properties
|
||||
};
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Generator iterating of over the children depth-first
|
||||
*/
|
||||
public * MakeIterator(): Generator<Container, void, unknown> {
|
||||
const queue: Container[] = [this];
|
||||
const visited = new Set<Container>(queue);
|
||||
while (queue.length > 0) {
|
||||
const container = queue.pop() as Container;
|
||||
|
||||
yield container;
|
||||
|
||||
// if this reverse() gets costly, replace it by a simple for
|
||||
container.props.children.reverse().forEach((child) => {
|
||||
if (visited.has(child)) {
|
||||
return;
|
||||
}
|
||||
visited.add(child);
|
||||
queue.push(child);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth of the container
|
||||
* @returns The depth of the container
|
||||
*/
|
||||
public getDepth() {
|
||||
let depth = 0;
|
||||
|
||||
let current: Container | null = this.props.parent;
|
||||
while (current != null) {
|
||||
depth++;
|
||||
current = current.props.parent;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute position by iterating to the parent
|
||||
* @returns The absolute position of the container
|
||||
*/
|
||||
public getAbsolutePosition(): [number, number] {
|
||||
let x = Number(this.props.properties.x);
|
||||
let y = Number(this.props.properties.y);
|
||||
let current = this.props.parent;
|
||||
while (current != null) {
|
||||
x += Number(current.props.properties.x);
|
||||
y += Number(current.props.properties.y);
|
||||
current = current.props.parent;
|
||||
}
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the container
|
||||
* @returns Render the container
|
||||
*/
|
||||
public render(): React.ReactNode {
|
||||
const containersElements = this.props.children.map(child => child.render());
|
||||
const xText = Number(this.props.properties.width) / 2;
|
||||
const yText = Number(this.props.properties.height) / 2;
|
||||
const containersElements = this.props.model.children.map(child => new Container({ model: child } as IContainerProps).render());
|
||||
const xText = Number(this.props.model.properties.width) / 2;
|
||||
const yText = Number(this.props.model.properties.height) / 2;
|
||||
|
||||
// g style
|
||||
const defaultStyle = {
|
||||
|
@ -98,7 +28,7 @@ export class Container extends React.Component<IContainerProps> {
|
|||
// Rect style
|
||||
const style = Object.assign(
|
||||
JSON.parse(JSON.stringify(defaultStyle)),
|
||||
this.props.properties
|
||||
this.props.model.properties
|
||||
);
|
||||
style.x = 0;
|
||||
style.y = 0;
|
||||
|
@ -106,18 +36,18 @@ export class Container extends React.Component<IContainerProps> {
|
|||
delete style.width;
|
||||
|
||||
// Dimension props
|
||||
const id = `dim-${this.props.properties.id}`;
|
||||
const id = `dim-${this.props.model.properties.id}`;
|
||||
const xStart: number = 0;
|
||||
const xEnd = Number(this.props.properties.width);
|
||||
const y = -(GAP * (this.getDepth() + 1));
|
||||
const xEnd = Number(this.props.model.properties.width);
|
||||
const y = -(GAP * (getDepth(this.props.model) + 1));
|
||||
const strokeWidth = 1;
|
||||
const text = (this.props.properties.width ?? 0).toString();
|
||||
const text = (this.props.model.properties.width ?? 0).toString();
|
||||
|
||||
return (
|
||||
<g
|
||||
style={defaultStyle}
|
||||
transform={`translate(${this.props.properties.x}, ${this.props.properties.y})`}
|
||||
key={`container-${this.props.properties.id}`}
|
||||
transform={`translate(${this.props.model.properties.x}, ${this.props.model.properties.y})`}
|
||||
key={`container-${this.props.model.properties.id}`}
|
||||
>
|
||||
<Dimension
|
||||
id={id}
|
||||
|
@ -128,8 +58,8 @@ export class Container extends React.Component<IContainerProps> {
|
|||
text={text}
|
||||
/>
|
||||
<rect
|
||||
width={this.props.properties.width}
|
||||
height={this.props.properties.height}
|
||||
width={this.props.model.properties.width}
|
||||
height={this.props.model.properties.height}
|
||||
style={style}
|
||||
>
|
||||
</rect>
|
||||
|
@ -137,7 +67,7 @@ export class Container extends React.Component<IContainerProps> {
|
|||
x={xText}
|
||||
y={yText}
|
||||
>
|
||||
{this.props.properties.id}
|
||||
{this.props.model.properties.id}
|
||||
</text>
|
||||
{ containersElements }
|
||||
</g>
|
||||
|
|
80
src/Components/SVG/Elements/ContainerModel.ts
Normal file
80
src/Components/SVG/Elements/ContainerModel.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Properties from '../../../Interfaces/Properties';
|
||||
|
||||
export interface IContainerModel {
|
||||
children: IContainerModel[],
|
||||
parent: IContainerModel | null,
|
||||
properties: Properties,
|
||||
userData: Record<string, string | number>
|
||||
}
|
||||
|
||||
export class ContainerModel implements IContainerModel {
|
||||
public children: IContainerModel[];
|
||||
public parent: IContainerModel | null;
|
||||
public properties: Properties;
|
||||
public userData: Record<string, string | number>;
|
||||
|
||||
constructor(
|
||||
parent: IContainerModel | null,
|
||||
properties: Properties,
|
||||
children: IContainerModel[] = [],
|
||||
userData = {}) {
|
||||
this.parent = parent;
|
||||
this.properties = properties;
|
||||
this.children = children;
|
||||
this.userData = userData;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a Generator iterating of over the children depth-first
|
||||
*/
|
||||
export function * MakeIterator(root: IContainerModel): Generator<IContainerModel, void, unknown> {
|
||||
const queue: IContainerModel[] = [root];
|
||||
const visited = new Set<IContainerModel>(queue);
|
||||
while (queue.length > 0) {
|
||||
const container = queue.pop() as IContainerModel;
|
||||
|
||||
yield container;
|
||||
|
||||
// if this reverse() gets costly, replace it by a simple for
|
||||
container.children.forEach((child) => {
|
||||
if (visited.has(child)) {
|
||||
return;
|
||||
}
|
||||
visited.add(child);
|
||||
queue.push(child);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth of the container
|
||||
* @returns The depth of the container
|
||||
*/
|
||||
export function getDepth(parent: IContainerModel) {
|
||||
let depth = 0;
|
||||
|
||||
let current: IContainerModel | null = parent;
|
||||
while (current != null) {
|
||||
depth++;
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute position by iterating to the parent
|
||||
* @returns The absolute position of the container
|
||||
*/
|
||||
export function getAbsolutePosition(container: IContainerModel): [number, number] {
|
||||
let x = Number(container.properties.x);
|
||||
let y = Number(container.properties.y);
|
||||
let current = container.parent;
|
||||
while (current != null) {
|
||||
x += Number(current.properties.x);
|
||||
y += Number(current.properties.y);
|
||||
current = current.parent;
|
||||
}
|
||||
return [x, y];
|
||||
}
|
|
@ -1,25 +1,25 @@
|
|||
import * as React from 'react';
|
||||
import { Container } from './Container';
|
||||
import { ContainerModel, getDepth, MakeIterator } from './ContainerModel';
|
||||
import { Dimension } from './Dimension';
|
||||
|
||||
interface IDimensionLayerProps {
|
||||
isHidden: boolean,
|
||||
roots: Container | Container[] | null,
|
||||
roots: ContainerModel | ContainerModel[] | null,
|
||||
}
|
||||
|
||||
const GAP: number = 50;
|
||||
|
||||
const getDimensionsNodes = (root: Container): React.ReactNode[] => {
|
||||
const it = root.MakeIterator();
|
||||
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
|
||||
const it = MakeIterator(root);
|
||||
const dimensions: React.ReactNode[] = [];
|
||||
for (const container of it) {
|
||||
// WARN: this might be dangerous later when using other units/rules
|
||||
const width = Number(container.props.properties.width);
|
||||
const width = Number(container.properties.width);
|
||||
|
||||
const id = `dim-${container.props.properties.id}`;
|
||||
const xStart: number = container.props.properties.x;
|
||||
const id = `dim-${container.properties.id}`;
|
||||
const xStart: number = container.properties.x;
|
||||
const xEnd = xStart + width;
|
||||
const y = -(GAP * (container.getDepth() + 1));
|
||||
const y = -(GAP * (getDepth(container) + 1));
|
||||
const strokeWidth = 1;
|
||||
const text = width.toString();
|
||||
const dimension = new Dimension({
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import { Container } from './Container';
|
||||
import { IContainerModel, getAbsolutePosition } from './ContainerModel';
|
||||
|
||||
interface ISelectorProps {
|
||||
selected: Container | null
|
||||
selected: IContainerModel | null
|
||||
}
|
||||
|
||||
export const Selector: React.FC<ISelectorProps> = (props) => {
|
||||
|
@ -13,10 +13,10 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
const [x, y] = props.selected.getAbsolutePosition();
|
||||
const [x, y] = getAbsolutePosition(props.selected);
|
||||
const [width, height] = [
|
||||
props.selected.props.properties.width,
|
||||
props.selected.props.properties.height
|
||||
props.selected.properties.width,
|
||||
props.selected.properties.height
|
||||
];
|
||||
const style = {
|
||||
stroke: '#3B82F6', // tw blue-500
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom';
|
||||
import { Container } from './Elements/Container';
|
||||
import { ContainerModel } from './Elements/ContainerModel';
|
||||
import { Selector } from './Elements/Selector';
|
||||
|
||||
interface ISVGProps {
|
||||
children: Container | Container[] | null,
|
||||
selected: Container | null
|
||||
children: ContainerModel | ContainerModel[] | null,
|
||||
selected: ContainerModel | null
|
||||
}
|
||||
|
||||
interface ISVGState {
|
||||
|
@ -65,9 +66,9 @@ export class SVG extends React.Component<ISVGProps> {
|
|||
|
||||
let children: React.ReactNode | React.ReactNode[] = [];
|
||||
if (Array.isArray(this.props.children)) {
|
||||
children = this.props.children.map(child => child.render());
|
||||
children = this.props.children.map(child => new Container({ model: child }).render());
|
||||
} else if (this.props.children !== null) {
|
||||
children = this.props.children.render();
|
||||
children = new Container({ model: this.props.children }).render();
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue