Merge branch 'dev' into dev.tests
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
2c7a1bc96c
26 changed files with 501 additions and 367 deletions
|
@ -5,7 +5,7 @@ module.exports = {
|
|||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'standard'
|
||||
'standard-with-typescript'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
@ -13,7 +13,8 @@ module.exports = {
|
|||
jsx: true
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json'
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
|
@ -21,9 +22,11 @@ module.exports = {
|
|||
],
|
||||
rules: {
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
'@typescript-eslint/space-before-function-paren': ['error', 'never'],
|
||||
indent: ['warn', 2, { SwitchCase: 1 }],
|
||||
semi: ['warn', 'always'],
|
||||
semi: 'off',
|
||||
'@typescript-eslint/semi': ['warn', 'always'],
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
}
|
||||
};
|
||||
|
|
29
package-lock.json
generated
29
package-lock.json
generated
|
@ -28,6 +28,7 @@
|
|||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-config-standard-with-typescript": "^22.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.2.4",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
|
@ -2697,6 +2698,24 @@
|
|||
"eslint-plugin-promise": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-standard-with-typescript": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-22.0.0.tgz",
|
||||
"integrity": "sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint-config-standard": "17.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-n": "^15.0.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
|
||||
|
@ -8093,6 +8112,16 @@
|
|||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-config-standard-with-typescript": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-22.0.0.tgz",
|
||||
"integrity": "sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint-config-standard": "17.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz",
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"autoprefixer": "^10.4.8",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-config-standard-with-typescript": "^22.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.2.4",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
|
|
136
src/App.tsx
136
src/App.tsx
|
@ -1,25 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import './App.scss';
|
||||
import { MainMenu } from './Components/MainMenu/MainMenu';
|
||||
import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel';
|
||||
import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel';
|
||||
import Editor, { IEditorState } from './Editor';
|
||||
import { AvailableContainer } from './Interfaces/AvailableContainer';
|
||||
import { Configuration } from './Interfaces/Configuration';
|
||||
import { Revive } from './utils/saveload';
|
||||
|
||||
export interface IHistoryState {
|
||||
MainContainer: IContainerModel | null,
|
||||
SelectedContainer: IContainerModel | null,
|
||||
SelectedContainerId: string,
|
||||
MainContainer: IContainerModel | null
|
||||
SelectedContainer: IContainerModel | null
|
||||
SelectedContainerId: string
|
||||
TypeCounters: Record<string, number>
|
||||
}
|
||||
|
||||
// App will never have props
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface IAppProps {
|
||||
}
|
||||
|
||||
interface IAppState {
|
||||
configuration: Configuration,
|
||||
history: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
configuration: Configuration
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
isLoaded: boolean
|
||||
}
|
||||
|
||||
|
@ -32,7 +34,12 @@ export class App extends React.Component<IAppProps> {
|
|||
configuration: {
|
||||
AvailableContainers: [],
|
||||
AvailableSymbols: [],
|
||||
MainContainer: {} as AvailableContainer
|
||||
MainContainer: {
|
||||
Type: 'EmptyContainer',
|
||||
Width: 3000,
|
||||
Height: 200,
|
||||
Style: {}
|
||||
}
|
||||
},
|
||||
history: [],
|
||||
historyCurrentStep: 0,
|
||||
|
@ -40,7 +47,7 @@ export class App extends React.Component<IAppProps> {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount(): void {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const state = urlParams.get('state');
|
||||
|
@ -50,35 +57,39 @@ export class App extends React.Component<IAppProps> {
|
|||
}
|
||||
|
||||
fetch(state)
|
||||
.then((response) => response.json())
|
||||
.then(
|
||||
async(response) => await response.json(),
|
||||
(error) => { throw new Error(error); }
|
||||
)
|
||||
.then((data: IEditorState) => {
|
||||
this.LoadState(data);
|
||||
});
|
||||
}, (error) => { throw new Error(error); });
|
||||
}
|
||||
|
||||
public NewEditor() {
|
||||
public NewEditor(): void {
|
||||
// Fetch the configuration from the API
|
||||
fetchConfiguration().then((configuration: Configuration) => {
|
||||
fetchConfiguration()
|
||||
.then((configuration: Configuration) => {
|
||||
// Set the main container from the given properties of the API
|
||||
const MainContainer = new ContainerModel(
|
||||
null,
|
||||
{
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: configuration.MainContainer.Width,
|
||||
height: configuration.MainContainer.Height,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
);
|
||||
const MainContainer = new ContainerModel(
|
||||
null,
|
||||
{
|
||||
id: 'main',
|
||||
parentId: 'null',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: configuration.MainContainer.Width,
|
||||
height: configuration.MainContainer.Height,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
);
|
||||
|
||||
// Save the configuration and the new MainContainer
|
||||
// and default the selected container to it
|
||||
this.setState({
|
||||
configuration,
|
||||
history:
|
||||
// Save the configuration and the new MainContainer
|
||||
// and default the selected container to it
|
||||
this.setState({
|
||||
configuration,
|
||||
history:
|
||||
[
|
||||
{
|
||||
MainContainer,
|
||||
|
@ -86,13 +97,14 @@ export class App extends React.Component<IAppProps> {
|
|||
TypeCounters: {}
|
||||
}
|
||||
],
|
||||
historyCurrentStep: 0,
|
||||
isLoaded: true
|
||||
} as IAppState);
|
||||
});
|
||||
historyCurrentStep: 0,
|
||||
isLoaded: true
|
||||
});
|
||||
}, (error) => { throw new Error(error); }
|
||||
);
|
||||
}
|
||||
|
||||
public LoadEditor(files: FileList | null) {
|
||||
public LoadEditor(files: FileList | null): void {
|
||||
if (files === null) {
|
||||
return;
|
||||
}
|
||||
|
@ -107,7 +119,7 @@ export class App extends React.Component<IAppProps> {
|
|||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
private LoadState(editorState: IEditorState) {
|
||||
private LoadState(editorState: IEditorState): void {
|
||||
Revive(editorState);
|
||||
|
||||
this.setState({
|
||||
|
@ -115,10 +127,10 @@ export class App extends React.Component<IAppProps> {
|
|||
history: editorState.history,
|
||||
historyCurrentStep: editorState.historyCurrentStep,
|
||||
isLoaded: true
|
||||
} as IAppState);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.isLoaded) {
|
||||
return (
|
||||
<div>
|
||||
|
@ -149,16 +161,17 @@ export class App extends React.Component<IAppProps> {
|
|||
export async function fetchConfiguration(): Promise<Configuration> {
|
||||
const url = `${import.meta.env.VITE_API_URL}`;
|
||||
// The test library cannot use the Fetch API
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
if (window.fetch) {
|
||||
return await fetch(url, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then((response) =>
|
||||
response.json()
|
||||
.then(async(response) =>
|
||||
await response.json()
|
||||
) as Configuration;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
return await new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.onreadystatechange = function() { // Call a function when the state changes.
|
||||
|
@ -169,38 +182,3 @@ export async function fetchConfiguration(): Promise<Configuration> {
|
|||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Revive the Editor state
|
||||
* by setting the containers references to their parent
|
||||
* @param editorState Editor state
|
||||
*/
|
||||
function Revive(editorState: IEditorState): void {
|
||||
const history = editorState.history;
|
||||
for (const state of history) {
|
||||
if (state.MainContainer === null || state.MainContainer === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const it = MakeIterator(state.MainContainer);
|
||||
for (const container of it) {
|
||||
const parentId = container.properties.parentId;
|
||||
if (parentId === null) {
|
||||
container.parent = null;
|
||||
continue;
|
||||
}
|
||||
const parent = findContainerById(state.MainContainer, parentId);
|
||||
if (parent === undefined) {
|
||||
continue;
|
||||
}
|
||||
container.parent = parent;
|
||||
}
|
||||
|
||||
const selected = findContainerById(state.MainContainer, state.SelectedContainerId);
|
||||
if (selected === undefined) {
|
||||
state.SelectedContainer = null;
|
||||
continue;
|
||||
}
|
||||
state.SelectedContainer = selected;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Properties } from '../Properties/Properties';
|
||||
import { IContainerModel, getDepth, MakeIterator } from '../../Interfaces/ContainerModel';
|
||||
import { IContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { getDepth, MakeIterator } from '../../utils/itertools';
|
||||
|
||||
interface IElementsSidebarProps {
|
||||
MainContainer: IContainerModel | null,
|
||||
isOpen: boolean,
|
||||
MainContainer: IContainerModel | null
|
||||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
SelectedContainer: IContainerModel | null,
|
||||
onClick: () => void,
|
||||
onPropertyChange: (key: string, value: string) => void,
|
||||
SelectedContainer: IContainerModel | null
|
||||
onClick: () => void
|
||||
onPropertyChange: (key: string, value: string) => void
|
||||
selectContainer: (container: IContainerModel) => void
|
||||
}
|
||||
|
||||
export class ElementsSidebar extends React.PureComponent<IElementsSidebarProps> {
|
||||
public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode {
|
||||
if (!this.props.MainContainer) {
|
||||
if (this.props.MainContainer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const it = MakeIterator(this.props.MainContainer);
|
||||
for (const container of it) {
|
||||
handleContainer(container as IContainerModel);
|
||||
handleContainer(container);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
let isOpenClasses = '-right-64';
|
||||
if (this.props.isOpen) {
|
||||
isOpenClasses = this.props.isHistoryOpen
|
||||
|
|
|
@ -9,7 +9,7 @@ interface IFloatingButtonProps {
|
|||
const toggleState = (
|
||||
isHidden: boolean,
|
||||
setHidden: React.Dispatch<React.SetStateAction<boolean>>
|
||||
) => {
|
||||
): void => {
|
||||
setHidden(!isHidden);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,19 +2,19 @@ import * as React from 'react';
|
|||
import { IHistoryState } from '../../App';
|
||||
|
||||
interface IHistoryProps {
|
||||
history: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
isOpen: boolean,
|
||||
onClick: () => void,
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
isOpen: boolean
|
||||
onClick: () => void
|
||||
jumpTo: (move: number) => void
|
||||
}
|
||||
|
||||
export class History extends React.PureComponent<IHistoryProps> {
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64';
|
||||
|
||||
const states = this.props.history.map((step, move) => {
|
||||
const desc = move
|
||||
const desc = move > 0
|
||||
? `Go to modification n°${move}`
|
||||
: 'Go to the beginning';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
|
||||
interface IMainMenuProps {
|
||||
newEditor: () => void;
|
||||
newEditor: () => void
|
||||
loadEditor: (files: FileList | null) => void
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import * as React from 'react';
|
|||
import ContainerProperties from '../../Interfaces/Properties';
|
||||
|
||||
interface IPropertiesProps {
|
||||
properties?: ContainerProperties,
|
||||
properties?: ContainerProperties
|
||||
onChange: (key: string, value: string) => void
|
||||
}
|
||||
|
||||
export class Properties extends React.PureComponent<IPropertiesProps> {
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.props.properties === undefined) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export class Properties extends React.PureComponent<IPropertiesProps> {
|
|||
public handleProperties = (
|
||||
[key, value]: [string, string | number],
|
||||
groupInput: React.ReactNode[]
|
||||
) => {
|
||||
): void => {
|
||||
const id = `property-${key}`;
|
||||
const type = 'text';
|
||||
const isDisabled = key === 'id' || key === 'parentId'; // hardcoded
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { getDepth, IContainerModel } from '../../../Interfaces/ContainerModel';
|
||||
import { IContainerModel } from '../../../Interfaces/ContainerModel';
|
||||
import { getDepth } from '../../../utils/itertools';
|
||||
import { Dimension } from './Dimension';
|
||||
|
||||
export interface IContainerProps {
|
||||
|
@ -20,11 +21,11 @@ export class Container extends React.PureComponent<IContainerProps> {
|
|||
const transform = `translate(${Number(this.props.model.properties.x)}, ${Number(this.props.model.properties.y)})`;
|
||||
|
||||
// g style
|
||||
const defaultStyle = {
|
||||
const defaultStyle: React.CSSProperties = {
|
||||
transitionProperty: 'all',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '150ms'
|
||||
} as React.CSSProperties;
|
||||
};
|
||||
|
||||
// Rect style
|
||||
const style = Object.assign(
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import * as React from 'react';
|
||||
|
||||
interface IDimensionProps {
|
||||
id: string;
|
||||
xStart: number;
|
||||
xEnd: number;
|
||||
y: number;
|
||||
text: string;
|
||||
strokeWidth: number;
|
||||
id: string
|
||||
xStart: number
|
||||
xEnd: number
|
||||
y: number
|
||||
text: string
|
||||
strokeWidth: number
|
||||
}
|
||||
|
||||
export class Dimension extends React.PureComponent<IDimensionProps> {
|
||||
public render() {
|
||||
const style = {
|
||||
public render(): JSX.Element {
|
||||
const style: React.CSSProperties = {
|
||||
stroke: 'black'
|
||||
} as React.CSSProperties;
|
||||
};
|
||||
return (
|
||||
<g key={this.props.id}>
|
||||
<line
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import { ContainerModel, getDepth, MakeIterator } from '../../../Interfaces/ContainerModel';
|
||||
import { ContainerModel } from '../../../Interfaces/ContainerModel';
|
||||
import { getDepth, MakeIterator } from '../../../utils/itertools';
|
||||
import { Dimension } from './Dimension';
|
||||
|
||||
interface IDimensionLayerProps {
|
||||
isHidden: boolean,
|
||||
roots: ContainerModel | ContainerModel[] | null,
|
||||
isHidden: boolean
|
||||
roots: ContainerModel | ContainerModel[] | null
|
||||
}
|
||||
|
||||
const GAP: number = 50;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { IContainerModel, getAbsolutePosition } from '../../../Interfaces/ContainerModel';
|
||||
import { IContainerModel } from '../../../Interfaces/ContainerModel';
|
||||
import { getAbsolutePosition } from '../../../utils/itertools';
|
||||
|
||||
interface ISelectorProps {
|
||||
selected: IContainerModel | null
|
||||
|
@ -18,7 +19,7 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
|
|||
props.selected.properties.width,
|
||||
props.selected.properties.height
|
||||
];
|
||||
const style = {
|
||||
const style: React.CSSProperties = {
|
||||
stroke: '#3B82F6', // tw blue-500
|
||||
strokeWidth: 4,
|
||||
fillOpacity: 0,
|
||||
|
@ -26,7 +27,7 @@ export const Selector: React.FC<ISelectorProps> = (props) => {
|
|||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '150ms',
|
||||
animation: 'fadein 750ms ease-in alternate infinite'
|
||||
} as React.CSSProperties;
|
||||
};
|
||||
|
||||
return (
|
||||
<rect
|
||||
|
|
|
@ -1,37 +1,49 @@
|
|||
import * as React from 'react';
|
||||
import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom';
|
||||
import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
|
||||
import { Container } from './Elements/Container';
|
||||
import { ContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { Selector } from './Elements/Selector';
|
||||
|
||||
interface ISVGProps {
|
||||
width: number,
|
||||
height: number,
|
||||
children: ContainerModel | ContainerModel[] | null,
|
||||
width: number
|
||||
height: number
|
||||
children: ContainerModel | ContainerModel[] | null
|
||||
selected: ContainerModel | null
|
||||
}
|
||||
|
||||
interface ISVGState {
|
||||
value: Value,
|
||||
tool: Tool
|
||||
viewerWidth: number,
|
||||
viewerHeight: number,
|
||||
}
|
||||
|
||||
export class SVG extends React.PureComponent<ISVGProps> {
|
||||
public state: ISVGState;
|
||||
public static ID = 'svg';
|
||||
public state: ISVGState;
|
||||
|
||||
constructor(props: ISVGProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: {
|
||||
viewerWidth: window.innerWidth,
|
||||
viewerHeight: window.innerHeight
|
||||
} as Value,
|
||||
tool: TOOL_PAN
|
||||
viewerWidth: window.innerWidth,
|
||||
viewerHeight: window.innerHeight
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
resizeViewBox(): void {
|
||||
this.setState({
|
||||
viewerWidth: window.innerWidth,
|
||||
viewerHeight: window.innerHeight
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener('resize', () => this.resizeViewBox());
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener('resize', () => this.resizeViewBox());
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const xmlns = '<http://www.w3.org/2000/svg>';
|
||||
|
||||
const properties = {
|
||||
|
@ -49,13 +61,11 @@ export class SVG extends React.PureComponent<ISVGProps> {
|
|||
|
||||
return (
|
||||
<div id={SVG.ID}>
|
||||
<ReactSVGPanZoom
|
||||
width={window.innerWidth}
|
||||
height={window.innerHeight}
|
||||
<UncontrolledReactSVGPanZoom
|
||||
width={this.state.viewerWidth}
|
||||
height={this.state.viewerHeight}
|
||||
background={'#ffffff'}
|
||||
defaultTool='pan'
|
||||
value={this.state.value} onChangeValue={value => this.setState({ value })}
|
||||
tool={this.state.tool} onChangeTool={tool => this.setState({ tool })}
|
||||
miniatureProps={{
|
||||
position: 'left',
|
||||
background: '#616264',
|
||||
|
@ -67,9 +77,8 @@ export class SVG extends React.PureComponent<ISVGProps> {
|
|||
{ children }
|
||||
<Selector selected={this.props.selected} />
|
||||
</svg>
|
||||
</ReactSVGPanZoom>
|
||||
</UncontrolledReactSVGPanZoom>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ import { AvailableContainer } from '../../Interfaces/AvailableContainer';
|
|||
|
||||
interface ISidebarProps {
|
||||
componentOptions: AvailableContainer[]
|
||||
isOpen: boolean;
|
||||
onClick: () => void;
|
||||
buttonOnClick: (type: string) => void;
|
||||
isOpen: boolean
|
||||
onClick: () => void
|
||||
buttonOnClick: (type: string) => void
|
||||
}
|
||||
|
||||
export default class Sidebar extends React.PureComponent<ISidebarProps> {
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
const listElements = this.props.componentOptions.map(componentOption =>
|
||||
<button className='hover:bg-blue-600 transition-all sidebar-row' key={componentOption.Type} onClick={() => this.props.buttonOnClick(componentOption.Type)}>
|
||||
{componentOption.Type}
|
||||
|
|
139
src/Components/UI/UI.tsx
Normal file
139
src/Components/UI/UI.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
import * as React from 'react';
|
||||
import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
|
||||
import Sidebar from '../Sidebar/Sidebar';
|
||||
import { History } from '../History/History';
|
||||
import { AvailableContainer } from '../../Interfaces/AvailableContainer';
|
||||
import { ContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { IHistoryState } from '../../App';
|
||||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||
import FloatingButton from '../FloatingButton/FloatingButton';
|
||||
|
||||
interface IUIProps {
|
||||
current: IHistoryState
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
AvailableContainers: AvailableContainer[]
|
||||
SelectContainer: (container: ContainerModel) => void
|
||||
OnPropertyChange: (key: string, value: string) => void
|
||||
AddContainer: (type: string) => void
|
||||
SaveEditorAsJSON: () => void
|
||||
SaveEditorAsSVG: () => void
|
||||
LoadState: (move: number) => void
|
||||
}
|
||||
|
||||
interface IUIState {
|
||||
isSidebarOpen: boolean
|
||||
isElementsSidebarOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
}
|
||||
|
||||
export class UI extends React.PureComponent<IUIProps, IUIState> {
|
||||
constructor(props: IUIProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSidebarOpen: true,
|
||||
isElementsSidebarOpen: false,
|
||||
isHistoryOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the components sidebar
|
||||
*/
|
||||
public ToggleSidebar(): void {
|
||||
this.setState({
|
||||
isSidebarOpen: !this.state.isSidebarOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the elements
|
||||
*/
|
||||
public ToggleElementsSidebar(): void {
|
||||
this.setState({
|
||||
isElementsSidebarOpen: !this.state.isElementsSidebarOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the elements
|
||||
*/
|
||||
public ToggleHistory(): void {
|
||||
this.setState({
|
||||
isHistoryOpen: !this.state.isHistoryOpen
|
||||
});
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
let buttonRightOffsetClasses = 'right-12';
|
||||
if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) {
|
||||
buttonRightOffsetClasses = 'right-72';
|
||||
}
|
||||
if (this.state.isHistoryOpen && this.state.isElementsSidebarOpen) {
|
||||
buttonRightOffsetClasses = 'right-[544px]';
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
componentOptions={this.props.AvailableContainers}
|
||||
isOpen={this.state.isSidebarOpen}
|
||||
onClick={() => this.ToggleSidebar()}
|
||||
buttonOnClick={(type: string) => this.props.AddContainer(type)}
|
||||
/>
|
||||
<button
|
||||
className='fixed z-10 top-4 left-4 text-lg bg-blue-200 hover:bg-blue-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||
onClick={() => this.ToggleSidebar()}
|
||||
>
|
||||
☰ Components
|
||||
</button>
|
||||
|
||||
<ElementsSidebar
|
||||
MainContainer={this.props.current.MainContainer}
|
||||
SelectedContainer={this.props.current.SelectedContainer}
|
||||
isOpen={this.state.isElementsSidebarOpen}
|
||||
isHistoryOpen={this.state.isHistoryOpen}
|
||||
onClick={() => this.ToggleElementsSidebar()}
|
||||
onPropertyChange={this.props.OnPropertyChange}
|
||||
selectContainer={this.props.SelectContainer}
|
||||
/>
|
||||
<button
|
||||
className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||
onClick={() => this.ToggleElementsSidebar()}
|
||||
>
|
||||
☰ Elements
|
||||
</button>
|
||||
|
||||
<History
|
||||
history={this.props.history}
|
||||
historyCurrentStep={this.props.historyCurrentStep}
|
||||
isOpen={this.state.isHistoryOpen}
|
||||
onClick={() => this.ToggleHistory()}
|
||||
jumpTo={this.props.LoadState}
|
||||
/>
|
||||
<button
|
||||
className='fixed z-10 top-4 right-72 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||
onClick={() => this.ToggleHistory()}>
|
||||
☰ History
|
||||
</button>
|
||||
|
||||
<FloatingButton className={`fixed flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
|
||||
<button
|
||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||
title='Export as JSON'
|
||||
onClick={this.props.SaveEditorAsJSON}
|
||||
>
|
||||
<UploadIcon className="h-full w-full text-white align-middle items-center justify-center" />
|
||||
</button>
|
||||
<button
|
||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||
title='Export as SVG'
|
||||
onClick={this.props.SaveEditorAsSVG}
|
||||
>
|
||||
<PhotographIcon className="h-full w-full text-white align-middle items-center justify-center" />
|
||||
</button>
|
||||
</FloatingButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
192
src/Editor.tsx
192
src/Editor.tsx
|
@ -1,28 +1,22 @@
|
|||
import React from 'react';
|
||||
import { UploadIcon, PhotographIcon } from '@heroicons/react/outline';
|
||||
import './Editor.scss';
|
||||
import Sidebar from './Components/Sidebar/Sidebar';
|
||||
import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar';
|
||||
import { Configuration } from './Interfaces/Configuration';
|
||||
import { SVG } from './Components/SVG/SVG';
|
||||
import { History } from './Components/History/History';
|
||||
import { ContainerModel, findContainerById, IContainerModel, MakeIterator } from './Interfaces/ContainerModel';
|
||||
import Properties from './Interfaces/Properties';
|
||||
import { ContainerModel, IContainerModel } from './Interfaces/ContainerModel';
|
||||
import { findContainerById, MakeIterator } from './utils/itertools';
|
||||
import { IHistoryState } from './App';
|
||||
import FloatingButton from './Components/FloatingButton/FloatingButton';
|
||||
import { getCircularReplacer } from './utils/saveload';
|
||||
import { UI } from './Components/UI/UI';
|
||||
|
||||
interface IEditorProps {
|
||||
configuration: Configuration,
|
||||
history: Array<IHistoryState>,
|
||||
configuration: Configuration
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
}
|
||||
|
||||
export interface IEditorState {
|
||||
isSidebarOpen: boolean,
|
||||
isElementsSidebarOpen: boolean,
|
||||
isHistoryOpen: boolean,
|
||||
history: Array<IHistoryState>,
|
||||
historyCurrentStep: number,
|
||||
history: IHistoryState[]
|
||||
historyCurrentStep: number
|
||||
// do not use it, use props.configuration
|
||||
// only used for serialization purpose
|
||||
configuration: Configuration
|
||||
|
@ -34,50 +28,44 @@ class Editor extends React.Component<IEditorProps> {
|
|||
constructor(props: IEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSidebarOpen: true,
|
||||
isElementsSidebarOpen: false,
|
||||
isHistoryOpen: false,
|
||||
configuration: Object.assign({}, props.configuration),
|
||||
history: [...props.history],
|
||||
historyCurrentStep: props.historyCurrentStep
|
||||
} as IEditorState;
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener('keyup', (event) => this.onKeyDown(event));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener('keyup', (event) => this.onKeyDown(event));
|
||||
}
|
||||
|
||||
public onKeyDown(event: KeyboardEvent): void {
|
||||
event.preventDefault();
|
||||
if (event.isComposing || event.keyCode === 229) {
|
||||
return;
|
||||
}
|
||||
if (event.key === 'z' &&
|
||||
event.ctrlKey &&
|
||||
this.state.historyCurrentStep > 0) {
|
||||
this.LoadState(this.state.historyCurrentStep - 1);
|
||||
} else if (event.key === 'y' &&
|
||||
event.ctrlKey &&
|
||||
this.state.historyCurrentStep < this.state.history.length - 1) {
|
||||
this.LoadState(this.state.historyCurrentStep + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentHistory = (): IHistoryState[] => this.state.history.slice(0, this.state.historyCurrentStep + 1);
|
||||
public getCurrentHistoryState = (): IHistoryState => this.state.history[this.state.historyCurrentStep];
|
||||
|
||||
/**
|
||||
* Toggle the components sidebar
|
||||
*/
|
||||
public ToggleSidebar() {
|
||||
this.setState({
|
||||
isSidebarOpen: !this.state.isSidebarOpen
|
||||
} as IEditorState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the elements
|
||||
*/
|
||||
public ToggleElementsSidebar() {
|
||||
this.setState({
|
||||
isElementsSidebarOpen: !this.state.isElementsSidebarOpen
|
||||
} as IEditorState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the elements
|
||||
*/
|
||||
public ToggleHistory() {
|
||||
this.setState({
|
||||
isHistoryOpen: !this.state.isHistoryOpen
|
||||
} as IEditorState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a container
|
||||
* @param container Selected container
|
||||
*/
|
||||
public SelectContainer(container: ContainerModel) {
|
||||
public SelectContainer(container: ContainerModel): void {
|
||||
const history = this.getCurrentHistory();
|
||||
const current = history[history.length - 1];
|
||||
|
||||
|
@ -100,7 +88,7 @@ class Editor extends React.Component<IEditorProps> {
|
|||
SelectedContainerId: SelectedContainer.properties.id
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
} as IEditorState);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,7 +122,7 @@ class Editor extends React.Component<IEditorProps> {
|
|||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
} as IEditorState);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -156,7 +144,7 @@ class Editor extends React.Component<IEditorProps> {
|
|||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
} as IEditorState);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,8 +184,7 @@ class Editor extends React.Component<IEditorProps> {
|
|||
const count = newCounters[type];
|
||||
|
||||
// Create maincontainer model
|
||||
const structure: IContainerModel = structuredClone(current.MainContainer);
|
||||
const clone = Object.assign(new ContainerModel(null, {} as Properties), structure);
|
||||
const clone: IContainerModel = structuredClone(current.MainContainer);
|
||||
|
||||
// Find the parent
|
||||
const it = MakeIterator(clone);
|
||||
|
@ -230,7 +217,7 @@ class Editor extends React.Component<IEditorProps> {
|
|||
width: properties?.Width,
|
||||
height: parent.properties.height,
|
||||
...properties.Style
|
||||
} as Properties,
|
||||
},
|
||||
[],
|
||||
{
|
||||
type
|
||||
|
@ -249,16 +236,16 @@ class Editor extends React.Component<IEditorProps> {
|
|||
SelectedContainerId: parent.properties.id
|
||||
}]),
|
||||
historyCurrentStep: history.length
|
||||
} as IEditorState);
|
||||
});
|
||||
}
|
||||
|
||||
public jumpTo(move: number): void {
|
||||
public LoadState(move: number): void {
|
||||
this.setState({
|
||||
historyCurrentStep: move
|
||||
} as IEditorState);
|
||||
});
|
||||
}
|
||||
|
||||
public SaveEditorAsJSON() {
|
||||
public SaveEditorAsJSON(): void {
|
||||
const exportName = 'state';
|
||||
const spaces = import.meta.env.DEV ? 4 : 0;
|
||||
const data = JSON.stringify(this.state, getCircularReplacer(), spaces);
|
||||
|
@ -271,7 +258,7 @@ class Editor extends React.Component<IEditorProps> {
|
|||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
public SaveEditorAsSVG() {
|
||||
public SaveEditorAsSVG(): void {
|
||||
const svgWrapper = document.getElementById(SVG.ID) as HTMLElement;
|
||||
const svg = svgWrapper.querySelector('svg') as SVGSVGElement;
|
||||
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
|
||||
|
@ -289,59 +276,22 @@ class Editor extends React.Component<IEditorProps> {
|
|||
* Render the application
|
||||
* @returns {JSX.Element} Rendered JSX element
|
||||
*/
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const current = this.getCurrentHistoryState();
|
||||
let buttonRightOffsetClasses = 'right-12';
|
||||
if (this.state.isElementsSidebarOpen || this.state.isHistoryOpen) {
|
||||
buttonRightOffsetClasses = 'right-72';
|
||||
}
|
||||
if (this.state.isHistoryOpen && this.state.isElementsSidebarOpen) {
|
||||
buttonRightOffsetClasses = 'right-[544px]';
|
||||
}
|
||||
return (
|
||||
<div className="App font-sans h-full">
|
||||
<Sidebar
|
||||
componentOptions={this.props.configuration.AvailableContainers}
|
||||
isOpen={this.state.isSidebarOpen}
|
||||
onClick={() => this.ToggleSidebar()}
|
||||
buttonOnClick={(type: string) => this.AddContainer(type)}
|
||||
/>
|
||||
<button
|
||||
className='fixed z-10 top-4 left-4 text-lg bg-blue-200 hover:bg-blue-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||
onClick={() => this.ToggleSidebar()}
|
||||
>
|
||||
☰ Components
|
||||
</button>
|
||||
|
||||
<ElementsSidebar
|
||||
MainContainer={current.MainContainer}
|
||||
SelectedContainer={current.SelectedContainer}
|
||||
isOpen={this.state.isElementsSidebarOpen}
|
||||
isHistoryOpen={this.state.isHistoryOpen}
|
||||
onClick={() => this.ToggleElementsSidebar()}
|
||||
onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)}
|
||||
selectContainer={(container: ContainerModel) => this.SelectContainer(container)}
|
||||
/>
|
||||
<button
|
||||
className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||
onClick={() => this.ToggleElementsSidebar()}
|
||||
>
|
||||
☰ Elements
|
||||
</button>
|
||||
|
||||
<History
|
||||
<UI
|
||||
current={current}
|
||||
history={this.state.history}
|
||||
historyCurrentStep={this.state.historyCurrentStep}
|
||||
isOpen={this.state.isHistoryOpen}
|
||||
onClick={() => this.ToggleHistory()}
|
||||
jumpTo={(move) => { this.jumpTo(move); }}
|
||||
AvailableContainers={this.state.configuration.AvailableContainers}
|
||||
SelectContainer={(container) => this.SelectContainer(container)}
|
||||
OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)}
|
||||
AddContainer={(type) => this.AddContainer(type)}
|
||||
SaveEditorAsJSON={() => this.SaveEditorAsJSON()}
|
||||
SaveEditorAsSVG={() => this.SaveEditorAsSVG()}
|
||||
LoadState={(move) => this.LoadState(move)}
|
||||
/>
|
||||
<button
|
||||
className='fixed z-10 top-4 right-72 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg'
|
||||
onClick={() => this.ToggleHistory()}>
|
||||
☰ History
|
||||
</button>
|
||||
|
||||
<SVG
|
||||
width={Number(current.MainContainer?.properties.width)}
|
||||
height={Number(current.MainContainer?.properties.height)}
|
||||
|
@ -349,43 +299,9 @@ class Editor extends React.Component<IEditorProps> {
|
|||
>
|
||||
{ current.MainContainer }
|
||||
</SVG>
|
||||
<FloatingButton className={`fixed flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
|
||||
<button
|
||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||
title='Export as JSON'
|
||||
onClick={() => this.SaveEditorAsJSON()}
|
||||
>
|
||||
<UploadIcon className="h-full w-full text-white align-middle items-center justify-center" />
|
||||
</button>
|
||||
<button
|
||||
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
|
||||
title='Export as SVG'
|
||||
onClick={() => this.SaveEditorAsSVG()}
|
||||
>
|
||||
<PhotographIcon className="h-full w-full text-white align-middle items-center justify-center" />
|
||||
</button>
|
||||
</FloatingButton>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (key: any, value: object | null) => {
|
||||
if (key === 'parent') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export enum AddingBehavior {
|
||||
InsertInto,
|
||||
Replace
|
||||
InsertInto,
|
||||
Replace
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export enum XPositionReference {
|
||||
Left,
|
||||
Center,
|
||||
Right
|
||||
Left,
|
||||
Center,
|
||||
Right
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { AvailableSymbolModel } from './AvailableSymbol';
|
|||
|
||||
/** Model of configuration for the application to configure it */
|
||||
export interface Configuration {
|
||||
AvailableContainers: AvailableContainer[];
|
||||
AvailableSymbols: AvailableSymbolModel[];
|
||||
MainContainer: AvailableContainer;
|
||||
AvailableContainers: AvailableContainer[]
|
||||
AvailableSymbols: AvailableSymbolModel[]
|
||||
MainContainer: AvailableContainer
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Properties from './Properties';
|
||||
|
||||
export interface IContainerModel {
|
||||
children: IContainerModel[],
|
||||
parent: IContainerModel | null,
|
||||
properties: Properties,
|
||||
children: IContainerModel[]
|
||||
parent: IContainerModel | null
|
||||
properties: Properties
|
||||
userData: Record<string, string | number>
|
||||
}
|
||||
|
||||
|
@ -24,67 +24,3 @@ export class ContainerModel implements IContainerModel {
|
|||
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];
|
||||
}
|
||||
|
||||
export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
|
||||
const it = MakeIterator(root);
|
||||
for (const container of it) {
|
||||
if (container.properties.id === id) {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/** Model of an image with multiple source */
|
||||
export interface Image {
|
||||
Name: string;
|
||||
Url: string;
|
||||
Base64Image: string;
|
||||
Svg: string;
|
||||
Name: string
|
||||
Url: string
|
||||
Base64Image: string
|
||||
Svg: string
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export default interface Properties extends React.CSSProperties {
|
||||
id: string,
|
||||
parentId: string | null,
|
||||
x: number,
|
||||
id: string
|
||||
parentId: string | null
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
|
65
src/utils/itertools.ts
Normal file
65
src/utils/itertools.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { IContainerModel } from '../Interfaces/ContainerModel';
|
||||
|
||||
/**
|
||||
* 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): number {
|
||||
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];
|
||||
}
|
||||
|
||||
export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
|
||||
const it = MakeIterator(root);
|
||||
for (const container of it) {
|
||||
if (container.properties.id === id) {
|
||||
return container;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
54
src/utils/saveload.ts
Normal file
54
src/utils/saveload.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { findContainerById, MakeIterator } from './itertools';
|
||||
import { IEditorState } from '../Editor';
|
||||
|
||||
/**
|
||||
* Revive the Editor state
|
||||
* by setting the containers references to their parent
|
||||
* @param editorState Editor state
|
||||
*/
|
||||
export function Revive(editorState: IEditorState): void {
|
||||
const history = editorState.history;
|
||||
for (const state of history) {
|
||||
if (state.MainContainer === null || state.MainContainer === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const it = MakeIterator(state.MainContainer);
|
||||
for (const container of it) {
|
||||
const parentId = container.properties.parentId;
|
||||
if (parentId === null) {
|
||||
container.parent = null;
|
||||
continue;
|
||||
}
|
||||
const parent = findContainerById(state.MainContainer, parentId);
|
||||
if (parent === undefined) {
|
||||
continue;
|
||||
}
|
||||
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 => {
|
||||
const seen = new WeakSet();
|
||||
return (key: any, value: object | null) => {
|
||||
if (key === 'parent') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
|
@ -1,13 +1,13 @@
|
|||
/* eslint-disable import/export */
|
||||
import * as React from 'react';
|
||||
import { cleanup, render } from '@testing-library/react';
|
||||
import { cleanup, render, RenderResult } from '@testing-library/react';
|
||||
import { afterEach } from 'vitest';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const customRender = (ui: React.ReactElement, options = {}) =>
|
||||
const customRender = (ui: React.ReactElement, options = {}): RenderResult =>
|
||||
render(ui, {
|
||||
// wrap provider(s) here if needed
|
||||
wrapper: ({ children }) => children,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue