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
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IAppProps {
root: Element | Document
}
export const App: React.FunctionComponent<IAppProps> = (props) => {
@ -59,6 +60,7 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
return (
<div>
<Editor
root={props.root}
configuration={editorState.configuration}
history={editorState.history}
historyCurrentStep={editorState.historyCurrentStep}

View file

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

View file

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

View file

@ -81,7 +81,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (
: 'bg-slate-300/60 hover:bg-slate-300';
return (
<button
<button type="button"
className={`w-full border-blue-500 elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`}
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}`}>
{ props.children }
</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'}
title='Open menu'
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';
return (
<button
<button type="button"
key={reversedIndex}
style={style}
onClick={() => props.jumpTo(reversedIndex)}

View file

@ -36,7 +36,7 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
"/>
</label>
</form>
<button
<button type="button"
onClick={() => setWindowState(WindowState.MAIN)}
className='normal-btn block
mt-8 '
@ -49,8 +49,8 @@ export const MainMenu: React.FC<IMainMenuProps> = (props) => {
default:
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'>
<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={props.newEditor}>Start from scratch</button>
<button type="button" className='mainmenu-btn' onClick={() => setWindowState(WindowState.LOAD)}>Load a configuration file</button>
</div>
);
}

View file

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

View file

@ -14,7 +14,7 @@ function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
export const Sidebar: React.FC<ISidebarProps> = (props: ISidebarProps) => {
const listElements = props.componentOptions.map(componentOption =>
<button
<button type="button"
className='justify-center transition-all sidebar-component'
key={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 => {
if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) {
const url = componentOption.Image.Base64Image ?? componentOption.Image.Url;
return (<button
return (<button type="button"
className='justify-center sidebar-component-card hover:h-full'
key={componentOption.Name}
id={componentOption.Name}
@ -37,7 +37,7 @@ export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
</button>);
}
return (<button
return (<button type="button"
className='group justify-center sidebar-component hover:h-full'
key={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';
return (
<button
<button type="button"
className={
`w-full border-blue-500 elements-sidebar-row whitespace-pre
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}`}>
<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'}
title='Export as JSON'
onClick={props.SaveEditorAsJSON}
>
<UploadIcon className="heroicon text-white" />
</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'}
title='Export as SVG'
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 });
document.dispatchEvent(customEvent);
root.dispatchEvent(customEvent);
};
const getCurrentHistoryState = (editorState: IEditorState): void => {
const getCurrentHistoryState = (
root: Element | Document,
editorState: IEditorState
): void => {
const customEvent = new CustomEvent<IHistoryState>(
'getCurrentHistoryState',
{ detail: editorState.history[editorState.historyCurrentStep] });
document.dispatchEvent(customEvent);
root.dispatchEvent(customEvent);
};
const appendNewState = (
root: Element | Document,
editorState: IEditorState,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
@ -41,6 +48,7 @@ const appendNewState = (
export interface IEditorEvent {
name: string
func: (
root: Element | Document,
editorState: IEditorState,
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>,

View file

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