diff --git a/src/components/document-read-only-page/document-read-only-page.tsx b/src/components/document-read-only-page/document-read-only-page.tsx index 07327bf5b..f3f3afc71 100644 --- a/src/components/document-read-only-page/document-read-only-page.tsx +++ b/src/components/document-read-only-page/document-read-only-page.tsx @@ -19,10 +19,11 @@ import { RenderIframe } from '../editor-page/renderer-pane/render-iframe' import { DocumentInfobar } from './document-infobar' import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert' 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 { 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 { 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 = () => { useTranslation() @@ -35,9 +36,10 @@ export const DocumentReadOnlyPage: React.FC = () => { const [error, loading] = useLoadNoteFromServer() const markdownContent = useNoteMarkdownContentWithoutFrontmatter() const noteDetails = useApplicationState((state) => state.noteDetails) + useSendFrontmatterInfoFromReduxToRenderer() return ( - + @@ -63,7 +65,7 @@ export const DocumentReadOnlyPage: React.FC = () => { /> - + ) } diff --git a/src/components/editor-page/document-bar/document-info/document-info-line-word-count.tsx b/src/components/editor-page/document-bar/document-info/document-info-line-word-count.tsx index e8338465f..ff3ce93e4 100644 --- a/src/components/editor-page/document-bar/document-info/document-info-line-word-count.tsx +++ b/src/components/editor-page/document-bar/document-info/document-info-line-word-count.tsx @@ -4,13 +4,18 @@ * 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 { ShowIf } from '../../../common/show-if/show-if' import { DocumentInfoLine } from './document-info-line' import { UnitalicBoldText } from './unitalic-bold-text' -import { useIFrameEditorToRendererCommunicator } from '../../render-context/iframe-editor-to-renderer-communicator-context-provider' -import { useApplicationState } from '../../../../hooks/common/use-application-state' +import { useEditorToRendererCommunicator } from '../../render-context/editor-to-renderer-communicator-context-provider' +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 @@ -18,24 +23,19 @@ import { useApplicationState } from '../../../../hooks/common/use-application-st */ export const DocumentInfoLineWordCount: React.FC = () => { useTranslation() - const iframeEditorToRendererCommunicator = useIFrameEditorToRendererCommunicator() + const editorToRendererCommunicator = useEditorToRendererCommunicator() const [wordCount, setWordCount] = useState(null) - const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) - useEffect(() => { - iframeEditorToRendererCommunicator.onWordCountCalculated((words) => { - setWordCount(words) - }) - return () => { - iframeEditorToRendererCommunicator.onWordCountCalculated(undefined) - } - }, [iframeEditorToRendererCommunicator, setWordCount]) + useEditorReceiveHandler( + CommunicationMessageType.ON_WORD_COUNT_CALCULATED, + useCallback((values: OnWordCountCalculatedMessage) => setWordCount(values.words), [setWordCount]) + ) - useEffect(() => { - if (rendererReady) { - iframeEditorToRendererCommunicator.sendGetWordCount() - } - }, [iframeEditorToRendererCommunicator, rendererReady]) + useEffectOnRendererReady( + useCallback(() => { + editorToRendererCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.GET_WORD_COUNT }) + }, [editorToRendererCommunicator]) + ) return ( diff --git a/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx b/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx index 4cc4799f5..dbcd78cbe 100644 --- a/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx +++ b/src/components/editor-page/editor-document-renderer/editor-document-renderer.tsx @@ -7,6 +7,7 @@ import React from 'react' import { RenderIframe, RenderIframeProps } from '../renderer-pane/render-iframe' 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 @@ -17,5 +18,8 @@ export type EditorDocumentRendererProps = Omit = (props) => { const markdownContent = useNoteMarkdownContentWithoutFrontmatter() + + useSendFrontmatterInfoFromReduxToRenderer() + return } diff --git a/src/components/editor-page/editor-page.tsx b/src/components/editor-page/editor-page.tsx index 6fb26455b..cd8e75dcb 100644 --- a/src/components/editor-page/editor-page.tsx +++ b/src/components/editor-page/editor-page.tsx @@ -21,14 +21,14 @@ import { useViewModeShortcuts } from './hooks/useViewModeShortcuts' import { Sidebar } from './sidebar/sidebar' import { Splitter } from './splitter/splitter' 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 { UiNotifications } from '../notifications/ui-notifications' import { useNotificationTest } from './use-notification-test' -import { IframeEditorToRendererCommunicatorContextProvider } from './render-context/iframe-editor-to-renderer-communicator-context-provider' import { useUpdateLocalHistoryEntry } from './hooks/useUpdateLocalHistoryEntry' import { useApplicationState } from '../../hooks/common/use-application-state' import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer' +import { EditorToRendererCommunicatorContextProvider } from './render-context/editor-to-renderer-communicator-context-provider' export interface EditorPagePathParams { id: string @@ -53,7 +53,11 @@ export const EditorPage: React.FC = () => { const onMarkdownRendererScroll = useCallback( (newScrollState: ScrollState) => { 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] @@ -62,7 +66,11 @@ export const EditorPage: React.FC = () => { const onEditorScroll = useCallback( (newScrollState: ScrollState) => { 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] @@ -79,10 +87,12 @@ export const EditorPage: React.FC = () => { const setRendererToScrollSource = useCallback(() => { scrollSource.current = ScrollSource.RENDERER + console.debug('[EditorPage] Make renderer scroll source') }, []) const setEditorToScrollSource = useCallback(() => { scrollSource.current = ScrollSource.EDITOR + console.debug('[EditorPage] Make editor scroll source') }, []) useNotificationTest() @@ -114,7 +124,7 @@ export const EditorPage: React.FC = () => { ) return ( - + @@ -136,7 +146,7 @@ export const EditorPage: React.FC = () => { - + ) } diff --git a/src/components/editor-page/render-context/editor-to-renderer-communicator-context-provider.tsx b/src/components/editor-page/render-context/editor-to-renderer-communicator-context-provider.tsx new file mode 100644 index 000000000..034cf6b93 --- /dev/null +++ b/src/components/editor-page/render-context/editor-to-renderer-communicator-context-provider.tsx @@ -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(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(() => new EditorToRendererCommunicator(), []) + + return ( + + {children} + + ) +} diff --git a/src/components/editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider.tsx b/src/components/editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider.tsx deleted file mode 100644 index ab6255363..000000000 --- a/src/components/editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider.tsx +++ /dev/null @@ -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( - 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( - () => new IframeEditorToRendererCommunicator(), - [] - ) - - return ( - - {children} - - ) -} diff --git a/src/components/editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider.tsx b/src/components/editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider.tsx deleted file mode 100644 index 2d03fb544..000000000 --- a/src/components/editor-page/render-context/iframe-renderer-to-editor-communicator-context-provider.tsx +++ /dev/null @@ -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( - 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(() => { - const newCommunicator = new IframeRendererToEditorCommunicator() - newCommunicator.setMessageTarget(window.parent, editorOrigin) - return newCommunicator - }, [editorOrigin]) - - useEffect(() => { - const currentIFrame = currentIFrameCommunicator - currentIFrame?.sendRendererReady() - return () => currentIFrame?.unregisterEventListener() - }, [currentIFrameCommunicator]) - - return ( - - {children} - - ) -} diff --git a/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx b/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx new file mode 100644 index 000000000..7199e0a45 --- /dev/null +++ b/src/components/editor-page/render-context/renderer-to-editor-communicator-context-provider.tsx @@ -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(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(() => { + 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 ( + + {children} + + ) +} diff --git a/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx b/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx new file mode 100644 index 000000000..d6e79fd30 --- /dev/null +++ b/src/components/editor-page/renderer-pane/communicator-image-lightbox.tsx @@ -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(undefined) + const [show, setShow] = useState(false) + + useEditorReceiveHandler( + CommunicationMessageType.IMAGE_CLICKED, + useCallback( + (values: ImageClickedMessage) => { + setLightboxDetails?.(values.details) + setShow(true) + }, + [setLightboxDetails] + ) + ) + + const hideLightbox = useCallback(() => { + setShow(false) + }, []) + + return ( + + ) +} diff --git a/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts b/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts index b70af9603..c3b8a64ca 100644 --- a/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts +++ b/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts @@ -5,11 +5,11 @@ */ 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 = ( frameReference: RefObject, - iframeCommunicator: IframeEditorToRendererCommunicator, + iframeCommunicator: EditorToRendererCommunicator, rendererOrigin: string, renderPageUrl: string, onNavigateAway: () => void diff --git a/src/components/editor-page/renderer-pane/hooks/use-send-dark-mode-status-to-renderer.ts b/src/components/editor-page/renderer-pane/hooks/use-send-dark-mode-status-to-renderer.ts new file mode 100644 index 000000000..fd9dbd5e6 --- /dev/null +++ b/src/components/editor-page/renderer-pane/hooks/use-send-dark-mode-status-to-renderer.ts @@ -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] + ) + ) +} diff --git a/src/components/editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer.ts b/src/components/editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer.ts new file mode 100644 index 000000000..1d24fc835 --- /dev/null +++ b/src/components/editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer.ts @@ -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] + ) + ) +} diff --git a/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts b/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts new file mode 100644 index 000000000..0f39e78bd --- /dev/null +++ b/src/components/editor-page/renderer-pane/hooks/use-send-markdown-to-renderer.ts @@ -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] + ) + ) +} diff --git a/src/components/editor-page/renderer-pane/hooks/use-send-scroll-state.ts b/src/components/editor-page/renderer-pane/hooks/use-send-scroll-state.ts new file mode 100644 index 000000000..bed102538 --- /dev/null +++ b/src/components/editor-page/renderer-pane/hooks/use-send-scroll-state.ts @@ -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(undefined) + + useEffectOnRendererReady( + useCallback(() => { + if (scrollState && !equal(scrollState, oldScrollState.current)) { + oldScrollState.current = scrollState + iframeCommunicator.sendMessageToOtherSide({ type: CommunicationMessageType.SET_SCROLL_STATE, scrollState }) + } + }, [iframeCommunicator, scrollState]) + ) +} diff --git a/src/components/editor-page/renderer-pane/render-iframe.tsx b/src/components/editor-page/renderer-pane/render-iframe.tsx index 4a30402fd..05cf7850a 100644 --- a/src/components/editor-page/renderer-pane/render-iframe.tsx +++ b/src/components/editor-page/renderer-pane/render-iframe.tsx @@ -3,18 +3,27 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import equal from 'fast-deep-equal' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' 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 { RendererProps } from '../../render-page/markdown-document' -import { ImageDetails, RendererType } from '../../render-page/rendering-message' -import { useIFrameEditorToRendererCommunicator } from '../render-context/iframe-editor-to-renderer-communicator-context-provider' -import { ScrollState } from '../synced-scroll/scroll-props' +import { + CommunicationMessageType, + 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 { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox' +import { CommunicatorImageLightbox } from './communicator-image-lightbox' 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 { rendererType: RendererType @@ -33,16 +42,12 @@ export const RenderIframe: React.FC = ({ rendererType, forcedDarkMode }) => { - const savedDarkMode = useIsDarkModeActivated() - const darkMode = forcedDarkMode ?? savedDarkMode - const [lightboxDetails, setLightboxDetails] = useState(undefined) - const frameReference = useRef(null) - const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo) const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin) const renderPageUrl = `${rendererOrigin}render` const resetRendererReady = useCallback(() => setRendererStatus(false), []) - const iframeCommunicator = useIFrameEditorToRendererCommunicator() + const iframeCommunicator = useEditorToRendererCommunicator() + const rendererReady = useIsRendererReady() const onIframeLoad = useOnIframeLoad( frameReference, iframeCommunicator, @@ -52,8 +57,6 @@ export const RenderIframe: React.FC = ({ ) const [frameHeight, setFrameHeight] = useState(0) - const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) - useEffect( () => () => { iframeCommunicator.unregisterEventListener() @@ -62,76 +65,59 @@ export const RenderIframe: React.FC = ({ [iframeCommunicator] ) - useEffect(() => { - iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange) - return () => iframeCommunicator.onFirstHeadingChange(undefined) - }, [iframeCommunicator, onFirstHeadingChange]) + useEditorReceiveHandler( + CommunicationMessageType.ON_FIRST_HEADING_CHANGE, + useCallback( + (values: OnFirstHeadingChangeMessage) => onFirstHeadingChange?.(values.firstHeading), + [onFirstHeadingChange] + ) + ) - useEffect(() => { - iframeCommunicator.onSetScrollState(onScroll) - return () => iframeCommunicator.onSetScrollState(undefined) - }, [iframeCommunicator, onScroll]) + useEditorReceiveHandler( + CommunicationMessageType.SET_SCROLL_STATE, + useCallback((values: SetScrollStateMessage) => onScroll?.(values.scrollState), [onScroll]) + ) - useEffect(() => { - iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource) - return () => iframeCommunicator.onSetScrollSourceToRenderer(undefined) - }, [iframeCommunicator, onMakeScrollSource]) + useEditorReceiveHandler( + CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER, + useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource]) + ) - useEffect(() => { - iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange) - return () => iframeCommunicator.onTaskCheckboxChange(undefined) - }, [iframeCommunicator, onTaskCheckedChange]) + useEditorReceiveHandler( + CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE, + useCallback( + (values: OnTaskCheckboxChangeMessage) => onTaskCheckedChange?.(values.lineInMarkdown, values.checked), + [onTaskCheckedChange] + ) + ) - useEffect(() => { - iframeCommunicator.onImageClicked(setLightboxDetails) - return () => iframeCommunicator.onImageClicked(undefined) - }, [iframeCommunicator]) + useEditorReceiveHandler( + CommunicationMessageType.ON_HEIGHT_CHANGE, + useCallback((values: OnHeightChangeMessage) => setFrameHeight?.(values.height), [setFrameHeight]) + ) - useEffect(() => { - iframeCommunicator.onHeightChange(setFrameHeight) - return () => iframeCommunicator.onHeightChange(undefined) - }, [iframeCommunicator]) - - useEffect(() => { - iframeCommunicator.onRendererReady(() => { - iframeCommunicator.sendSetBaseConfiguration({ - baseUrl: window.location.toString(), - rendererType + useEditorReceiveHandler( + CommunicationMessageType.RENDERER_READY, + useCallback(() => { + iframeCommunicator.enableCommunication() + iframeCommunicator.sendMessageToOtherSide({ + type: CommunicationMessageType.SET_BASE_CONFIGURATION, + baseConfiguration: { + baseUrl: window.location.toString(), + rendererType + } }) setRendererStatus(true) - }) - return () => iframeCommunicator.onRendererReady(undefined) - }, [iframeCommunicator, rendererType]) + }, [iframeCommunicator, rendererType]) + ) - useEffect(() => { - if (rendererReady) { - iframeCommunicator.sendSetDarkmode(darkMode) - } - }, [darkMode, iframeCommunicator, rendererReady]) - - const oldScrollState = useRef(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]) + useSendScrollState(scrollState) + useSendDarkModeStatusToRenderer(forcedDarkMode) + useSendMarkdownToRenderer(markdownContent) return ( - + = ({ details }) => { - const [show, setShow] = useState(false) - - const hideLightbox = useCallback(() => { - setShow(false) - }, []) - - useEffect(() => { - if (details) { - setShow(true) - } - }, [details]) - - return ( - - ) -} diff --git a/src/components/intro-page/intro-page.tsx b/src/components/intro-page/intro-page.tsx index 6b1605147..59e7fcee9 100644 --- a/src/components/intro-page/intro-page.tsx +++ b/src/components/intro-page/intro-page.tsx @@ -17,17 +17,17 @@ import { CoverButtons } from './cover-buttons/cover-buttons' import { FeatureLinks } from './feature-links' import { useIntroPageContent } from './hooks/use-intro-page-content' 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 { IframeEditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/iframe-editor-to-renderer-communicator-context-provider' 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 = () => { const introPageContent = useIntroPageContent() const rendererReady = useApplicationState((state) => state.rendererStatus.rendererReady) return ( - + @@ -53,6 +53,6 @@ export const IntroPage: React.FC = () => { - + ) } diff --git a/src/components/render-page/hooks/use-image-click-handler.ts b/src/components/render-page/hooks/use-image-click-handler.ts index fdc072dac..718375726 100644 --- a/src/components/render-page/hooks/use-image-click-handler.ts +++ b/src/components/render-page/hooks/use-image-click-handler.ts @@ -6,19 +6,23 @@ import React, { useCallback } from 'react' 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( (event: React.MouseEvent) => { const image = event.target as HTMLImageElement if (image.src === '') { return } - iframeCommunicator.sendClickedImageUrl({ - src: image.src, - alt: image.alt, - title: image.title + iframeCommunicator.sendMessageToOtherSide({ + type: CommunicationMessageType.IMAGE_CLICKED, + details: { + src: image.src, + alt: image.alt, + title: image.title + } }) }, [iframeCommunicator] diff --git a/src/components/render-page/iframe-editor-to-renderer-communicator.ts b/src/components/render-page/iframe-editor-to-renderer-communicator.ts deleted file mode 100644 index e376e2864..000000000 --- a/src/components/render-page/iframe-editor-to-renderer-communicator.ts +++ /dev/null @@ -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): 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 - } - } -} diff --git a/src/components/render-page/iframe-markdown-renderer.tsx b/src/components/render-page/iframe-markdown-renderer.tsx index b68ef026a..450f31b30 100644 --- a/src/components/render-page/iframe-markdown-renderer.tsx +++ b/src/components/render-page/iframe-markdown-renderer.tsx @@ -4,16 +4,21 @@ * 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 { BaseConfiguration, RendererType } from './rendering-message' +import { + BaseConfiguration, + CommunicationMessageType, + RendererType +} from './window-post-message-communicator/rendering-message' import { setDarkMode } from '../../redux/dark-mode/methods' import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer' import { useImageClickHandler } from './hooks/use-image-click-handler' 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 { 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 = () => { const [markdownContent, setMarkdownContent] = useState('') @@ -25,60 +30,76 @@ export const IframeMarkdownRenderer: React.FC = () => { deprecatedSyntax: false }) - const iframeCommunicator = useIFrameRendererToEditorCommunicator() + const communicator = useRendererToEditorCommunicator() const countWordsInRenderedDocument = useCallback(() => { const documentContainer = document.querySelector('.markdown-body') - if (!documentContainer) { - iframeCommunicator.sendWordCountCalculated(0) - return - } - const wordCount = countWords(documentContainer) - iframeCommunicator.sendWordCountCalculated(wordCount) - }, [iframeCommunicator]) + communicator.sendMessageToOtherSide({ + type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED, + words: documentContainer ? countWords(documentContainer) : 0 + }) + }, [communicator]) - useEffect(() => iframeCommunicator.onSetBaseConfiguration(setBaseConfiguration), [iframeCommunicator]) - useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator]) - 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_BASE_CONFIGURATION, (values) => + setBaseConfiguration(values.baseConfiguration) ) + 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( (lineInMarkdown: number, checked: boolean) => { - iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked) + communicator.sendMessageToOtherSide({ + type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE, + checked, + lineInMarkdown + }) }, - [iframeCommunicator] + [communicator] ) const onFirstHeadingChange = useCallback( (firstHeading?: string) => { - iframeCommunicator.sendFirstHeadingChanged(firstHeading) + communicator.sendMessageToOtherSide({ + type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE, + firstHeading + }) }, - [iframeCommunicator] + [communicator] ) const onMakeScrollSource = useCallback(() => { - iframeCommunicator.sendSetScrollSourceToRenderer() - }, [iframeCommunicator]) + communicator.sendMessageToOtherSide({ + type: CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER + }) + }, [communicator]) const onScroll = useCallback( (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( (height: number) => { - iframeCommunicator.sendHeightChange(height) + communicator.sendMessageToOtherSide({ + type: CommunicationMessageType.ON_HEIGHT_CHANGE, + height + }) }, - [iframeCommunicator] + [communicator] ) if (!baseConfiguration) { diff --git a/src/components/render-page/iframe-renderer-to-editor-communicator.ts b/src/components/render-page/iframe-renderer-to-editor-communicator.ts deleted file mode 100644 index c874fd677..000000000 --- a/src/components/render-page/iframe-renderer-to-editor-communicator.ts +++ /dev/null @@ -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): 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 - } - } -} diff --git a/src/components/render-page/render-page.tsx b/src/components/render-page/render-page.tsx index 8d7fd8c3c..a99b76297 100644 --- a/src/components/render-page/render-page.tsx +++ b/src/components/render-page/render-page.tsx @@ -6,15 +6,15 @@ import React from 'react' import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode' 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 = () => { useApplyDarkMode() return ( - + - + ) } diff --git a/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts b/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts new file mode 100644 index 000000000..3910f23a6 --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts @@ -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' + } +} diff --git a/src/components/render-page/window-post-message-communicator/hooks/use-editor-receive-handler.ts b/src/components/render-page/window-post-message-communicator/hooks/use-editor-receive-handler.ts new file mode 100644 index 000000000..fafe952e5 --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/hooks/use-editor-receive-handler.ts @@ -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 = ( + messageType: R, + handler: Handler +): void => { + const editorToRendererCommunicator = useEditorToRendererCommunicator() + useEffect(() => { + editorToRendererCommunicator.setHandler(messageType, handler) + return () => { + editorToRendererCommunicator.setHandler(messageType, undefined) + } + }, [editorToRendererCommunicator, handler, messageType]) +} diff --git a/src/components/render-page/window-post-message-communicator/hooks/use-effect-on-renderer-ready.ts b/src/components/render-page/window-post-message-communicator/hooks/use-effect-on-renderer-ready.ts new file mode 100644 index 000000000..98739db7e --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/hooks/use-effect-on-renderer-ready.ts @@ -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]) +} diff --git a/src/components/render-page/window-post-message-communicator/hooks/use-is-renderer-ready.ts b/src/components/render-page/window-post-message-communicator/hooks/use-is-renderer-ready.ts new file mode 100644 index 000000000..7b9510f29 --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/hooks/use-is-renderer-ready.ts @@ -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) diff --git a/src/components/render-page/window-post-message-communicator/hooks/use-renderer-receive-handler.ts b/src/components/render-page/window-post-message-communicator/hooks/use-renderer-receive-handler.ts new file mode 100644 index 000000000..3a813c264 --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/hooks/use-renderer-receive-handler.ts @@ -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 = ( + messageType: MESSAGE_TYPE, + handler: Handler +): void => { + const editorToRendererCommunicator = useRendererToEditorCommunicator() + useEffect(() => { + editorToRendererCommunicator.setHandler(messageType, handler) + return () => { + editorToRendererCommunicator.setHandler(messageType, undefined) + } + }, [editorToRendererCommunicator, handler, messageType]) +} diff --git a/src/components/render-page/window-post-message-communicator/hooks/use-send-to-renderer.ts b/src/components/render-page/window-post-message-communicator/hooks/use-send-to-renderer.ts new file mode 100644 index 000000000..c6f4bbf2d --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/hooks/use-send-to-renderer.ts @@ -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> +): void => { + const iframeCommunicator = useEditorToRendererCommunicator() + + useEffectOnRendererReady( + useCallback(() => { + if (message) { + iframeCommunicator.sendMessageToOtherSide(message) + } + }, [iframeCommunicator, message]) + ) +} diff --git a/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts b/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts new file mode 100644 index 000000000..287fcb31e --- /dev/null +++ b/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts @@ -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' + } +} diff --git a/src/components/render-page/rendering-message.ts b/src/components/render-page/window-post-message-communicator/rendering-message.ts similarity index 54% rename from src/components/render-page/rendering-message.ts rename to src/components/render-page/window-post-message-communicator/rendering-message.ts index 34270bd2b..3b6e156ab 100644 --- a/src/components/render-page/rendering-message.ts +++ b/src/components/render-page/window-post-message-communicator/rendering-message.ts @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { ScrollState } from '../editor-page/synced-scroll/scroll-props' -import { RendererFrontmatterInfo } from '../common/note-frontmatter/types' +import { ScrollState } from '../../editor-page/synced-scroll/scroll-props' +import { RendererFrontmatterInfo } from '../../common/note-frontmatter/types' -export enum RenderIframeMessageType { +export enum CommunicationMessageType { SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT', RENDERER_READY = 'RENDERER_READY', SET_DARKMODE = 'SET_DARKMODE', @@ -22,12 +22,12 @@ export enum RenderIframeMessageType { SET_FRONTMATTER_INFO = 'SET_FRONTMATTER_INFO' } -export interface RendererToEditorSimpleMessage { - type: RenderIframeMessageType.RENDERER_READY | RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER +export interface NoPayloadMessage { + type: CommunicationMessageType.RENDERER_READY | CommunicationMessageType.SET_SCROLL_SOURCE_TO_RENDERER } export interface SetDarkModeMessage { - type: RenderIframeMessageType.SET_DARKMODE + type: CommunicationMessageType.SET_DARKMODE activated: boolean } @@ -38,72 +38,87 @@ export interface ImageDetails { } export interface SetBaseUrlMessage { - type: RenderIframeMessageType.SET_BASE_CONFIGURATION + type: CommunicationMessageType.SET_BASE_CONFIGURATION baseConfiguration: BaseConfiguration } export interface GetWordCountMessage { - type: RenderIframeMessageType.GET_WORD_COUNT + type: CommunicationMessageType.GET_WORD_COUNT } export interface ImageClickedMessage { - type: RenderIframeMessageType.IMAGE_CLICKED + type: CommunicationMessageType.IMAGE_CLICKED details: ImageDetails } export interface SetMarkdownContentMessage { - type: RenderIframeMessageType.SET_MARKDOWN_CONTENT + type: CommunicationMessageType.SET_MARKDOWN_CONTENT content: string } export interface SetScrollStateMessage { - type: RenderIframeMessageType.SET_SCROLL_STATE + type: CommunicationMessageType.SET_SCROLL_STATE scrollState: ScrollState } export interface OnTaskCheckboxChangeMessage { - type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE + type: CommunicationMessageType.ON_TASK_CHECKBOX_CHANGE lineInMarkdown: number checked: boolean } export interface OnFirstHeadingChangeMessage { - type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE + type: CommunicationMessageType.ON_FIRST_HEADING_CHANGE firstHeading: string | undefined } export interface SetFrontmatterInfoMessage { - type: RenderIframeMessageType.SET_FRONTMATTER_INFO + type: CommunicationMessageType.SET_FRONTMATTER_INFO frontmatterInfo: RendererFrontmatterInfo } export interface OnHeightChangeMessage { - type: RenderIframeMessageType.ON_HEIGHT_CHANGE + type: CommunicationMessageType.ON_HEIGHT_CHANGE height: number } export interface OnWordCountCalculatedMessage { - type: RenderIframeMessageType.ON_WORD_COUNT_CALCULATED + type: CommunicationMessageType.ON_WORD_COUNT_CALCULATED words: number } -export type EditorToRendererIframeMessage = - | SetMarkdownContentMessage +export type CommunicationMessages = + | NoPayloadMessage | SetDarkModeMessage - | SetScrollStateMessage | SetBaseUrlMessage | GetWordCountMessage - | SetFrontmatterInfoMessage - -export type RendererToEditorIframeMessage = - | RendererToEditorSimpleMessage - | OnFirstHeadingChangeMessage - | OnTaskCheckboxChangeMessage - | SetScrollStateMessage | ImageClickedMessage + | SetMarkdownContentMessage + | SetScrollStateMessage + | OnTaskCheckboxChangeMessage + | OnFirstHeadingChangeMessage + | SetFrontmatterInfoMessage | OnHeightChangeMessage | 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 { DOCUMENT, INTRO, diff --git a/src/components/render-page/iframe-communicator.ts b/src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts similarity index 50% rename from src/components/render-page/iframe-communicator.ts rename to src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts index f2ba00647..98a774267 100644 --- a/src/components/render-page/iframe-communicator.ts +++ b/src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts @@ -9,19 +9,39 @@ */ export class IframeCommunicatorSendingError extends Error {} +export type Handler = + | ((values: Extract>) => void) + | undefined + +export type HandlerMap = Partial<{ + [key in MESSAGE_TYPE]: Handler +}> + +export interface PostMessage { + type: MESSAGE_TYPE +} + /** * Base class for communication between renderer and editor. */ -export abstract class IframeCommunicator { +export abstract class WindowPostMessageCommunicator< + RECEIVE_TYPE extends string, + SEND_TYPE extends string, + MESSAGES extends PostMessage +> { private messageTarget?: Window private targetOrigin?: string private communicationEnabled: boolean + private handlers: HandlerMap = {} constructor() { window.addEventListener('message', this.handleEvent.bind(this)) this.communicationEnabled = false } + /** + * Removes the message event listener from the {@link window} + */ public unregisterEventListener(): void { window.removeEventListener('message', this.handleEvent.bind(this)) } @@ -53,7 +73,7 @@ export abstract class IframeCommunicator { * Enables the message communication. * Should be called as soon as the other sides is ready to receive messages. */ - protected enableCommunication(): void { + public enableCommunication(): void { this.communicationEnabled = true } @@ -62,7 +82,7 @@ export abstract class IframeCommunicator { * * @param message The message to send. */ - protected sendMessageToOtherSide(message: SEND): void { + public sendMessageToOtherSide(message: Extract>): void { if (this.messageTarget === undefined || this.targetOrigin === undefined) { throw new IframeCommunicatorSendingError(`Other side is not set.\nMessage was: ${JSON.stringify(message)}`) } @@ -71,8 +91,42 @@ export abstract class IframeCommunicator { `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) } - protected abstract handleEvent(event: MessageEvent): 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(messageType: R, handler: Handler): void { + this.handlers[messageType] = handler as Handler + } + + /** + * 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>): 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>) + return false + } }