namespace SmartBusiness.Web.Components { /** * Types macros */ type IHistoryState = SVGLD.IHistoryState; type IEditorState = SVGLD.IEditorState; export class SVGLayoutDesigner extends Components.ComponentBase { private _hooks: Record void>; public App: AppController; public Editor: EditorController; public constructor(componentInfo: KnockoutComponentTypes.ComponentInfo, params: any) { super(componentInfo, params); this.App = new AppController(this, this.$component); this.Editor = new EditorController(this, this.$component); this._hooks = {}; } /** * Return the root HTML component of the SmartComponent * In the iframe, it would be the document. */ public GetRootComponent() { return this.$component[0] .querySelector('iframe') .contentDocument; } /** * Add the a new event listener that will be delete on call, * optionnally call a callback * @param callback Callback function to call in the event listener * @param eventType Event type for the listener to listen to */ public AddEventListener(eventType: string, callback: ((...args: any[]) => void) | undefined) { const root = this.GetRootComponent(); const listener = (e: CustomEvent) => { e.target.removeEventListener(e.type, listener); callback && callback(e.detail); }; root.addEventListener(eventType, listener); } /// Hooks /// private static EDITOR_LISTENER_TYPE = 'editorListener'; /** * Add a hook to the editor state change. * After every time an action is perform on the editor, the callback will be called * @param callback Callback to add that listen to the event */ public AddEditorListenerHook(hookId: string, callback: (state: IEditorState) => void): void { const root = this.GetRootComponent(); const customEvent = (e: CustomEvent) => { callback(e.detail); } if (this._hooks[hookId] !== undefined) { console.error(`HookId is already occupied. Please use a different HookId: ${hookId}`); return; } this._hooks[hookId] = customEvent; root.addEventListener(SVGLayoutDesigner.EDITOR_LISTENER_TYPE, customEvent); } /** * Remove a hook to the editor state change. * @param callback Callback to remove that listen to the event */ public RemoveEditorListenerHook(hookId): void { const root = this.GetRootComponent(); root.removeEventListener(SVGLayoutDesigner.EDITOR_LISTENER_TYPE, this._hooks[hookId]); delete this._hooks[hookId]; } /// Macros /// /** * Reset to the first state and clear all history */ public Reset(): void { this.Editor.GetEditorState((state) => { this.Editor.SetHistory([state.history[0]]); }); } /** * Clear all previous history but the last state */ public ClearHistory(): void { this.Editor.GetEditorState((state) => { this.Editor.SetHistory([state.history[state.history.length - 1]]); }); } } class AppController { app: SVGLayoutDesigner; $component: JQuery; constructor(app: SVGLayoutDesigner, $component: JQuery) { this.app = app; this.$component = $component; } /** * Return the HTML component handling the editor */ public GetAppComponent() { const component = this.$component[0] .querySelector('iframe') .contentDocument .querySelector('.App'); if (component === undefined) { throw new Error('[SVGLD] Cannot hook the event because the editor is not yet open') } return component; } /// App Events /// /** * Not to be confused with setHistory, * change the default configuration for the new containers, symbols etc. * @param newEditor New editor configuration to set * @param callback */ public SetEditor(newEditor: IEditorState, callback?: (state: IEditorState) => void) { const eventType = 'setEditor'; this.app.AddEventListener(eventType, callback); const component = this.GetAppComponent(); component.dispatchEvent(new CustomEvent(eventType, { detail: newEditor })) } /** * Hide the main menu to go to the application. * SetEditor must be called first or the application will crash. * @param isLoaded * @param callback */ public SetLoaded(isLoaded: boolean, callback?: (state: IEditorState) => void) { const eventType = 'setLoaded'; this.app.AddEventListener(eventType, callback); const component = this.GetAppComponent(); component.dispatchEvent(new CustomEvent(eventType, { detail: isLoaded })) } } class EditorController { app: SVGLayoutDesigner; $component: JQuery; constructor(app: SVGLayoutDesigner, $component: JQuery) { this.app = app; this.$component = $component; } /** * Return the HTML component handling the editor */ public GetEditorComponent() { const component = this.$component[0] .querySelector('iframe') .contentDocument .querySelector('.Editor'); if (component === undefined) { throw new Error('[SVGLD] Cannot hook the event because the editor is not yet open') } return component; } /// Editor Events /// /** * Return in a callback the current state in the history of the editor * @param callback */ public GetCurrentHistoryState(callback: (state: IHistoryState) => void) { const eventType = 'getCurrentHistoryState'; this.app.AddEventListener(eventType, callback); const component = this.GetEditorComponent(); component.dispatchEvent(new CustomEvent(eventType)); } /** * Return in a callback the current history of the editor * @param callback */ public GetEditorState(callback: (state: IEditorState) => void) { const eventType = 'getEditorState'; this.app.AddEventListener(eventType, callback); const component = this.GetEditorComponent(); component.dispatchEvent(new CustomEvent(eventType)); } /** * Return in a callback the current history of the editor as string * @param callback */ public GetEditorStateAsString(callback: (state: string) => void) { const eventType = 'getEditorStateAsString'; this.app.AddEventListener(eventType, callback); const component = this.GetEditorComponent(); component.dispatchEvent(new CustomEvent(eventType)); } /** * Set the current history of the editor * @param history Whole history of the editor * @param callback (optional) */ public SetHistory(history: IHistoryState[], callback?: (state: IEditorState) => void) { const eventType = 'setHistory'; this.app.AddEventListener(eventType, callback); const component = this.GetEditorComponent(); component.dispatchEvent(new CustomEvent(eventType, { detail: history })); } /** * Revive the references in the editor state by mutation * Useful after using JSON.stringify with a replacer * @param editorState Editor state to revive * @param callback Callback with the revived state */ public ReviveEditorState(editorState: IEditorState, callback: (state: IEditorState) => void) { const eventType = 'reviveEditorState'; this.app.AddEventListener(eventType, callback); const component = this.GetEditorComponent(); component.dispatchEvent(new CustomEvent(eventType, { detail: editorState })); } /** * Revive the references in the history by mutation * Useful after using JSON.stringify with a replacer * @param history History to revive * @param callback Callback with the revived state */ public ReviveHistory(history: IHistoryState[], callback: (state: IHistoryState[]) => void) { const eventType = 'reviveHistory'; this.app.AddEventListener(eventType, callback); const component = this.GetEditorComponent(); component.dispatchEvent(new CustomEvent(eventType, { detail: history })); } /** * Add a new state to the editor * @param historyState New history state to append * @param callback */ public AppendNewHistoryState(historyState: IHistoryState, callback?: (state: IEditorState) => void) { const eventType = 'appendNewState'; this.app.AddEventListener(eventType, callback); this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail: historyState })); } /** * Create a new container at the given index position in a given parent container * @param index Position to insert the container * @param type Container type to create * @param parentId Parent container of the new container * @param callback */ public AddContainer(index: number, type: string, parentId: string, callback?: (state: IEditorState) => void) { const eventType = 'addContainer'; this.app.AddEventListener(eventType, callback); const detail = { index, type, parentId } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Create a new container at the given index position in the current selected container * @param index Position to insert the container * @param type Container type to create * @param callback */ public AddContainerToSelectedContainer(index: number, type: string, callback?: (state: IEditorState) => void) { const eventType = 'addContainerToSelectedContainer'; this.app.AddEventListener(eventType, callback); const detail = { index, type } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Append a new container in a given parent container * @param type Container type to create * @param parentId Parent container of the new container * @param callback */ public AppendContainer(type: string, parentId: string, callback?: (state: IEditorState) => void) { const eventType = 'appendContainer'; this.app.AddEventListener(eventType, callback); const detail = { type, parentId } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Append a new container in the current selected container * @param type Container type to create * @param callback */ public AppendContainerToSelectedContainer(type: string, callback?: (state: IEditorState) => void) { const eventType = 'appendContainerToSelectedContainer'; this.app.AddEventListener(eventType, callback); const detail = { type } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Select a container by id * @param containerId Container's id to select * @param callback */ public SelectContainer(containerId: string, callback?: (state: IEditorState) => void) { const eventType = 'selectContainer'; this.app.AddEventListener(eventType, callback); const detail = { containerId } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Delete a container by id * @param containerId Container's id to delete * @param callback */ public DeleteContainer(containerId: string, callback?: (state: IEditorState) => void) { const eventType = 'deleteContainer'; this.app.AddEventListener(eventType, callback); const detail = { containerId } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Create a new symbol * @param name Name of the symbol present in the config * @param callback */ public AddSymbol(name: string, callback?: (state: IEditorState) => void) { const eventType = 'addSymbol'; this.app.AddEventListener(eventType, callback); const detail = { name } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Select a symbol by id * @param symbolId Symbol's id to select * @param callback */ public SelectSymbol(symbolId: string, callback?: (state: IEditorState) => void) { const eventType = 'selectSymbol'; this.app.AddEventListener(eventType, callback); const detail = { symbolId } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Delete a symbol by id * @param symbolId Symbol's id to delete * @param callback */ public DeleteSymbol(symbolId: string, callback?: (state: IEditorState) => void) { const eventType = 'deleteSymbol'; this.app.AddEventListener(eventType, callback); const detail = { symbolId } this.GetEditorComponent().dispatchEvent(new CustomEvent(eventType, { 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' } }); }