From 460669987d88b9eaea8fe02935bd7338197cdddf Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Wed, 1 Mar 2023 09:53:06 +0000 Subject: [PATCH] Merged PR 392: Events: Fix needing to use setTimeout for callbacks --- .eslintrc.cjs | 1 + public/smartcomponent/svg-layout-designer.ts | 12 +- src/Components/API/api.test.tsx | 2 +- src/Components/App/App.tsx | 2 + src/Components/Editor/Actions/Save.ts | 4 +- src/Components/Editor/Editor.tsx | 2 + src/Events/AppEvents.ts | 117 ++++++++++++----- src/Events/EditorEvents.ts | 130 ++++++++++++------- 8 files changed, 182 insertions(+), 88 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index df59f7d..09807f9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -36,6 +36,7 @@ module.exports = { 'max-depth': ['error', 4], 'function-paren-newline': ['error', 'multiline-arguments'], 'multiline-ternary': ['error', 'always'], + 'prefer-template': "error", // Import/export 'import/no-default-export': 'error', diff --git a/public/smartcomponent/svg-layout-designer.ts b/public/smartcomponent/svg-layout-designer.ts index 45a94f7..3915f5b 100644 --- a/public/smartcomponent/svg-layout-designer.ts +++ b/public/smartcomponent/svg-layout-designer.ts @@ -137,14 +137,10 @@ */ public LoadEditor(editorState: IEditorState) { this.ReviveEditorState(editorState, (state) => { - this.SetEditor(state, (currentState) => { - setTimeout(() => { - this.app.App.SetAppState(2, () => { - setTimeout(() => { - this.app.Editor.SetHistory({ history: state.history, historyCurrentStep: state.historyCurrentStep }); - }, 200); - }); - }, 200); + this.SetEditor(state, () => { + this.app.App.SetAppState(2, () => { + this.app.Editor.SetHistory({ history: state.history, historyCurrentStep: state.historyCurrentStep }); + }); }); }); } diff --git a/src/Components/API/api.test.tsx b/src/Components/API/api.test.tsx index 75f78e6..9c807d8 100644 --- a/src/Components/API/api.test.tsx +++ b/src/Components/API/api.test.tsx @@ -15,7 +15,7 @@ import { FetchConfiguration } from './api'; const CSHARP_WEB_API_BASE_URL = 'http://localhost:5209/'; const CHARP_WEB_API_RESOURCE_URL = 'SVGLD'; -const CSHARP_WEB_API_URL = CSHARP_WEB_API_BASE_URL + CHARP_WEB_API_RESOURCE_URL + '/'; +const CSHARP_WEB_API_URL = `${CSHARP_WEB_API_BASE_URL + CHARP_WEB_API_RESOURCE_URL}/`; // TODO: Migrate this test to SVGLDWebAPI rather than using test-server/ describe.concurrent('Test server test', () => { diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index aa7b22e..54d13d9 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -47,6 +47,7 @@ function UseHTTPGETStatePreloading( export function App(props: IAppProps): JSX.Element { const [appState, setAppState] = useState(FAST_BOOT); const appRef = useRef(null); + const eventCallbackQueue = useRef([]); const languageContext = useContext(LanguageContext); const defaultMainContainer = new ContainerModel(DEFAULT_MAINCONTAINER_PROPS); @@ -66,6 +67,7 @@ export function App(props: IAppProps): JSX.Element { }); UseCustomEvents( + eventCallbackQueue, props.root, appRef, languageContext, diff --git a/src/Components/Editor/Actions/Save.ts b/src/Components/Editor/Actions/Save.ts index dba1f4b..4546999 100644 --- a/src/Components/Editor/Actions/Save.ts +++ b/src/Components/Editor/Actions/Save.ts @@ -75,10 +75,10 @@ export function SaveEditorAsSVG(): void { } // add xml declaration - source = '\r\n' + source; + source = `\r\n${source}`; // convert svg source to URI data scheme. - const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source); + const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`; CreateDownloadNode('state.svg', url); } diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index a7ef6ac..2859ef4 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -83,6 +83,7 @@ export function Editor(props: IEditorProps): JSX.Element { const [selectedSymbolId, setSelectedSymbolId] = React.useState(''); const editorRef = useRef(null); + const eventCallbackQueue = useRef([]); const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep); function ResetState(): void { @@ -100,6 +101,7 @@ export function Editor(props: IEditorProps): JSX.Element { ResetState ); UseCustomEvents( + eventCallbackQueue, props.root, history, historyCurrentStep, diff --git a/src/Events/AppEvents.ts b/src/Events/AppEvents.ts index ec3fd59..20f64a9 100644 --- a/src/Events/AppEvents.ts +++ b/src/Events/AppEvents.ts @@ -9,16 +9,20 @@ import { GetDefaultEditorState as GetDefaultEditorStateAction } from '../utils/d 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 WrappedCustomEvent { + event: CustomEvent + runAfterRender: boolean +} + export interface IAppEvent { name: string - func: (params: IAppEventParams) => void + func: (params: IAppEventParams) => WrappedCustomEvent } export const events: IAppEvent[] = [ @@ -33,6 +37,7 @@ export const events: IAppEvent[] = [ ]; export function UseCustomEvents( + callbackQueue: React.RefObject, root: Element | Document, appRef: React.RefObject, languageContext: ILanguage, @@ -49,13 +54,21 @@ export function UseCustomEvents( const funcs = new Map void>(); for (const event of events) { function Func(eventInitDict?: CustomEventInit): void { - event.func({ - root, + console.debug(`RUN INITIAL FUNCTION: ${event.name}`); + const customEvent = event.func({ languageContext, setEditor, setAppState, eventInitDict }); + + if (customEvent.runAfterRender) { + console.debug(`ADDING CALLBACK TO QUEUE: ${customEvent.event.type}`); + callbackQueue.current?.push(customEvent.event); + } else { + console.debug(`EXECUTING CALLBACK IMMEDIATELY: ${customEvent.event.type}`); + root.dispatchEvent(customEvent.event); + } } current.addEventListener(event.name, Func); funcs.set(event.name, Func); @@ -70,89 +83,127 @@ export function UseCustomEvents( } }; }); + + useEffect(() => { + if (callbackQueue.current === null) { + return; + } + + if (callbackQueue.current.length > 0) { + const callback = callbackQueue.current.shift(); + + if (callback === undefined) { + return; + } + + console.debug(`RUNNING CALLBACK FROM QUEUE: ${callback.type}`); + root.dispatchEvent(callback); + } + }); + + } function SetEditor({ - root, setEditor, setAppState, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { const editor: IEditorState = eventInitDict?.detail; setEditor(editor); setAppState(AppState.Loading); - const customEvent = new CustomEvent('setEditor', { detail: editor }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('setEditor', { detail: editor }); + return { + event, + runAfterRender: true + }; } function SetAppState({ setAppState, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { const appState: AppState = eventInitDict?.detail; setAppState(appState); + return { + event: new CustomEvent('setAppState'), + runAfterRender: true + }; } function ReviveEditorState({ - root, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { const anEditorState: IEditorState = eventInitDict?.detail; Revive(anEditorState); - const customEvent = new CustomEvent('reviveEditorState', { detail: anEditorState }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('reviveEditorState', { detail: anEditorState }); + return { + event, + runAfterRender: false + }; } function ReviveHistory({ - root, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { const history: IHistoryState[] = eventInitDict?.detail; ReviveHistoryAction(history); - const customEvent = new CustomEvent('reviveHistory', { detail: history }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('reviveHistory', { detail: history }); + return { + event, + runAfterRender: false + }; } function GetDefaultEditorState({ - root, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { const configuration: IConfiguration = eventInitDict?.detail; const editorState = GetDefaultEditorStateAction(configuration); - const customEvent = new CustomEvent('getDefaultEditorState', { detail: editorState }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('getDefaultEditorState', { detail: editorState }); + return { + event, + runAfterRender: false + }; } function AddLanguage({ - root, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { 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); + const event = new CustomEvent('addLanguage'); + + return { + event, + runAfterRender: true + }; } function SetLanguage({ - root, languageContext, eventInitDict -}: IAppEventParams): void { +}: IAppEventParams): WrappedCustomEvent { const language: string = eventInitDict?.detail; let success = false; if (languageContext.languageChange !== undefined) { languageContext.languageChange(language); success = true; } - const customEvent = new CustomEvent('setLanguage', { detail: success }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('setLanguage', { detail: success }); + + return { + event, + runAfterRender: true + }; } -function GetLanguages({ - root -}: IAppEventParams): void { +function GetLanguages(): WrappedCustomEvent { const customEvent = new CustomEvent>>('getLanguages', { detail: translations }); - root.dispatchEvent(customEvent); + return { + event: customEvent, + runAfterRender: false + }; } diff --git a/src/Events/EditorEvents.ts b/src/Events/EditorEvents.ts index 45cd998..7bca82a 100644 --- a/src/Events/EditorEvents.ts +++ b/src/Events/EditorEvents.ts @@ -15,16 +15,16 @@ import { type IEditorState } from '../Interfaces/IEditorState'; import { type IHistoryState } from '../Interfaces/IHistoryState'; import { FindContainerById } from '../utils/itertools'; import { GetCircularReplacer } from '../utils/saveload'; +import { type WrappedCustomEvent } from './AppEvents'; interface IEditorEventParams { - root: Element | Document editorState: IEditorState setNewHistory: (newHistory: IHistoryState[], historyCurrentStep?: number) => void eventInitDict?: CustomEventInit } export interface IEditorEvent { name: string - func: (params: IEditorEventParams) => void + func: (params: IEditorEventParams) => WrappedCustomEvent } export const events: IEditorEvent[] = [ @@ -41,6 +41,7 @@ export const events: IEditorEvent[] = [ ]; export function UseCustomEvents( + callbackQueue: React.RefObject, root: Element | Document, history: IHistoryState[], historyCurrentStep: number, @@ -64,12 +65,17 @@ export function UseCustomEvents( for (const event of events) { function Func(eventInitDict?: CustomEventInit): void { - event.func({ - root, + const customEvent = event.func({ editorState, setNewHistory, eventInitDict }); + + if (customEvent.runAfterRender) { + callbackQueue.current?.push(customEvent.event); + } else { + root.dispatchEvent(customEvent.event); + } } current?.addEventListener(event.name, Func); funcs.set(event.name, Func); @@ -84,6 +90,22 @@ export function UseCustomEvents( } }; }); + + useEffect(() => { + if (callbackQueue.current === null) { + return; + } + + while (callbackQueue.current.length > 0) { + const callback = callbackQueue.current.shift(); + + if (callback === undefined) { + continue; + } + + root.dispatchEvent(callback); + } + }); } export function UseEditorListener( @@ -104,74 +126,83 @@ export function UseEditorListener( } function GetEditorState({ - root, editorState -}: IEditorEventParams): void { - const customEvent = new CustomEvent('getEditorState', { detail: structuredClone(editorState) }); - root.dispatchEvent(customEvent); +}: IEditorEventParams): WrappedCustomEvent { + const event = new CustomEvent('getEditorState', { detail: structuredClone(editorState) }); + return { + event, + runAfterRender: false + }; } function GetEditorStateAsString({ - root, editorState -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const spaces = import.meta.env.DEV ? 4 : 0; const data = JSON.stringify(editorState, GetCircularReplacer(), spaces); - const customEvent = new CustomEvent('getEditorStateAsString', { detail: data }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('getEditorStateAsString', { detail: data }); + return { + event, + runAfterRender: false + }; } function SetHistory({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const history: IHistoryState[] = eventInitDict?.detail.history; const historyCurrentStep: number | undefined = eventInitDict?.detail.historyCurrentStep; setNewHistory(history, historyCurrentStep); - const customEvent = new CustomEvent('setHistory', { detail: editorState }); - root.dispatchEvent(customEvent); + const event = new CustomEvent('setHistory', { detail: editorState }); + return { + event, + runAfterRender: true + }; } function GetCurrentHistoryState({ - root, editorState -}: IEditorEventParams): void { - const customEvent = new CustomEvent( +}: IEditorEventParams): WrappedCustomEvent { + const event = new CustomEvent( 'getCurrentHistoryState', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; } function AppendNewState({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const state: IHistoryState = eventInitDict?.detail.state; const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep); history.push(state); setNewHistory(history); - const customEvent = new CustomEvent( + const event = new CustomEvent( 'appendNewState', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; } function AddContainer({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const { index, type, @@ -189,19 +220,21 @@ function AddContainer({ ); setNewHistory(newHistory); - const customEvent = new CustomEvent( + const event = new CustomEvent( 'addContainer', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; } function AppendContainer({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const { type, parentId @@ -222,19 +255,21 @@ function AppendContainer({ ); setNewHistory(newHistory); - const customEvent = new CustomEvent( + const event = new CustomEvent( 'appendContainerToSelectedContainer', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; } function DeleteContainer({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const { containerId } = eventInitDict?.detail; @@ -248,19 +283,21 @@ function DeleteContainer({ ); setNewHistory(newHistory); - const customEvent = new CustomEvent( + const event = new CustomEvent( 'deleteContainer', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; } function AddSymbol({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const { name } = eventInitDict?.detail; @@ -275,19 +312,21 @@ function AddSymbol({ ); setNewHistory(newHistory); - const customEvent = new CustomEvent( + const event = new CustomEvent( 'AddSymbol', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; } function DeleteSymbol({ - root, editorState, setNewHistory, eventInitDict -}: IEditorEventParams): void { +}: IEditorEventParams): WrappedCustomEvent { const { symbolId } = eventInitDict?.detail; @@ -300,9 +339,12 @@ function DeleteSymbol({ ); setNewHistory(newHistory); - const customEvent = new CustomEvent( + const event = new CustomEvent( 'DeleteSymbol', { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) } ); - root.dispatchEvent(customEvent); + return { + event, + runAfterRender: true + }; }