refactor(frameCommunication): align Editor- and Renderer Handler

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares
2026-07-31 23:17:15 +02:00
committed by Philip Molares
parent 5b7fa9aaba
commit 17c7ca48d7
13 changed files with 73 additions and 50 deletions
@@ -27,9 +27,7 @@ export const useForceRenderPageUrlOnIframeLoadCallback = (
const forcedUrl = useMemo(() => { const forcedUrl = useMemo(() => {
const renderUrl = new URL(rendererBaseUrl) const renderUrl = new URL(rendererBaseUrl)
renderUrl.pathname += 'render' renderUrl.pathname += 'render'
if (iframeCommunicator !== undefined) { renderUrl.searchParams.set('uuid', iframeCommunicator.getUuid())
renderUrl.searchParams.set('uuid', iframeCommunicator.getUuid())
}
return renderUrl.toString() return renderUrl.toString()
}, [iframeCommunicator, rendererBaseUrl]) }, [iframeCommunicator, rendererBaseUrl])
const redirectionInProgress = useRef<boolean>(false) const redirectionInProgress = useRef<boolean>(false)
@@ -16,7 +16,7 @@ export const useSendFragmentToRenderer = (rendererReady: boolean): void => {
const iframeCommunicator = useEditorToRendererCommunicator() const iframeCommunicator = useEditorToRendererCommunicator()
useEffect(() => { useEffect(() => {
if (!rendererReady || !iframeCommunicator) { if (!rendererReady) {
return return
} }
@@ -12,12 +12,7 @@ import type { ScrollProps } from '../../editor-page/synced-scroll/scroll-props'
import { useExtensionEventEmitter } from '../../markdown-renderer/hooks/use-extension-event-emitter' import { useExtensionEventEmitter } from '../../markdown-renderer/hooks/use-extension-event-emitter'
import type { CommonMarkdownRendererProps } from '../../render-page/renderers/common-markdown-renderer-props' import type { CommonMarkdownRendererProps } from '../../render-page/renderers/common-markdown-renderer-props'
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler' import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import type { import type { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
ExtensionEvent,
OnHeightChangeMessage,
RendererType,
SetScrollStateMessage
} from '../../render-page/window-post-message-communicator/rendering-message'
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message' import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
import { WaitSpinner } from '../wait-spinner/wait-spinner' import { WaitSpinner } from '../wait-spinner/wait-spinner'
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change' import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
@@ -67,7 +62,7 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
const [rendererReady, setRendererReady] = useState<boolean>(false) const [rendererReady, setRendererReady] = useState<boolean>(false)
const frameReference = useRef<HTMLIFrameElement>(null) const frameReference = useRef<HTMLIFrameElement>(null)
const iframeCommunicator = useEditorToRendererCommunicator() const iframeCommunicator = useEditorToRendererCommunicator()
const log = useMemo(() => new Logger(`RendererIframe[${iframeCommunicator?.getUuid()}]`), [iframeCommunicator]) const log = useMemo(() => new Logger(`RendererIframe[${iframeCommunicator.getUuid()}]`), [iframeCommunicator])
const resetRendererReady = useCallback(() => { const resetRendererReady = useCallback(() => {
log.debug('Reset render status') log.debug('Reset render status')
@@ -98,7 +93,7 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
useEffect(() => { useEffect(() => {
if (!rendererReady) { if (!rendererReady) {
iframeCommunicator?.unsetMessageTarget() iframeCommunicator.unsetMessageTarget()
} }
}, [iframeCommunicator, rendererReady]) }, [iframeCommunicator, rendererReady])
@@ -111,22 +106,19 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
useEditorReceiveHandler( useEditorReceiveHandler(
CommunicationMessageType.EXTENSION_EVENT, CommunicationMessageType.EXTENSION_EVENT,
useMemo(() => { useMemo(() => {
return eventEmitter === undefined return eventEmitter === undefined ? null : ({ eventName, payload }) => eventEmitter.emit(eventName, payload)
? null
: (values: ExtensionEvent) => eventEmitter.emit(values.eventName, values.payload)
}, [eventEmitter]) }, [eventEmitter])
) )
useEditorReceiveHandler( useEditorReceiveHandler(
CommunicationMessageType.ON_HEIGHT_CHANGE, CommunicationMessageType.ON_HEIGHT_CHANGE,
useCallback( useMemo(() => {
(values: OnHeightChangeMessage) => { return ({ height }) => {
if (adaptFrameHeightToContent) { if (adaptFrameHeightToContent) {
setFrameHeight?.(values.height) setFrameHeight?.(height)
} }
}, }
[adaptFrameHeightToContent] }, [adaptFrameHeightToContent])
)
) )
useEditorReceiveHandler( useEditorReceiveHandler(
@@ -142,9 +134,9 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
log.error('Load triggered without content window') log.error('Load triggered without content window')
return return
} }
iframeCommunicator?.setMessageTarget(otherWindow) iframeCommunicator.setMessageTarget(otherWindow)
iframeCommunicator?.enableCommunication() iframeCommunicator.enableCommunication()
iframeCommunicator?.sendMessageToOtherSide({ iframeCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.SET_BASE_CONFIGURATION, type: CommunicationMessageType.SET_BASE_CONFIGURATION,
baseConfiguration: { baseConfiguration: {
baseUrl: window.location.toString(), baseUrl: window.location.toString(),
@@ -163,12 +155,20 @@ export const RendererIframe: React.FC<RendererIframeProps> = ({
useSendScrollState(scrollState ?? null, rendererReady) useSendScrollState(scrollState ?? null, rendererReady)
useEditorReceiveHandler( useEditorReceiveHandler(
CommunicationMessageType.SET_SCROLL_STATE, CommunicationMessageType.SET_SCROLL_STATE,
useCallback((values: SetScrollStateMessage) => onScroll?.(values.scrollState), [onScroll]) useMemo(() => {
return ({ scrollState }) => onScroll?.(scrollState)
}, [onScroll])
) )
useEditorReceiveHandler( useEditorReceiveHandler(
CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE, CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE,
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource]) useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
) )
useEditorReceiveHandler(
CommunicationMessageType.SET_URL_HASH,
useMemo(() => {
return ({ hash }) => (window.location.hash = hash)
}, [])
)
const frameClassNames = useMemo( const frameClassNames = useMemo(
() => concatCssClasses({ 'd-none': !rendererReady && showWaitSpinner }, 'border-0', styles.frame, frameClasses), () => concatCssClasses({ 'd-none': !rendererReady && showWaitSpinner }, 'border-0', styles.frame, frameClasses),
@@ -10,9 +10,7 @@ import { EditorToRendererCommunicator } from '../../render-page/window-post-mess
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React, { createContext, useContext, useEffect, useMemo } from 'react' import React, { createContext, useContext, useEffect, useMemo } from 'react'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { Logger } from '../../../utils/logger'
const logger = new Logger('EditorToRendererCommunicator')
const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommunicator | undefined>(undefined) const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommunicator | undefined>(undefined)
/** /**
@@ -21,11 +19,10 @@ const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommun
* @return the received communicator * @return the received communicator
* @throws {Error} if no communicator was received * @throws {Error} if no communicator was received
*/ */
export const useEditorToRendererCommunicator = (): EditorToRendererCommunicator | undefined => { export const useEditorToRendererCommunicator = (): EditorToRendererCommunicator => {
const communicatorFromContext = useContext(EditorToRendererCommunicatorContext) const communicatorFromContext = useContext(EditorToRendererCommunicatorContext)
if (!communicatorFromContext) { if (!communicatorFromContext) {
logger.error('No editor-to-renderer-iframe-communicator received. Did you forget to use the provider component?') throw new Error('No editor-to-renderer-iframe-communicator received. Did you forget to use the provider component?')
return undefined
} }
return communicatorFromContext return communicatorFromContext
} }
@@ -20,7 +20,7 @@ const RendererToEditorCommunicatorContext = createContext<RendererToEditorCommun
* @return the received communicator * @return the received communicator
* @throws {Error} if no communicator was received * @throws {Error} if no communicator was received
*/ */
export const useRendererToEditorCommunicator: () => RendererToEditorCommunicator = () => { export const useRendererToEditorCommunicator = (): RendererToEditorCommunicator => {
const communicatorFromContext = useContext(RendererToEditorCommunicatorContext) const communicatorFromContext = useContext(RendererToEditorCommunicatorContext)
if (!communicatorFromContext) { if (!communicatorFromContext) {
throw new Error('No renderer-to-editor-iframe-communicator received. Did you forget to use the provider component?') throw new Error('No renderer-to-editor-iframe-communicator received. Did you forget to use the provider component?')
@@ -35,7 +35,7 @@ export const NoteInfoLineWordCount: React.FC<NoteInfoLineWordCountProps> = ({ vi
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
useEffect(() => { useEffect(() => {
if (rendererReady && visible) { if (rendererReady && visible) {
editorToRendererCommunicator?.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT }) editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT })
} }
}, [editorToRendererCommunicator, rendererReady, visible]) }, [editorToRendererCommunicator, rendererReady, visible])
@@ -28,13 +28,13 @@ export const usePrintIframe = (): (() => void) => {
if (!iframe || !iframe.contentWindow) { if (!iframe || !iframe.contentWindow) {
return return
} }
iframeCommunicator?.sendMessageToOtherSide({ iframeCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.SET_PRINT_MODE, type: CommunicationMessageType.SET_PRINT_MODE,
printMode: true printMode: true
}) })
setTimeout(() => { setTimeout(() => {
iframe.contentWindow?.print() iframe.contentWindow?.print()
iframeCommunicator?.sendMessageToOtherSide({ iframeCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.SET_PRINT_MODE, type: CommunicationMessageType.SET_PRINT_MODE,
printMode: false printMode: false
}) })
@@ -5,6 +5,8 @@
*/ */
import type { AllHTMLAttributes } from 'react' import type { AllHTMLAttributes } from 'react'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { useRendererToEditorCommunicator } from '../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
export interface JumpAnchorProps extends AllHTMLAttributes<HTMLAnchorElement> { export interface JumpAnchorProps extends AllHTMLAttributes<HTMLAnchorElement> {
jumpTargetId: string jumpTargetId: string
@@ -18,8 +20,13 @@ export interface JumpAnchorProps extends AllHTMLAttributes<HTMLAnchorElement> {
* @param props Additional props directly given to the link * @param props Additional props directly given to the link
*/ */
export const JumpAnchor: React.FC<JumpAnchorProps> = ({ jumpTargetId, children, ...props }) => { export const JumpAnchor: React.FC<JumpAnchorProps> = ({ jumpTargetId, children, ...props }) => {
const iframeCommunicator = useRendererToEditorCommunicator()
const jumpToTargetId = useCallback( const jumpToTargetId = useCallback(
(event: React.MouseEvent<HTMLElement, MouseEvent>): void => { (event: React.MouseEvent<HTMLElement, MouseEvent>): void => {
iframeCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.SET_URL_HASH,
hash: jumpTargetId
})
const intoViewElement = document.getElementById(jumpTargetId) const intoViewElement = document.getElementById(jumpTargetId)
const scrollElement = document.querySelector('[data-scroll-element]') const scrollElement = document.querySelector('[data-scroll-element]')
if (!intoViewElement || !scrollElement) { if (!intoViewElement || !scrollElement) {
@@ -30,7 +37,7 @@ export const JumpAnchor: React.FC<JumpAnchorProps> = ({ jumpTargetId, children,
scrollElement.scrollTo({ behavior: 'smooth', top: intoViewElement.offsetTop }) scrollElement.scrollTo({ behavior: 'smooth', top: intoViewElement.offsetTop })
event.preventDefault() event.preventDefault()
}, },
[jumpTargetId] [iframeCommunicator, jumpTargetId]
) )
return ( return (
@@ -11,8 +11,12 @@ import { DocumentMarkdownRenderer } from './renderers/document/document-markdown
import { SimpleMarkdownRenderer } from './renderers/simple/simple-markdown-renderer' import { SimpleMarkdownRenderer } from './renderers/simple/simple-markdown-renderer'
import { SlideshowMarkdownRenderer } from './renderers/slideshow/slideshow-markdown-renderer' import { SlideshowMarkdownRenderer } from './renderers/slideshow/slideshow-markdown-renderer'
import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler' import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message' import type {
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message' BaseConfiguration} from './window-post-message-communicator/rendering-message';
import {
CommunicationMessageType,
RendererType
} from './window-post-message-communicator/rendering-message'
import { countWords } from './word-counter' import { countWords } from './word-counter'
import type { RevealOptions } from 'reveal.js' import type { RevealOptions } from 'reveal.js'
import { EventEmitter2 } from 'eventemitter2' import { EventEmitter2 } from 'eventemitter2'
@@ -8,22 +8,28 @@ import type { CommunicationMessages, RendererToEditorMessageType } from '../rend
import type { Handler } from '../window-post-message-communicator' import type { Handler } from '../window-post-message-communicator'
import { useEffect } from 'react' import { useEffect } from 'react'
type EditorMessageHandler<MESSAGE_TYPE extends RendererToEditorMessageType> = Handler<
CommunicationMessages,
MESSAGE_TYPE
> | null
/** /**
* Sets the handler for the given message type in the current editor to renderer communicator. * Sets the handler for the given message type in the current editor to renderer communicator.
* *
* @param messageType The message type that should be used to listen to. * @param messageType The message type that should be used to listen to.
* @param handler The handler that should be called if a message with the given message type was received. * @param handler The handler that should be called if a message with the given message type was received.
*/ */
export const useEditorReceiveHandler = <R extends RendererToEditorMessageType>( export const useEditorReceiveHandler = <MESSAGE_TYPE extends RendererToEditorMessageType>(
messageType: R, messageType: MESSAGE_TYPE,
handler: Handler<CommunicationMessages, R> | null handler: EditorMessageHandler<MESSAGE_TYPE>,
): void => { ): void => {
const editorToRendererCommunicator = useEditorToRendererCommunicator() const editorToRendererCommunicator = useEditorToRendererCommunicator()
useEffect(() => { useEffect(() => {
if (!handler) { if (!handler) {
return return
} }
editorToRendererCommunicator?.on(messageType, handler) editorToRendererCommunicator.on(messageType, handler)
return () => editorToRendererCommunicator?.off(messageType, handler) return () => editorToRendererCommunicator.off(messageType, handler)
}, [editorToRendererCommunicator, handler, messageType]) }, [editorToRendererCommunicator, handler, messageType])
} }
@@ -8,7 +8,7 @@ import type { CommunicationMessages, EditorToRendererMessageType } from '../rend
import type { Handler } from '../window-post-message-communicator' import type { Handler } from '../window-post-message-communicator'
import { useEffect } from 'react' import { useEffect } from 'react'
export type CommunicationMessageHandler<MESSAGE_TYPE extends EditorToRendererMessageType> = Handler< type RendererMessageHandler<MESSAGE_TYPE extends EditorToRendererMessageType> = Handler<
CommunicationMessages, CommunicationMessages,
MESSAGE_TYPE MESSAGE_TYPE
> >
@@ -21,11 +21,14 @@ export type CommunicationMessageHandler<MESSAGE_TYPE extends EditorToRendererMes
*/ */
export const useRendererReceiveHandler = <MESSAGE_TYPE extends EditorToRendererMessageType>( export const useRendererReceiveHandler = <MESSAGE_TYPE extends EditorToRendererMessageType>(
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
handler: CommunicationMessageHandler<MESSAGE_TYPE> handler: RendererMessageHandler<MESSAGE_TYPE>,
): void => { ): void => {
const editorToRendererCommunicator = useRendererToEditorCommunicator() const rendererToEditorCommunicator = useRendererToEditorCommunicator()
useEffect(() => { useEffect(() => {
editorToRendererCommunicator.on(messageType, handler) if (!handler) {
return () => editorToRendererCommunicator.off(messageType, handler) return
}, [editorToRendererCommunicator, handler, messageType]) }
rendererToEditorCommunicator.on(messageType, handler)
return () => rendererToEditorCommunicator.off(messageType, handler)
}, [rendererToEditorCommunicator, handler, messageType])
} }
@@ -22,7 +22,7 @@ export const useSendToRenderer = (
useEffect(() => { useEffect(() => {
if (message && rendererReady) { if (message && rendererReady) {
iframeCommunicator?.sendMessageToOtherSide(message) iframeCommunicator.sendMessageToOtherSide(message)
} }
}, [iframeCommunicator, message, rendererReady]) }, [iframeCommunicator, message, rendererReady])
} }
@@ -22,7 +22,8 @@ export enum CommunicationMessageType {
IMAGE_UPLOAD = 'IMAGE_UPLOAD', IMAGE_UPLOAD = 'IMAGE_UPLOAD',
EXTENSION_EVENT = 'EXTENSION_EVENT', EXTENSION_EVENT = 'EXTENSION_EVENT',
SET_PRINT_MODE = 'SET_PRINT_MODE', SET_PRINT_MODE = 'SET_PRINT_MODE',
SCROLL_TO_ELEMENT = 'SCROLL_TO_ELEMENT' SCROLL_TO_ELEMENT = 'SCROLL_TO_ELEMENT',
SET_URL_HASH = 'SET_URL_HASH'
} }
export interface NoPayloadMessage<TYPE extends CommunicationMessageType> { export interface NoPayloadMessage<TYPE extends CommunicationMessageType> {
@@ -99,6 +100,11 @@ export interface ScrollToElementMessage {
elementId: string elementId: string
} }
export interface SetUrlHashMessage {
type: CommunicationMessageType.SET_URL_HASH
hash: string
}
export type CommunicationMessages = export type CommunicationMessages =
| NoPayloadMessage<CommunicationMessageType.RENDERER_READY> | NoPayloadMessage<CommunicationMessageType.RENDERER_READY>
| NoPayloadMessage<CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE> | NoPayloadMessage<CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE>
@@ -115,6 +121,7 @@ export type CommunicationMessages =
| ExtensionEvent | ExtensionEvent
| SetPrintModeConfigurationMessage | SetPrintModeConfigurationMessage
| ScrollToElementMessage | ScrollToElementMessage
| SetUrlHashMessage
export type EditorToRendererMessageType = export type EditorToRendererMessageType =
| CommunicationMessageType.SET_MARKDOWN_CONTENT | CommunicationMessageType.SET_MARKDOWN_CONTENT
@@ -135,6 +142,7 @@ export type RendererToEditorMessageType =
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED | CommunicationMessageType.ON_WORD_COUNT_CALCULATED
| CommunicationMessageType.IMAGE_UPLOAD | CommunicationMessageType.IMAGE_UPLOAD
| CommunicationMessageType.EXTENSION_EVENT | CommunicationMessageType.EXTENSION_EVENT
| CommunicationMessageType.SET_URL_HASH
export enum RendererType { export enum RendererType {
DOCUMENT = 'document', DOCUMENT = 'document',