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:
parent
7f3f6a489a
commit
444b96736a
17 changed files with 132 additions and 24 deletions
2
public/smartcomponent/svg-layout-designer.html
Normal file
2
public/smartcomponent/svg-layout-designer.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<div id="root">
|
||||||
|
</div>
|
67
public/smartcomponent/svg-layout-designer.ts
Normal file
67
public/smartcomponent/svg-layout-designer.ts
Normal 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' }
|
||||||
|
});
|
||||||
|
}
|
14
public/smartcomponent/svg-layout-designer.xcomponent
Normal file
14
public/smartcomponent/svg-layout-designer.xcomponent
Normal 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>
|
|
@ -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}
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}`
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
21
src/main.tsx
21
src/main.tsx
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue