Merged PR 168: Add SmartComponent source code + Restrict Events by giving a root at the first render + Added Render function to a namespace

- Add smartcomponent source code to public/
- Restrict Events by giving a root at the first render + Added Render function to a namespace
- Add attribute type="button" to all buttons
This commit is contained in:
Eric Nguyen 2022-08-26 09:13:51 +00:00
parent 7f3f6a489a
commit 444b96736a
17 changed files with 132 additions and 24 deletions

View file

@ -0,0 +1,2 @@
<div id="root">
</div>

View file

@ -0,0 +1,67 @@
namespace SmartBusiness.Web.Components {
export class SVGLayoutDesigner extends Components.ComponentBase {
public constructor(componentInfo: KnockoutComponentTypes.ComponentInfo, params: any) {
super(componentInfo, params);
// this.$component.id = SVGLayoutDesigner.generateUUID();
setTimeout(() => (window as any).SVGLayoutDesigner.Render(this.$component[0]));
this.InitEventsListener();
}
public static generateUUID() { // Public Domain/MIT
let d = new Date().getTime();//Timestamp
let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16;//random number between 0 and 16
if(d > 0){//Use timestamp until depleted
r = (d + r)%16 | 0;
d = Math.floor(d/16);
} else {//Use microseconds since page-load if supported
r = (d2 + r)%16 | 0;
d2 = Math.floor(d2/16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
public GetEditorComponent() {
return this.$component[0].querySelector('.Editor');
}
public GetCurrentHistoryState() {
this.GetEditorComponent().dispatchEvent(new CustomEvent('getCurrentHistoryState'));
}
public GetEditorState() {
this.GetEditorComponent().dispatchEvent(new CustomEvent('getEditorState'));
}
public SetEditorState(editorState: IEditorState) {
this.GetEditorComponent().dispatchEvent(new CustomEvent('SetEditorState', { detail: editorState }));
}
public AppendNewHistoryState(historyState: IHistoryState) {
this.GetEditorComponent().dispatchEvent(new CustomEvent('appendNewState', { detail: historyState }));
}
public OHistoryState: KnockoutObservable<any>;
private InitEventsListener() {
this.$component[0].addEventListener('getCurrentHistoryState', (e: CustomEvent) => {
this.OHistoryState(e.detail);
console.log(this.OHistoryState());
});
this.$component[0].addEventListener('getEditorState', (e) => console.log((e as any).detail));
}
}
ko.components.register('svg-layout-designer', {
viewModel: {
createViewModel: function (params, componentInfo) {
return new SmartBusiness.Web.Components.SVGLayoutDesigner(componentInfo, params);
}
},
template: { element: 'svg-layout-designer' }
});
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<ComponentModel xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.Techform.com/SmartExpert/2009/05">
<HasContent>false</HasContent>
<Id>0A61000D-FC2D-4490-BB3E-0FAED2AF3FDC</Id>
<ImageUrl />
<ItemName>svg-layout-designer</ItemName>
<Parameters>
<ParameterModel>
<ItemName>viewModel</ItemName>
<Text>ViewModel</Text>
</ParameterModel>
</Parameters>
<Text>svg-layout-designer</Text>
</ComponentModel>

View file

@ -11,6 +11,7 @@ import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default
// App will never have props // App will never have props
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IAppProps { interface IAppProps {
root: Element | Document
} }
export const App: React.FunctionComponent<IAppProps> = (props) => { export const App: React.FunctionComponent<IAppProps> = (props) => {
@ -59,6 +60,7 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
return ( return (
<div> <div>
<Editor <Editor
root={props.root}
configuration={editorState.configuration} configuration={editorState.configuration}
history={editorState.history} history={editorState.history}
historyCurrentStep={editorState.historyCurrentStep} historyCurrentStep={editorState.historyCurrentStep}

View file

@ -10,7 +10,7 @@ interface IBarIconProps {
export const BarIcon: React.FC<IBarIconProps> = (props) => { export const BarIcon: React.FC<IBarIconProps> = (props) => {
const isActiveClasses = props.isActive ? 'border-l-4 border-blue-500 bg-slate-200' : ''; const isActiveClasses = props.isActive ? 'border-l-4 border-blue-500 bg-slate-200' : '';
return ( return (
<button <button type="button"
className={`bar-btn group ${isActiveClasses}`} className={`bar-btn group ${isActiveClasses}`}
title={props.title} title={props.title}
onClick={() => props.onClick()} onClick={() => props.onClick()}

View file

@ -14,6 +14,7 @@ import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, Se
import { findContainerById } from '../../utils/itertools'; import { findContainerById } from '../../utils/itertools';
interface IEditorProps { interface IEditorProps {
root: Element | Document
configuration: IConfiguration configuration: IConfiguration
history: IHistoryState[] history: IHistoryState[]
historyCurrentStep: number historyCurrentStep: number
@ -57,6 +58,7 @@ function useShortcuts(
} }
function useWindowEvents( function useWindowEvents(
root: Element | Document,
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
configuration: IConfiguration, configuration: IConfiguration,
@ -75,6 +77,7 @@ function useWindowEvents(
const funcs = new Map<string, () => void>(); const funcs = new Map<string, () => void>();
for (const event of events) { for (const event of events) {
const func = (eventInitDict?: CustomEventInit): void => event.func( const func = (eventInitDict?: CustomEventInit): void => event.func(
root,
editorState, editorState,
setHistory, setHistory,
setHistoryCurrentStep, setHistoryCurrentStep,
@ -102,6 +105,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
useShortcuts(history, historyCurrentStep, setHistoryCurrentStep); useShortcuts(history, historyCurrentStep, setHistoryCurrentStep);
useWindowEvents( useWindowEvents(
props.root,
history, history,
historyCurrentStep, historyCurrentStep,
props.configuration, props.configuration,

View file

@ -81,7 +81,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
: 'bg-slate-300/60 hover:bg-slate-300'; : 'bg-slate-300/60 hover:bg-slate-300';
return ( return (
<button <button type="button"
className={`w-full border-blue-500 elements-sidebar-row whitespace-pre className={`w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`} text-left text-sm font-medium transition-all ${selectedClass}`}
id={key} id={key}

View file

@ -25,7 +25,7 @@ export const FloatingButton: React.FC<IFloatingButtonProps> = (props: IFloatingB
<div className={`transition-all flex flex-col gap-2 items-center ${buttonListClasses}`}> <div className={`transition-all flex flex-col gap-2 items-center ${buttonListClasses}`}>
{ props.children } { props.children }
</div> </div>
<button <button type="button"
className={'transition-all w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'} className={'transition-all w-14 h-14 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Open menu' title='Open menu'
onClick={() => toggleState(isHidden, setHidden)} onClick={() => toggleState(isHidden, setHidden)}

View file

@ -21,7 +21,7 @@ export const History: React.FC<IHistoryProps> = (props: IHistoryProps) => {
: 'bg-slate-500 hover:bg-slate-700'; : 'bg-slate-500 hover:bg-slate-700';
return ( return (
<button <button type="button"
key={reversedIndex} key={reversedIndex}
style={style} style={style}
onClick={() => props.jumpTo(reversedIndex)} onClick={() => props.jumpTo(reversedIndex)}

View file

@ -36,7 +36,7 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
"/> "/>
</label> </label>
</form> </form>
<button <button type="button"
onClick={() => setWindowState(WindowState.MAIN)} onClick={() => setWindowState(WindowState.MAIN)}
className='normal-btn block className='normal-btn block
mt-8 ' mt-8 '
@ -49,8 +49,8 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
default: default:
return ( return (
<div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'> <div className='absolute bg-blue-50 p-12 rounded-lg drop-shadow-lg grid grid-cols-1 md:grid-cols-2 gap-8 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<button className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button> <button type="button" className='mainmenu-btn' onClick={props.newEditor}>Start from scratch</button>
<button className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button> <button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button>
</div> </div>
); );
} }

View file

@ -8,7 +8,7 @@ interface IMenuItemProps {
export const MenuItem: React.FC<IMenuItemProps> = (props) => { export const MenuItem: React.FC<IMenuItemProps> = (props) => {
return ( return (
<button <button type="button"
className={props.className} className={props.className}
onClick={() => props.onClick()}>{props.text} onClick={() => props.onClick()}>{props.text}
</button> </button>

View file

@ -14,7 +14,7 @@ function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => { export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => {
const listElements = props.componentOptions.map(componentOption => const listElements = props.componentOptions.map(componentOption =>
<button <button type="button"
className='justify-center transition-all sidebar-component' className='justify-center transition-all sidebar-component'
key={componentOption.Type} key={componentOption.Type}
id={componentOption.Type} id={componentOption.Type}

View file

@ -16,7 +16,7 @@ export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
const listElements = props.componentOptions.map(componentOption => { const listElements = props.componentOptions.map(componentOption => {
if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) { if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) {
const url = componentOption.Image.Base64Image ?? componentOption.Image.Url; const url = componentOption.Image.Base64Image ?? componentOption.Image.Url;
return (<button return (<button type="button"
className='justify-center sidebar-component-card hover:h-full' className='justify-center sidebar-component-card hover:h-full'
key={componentOption.Name} key={componentOption.Name}
id={componentOption.Name} id={componentOption.Name}
@ -37,7 +37,7 @@ export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
</button>); </button>);
} }
return (<button return (<button type="button"
className='group justify-center sidebar-component hover:h-full' className='group justify-center sidebar-component hover:h-full'
key={componentOption.Name} key={componentOption.Name}
id={componentOption.Name} id={componentOption.Name}

View file

@ -56,7 +56,7 @@ export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSi
: 'bg-slate-300/60 hover:bg-slate-300'; : 'bg-slate-300/60 hover:bg-slate-300';
return ( return (
<button <button type="button"
className={ className={
`w-full border-blue-500 elements-sidebar-row whitespace-pre `w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}` text-left text-sm font-medium transition-all ${selectedClass}`

View file

@ -109,14 +109,14 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
/> />
<FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}> <FloatingButton className={`fixed z-10 flex flex-col gap-2 items-center bottom-40 ${buttonRightOffsetClasses}`}>
<button <button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'} className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as JSON' title='Export as JSON'
onClick={props.SaveEditorAsJSON} onClick={props.SaveEditorAsJSON}
> >
<UploadIcon className="heroicon text-white" /> <UploadIcon className="heroicon text-white" />
</button> </button>
<button <button type="button"
className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'} className={'transition-all w-10 h-10 p-2 align-middle items-center justify-center rounded-full bg-blue-500 hover:bg-blue-800'}
title='Export as SVG' title='Export as SVG'
onClick={props.SaveEditorAsSVG} onClick={props.SaveEditorAsSVG}

View file

@ -11,19 +11,26 @@ const initEditor = (configuration: IConfiguration): void => {
} }
const getEditorState = (editorState: IEditorState): void => { const getEditorState = (
root: Element | Document,
editorState: IEditorState
): void => {
const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: editorState }); const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: editorState });
document.dispatchEvent(customEvent); root.dispatchEvent(customEvent);
}; };
const getCurrentHistoryState = (editorState: IEditorState): void => { const getCurrentHistoryState = (
root: Element | Document,
editorState: IEditorState
): void => {
const customEvent = new CustomEvent<IHistoryState>( const customEvent = new CustomEvent<IHistoryState>(
'getCurrentHistoryState', 'getCurrentHistoryState',
{ detail: editorState.history[editorState.historyCurrentStep] }); { detail: editorState.history[editorState.historyCurrentStep] });
document.dispatchEvent(customEvent); root.dispatchEvent(customEvent);
}; };
const appendNewState = ( const appendNewState = (
root: Element | Document,
editorState: IEditorState, editorState: IEditorState,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>, setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
@ -41,6 +48,7 @@ const appendNewState = (
export interface IEditorEvent { export interface IEditorEvent {
name: string name: string
func: ( func: (
root: Element | Document,
editorState: IEditorState, editorState: IEditorState,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>, setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>, setHistoryCurrentStep: Dispatch<SetStateAction<number>>,

View file

@ -3,8 +3,19 @@ import ReactDOM from 'react-dom/client';
import { App } from './Components/App/App'; import { App } from './Components/App/App';
import './index.scss'; import './index.scss';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( function render(root: Element | Document): void {
<React.StrictMode> ReactDOM.createRoot(root.querySelector('#root') as HTMLDivElement).render(
<App /> <React.StrictMode>
</React.StrictMode> <App root={root}/>
); </React.StrictMode>
);
}
namespace SVGLayoutDesigner {
export const Render = render;
}
(window as any).SVGLayoutDesigner = SVGLayoutDesigner;
render(document);