Restructure Communicator (#1510)

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-09-26 11:09:46 +02:00 committed by GitHub
parent e6830598d5
commit f1e91b4574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 680 additions and 569 deletions

View file

@ -19,10 +19,11 @@ import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
import { DocumentInfobar } from './document-infobar' import { DocumentInfobar } from './document-infobar'
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert' import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from './LoadingNoteAlert' import { LoadingNoteAlert } from './LoadingNoteAlert'
import { RendererType } from '../render-page/rendering-message' import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
import { useApplicationState } from '../../hooks/common/use-application-state' import { useApplicationState } from '../../hooks/common/use-application-state'
import { IframeEditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider'
import { useNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-note-markdown-content-without-frontmatter' import { useNoteMarkdownContentWithoutFrontmatter } from '../../hooks/common/use-note-markdown-content-without-frontmatter'
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
export const DocumentReadOnlyPage: React.FC = () => { export const DocumentReadOnlyPage: React.FC = () => {
useTranslation() useTranslation()
@ -35,9 +36,10 @@ export const DocumentReadOnlyPage: React.FC = () => {
const [error, loading] = useLoadNoteFromServer() const [error, loading] = useLoadNoteFromServer()
const markdownContent = useNoteMarkdownContentWithoutFrontmatter() const markdownContent = useNoteMarkdownContentWithoutFrontmatter()
const noteDetails = useApplicationState((state) => state.noteDetails) const noteDetails = useApplicationState((state) => state.noteDetails)
useSendFrontmatterInfoFromReduxToRenderer()
return ( return (
<IframeEditorToRendererCommunicatorContextProvider> <EditorToRendererCommunicatorContextProvider>
<div className={'d-flex flex-column mvh-100 bg-light'}> <div className={'d-flex flex-column mvh-100 bg-light'}>
<MotdBanner /> <MotdBanner />
<AppBar mode={AppBarMode.BASIC} /> <AppBar mode={AppBarMode.BASIC} />
@ -63,7 +65,7 @@ export const DocumentReadOnlyPage: React.FC = () => {
/> />
</ShowIf> </ShowIf>
</div> </div>
</IframeEditorToRendererCommunicatorContextProvider> </EditorToRendererCommunicatorContextProvider>
) )
} }

View file

@ -4,13 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useEffect, useState } from 'react' import React, { useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ShowIf } from '../../../common/show-if/show-if' import { ShowIf } from '../../../common/show-if/show-if'
import { DocumentInfoLine } from './document-info-line' import { DocumentInfoLine } from './document-info-line'
import { UnitalicBoldText } from './unitalic-bold-text' import { UnitalicBoldText } from './unitalic-bold-text'
import { useIFrameEditorToRendererCommunicator } from '../../render-context/iframe-editor-to-renderer-communicator-context-provider' import { useEditorToRendererCommunicator } from '../../render-context/editor-to-renderer-communicator-context-provider'
import { useApplicationState } from '../../../../hooks/common/use-application-state' import {
CommunicationMessageType,
OnWordCountCalculatedMessage
} from '../../../render-page/window-post-message-communicator/rendering-message'
import { useEditorReceiveHandler } from '../../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import { useEffectOnRendererReady } from '../../../render-page/window-post-message-communicator/hooks/use-effect-on-renderer-ready'
/** /**
* Creates a new info line for the document information dialog that holds the * Creates a new info line for the document information dialog that holds the
@ -18,24 +23,19 @@ import { useApplicationState } from '../../../../hooks/common/use-application-st
*/ */
export const DocumentInfoLineWordCount: React.FC = () => { export const DocumentInfoLineWordCount: React.FC = () => {
useTranslation() useTranslation()
const iframeEditorToRendererCommunicator = useIFrameEditorToRendererCommunicator() const editorToRendererCommunicator = useEditorToRendererCommunicator()
const [wordCount, setWordCount] = useState<number | null>(null) const [wordCount, setWordCount] = useState<number | null>(null)
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
useEffect(() => { useEditorReceiveHandler(
iframeEditorToRendererCommunicator.onWordCountCalculated((words) => { CommunicationMessageType.ON_WORD_COUNT_CALCULATED,
setWordCount(words) useCallback((values: OnWordCountCalculatedMessage) => setWordCount(values.words), [setWordCount])
}) )
return () => {
iframeEditorToRendererCommunicator.onWordCountCalculated(undefined)
}
}, [iframeEditorToRendererCommunicator, setWordCount])
useEffect(() => { useEffectOnRendererReady(
if (rendererReady) { useCallback(() => {
iframeEditorToRendererCommunicator.sendGetWordCount() editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT })
} }, [editorToRendererCommunicator])
}, [iframeEditorToRendererCommunicator, rendererReady]) )
return ( return (
<DocumentInfoLine icon={'align-left'} size={'2x'}> <DocumentInfoLine icon={'align-left'} size={'2x'}>

View file

@ -7,6 +7,7 @@
import React from 'react' import React from 'react'
import { RenderIframe, RenderIframeProps } from '../renderer-pane/render-iframe' import { RenderIframe, RenderIframeProps } from '../renderer-pane/render-iframe'
import { useNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/common/use-note-markdown-content-without-frontmatter' import { useNoteMarkdownContentWithoutFrontmatter } from '../../../hooks/common/use-note-markdown-content-without-frontmatter'
import { useSendFrontmatterInfoFromReduxToRenderer } from '../renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
export type EditorDocumentRendererProps = Omit<RenderIframeProps, 'markdownContent'> export type EditorDocumentRendererProps = Omit<RenderIframeProps, 'markdownContent'>
@ -17,5 +18,8 @@ export type EditorDocumentRendererProps = Omit<RenderIframeProps, 'markdownConte
*/ */
export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = (props) => { export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = (props) => {
const markdownContent = useNoteMarkdownContentWithoutFrontmatter() const markdownContent = useNoteMarkdownContentWithoutFrontmatter()
useSendFrontmatterInfoFromReduxToRenderer()
return <RenderIframe frameClasses={'h-100 w-100'} markdownContent={markdownContent} {...props} /> return <RenderIframe frameClasses={'h-100 w-100'} markdownContent={markdownContent} {...props} />
} }

View file

@ -21,14 +21,14 @@ import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
import { Sidebar } from './sidebar/sidebar' import { Sidebar } from './sidebar/sidebar'
import { Splitter } from './splitter/splitter' import { Splitter } from './splitter/splitter'
import { DualScrollState, ScrollState } from './synced-scroll/scroll-props' import { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
import { RendererType } from '../render-page/rendering-message' import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
import { useEditorModeFromUrl } from './hooks/useEditorModeFromUrl' import { useEditorModeFromUrl } from './hooks/useEditorModeFromUrl'
import { UiNotifications } from '../notifications/ui-notifications' import { UiNotifications } from '../notifications/ui-notifications'
import { useNotificationTest } from './use-notification-test' import { useNotificationTest } from './use-notification-test'
import { IframeEditorToRendererCommunicatorContextProvider } from './render-context/iframe-editor-to-renderer-communicator-context-provider'
import { useUpdateLocalHistoryEntry } from './hooks/useUpdateLocalHistoryEntry' import { useUpdateLocalHistoryEntry } from './hooks/useUpdateLocalHistoryEntry'
import { useApplicationState } from '../../hooks/common/use-application-state' import { useApplicationState } from '../../hooks/common/use-application-state'
import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer' import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer'
import { EditorToRendererCommunicatorContextProvider } from './render-context/editor-to-renderer-communicator-context-provider'
export interface EditorPagePathParams { export interface EditorPagePathParams {
id: string id: string
@ -53,7 +53,11 @@ export const EditorPage: React.FC = () => {
const onMarkdownRendererScroll = useCallback( const onMarkdownRendererScroll = useCallback(
(newScrollState: ScrollState) => { (newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) { if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) {
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState })) setScrollState((old) => {
const newState = { editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }
console.debug('[EditorPage] set scroll state because of renderer scroll', newState)
return newState
})
} }
}, },
[editorSyncScroll] [editorSyncScroll]
@ -62,7 +66,11 @@ export const EditorPage: React.FC = () => {
const onEditorScroll = useCallback( const onEditorScroll = useCallback(
(newScrollState: ScrollState) => { (newScrollState: ScrollState) => {
if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) { if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) {
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState })) setScrollState((old) => {
const newState = { rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }
console.debug('[EditorPage] set scroll state because of editor scroll', newState)
return newState
})
} }
}, },
[editorSyncScroll] [editorSyncScroll]
@ -79,10 +87,12 @@ export const EditorPage: React.FC = () => {
const setRendererToScrollSource = useCallback(() => { const setRendererToScrollSource = useCallback(() => {
scrollSource.current = ScrollSource.RENDERER scrollSource.current = ScrollSource.RENDERER
console.debug('[EditorPage] Make renderer scroll source')
}, []) }, [])
const setEditorToScrollSource = useCallback(() => { const setEditorToScrollSource = useCallback(() => {
scrollSource.current = ScrollSource.EDITOR scrollSource.current = ScrollSource.EDITOR
console.debug('[EditorPage] Make editor scroll source')
}, []) }, [])
useNotificationTest() useNotificationTest()
@ -114,7 +124,7 @@ export const EditorPage: React.FC = () => {
) )
return ( return (
<IframeEditorToRendererCommunicatorContextProvider> <EditorToRendererCommunicatorContextProvider>
<UiNotifications /> <UiNotifications />
<MotdBanner /> <MotdBanner />
<div className={'d-flex flex-column vh-100'}> <div className={'d-flex flex-column vh-100'}>
@ -136,7 +146,7 @@ export const EditorPage: React.FC = () => {
</div> </div>
</ShowIf> </ShowIf>
</div> </div>
</IframeEditorToRendererCommunicatorContextProvider> </EditorToRendererCommunicatorContextProvider>
) )
} }

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { createContext, useContext, useMemo } from 'react'
import { EditorToRendererCommunicator } from '../../render-page/window-post-message-communicator/editor-to-renderer-communicator'
const EditorToRendererCommunicatorContext = createContext<EditorToRendererCommunicator | undefined>(undefined)
/**
* Provides the {@link EditorToRendererCommunicator editor to renderer iframe communicator} that is set by a {@link EditorToRendererCommunicatorContextProvider context provider}.
*
* @return the received communicator
* @throws {Error} if no communicator was received
*/
export const useEditorToRendererCommunicator: () => EditorToRendererCommunicator = () => {
const communicatorFromContext = useContext(EditorToRendererCommunicatorContext)
if (!communicatorFromContext) {
throw new Error('No editor-to-renderer-iframe-communicator received. Did you forget to use the provider component?')
}
return communicatorFromContext
}
/**
* Provides a {@link EditorToRendererCommunicator editor to renderer communicator} for the child components via Context.
*/
export const EditorToRendererCommunicatorContextProvider: React.FC = ({ children }) => {
const communicator = useMemo<EditorToRendererCommunicator>(() => new EditorToRendererCommunicator(), [])
return (
<EditorToRendererCommunicatorContext.Provider value={communicator}>
{children}
</EditorToRendererCommunicatorContext.Provider>
)
}

View file

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { createContext, useContext, useMemo } from 'react'
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
const IFrameEditorToRendererCommunicatorContext = createContext<IframeEditorToRendererCommunicator | undefined>(
undefined
)
/**
* Provides the {@link IframeEditorToRendererCommunicator editor to renderer iframe communicator} that is set by a {@link IframeEditorToRendererCommunicatorContextProvider context provider}.
*
* @return the received communicator
* @throws Error if no communicator was received
*/
export const useIFrameEditorToRendererCommunicator: () => IframeEditorToRendererCommunicator = () => {
const communicatorFromContext = useContext(IFrameEditorToRendererCommunicatorContext)
if (!communicatorFromContext) {
throw new Error('No editor-to-renderer-iframe-communicator received. Did you forget to use the provider component?')
}
return communicatorFromContext
}
export const IframeEditorToRendererCommunicatorContextProvider: React.FC = ({ children }) => {
const currentIFrameCommunicator = useMemo<IframeEditorToRendererCommunicator>(
() => new IframeEditorToRendererCommunicator(),
[]
)
return (
<IFrameEditorToRendererCommunicatorContext.Provider value={currentIFrameCommunicator}>
{children}
</IFrameEditorToRendererCommunicatorContext.Provider>
)
}

View file

@ -1,49 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { createContext, useContext, useEffect, useMemo } from 'react'
import { IframeRendererToEditorCommunicator } from '../../render-page/iframe-renderer-to-editor-communicator'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
const IFrameRendererToEditorCommunicatorContext = createContext<IframeRendererToEditorCommunicator | undefined>(
undefined
)
/**
* Provides the {@link IframeRendererToEditorCommunicator renderer to editor iframe communicator} that is set by a {@link IframeRendererToEditorCommunicatorContextProvider context provider}.
*
* @return the received communicator
* @throws Error if no communicator was received
*/
export const useIFrameRendererToEditorCommunicator: () => IframeRendererToEditorCommunicator = () => {
const communicatorFromContext = useContext(IFrameRendererToEditorCommunicatorContext)
if (!communicatorFromContext) {
throw new Error('No renderer-to-editor-iframe-communicator received. Did you forget to use the provider component?')
}
return communicatorFromContext
}
export const IframeRendererToEditorCommunicatorContextProvider: React.FC = ({ children }) => {
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
const currentIFrameCommunicator = useMemo<IframeRendererToEditorCommunicator>(() => {
const newCommunicator = new IframeRendererToEditorCommunicator()
newCommunicator.setMessageTarget(window.parent, editorOrigin)
return newCommunicator
}, [editorOrigin])
useEffect(() => {
const currentIFrame = currentIFrameCommunicator
currentIFrame?.sendRendererReady()
return () => currentIFrame?.unregisterEventListener()
}, [currentIFrameCommunicator])
return (
<IFrameRendererToEditorCommunicatorContext.Provider value={currentIFrameCommunicator}>
{children}
</IFrameRendererToEditorCommunicatorContext.Provider>
)
}

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { createContext, useContext, useEffect, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { RendererToEditorCommunicator } from '../../render-page/window-post-message-communicator/renderer-to-editor-communicator'
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
const RendererToEditorCommunicatorContext = createContext<RendererToEditorCommunicator | undefined>(undefined)
/**
* Provides the {@link RendererToEditorCommunicator renderer to editor iframe communicator} that is set by a {@link RendererToEditorCommunicatorContextProvider context provider}.
*
* @return the received communicator
* @throws {Error} if no communicator was received
*/
export const useRendererToEditorCommunicator: () => RendererToEditorCommunicator = () => {
const communicatorFromContext = useContext(RendererToEditorCommunicatorContext)
if (!communicatorFromContext) {
throw new Error('No renderer-to-editor-iframe-communicator received. Did you forget to use the provider component?')
}
return communicatorFromContext
}
export const RendererToEditorCommunicatorContextProvider: React.FC = ({ children }) => {
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
const communicator = useMemo<RendererToEditorCommunicator>(() => {
const newCommunicator = new RendererToEditorCommunicator()
newCommunicator.setMessageTarget(window.parent, editorOrigin)
return newCommunicator
}, [editorOrigin])
useEffect(() => {
const currentCommunicator = communicator
currentCommunicator.enableCommunication()
currentCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.RENDERER_READY
})
return () => currentCommunicator?.unregisterEventListener()
}, [communicator])
/**
* Provides a {@link RendererToEditorCommunicator renderer to editor communicator} for the child components via Context.
*/
return (
<RendererToEditorCommunicatorContext.Provider value={communicator}>
{children}
</RendererToEditorCommunicatorContext.Provider>
)
}

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useState } from 'react'
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
import {
CommunicationMessageType,
ImageClickedMessage,
ImageDetails
} from '../../render-page/window-post-message-communicator/rendering-message'
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
export const CommunicatorImageLightbox: React.FC = () => {
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const [show, setShow] = useState<boolean>(false)
useEditorReceiveHandler(
CommunicationMessageType.IMAGE_CLICKED,
useCallback(
(values: ImageClickedMessage) => {
setLightboxDetails?.(values.details)
setShow(true)
},
[setLightboxDetails]
)
)
const hideLightbox = useCallback(() => {
setShow(false)
}, [])
return (
<ImageLightboxModal
show={show}
onHide={hideLightbox}
src={lightboxDetails?.src}
alt={lightboxDetails?.alt}
title={lightboxDetails?.title}
/>
)
}

View file

@ -5,11 +5,11 @@
*/ */
import { RefObject, useCallback, useRef } from 'react' import { RefObject, useCallback, useRef } from 'react'
import { IframeEditorToRendererCommunicator } from '../../../render-page/iframe-editor-to-renderer-communicator' import { EditorToRendererCommunicator } from '../../../render-page/window-post-message-communicator/editor-to-renderer-communicator'
export const useOnIframeLoad = ( export const useOnIframeLoad = (
frameReference: RefObject<HTMLIFrameElement>, frameReference: RefObject<HTMLIFrameElement>,
iframeCommunicator: IframeEditorToRendererCommunicator, iframeCommunicator: EditorToRendererCommunicator,
rendererOrigin: string, rendererOrigin: string,
renderPageUrl: string, renderPageUrl: string,
onNavigateAway: () => void onNavigateAway: () => void

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
import { useMemo } from 'react'
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
/**
* Sends the current dark mode setting to the renderer.
*
* @param forcedDarkMode Overwrites the value from the global application states if set.
*/
export const useSendDarkModeStatusToRenderer = (forcedDarkMode?: boolean): void => {
const savedDarkMode = useIsDarkModeActivated()
useSendToRenderer(
useMemo(
() => ({
type: CommunicationMessageType.SET_DARKMODE,
activated: forcedDarkMode ?? savedDarkMode
}),
[forcedDarkMode, savedDarkMode]
)
)
}

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
import { useMemo } from 'react'
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Extracts the {@link RendererFrontmatterInfo frontmatter data}
* from the global application state and sends it to the renderer.
*/
export const useSendFrontmatterInfoFromReduxToRenderer = (): void => {
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
return useSendToRenderer(
useMemo(
() => ({
type: CommunicationMessageType.SET_FRONTMATTER_INFO,
frontmatterInfo
}),
[frontmatterInfo]
)
)
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
import { useMemo } from 'react'
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
/**
* Sends the given markdown content to the renderer.
*
* @param markdownContent The markdown content to send.
*/
export const useSendMarkdownToRenderer = (markdownContent: string): void => {
return useSendToRenderer(
useMemo(
() => ({
type: CommunicationMessageType.SET_MARKDOWN_CONTENT,
content: markdownContent
}),
[markdownContent]
)
)
}

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useCallback, useRef } from 'react'
import { ScrollState } from '../../synced-scroll/scroll-props'
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
import { useEffectOnRendererReady } from '../../../render-page/window-post-message-communicator/hooks/use-effect-on-renderer-ready'
import equal from 'fast-deep-equal'
import { useEditorToRendererCommunicator } from '../../render-context/editor-to-renderer-communicator-context-provider'
/**
* Sends the given {@link ScrollState scroll state} to the renderer if the content changed.
* @param scrollState The scroll state to send
*/
export const useSendScrollState = (scrollState: ScrollState | undefined): void => {
const iframeCommunicator = useEditorToRendererCommunicator()
const oldScrollState = useRef<ScrollState | undefined>(undefined)
useEffectOnRendererReady(
useCallback(() => {
if (scrollState && !equal(scrollState, oldScrollState.current)) {
oldScrollState.current = scrollState
iframeCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.SET_SCROLL_STATE, scrollState })
}
}, [iframeCommunicator, scrollState])
)
}

View file

@ -3,18 +3,27 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import equal from 'fast-deep-equal'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useApplicationState } from '../../../hooks/common/use-application-state' import { useApplicationState } from '../../../hooks/common/use-application-state'
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
import { isTestMode } from '../../../utils/test-modes' import { isTestMode } from '../../../utils/test-modes'
import { RendererProps } from '../../render-page/markdown-document' import { RendererProps } from '../../render-page/markdown-document'
import { ImageDetails, RendererType } from '../../render-page/rendering-message' import {
import { useIFrameEditorToRendererCommunicator } from '../render-context/iframe-editor-to-renderer-communicator-context-provider' CommunicationMessageType,
import { ScrollState } from '../synced-scroll/scroll-props' OnFirstHeadingChangeMessage,
OnHeightChangeMessage,
OnTaskCheckboxChangeMessage,
RendererType,
SetScrollStateMessage
} from '../../render-page/window-post-message-communicator/rendering-message'
import { useEditorToRendererCommunicator } from '../render-context/editor-to-renderer-communicator-context-provider'
import { useOnIframeLoad } from './hooks/use-on-iframe-load' import { useOnIframeLoad } from './hooks/use-on-iframe-load'
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox' import { CommunicatorImageLightbox } from './communicator-image-lightbox'
import { setRendererStatus } from '../../../redux/renderer-status/methods' import { setRendererStatus } from '../../../redux/renderer-status/methods'
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import { useIsRendererReady } from '../../render-page/window-post-message-communicator/hooks/use-is-renderer-ready'
import { useSendDarkModeStatusToRenderer } from './hooks/use-send-dark-mode-status-to-renderer'
import { useSendMarkdownToRenderer } from './hooks/use-send-markdown-to-renderer'
import { useSendScrollState } from './hooks/use-send-scroll-state'
export interface RenderIframeProps extends RendererProps { export interface RenderIframeProps extends RendererProps {
rendererType: RendererType rendererType: RendererType
@ -33,16 +42,12 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
rendererType, rendererType,
forcedDarkMode forcedDarkMode
}) => { }) => {
const savedDarkMode = useIsDarkModeActivated()
const darkMode = forcedDarkMode ?? savedDarkMode
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null) const frameReference = useRef<HTMLIFrameElement>(null)
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin) const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${rendererOrigin}render` const renderPageUrl = `${rendererOrigin}render`
const resetRendererReady = useCallback(() => setRendererStatus(false), []) const resetRendererReady = useCallback(() => setRendererStatus(false), [])
const iframeCommunicator = useIFrameEditorToRendererCommunicator() const iframeCommunicator = useEditorToRendererCommunicator()
const rendererReady = useIsRendererReady()
const onIframeLoad = useOnIframeLoad( const onIframeLoad = useOnIframeLoad(
frameReference, frameReference,
iframeCommunicator, iframeCommunicator,
@ -52,8 +57,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
) )
const [frameHeight, setFrameHeight] = useState<number>(0) const [frameHeight, setFrameHeight] = useState<number>(0)
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
useEffect( useEffect(
() => () => { () => () => {
iframeCommunicator.unregisterEventListener() iframeCommunicator.unregisterEventListener()
@ -62,76 +65,59 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
[iframeCommunicator] [iframeCommunicator]
) )
useEffect(() => { useEditorReceiveHandler(
iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange) CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
return () => iframeCommunicator.onFirstHeadingChange(undefined) useCallback(
}, [iframeCommunicator, onFirstHeadingChange]) (values: OnFirstHeadingChangeMessage) => onFirstHeadingChange?.(values.firstHeading),
[onFirstHeadingChange]
)
)
useEffect(() => { useEditorReceiveHandler(
iframeCommunicator.onSetScrollState(onScroll) CommunicationMessageType.SET_SCROLL_STATE,
return () => iframeCommunicator.onSetScrollState(undefined) useCallback((values: SetScrollStateMessage) => onScroll?.(values.scrollState), [onScroll])
}, [iframeCommunicator, onScroll]) )
useEffect(() => { useEditorReceiveHandler(
iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource) CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER,
return () => iframeCommunicator.onSetScrollSourceToRenderer(undefined) useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
}, [iframeCommunicator, onMakeScrollSource]) )
useEffect(() => { useEditorReceiveHandler(
iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange) CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
return () => iframeCommunicator.onTaskCheckboxChange(undefined) useCallback(
}, [iframeCommunicator, onTaskCheckedChange]) (values: OnTaskCheckboxChangeMessage) => onTaskCheckedChange?.(values.lineInMarkdown, values.checked),
[onTaskCheckedChange]
)
)
useEffect(() => { useEditorReceiveHandler(
iframeCommunicator.onImageClicked(setLightboxDetails) CommunicationMessageType.ON_HEIGHT_CHANGE,
return () => iframeCommunicator.onImageClicked(undefined) useCallback((values: OnHeightChangeMessage) => setFrameHeight?.(values.height), [setFrameHeight])
}, [iframeCommunicator]) )
useEffect(() => { useEditorReceiveHandler(
iframeCommunicator.onHeightChange(setFrameHeight) CommunicationMessageType.RENDERER_READY,
return () => iframeCommunicator.onHeightChange(undefined) useCallback(() => {
}, [iframeCommunicator]) iframeCommunicator.enableCommunication()
iframeCommunicator.sendMessageToOtherSide({
useEffect(() => { type: CommunicationMessageType.SET_BASE_CONFIGURATION,
iframeCommunicator.onRendererReady(() => { baseConfiguration: {
iframeCommunicator.sendSetBaseConfiguration({
baseUrl: window.location.toString(), baseUrl: window.location.toString(),
rendererType rendererType
}
}) })
setRendererStatus(true) setRendererStatus(true)
})
return () => iframeCommunicator.onRendererReady(undefined)
}, [iframeCommunicator, rendererType]) }, [iframeCommunicator, rendererType])
)
useEffect(() => { useSendScrollState(scrollState)
if (rendererReady) { useSendDarkModeStatusToRenderer(forcedDarkMode)
iframeCommunicator.sendSetDarkmode(darkMode) useSendMarkdownToRenderer(markdownContent)
}
}, [darkMode, iframeCommunicator, rendererReady])
const oldScrollState = useRef<ScrollState | undefined>(undefined)
useEffect(() => {
if (rendererReady && !equal(scrollState, oldScrollState.current)) {
oldScrollState.current = scrollState
iframeCommunicator.sendScrollState(scrollState)
}
}, [iframeCommunicator, rendererReady, scrollState])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetMarkdownContent(markdownContent)
}
}, [iframeCommunicator, markdownContent, rendererReady])
useEffect(() => {
if (rendererReady && frontmatterInfo !== undefined) {
iframeCommunicator.sendSetFrontmatterInfo(frontmatterInfo)
}
}, [iframeCommunicator, rendererReady, frontmatterInfo])
return ( return (
<Fragment> <Fragment>
<ShowOnPropChangeImageLightbox details={lightboxDetails} /> <CommunicatorImageLightbox />
<iframe <iframe
style={{ height: `${frameHeight}px` }} style={{ height: `${frameHeight}px` }}
data-cy={'documentIframe'} data-cy={'documentIframe'}

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useState } from 'react'
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
import { ImageDetails } from '../../render-page/rendering-message'
export interface ShowOnPropChangeImageLightboxProps {
details?: ImageDetails
}
export const ShowOnPropChangeImageLightbox: React.FC<ShowOnPropChangeImageLightboxProps> = ({ details }) => {
const [show, setShow] = useState<boolean>(false)
const hideLightbox = useCallback(() => {
setShow(false)
}, [])
useEffect(() => {
if (details) {
setShow(true)
}
}, [details])
return (
<ImageLightboxModal
show={show}
onHide={hideLightbox}
src={details?.src}
alt={details?.alt}
title={details?.title}
/>
)
}

View file

@ -17,17 +17,17 @@ import { CoverButtons } from './cover-buttons/cover-buttons'
import { FeatureLinks } from './feature-links' import { FeatureLinks } from './feature-links'
import { useIntroPageContent } from './hooks/use-intro-page-content' import { useIntroPageContent } from './hooks/use-intro-page-content'
import { ShowIf } from '../common/show-if/show-if' import { ShowIf } from '../common/show-if/show-if'
import { RendererType } from '../render-page/rendering-message' import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
import { WaitSpinner } from '../common/wait-spinner/wait-spinner' import { WaitSpinner } from '../common/wait-spinner/wait-spinner'
import { IframeEditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider'
import { useApplicationState } from '../../hooks/common/use-application-state' import { useApplicationState } from '../../hooks/common/use-application-state'
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
export const IntroPage: React.FC = () => { export const IntroPage: React.FC = () => {
const introPageContent = useIntroPageContent() const introPageContent = useIntroPageContent()
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
return ( return (
<IframeEditorToRendererCommunicatorContextProvider> <EditorToRendererCommunicatorContextProvider>
<div className={'flex-fill mt-3'}> <div className={'flex-fill mt-3'}>
<h1 dir='auto' className={'align-items-center d-flex justify-content-center flex-column'}> <h1 dir='auto' className={'align-items-center d-flex justify-content-center flex-column'}>
<HedgeDocLogoWithText logoType={HedgeDocLogoType.COLOR_VERTICAL} size={HedgeDocLogoSize.BIG} /> <HedgeDocLogoWithText logoType={HedgeDocLogoType.COLOR_VERTICAL} size={HedgeDocLogoSize.BIG} />
@ -53,6 +53,6 @@ export const IntroPage: React.FC = () => {
<hr className={'mb-5'} /> <hr className={'mb-5'} />
</div> </div>
<FeatureLinks /> <FeatureLinks />
</IframeEditorToRendererCommunicatorContextProvider> </EditorToRendererCommunicatorContextProvider>
) )
} }

View file

@ -6,19 +6,23 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer' import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer'
import { IframeRendererToEditorCommunicator } from '../iframe-renderer-to-editor-communicator' import { RendererToEditorCommunicator } from '../window-post-message-communicator/renderer-to-editor-communicator'
import { CommunicationMessageType } from '../window-post-message-communicator/rendering-message'
export const useImageClickHandler = (iframeCommunicator: IframeRendererToEditorCommunicator): ImageClickHandler => { export const useImageClickHandler = (iframeCommunicator: RendererToEditorCommunicator): ImageClickHandler => {
return useCallback( return useCallback(
(event: React.MouseEvent<HTMLImageElement, MouseEvent>) => { (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
const image = event.target as HTMLImageElement const image = event.target as HTMLImageElement
if (image.src === '') { if (image.src === '') {
return return
} }
iframeCommunicator.sendClickedImageUrl({ iframeCommunicator.sendMessageToOtherSide({
type: CommunicationMessageType.IMAGE_CLICKED,
details: {
src: image.src, src: image.src,
alt: image.alt, alt: image.alt,
title: image.title title: image.title
}
}) })
}, },
[iframeCommunicator] [iframeCommunicator]

View file

@ -1,136 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
import { IframeCommunicator } from './iframe-communicator'
import {
BaseConfiguration,
EditorToRendererIframeMessage,
ImageDetails,
RendererToEditorIframeMessage,
RenderIframeMessageType
} from './rendering-message'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
export class IframeEditorToRendererCommunicator extends IframeCommunicator<
EditorToRendererIframeMessage,
RendererToEditorIframeMessage
> {
private onSetScrollSourceToRendererHandler?: () => void
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
private onFirstHeadingChangeHandler?: (heading?: string) => void
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
private onRendererReadyHandler?: () => void
private onImageClickedHandler?: (details: ImageDetails) => void
private onHeightChangeHandler?: (height: number) => void
private onWordCountCalculatedHandler?: (words: number) => void
public onHeightChange(handler?: (height: number) => void): void {
this.onHeightChangeHandler = handler
}
public onImageClicked(handler?: (details: ImageDetails) => void): void {
this.onImageClickedHandler = handler
}
public onRendererReady(handler?: () => void): void {
this.onRendererReadyHandler = handler
}
public onSetScrollSourceToRenderer(handler?: () => void): void {
this.onSetScrollSourceToRendererHandler = handler
}
public onTaskCheckboxChange(handler?: (lineInMarkdown: number, checked: boolean) => void): void {
this.onTaskCheckboxChangeHandler = handler
}
public onFirstHeadingChange(handler?: (heading?: string) => void): void {
this.onFirstHeadingChangeHandler = handler
}
public onSetScrollState(handler?: (scrollState: ScrollState) => void): void {
this.onSetScrollStateHandler = handler
}
public onWordCountCalculated(handler?: (words: number) => void): void {
this.onWordCountCalculatedHandler = handler
}
public sendSetBaseConfiguration(baseConfiguration: BaseConfiguration): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_BASE_CONFIGURATION,
baseConfiguration
})
}
public sendSetMarkdownContent(markdownContent: string): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT,
content: markdownContent
})
}
public sendSetDarkmode(darkModeActivated: boolean): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_DARKMODE,
activated: darkModeActivated
})
}
public sendScrollState(scrollState?: ScrollState): void {
if (!scrollState) {
return
}
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_STATE,
scrollState
})
}
public sendGetWordCount(): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.GET_WORD_COUNT
})
}
public sendSetFrontmatterInfo(frontmatterInfo: RendererFrontmatterInfo): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_FRONTMATTER_INFO,
frontmatterInfo: frontmatterInfo
})
}
protected handleEvent(event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.enableCommunication()
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
this.onSetScrollSourceToRendererHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
return false
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false
case RenderIframeMessageType.ON_HEIGHT_CHANGE:
this.onHeightChangeHandler?.(renderMessage.height)
return false
case RenderIframeMessageType.ON_WORD_COUNT_CALCULATED:
this.onWordCountCalculatedHandler?.(renderMessage.words)
return false
}
}
}

View file

@ -4,16 +4,21 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useState } from 'react'
import { ScrollState } from '../editor-page/synced-scroll/scroll-props' import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
import { BaseConfiguration, RendererType } from './rendering-message' import {
BaseConfiguration,
CommunicationMessageType,
RendererType
} from './window-post-message-communicator/rendering-message'
import { setDarkMode } from '../../redux/dark-mode/methods' import { setDarkMode } from '../../redux/dark-mode/methods'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer' import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import { useImageClickHandler } from './hooks/use-image-click-handler' import { useImageClickHandler } from './hooks/use-image-click-handler'
import { MarkdownDocument } from './markdown-document' import { MarkdownDocument } from './markdown-document'
import { useIFrameRendererToEditorCommunicator } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider'
import { countWords } from './word-counter' import { countWords } from './word-counter'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types' import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
export const IframeMarkdownRenderer: React.FC = () => { export const IframeMarkdownRenderer: React.FC = () => {
const [markdownContent, setMarkdownContent] = useState('') const [markdownContent, setMarkdownContent] = useState('')
@ -25,60 +30,76 @@ export const IframeMarkdownRenderer: React.FC = () => {
deprecatedSyntax: false deprecatedSyntax: false
}) })
const iframeCommunicator = useIFrameRendererToEditorCommunicator() const communicator = useRendererToEditorCommunicator()
const countWordsInRenderedDocument = useCallback(() => { const countWordsInRenderedDocument = useCallback(() => {
const documentContainer = document.querySelector('.markdown-body') const documentContainer = document.querySelector('.markdown-body')
if (!documentContainer) { communicator.sendMessageToOtherSide({
iframeCommunicator.sendWordCountCalculated(0) type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED,
return words: documentContainer ? countWords(documentContainer) : 0
} })
const wordCount = countWords(documentContainer) }, [communicator])
iframeCommunicator.sendWordCountCalculated(wordCount)
}, [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetBaseConfiguration(setBaseConfiguration), [iframeCommunicator]) useRendererReceiveHandler(CommunicationMessageType.SET_BASE_CONFIGURATION, (values) =>
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator]) setBaseConfiguration(values.baseConfiguration)
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
useEffect(() => iframeCommunicator.onSetFrontmatterInfo(setFrontmatterInfo), [iframeCommunicator, setFrontmatterInfo])
useEffect(
() => iframeCommunicator.onGetWordCount(countWordsInRenderedDocument),
[iframeCommunicator, countWordsInRenderedDocument]
) )
useRendererReceiveHandler(CommunicationMessageType.SET_MARKDOWN_CONTENT, (values) =>
setMarkdownContent(values.content)
)
useRendererReceiveHandler(CommunicationMessageType.SET_DARKMODE, (values) => setDarkMode(values.activated))
useRendererReceiveHandler(CommunicationMessageType.SET_SCROLL_STATE, (values) => setScrollState(values.scrollState))
useRendererReceiveHandler(CommunicationMessageType.SET_FRONTMATTER_INFO, (values) =>
setFrontmatterInfo(values.frontmatterInfo)
)
useRendererReceiveHandler(CommunicationMessageType.GET_WORD_COUNT, () => countWordsInRenderedDocument())
const onTaskCheckedChange = useCallback( const onTaskCheckedChange = useCallback(
(lineInMarkdown: number, checked: boolean) => { (lineInMarkdown: number, checked: boolean) => {
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked) communicator.sendMessageToOtherSide({
type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE,
checked,
lineInMarkdown
})
}, },
[iframeCommunicator] [communicator]
) )
const onFirstHeadingChange = useCallback( const onFirstHeadingChange = useCallback(
(firstHeading?: string) => { (firstHeading?: string) => {
iframeCommunicator.sendFirstHeadingChanged(firstHeading) communicator.sendMessageToOtherSide({
type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
firstHeading
})
}, },
[iframeCommunicator] [communicator]
) )
const onMakeScrollSource = useCallback(() => { const onMakeScrollSource = useCallback(() => {
iframeCommunicator.sendSetScrollSourceToRenderer() communicator.sendMessageToOtherSide({
}, [iframeCommunicator]) type: CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
})
}, [communicator])
const onScroll = useCallback( const onScroll = useCallback(
(scrollState: ScrollState) => { (scrollState: ScrollState) => {
iframeCommunicator.sendSetScrollState(scrollState) communicator.sendMessageToOtherSide({
type: CommunicationMessageType.SET_SCROLL_STATE,
scrollState
})
}, },
[iframeCommunicator] [communicator]
) )
const onImageClick: ImageClickHandler = useImageClickHandler(iframeCommunicator) const onImageClick: ImageClickHandler = useImageClickHandler(communicator)
const onHeightChange = useCallback( const onHeightChange = useCallback(
(height: number) => { (height: number) => {
iframeCommunicator.sendHeightChange(height) communicator.sendMessageToOtherSide({
type: CommunicationMessageType.ON_HEIGHT_CHANGE,
height
})
}, },
[iframeCommunicator] [communicator]
) )
if (!baseConfiguration) { if (!baseConfiguration) {

View file

@ -1,132 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ScrollState } from '../editor-page/synced-scroll/scroll-props'
import { IframeCommunicator } from './iframe-communicator'
import {
BaseConfiguration,
EditorToRendererIframeMessage,
ImageDetails,
RendererToEditorIframeMessage,
RenderIframeMessageType
} from './rendering-message'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
export class IframeRendererToEditorCommunicator extends IframeCommunicator<
RendererToEditorIframeMessage,
EditorToRendererIframeMessage
> {
private onSetMarkdownContentHandler?: (markdownContent: string) => void
private onSetDarkModeHandler?: (darkModeActivated: boolean) => void
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
private onSetBaseConfigurationHandler?: (baseConfiguration: BaseConfiguration) => void
private onGetWordCountHandler?: () => void
private onSetFrontmatterInfoHandler?: (frontmatterInfo: RendererFrontmatterInfo) => void
public onSetBaseConfiguration(handler?: (baseConfiguration: BaseConfiguration) => void): void {
this.onSetBaseConfigurationHandler = handler
}
public onSetMarkdownContent(handler?: (markdownContent: string) => void): void {
this.onSetMarkdownContentHandler = handler
}
public onSetDarkMode(handler?: (darkModeActivated: boolean) => void): void {
this.onSetDarkModeHandler = handler
}
public onSetScrollState(handler?: (scrollState: ScrollState) => void): void {
this.onSetScrollStateHandler = handler
}
public onGetWordCount(handler?: () => void): void {
this.onGetWordCountHandler = handler
}
public onSetFrontmatterInfo(handler?: (frontmatterInfo: RendererFrontmatterInfo) => void): void {
this.onSetFrontmatterInfoHandler = handler
}
public sendRendererReady(): void {
this.enableCommunication()
this.sendMessageToOtherSide({
type: RenderIframeMessageType.RENDERER_READY
})
}
public sendTaskCheckBoxChange(lineInMarkdown: number, checked: boolean): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE,
checked,
lineInMarkdown
})
}
public sendFirstHeadingChanged(firstHeading: string | undefined): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE,
firstHeading
})
}
public sendSetScrollSourceToRenderer(): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER
})
}
public sendSetScrollState(scrollState: ScrollState): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.SET_SCROLL_STATE,
scrollState
})
}
public sendClickedImageUrl(details: ImageDetails): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.IMAGE_CLICKED,
details: details
})
}
public sendHeightChange(height: number): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_HEIGHT_CHANGE,
height
})
}
public sendWordCountCalculated(words: number): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_WORD_COUNT_CALCULATED,
words
})
}
protected handleEvent(event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.SET_MARKDOWN_CONTENT:
this.onSetMarkdownContentHandler?.(renderMessage.content)
return false
case RenderIframeMessageType.SET_DARKMODE:
this.onSetDarkModeHandler?.(renderMessage.activated)
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.SET_BASE_CONFIGURATION:
this.onSetBaseConfigurationHandler?.(renderMessage.baseConfiguration)
return false
case RenderIframeMessageType.GET_WORD_COUNT:
this.onGetWordCountHandler?.()
return false
case RenderIframeMessageType.SET_FRONTMATTER_INFO:
this.onSetFrontmatterInfoHandler?.(renderMessage.frontmatterInfo)
return false
}
}
}

View file

@ -6,15 +6,15 @@
import React from 'react' import React from 'react'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode' import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { IframeMarkdownRenderer } from './iframe-markdown-renderer' import { IframeMarkdownRenderer } from './iframe-markdown-renderer'
import { IframeRendererToEditorCommunicatorContextProvider } from '../editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider' import { RendererToEditorCommunicatorContextProvider } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
export const RenderPage: React.FC = () => { export const RenderPage: React.FC = () => {
useApplyDarkMode() useApplyDarkMode()
return ( return (
<IframeRendererToEditorCommunicatorContextProvider> <RendererToEditorCommunicatorContextProvider>
<IframeMarkdownRenderer /> <IframeMarkdownRenderer />
</IframeRendererToEditorCommunicatorContextProvider> </RendererToEditorCommunicatorContextProvider>
) )
} }

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { WindowPostMessageCommunicator } from './window-post-message-communicator'
import { CommunicationMessages, EditorToRendererMessageType, RendererToEditorMessageType } from './rendering-message'
/**
* The communicator that is used to send messages from the editor to the renderer.
*/
export class EditorToRendererCommunicator extends WindowPostMessageCommunicator<
RendererToEditorMessageType,
EditorToRendererMessageType,
CommunicationMessages
> {
protected generateLogIdentifier(): string {
return 'E=>R'
}
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react'
import { CommunicationMessages, RendererToEditorMessageType } from '../rendering-message'
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { Handler } from '../window-post-message-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 handler The handler that should be called if a message with the given message type was received.
*/
export const useEditorReceiveHandler = <R extends RendererToEditorMessageType>(
messageType: R,
handler: Handler<CommunicationMessages, R>
): void => {
const editorToRendererCommunicator = useEditorToRendererCommunicator()
useEffect(() => {
editorToRendererCommunicator.setHandler(messageType, handler)
return () => {
editorToRendererCommunicator.setHandler(messageType, undefined)
}
}, [editorToRendererCommunicator, handler, messageType])
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Executes the given callback if it changes or the renderer is ready for receiving messages.
*
* @param sendOnReadyCallback The callback that should get executed.
*/
export const useEffectOnRendererReady = (sendOnReadyCallback: () => void): void => {
const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady)
useEffect(() => {
if (rendererReady) {
sendOnReadyCallback()
}
}, [rendererReady, sendOnReadyCallback])
}

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useApplicationState } from '../../../../hooks/common/use-application-state'
/**
* Returns the current ready status of the renderer.
*/
export const useIsRendererReady = (): boolean => useApplicationState((state) => state.rendererStatus.rendererReady)

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react'
import { CommunicationMessages, EditorToRendererMessageType } from '../rendering-message'
import { Handler } from '../window-post-message-communicator'
import { useRendererToEditorCommunicator } from '../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
/**
* Sets the handler for the given message type in the current renderer to editor communicator.
*
* @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.
*/
export const useRendererReceiveHandler = <MESSAGE_TYPE extends EditorToRendererMessageType>(
messageType: MESSAGE_TYPE,
handler: Handler<CommunicationMessages, MESSAGE_TYPE>
): void => {
const editorToRendererCommunicator = useRendererToEditorCommunicator()
useEffect(() => {
editorToRendererCommunicator.setHandler(messageType, handler)
return () => {
editorToRendererCommunicator.setHandler(messageType, undefined)
}
}, [editorToRendererCommunicator, handler, messageType])
}

View file

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useCallback } from 'react'
import { CommunicationMessages, EditorToRendererMessageType } from '../rendering-message'
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { PostMessage } from '../window-post-message-communicator'
import { useEffectOnRendererReady } from './use-effect-on-renderer-ready'
export const useSendToRenderer = (
message: undefined | Extract<CommunicationMessages, PostMessage<EditorToRendererMessageType>>
): void => {
const iframeCommunicator = useEditorToRendererCommunicator()
useEffectOnRendererReady(
useCallback(() => {
if (message) {
iframeCommunicator.sendMessageToOtherSide(message)
}
}, [iframeCommunicator, message])
)
}

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { WindowPostMessageCommunicator } from './window-post-message-communicator'
import { CommunicationMessages, EditorToRendererMessageType, RendererToEditorMessageType } from './rendering-message'
/**
* The communicator that is used to send messages from the renderer to the editor.
*/
export class RendererToEditorCommunicator extends WindowPostMessageCommunicator<
EditorToRendererMessageType,
RendererToEditorMessageType,
CommunicationMessages
> {
protected generateLogIdentifier(): string {
return 'E<=R'
}
}

View file

@ -3,10 +3,10 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { ScrollState } from '../editor-page/synced-scroll/scroll-props' import { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types' import { RendererFrontmatterInfo } from '../../common/note-frontmatter/types'
export enum RenderIframeMessageType { export enum CommunicationMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT', SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
RENDERER_READY = 'RENDERER_READY', RENDERER_READY = 'RENDERER_READY',
SET_DARKMODE = 'SET_DARKMODE', SET_DARKMODE = 'SET_DARKMODE',
@ -22,12 +22,12 @@ export enum RenderIframeMessageType {
SET_FRONTMATTER_INFO = 'SET_FRONTMATTER_INFO' SET_FRONTMATTER_INFO = 'SET_FRONTMATTER_INFO'
} }
export interface RendererToEditorSimpleMessage { export interface NoPayloadMessage {
type: RenderIframeMessageType.RENDERER_READY | RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER type: CommunicationMessageType.RENDERER_READY | CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
} }
export interface SetDarkModeMessage { export interface SetDarkModeMessage {
type: RenderIframeMessageType.SET_DARKMODE type: CommunicationMessageType.SET_DARKMODE
activated: boolean activated: boolean
} }
@ -38,72 +38,87 @@ export interface ImageDetails {
} }
export interface SetBaseUrlMessage { export interface SetBaseUrlMessage {
type: RenderIframeMessageType.SET_BASE_CONFIGURATION type: CommunicationMessageType.SET_BASE_CONFIGURATION
baseConfiguration: BaseConfiguration baseConfiguration: BaseConfiguration
} }
export interface GetWordCountMessage { export interface GetWordCountMessage {
type: RenderIframeMessageType.GET_WORD_COUNT type: CommunicationMessageType.GET_WORD_COUNT
} }
export interface ImageClickedMessage { export interface ImageClickedMessage {
type: RenderIframeMessageType.IMAGE_CLICKED type: CommunicationMessageType.IMAGE_CLICKED
details: ImageDetails details: ImageDetails
} }
export interface SetMarkdownContentMessage { export interface SetMarkdownContentMessage {
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT type: CommunicationMessageType.SET_MARKDOWN_CONTENT
content: string content: string
} }
export interface SetScrollStateMessage { export interface SetScrollStateMessage {
type: RenderIframeMessageType.SET_SCROLL_STATE type: CommunicationMessageType.SET_SCROLL_STATE
scrollState: ScrollState scrollState: ScrollState
} }
export interface OnTaskCheckboxChangeMessage { export interface OnTaskCheckboxChangeMessage {
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
lineInMarkdown: number lineInMarkdown: number
checked: boolean checked: boolean
} }
export interface OnFirstHeadingChangeMessage { export interface OnFirstHeadingChangeMessage {
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE
firstHeading: string | undefined firstHeading: string | undefined
} }
export interface SetFrontmatterInfoMessage { export interface SetFrontmatterInfoMessage {
type: RenderIframeMessageType.SET_FRONTMATTER_INFO type: CommunicationMessageType.SET_FRONTMATTER_INFO
frontmatterInfo: RendererFrontmatterInfo frontmatterInfo: RendererFrontmatterInfo
} }
export interface OnHeightChangeMessage { export interface OnHeightChangeMessage {
type: RenderIframeMessageType.ON_HEIGHT_CHANGE type: CommunicationMessageType.ON_HEIGHT_CHANGE
height: number height: number
} }
export interface OnWordCountCalculatedMessage { export interface OnWordCountCalculatedMessage {
type: RenderIframeMessageType.ON_WORD_COUNT_CALCULATED type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED
words: number words: number
} }
export type EditorToRendererIframeMessage = export type CommunicationMessages =
| SetMarkdownContentMessage | NoPayloadMessage
| SetDarkModeMessage | SetDarkModeMessage
| SetScrollStateMessage
| SetBaseUrlMessage | SetBaseUrlMessage
| GetWordCountMessage | GetWordCountMessage
| SetFrontmatterInfoMessage
export type RendererToEditorIframeMessage =
| RendererToEditorSimpleMessage
| OnFirstHeadingChangeMessage
| OnTaskCheckboxChangeMessage
| SetScrollStateMessage
| ImageClickedMessage | ImageClickedMessage
| SetMarkdownContentMessage
| SetScrollStateMessage
| OnTaskCheckboxChangeMessage
| OnFirstHeadingChangeMessage
| SetFrontmatterInfoMessage
| OnHeightChangeMessage | OnHeightChangeMessage
| OnWordCountCalculatedMessage | OnWordCountCalculatedMessage
export type EditorToRendererMessageType =
| CommunicationMessageType.SET_MARKDOWN_CONTENT
| CommunicationMessageType.SET_DARKMODE
| CommunicationMessageType.SET_SCROLL_STATE
| CommunicationMessageType.SET_BASE_CONFIGURATION
| CommunicationMessageType.GET_WORD_COUNT
| CommunicationMessageType.SET_FRONTMATTER_INFO
export type RendererToEditorMessageType =
| CommunicationMessageType.RENDERER_READY
| CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER
| CommunicationMessageType.ON_FIRST_HEADING_CHANGE
| CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE
| CommunicationMessageType.SET_SCROLL_STATE
| CommunicationMessageType.IMAGE_CLICKED
| CommunicationMessageType.ON_HEIGHT_CHANGE
| CommunicationMessageType.ON_WORD_COUNT_CALCULATED
export enum RendererType { export enum RendererType {
DOCUMENT, DOCUMENT,
INTRO, INTRO,

View file

@ -9,19 +9,39 @@
*/ */
export class IframeCommunicatorSendingError extends Error {} export class IframeCommunicatorSendingError extends Error {}
export type Handler<MESSAGES, MESSAGE_TYPE extends string> =
| ((values: Extract<MESSAGES, PostMessage<MESSAGE_TYPE>>) => void)
| undefined
export type HandlerMap<MESSAGES, MESSAGE_TYPE extends string> = Partial<{
[key in MESSAGE_TYPE]: Handler<MESSAGES, MESSAGE_TYPE>
}>
export interface PostMessage<MESSAGE_TYPE extends string> {
type: MESSAGE_TYPE
}
/** /**
* Base class for communication between renderer and editor. * Base class for communication between renderer and editor.
*/ */
export abstract class IframeCommunicator<SEND, RECEIVE> { export abstract class WindowPostMessageCommunicator<
RECEIVE_TYPE extends string,
SEND_TYPE extends string,
MESSAGES extends PostMessage<RECEIVE_TYPE | SEND_TYPE>
> {
private messageTarget?: Window private messageTarget?: Window
private targetOrigin?: string private targetOrigin?: string
private communicationEnabled: boolean private communicationEnabled: boolean
private handlers: HandlerMap<MESSAGES, RECEIVE_TYPE> = {}
constructor() { constructor() {
window.addEventListener('message', this.handleEvent.bind(this)) window.addEventListener('message', this.handleEvent.bind(this))
this.communicationEnabled = false this.communicationEnabled = false
} }
/**
* Removes the message event listener from the {@link window}
*/
public unregisterEventListener(): void { public unregisterEventListener(): void {
window.removeEventListener('message', this.handleEvent.bind(this)) window.removeEventListener('message', this.handleEvent.bind(this))
} }
@ -53,7 +73,7 @@ export abstract class IframeCommunicator<SEND, RECEIVE> {
* Enables the message communication. * Enables the message communication.
* Should be called as soon as the other sides is ready to receive messages. * Should be called as soon as the other sides is ready to receive messages.
*/ */
protected enableCommunication(): void { public enableCommunication(): void {
this.communicationEnabled = true this.communicationEnabled = true
} }
@ -62,7 +82,7 @@ export abstract class IframeCommunicator<SEND, RECEIVE> {
* *
* @param message The message to send. * @param message The message to send.
*/ */
protected sendMessageToOtherSide(message: SEND): void { public sendMessageToOtherSide(message: Extract<MESSAGES, PostMessage<SEND_TYPE>>): void {
if (this.messageTarget === undefined || this.targetOrigin === undefined) { if (this.messageTarget === undefined || this.targetOrigin === undefined) {
throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`) throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`)
} }
@ -71,8 +91,42 @@ export abstract class IframeCommunicator<SEND, RECEIVE> {
`Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}` `Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}`
) )
} }
console.debug('[WPMC ' + this.generateLogIdentifier() + '] Sent event', message)
this.messageTarget.postMessage(message, this.targetOrigin) this.messageTarget.postMessage(message, this.targetOrigin)
} }
protected abstract handleEvent(event: MessageEvent<RECEIVE>): void /**
* Sets the handler method that processes messages with the given message type.
* If there is already a handler for the given message type then the handler will be overwritten.
*
* @param messageType The message type for which the handler should be called
* @param handler The handler that processes messages with the given message type.
*/
public setHandler<R extends RECEIVE_TYPE>(messageType: R, handler: Handler<MESSAGES, R>): void {
this.handlers[messageType] = handler as Handler<MESSAGES, RECEIVE_TYPE>
}
/**
* Generates a unique identifier that helps to separate log messages in the console from different communicators.
* @return the identifier
*/
protected abstract generateLogIdentifier(): string
/**
* Receives the message events and calls the handler that is mapped to the correct type.
*
* @param event The received event
* @return {@code true} if the event was processed.
*/
protected handleEvent(event: MessageEvent<PostMessage<RECEIVE_TYPE>>): boolean | undefined {
const data = event.data
const handler = this.handlers[data.type]
if (!handler) {
return true
}
console.debug('[WPMC ' + this.generateLogIdentifier() + '] Received event ', data)
handler(data as Extract<MESSAGES, PostMessage<RECEIVE_TYPE>>)
return false
}
} }