Merge branch 'dev' into dev.loading

This commit is contained in:
Eric NGUYEN 2022-11-04 12:05:37 +01:00
commit 0ac42b3500
41 changed files with 1609 additions and 1241 deletions

1
.gitattributes vendored
View file

@ -2,3 +2,4 @@
*.pdf filter=lfs diff=lfs merge=lfs -text *.pdf filter=lfs diff=lfs merge=lfs -text
*.dwg filter=lfs diff=lfs merge=lfs -text *.dwg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.pptx filter=lfs diff=lfs merge=lfs -text

View file

@ -1,4 +1,5 @@
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Collections.Generic;
namespace SVGLDLibs.Models namespace SVGLDLibs.Models
{ {
@ -149,6 +150,6 @@ namespace SVGLDLibs.Models
* User data that can be used for data storage or custom SVG * User data that can be used for data storage or custom SVG
*/ */
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public object userData; public Dictionary<string, string> userData;
} }
} }

View file

@ -6,8 +6,8 @@ namespace SVGLDLibs.Models
public class PointModel public class PointModel
{ {
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string X { get; set; } public double x { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string Y { get; set; } public double y { get; set; }
} }
} }

View file

@ -1,17 +1,24 @@
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Collections.Generic;
namespace SVGLDLibs.Models namespace SVGLDLibs.Models
{ {
[DataContract] [DataContract]
public class SymbolModel : AvailableSymbolModel public class SymbolModel
{ {
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string Id { get; set; } public string id { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public PointModel Point { get; set; } public string type { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public bool IsLinkedToContainer { get; set; } public AvailableSymbolModel config { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string LinkedContainerId { get; set; } public double x { get; set; }
[DataMember(EmitDefaultValue = false)]
public double width { get; set; }
[DataMember(EmitDefaultValue = false)]
public double height { get; set; }
[DataMember(EmitDefaultValue = false)]
public List<string> linkedContainers { get; set; }
} }
} }

View file

@ -45,7 +45,7 @@ public class SVGLDController : ControllerBase
} }
[HttpPost(Name = nameof(SVGLDLibs.Models.Configuration))] [HttpPost(Name = nameof(SVGLDLibs.Models.Configuration))]
public bool ConfigurationResponseModel(Configuration model) public bool Configuration(Configuration model)
{ {
return true; return true;
} }

BIN
docs/#Project/Pages/SVGLD_Cotes.pdf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/#Project/Pages/SVGLD_Cotes.pptx (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/#Project/Pages/Translations.drawio (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -5,6 +5,7 @@
type IHistoryState = SVGLD.IHistoryState; type IHistoryState = SVGLD.IHistoryState;
type IEditorState = SVGLD.IEditorState; type IEditorState = SVGLD.IEditorState;
type IConfiguration = SVGLD.IConfiguration; type IConfiguration = SVGLD.IConfiguration;
type ILanguage = SVGLD.ILanguage;
export class SVGLayoutDesigner extends Components.ComponentBase { export class SVGLayoutDesigner extends Components.ComponentBase {
@ -210,6 +211,43 @@
const component = this.GetAppComponent(); const component = this.GetAppComponent();
component.dispatchEvent(new CustomEvent(eventType, { detail: configuration })); component.dispatchEvent(new CustomEvent(eventType, { detail: configuration }));
} }
/**
* Add a language to the app
* @param option Displayed string of the language
* @param language Language containing an id and a dictionary
* @param callback Callback
*/
public AddLanguage(option: string, language: ILanguage, callback?: () => void) {
const eventType = 'addLanguage';
this.app.AddEventListener(eventType, callback);
const component = this.GetAppComponent();
component.dispatchEvent(new CustomEvent(eventType, { detail: { language, option } }));
}
/**
* Set the language of the app (defaults: ['fr', 'en'])
* @param languageId Language identifier for the language to select
* @param callback Callback
*/
public SetLanguage(languageId: string, callback?: (success: boolean) => void) {
const eventType = 'setLanguage';
this.app.AddEventListener(eventType, callback);
const component = this.GetAppComponent();
component.dispatchEvent(new CustomEvent(eventType, { detail: languageId }));
}
/**
* Set the language of the app (defaults: ['fr', 'en'])
* @param languageId Language identifier for the language to select
* @param callback Callback
*/
public GetLanguages(callback: (languages: Record<string, Record<string, string>>) => void) {
const eventType = 'getLanguages';
this.app.AddEventListener(eventType, callback);
const component = this.GetAppComponent();
component.dispatchEvent(new CustomEvent(eventType));
}
} }

View file

@ -190,12 +190,7 @@ describe.concurrent('Models test suite', () => {
} }
], ],
CustomSVG: 'string', CustomSVG: 'string',
Style: {}, Style: {}
UserData: {
additionalProp1: 'string',
additionalProp2: 'string',
additionalProp3: 'string'
}
}; };
it('AvailableContainerModel', async() => { it('AvailableContainerModel', async() => {
@ -238,7 +233,7 @@ describe.concurrent('Models test suite', () => {
wrapper: 'string' wrapper: 'string'
}; };
it('ConfigurationResponseModel', async() => { it('Configuration', async() => {
const model: IConfiguration = { const model: IConfiguration = {
AvailableContainers: [ AvailableContainers: [
availableContainerModel availableContainerModel
@ -250,10 +245,15 @@ describe.concurrent('Models test suite', () => {
category category
], ],
MainContainer: availableContainerModel, MainContainer: availableContainerModel,
Patterns: [pattern] Patterns: [pattern],
APIConfiguration: {
apiFetchUrl: '',
apiGetFeedbackUrl: '',
apiSetContainerListUrl: ''
}
}; };
const res = await Post2API('ConfigurationResponseModel', JSON.stringify(model)); const res = await Post2API('Configuration', JSON.stringify(model));
expect(res).toBe(true); expect(res).toBe(true);
}); });

View file

@ -1,4 +1,4 @@
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; import React, { Dispatch, SetStateAction, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { UseCustomEvents } from '../../Events/AppEvents'; import { UseCustomEvents } from '../../Events/AppEvents';
import { MainMenu } from '../MainMenu/MainMenu'; import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
@ -9,6 +9,7 @@ import { LoadEditor, NewEditor } from './Actions/MenuActions';
import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS, FAST_BOOT } from '../../utils/default'; import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS, FAST_BOOT } from '../../utils/default';
import { AppState } from '../../Enums/AppState'; import { AppState } from '../../Enums/AppState';
import { Loader } from '../Loader/Loader'; import { Loader } from '../Loader/Loader';
import { LanguageContext } from '../LanguageProvider/LanguageProvider';
// App will never have props // App will never have props
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -46,6 +47,7 @@ function UseHTTPGETStatePreloading(
export function App(props: IAppProps): JSX.Element { export function App(props: IAppProps): JSX.Element {
const [appState, setAppState] = useState<AppState>(FAST_BOOT); const [appState, setAppState] = useState<AppState>(FAST_BOOT);
const appRef = useRef<HTMLDivElement>(null); const appRef = useRef<HTMLDivElement>(null);
const languageContext = useContext(LanguageContext);
const defaultMainContainer = new ContainerModel( const defaultMainContainer = new ContainerModel(
DEFAULT_MAINCONTAINER_PROPS DEFAULT_MAINCONTAINER_PROPS
@ -70,6 +72,7 @@ export function App(props: IAppProps): JSX.Element {
UseCustomEvents( UseCustomEvents(
props.root, props.root,
appRef, appRef,
languageContext,
setEditorState, setEditorState,
setAppState setAppState
); );

View file

@ -14,6 +14,7 @@ import {
Cog8ToothIcon as Cog8ToothIconS Cog8ToothIcon as Cog8ToothIconS
} from '@heroicons/react/24/solid'; } from '@heroicons/react/24/solid';
import { BarIcon } from './BarIcon'; import { BarIcon } from './BarIcon';
import { Text } from '../Text/Text';
interface IBarProps { interface IBarProps {
isComponentsOpen: boolean isComponentsOpen: boolean
@ -35,7 +36,7 @@ export function Bar(props: IBarProps): JSX.Element {
<div className='bar'> <div className='bar'>
<BarIcon <BarIcon
isActive={props.isComponentsOpen} isActive={props.isComponentsOpen}
title='Components' title={Text({ textId: '@Components' })}
onClick={() => props.toggleComponents()}> onClick={() => props.toggleComponents()}>
{ {
props.isComponentsOpen props.isComponentsOpen
@ -45,7 +46,7 @@ export function Bar(props: IBarProps): JSX.Element {
</BarIcon> </BarIcon>
<BarIcon <BarIcon
isActive={props.isSymbolsOpen} isActive={props.isSymbolsOpen}
title='Symbols' title={Text({ textId: '@Symbols' })}
onClick={() => props.toggleSymbols()}> onClick={() => props.toggleSymbols()}>
{ {
props.isSymbolsOpen props.isSymbolsOpen
@ -55,7 +56,7 @@ export function Bar(props: IBarProps): JSX.Element {
</BarIcon> </BarIcon>
<BarIcon <BarIcon
isActive={props.isMessagesOpen} isActive={props.isMessagesOpen}
title='Messages' title={Text({ textId: '@Messages' })}
onClick={() => props.toggleMessages()}> onClick={() => props.toggleMessages()}>
{ {
props.isMessagesOpen props.isMessagesOpen
@ -66,7 +67,7 @@ export function Bar(props: IBarProps): JSX.Element {
<div className='grow'></div> <div className='grow'></div>
<BarIcon <BarIcon
isActive={props.isHistoryOpen} isActive={props.isHistoryOpen}
title='Timeline' title={Text({ textId: '@Timeline' })}
onClick={() => props.toggleTimeline()}> onClick={() => props.toggleTimeline()}>
{ {
props.isHistoryOpen props.isHistoryOpen
@ -76,7 +77,7 @@ export function Bar(props: IBarProps): JSX.Element {
</BarIcon> </BarIcon>
<BarIcon <BarIcon
isActive={props.isSettingsOpen} isActive={props.isSettingsOpen}
title='Settings' title={Text({ textId: '@Settings' })}
onClick={() => props.toggleSettings()}> onClick={() => props.toggleSettings()}>
{ {
props.isSettingsOpen props.isSettingsOpen

View file

@ -5,6 +5,7 @@ import { ICategory } from '../../Interfaces/ICategory';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { TruncateString } from '../../utils/stringtools'; import { TruncateString } from '../../utils/stringtools';
import { Category } from '../Category/Category'; import { Category } from '../Category/Category';
import { Text } from '../Text/Text';
interface IComponentsProps { interface IComponentsProps {
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
@ -24,7 +25,7 @@ interface SidebarCategory {
export function Components(props: IComponentsProps): JSX.Element { export function Components(props: IComponentsProps): JSX.Element {
const [hideDisabled, setHideDisabled] = React.useState<boolean>(false); const [hideDisabled, setHideDisabled] = React.useState<boolean>(false);
const disabledTitle = hideDisabled ? 'Show disabled components' : 'Hide disabled components'; const disabledTitle = hideDisabled ? Text({ textId: '@ShowDisabledComponents' }) : Text({ textId: '@HideDisabledComponents' });
const rootElements: Array<JSX.Element | undefined> = []; const rootElements: Array<JSX.Element | undefined> = [];
const categories = new Map<string, SidebarCategory>(props.categories.map(category => [ const categories = new Map<string, SidebarCategory>(props.categories.map(category => [

View file

@ -4,6 +4,7 @@ import { IContainerProperties } from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../utils/default'; import { SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../utils/default';
import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, RestoreY, TransformX, TransformY } from '../../utils/svg'; import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, RestoreY, TransformX, TransformY } from '../../utils/svg';
import { Text } from '../Text/Text';
import { InputGroup } from '../InputGroup/InputGroup'; import { InputGroup } from '../InputGroup/InputGroup';
import { TextInputGroup } from '../InputGroup/TextInputGroup'; import { TextInputGroup } from '../InputGroup/TextInputGroup';
import { Select } from '../Select/Select'; import { Select } from '../Select/Select';
@ -44,7 +45,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<div className='grid grid-cols-1 gap-y-4 items-center'> <div className='grid grid-cols-1 gap-y-4 items-center'>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-displayedText`} id={`${props.properties.id}-displayedText`}
labelText='Displayed text' labelText={Text({ textId: '@ContainerDisplayedText' })}
inputKey='displayedText' inputKey='displayedText'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -54,7 +55,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<OrientationSelector <OrientationSelector
id='orientation' id='orientation'
name='Orientation' name='Orientation'
labelText='Orientation' labelText={Text({ textId: '@ContainerOrientation' })}
value={props.properties.orientation} value={props.properties.orientation}
onChange={props.onChange} onChange={props.onChange}
/> />
@ -62,14 +63,14 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<Category <Category
category={{ category={{
Type: 'Properties', Type: 'Properties',
DisplayedText: 'Properties' DisplayedText: Text({ textId: '@ContainerProperties' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >
<div className='grid grid-cols-1 gap-y-6 items-center prop-category-body'> <div className='grid grid-cols-1 gap-y-6 items-center prop-category-body'>
<div> <div>
<InputGroup <InputGroup
labelText='Name' labelText={Text({ textId: '@ContainerName' })}
inputKey='id' inputKey='id'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -79,7 +80,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
</div> </div>
<div> <div>
<InputGroup <InputGroup
labelText='Parent name' labelText={Text({ textId: '@ContainerParentName' })}
inputKey='parentId' inputKey='parentId'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -89,7 +90,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
</div> </div>
<div> <div>
<InputGroup <InputGroup
labelText='Type' labelText={Text({ textId: '@ContainerType' })}
inputKey='type' inputKey='type'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -102,7 +103,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<Category category={{ <Category category={{
Type: 'Position', Type: 'Position',
DisplayedText: 'Position' DisplayedText: Text({ textId: '@ContainerPosition' })
}} }}
defaultIsOpen={true} defaultIsOpen={true}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
@ -110,7 +111,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<div className='grid grid-cols-3 gap-y-2 items-center prop-category-body'> <div className='grid grid-cols-3 gap-y-2 items-center prop-category-body'>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-x`} id={`${props.properties.id}-x`}
labelText='x' labelText={Text({ textId: '@ContainerX' })}
inputKey='x' inputKey='x'
labelClassName='' labelClassName=''
inputClassName='col-span-2' inputClassName='col-span-2'
@ -134,7 +135,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
)} /> )} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-y`} id={`${props.properties.id}-y`}
labelText='y' labelText={Text({ textId: '@ContainerY' })}
inputKey='y' inputKey='y'
labelClassName='' labelClassName=''
inputClassName='col-span-2' inputClassName='col-span-2'
@ -158,16 +159,17 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
</div> </div>
</Category> </Category>
<Category category={{ <Category
Type: 'Size', category={{
DisplayedText: 'Size' Type: 'Size',
}} DisplayedText: Text({ textId: '@ContainerSize' })
heightClass={`${categoryHeight}`} }}
heightClass={`${categoryHeight}`}
> >
<div className='grid grid-cols-5 gap-y-2 items-center prop-category-body'> <div className='grid grid-cols-5 gap-y-2 items-center prop-category-body'>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-minWidth`} id={`${props.properties.id}-minWidth`}
labelText='Minimum Width' labelText={Text({ textId: '@ContainerMinWidth' })}
inputKey='minWidth' inputKey='minWidth'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
@ -177,7 +179,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
onChange={(value) => props.onChange('minWidth', Number(value))} /> onChange={(value) => props.onChange('minWidth', Number(value))} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-width`} id={`${props.properties.id}-width`}
labelText='Width' labelText={Text({ textId: '@ContainerWidth' })}
inputKey='width' inputKey='width'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
@ -189,7 +191,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
isDisabled={props.properties.isFlex} /> isDisabled={props.properties.isFlex} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-maxWidth`} id={`${props.properties.id}-maxWidth`}
labelText='Maximum Width' labelText={Text({ textId: '@ContainerMaxWidth' })}
inputKey='maxWidth' inputKey='maxWidth'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
@ -200,7 +202,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<div className='col-span-5 p-3'></div> <div className='col-span-5 p-3'></div>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-minHeight`} id={`${props.properties.id}-minHeight`}
labelText='Minimum Height' labelText={Text({ textId: '@ContainerMinHeight' })}
inputKey='minHeight' inputKey='minHeight'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
@ -210,7 +212,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
onChange={(value) => props.onChange('minHeight', Number(value))} /> onChange={(value) => props.onChange('minHeight', Number(value))} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-height`} id={`${props.properties.id}-height`}
labelText='Height' labelText={Text({ textId: '@ContainerHeight' })}
inputKey='height' inputKey='height'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
@ -223,7 +225,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
/> />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-maxHeight`} id={`${props.properties.id}-maxHeight`}
labelText='Maximum Height' labelText={Text({ textId: '@ContainerMaxHeight' })}
inputKey='maxHeight' inputKey='maxHeight'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
@ -236,14 +238,14 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<Category category={{ <Category category={{
Type: 'Margins', Type: 'Margins',
DisplayedText: 'Margins' DisplayedText: Text({ textId: '@ContainerMargins' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >
<div className='grid grid-cols-2 items-center gap-y-2 prop-category-body'> <div className='grid grid-cols-2 items-center gap-y-2 prop-category-body'>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-ml`} id={`${props.properties.id}-ml`}
labelText='Margin left' labelText={Text({ textId: '@ContainerMarginLeft' })}
inputKey='left' inputKey='left'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -253,7 +255,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
onChange={(value) => props.onChange('left', Number(value), PropertyType.Margin)} /> onChange={(value) => props.onChange('left', Number(value), PropertyType.Margin)} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-mb`} id={`${props.properties.id}-mb`}
labelText='Margin bottom' labelText={Text({ textId: '@ContainerMarginBottom' })}
inputKey='bottom' inputKey='bottom'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -263,7 +265,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
onChange={(value) => props.onChange('bottom', Number(value), PropertyType.Margin)} /> onChange={(value) => props.onChange('bottom', Number(value), PropertyType.Margin)} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-mt`} id={`${props.properties.id}-mt`}
labelText='Margin top' labelText={Text({ textId: '@ContainerMarginTop' })}
inputKey='top' inputKey='top'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -273,7 +275,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
onChange={(value) => props.onChange('top', Number(value), PropertyType.Margin)} /> onChange={(value) => props.onChange('top', Number(value), PropertyType.Margin)} />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-mr`} id={`${props.properties.id}-mr`}
labelText='Margin right' labelText={Text({ textId: '@ContainerMarginRight' })}
inputKey='right' inputKey='right'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -286,13 +288,13 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<Category category={{ <Category category={{
Type: 'Behaviors', Type: 'Behaviors',
DisplayedText: 'Behaviors' DisplayedText: Text({ textId: '@ContainerBehaviors' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >
<div className='grid grid-cols-2 items-center gap-y-2 prop-category-body'> <div className='grid grid-cols-2 items-center gap-y-2 prop-category-body'>
<ToggleButton <ToggleButton
labelText='Flex' labelText={Text({ textId: '@ContainerFlex' })}
inputKey='isFlex' inputKey='isFlex'
labelClassName='' labelClassName=''
inputClassName='ml-auto mr-auto block' inputClassName='ml-auto mr-auto block'
@ -301,7 +303,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
onChange={(event) => props.onChange('isFlex', event.target.checked)} onChange={(event) => props.onChange('isFlex', event.target.checked)}
/> />
<ToggleButton <ToggleButton
labelText='Anchor' labelText={Text({ textId: '@ContainerAnchor' })}
inputKey='isAnchor' inputKey='isAnchor'
labelClassName='' labelClassName=''
inputClassName='ml-auto mr-auto block' inputClassName='ml-auto mr-auto block'
@ -313,7 +315,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<Category category={{ <Category category={{
Type: 'Alignment', Type: 'Alignment',
DisplayedText: 'Alignment' DisplayedText: Text({ textId: '@ContainerAlignment' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >
@ -321,14 +323,14 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<PositionReferenceSelector <PositionReferenceSelector
id='positionReference' id='positionReference'
name='PositionReference' name='PositionReference'
labelText='Alignment' labelText={Text({ textId: '@ContainerAlignmentInput' })}
value={props.properties.positionReference} value={props.properties.positionReference}
onChange={props.onChange} onChange={props.onChange}
/> />
<div className='p-3'></div> <div className='p-3'></div>
<Select <Select
inputKey='linkedSymbolId' inputKey='linkedSymbolId'
labelText='Align with symbol' labelText={Text({ textId: '@ContainerAlignWithSymbol' })}
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
inputs={[...props.symbols.values()].map(symbol => ({ inputs={[...props.symbols.values()].map(symbol => ({
@ -343,7 +345,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<Category category={{ <Category category={{
Type: 'Dimensions', Type: 'Dimensions',
DisplayedText: 'Dimensions' DisplayedText: Text({ textId: '@ContainerDimensions' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >
@ -354,7 +356,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<PositionCheckboxes <PositionCheckboxes
id='showSelfDimensions' id='showSelfDimensions'
name='ShowSelfDimensions' name='ShowSelfDimensions'
labelText='Show dimension' labelText={Text({ textId: '@ContainerShowDimension' })}
value={props.properties.showSelfDimensions} value={props.properties.showSelfDimensions}
onChange={props.onChange} onChange={props.onChange}
/> />
@ -366,7 +368,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<PositionCheckboxes <PositionCheckboxes
id='showChildrenDimensions' id='showChildrenDimensions'
name='ShowChildrenDimensions' name='ShowChildrenDimensions'
labelText='Show overall dimension of its children' labelText={Text({ textId: '@ContainerShowChildrenDimension' })}
value={props.properties.showChildrenDimensions} value={props.properties.showChildrenDimensions}
onChange={props.onChange} onChange={props.onChange}
/> />
@ -374,33 +376,33 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
} }
{ {
SHOW_BORROWER_DIMENSIONS && SHOW_BORROWER_DIMENSIONS &&
<> <>
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<OrientationCheckboxes <OrientationCheckboxes
id='markPosition' id='markPosition'
name='MarkPosition' name='MarkPosition'
value={props.properties.markPosition} value={props.properties.markPosition}
labelText='Mark the position' labelText={Text({ textId: '@ContainerMarkPosition' })}
onChange={props.onChange} onChange={props.onChange}
/> />
</div> </div>
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<PositionCheckboxes <PositionCheckboxes
id='showDimensionWithMarks' id='showDimensionWithMarks'
name='ShowDimensionWithMarks' name='ShowDimensionWithMarks'
labelText='Show dimension with marked children' labelText={Text({ textId: '@ContainerShowDimensionWithMarks' })}
value={props.properties.showDimensionWithMarks} value={props.properties.showDimensionWithMarks}
onChange={props.onChange} onChange={props.onChange}
/> />
</div> </div>
</> </>
} }
</div> </div>
</Category> </Category>
<Category category={{ <Category category={{
Type: 'Style', Type: 'Style',
DisplayedText: 'Style' DisplayedText: Text({ textId: '@ContainerStyle' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >

View file

@ -353,7 +353,7 @@ function BuildPatterns(
continue; continue;
} }
for (let i = newParent.pattern.children.length - 1; i >= 0; i--) { for (let i = 0; i <= newParent.pattern.children.length - 1; i++) {
const nextNode = GetNextNode(newParent.parent, newParent.pattern, i, configs, patterns); const nextNode = GetNextNode(newParent.parent, newParent.pattern, i, configs, patterns);
if (nextNode === undefined) { if (nextNode === undefined) {

View file

@ -289,17 +289,17 @@ function SetContainer(
symbols symbols
); );
// sort the children list by their position
const parent = FindContainerById(containers, container.properties.parentId);
if (parent !== null && parent !== undefined) {
SortChildren(containers, parent);
}
// Apply special behaviors: rigid, flex, symbol, anchor // Apply special behaviors: rigid, flex, symbol, anchor
ApplyBehaviors(containers, container, symbols); ApplyBehaviors(containers, container, symbols);
// Apply special behaviors on siblings // Apply special behaviors on siblings
ApplyBehaviorsOnSiblingsChildren(containers, container, symbols); ApplyBehaviorsOnSiblingsChildren(containers, container, symbols);
// sort the children list by their position
const parent = FindContainerById(containers, container.properties.parentId);
if (parent !== null && parent !== undefined) {
SortChildren(containers, parent);
}
} }
/** /**

View file

@ -15,6 +15,7 @@ import { GetCurrentHistoryState } from '../Editor';
import { AddContainers } from './AddContainer'; import { AddContainers } from './AddContainer';
import { DeleteContainer } from './ContainerOperations'; import { DeleteContainer } from './ContainerOperations';
import { DeleteSymbol } from './SymbolOperations'; import { DeleteSymbol } from './SymbolOperations';
import { Text } from '../../Text/Text';
export function InitActions( export function InitActions(
menuActions: Map<string, IMenuAction[]>, menuActions: Map<string, IMenuAction[]>,
@ -28,8 +29,8 @@ export function InitActions(
'', '',
[ [
{ {
text: 'Undo', text: Text({ textId: '@Undo' }),
title: 'Undo last action', title: Text({ textId: '@UndoTitle' }),
shortcut: '<kbd>Ctrl</kbd>+<kbd>Z</kbd>', shortcut: '<kbd>Ctrl</kbd>+<kbd>Z</kbd>',
action: () => { action: () => {
if (historyCurrentStep <= 0) { if (historyCurrentStep <= 0) {
@ -39,8 +40,8 @@ export function InitActions(
} }
}, },
{ {
text: 'Redo', text: Text({ textId: '@Redo' }),
title: 'Redo last action', title: Text({ textId: '@RedoTitle' }),
shortcut: '<kbd>Ctrl</kbd>+<kbd>Y</kbd>', shortcut: '<kbd>Ctrl</kbd>+<kbd>Y</kbd>',
action: () => { action: () => {
if (historyCurrentStep >= history.length - 1) { if (historyCurrentStep >= history.length - 1) {
@ -55,8 +56,8 @@ export function InitActions(
menuActions.set( menuActions.set(
'elements-sidebar-row', 'elements-sidebar-row',
[{ [{
text: 'Delete', text: Text({ textId: '@DeleteContainer' }),
title: 'Delete the container', title: Text({ textId: '@DeleteContainerTitle' }),
shortcut: '<kbd>Suppr</kbd>', shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => { action: (target: HTMLElement) => {
const id = target.id; const id = target.id;
@ -73,8 +74,8 @@ export function InitActions(
menuActions.set( menuActions.set(
'symbols-sidebar-row', 'symbols-sidebar-row',
[{ [{
text: 'Delete', text: Text({ textId: '@DeleteSymbol' }),
title: 'Delete the container', title: Text({ textId: '@DeleteSymbolTitle' }),
shortcut: '<kbd>Suppr</kbd>', shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => { action: (target: HTMLElement) => {
const id = target.id; const id = target.id;

View file

@ -135,7 +135,6 @@ export function ElementsList(props: IElementsListProps): JSX.Element {
}): JSX.Element { }): JSX.Element {
const { container, depth } = containers[index]; const { container, depth } = containers[index];
const key = container.properties.id.toString(); const key = container.properties.id.toString();
const tabs = '|\t'.repeat(depth);
const text = container.properties.displayedText === key const text = container.properties.displayedText === key
? `${key}` ? `${key}`
: `${container.properties.displayedText}`; : `${container.properties.displayedText}`;
@ -144,29 +143,19 @@ export function ElementsList(props: IElementsListProps): JSX.Element {
props.selectedContainer !== null && props.selectedContainer !== null &&
props.selectedContainer.properties.id === container.properties.id; props.selectedContainer.properties.id === container.properties.id;
const selectedClass: string = isSelected
? 'border-l-4 bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50'
: 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400';
return ( return (
<button type="button" ElementsListRow(
className={`transition-all w-full border-blue-500 hover:shadow-lg elements-sidebar-row whitespace-pre key,
text-left text-sm font-medium inline-flex ${container.properties.type} ${selectedClass}`} container,
id={key} depth,
key={key} isSelected,
style={style} style,
title={container.properties.warning} text,
onClick={() => props.selectContainer(container.properties.id)} props.containers,
onDrop={(event) => HandleOnDrop(event, props.containers, props.mainContainer, props.addContainer)} props.mainContainer,
onDragOver={(event) => HandleDragOver(event, props.mainContainer)} props.addContainer,
onDragLeave={(event) => HandleDragLeave(event)} props.selectContainer
> )
{tabs}
{text}
{container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='w-8'/>
}
</button>
); );
} }
@ -193,3 +182,52 @@ export function ElementsList(props: IElementsListProps): JSX.Element {
</div> </div>
); );
} }
function ElementsListRow(
key: string,
container: IContainerModel,
depth: number,
isSelected: boolean,
style: React.CSSProperties,
text: string,
containers: Map<string, IContainerModel>,
mainContainer: IContainerModel,
addContainer: (index: number, type: string, parent: string) => void,
selectContainer: (containerId: string) => void
): JSX.Element {
const verticalBars: JSX.Element[] = [];
const verticalBarSelectedClass = isSelected
? 'border-l-blue-400 group-hover:border-l-blue-300'
: 'border-l-slate-400 group-hover:border-l-slate-300';
for (let i = 0; i < depth; i++) {
verticalBars.push(
<span
className={`h-full border-l-2 pr-2 ${verticalBarSelectedClass}`}
></span>
);
}
const buttonSelectedClass: string = isSelected
? 'bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50'
: 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400';
return <button type="button"
className={`transition-all border-blue-500 hover:shadow-lg elements-sidebar-row whitespace-pre
text-left text-sm font-medium inline-flex group ${container.properties.type} ${buttonSelectedClass}`}
id={key}
key={key}
style={style}
title={container.properties.warning}
onClick={() => selectContainer(container.properties.id)}
onDrop={(event) => HandleOnDrop(event, containers, mainContainer, addContainer)}
onDragOver={(event) => HandleDragOver(event, mainContainer)}
onDragLeave={(event) => HandleDragLeave(event)}
>
{verticalBars}
<div className='pt-2 pb-2 inline-flex'>
{text}
{container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='pl-2 w-7' />}
</div>
</button>;
}

View file

@ -0,0 +1,31 @@
import * as React from 'react';
import { createContext, useState } from 'react';
import { ILanguage } from '../../Interfaces/ILanguage';
import { languageOptions, translations } from '../../Translations/Translations';
import { DEFAULT_LANGUAGE } from '../../utils/default';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const LanguageContext = createContext<ILanguage>({
language: DEFAULT_LANGUAGE,
dictionary: translations.en
});
export function LanguageProvider({ children }: { children: React.ReactNode | React.ReactNode[] | undefined }): JSX.Element {
const [language, setLanguage] = useState(DEFAULT_LANGUAGE);
const provider = {
language,
dictionary: translations[language],
languageChange: (selected: string) => {
const newLanguage = languageOptions[selected] !== undefined ? selected : DEFAULT_LANGUAGE;
setLanguage(newLanguage);
}
};
return (
<LanguageContext.Provider
value={provider}
>
{ children }
</LanguageContext.Provider>
);
};

View file

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Text } from '../Text/Text';
interface IMainMenuProps { interface IMainMenuProps {
newEditor: () => void newEditor: () => void
@ -63,8 +64,16 @@ export function MainMenu(props: IMainMenuProps): JSX.Element {
sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2'> sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2'>
<button type="button" className='mainmenu-btn' onClick={() => { <button type="button" className='mainmenu-btn' onClick={() => {
props.newEditor(); props.newEditor();
}}>Start from scratch</button> }}>
<button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.Load)}>Load a configuration file</button> {Text({ textId: '@StartFromScratch' })}
</button>
<button
type="button"
className='mainmenu-btn'
onClick={() => setWindowState(WindowState.Load)}
>
{Text({ textId: '@LoadConfigFile' })}
</button>
</div> </div>
); );
} }

View file

@ -7,6 +7,7 @@ import { IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default'; import { DISABLE_API } from '../../utils/default';
import { GetCircularReplacer } from '../../utils/saveload'; import { GetCircularReplacer } from '../../utils/saveload';
import { TITLE_BAR_HEIGHT } from '../Sidebar/Sidebar'; import { TITLE_BAR_HEIGHT } from '../Sidebar/Sidebar';
import { Text } from '../Text/Text';
interface IMessagesProps { interface IMessagesProps {
historyState: IHistoryState historyState: IHistoryState
@ -49,8 +50,8 @@ export function Messages(props: IMessagesProps): JSX.Element {
<button <button
onClick={() => { props.clearMessage(); }} onClick={() => { props.clearMessage(); }}
className='h-full hover:bg-slate-400 rounded-lg p-1' className='h-full hover:bg-slate-400 rounded-lg p-1'
aria-label='Clear all messages' aria-label={Text({ textId: '@ClearAllMessages' })}
title='Clear all messages' title={Text({ textId: '@ClearAllMessages' })}
> >
<TrashIcon className='heroicon'></TrashIcon> <TrashIcon className='heroicon'></TrashIcon>
</button> </button>

View file

@ -74,7 +74,7 @@ function AddNewDimension(currentDepth: number, min: number, max: number, lastY:
const id = `dim-depth-${currentDepth}`; const id = `dim-depth-${currentDepth}`;
const xStart = min; const xStart = min;
const xEnd = max; const xEnd = max;
const y = (lastY + (DIMENSION_MARGIN * (currentDepth + 1))) / scale; const y = lastY + (DIMENSION_MARGIN * (currentDepth + 1)) / scale;
const strokeWidth = 1; const strokeWidth = 1;
const width = xEnd - xStart; const width = xEnd - xStart;
const text = width const text = width

View file

@ -1,5 +1,6 @@
import { ArrowUpOnSquareIcon, CameraIcon } from '@heroicons/react/24/outline'; import { ArrowUpOnSquareIcon, CameraIcon } from '@heroicons/react/24/outline';
import React from 'react'; import React from 'react';
import { Text } from '../Text/Text';
interface ISettingsProps { interface ISettingsProps {
saveEditorAsJSON: () => void saveEditorAsJSON: () => void
@ -12,19 +13,19 @@ export function Settings(props: ISettingsProps): JSX.Element {
m-2 md:text-xs font-bold'> m-2 md:text-xs font-bold'>
<button type="button" <button type="button"
className={'w-full transition-all flex sidebar-component'} className={'w-full transition-all flex sidebar-component'}
title='Export as JSON' title={Text({ textId: '@ExportAsJSON' })}
onClick={props.saveEditorAsJSON} onClick={props.saveEditorAsJSON}
> >
<ArrowUpOnSquareIcon className="heroicon w-16 h-7" /> <ArrowUpOnSquareIcon className="heroicon w-16 h-7" />
Export as JSON {Text({ textId: '@ExportAsJSON' })}
</button> </button>
<button type="button" <button type="button"
className={'w-full transition-all flex sidebar-component'} className={'w-full transition-all flex sidebar-component'}
title='Export as SVG' title={Text({ textId: '@ExportAsSVG' })}
onClick={props.saveEditorAsSVG} onClick={props.saveEditorAsSVG}
> >
<CameraIcon className="heroicon w-16 h-7" /> <CameraIcon className="heroicon w-16 h-7" />
Export as SVG {Text({ textId: '@ExportAsSVG' })}
</button> </button>
</div> </div>
); );

View file

@ -4,6 +4,7 @@ import { RestoreX, TransformX } from '../../utils/svg';
import { InputGroup } from '../InputGroup/InputGroup'; import { InputGroup } from '../InputGroup/InputGroup';
import { TextInputGroup } from '../InputGroup/TextInputGroup'; import { TextInputGroup } from '../InputGroup/TextInputGroup';
import { PositionReferenceSelector } from '../RadioGroupButtons/PositionReferenceSelector'; import { PositionReferenceSelector } from '../RadioGroupButtons/PositionReferenceSelector';
import { Text } from '../Text/Text';
interface ISymbolFormProps { interface ISymbolFormProps {
symbol: ISymbolModel symbol: ISymbolModel
@ -15,7 +16,7 @@ export function SymbolForm(props: ISymbolFormProps): JSX.Element {
return ( return (
<div className='grid grid-cols-2 gap-y-4'> <div className='grid grid-cols-2 gap-y-4'>
<InputGroup <InputGroup
labelText='Name' labelText={Text({ textId: '@SymbolName' })}
inputKey='id' inputKey='id'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -24,7 +25,7 @@ export function SymbolForm(props: ISymbolFormProps): JSX.Element {
isDisabled={true} /> isDisabled={true} />
<TextInputGroup <TextInputGroup
id='x' id='x'
labelText='x' labelText={Text({ textId: '@SymbolX' })}
inputKey='x' inputKey='x'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -33,7 +34,7 @@ export function SymbolForm(props: ISymbolFormProps): JSX.Element {
onChange={(value) => props.onChange('x', RestoreX(Number(value), props.symbol.width, props.symbol.config.PositionReference))} /> onChange={(value) => props.onChange('x', RestoreX(Number(value), props.symbol.width, props.symbol.config.PositionReference))} />
<TextInputGroup <TextInputGroup
id='height' id='height'
labelText='Height' labelText={Text({ textId: '@SymbolHeight' })}
inputKey='height' inputKey='height'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
@ -43,7 +44,7 @@ export function SymbolForm(props: ISymbolFormProps): JSX.Element {
onChange={(value) => props.onChange('height', Number(value))} /> onChange={(value) => props.onChange('height', Number(value))} />
<TextInputGroup <TextInputGroup
id='width' id='width'
labelText='Width' labelText={Text({ textId: '@SymbolWidth' })}
inputKey='width' inputKey='width'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''

View file

@ -0,0 +1,11 @@
import { useContext } from 'react';
import { LanguageContext } from '../LanguageProvider/LanguageProvider';
interface TextProps {
textId: string
};
export function Text({ textId }: TextProps): string {
const languageContext = useContext(LanguageContext);
return languageContext.dictionary[textId] ?? textId;
};

View file

@ -16,6 +16,7 @@ import { UseWorker, UseAsync } from './UseWorker';
import { FindContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
import { IEditorState } from '../../Interfaces/IEditorState'; import { IEditorState } from '../../Interfaces/IEditorState';
import { GetCurrentHistoryState } from '../Editor/Editor'; import { GetCurrentHistoryState } from '../Editor/Editor';
import { Text } from '../Text/Text';
export interface IUIProps { export interface IUIProps {
editorState: IEditorState editorState: IEditorState
@ -95,14 +96,14 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
switch (selectedSidebar) { switch (selectedSidebar) {
case SidebarType.Components: case SidebarType.Components:
leftSidebarTitle = 'Components'; leftSidebarTitle = Text({ textId: '@Components' });
leftChildren = <Components leftChildren = <Components
selectedContainer={selectedContainer} selectedContainer={selectedContainer}
componentOptions={configuration.AvailableContainers} componentOptions={configuration.AvailableContainers}
categories={configuration.Categories} categories={configuration.Categories}
buttonOnClick={methods.addContainer} buttonOnClick={methods.addContainer}
/>; />;
rightSidebarTitle = 'Elements'; rightSidebarTitle = Text({ textId: '@Elements' });
rightChildren = <ElementsList rightChildren = <ElementsList
containers={current.containers} containers={current.containers}
mainContainer={mainContainer} mainContainer={mainContainer}
@ -115,12 +116,12 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
break; break;
case SidebarType.Symbols: case SidebarType.Symbols:
leftSidebarTitle = 'Symbols'; leftSidebarTitle = Text({ textId: '@SymbolsLeft' });
leftChildren = <Symbols leftChildren = <Symbols
componentOptions={configuration.AvailableSymbols} componentOptions={configuration.AvailableSymbols}
buttonOnClick={methods.addSymbol} buttonOnClick={methods.addSymbol}
/>; />;
rightSidebarTitle = 'Symbols'; rightSidebarTitle = Text({ textId: '@SymbolsRight' });
rightChildren = <SymbolsSidebar rightChildren = <SymbolsSidebar
selectedSymbolId={current.selectedSymbolId} selectedSymbolId={current.selectedSymbolId}
symbols={current.symbols} symbols={current.symbols}
@ -130,7 +131,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
break; break;
case SidebarType.History: case SidebarType.History:
leftSidebarTitle = 'Timeline'; leftSidebarTitle = Text({ textId: '@Timeline' });
leftChildren = <History leftChildren = <History
history={editorState.history} history={editorState.history}
historyCurrentStep={editorState.historyCurrentStep} historyCurrentStep={editorState.historyCurrentStep}
@ -139,7 +140,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
break; break;
case SidebarType.Messages: case SidebarType.Messages:
leftSidebarTitle = 'Messages'; leftSidebarTitle = Text({ textId: '@Messages' });
leftChildren = <Messages leftChildren = <Messages
historyState={current} historyState={current}
messages={messages} messages={messages}
@ -148,7 +149,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
break; break;
case SidebarType.Settings: case SidebarType.Settings:
leftSidebarTitle = 'Settings'; leftSidebarTitle = Text({ textId: '@Settings' });
leftChildren = <Settings leftChildren = <Settings
saveEditorAsJSON={methods.saveEditorAsJSON} saveEditorAsJSON={methods.saveEditorAsJSON}
saveEditorAsSVG={methods.saveEditorAsSVG} saveEditorAsSVG={methods.saveEditorAsSVG}

View file

@ -3,17 +3,22 @@ import { AppState } from '../Enums/AppState';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { IConfiguration } from '../Interfaces/IConfiguration';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
import { ILanguage } from '../Interfaces/ILanguage';
import { languageOptions, translations } from '../Translations/Translations';
import { GetDefaultEditorState as GetDefaultEditorStateAction } from '../utils/default'; import { GetDefaultEditorState as GetDefaultEditorStateAction } from '../utils/default';
import { Revive, ReviveHistory as ReviveHistoryAction } from '../utils/saveload'; import { Revive, ReviveHistory as ReviveHistoryAction } from '../utils/saveload';
interface IAppEventParams {
root: Element | Document
languageContext: ILanguage
setEditor: (newState: IEditorState) => void
setAppState: (appState: AppState) => void
eventInitDict?: CustomEventInit
}
export interface IAppEvent { export interface IAppEvent {
name: string name: string
func: ( func: (params: IAppEventParams) => void
root: Element | Document,
setEditor: (newState: IEditorState) => void,
setAppState: (appState: AppState) => void,
eventInitDict?: CustomEventInit
) => void
} }
export const events: IAppEvent[] = [ export const events: IAppEvent[] = [
@ -21,12 +26,16 @@ export const events: IAppEvent[] = [
{ name: 'setAppState', func: SetAppState }, { name: 'setAppState', func: SetAppState },
{ name: 'reviveEditorState', func: ReviveEditorState }, { name: 'reviveEditorState', func: ReviveEditorState },
{ name: 'reviveHistory', func: ReviveHistory }, { name: 'reviveHistory', func: ReviveHistory },
{ name: 'getDefaultEditorState', func: GetDefaultEditorState } { name: 'getDefaultEditorState', func: GetDefaultEditorState },
{ name: 'addLanguage', func: AddLanguage },
{ name: 'setLanguage', func: SetLanguage },
{ name: 'getLanguages', func: GetLanguages }
]; ];
export function UseCustomEvents( export function UseCustomEvents(
root: Element | Document, root: Element | Document,
appRef: React.RefObject<HTMLDivElement>, appRef: React.RefObject<HTMLDivElement>,
languageContext: ILanguage,
setEditor: (newState: IEditorState) => void, setEditor: (newState: IEditorState) => void,
setAppState: (appState: AppState) => void setAppState: (appState: AppState) => void
): void { ): void {
@ -34,12 +43,13 @@ export function UseCustomEvents(
const funcs = new Map<string, () => void>(); const funcs = new Map<string, () => void>();
for (const event of events) { for (const event of events) {
function Func(eventInitDict?: CustomEventInit): void { function Func(eventInitDict?: CustomEventInit): void {
return event.func( return event.func({
root, root,
languageContext,
setEditor, setEditor,
setAppState, setAppState,
eventInitDict eventInitDict
); });
} }
appRef.current?.addEventListener(event.name, Func); appRef.current?.addEventListener(event.name, Func);
funcs.set(event.name, Func); funcs.set(event.name, Func);
@ -56,12 +66,12 @@ export function UseCustomEvents(
}); });
} }
function SetEditor( function SetEditor({
root: Element | Document, root,
setEditor: (newState: IEditorState) => void, setEditor,
setAppState: (appState: AppState) => void, setAppState,
eventInitDict?: CustomEventInit eventInitDict
): void { }: IAppEventParams): void {
const editor: IEditorState = eventInitDict?.detail; const editor: IEditorState = eventInitDict?.detail;
setEditor(editor); setEditor(editor);
setAppState(AppState.Loading); setAppState(AppState.Loading);
@ -69,45 +79,74 @@ function SetEditor(
root.dispatchEvent(customEvent); root.dispatchEvent(customEvent);
} }
function SetAppState( function SetAppState({
root: Element | Document, setAppState,
setEditor: (newState: IEditorState) => void, eventInitDict
setAppState: (appState: AppState) => void, }: IAppEventParams): void {
eventInitDict?: CustomEventInit
): void {
const appState: AppState = eventInitDict?.detail; const appState: AppState = eventInitDict?.detail;
setAppState(appState); setAppState(appState);
} }
function ReviveEditorState( function ReviveEditorState({
root: Element | Document, root,
setEditor: (newState: IEditorState) => void, eventInitDict
setAppState: (appState: AppState) => void, }: IAppEventParams): void {
eventInitDict?: CustomEventInit): void {
const anEditorState: IEditorState = eventInitDict?.detail; const anEditorState: IEditorState = eventInitDict?.detail;
Revive(anEditorState); Revive(anEditorState);
const customEvent = new CustomEvent<IEditorState>('reviveEditorState', { detail: anEditorState }); const customEvent = new CustomEvent<IEditorState>('reviveEditorState', { detail: anEditorState });
root.dispatchEvent(customEvent); root.dispatchEvent(customEvent);
} }
function ReviveHistory( function ReviveHistory({
root: Element | Document, root,
setEditor: (newState: IEditorState) => void, eventInitDict
setAppState: (appState: AppState) => void, }: IAppEventParams): void {
eventInitDict?: CustomEventInit): void {
const history: IHistoryState[] = eventInitDict?.detail; const history: IHistoryState[] = eventInitDict?.detail;
ReviveHistoryAction(history); ReviveHistoryAction(history);
const customEvent = new CustomEvent<IHistoryState[]>('reviveHistory', { detail: history }); const customEvent = new CustomEvent<IHistoryState[]>('reviveHistory', { detail: history });
root.dispatchEvent(customEvent); root.dispatchEvent(customEvent);
} }
function GetDefaultEditorState( function GetDefaultEditorState({
root: Element | Document, root,
setEditor: (newState: IEditorState) => void, eventInitDict
setAppState: (appState: AppState) => void, }: IAppEventParams): void {
eventInitDict?: CustomEventInit): void {
const configuration: IConfiguration = eventInitDict?.detail; const configuration: IConfiguration = eventInitDict?.detail;
const editorState = GetDefaultEditorStateAction(configuration); const editorState = GetDefaultEditorStateAction(configuration);
const customEvent = new CustomEvent<IEditorState>('getDefaultEditorState', { detail: editorState }); const customEvent = new CustomEvent<IEditorState>('getDefaultEditorState', { detail: editorState });
root.dispatchEvent(customEvent); root.dispatchEvent(customEvent);
} }
function AddLanguage({
root,
eventInitDict
}: IAppEventParams): void {
const language: ILanguage = eventInitDict?.detail.language;
const option: string = eventInitDict?.detail.option;
languageOptions[language.language] = option;
translations[language.language] = language.dictionary;
const customEvent = new CustomEvent('addLanguage');
root.dispatchEvent(customEvent);
}
function SetLanguage({
root,
languageContext,
eventInitDict
}: IAppEventParams): void {
const language: string = eventInitDict?.detail;
let success = false;
if (languageContext.languageChange !== undefined) {
languageContext.languageChange(language);
success = true;
}
const customEvent = new CustomEvent<boolean>('setLanguage', { detail: success });
root.dispatchEvent(customEvent);
}
function GetLanguages({
root
}: IAppEventParams): void {
const customEvent = new CustomEvent<Record<string, Record<string, string>>>('getLanguages', { detail: translations });
root.dispatchEvent(customEvent);
}

View file

@ -9,6 +9,7 @@ import { IHistoryState } from '../Interfaces/IHistoryState';
import { FindContainerById } from '../utils/itertools'; import { FindContainerById } from '../utils/itertools';
import { GetCircularReplacer } from '../utils/saveload'; import { GetCircularReplacer } from '../utils/saveload';
// TODO: set the params of func in an interface
export interface IEditorEvent { export interface IEditorEvent {
name: string name: string
func: ( func: (

View file

@ -6,6 +6,7 @@ import { IAction } from './IAction';
import { IMargin } from './IMargin'; import { IMargin } from './IMargin';
import { Orientation } from '../Enums/Orientation'; import { Orientation } from '../Enums/Orientation';
import { Position } from '../Enums/Position'; import { Position } from '../Enums/Position';
import { IKeyValue } from './IKeyValue';
/** Model of available container used in application configuration */ /** Model of available container used in application configuration */
export interface IAvailableContainer { export interface IAvailableContainer {
@ -161,5 +162,5 @@ export interface IAvailableContainer {
* (optional) * (optional)
* User data that can be used for data storage or custom SVG * User data that can be used for data storage or custom SVG
*/ */
UserData?: object UserData?: IKeyValue[]
} }

View file

@ -3,6 +3,7 @@ import { PositionReference } from '../Enums/PositionReference';
import { IMargin } from './IMargin'; import { IMargin } from './IMargin';
import { Orientation } from '../Enums/Orientation'; import { Orientation } from '../Enums/Orientation';
import { Position } from '../Enums/Position'; import { Position } from '../Enums/Position';
import { IKeyValue } from './IKeyValue';
/** /**
* Properties of a container * Properties of a container
@ -122,5 +123,5 @@ export interface IContainerProperties {
* (optional) * (optional)
* User data that can be used for data storage or custom SVG * User data that can be used for data storage or custom SVG
*/ */
userData?: object userData?: IKeyValue[]
} }

View file

@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
export interface IKeyValue {
Key: string
Value: string
}

View file

@ -0,0 +1,5 @@
export interface ILanguage {
language: string
dictionary: Record<string, string>
languageChange?: (selected: string) => void
}

View file

@ -0,0 +1,14 @@
import en from './translation.en.json';
import fr from './translation.fr.json';
export const translations: Record<string, Record<string, string>> = {
en,
fr
};
// Use languageOptions to restrict existing dictionary
// and create a select input
export const languageOptions: Record<string, string> = {
en: 'English',
fr: 'French'
};

View file

@ -0,0 +1,70 @@
{
"@StartFromScratch": "Start from scratch",
"@LoadConfigFile": "Load a configuration file",
"@GoBack": "Go back",
"@Components": "Components",
"@Elements": "Elements",
"@Symbols": "Symbols",
"@SymbolsLeft": "Symbols",
"@SymbolsRight": "Symbols",
"@Timeline": "Timeline",
"@Messages": "Messages",
"@Settings": "Settings",
"@Undo": "Undo",
"@UndoTitle": "Undo last action",
"@Redo": "Redo",
"@RedoTitle": "Redo last action",
"@DeleteContainer": "Delete",
"@DeleteContainerTitle": "Delete the container",
"@DeleteSymbol": "Delete",
"@DeleteSymbolTitle": "Delete the container",
"@ExportAsJSON": "Export as JSON",
"@ExportAsSVG": "Export as SVG",
"@HideDisabledComponents": "Hide disabled components",
"@ShowDisabledComponents": "Show disabled components",
"@ClearAllMessages": "Clear all messages",
"@ContainerDisplayedText": "Displayed text",
"@ContainerOrientation": "Orientation",
"@ContainerProperties": "Properties",
"@ContainerName": "Name",
"@ContainerParentName": "Parent name",
"@ContainerType": "Type",
"@ContainerPosition": "Position",
"@ContainerX": "x",
"@ContainerY": "y",
"@ContainerSize": "Size",
"@ContainerMinWidth": "Minimum Width",
"@ContainerWidth": "Width",
"@ContainerMaxWidth": "Maximum Width",
"@ContainerMinHeight": "Minimum Height",
"@ContainerHeight": "Height",
"@ContainerMaxHeight": "Maximum Height",
"@ContainerMargins": "Margins",
"@ContainerMarginLeft": "Margin Left",
"@ContainerMarginBottom": "Margin Bottom",
"@ContainerMarginTop": "Margin Top",
"@ContainerMarginRight": "Margin Right",
"@ContainerBehaviors": "Behaviors",
"@ContainerFlex": "Flex",
"@ContainerAnchor": "Lock position and size",
"@ContainerAlignment": "Alignment",
"@ContainerAlignmentInput": "Alignment",
"@ContainerAlignWithSymbol": "Align to symbol",
"@ContainerDimensions": "Dimensions",
"@ContainerShowDimension": "Show Dimension",
"@ContainerShowChildrenDimension": "Show surrounding dimension of children",
"@ContainerMarkPosition": "Mark the position for the parents",
"@ContainerShowDimensionWithMarks": "Show dimension with marked children",
"@ContainerStyle": "Style",
"@SymbolName": "Name",
"@SymbolX": "x",
"@SymbolHeight": "Height",
"@SymbolWidth": "Width"
}

View file

@ -0,0 +1,70 @@
{
"@StartFromScratch": "Partir de zéro",
"@LoadConfigFile": "Charger un fichier de configuration",
"@GoBack": "Revenir",
"@Components": "Composants",
"@Elements": "Éléments",
"@Symbols": "Symboles",
"@SymbolsLeft": "Symboles",
"@SymbolsRight": "Symboles",
"@Timeline": "Chronologie",
"@Messages": "Messages",
"@Settings": "Paramètres",
"@Undo": "Annuler",
"@UndoTitle": "Annuler la dernière action",
"@Redo": "Refaire",
"@RedoTitle": "Refaire la dernière action",
"@DeleteContainer": "Supprimer",
"@DeleteContainerTitle": "Supprimer le conteneur",
"@DeleteSymbol": "Supprimer",
"@DeleteSymbolTitle": "Supprimer le symbole",
"@ExportAsJSON": "Exporter en JSON",
"@ExportAsSVG": "Exporter en SVG",
"@HideDisabledComponents": "Cacher les composants désactivés",
"@ShowDisabledComponents": "Montrer les composants activés",
"@ClearAllMessages": "Effacer tous les messages",
"@ContainerDisplayedText": "Texte affiché",
"@ContainerOrientation": "Orientation",
"@ContainerProperties": "Propriétés",
"@ContainerName": "Nom",
"@ContainerParentName": "Nom du parent",
"@ContainerType": "Type",
"@ContainerPosition": "Position",
"@ContainerX": "x",
"@ContainerY": "y",
"@ContainerSize": "Taille",
"@ContainerMinWidth": "Largeur minimale",
"@ContainerWidth": "Largeur",
"@ContainerMaxWidth": "Largeur maximale",
"@ContainerMinHeight": "Hauteur minimale",
"@ContainerHeight": "Hauteur",
"@ContainerMaxHeight": "Hauteur maximale",
"@ContainerMargins": "Marges",
"@ContainerMarginLeft": "Marge gauche",
"@ContainerMarginBottom": "Marge inférieure",
"@ContainerMarginTop": "Marge supérieure",
"@ContainerMarginRight": "Marge droite",
"@ContainerBehaviors": "Comportements",
"@ContainerFlex": "Flexible",
"@ContainerAnchor": "Verrouiller la position et la taille",
"@ContainerAlignment": "Alignement",
"@ContainerAlignmentInput": "Alignement",
"@ContainerAlignWithSymbol": "Aligner au symbole",
"@ContainerDimensions": "Cotations",
"@ContainerShowDimension": "Afficher les cotations",
"@ContainerShowChildrenDimension": "Afficher les cotations englobante des enfants",
"@ContainerMarkPosition": "Marquer la position pour les parents",
"@ContainerShowDimensionWithMarks": "Afficher les cotations avec les enfants marqués",
"@ContainerStyle": "Style",
"@SymbolName": "Nom",
"@SymbolX": "x",
"@SymbolHeight": "Hauteur",
"@SymbolWidth": "Largeur"
}

View file

@ -49,7 +49,7 @@
} }
.elements-sidebar-row { .elements-sidebar-row {
@apply pl-6 pr-6 pt-2 pb-2 w-full @apply pl-6 pr-6 w-full
} }
.symbols-sidebar-row { .symbols-sidebar-row {
@apply elements-sidebar-row @apply elements-sidebar-row

View file

@ -1,12 +1,15 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { App } from './Components/App/App'; import { App } from './Components/App/App';
import { LanguageProvider } from './Components/LanguageProvider/LanguageProvider';
import './index.scss'; import './index.scss';
function RenderRoot(root: Element | Document): void { function RenderRoot(root: Element | Document): void {
ReactDOM.createRoot(root.querySelector('#root') as HTMLDivElement).render( ReactDOM.createRoot(root.querySelector('#root') as HTMLDivElement).render(
<React.StrictMode> <React.StrictMode>
<App root={root}/> <LanguageProvider>
<App root={root}/>
</LanguageProvider>
</React.StrictMode> </React.StrictMode>
); );
} }

View file

@ -17,6 +17,8 @@ export const FAST_BOOT = 0;
/** Disable any call to the API (default = false) */ /** Disable any call to the API (default = false) */
export const DISABLE_API = false; export const DISABLE_API = false;
export const DEFAULT_LANGUAGE = 'fr';
/** /**
* Replace the SVG viewer by a canvas * Replace the SVG viewer by a canvas
* Better compatibility with Gecko and WebKit engines like Firefox and Safari. * Better compatibility with Gecko and WebKit engines like Firefox and Safari.
@ -234,8 +236,6 @@ export function GetDefaultContainerProps(type: string,
height: number, height: number,
containerConfig: IAvailableContainer): IContainerProperties { containerConfig: IAvailableContainer): IContainerProperties {
const orientation = containerConfig.Orientation ?? Orientation.Horizontal; const orientation = containerConfig.Orientation ?? Orientation.Horizontal;
const defaultIsFlex = (orientation === Orientation.Vertical && containerConfig.Height === undefined) ||
(orientation === Orientation.Horizontal && containerConfig.Width === undefined);
return ({ return ({
id: `${type}-${typeCount}`, id: `${type}-${typeCount}`,
type, type,
@ -249,7 +249,7 @@ export function GetDefaultContainerProps(type: string,
width, width,
height, height,
isAnchor: containerConfig.IsAnchor ?? false, isAnchor: containerConfig.IsAnchor ?? false,
isFlex: containerConfig.IsFlex ?? defaultIsFlex, isFlex: containerConfig.IsFlex ?? false,
positionReference: containerConfig.PositionReference ?? PositionReference.TopLeft, positionReference: containerConfig.PositionReference ?? PositionReference.TopLeft,
minWidth: containerConfig.MinWidth ?? 1, minWidth: containerConfig.MinWidth ?? 1,
maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER, maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER,

View file

@ -2,6 +2,7 @@
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
import { IContainerModel } from '../Interfaces/IContainerModel'; import { IContainerModel } from '../Interfaces/IContainerModel';
import { ISymbolModel } from '../Interfaces/ISymbolModel';
/** /**
* Revive the Editor state * Revive the Editor state
@ -29,13 +30,14 @@ export function ReviveState(state: IHistoryState): void {
return; return;
} }
state.symbols = new Map(state.symbols); const symbols: Array<{ Key: string, Value: ISymbolModel }> = (state.symbols) as any;
state.symbols = new Map(symbols.map(({ Key, Value }) => [Key, Value]));
for (const symbol of state.symbols.values()) { for (const symbol of state.symbols.values()) {
symbol.linkedContainers = new Set(symbol.linkedContainers); symbol.linkedContainers = new Set(symbol.linkedContainers);
} }
const containers: Array<{ Key: string, Value: IContainerModel }> = (state.containers) as any; const containers: Array<{ Key: string, Value: IContainerModel }> = (state.containers) as any;
state.containers = new Map(containers.map(({ Key, Value }: {Key: string, Value: IContainerModel}) => [Key, Value])); state.containers = new Map(containers.map(({ Key, Value }) => [Key, Value]));
} }
export function GetCircularReplacer(): (key: any, value: object | Map<string, any> | null) => object | null | undefined { export function GetCircularReplacer(): (key: any, value: object | Map<string, any> | null) => object | null | undefined {