mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-09 13:51:57 -04:00
refactor: move render-iframe to commons
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
18206c0615
commit
aef0211092
13 changed files with 15 additions and 15 deletions
|
@ -6,7 +6,7 @@
|
|||
import * as UseBaseUrlModule from '../../../hooks/common/use-base-url'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
import * as RenderIframeModule from '../../editor-page/renderer-pane/render-iframe'
|
||||
import * as RenderIframeModule from '../../common/render-iframe/render-iframe'
|
||||
import type { CommonModalProps } from '../modals/common-modal'
|
||||
import * as CommonModalModule from '../modals/common-modal'
|
||||
import * as fetchMotdModule from './fetch-motd'
|
||||
|
@ -17,7 +17,7 @@ import React from 'react'
|
|||
|
||||
jest.mock('./fetch-motd')
|
||||
jest.mock('../modals/common-modal')
|
||||
jest.mock('../../editor-page/renderer-pane/render-iframe')
|
||||
jest.mock('../../common/render-iframe/render-iframe')
|
||||
jest.mock('../../../hooks/common/use-base-url')
|
||||
|
||||
describe('motd modal', () => {
|
||||
|
|
|
@ -7,9 +7,9 @@ import { cypressId } from '../../../utils/cypress-attribute'
|
|||
import { Logger } from '../../../utils/logger'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
import { EditorToRendererCommunicatorContextProvider } from '../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import { RenderIframe } from '../../editor-page/renderer-pane/render-iframe'
|
||||
import { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { CommonModal } from '../modals/common-modal'
|
||||
import { RenderIframe } from '../render-iframe/render-iframe'
|
||||
import { fetchMotd, MOTD_LOCAL_STORAGE_KEY } from './fetch-motd'
|
||||
import React, { useCallback, useMemo, useEffect, useState } from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Execute the given reload callback if the given render type changes.
|
||||
*
|
||||
* @param rendererType The render type to watch
|
||||
* @param effectCallback The callback that should be executed if the render type changes.
|
||||
*/
|
||||
export const useEffectOnRenderTypeChange = (rendererType: RendererType, effectCallback: () => void): void => {
|
||||
const lastRendererType = useRef<RendererType>(rendererType)
|
||||
|
||||
useEffect(() => {
|
||||
if (lastRendererType.current === rendererType) {
|
||||
return
|
||||
}
|
||||
effectCallback()
|
||||
lastRendererType.current = rendererType
|
||||
}, [effectCallback, rendererType])
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ORIGIN, useBaseUrl } from '../../../../hooks/common/use-base-url'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { useEditorToRendererCommunicator } from '../../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import type { RefObject } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
const log = new Logger('IframeLoader')
|
||||
|
||||
/**
|
||||
* Generates a callback for an iframe load handler, that enforces a given URL if frame navigates away.
|
||||
*
|
||||
* @param iFrameReference A reference to the iframe react dom element.
|
||||
* @param onNavigateAway An optional callback that is executed when the iframe leaves the enforced URL.
|
||||
*/
|
||||
export const useForceRenderPageUrlOnIframeLoadCallback = (
|
||||
iFrameReference: RefObject<HTMLIFrameElement>,
|
||||
onNavigateAway: () => void
|
||||
): (() => void) => {
|
||||
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||
const rendererBaseUrl = useBaseUrl(ORIGIN.RENDERER)
|
||||
const forcedUrl = useMemo(() => {
|
||||
const renderUrl = new URL(rendererBaseUrl)
|
||||
renderUrl.pathname += 'render'
|
||||
renderUrl.searchParams.set('uuid', iframeCommunicator.getUuid())
|
||||
return renderUrl.toString()
|
||||
}, [iframeCommunicator, rendererBaseUrl])
|
||||
const redirectionInProgress = useRef<boolean>(false)
|
||||
|
||||
const loadCallback = useCallback(() => {
|
||||
const frame = iFrameReference.current
|
||||
|
||||
if (!frame) {
|
||||
log.debug('No frame in reference')
|
||||
return
|
||||
}
|
||||
|
||||
if (redirectionInProgress.current) {
|
||||
redirectionInProgress.current = false
|
||||
log.debug('Redirect complete')
|
||||
} else {
|
||||
log.warn(`Navigated away from unknown URL. Forcing back to ${forcedUrl}`)
|
||||
onNavigateAway?.()
|
||||
redirectionInProgress.current = true
|
||||
frame.src = forcedUrl
|
||||
}
|
||||
}, [iFrameReference, onNavigateAway, forcedUrl])
|
||||
|
||||
useEffect(() => {
|
||||
loadCallback()
|
||||
}, [loadCallback])
|
||||
|
||||
return loadCallback
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { DarkModePreference } from '../../../../redux/dark-mode/types'
|
||||
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Sends additional configuration options (dark mode, line break, etc.) to the renderer.
|
||||
*
|
||||
* @param rendererReady Defines if the target renderer is ready
|
||||
* @param forcedDarkMode Overwrites the value from the global application states if set.
|
||||
*/
|
||||
export const useSendAdditionalConfigurationToRenderer = (
|
||||
rendererReady: boolean,
|
||||
forcedDarkMode: DarkModePreference = DarkModePreference.AUTO
|
||||
): void => {
|
||||
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails.frontmatter.newlinesAreBreaks)
|
||||
|
||||
const darkMode = useMemo(() => {
|
||||
return forcedDarkMode === DarkModePreference.AUTO ? darkModePreference : forcedDarkMode
|
||||
}, [darkModePreference, forcedDarkMode])
|
||||
|
||||
useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_ADDITIONAL_CONFIGURATION,
|
||||
darkModePreference: darkMode,
|
||||
newLinesAreBreaks: newlinesAreBreaks
|
||||
}),
|
||||
[darkMode, newlinesAreBreaks]
|
||||
),
|
||||
rendererReady
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 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 { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Sends the given markdown content to the renderer.
|
||||
*
|
||||
* @param markdownContentLines The markdown content to send.
|
||||
* @param rendererReady Defines if the target renderer is ready
|
||||
*/
|
||||
export const useSendMarkdownToRenderer = (markdownContentLines: string[], rendererReady: boolean): void => {
|
||||
return useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_MARKDOWN_CONTENT,
|
||||
content: markdownContentLines
|
||||
}),
|
||||
[markdownContentLines]
|
||||
),
|
||||
rendererReady
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import equal from 'fast-deep-equal'
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Sends the given {@link ScrollState scroll state} to the renderer if the content changed.
|
||||
*
|
||||
* @param scrollState The scroll state to send
|
||||
* @param rendererReady Defines if the target renderer is ready
|
||||
*/
|
||||
export const useSendScrollState = (scrollState: ScrollState | undefined, rendererReady: boolean): void => {
|
||||
const oldScrollState = useRef<ScrollState | undefined>(undefined)
|
||||
|
||||
useSendToRenderer(
|
||||
useMemo(() => {
|
||||
if (!scrollState || equal(scrollState, oldScrollState.current)) {
|
||||
return
|
||||
}
|
||||
oldScrollState.current = scrollState
|
||||
return { type: CommunicationMessageType.SET_SCROLL_STATE, scrollState }
|
||||
}, [scrollState]),
|
||||
rendererReady
|
||||
)
|
||||
}
|
178
frontend/src/components/common/render-iframe/render-iframe.tsx
Normal file
178
frontend/src/components/common/render-iframe/render-iframe.tsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { DarkModePreference } from '../../../redux/dark-mode/types'
|
||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import { isTestMode } from '../../../utils/test-modes'
|
||||
import { useEditorToRendererCommunicator } from '../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import type { ScrollProps } from '../../editor-page/synced-scroll/scroll-props'
|
||||
import { useExtensionEventEmitter } from '../../markdown-renderer/hooks/use-extension-event-emitter'
|
||||
import type { CommonMarkdownRendererProps } from '../../render-page/renderers/common-markdown-renderer-props'
|
||||
import { useEditorReceiveHandler } from '../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
|
||||
import type {
|
||||
ExtensionEvent,
|
||||
OnHeightChangeMessage,
|
||||
SetScrollStateMessage
|
||||
} from '../../render-page/window-post-message-communicator/rendering-message'
|
||||
import type { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { CommunicationMessageType } from '../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { ShowIf } from '../show-if/show-if'
|
||||
import { WaitSpinner } from '../wait-spinner/wait-spinner'
|
||||
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
||||
import { useForceRenderPageUrlOnIframeLoadCallback } from './hooks/use-force-render-page-url-on-iframe-load-callback'
|
||||
import { useSendAdditionalConfigurationToRenderer } from './hooks/use-send-additional-configuration-to-renderer'
|
||||
import { useSendMarkdownToRenderer } from './hooks/use-send-markdown-to-renderer'
|
||||
import { useSendScrollState } from './hooks/use-send-scroll-state'
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
export interface RenderIframeProps extends Omit<CommonMarkdownRendererProps & ScrollProps, 'baseUrl'> {
|
||||
rendererType: RendererType
|
||||
forcedDarkMode?: DarkModePreference
|
||||
frameClasses?: string
|
||||
onRendererStatusChange?: undefined | ((rendererReady: boolean) => void)
|
||||
adaptFrameHeightToContent?: boolean
|
||||
}
|
||||
|
||||
const log = new Logger('RenderIframe')
|
||||
|
||||
/**
|
||||
* Renders the iframe for the HTML-rendering of the markdown content.
|
||||
* The iframe is enhanced by the {@link useEditorToRendererCommunicator iframe communicator} which is used for
|
||||
* passing data from the parent frame into the iframe as well as receiving status messages and data from the iframe.
|
||||
*
|
||||
* @param markdownContentLines Array of lines of the markdown content
|
||||
* @param onTaskCheckedChange Callback that is fired when a task-list item in the iframe is checked
|
||||
* @param scrollState The current {@link ScrollState}
|
||||
* @param onScroll Callback that is fired when the user scrolls in the iframe
|
||||
* @param onMakeScrollSource Callback that is fired when the renderer requests to be set as the current scroll source
|
||||
* @param frameClasses CSS classes that should be applied to the iframe
|
||||
* @param rendererType The {@link RendererType type} of the renderer to use.
|
||||
* @param forcedDarkMode If set, the dark mode will be set to the given value. Otherwise, the dark mode won't be changed.
|
||||
* @param adaptFrameHeightToContent If set, the iframe height will be adjusted to the content height
|
||||
* @param onRendererStatusChange Callback that is fired when the renderer in the iframe is ready
|
||||
*/
|
||||
export const RenderIframe: React.FC<RenderIframeProps> = ({
|
||||
markdownContentLines,
|
||||
scrollState,
|
||||
onScroll,
|
||||
onMakeScrollSource,
|
||||
frameClasses,
|
||||
rendererType,
|
||||
forcedDarkMode,
|
||||
adaptFrameHeightToContent,
|
||||
onRendererStatusChange
|
||||
}) => {
|
||||
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
||||
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||
const resetRendererReady = useCallback(() => {
|
||||
log.debug('Reset render status')
|
||||
setRendererReady(false)
|
||||
}, [])
|
||||
const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, resetRendererReady)
|
||||
const [frameHeight, setFrameHeight] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
onRendererStatusChange?.(rendererReady)
|
||||
}, [onRendererStatusChange, rendererReady])
|
||||
|
||||
useEffect(() => () => setRendererReady(false), [iframeCommunicator])
|
||||
|
||||
useEffect(() => {
|
||||
if (!rendererReady) {
|
||||
iframeCommunicator.unsetMessageTarget()
|
||||
}
|
||||
}, [iframeCommunicator, rendererReady])
|
||||
|
||||
useEffect(() => {
|
||||
onRendererStatusChange?.(rendererReady)
|
||||
}, [onRendererStatusChange, rendererReady])
|
||||
|
||||
const eventEmitter = useExtensionEventEmitter()
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.EXTENSION_EVENT,
|
||||
useMemo(() => {
|
||||
return eventEmitter === undefined
|
||||
? undefined
|
||||
: (values: ExtensionEvent) => eventEmitter.emit(values.eventName, values.payload)
|
||||
}, [eventEmitter])
|
||||
)
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.ON_HEIGHT_CHANGE,
|
||||
useCallback(
|
||||
(values: OnHeightChangeMessage) => {
|
||||
if (adaptFrameHeightToContent) {
|
||||
setFrameHeight?.(values.height)
|
||||
}
|
||||
},
|
||||
[adaptFrameHeightToContent]
|
||||
)
|
||||
)
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.RENDERER_READY,
|
||||
useCallback(() => {
|
||||
const frame = frameReference.current
|
||||
if (!frame) {
|
||||
log.error('Load triggered without frame in ref')
|
||||
return
|
||||
}
|
||||
const otherWindow = frame.contentWindow
|
||||
if (!otherWindow) {
|
||||
log.error('Load triggered without content window')
|
||||
return
|
||||
}
|
||||
iframeCommunicator.setMessageTarget(otherWindow)
|
||||
iframeCommunicator.enableCommunication()
|
||||
iframeCommunicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.SET_BASE_CONFIGURATION,
|
||||
baseConfiguration: {
|
||||
baseUrl: window.location.toString(),
|
||||
rendererType
|
||||
}
|
||||
})
|
||||
setRendererReady(true)
|
||||
}, [iframeCommunicator, rendererType])
|
||||
)
|
||||
|
||||
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
|
||||
useSendAdditionalConfigurationToRenderer(rendererReady, forcedDarkMode)
|
||||
useSendMarkdownToRenderer(markdownContentLines, rendererReady)
|
||||
|
||||
useSendScrollState(scrollState, rendererReady)
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.SET_SCROLL_STATE,
|
||||
useCallback((values: SetScrollStateMessage) => onScroll?.(values.scrollState), [onScroll])
|
||||
)
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.ENABLE_RENDERER_SCROLL_SOURCE,
|
||||
useCallback(() => onMakeScrollSource?.(), [onMakeScrollSource])
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ShowIf condition={!rendererReady}>
|
||||
<WaitSpinner />
|
||||
</ShowIf>
|
||||
<iframe
|
||||
style={{ height: `${frameHeight}px` }}
|
||||
{...cypressId('documentIframe')}
|
||||
onLoad={onIframeLoad}
|
||||
title='render'
|
||||
{...(isTestMode ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' })}
|
||||
allowFullScreen={true}
|
||||
ref={frameReference}
|
||||
referrerPolicy={'no-referrer'}
|
||||
className={`border-0 ${frameClasses ?? ''}`}
|
||||
allow={'clipboard-write'}
|
||||
{...cypressAttribute('renderer-ready', rendererReady ? 'true' : 'false')}
|
||||
{...cypressAttribute('renderer-type', rendererType)}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue